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