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    }