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        private AtomicBoolean enabled = new AtomicBoolean(false);
055
056        private Thread runner = null;
057
058        protected WebcamDiscoveryService(WebcamDriver driver) {
059
060                if (driver == null) {
061                        throw new IllegalArgumentException("Driver cannot be null!");
062                }
063
064                this.driver = driver;
065                this.support = (WebcamDiscoverySupport) (driver instanceof WebcamDiscoverySupport ? driver : null);
066        }
067
068        private static List<Webcam> toWebcams(List<WebcamDevice> devices) {
069                List<Webcam> webcams = new ArrayList<Webcam>();
070                for (WebcamDevice device : devices) {
071                        webcams.add(new Webcam(device));
072                }
073                return webcams;
074        }
075
076        /**
077         * Get list of devices used by webcams.
078         * 
079         * @return List of webcam devices
080         */
081        private static List<WebcamDevice> getDevices(List<Webcam> webcams) {
082                List<WebcamDevice> devices = new ArrayList<WebcamDevice>();
083                for (Webcam webcam : webcams) {
084                        devices.add(webcam.getDevice());
085                }
086                return devices;
087        }
088
089        public List<Webcam> getWebcams(long timeout, TimeUnit tunit) throws TimeoutException {
090
091                if (timeout < 0) {
092                        throw new IllegalArgumentException("Timeout cannot be negative");
093                }
094
095                if (tunit == null) {
096                        throw new IllegalArgumentException("Time unit cannot be null!");
097                }
098
099                List<Webcam> tmp = null;
100
101                synchronized (Webcam.class) {
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 RuntimeException(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                                tmp = new ArrayList<Webcam>(webcams);
132
133                                if (Webcam.isHandleTermSignal()) {
134                                        WebcamDeallocator.store(webcams.toArray(new Webcam[webcams.size()]));
135                                }
136                        }
137                }
138
139                if (tmp != null) {
140                        WebcamDiscoveryListener[] listeners = Webcam.getDiscoveryListeners();
141                        for (Webcam webcam : tmp) {
142                                notifyWebcamFound(webcam, listeners);
143                        }
144                }
145
146                return Collections.unmodifiableList(webcams);
147        }
148
149        /**
150         * Scan for newly added or already removed webcams.
151         */
152        public void scan() {
153
154                WebcamDiscoveryListener[] listeners = Webcam.getDiscoveryListeners();
155
156                List<WebcamDevice> tmpnew = driver.getDevices();
157                List<WebcamDevice> tmpold = null;
158
159                try {
160                        tmpold = getDevices(getWebcams(Long.MAX_VALUE, TimeUnit.MILLISECONDS));
161                } catch (TimeoutException e) {
162                        throw new WebcamException(e);
163                }
164
165                // convert to linked list due to O(1) on remove operation on
166                // iterator versus O(n) for the same operation in array list
167
168                List<WebcamDevice> oldones = new LinkedList<WebcamDevice>(tmpold);
169                List<WebcamDevice> newones = new LinkedList<WebcamDevice>(tmpnew);
170
171                Iterator<WebcamDevice> oi = oldones.iterator();
172                Iterator<WebcamDevice> ni = null;
173
174                WebcamDevice od = null; // old device
175                WebcamDevice nd = null; // new device
176
177                // reduce lists
178
179                while (oi.hasNext()) {
180
181                        od = oi.next();
182                        ni = newones.iterator();
183
184                        while (ni.hasNext()) {
185
186                                nd = ni.next();
187
188                                // remove both elements, if device name is the same, which
189                                // actually means that device is exactly the same
190
191                                if (nd.getName().equals(od.getName())) {
192                                        ni.remove();
193                                        oi.remove();
194                                        break;
195                                }
196                        }
197                }
198
199                // if any left in old ones it means that devices has been removed
200                if (oldones.size() > 0) {
201
202                        List<Webcam> notified = new ArrayList<Webcam>();
203
204                        for (WebcamDevice device : oldones) {
205                                for (Webcam webcam : webcams) {
206                                        if (webcam.getDevice().getName().equals(device.getName())) {
207                                                notified.add(webcam);
208                                                break;
209                                        }
210                                }
211                        }
212
213                        setCurrentWebcams(tmpnew);
214
215                        for (Webcam webcam : notified) {
216                                notifyWebcamGone(webcam, listeners);
217                                webcam.dispose();
218                        }
219                }
220
221                // if any left in new ones it means that devices has been added
222                if (newones.size() > 0) {
223
224                        setCurrentWebcams(tmpnew);
225
226                        for (WebcamDevice device : newones) {
227                                for (Webcam webcam : webcams) {
228                                        if (webcam.getDevice().getName().equals(device.getName())) {
229                                                notifyWebcamFound(webcam, listeners);
230                                                break;
231                                        }
232                                }
233                        }
234                }
235        }
236
237        @Override
238        public void run() {
239
240                // do not run if driver does not support discovery
241
242                if (support == null) {
243                        return;
244                }
245
246                // wait initial time interval since devices has been initially
247                // discovered
248
249                Object monitor = new Object();
250                do {
251
252                        synchronized (monitor) {
253                                try {
254                                        monitor.wait(support.getScanInterval());
255                                } catch (InterruptedException e) {
256                                        break;
257                                } catch (Exception e) {
258                                        throw new RuntimeException("Problem waiting on monitor", e);
259                                }
260                        }
261
262                        scan();
263
264                } while (running.get());
265
266                LOG.debug("Webcam discovery service loop has been stopped");
267        }
268
269        private void setCurrentWebcams(List<WebcamDevice> devices) {
270                webcams = toWebcams(devices);
271                if (Webcam.isHandleTermSignal()) {
272                        WebcamDeallocator.unstore();
273                        WebcamDeallocator.store(webcams.toArray(new Webcam[webcams.size()]));
274                }
275        }
276
277        private static void notifyWebcamGone(Webcam webcam, WebcamDiscoveryListener[] listeners) {
278                WebcamDiscoveryEvent event = new WebcamDiscoveryEvent(webcam, WebcamDiscoveryEvent.REMOVED);
279                for (WebcamDiscoveryListener l : listeners) {
280                        try {
281                                l.webcamGone(event);
282                        } catch (Exception e) {
283                                LOG.error(String.format("Webcam gone, exception when calling listener %s", l.getClass()), e);
284                        }
285                }
286        }
287
288        private static void notifyWebcamFound(Webcam webcam, WebcamDiscoveryListener[] listeners) {
289                WebcamDiscoveryEvent event = new WebcamDiscoveryEvent(webcam, WebcamDiscoveryEvent.ADDED);
290                for (WebcamDiscoveryListener l : listeners) {
291                        try {
292                                l.webcamFound(event);
293                        } catch (Exception e) {
294                                LOG.error(String.format("Webcam found, exception when calling listener %s", l.getClass()), e);
295                        }
296                }
297        }
298
299        /**
300         * Stop discovery service.
301         */
302        public void stop() {
303
304                // return if not running
305
306                if (!running.compareAndSet(true, false)) {
307                        return;
308                }
309
310                try {
311                        runner.join();
312                } catch (InterruptedException e) {
313                        throw new WebcamException("Joint interrupted");
314                }
315
316                LOG.debug("Discovery service has been stopped");
317
318                runner = null;
319        }
320
321        /**
322         * Start discovery service.
323         */
324        public void start() {
325
326                // if configured to not start, then simply return
327
328                if (!enabled.get()) {
329                        LOG.info("Discovery service has been disabled and thus it will not be started");
330                        return;
331                }
332
333                // capture driver does not support discovery - nothing to do
334
335                if (support == null) {
336                        LOG.info("Discovery will not run - driver {} does not support this feature", driver.getClass().getSimpleName());
337                        return;
338                }
339
340                // return if already running
341
342                if (!running.compareAndSet(false, true)) {
343                        return;
344                }
345
346                // start discovery service runner
347
348                runner = new Thread(this, "webcam-discovery-service");
349                runner.setUncaughtExceptionHandler(WebcamExceptionHandler.getInstance());
350                runner.setDaemon(true);
351                runner.start();
352        }
353
354        /**
355         * Is discovery service running?
356         * 
357         * @return True or false
358         */
359        public boolean isRunning() {
360                return running.get();
361        }
362
363        /**
364         * Webcam discovery service will be automatically started if it's enabled,
365         * otherwise, when set to disabled, it will never start, even when user try
366         * to run it.
367         * 
368         * @param enabled the parameter controlling if discovery shall be started
369         */
370        public void setEnabled(boolean enabled) {
371                this.enabled.set(enabled);
372        }
373
374        /**
375         * Cleanup.
376         */
377        protected void shutdown() {
378
379                stop();
380
381                // dispose all webcams
382
383                Iterator<Webcam> wi = webcams.iterator();
384                while (wi.hasNext()) {
385                        Webcam webcam = wi.next();
386                        webcam.dispose();
387                }
388
389                synchronized (Webcam.class) {
390
391                        // clear webcams list
392
393                        webcams.clear();
394
395                        // unassign webcams from deallocator
396
397                        if (Webcam.isHandleTermSignal()) {
398                                WebcamDeallocator.unstore();
399                        }
400                }
401        }
402}