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(true);
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                if (!support.isScanPossible()) {
246                        return;
247                }
248
249                // wait initial time interval since devices has been initially
250                // discovered
251
252                Object monitor = new Object();
253                do {
254
255                        synchronized (monitor) {
256                                try {
257                                        monitor.wait(support.getScanInterval());
258                                } catch (InterruptedException e) {
259                                        break;
260                                } catch (Exception e) {
261                                        throw new RuntimeException("Problem waiting on monitor", e);
262                                }
263                        }
264
265                        scan();
266
267                } while (running.get());
268
269                LOG.debug("Webcam discovery service loop has been stopped");
270        }
271
272        private void setCurrentWebcams(List<WebcamDevice> devices) {
273                webcams = toWebcams(devices);
274                if (Webcam.isHandleTermSignal()) {
275                        WebcamDeallocator.unstore();
276                        WebcamDeallocator.store(webcams.toArray(new Webcam[webcams.size()]));
277                }
278        }
279
280        private static void notifyWebcamGone(Webcam webcam, WebcamDiscoveryListener[] listeners) {
281                WebcamDiscoveryEvent event = new WebcamDiscoveryEvent(webcam, WebcamDiscoveryEvent.REMOVED);
282                for (WebcamDiscoveryListener l : listeners) {
283                        try {
284                                l.webcamGone(event);
285                        } catch (Exception e) {
286                                LOG.error(String.format("Webcam gone, exception when calling listener %s", l.getClass()), e);
287                        }
288                }
289        }
290
291        private static void notifyWebcamFound(Webcam webcam, WebcamDiscoveryListener[] listeners) {
292                WebcamDiscoveryEvent event = new WebcamDiscoveryEvent(webcam, WebcamDiscoveryEvent.ADDED);
293                for (WebcamDiscoveryListener l : listeners) {
294                        try {
295                                l.webcamFound(event);
296                        } catch (Exception e) {
297                                LOG.error(String.format("Webcam found, exception when calling listener %s", l.getClass()), e);
298                        }
299                }
300        }
301
302        /**
303         * Stop discovery service.
304         */
305        public void stop() {
306
307                // return if not running
308
309                if (!running.compareAndSet(true, false)) {
310                        return;
311                }
312
313                try {
314                        runner.join();
315                } catch (InterruptedException e) {
316                        throw new WebcamException("Joint interrupted");
317                }
318
319                LOG.debug("Discovery service has been stopped");
320
321                runner = null;
322        }
323
324        /**
325         * Start discovery service.
326         */
327        public void start() {
328
329                // if configured to not start, then simply return
330
331                if (!enabled.get()) {
332                        LOG.info("Discovery service has been disabled and thus it will not be started");
333                        return;
334                }
335
336                // capture driver does not support discovery - nothing to do
337
338                if (support == null) {
339                        LOG.info("Discovery will not run - driver {} does not support this feature", driver.getClass().getSimpleName());
340                        return;
341                }
342
343                // return if already running
344
345                if (!running.compareAndSet(false, true)) {
346                        return;
347                }
348
349                // start discovery service runner
350
351                runner = new Thread(this, "webcam-discovery-service");
352                runner.setUncaughtExceptionHandler(WebcamExceptionHandler.getInstance());
353                runner.setDaemon(true);
354                runner.start();
355        }
356
357        /**
358         * Is discovery service running?
359         * 
360         * @return True or false
361         */
362        public boolean isRunning() {
363                return running.get();
364        }
365
366        /**
367         * Webcam discovery service will be automatically started if it's enabled,
368         * otherwise, when set to disabled, it will never start, even when user try
369         * to run it.
370         * 
371         * @param enabled the parameter controlling if discovery shall be started
372         */
373        public void setEnabled(boolean enabled) {
374                this.enabled.set(enabled);
375        }
376
377        /**
378         * Cleanup.
379         */
380        protected void shutdown() {
381
382                stop();
383
384                // dispose all webcams
385
386                Iterator<Webcam> wi = webcams.iterator();
387                while (wi.hasNext()) {
388                        Webcam webcam = wi.next();
389                        webcam.dispose();
390                }
391
392                synchronized (Webcam.class) {
393
394                        // clear webcams list
395
396                        webcams.clear();
397
398                        // unassign webcams from deallocator
399
400                        if (Webcam.isHandleTermSignal()) {
401                                WebcamDeallocator.unstore();
402                        }
403                }
404        }
405}