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