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    }