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