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             * Webcam class.
122             * 
123             * @param device - device to be used as webcam
124             * @throws IllegalArgumentException when device argument is null
125             */
126            protected Webcam(WebcamDevice device) {
127                    if (device == null) {
128                            throw new IllegalArgumentException("Webcam device cannot be null");
129                    }
130                    this.device = device;
131            }
132    
133            /**
134             * Open the webcam in blocking (synchronous) mode.
135             * 
136             * @see #open(boolean)
137             */
138            public boolean open() {
139                    return open(false);
140            }
141    
142            /**
143             * Open the webcam in either blocking (synchronous) or non-blocking
144             * (asynchronous) mode.The difference between those two modes lies in the
145             * image acquisition mechanism.<br>
146             * <br>
147             * In blocking mode, when user calls {@link #getImage()} method, device is
148             * being queried for new image buffer and user have to wait for it to be
149             * available.<br>
150             * <br>
151             * In non-blocking mode, there is a special thread running in the background
152             * which constantly fetch new images and cache them internally for further
153             * use. This cached instance is returned every time when user request new
154             * image. Because of that it can be used when timeing is very important,
155             * because all users calls for new image do not have to wait on device
156             * response. By using this mode user should be aware of the fact that in
157             * some cases, when two consecutive calls to get new image are executed more
158             * often than webcam device can serve them, the same image instance will be
159             * returned. User should use {@link #isImageNew()} method to distinguish if
160             * returned image is not the same as the previous one.
161             * 
162             * @param asynchronous true for non-blocking mode, false for blocking
163             */
164            public boolean open(boolean async) {
165    
166                    if (open.compareAndSet(false, true)) {
167    
168                            WebcamOpenTask task = new WebcamOpenTask(driver, device);
169                            try {
170                                    task.open();
171                            } catch (InterruptedException e) {
172                                    open.set(false);
173                                    LOG.error("Processor has been interrupted before webcam was open!", e);
174                                    return false;
175                            } catch (WebcamException e) {
176                                    open.set(false);
177                                    throw e;
178                            }
179    
180                            LOG.debug("Webcam is now open {}", getName());
181    
182                            // setup non-blocking configuration
183    
184                            asynchronous = async;
185    
186                            if (async) {
187                                    updater.start();
188                            }
189    
190                            // install shutdown hook
191    
192                            Runtime.getRuntime().addShutdownHook(hook = new WebcamShutdownHook(this));
193    
194                            // notify listeners
195    
196                            WebcamEvent we = new WebcamEvent(WebcamEventType.OPEN, this);
197                            for (WebcamListener l : getWebcamListeners()) {
198                                    try {
199                                            l.webcamOpen(we);
200                                    } catch (Exception e) {
201                                            LOG.error(String.format("Notify webcam open, exception when calling listener %s", l.getClass()), e);
202                                    }
203                            }
204    
205                    } else {
206                            LOG.debug("Webcam is already open {}", getName());
207                    }
208    
209                    return true;
210            }
211    
212            /**
213             * Close the webcam.
214             */
215            public boolean close() {
216    
217                    if (open.compareAndSet(true, false)) {
218    
219                            // close webcam
220    
221                            WebcamCloseTask task = new WebcamCloseTask(driver, device);
222                            try {
223                                    task.close();
224                            } catch (InterruptedException e) {
225                                    open.set(true);
226                                    LOG.error("Processor has been interrupted before webcam was closed!", e);
227                                    return false;
228                            } catch (WebcamException e) {
229                                    open.set(false);
230                                    throw e;
231                            }
232    
233                            // stop updater
234    
235                            if (asynchronous) {
236                                    updater.stop();
237                            }
238    
239                            // remove shutdown hook (it's not more necessary)
240    
241                            Runtime.getRuntime().removeShutdownHook(hook);
242    
243                            // notify listeners
244    
245                            WebcamEvent we = new WebcamEvent(WebcamEventType.CLOSED, this);
246                            for (WebcamListener l : getWebcamListeners()) {
247                                    try {
248                                            l.webcamClosed(we);
249                                    } catch (Exception e) {
250                                            LOG.error(String.format("Notify webcam closed, exception when calling %s listener", l.getClass()), e);
251                                    }
252                            }
253    
254                    } else {
255                            LOG.debug("Webcam is already closed {}", getName());
256                    }
257    
258                    return true;
259            }
260    
261            /**
262             * Is webcam open?
263             * 
264             * @return true if open, false otherwise
265             */
266            public boolean isOpen() {
267                    return open.get();
268            }
269    
270            /**
271             * Get current webcam resolution in pixels.
272             * 
273             * @return Webcam resolution (picture size) in pixels.
274             */
275            public Dimension getViewSize() {
276                    return device.getResolution();
277            }
278    
279            /**
280             * Return list of supported view sizes. It can differ between vary webcam
281             * data sources.
282             * 
283             * @return
284             */
285            public Dimension[] getViewSizes() {
286                    return device.getResolutions();
287            }
288    
289            /**
290             * Set custom resolution. If you are using this method you have to make sure
291             * that your webcam device can support this specific resolution.
292             * 
293             * @param sizes the array of custom resolutions to be supported by webcam
294             */
295            public void setCustomViewSizes(Dimension[] sizes) {
296                    if (sizes == null) {
297                            customSizes.clear();
298                            return;
299                    }
300                    customSizes = Arrays.asList(sizes);
301            }
302    
303            public Dimension[] getCustomViewSizes() {
304                    return customSizes.toArray(new Dimension[customSizes.size()]);
305            }
306    
307            /**
308             * Set new view size. New size has to exactly the same as one of the default
309             * sized or exactly the same as one of the custom ones.
310             * 
311             * @param size the new view size to be set
312             * @see Webcam#setCustomViewSizes(Dimension[])
313             * @see Webcam#getViewSizes()
314             */
315            public void setViewSize(Dimension size) {
316    
317                    if (size == null) {
318                            throw new IllegalArgumentException("Resolution cannot be null!");
319                    }
320    
321                    if (open.get()) {
322                            throw new IllegalStateException("Cannot change resolution when webcam is open, please close it first");
323                    }
324    
325                    // check if new resolution is the same as current one
326    
327                    Dimension current = getViewSize();
328                    if (current != null && current.width == size.width && current.height == size.height) {
329                            return;
330                    }
331    
332                    // check if new resolution is valid
333    
334                    Dimension[] predefined = getViewSizes();
335                    Dimension[] custom = getCustomViewSizes();
336    
337                    boolean ok = false;
338                    for (Dimension d : predefined) {
339                            if (d.width == size.width && d.height == size.height) {
340                                    ok = true;
341                                    break;
342                            }
343                    }
344                    if (!ok) {
345                            for (Dimension d : custom) {
346                                    if (d.width == size.width && d.height == size.height) {
347                                            ok = true;
348                                            break;
349                                    }
350                            }
351                    }
352    
353                    if (!ok) {
354                            StringBuilder sb = new StringBuilder("Incorrect dimension [");
355                            sb.append(size.width).append("x").append(size.height).append("] ");
356                            sb.append("possible ones are ");
357                            for (Dimension d : predefined) {
358                                    sb.append("[").append(d.width).append("x").append(d.height).append("] ");
359                            }
360                            for (Dimension d : custom) {
361                                    sb.append("[").append(d.width).append("x").append(d.height).append("] ");
362                            }
363                            throw new IllegalArgumentException(sb.toString());
364                    }
365    
366                    LOG.debug("Setting new resolution {}x{}", size.width, size.height);
367    
368                    device.setResolution(size);
369            }
370    
371            /**
372             * Capture image from webcam and return it. Will return image object or null
373             * if webcam is closed or has been already disposed by JVM.<br>
374             * <br>
375             * <b>IMPORTANT NOTE!!!</b><br>
376             * <br>
377             * There are two possible behaviors of what webcam should do when you try to
378             * get image and webcam is actually closed. Normally it will return null,
379             * but there is a special flag which can be statically set to switch all
380             * webcams to auto open mode. In this mode, webcam will be automatically
381             * open, when you try to get image from closed webcam. Please be aware of
382             * some side effects! In case of multi-threaded applications, there is no
383             * guarantee that one thread will not try to open webcam even if it was
384             * manually closed in different thread.
385             * 
386             * @return Captured image or null if webcam is closed or disposed by JVM
387             */
388            public BufferedImage getImage() {
389    
390                    if (!isReady()) {
391                            return null;
392                    }
393    
394                    long t1 = 0;
395                    long t2 = 0;
396    
397                    if (asynchronous) {
398                            return updater.getImage();
399                    } else {
400    
401                            // get image
402    
403                            t1 = System.currentTimeMillis();
404                            BufferedImage image = new WebcamReadImageTask(driver, device).getImage();
405                            t2 = System.currentTimeMillis();
406    
407                            if (image == null) {
408                                    return null;
409                            }
410    
411                            // calculate FPS
412    
413                            // +1 to avoid division by zero
414                            fps = (4 * fps + 1000 / (t2 - t1 + 1)) / 5;
415    
416                            // notify webcam listeners about new image available
417    
418                            updater.notifyWebcamImageObtained(this, image);
419    
420                            return image;
421                    }
422            }
423    
424            protected double getFPS() {
425                    if (asynchronous) {
426                            return updater.getFPS();
427                    } else {
428                            return fps;
429                    }
430            }
431    
432            /**
433             * Get RAW image ByteBuffer. It will always return buffer with 3 x 1 bytes
434             * per each pixel, where RGB components are on (0, 1, 2) and color space is
435             * sRGB.<br>
436             * <br>
437             * 
438             * <b>IMPORTANT!</b><br>
439             * Some drivers can return direct ByteBuffer, so there is no guarantee that
440             * underlying bytes will not be released in next read image operation.
441             * Therefore, to avoid potential bugs you should convert this ByteBuffer to
442             * bytes array before you fetch next image.
443             * 
444             * @return Byte buffer
445             */
446            public ByteBuffer getImageBytes() {
447    
448                    if (!isReady()) {
449                            return null;
450                    }
451    
452                    // some devices can support direct image buffers, and for those call
453                    // processor task, and for those which does not support direct image
454                    // buffers, just convert image to RGB byte array
455    
456                    if (device instanceof BufferAccess) {
457                            return new WebcamReadBufferTask(driver, device).getBuffer();
458                    } else {
459                            return ByteBuffer.wrap(ImageUtils.toRawByteArray(getImage()));
460                    }
461            }
462    
463            /**
464             * Is webcam ready to be read.
465             * 
466             * @return True if ready, false otherwise
467             */
468            private boolean isReady() {
469    
470                    if (disposed.get()) {
471                            LOG.warn("Cannot get image, webcam has been already disposed");
472                            return false;
473                    }
474    
475                    if (!open.get()) {
476                            if (autoOpen) {
477                                    open();
478                            } else {
479                                    return false;
480                            }
481                    }
482    
483                    return true;
484            }
485    
486            /**
487             * Get list of webcams to use. This method will wait predefined time
488             * interval for webcam devices to be discovered. By default this time is set
489             * to 1 minute.
490             * 
491             * @return List of webcams existing in the ssytem
492             * @throws WebcamException when something is wrong
493             * @see Webcam#getWebcams(long, TimeUnit)
494             */
495            public static List<Webcam> getWebcams() throws WebcamException {
496    
497                    // timeout exception below will never be caught since user would have to
498                    // wait around three hundreds billion years for it to occur
499    
500                    try {
501                            return getWebcams(Long.MAX_VALUE);
502                    } catch (TimeoutException e) {
503                            throw new RuntimeException(e);
504                    }
505            }
506    
507            /**
508             * Get list of webcams to use. This method will wait given time interval for
509             * webcam devices to be discovered. Time argument is given in milliseconds.
510             * 
511             * @param timeout the time to wait for webcam devices to be discovered
512             * @return List of webcams existing in the ssytem
513             * @throws WebcamException when something is wrong
514             * @see Webcam#getWebcams(long, TimeUnit)
515             */
516            public static List<Webcam> getWebcams(long timeout) throws TimeoutException, WebcamException {
517                    return getWebcams(timeout, TimeUnit.MILLISECONDS);
518            }
519    
520            /**
521             * Get list of webcams to use. This method will wait given time interval for
522             * webcam devices to be discovered.
523             * 
524             * @param timeout the devices discovery timeout
525             * @param tunit the time unit
526             * @return List of webcams
527             * @throws TimeoutException when timeout has been exceeded
528             * @throws WebcamException when something is wrong
529             */
530            public static synchronized List<Webcam> getWebcams(long timeout, TimeUnit tunit) throws TimeoutException, WebcamException {
531    
532                    WebcamDiscoveryService discovery = getDiscoveryService();
533                    List<Webcam> webcams = discovery.getWebcams(timeout, tunit);
534    
535                    if (!discovery.isRunning()) {
536                            discovery.start();
537                    }
538    
539                    return webcams;
540            }
541    
542            /**
543             * Will discover and return first webcam available in the system.
544             * 
545             * @return Default webcam (first from the list)
546             * @throws WebcamException if something is really wrong
547             * @see Webcam#getWebcams()
548             */
549            public static Webcam getDefault() throws WebcamException {
550    
551                    try {
552                            return getDefault(Long.MAX_VALUE);
553                    } catch (TimeoutException e) {
554                            // this should never happen since user would have to wait 300000000
555                            // years for it to occur
556                            throw new RuntimeException(e);
557                    }
558            }
559    
560            /**
561             * Will discover and return first webcam available in the system.
562             * 
563             * @param timeout the webcam discovery timeout (1 minute by default)
564             * @return Default webcam (first from the list)
565             * @throws TimeoutException when discovery timeout has been exceeded
566             * @throws WebcamException if something is really wrong
567             * @see Webcam#getWebcams(long)
568             */
569            public static Webcam getDefault(long timeout) throws TimeoutException, WebcamException {
570                    return getDefault(timeout, TimeUnit.MILLISECONDS);
571            }
572    
573            /**
574             * Will discover and return first webcam available in the system.
575             * 
576             * @param timeout the webcam discovery timeout (1 minute by default)
577             * @param tunit the time unit
578             * @return Default webcam (first from the list)
579             * @throws TimeoutException when discovery timeout has been exceeded
580             * @throws WebcamException if something is really wrong
581             * @see Webcam#getWebcams(long, TimeUnit)
582             */
583            public static Webcam getDefault(long timeout, TimeUnit tunit) throws TimeoutException, WebcamException {
584    
585                    if (timeout < 0) {
586                            throw new IllegalArgumentException("Timeout cannot be negative");
587                    }
588                    if (tunit == null) {
589                            throw new IllegalArgumentException("Time unit cannot be null!");
590                    }
591    
592                    List<Webcam> webcams = getWebcams(timeout, tunit);
593    
594                    if (!webcams.isEmpty()) {
595                            return webcams.get(0);
596                    }
597    
598                    LOG.warn("No webcam has been detected!");
599    
600                    return null;
601            }
602    
603            /**
604             * Get webcam name (device name). The name of device depends on the value
605             * returned by the underlying data source, so in some cases it can be
606             * human-readable value and sometimes it can be some strange number.
607             * 
608             * @return Name
609             */
610            public String getName() {
611                    return device.getName();
612            }
613    
614            @Override
615            public String toString() {
616                    return String.format("Webcam %s", getName());
617            }
618    
619            /**
620             * Add webcam listener.
621             * 
622             * @param l the listener to be added
623             * @throws IllegalArgumentException when argument is null
624             */
625            public boolean addWebcamListener(WebcamListener l) {
626                    if (l == null) {
627                            throw new IllegalArgumentException("Webcam listener cannot be null!");
628                    }
629                    return listeners.add(l);
630            }
631    
632            /**
633             * @return All webcam listeners
634             */
635            public WebcamListener[] getWebcamListeners() {
636                    return listeners.toArray(new WebcamListener[listeners.size()]);
637            }
638    
639            /**
640             * @return Number of webcam listeners
641             */
642            public int getWebcamListenersCount() {
643                    return listeners.size();
644            }
645    
646            /**
647             * Removes webcam listener.
648             * 
649             * @param l the listener to be removed
650             * @return True if listener has been removed, false otherwise
651             */
652            public boolean removeWebcamListener(WebcamListener l) {
653                    return listeners.remove(l);
654            }
655    
656            /**
657             * Return webcam driver. Perform search if necessary.<br>
658             * <br>
659             * <b>This method is not thread-safe!</b>
660             * 
661             * @return Webcam driver
662             */
663            public static synchronized WebcamDriver getDriver() {
664    
665                    if (driver == null) {
666                            driver = WebcamDriverUtils.findDriver(DRIVERS_LIST, DRIVERS_CLASS_LIST);
667                    }
668    
669                    if (driver == null) {
670                            driver = new WebcamDefaultDriver();
671                    }
672    
673                    LOG.info("{} capture driver will be used", driver.getClass().getSimpleName());
674    
675                    return driver;
676            }
677    
678            /**
679             * Set new video driver to be used by webcam.<br>
680             * <br>
681             * <b>This method is not thread-safe!</b>
682             * 
683             * @param driver new webcam driver to be used (e.g. LtiCivil, JFM, FMJ, QTJ)
684             * @throws IllegalArgumentException when argument is null
685             */
686            public static synchronized void setDriver(WebcamDriver driver) {
687    
688                    if (driver == null) {
689                            throw new IllegalArgumentException("Webcam driver cannot be null!");
690                    }
691    
692                    resetDriver();
693    
694                    Webcam.driver = driver;
695            }
696    
697            /**
698             * Set new video driver class to be used by webcam. Class given in the
699             * argument shall extend {@link WebcamDriver} interface and should have
700             * public default constructor, so instance can be created by reflection.<br>
701             * <br>
702             * <b>This method is not thread-safe!</b>
703             * 
704             * @param driver new video driver class to use
705             * @throws IllegalArgumentException when argument is null
706             */
707            public static synchronized void setDriver(Class<? extends WebcamDriver> driverClass) {
708    
709                    resetDriver();
710    
711                    if (driverClass == null) {
712                            throw new IllegalArgumentException("Webcam driver class cannot be null!");
713                    }
714    
715                    try {
716                            driver = driverClass.newInstance();
717                    } catch (InstantiationException e) {
718                            throw new WebcamException(e);
719                    } catch (IllegalAccessException e) {
720                            throw new WebcamException(e);
721                    }
722            }
723    
724            /**
725             * Reset webcam driver.<br>
726             * <br>
727             * <b>This method is not thread-safe!</b>
728             */
729            public static synchronized void resetDriver() {
730    
731                    DRIVERS_LIST.clear();
732    
733                    if (discovery != null) {
734                            discovery.shutdown();
735                            discovery = null;
736                    }
737    
738                    driver = null;
739            }
740    
741            /**
742             * Register new webcam video driver.
743             * 
744             * @param clazz webcam video driver class
745             * @throws IllegalArgumentException when argument is null
746             */
747            public static void registerDriver(Class<? extends WebcamDriver> clazz) {
748                    if (clazz == null) {
749                            throw new IllegalArgumentException("Webcam driver class to register cannot be null!");
750                    }
751                    DRIVERS_CLASS_LIST.add(clazz);
752                    registerDriver(clazz.getCanonicalName());
753            }
754    
755            /**
756             * Register new webcam video driver.
757             * 
758             * @param clazzName webcam video driver class name
759             * @throws IllegalArgumentException when argument is null
760             */
761            public static void registerDriver(String clazzName) {
762                    if (clazzName == null) {
763                            throw new IllegalArgumentException("Webcam driver class name to register cannot be null!");
764                    }
765                    DRIVERS_LIST.add(clazzName);
766            }
767    
768            /**
769             * Return underlying webcam device. Depending on the driver used to discover
770             * devices, this method can return instances of different class. By default
771             * {@link WebcamDefaultDevice} is returned when no external driver is used.
772             * 
773             * @return Underlying webcam device instance
774             */
775            public WebcamDevice getDevice() {
776                    return device;
777            }
778    
779            /**
780             * Completely dispose capture device. After this operation webcam cannot be
781             * used any more and full reinstantiation is required.
782             */
783            protected void dispose() {
784    
785                    if (!disposed.compareAndSet(false, true)) {
786                            return;
787                    }
788    
789                    open.set(false);
790    
791                    LOG.info("Disposing webcam {}", getName());
792    
793                    WebcamDisposeTask task = new WebcamDisposeTask(driver, device);
794                    try {
795                            task.dispose();
796                    } catch (InterruptedException e) {
797                            LOG.error("Processor has been interrupted before webcam was disposed!", e);
798                            return;
799                    }
800    
801                    WebcamEvent we = new WebcamEvent(WebcamEventType.DISPOSED, this);
802                    for (WebcamListener l : listeners) {
803                            try {
804                                    l.webcamClosed(we);
805                                    l.webcamDisposed(we);
806                            } catch (Exception e) {
807                                    LOG.error(String.format("Notify webcam disposed, exception when calling %s listener", l.getClass()), e);
808                            }
809                    }
810    
811                    // hook can be null because there is a possibility that webcam has never
812                    // been open and therefore hook was not created
813                    if (hook != null) {
814                            try {
815                                    Runtime.getRuntime().removeShutdownHook(hook);
816                            } catch (IllegalStateException e) {
817                                    LOG.trace("Shutdown in progress, cannot remove hook");
818                            }
819                    }
820    
821                    LOG.debug("Webcam disposed {}", getName());
822            }
823    
824            /**
825             * <b>CAUTION!!!</b><br>
826             * <br>
827             * This is experimental feature to be used mostly in in development phase.
828             * After you set handle term signal to true, and fetch capture devices,
829             * Webcam Capture API will listen for TERM signal and try to close all
830             * devices after it has been received. <b>This feature can be unstable on
831             * some systems!</b>
832             * 
833             * @param on signal handling will be enabled if true, disabled otherwise
834             */
835            public static void setHandleTermSignal(boolean on) {
836                    if (on) {
837                            LOG.warn("Automated deallocation on TERM signal is now enabled! Make sure to not use it in production!");
838                    }
839                    deallocOnTermSignal = on;
840            }
841    
842            /**
843             * Is TERM signal handler enabled.
844             * 
845             * @return True if enabled, false otherwise
846             */
847            public static boolean isHandleTermSignal() {
848                    return deallocOnTermSignal;
849            }
850    
851            /**
852             * Switch all webcams to auto open mode. In this mode, each webcam will be
853             * automatically open whenever user will try to get image from instance
854             * which has not yet been open. Please be aware of some side effects! In
855             * case of multi-threaded applications, there is no guarantee that one
856             * thread will not try to open webcam even if it was manually closed in
857             * different thread.
858             * 
859             * @param on true to enable, false to disable
860             */
861            public static void setAutoOpenMode(boolean on) {
862                    autoOpen = on;
863            }
864    
865            /**
866             * Is auto open mode enabled. Auto open mode will will automatically open
867             * webcam whenever user will try to get image from instance which has not
868             * yet been open. Please be aware of some side effects! In case of
869             * multi-threaded applications, there is no guarantee that one thread will
870             * not try to open webcam even if it was manually closed in different
871             * thread.
872             * 
873             * @return True if mode is enabled, false otherwise
874             */
875            public static boolean isAutoOpenMode() {
876                    return autoOpen;
877            }
878    
879            /**
880             * Add new webcam discovery listener.
881             * 
882             * @param l the listener to be added
883             * @return True, if listeners list size has been changed, false otherwise
884             * @throws IllegalArgumentException when argument is null
885             */
886            public static boolean addDiscoveryListener(WebcamDiscoveryListener l) {
887                    if (l == null) {
888                            throw new IllegalArgumentException("Webcam discovery listener cannot be null!");
889                    }
890                    return DISCOVERY_LISTENERS.add(l);
891            }
892    
893            public static WebcamDiscoveryListener[] getDiscoveryListeners() {
894                    return DISCOVERY_LISTENERS.toArray(new WebcamDiscoveryListener[DISCOVERY_LISTENERS.size()]);
895            }
896    
897            /**
898             * Remove discovery listener
899             * 
900             * @param l the listener to be removed
901             * @return True if listeners list contained the specified element
902             */
903            public static boolean removeDiscoveryListener(WebcamDiscoveryListener l) {
904                    return DISCOVERY_LISTENERS.remove(l);
905            }
906    
907            /**
908             * Return discovery service.
909             * 
910             * @return Discovery service
911             */
912            public static synchronized WebcamDiscoveryService getDiscoveryService() {
913                    if (discovery == null) {
914                            discovery = new WebcamDiscoveryService(getDriver());
915                    }
916                    return discovery;
917            }
918    }