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.Iterator;
010    import java.util.List;
011    import java.util.concurrent.CopyOnWriteArrayList;
012    import java.util.concurrent.TimeUnit;
013    import java.util.concurrent.TimeoutException;
014    import java.util.concurrent.atomic.AtomicBoolean;
015    
016    import org.slf4j.Logger;
017    import org.slf4j.LoggerFactory;
018    
019    import com.github.sarxos.webcam.WebcamDevice.BufferAccess;
020    import com.github.sarxos.webcam.ds.buildin.WebcamDefaultDevice;
021    import com.github.sarxos.webcam.ds.buildin.WebcamDefaultDriver;
022    import com.github.sarxos.webcam.ds.cgt.WebcamCloseTask;
023    import com.github.sarxos.webcam.ds.cgt.WebcamDisposeTask;
024    import com.github.sarxos.webcam.ds.cgt.WebcamOpenTask;
025    import com.github.sarxos.webcam.ds.cgt.WebcamReadBufferTask;
026    import com.github.sarxos.webcam.ds.cgt.WebcamReadImageTask;
027    import 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     */
035    public 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    }