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