001package com.github.sarxos.webcam;
002
003import java.util.ArrayList;
004import java.util.Collections;
005import java.util.Iterator;
006import java.util.LinkedList;
007import java.util.List;
008import java.util.concurrent.Callable;
009import java.util.concurrent.ExecutionException;
010import java.util.concurrent.ExecutorService;
011import java.util.concurrent.Executors;
012import java.util.concurrent.Future;
013import java.util.concurrent.ThreadFactory;
014import java.util.concurrent.TimeUnit;
015import java.util.concurrent.TimeoutException;
016import java.util.concurrent.atomic.AtomicBoolean;
017
018import org.slf4j.Logger;
019import org.slf4j.LoggerFactory;
020
021
022public 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 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}