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