001package com.github.sarxos.webcam;
002
003import java.awt.Dimension;
004import java.awt.image.BufferedImage;
005import java.nio.ByteBuffer;
006import java.util.ArrayList;
007import java.util.Arrays;
008import java.util.Collections;
009import java.util.Iterator;
010import java.util.List;
011import java.util.concurrent.CopyOnWriteArrayList;
012import java.util.concurrent.ExecutorService;
013import java.util.concurrent.Executors;
014import java.util.concurrent.ThreadFactory;
015import java.util.concurrent.TimeUnit;
016import java.util.concurrent.TimeoutException;
017import java.util.concurrent.atomic.AtomicBoolean;
018
019import org.slf4j.Logger;
020import org.slf4j.LoggerFactory;
021
022import com.github.sarxos.webcam.WebcamDevice.BufferAccess;
023import com.github.sarxos.webcam.ds.buildin.WebcamDefaultDevice;
024import com.github.sarxos.webcam.ds.buildin.WebcamDefaultDriver;
025import com.github.sarxos.webcam.ds.cgt.WebcamCloseTask;
026import com.github.sarxos.webcam.ds.cgt.WebcamDisposeTask;
027import com.github.sarxos.webcam.ds.cgt.WebcamOpenTask;
028import com.github.sarxos.webcam.ds.cgt.WebcamReadBufferTask;
029import com.github.sarxos.webcam.ds.cgt.WebcamReadImageTask;
030import com.github.sarxos.webcam.util.ImageUtils;
031
032
033/**
034 * Webcam class. It wraps webcam device obtained from webcam driver.
035 * 
036 * @author Bartosz Firyn (bfiryn)
037 */
038public class Webcam {
039
040        /**
041         * Class used to asynchronously notify all webcam listeners about new image
042         * available.
043         * 
044         * @author Bartosz Firyn (sarxos)
045         */
046        private static final class ImageNotification implements Runnable {
047
048                /**
049                 * Camera.
050                 */
051                private final Webcam webcam;
052
053                /**
054                 * Acquired image.
055                 */
056                private final BufferedImage image;
057
058                /**
059                 * Create new notification.
060                 * 
061                 * @param webcam the webcam from which image has been acquired
062                 * @param image the acquired image
063                 */
064                public ImageNotification(Webcam webcam, BufferedImage image) {
065                        this.webcam = webcam;
066                        this.image = image;
067                }
068
069                @Override
070                public void run() {
071                        if (image != null) {
072                                WebcamEvent we = new WebcamEvent(WebcamEventType.NEW_IMAGE, webcam, image);
073                                for (WebcamListener l : webcam.getWebcamListeners()) {
074                                        try {
075                                                l.webcamImageObtained(we);
076                                        } catch (Exception e) {
077                                                LOG.error(String.format("Notify image acquired, exception when calling listener %s", l.getClass()), e);
078                                        }
079                                }
080                        }
081                }
082        }
083
084        private final class NotificationThreadFactory implements ThreadFactory {
085
086                @Override
087                public Thread newThread(Runnable r) {
088                        Thread t = new Thread(r, String.format("notificator-[%s]", getName()));
089                        t.setUncaughtExceptionHandler(WebcamExceptionHandler.getInstance());
090                        t.setDaemon(true);
091                        return t;
092                }
093        }
094
095        /**
096         * Logger instance.
097         */
098        private static final Logger LOG = LoggerFactory.getLogger(Webcam.class);
099
100        /**
101         * List of driver classes names to search for.
102         */
103        private static final List<String> DRIVERS_LIST = new ArrayList<String>();
104
105        /**
106         * List of driver classes to search for.
107         */
108        private static final List<Class<?>> DRIVERS_CLASS_LIST = new ArrayList<Class<?>>();
109
110        /**
111         * Discovery listeners.
112         */
113        private static final List<WebcamDiscoveryListener> DISCOVERY_LISTENERS = Collections.synchronizedList(new ArrayList<WebcamDiscoveryListener>());
114
115        /**
116         * Webcam driver (LtiCivil, JMF, FMJ, JQT, OpenCV, VLCj, etc).
117         */
118        private static volatile WebcamDriver driver = null;
119
120        /**
121         * Webcam discovery service.
122         */
123        private static volatile WebcamDiscoveryService discovery = null;
124
125        /**
126         * Is automated deallocation on TERM signal enabled.
127         */
128        private static boolean deallocOnTermSignal = false;
129
130        /**
131         * Is auto-open feature enabled?
132         */
133        private static boolean autoOpen = false;
134
135        /**
136         * Webcam listeners.
137         */
138        private List<WebcamListener> listeners = new CopyOnWriteArrayList<WebcamListener>();
139
140        /**
141         * List of custom resolution sizes supported by webcam instance.
142         */
143        private List<Dimension> customSizes = new ArrayList<Dimension>();
144
145        /**
146         * Shutdown hook.
147         */
148        private WebcamShutdownHook hook = null;
149
150        /**
151         * Underlying webcam device.
152         */
153        private WebcamDevice device = null;
154
155        /**
156         * Is webcam open?
157         */
158        private AtomicBoolean open = new AtomicBoolean(false);
159
160        /**
161         * Is webcam already disposed?
162         */
163        private AtomicBoolean disposed = new AtomicBoolean(false);
164
165        /**
166         * Is non-blocking (asynchronous) access enabled?
167         */
168        private volatile boolean asynchronous = false;
169
170        /**
171         * Current FPS.
172         */
173        private volatile double fps = 0;
174
175        /**
176         * Webcam image updater.
177         */
178        private volatile WebcamUpdater updater = null;
179
180        /**
181         * Image transformer.
182         */
183        private volatile WebcamImageTransformer transformer = null;
184
185        /**
186         * Lock which denies access to the given webcam when it's already in use by
187         * other webcam capture API process or thread.
188         */
189        private WebcamLock lock = null;
190
191        /**
192         * Executor service for image notifications.
193         */
194        private ExecutorService notificator = null;
195
196        /**
197         * Webcam class.
198         * 
199         * @param device - device to be used as webcam
200         * @throws IllegalArgumentException when device argument is null
201         */
202        protected Webcam(WebcamDevice device) {
203                if (device == null) {
204                        throw new IllegalArgumentException("Webcam device cannot be null");
205                }
206                this.device = device;
207                this.lock = new WebcamLock(this);
208        }
209
210        /**
211         * Asynchronously start new thread which will notify all webcam listeners
212         * about the new image available.
213         */
214        protected void notifyWebcamImageAcquired(BufferedImage image) {
215
216                // notify webcam listeners of new image available, do that only if there
217                // are any webcam listeners available because there is no sense to start
218                // additional threads for no purpose
219
220                if (getWebcamListenersCount() > 0) {
221                        notificator.execute(new ImageNotification(this, image));
222                }
223        }
224
225        /**
226         * Open the webcam in blocking (synchronous) mode.
227         * 
228         * @see #open(boolean)
229         */
230        public boolean open() {
231                return open(false);
232        }
233
234        /**
235         * Open the webcam in either blocking (synchronous) or non-blocking
236         * (asynchronous) mode.The difference between those two modes lies in the
237         * image acquisition mechanism.<br>
238         * <br>
239         * In blocking mode, when user calls {@link #getImage()} method, device is
240         * being queried for new image buffer and user have to wait for it to be
241         * available.<br>
242         * <br>
243         * In non-blocking mode, there is a special thread running in the background
244         * which constantly fetch new images and cache them internally for further
245         * use. This cached instance is returned every time when user request new
246         * image. Because of that it can be used when timeing is very important,
247         * because all users calls for new image do not have to wait on device
248         * response. By using this mode user should be aware of the fact that in
249         * some cases, when two consecutive calls to get new image are executed more
250         * often than webcam device can serve them, the same image instance will be
251         * returned. User should use {@link #isImageNew()} method to distinguish if
252         * returned image is not the same as the previous one.
253         * 
254         * @param async true for non-blocking mode, false for blocking
255         */
256        public boolean open(boolean async) {
257
258                if (open.compareAndSet(false, true)) {
259
260                        assert lock != null;
261
262                        notificator = Executors.newSingleThreadExecutor(new NotificationThreadFactory());
263
264                        // lock webcam for other Java (only) processes
265
266                        lock.lock();
267
268                        // open webcam device
269
270                        WebcamOpenTask task = new WebcamOpenTask(driver, device);
271                        try {
272                                task.open();
273                        } catch (InterruptedException e) {
274                                lock.unlock();
275                                open.set(false);
276                                LOG.debug("Thread has been interrupted in the middle of webcam opening process!", e);
277                                return false;
278                        } catch (WebcamException e) {
279                                lock.unlock();
280                                open.set(false);
281                                LOG.debug("Webcam exception when opening", e);
282                                throw e;
283                        }
284
285                        LOG.debug("Webcam is now open {}", getName());
286
287                        // setup non-blocking configuration
288
289                        if (asynchronous = async) {
290                                if (updater == null) {
291                                        updater = new WebcamUpdater(this);
292                                }
293                                updater.start();
294                        }
295
296                        // install shutdown hook
297
298                        Runtime.getRuntime().addShutdownHook(hook = new WebcamShutdownHook(this));
299
300                        // notify listeners
301
302                        WebcamEvent we = new WebcamEvent(WebcamEventType.OPEN, this);
303                        Iterator<WebcamListener> wli = listeners.iterator();
304                        WebcamListener l = null;
305
306                        while (wli.hasNext()) {
307                                l = wli.next();
308                                try {
309                                        l.webcamOpen(we);
310                                } catch (Exception e) {
311                                        LOG.error(String.format("Notify webcam open, exception when calling listener %s", l.getClass()), e);
312                                }
313                        }
314
315                } else {
316                        LOG.debug("Webcam is already open {}", getName());
317                }
318
319                return true;
320        }
321
322        /**
323         * Close the webcam.
324         */
325        public boolean close() {
326
327                if (open.compareAndSet(true, false)) {
328
329                        LOG.debug("Closing webcam {}", getName());
330
331                        assert lock != null;
332
333                        // close webcam
334
335                        WebcamCloseTask task = new WebcamCloseTask(driver, device);
336                        try {
337                                task.close();
338                        } catch (InterruptedException e) {
339                                open.set(true);
340                                LOG.debug("Thread has been interrupted before webcam was closed!", e);
341                                return false;
342                        } catch (WebcamException e) {
343                                open.set(true);
344                                throw e;
345                        }
346
347                        // stop updater
348                        if (asynchronous) {
349                                updater.stop();
350                        }
351
352                        // remove shutdown hook (it's not more necessary)
353                        removeShutdownHook();
354
355                        // unlock webcam so other Java processes can start using it
356                        lock.unlock();
357
358                        // notify listeners
359
360                        WebcamEvent we = new WebcamEvent(WebcamEventType.CLOSED, this);
361                        Iterator<WebcamListener> wli = listeners.iterator();
362                        WebcamListener l = null;
363
364                        while (wli.hasNext()) {
365                                l = wli.next();
366                                try {
367                                        l.webcamClosed(we);
368                                } catch (Exception e) {
369                                        LOG.error(String.format("Notify webcam closed, exception when calling %s listener", l.getClass()), e);
370                                }
371                        }
372
373                        notificator.shutdown();
374                        while (!notificator.isTerminated()) {
375                                try {
376                                        notificator.awaitTermination(100, TimeUnit.MILLISECONDS);
377                                } catch (InterruptedException e) {
378                                        return false;
379                                }
380                        }
381
382                        LOG.debug("Webcam {} has been closed", getName());
383
384                } else {
385                        LOG.debug("Webcam {} is already closed", getName());
386                }
387
388                return true;
389        }
390
391        /**
392         * Return underlying webcam device. Depending on the driver used to discover
393         * devices, this method can return instances of different class. By default
394         * {@link WebcamDefaultDevice} is returned when no external driver is used.
395         * 
396         * @return Underlying webcam device instance
397         */
398        public WebcamDevice getDevice() {
399                assert device != null;
400                return device;
401        }
402
403        /**
404         * Completely dispose capture device. After this operation webcam cannot be
405         * used any more and full reinstantiation is required.
406         */
407        protected void dispose() {
408
409                assert disposed != null;
410                assert open != null;
411                assert driver != null;
412                assert device != null;
413                assert listeners != null;
414
415                if (!disposed.compareAndSet(false, true)) {
416                        return;
417                }
418
419                open.set(false);
420
421                LOG.info("Disposing webcam {}", getName());
422
423                WebcamDisposeTask task = new WebcamDisposeTask(driver, device);
424                try {
425                        task.dispose();
426                } catch (InterruptedException e) {
427                        LOG.error("Processor has been interrupted before webcam was disposed!", e);
428                        return;
429                }
430
431                WebcamEvent we = new WebcamEvent(WebcamEventType.DISPOSED, this);
432                Iterator<WebcamListener> wli = listeners.iterator();
433                WebcamListener l = null;
434
435                while (wli.hasNext()) {
436                        l = wli.next();
437                        try {
438                                l.webcamClosed(we);
439                                l.webcamDisposed(we);
440                        } catch (Exception e) {
441                                LOG.error(String.format("Notify webcam disposed, exception when calling %s listener", l.getClass()), e);
442                        }
443                }
444
445                removeShutdownHook();
446
447                LOG.debug("Webcam disposed {}", getName());
448        }
449
450        private void removeShutdownHook() {
451
452                // hook can be null because there is a possibility that webcam has never
453                // been open and therefore hook was not created
454
455                if (hook != null) {
456                        try {
457                                Runtime.getRuntime().removeShutdownHook(hook);
458                        } catch (IllegalStateException e) {
459                                LOG.trace("Shutdown in progress, cannot remove hook");
460                        }
461                }
462        }
463
464        /**
465         * TRansform image using image transformer. If image transformer has not
466         * been set, this method return instance passed in the argument, without any
467         * modifications.
468         * 
469         * @param image the image to be transformed
470         * @return Transformed image (if transformer is set)
471         */
472        protected BufferedImage transform(BufferedImage image) {
473                if (image != null) {
474                        WebcamImageTransformer tr = getImageTransformer();
475                        if (tr != null) {
476                                return tr.transform(image);
477                        }
478                }
479                return image;
480        }
481
482        /**
483         * Is webcam open?
484         * 
485         * @return true if open, false otherwise
486         */
487        public boolean isOpen() {
488                return open.get();
489        }
490
491        /**
492         * Get current webcam resolution in pixels.
493         * 
494         * @return Webcam resolution (picture size) in pixels.
495         */
496        public Dimension getViewSize() {
497                return device.getResolution();
498        }
499
500        /**
501         * Return list of supported view sizes. It can differ between vary webcam
502         * data sources.
503         * 
504         * @return Array of supported dimensions
505         */
506        public Dimension[] getViewSizes() {
507                return device.getResolutions();
508        }
509
510        /**
511         * Set custom resolution. If you are using this method you have to make sure
512         * that your webcam device can support this specific resolution.
513         * 
514         * @param sizes the array of custom resolutions to be supported by webcam
515         */
516        public void setCustomViewSizes(Dimension[] sizes) {
517                assert customSizes != null;
518                if (sizes == null) {
519                        customSizes.clear();
520                        return;
521                }
522                customSizes = Arrays.asList(sizes);
523        }
524
525        public Dimension[] getCustomViewSizes() {
526                assert customSizes != null;
527                return customSizes.toArray(new Dimension[customSizes.size()]);
528        }
529
530        /**
531         * Set new view size. New size has to exactly the same as one of the default
532         * sized or exactly the same as one of the custom ones.
533         * 
534         * @param size the new view size to be set
535         * @see Webcam#setCustomViewSizes(Dimension[])
536         * @see Webcam#getViewSizes()
537         */
538        public void setViewSize(Dimension size) {
539
540                if (size == null) {
541                        throw new IllegalArgumentException("Resolution cannot be null!");
542                }
543
544                if (open.get()) {
545                        throw new IllegalStateException("Cannot change resolution when webcam is open, please close it first");
546                }
547
548                // check if new resolution is the same as current one
549
550                Dimension current = getViewSize();
551                if (current != null && current.width == size.width && current.height == size.height) {
552                        return;
553                }
554
555                // check if new resolution is valid
556
557                Dimension[] predefined = getViewSizes();
558                Dimension[] custom = getCustomViewSizes();
559
560                assert predefined != null;
561                assert custom != null;
562
563                boolean ok = false;
564                for (Dimension d : predefined) {
565                        if (d.width == size.width && d.height == size.height) {
566                                ok = true;
567                                break;
568                        }
569                }
570                if (!ok) {
571                        for (Dimension d : custom) {
572                                if (d.width == size.width && d.height == size.height) {
573                                        ok = true;
574                                        break;
575                                }
576                        }
577                }
578
579                if (!ok) {
580                        StringBuilder sb = new StringBuilder("Incorrect dimension [");
581                        sb.append(size.width).append("x").append(size.height).append("] ");
582                        sb.append("possible ones are ");
583                        for (Dimension d : predefined) {
584                                sb.append("[").append(d.width).append("x").append(d.height).append("] ");
585                        }
586                        for (Dimension d : custom) {
587                                sb.append("[").append(d.width).append("x").append(d.height).append("] ");
588                        }
589                        throw new IllegalArgumentException(sb.toString());
590                }
591
592                LOG.debug("Setting new resolution {}x{}", size.width, size.height);
593
594                device.setResolution(size);
595        }
596
597        /**
598         * Capture image from webcam and return it. Will return image object or null
599         * if webcam is closed or has been already disposed by JVM.<br>
600         * <br>
601         * <b>IMPORTANT NOTE!!!</b><br>
602         * <br>
603         * There are two possible behaviors of what webcam should do when you try to
604         * get image and webcam is actually closed. Normally it will return null,
605         * but there is a special flag which can be statically set to switch all
606         * webcams to auto open mode. In this mode, webcam will be automatically
607         * open, when you try to get image from closed webcam. Please be aware of
608         * some side effects! In case of multi-threaded applications, there is no
609         * guarantee that one thread will not try to open webcam even if it was
610         * manually closed in different thread.
611         * 
612         * @return Captured image or null if webcam is closed or disposed by JVM
613         */
614        public BufferedImage getImage() {
615
616                if (!isReady()) {
617                        return null;
618                }
619
620                long t1 = 0;
621                long t2 = 0;
622
623                if (asynchronous) {
624                        return updater.getImage();
625                } else {
626
627                        // get image
628
629                        t1 = System.currentTimeMillis();
630                        BufferedImage image = transform(new WebcamReadImageTask(driver, device).getImage());
631                        t2 = System.currentTimeMillis();
632
633                        if (image == null) {
634                                return null;
635                        }
636
637                        // get FPS
638
639                        if (device instanceof WebcamDevice.FPSSource) {
640                                fps = ((WebcamDevice.FPSSource) device).getFPS();
641                        } else {
642                                // +1 to avoid division by zero
643                                fps = (4 * fps + 1000 / (t2 - t1 + 1)) / 5;
644                        }
645
646                        // notify webcam listeners about new image available
647
648                        notifyWebcamImageAcquired(image);
649
650                        return image;
651                }
652        }
653
654        public boolean isImageNew() {
655                if (asynchronous) {
656                        return updater.isImageNew();
657                }
658                return true;
659        }
660
661        public double getFPS() {
662                if (asynchronous) {
663                        return updater.getFPS();
664                } else {
665                        return fps;
666                }
667        }
668
669        /**
670         * Get RAW image ByteBuffer. It will always return buffer with 3 x 1 bytes
671         * per each pixel, where RGB components are on (0, 1, 2) and color space is
672         * sRGB.<br>
673         * <br>
674         * 
675         * <b>IMPORTANT!</b><br>
676         * Some drivers can return direct ByteBuffer, so there is no guarantee that
677         * underlying bytes will not be released in next read image operation.
678         * Therefore, to avoid potential bugs you should convert this ByteBuffer to
679         * bytes array before you fetch next image.
680         * 
681         * @return Byte buffer
682         */
683        public ByteBuffer getImageBytes() {
684
685                if (!isReady()) {
686                        return null;
687                }
688
689                assert driver != null;
690                assert device != null;
691
692                // some devices can support direct image buffers, and for those call
693                // processor task, and for those which does not support direct image
694                // buffers, just convert image to RGB byte array
695
696                if (device instanceof BufferAccess) {
697                        return new WebcamReadBufferTask(driver, device).getBuffer();
698                } else {
699                        BufferedImage image = getImage();
700                        if (image != null) {
701                                return ByteBuffer.wrap(ImageUtils.toRawByteArray(image));
702                        } else {
703                                return null;
704                        }
705                }
706        }
707
708        /**
709         * Is webcam ready to be read.
710         * 
711         * @return True if ready, false otherwise
712         */
713        private boolean isReady() {
714
715                assert disposed != null;
716                assert open != null;
717
718                if (disposed.get()) {
719                        LOG.warn("Cannot get image, webcam has been already disposed");
720                        return false;
721                }
722
723                if (!open.get()) {
724                        if (autoOpen) {
725                                open();
726                        } else {
727                                return false;
728                        }
729                }
730
731                return true;
732        }
733
734        /**
735         * Get list of webcams to use. This method will wait predefined time
736         * interval for webcam devices to be discovered. By default this time is set
737         * to 1 minute.
738         * 
739         * @return List of webcams existing in the system
740         * @throws WebcamException when something is wrong
741         * @see Webcam#getWebcams(long, TimeUnit)
742         */
743        public static List<Webcam> getWebcams() throws WebcamException {
744
745                // timeout exception below will never be caught since user would have to
746                // wait around three hundreds billion years for it to occur
747
748                try {
749                        return getWebcams(Long.MAX_VALUE);
750                } catch (TimeoutException e) {
751                        throw new RuntimeException(e);
752                }
753        }
754
755        /**
756         * Get list of webcams to use. This method will wait given time interval for
757         * webcam devices to be discovered. Time argument is given in milliseconds.
758         * 
759         * @param timeout the time to wait for webcam devices to be discovered
760         * @return List of webcams existing in the ssytem
761         * @throws WebcamException when something is wrong
762         * @throws IllegalArgumentException when timeout is negative
763         * @see Webcam#getWebcams(long, TimeUnit)
764         */
765        public static List<Webcam> getWebcams(long timeout) throws TimeoutException, WebcamException {
766                if (timeout < 0) {
767                        throw new IllegalArgumentException(String.format("Timeout cannot be negative (%d)", timeout));
768                }
769                return getWebcams(timeout, TimeUnit.MILLISECONDS);
770        }
771
772        /**
773         * Get list of webcams to use. This method will wait given time interval for
774         * webcam devices to be discovered.
775         * 
776         * @param timeout the devices discovery timeout
777         * @param tunit the time unit
778         * @return List of webcams
779         * @throws TimeoutException when timeout has been exceeded
780         * @throws WebcamException when something is wrong
781         * @throws IllegalArgumentException when timeout is negative or tunit null
782         */
783        public static synchronized List<Webcam> getWebcams(long timeout, TimeUnit tunit) throws TimeoutException, WebcamException {
784
785                if (timeout < 0) {
786                        throw new IllegalArgumentException(String.format("Timeout cannot be negative (%d)", timeout));
787                }
788                if (tunit == null) {
789                        throw new IllegalArgumentException("Time unit cannot be null!");
790                }
791
792                WebcamDiscoveryService discovery = getDiscoveryService();
793
794                assert discovery != null;
795
796                List<Webcam> webcams = discovery.getWebcams(timeout, tunit);
797                if (!discovery.isRunning()) {
798                        discovery.start();
799                }
800
801                return webcams;
802        }
803
804        /**
805         * Will discover and return first webcam available in the system.
806         * 
807         * @return Default webcam (first from the list)
808         * @throws WebcamException if something is really wrong
809         * @see Webcam#getWebcams()
810         */
811        public static Webcam getDefault() throws WebcamException {
812
813                try {
814                        return getDefault(Long.MAX_VALUE);
815                } catch (TimeoutException e) {
816                        // this should never happen since user would have to wait 300000000
817                        // years for it to occur
818                        throw new RuntimeException(e);
819                }
820        }
821
822        /**
823         * Will discover and return first webcam available in the system.
824         * 
825         * @param timeout the webcam discovery timeout (1 minute by default)
826         * @return Default webcam (first from the list)
827         * @throws TimeoutException when discovery timeout has been exceeded
828         * @throws WebcamException if something is really wrong
829         * @throws IllegalArgumentException when timeout is negative
830         * @see Webcam#getWebcams(long)
831         */
832        public static Webcam getDefault(long timeout) throws TimeoutException, WebcamException {
833                if (timeout < 0) {
834                        throw new IllegalArgumentException(String.format("Timeout cannot be negative (%d)", timeout));
835                }
836                return getDefault(timeout, TimeUnit.MILLISECONDS);
837        }
838
839        /**
840         * Will discover and return first webcam available in the system.
841         * 
842         * @param timeout the webcam discovery timeout (1 minute by default)
843         * @param tunit the time unit
844         * @return Default webcam (first from the list)
845         * @throws TimeoutException when discovery timeout has been exceeded
846         * @throws WebcamException if something is really wrong
847         * @throws IllegalArgumentException when timeout is negative or tunit null
848         * @see Webcam#getWebcams(long, TimeUnit)
849         */
850        public static Webcam getDefault(long timeout, TimeUnit tunit) throws TimeoutException, WebcamException {
851
852                if (timeout < 0) {
853                        throw new IllegalArgumentException(String.format("Timeout cannot be negative (%d)", timeout));
854                }
855                if (tunit == null) {
856                        throw new IllegalArgumentException("Time unit cannot be null!");
857                }
858
859                List<Webcam> webcams = getWebcams(timeout, tunit);
860
861                assert webcams != null;
862
863                if (!webcams.isEmpty()) {
864                        return webcams.get(0);
865                }
866
867                LOG.warn("No webcam has been detected!");
868
869                return null;
870        }
871
872        /**
873         * Get webcam name (device name). The name of device depends on the value
874         * returned by the underlying data source, so in some cases it can be
875         * human-readable value and sometimes it can be some strange number.
876         * 
877         * @return Name
878         */
879        public String getName() {
880                assert device != null;
881                return device.getName();
882        }
883
884        @Override
885        public String toString() {
886                return String.format("Webcam %s", getName());
887        }
888
889        /**
890         * Add webcam listener.
891         * 
892         * @param l the listener to be added
893         * @throws IllegalArgumentException when argument is null
894         */
895        public boolean addWebcamListener(WebcamListener l) {
896                if (l == null) {
897                        throw new IllegalArgumentException("Webcam listener cannot be null!");
898                }
899                assert listeners != null;
900                return listeners.add(l);
901        }
902
903        /**
904         * @return All webcam listeners
905         */
906        public WebcamListener[] getWebcamListeners() {
907                assert listeners != null;
908                return listeners.toArray(new WebcamListener[listeners.size()]);
909        }
910
911        /**
912         * @return Number of webcam listeners
913         */
914        public int getWebcamListenersCount() {
915                assert listeners != null;
916                return listeners.size();
917        }
918
919        /**
920         * Removes webcam listener.
921         * 
922         * @param l the listener to be removed
923         * @return True if listener has been removed, false otherwise
924         */
925        public boolean removeWebcamListener(WebcamListener l) {
926                assert listeners != null;
927                return listeners.remove(l);
928        }
929
930        /**
931         * Return webcam driver. Perform search if necessary.<br>
932         * <br>
933         * <b>This method is not thread-safe!</b>
934         * 
935         * @return Webcam driver
936         */
937        public static synchronized WebcamDriver getDriver() {
938
939                if (driver != null) {
940                        return driver;
941                }
942
943                if (driver == null) {
944                        driver = WebcamDriverUtils.findDriver(DRIVERS_LIST, DRIVERS_CLASS_LIST);
945                }
946                if (driver == null) {
947                        driver = new WebcamDefaultDriver();
948                }
949
950                LOG.info("{} capture driver will be used", driver.getClass().getSimpleName());
951
952                return driver;
953        }
954
955        /**
956         * Set new video driver to be used by webcam.<br>
957         * <br>
958         * <b>This method is not thread-safe!</b>
959         * 
960         * @param wd new webcam driver to be used (e.g. LtiCivil, JFM, FMJ, QTJ)
961         * @throws IllegalArgumentException when argument is null
962         */
963        public static void setDriver(WebcamDriver wd) {
964
965                if (wd == null) {
966                        throw new IllegalArgumentException("Webcam driver cannot be null!");
967                }
968
969                LOG.debug("Setting new capture driver {}", wd);
970
971                resetDriver();
972
973                driver = wd;
974        }
975
976        /**
977         * Set new video driver class to be used by webcam. Class given in the
978         * argument shall extend {@link WebcamDriver} interface and should have
979         * public default constructor, so instance can be created by reflection.<br>
980         * <br>
981         * <b>This method is not thread-safe!</b>
982         * 
983         * @param driverClass new video driver class to use
984         * @throws IllegalArgumentException when argument is null
985         */
986        public static void setDriver(Class<? extends WebcamDriver> driverClass) {
987
988                if (driverClass == null) {
989                        throw new IllegalArgumentException("Webcam driver class cannot be null!");
990                }
991
992                resetDriver();
993
994                try {
995                        driver = driverClass.newInstance();
996                } catch (InstantiationException e) {
997                        throw new WebcamException(e);
998                } catch (IllegalAccessException e) {
999                        throw new WebcamException(e);
1000                }
1001        }
1002
1003        /**
1004         * Reset webcam driver.<br>
1005         * <br>
1006         * <b>This method is not thread-safe!</b>
1007         */
1008        public static void resetDriver() {
1009
1010                synchronized (DRIVERS_LIST) {
1011                        DRIVERS_LIST.clear();
1012                }
1013
1014                if (discovery != null) {
1015                        discovery.shutdown();
1016                        discovery = null;
1017                }
1018
1019                driver = null;
1020        }
1021
1022        /**
1023         * Register new webcam video driver.
1024         * 
1025         * @param clazz webcam video driver class
1026         * @throws IllegalArgumentException when argument is null
1027         */
1028        public static void registerDriver(Class<? extends WebcamDriver> clazz) {
1029                if (clazz == null) {
1030                        throw new IllegalArgumentException("Webcam driver class to register cannot be null!");
1031                }
1032                DRIVERS_CLASS_LIST.add(clazz);
1033                registerDriver(clazz.getCanonicalName());
1034        }
1035
1036        /**
1037         * Register new webcam video driver.
1038         * 
1039         * @param clazzName webcam video driver class name
1040         * @throws IllegalArgumentException when argument is null
1041         */
1042        public static void registerDriver(String clazzName) {
1043                if (clazzName == null) {
1044                        throw new IllegalArgumentException("Webcam driver class name to register cannot be null!");
1045                }
1046                DRIVERS_LIST.add(clazzName);
1047        }
1048
1049        /**
1050         * <b>CAUTION!!!</b><br>
1051         * <br>
1052         * This is experimental feature to be used mostly in in development phase.
1053         * After you set handle term signal to true, and fetch capture devices,
1054         * Webcam Capture API will listen for TERM signal and try to close all
1055         * devices after it has been received. <b>This feature can be unstable on
1056         * some systems!</b>
1057         * 
1058         * @param on signal handling will be enabled if true, disabled otherwise
1059         */
1060        public static void setHandleTermSignal(boolean on) {
1061                if (on) {
1062                        LOG.warn("Automated deallocation on TERM signal is now enabled! Make sure to not use it in production!");
1063                }
1064                deallocOnTermSignal = on;
1065        }
1066
1067        /**
1068         * Is TERM signal handler enabled.
1069         * 
1070         * @return True if enabled, false otherwise
1071         */
1072        public static boolean isHandleTermSignal() {
1073                return deallocOnTermSignal;
1074        }
1075
1076        /**
1077         * Switch all webcams to auto open mode. In this mode, each webcam will be
1078         * automatically open whenever user will try to get image from instance
1079         * which has not yet been open. Please be aware of some side effects! In
1080         * case of multi-threaded applications, there is no guarantee that one
1081         * thread will not try to open webcam even if it was manually closed in
1082         * different thread.
1083         * 
1084         * @param on true to enable, false to disable
1085         */
1086        public static void setAutoOpenMode(boolean on) {
1087                autoOpen = on;
1088        }
1089
1090        /**
1091         * Is auto open mode enabled. Auto open mode will will automatically open
1092         * webcam whenever user will try to get image from instance which has not
1093         * yet been open. Please be aware of some side effects! In case of
1094         * multi-threaded applications, there is no guarantee that one thread will
1095         * not try to open webcam even if it was manually closed in different
1096         * thread.
1097         * 
1098         * @return True if mode is enabled, false otherwise
1099         */
1100        public static boolean isAutoOpenMode() {
1101                return autoOpen;
1102        }
1103
1104        /**
1105         * Add new webcam discovery listener.
1106         * 
1107         * @param l the listener to be added
1108         * @return True, if listeners list size has been changed, false otherwise
1109         * @throws IllegalArgumentException when argument is null
1110         */
1111        public static boolean addDiscoveryListener(WebcamDiscoveryListener l) {
1112                if (l == null) {
1113                        throw new IllegalArgumentException("Webcam discovery listener cannot be null!");
1114                }
1115                return DISCOVERY_LISTENERS.add(l);
1116        }
1117
1118        public static WebcamDiscoveryListener[] getDiscoveryListeners() {
1119                return DISCOVERY_LISTENERS.toArray(new WebcamDiscoveryListener[DISCOVERY_LISTENERS.size()]);
1120        }
1121
1122        /**
1123         * Remove discovery listener
1124         * 
1125         * @param l the listener to be removed
1126         * @return True if listeners list contained the specified element
1127         */
1128        public static boolean removeDiscoveryListener(WebcamDiscoveryListener l) {
1129                return DISCOVERY_LISTENERS.remove(l);
1130        }
1131
1132        /**
1133         * Return discovery service.
1134         * 
1135         * @return Discovery service
1136         */
1137        public static synchronized WebcamDiscoveryService getDiscoveryService() {
1138                if (discovery == null) {
1139                        discovery = new WebcamDiscoveryService(getDriver());
1140                }
1141                return discovery;
1142        }
1143
1144        /**
1145         * Return discovery service without creating it if not exists.
1146         * 
1147         * @return Discovery service or null if not yet created
1148         */
1149        public static synchronized WebcamDiscoveryService getDiscoveryServiceRef() {
1150                return discovery;
1151        }
1152
1153        /**
1154         * Return image transformer.
1155         * 
1156         * @return Transformer instance
1157         */
1158        public WebcamImageTransformer getImageTransformer() {
1159                return transformer;
1160        }
1161
1162        /**
1163         * Set image transformer.
1164         * 
1165         * @param transformer the transformer to be set
1166         */
1167        public void setImageTransformer(WebcamImageTransformer transformer) {
1168                this.transformer = transformer;
1169        }
1170
1171        /**
1172         * Return webcam lock.
1173         * 
1174         * @return Webcam lock
1175         */
1176        public WebcamLock getLock() {
1177                return lock;
1178        }
1179
1180        public static void shutdown() {
1181
1182                // stop discovery service
1183                WebcamDiscoveryService discovery = getDiscoveryServiceRef();
1184                if (discovery != null) {
1185                        discovery.stop();
1186                }
1187
1188                // stop processor
1189                WebcamProcessor.getInstance().shutdown();
1190        }
1191}