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    }