001 package com.github.sarxos.webcam;
002
003 import java.util.ArrayList;
004 import java.util.Collections;
005 import java.util.Iterator;
006 import java.util.LinkedList;
007 import java.util.List;
008 import java.util.concurrent.Callable;
009 import java.util.concurrent.ExecutionException;
010 import java.util.concurrent.ExecutorService;
011 import java.util.concurrent.Executors;
012 import java.util.concurrent.Future;
013 import java.util.concurrent.ThreadFactory;
014 import java.util.concurrent.TimeUnit;
015 import java.util.concurrent.TimeoutException;
016
017 import org.slf4j.Logger;
018 import org.slf4j.LoggerFactory;
019
020
021 public class WebcamDiscoveryService implements Runnable {
022
023 private static final Logger LOG = LoggerFactory.getLogger(WebcamDiscoveryService.class);
024
025 private static final class WebcamsDiscovery implements Callable<List<Webcam>>, ThreadFactory {
026
027 private final WebcamDriver driver;
028
029 public WebcamsDiscovery(WebcamDriver driver) {
030 this.driver = driver;
031 }
032
033 @Override
034 public List<Webcam> call() throws Exception {
035 return toWebcams(driver.getDevices());
036 }
037
038 @Override
039 public Thread newThread(Runnable r) {
040 Thread t = new Thread(r, "webcam-discovery-service");
041 t.setDaemon(true);
042 t.setUncaughtExceptionHandler(WebcamExceptionHandler.getInstance());
043 return t;
044 }
045 }
046
047 private final WebcamDriver driver;
048 private final WebcamDiscoverySupport support;
049
050 private volatile List<Webcam> webcams = null;
051
052 private volatile boolean running = false;
053
054 private Thread runner = null;
055
056 protected WebcamDiscoveryService(WebcamDriver driver) {
057
058 if (driver == null) {
059 throw new IllegalArgumentException("Driver cannot be null!");
060 }
061
062 this.driver = driver;
063 this.support = (WebcamDiscoverySupport) (driver instanceof WebcamDiscoverySupport ? driver : null);
064 }
065
066 private static List<Webcam> toWebcams(List<WebcamDevice> devices) {
067 List<Webcam> webcams = new ArrayList<Webcam>();
068 for (WebcamDevice device : devices) {
069 webcams.add(new Webcam(device));
070 }
071 return webcams;
072 }
073
074 /**
075 * Get list of devices used by webcams.
076 *
077 * @return List of webcam devices
078 */
079 private static List<WebcamDevice> getDevices(List<Webcam> webcams) {
080 List<WebcamDevice> devices = new ArrayList<WebcamDevice>();
081 for (Webcam webcam : webcams) {
082 devices.add(webcam.getDevice());
083 }
084 return devices;
085 }
086
087 public synchronized List<Webcam> getWebcams(long timeout, TimeUnit tunit) throws TimeoutException {
088
089 if (timeout < 0) {
090 throw new IllegalArgumentException("Timeout cannot be negative");
091 }
092
093 if (tunit == null) {
094 throw new IllegalArgumentException("Time unit cannot be null!");
095 }
096
097 if (webcams == null) {
098
099 WebcamsDiscovery discovery = new WebcamsDiscovery(driver);
100 ExecutorService executor = Executors.newSingleThreadExecutor(discovery);
101 Future<List<Webcam>> future = executor.submit(discovery);
102
103 executor.shutdown();
104
105 try {
106
107 executor.awaitTermination(timeout, tunit);
108
109 if (future.isDone()) {
110 webcams = future.get();
111 } else {
112 future.cancel(true);
113 }
114
115 } catch (InterruptedException e) {
116 throw new WebcamException(e);
117 } catch (ExecutionException e) {
118 throw new WebcamException(e);
119 }
120
121 if (webcams == null) {
122 throw new TimeoutException(String.format("Webcams discovery timeout (%d ms) has been exceeded", timeout));
123 }
124
125 WebcamDiscoveryListener[] listeners = Webcam.getDiscoveryListeners();
126 for (Webcam webcam : webcams) {
127 notifyWebcamFound(webcam, listeners);
128 }
129
130 if (Webcam.isHandleTermSignal()) {
131 WebcamDeallocator.store(webcams.toArray(new Webcam[webcams.size()]));
132 }
133 }
134
135 return Collections.unmodifiableList(webcams);
136 }
137
138 /**
139 * Scan for newly added or already removed webcams.
140 */
141 public void scan() {
142
143 WebcamDiscoveryListener[] listeners = Webcam.getDiscoveryListeners();
144
145 List<WebcamDevice> tmpnew = driver.getDevices();
146 List<WebcamDevice> tmpold = null;
147
148 try {
149 tmpold = getDevices(getWebcams(Long.MAX_VALUE, TimeUnit.MILLISECONDS));
150 } catch (TimeoutException e) {
151 throw new WebcamException(e);
152 }
153
154 // convert to linked list due to O(1) on remove operation on
155 // iterator versus O(n) for the same operation in array list
156
157 List<WebcamDevice> oldones = new LinkedList<WebcamDevice>(tmpold);
158 List<WebcamDevice> newones = new LinkedList<WebcamDevice>(tmpnew);
159
160 Iterator<WebcamDevice> oi = oldones.iterator();
161 Iterator<WebcamDevice> ni = null;
162
163 WebcamDevice od = null; // old device
164 WebcamDevice nd = null; // new device
165
166 // reduce lists
167
168 while (oi.hasNext()) {
169
170 od = oi.next();
171 ni = newones.iterator();
172
173 while (ni.hasNext()) {
174
175 nd = ni.next();
176
177 // remove both elements, if device name is the same, which
178 // actually means that device is exactly the same
179
180 if (nd.getName().equals(od.getName())) {
181 ni.remove();
182 oi.remove();
183 break;
184 }
185 }
186 }
187
188 // if any left in old ones it means that devices has been removed
189 if (oldones.size() > 0) {
190
191 List<Webcam> notified = new ArrayList<Webcam>();
192
193 for (WebcamDevice device : oldones) {
194 for (Webcam webcam : webcams) {
195 if (webcam.getDevice().getName().equals(device.getName())) {
196 notified.add(webcam);
197 break;
198 }
199 }
200 }
201
202 setCurrentWebcams(tmpnew);
203
204 for (Webcam webcam : notified) {
205 notifyWebcamGone(webcam, listeners);
206 webcam.dispose();
207 }
208 }
209
210 // if any left in new ones it means that devices has been added
211 if (newones.size() > 0) {
212
213 setCurrentWebcams(tmpnew);
214
215 for (WebcamDevice device : newones) {
216 for (Webcam webcam : webcams) {
217 if (webcam.getDevice().getName().equals(device.getName())) {
218 notifyWebcamFound(webcam, listeners);
219 break;
220 }
221 }
222 }
223 }
224 }
225
226 @Override
227 public void run() {
228
229 // do not run if driver does not support discovery
230
231 if (support == null) {
232 return;
233 }
234
235 running = true;
236
237 // wait initial time interval since devices has been initially
238 // discovered
239
240 Object monitor = new Object();
241
242 do {
243
244 synchronized (monitor) {
245 try {
246 monitor.wait(support.getScanInterval());
247 } catch (InterruptedException e) {
248 if (LOG.isTraceEnabled()) {
249 LOG.error("Interrupted", e);
250 }
251 break;
252 } catch (Exception e) {
253 throw new RuntimeException("Problem waiting on monitor", e);
254 }
255 }
256
257 scan();
258
259 } while (running);
260 }
261
262 private void setCurrentWebcams(List<WebcamDevice> devices) {
263 webcams = toWebcams(devices);
264 if (Webcam.isHandleTermSignal()) {
265 WebcamDeallocator.unstore();
266 WebcamDeallocator.store(webcams.toArray(new Webcam[webcams.size()]));
267 }
268 }
269
270 private static void notifyWebcamGone(Webcam webcam, WebcamDiscoveryListener[] listeners) {
271 WebcamDiscoveryEvent event = new WebcamDiscoveryEvent(webcam, WebcamDiscoveryEvent.REMOVED);
272 for (WebcamDiscoveryListener l : listeners) {
273 try {
274 l.webcamGone(event);
275 } catch (Exception e) {
276 LOG.error(String.format("Webcam gone, exception when calling listener %s", l.getClass()), e);
277 }
278 }
279 }
280
281 private static void notifyWebcamFound(Webcam webcam, WebcamDiscoveryListener[] listeners) {
282 WebcamDiscoveryEvent event = new WebcamDiscoveryEvent(webcam, WebcamDiscoveryEvent.ADDED);
283 for (WebcamDiscoveryListener l : listeners) {
284 try {
285 l.webcamFound(event);
286 } catch (Exception e) {
287 LOG.error(String.format("Webcam found, exception when calling listener %s", l.getClass()), e);
288 }
289 }
290 }
291
292 /**
293 * Stop discovery service.
294 */
295 public synchronized void stop() {
296
297 running = false;
298
299 if (runner == null) {
300 return;
301 }
302
303 try {
304 runner.join();
305 } catch (InterruptedException e) {
306 throw new WebcamException("Joint interrupted");
307 }
308
309 runner = null;
310 }
311
312 /**
313 * Start discovery service.
314 */
315 public synchronized void start() {
316
317 // discovery service has been already started
318
319 if (runner != null) {
320 return;
321 }
322
323 // capture driver does not support discovery - nothing to do
324
325 if (support == null) {
326 return;
327 }
328
329 // start discovery service runner
330
331 runner = new Thread(this, "webcam-discovery-service");
332 runner.setUncaughtExceptionHandler(WebcamExceptionHandler.getInstance());
333 runner.setDaemon(true);
334 runner.start();
335 }
336
337 /**
338 * Is discovery service running?
339 *
340 * @return True or false
341 */
342 public boolean isRunning() {
343 return running;
344 }
345
346 /**
347 * Cleanup.
348 */
349 protected synchronized void shutdown() {
350
351 stop();
352
353 // dispose all webcams
354
355 for (Webcam webcam : webcams) {
356 webcam.dispose();
357 }
358
359 webcams.clear();
360
361 // unassign webcams from deallocator
362
363 if (Webcam.isHandleTermSignal()) {
364 WebcamDeallocator.unstore();
365 }
366 }
367 }