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}