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            @Override
139            public void run() {
140    
141                    // do not run if driver does not support discovery
142    
143                    if (support == null) {
144                            return;
145                    }
146    
147                    running = true;
148    
149                    // wait initial time interval since devices has been initially
150                    // discovered
151    
152                    Object monitor = new Object();
153    
154                    do {
155    
156                            synchronized (monitor) {
157                                    try {
158                                            monitor.wait(support.getScanInterval());
159                                    } catch (InterruptedException e) {
160                                            if (LOG.isTraceEnabled()) {
161                                                    LOG.error("Interrupted", e);
162                                            }
163                                            break;
164                                    } catch (Exception e) {
165                                            throw new RuntimeException("Problem waiting on monitor", e);
166                                    }
167                            }
168    
169                            WebcamDiscoveryListener[] listeners = Webcam.getDiscoveryListeners();
170    
171                            // do nothing when there are no listeners to be notified
172    
173                            if (listeners.length == 0) {
174                                    continue;
175                            }
176    
177                            List<WebcamDevice> tmpnew = driver.getDevices();
178                            List<WebcamDevice> tmpold = null;
179    
180                            try {
181                                    tmpold = getDevices(getWebcams(Long.MAX_VALUE, TimeUnit.MILLISECONDS));
182                            } catch (TimeoutException e) {
183                                    throw new WebcamException(e);
184                            }
185    
186                            // convert to linked list due to O(1) on remove operation on
187                            // iterator versus O(n) for the same operation in array list
188    
189                            List<WebcamDevice> oldones = new LinkedList<WebcamDevice>(tmpold);
190                            List<WebcamDevice> newones = new LinkedList<WebcamDevice>(tmpnew);
191    
192                            Iterator<WebcamDevice> oi = oldones.iterator();
193                            Iterator<WebcamDevice> ni = null;
194    
195                            WebcamDevice od = null; // old device
196                            WebcamDevice nd = null; // new device
197    
198                            // reduce lists
199    
200                            while (oi.hasNext()) {
201    
202                                    od = oi.next();
203                                    ni = newones.iterator();
204    
205                                    while (ni.hasNext()) {
206    
207                                            nd = ni.next();
208    
209                                            // remove both elements, if device name is the same, which
210                                            // actually means that device is exactly the same
211    
212                                            if (nd.getName().equals(od.getName())) {
213                                                    ni.remove();
214                                                    oi.remove();
215                                                    break;
216                                            }
217                                    }
218                            }
219    
220                            // if any left in old ones it means that devices has been removed
221                            if (oldones.size() > 0) {
222    
223                                    List<Webcam> notified = new ArrayList<Webcam>();
224    
225                                    for (WebcamDevice device : oldones) {
226                                            for (Webcam webcam : webcams) {
227                                                    if (webcam.getDevice().getName().equals(device.getName())) {
228                                                            notified.add(webcam);
229                                                            break;
230                                                    }
231                                            }
232                                    }
233    
234                                    setCurrentWebcams(tmpnew);
235    
236                                    for (Webcam webcam : notified) {
237                                            notifyWebcamGone(webcam, listeners);
238                                            webcam.dispose();
239                                    }
240                            }
241    
242                            // if any left in new ones it means that devices has been added
243                            if (newones.size() > 0) {
244    
245                                    setCurrentWebcams(tmpnew);
246    
247                                    for (WebcamDevice device : newones) {
248                                            for (Webcam webcam : webcams) {
249                                                    if (webcam.getDevice().getName().equals(device.getName())) {
250                                                            notifyWebcamFound(webcam, listeners);
251                                                            break;
252                                                    }
253                                            }
254                                    }
255                            }
256    
257                    } while (running);
258            }
259    
260            private void setCurrentWebcams(List<WebcamDevice> devices) {
261                    webcams = toWebcams(devices);
262                    if (Webcam.isHandleTermSignal()) {
263                            WebcamDeallocator.unstore();
264                            WebcamDeallocator.store(webcams.toArray(new Webcam[webcams.size()]));
265                    }
266            }
267    
268            private static void notifyWebcamGone(Webcam webcam, WebcamDiscoveryListener[] listeners) {
269                    WebcamDiscoveryEvent event = new WebcamDiscoveryEvent(webcam, WebcamDiscoveryEvent.REMOVED);
270                    for (WebcamDiscoveryListener l : listeners) {
271                            try {
272                                    l.webcamGone(event);
273                            } catch (Exception e) {
274                                    LOG.error(String.format("Webcam gone, exception when calling listener %s", l.getClass()), e);
275                            }
276                    }
277            }
278    
279            private static void notifyWebcamFound(Webcam webcam, WebcamDiscoveryListener[] listeners) {
280                    WebcamDiscoveryEvent event = new WebcamDiscoveryEvent(webcam, WebcamDiscoveryEvent.ADDED);
281                    for (WebcamDiscoveryListener l : listeners) {
282                            try {
283                                    l.webcamFound(event);
284                            } catch (Exception e) {
285                                    LOG.error(String.format("Webcam found, exception when calling listener %s", l.getClass()), e);
286                            }
287                    }
288            }
289    
290            /**
291             * Stop discovery service.
292             */
293            public synchronized void stop() {
294    
295                    running = false;
296    
297                    if (runner == null) {
298                            return;
299                    }
300    
301                    try {
302                            runner.join();
303                    } catch (InterruptedException e) {
304                            throw new WebcamException("Joint interrupted");
305                    }
306    
307                    runner = null;
308            }
309    
310            /**
311             * Start discovery service.
312             */
313            public synchronized void start() {
314    
315                    // discovery service has been already started
316    
317                    if (runner != null) {
318                            return;
319                    }
320    
321                    // capture driver does not support discovery - nothing to do
322    
323                    if (support == null) {
324                            return;
325                    }
326    
327                    // start discovery service runner
328    
329                    runner = new Thread(this, "webcam-discovery-service");
330                    runner.setUncaughtExceptionHandler(WebcamExceptionHandler.getInstance());
331                    runner.setDaemon(true);
332                    runner.start();
333            }
334    
335            /**
336             * Is discovery service running?
337             * 
338             * @return True or false
339             */
340            public boolean isRunning() {
341                    return running;
342            }
343    
344            /**
345             * Cleanup.
346             */
347            protected synchronized void shutdown() {
348    
349                    stop();
350    
351                    // dispose all webcams
352    
353                    for (Webcam webcam : webcams) {
354                            webcam.dispose();
355                    }
356    
357                    webcams.clear();
358    
359                    // unassign webcams from deallocator
360    
361                    if (Webcam.isHandleTermSignal()) {
362                            WebcamDeallocator.unstore();
363                    }
364            }
365    }