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