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