001package com.github.sarxos.webcam; 002 003import java.awt.AlphaComposite; 004import java.awt.BasicStroke; 005import java.awt.Color; 006import java.awt.Dimension; 007import java.awt.FontMetrics; 008import java.awt.Graphics; 009import java.awt.Graphics2D; 010import java.awt.RenderingHints; 011import java.awt.image.BufferedImage; 012import java.beans.PropertyChangeEvent; 013import java.beans.PropertyChangeListener; 014import java.util.Locale; 015import java.util.ResourceBundle; 016import java.util.concurrent.Executors; 017import java.util.concurrent.ScheduledExecutorService; 018import java.util.concurrent.ThreadFactory; 019import java.util.concurrent.TimeUnit; 020import java.util.concurrent.atomic.AtomicBoolean; 021import java.util.concurrent.atomic.AtomicInteger; 022 023import javax.swing.JPanel; 024import javax.swing.SwingUtilities; 025 026import org.slf4j.Logger; 027import org.slf4j.LoggerFactory; 028 029 030/** 031 * Simply implementation of JPanel allowing users to render pictures taken with 032 * webcam. 033 * 034 * @author Bartosz Firyn (SarXos) 035 */ 036public class WebcamPanel extends JPanel implements WebcamListener, PropertyChangeListener { 037 038 /** 039 * Interface of the painter used to draw image in panel. 040 * 041 * @author Bartosz Firyn (SarXos) 042 */ 043 public static interface Painter { 044 045 /** 046 * Paints panel without image. 047 * 048 * @param g2 the graphics 2D object used for drawing 049 */ 050 void paintPanel(WebcamPanel panel, Graphics2D g2); 051 052 /** 053 * Paints webcam image in panel. 054 * 055 * @param g2 the graphics 2D object used for drawing 056 */ 057 void paintImage(WebcamPanel panel, BufferedImage image, Graphics2D g2); 058 } 059 060 /** 061 * Default painter used to draw image in panel. 062 * 063 * @author Bartosz Firyn (SarXos) 064 */ 065 public class DefaultPainter implements Painter { 066 067 private String name = null; 068 069 @Override 070 public void paintPanel(WebcamPanel owner, Graphics2D g2) { 071 072 assert owner != null; 073 assert g2 != null; 074 075 g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); 076 g2.setBackground(Color.BLACK); 077 g2.fillRect(0, 0, getWidth(), getHeight()); 078 079 int cx = (getWidth() - 70) / 2; 080 int cy = (getHeight() - 40) / 2; 081 082 g2.setStroke(new BasicStroke(2)); 083 g2.setColor(Color.LIGHT_GRAY); 084 g2.fillRoundRect(cx, cy, 70, 40, 10, 10); 085 g2.setColor(Color.WHITE); 086 g2.fillOval(cx + 5, cy + 5, 30, 30); 087 g2.setColor(Color.LIGHT_GRAY); 088 g2.fillOval(cx + 10, cy + 10, 20, 20); 089 g2.setColor(Color.WHITE); 090 g2.fillOval(cx + 12, cy + 12, 16, 16); 091 g2.fillRoundRect(cx + 50, cy + 5, 15, 10, 5, 5); 092 g2.fillRect(cx + 63, cy + 25, 7, 2); 093 g2.fillRect(cx + 63, cy + 28, 7, 2); 094 g2.fillRect(cx + 63, cy + 31, 7, 2); 095 096 g2.setColor(Color.DARK_GRAY); 097 g2.setStroke(new BasicStroke(3)); 098 g2.drawLine(0, 0, getWidth(), getHeight()); 099 g2.drawLine(0, getHeight(), getWidth(), 0); 100 101 String str = null; 102 103 final String strInitDevice = rb.getString("INITIALIZING_DEVICE"); 104 final String strNoImage = rb.getString("NO_IMAGE"); 105 final String strDeviceError = rb.getString("DEVICE_ERROR"); 106 107 if (!errored) { 108 str = starting ? strInitDevice : strNoImage; 109 } else { 110 str = strDeviceError; 111 } 112 113 FontMetrics metrics = g2.getFontMetrics(getFont()); 114 int w = metrics.stringWidth(str); 115 int h = metrics.getHeight(); 116 117 int x = (getWidth() - w) / 2; 118 int y = cy - h; 119 120 g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_OFF); 121 g2.setFont(getFont()); 122 g2.setColor(Color.WHITE); 123 g2.drawString(str, x, y); 124 125 if (name == null) { 126 name = webcam.getName(); 127 } 128 129 str = name; 130 131 w = metrics.stringWidth(str); 132 h = metrics.getHeight(); 133 134 g2.drawString(str, (getWidth() - w) / 2, cy - 2 * h); 135 } 136 137 @Override 138 public void paintImage(WebcamPanel owner, BufferedImage image, Graphics2D g2) { 139 140 int w = getWidth(); 141 int h = getHeight(); 142 143 if (fillArea && image.getWidth() != w && image.getHeight() != h) { 144 145 BufferedImage resized = new BufferedImage(w, h, BufferedImage.TYPE_3BYTE_BGR); 146 Graphics2D gr = resized.createGraphics(); 147 gr.setComposite(AlphaComposite.Src); 148 gr.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BILINEAR); 149 gr.setRenderingHint(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY); 150 gr.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); 151 gr.drawImage(image, 0, 0, w, h, null); 152 gr.dispose(); 153 resized.flush(); 154 155 image = resized; 156 } 157 158 g2.drawImage(image, 0, 0, null); 159 160 if (isFPSDisplayed()) { 161 162 String str = String.format("FPS: %.1f", webcam.getFPS()); 163 164 int x = 5; 165 int y = getHeight() - 5; 166 167 g2.setFont(getFont()); 168 g2.setColor(Color.BLACK); 169 g2.drawString(str, x + 1, y + 1); 170 g2.setColor(Color.WHITE); 171 g2.drawString(str, x, y); 172 } 173 } 174 } 175 176 private static final class PanelThreadFactory implements ThreadFactory { 177 178 private static final AtomicInteger number = new AtomicInteger(0); 179 180 @Override 181 public Thread newThread(Runnable r) { 182 Thread t = new Thread(r, String.format("webcam-panel-scheduled-executor-%d", number.incrementAndGet())); 183 t.setUncaughtExceptionHandler(WebcamExceptionHandler.getInstance()); 184 t.setDaemon(true); 185 return t; 186 } 187 188 } 189 190 /** 191 * This runnable will do nothing more than repaint panel. 192 */ 193 private static final class SwingRepainter implements Runnable { 194 195 private WebcamPanel panel = null; 196 197 public SwingRepainter(WebcamPanel panel) { 198 this.panel = panel; 199 } 200 201 @Override 202 public void run() { 203 panel.repaint(); 204 } 205 } 206 207 /** 208 * S/N used by Java to serialize beans. 209 */ 210 private static final long serialVersionUID = 1L; 211 212 /** 213 * Logger. 214 */ 215 private static final Logger LOG = LoggerFactory.getLogger(WebcamPanel.class); 216 217 /** 218 * Minimum FPS frequency. 219 */ 220 public static final double MIN_FREQUENCY = 0.016; // 1 frame per minute 221 222 /** 223 * Maximum FPS frequency. 224 */ 225 private static final double MAX_FREQUENCY = 50; // 50 frames per second 226 227 /** 228 * Thread factory used by execution service. 229 */ 230 private static final ThreadFactory THREAD_FACTORY = new PanelThreadFactory(); 231 232 /** 233 * This runnable will do nothing more than repaint panel. 234 */ 235 private final Runnable repaint = new SwingRepainter(this); 236 237 /** 238 * Scheduled executor acting as timer. 239 */ 240 private ScheduledExecutorService executor = null; 241 242 /** 243 * Image updater reads images from camera and force panel to be repainted. 244 * 245 * @author Bartosz Firyn (SarXos) 246 */ 247 private class ImageUpdater implements Runnable { 248 249 /** 250 * Repaint scheduler schedule panel updates. 251 * 252 * @author Bartosz Firyn (sarxos) 253 */ 254 private class RepaintScheduler extends Thread { 255 256 /** 257 * Repaint scheduler schedule panel updates. 258 */ 259 public RepaintScheduler() { 260 setUncaughtExceptionHandler(WebcamExceptionHandler.getInstance()); 261 setName(String.format("repaint-scheduler-%s", webcam.getName())); 262 setDaemon(true); 263 } 264 265 @Override 266 public void run() { 267 268 // do nothing when not running 269 if (!running.get()) { 270 return; 271 } 272 273 repaintPanel(); 274 275 // loop when starting, to wait for images 276 while (starting) { 277 try { 278 Thread.sleep(50); 279 } catch (InterruptedException e) { 280 throw new RuntimeException(e); 281 } 282 } 283 284 // schedule update when webcam is open, otherwise schedule 285 // second scheduler execution 286 287 if (webcam.isOpen()) { 288 289 // FPS limit means that panel rendering frequency is limited 290 // to the specific value and panel will not be rendered more 291 // often then specific value 292 293 // TODO: rename FPS value in panel to rendering frequency 294 295 if (isFPSLimited()) { 296 executor.scheduleAtFixedRate(updater, 0, (long) (1000 / frequency), TimeUnit.MILLISECONDS); 297 } else { 298 executor.scheduleWithFixedDelay(updater, 100, 1, TimeUnit.MILLISECONDS); 299 } 300 } else { 301 executor.schedule(this, 500, TimeUnit.MILLISECONDS); 302 } 303 } 304 305 } 306 307 /** 308 * Update scheduler thread. 309 */ 310 private Thread scheduler = new RepaintScheduler(); 311 312 /** 313 * Is repainter running? 314 */ 315 private AtomicBoolean running = new AtomicBoolean(false); 316 317 /** 318 * Start repainter. Can be invoked many times, but only first call will 319 * take effect. 320 */ 321 public void start() { 322 if (running.compareAndSet(false, true)) { 323 executor = Executors.newScheduledThreadPool(1, THREAD_FACTORY); 324 scheduler.start(); 325 } 326 } 327 328 /** 329 * Stop repainter. Can be invoked many times, but only first call will 330 * take effect. 331 * 332 * @throws InterruptedException 333 */ 334 public void stop() throws InterruptedException { 335 if (running.compareAndSet(true, false)) { 336 executor.shutdown(); 337 executor.awaitTermination(5000, TimeUnit.MILLISECONDS); 338 scheduler.join(); 339 } 340 } 341 342 @Override 343 public void run() { 344 try { 345 update(); 346 } catch (Throwable t) { 347 errored = true; 348 WebcamExceptionHandler.handle(t); 349 } 350 } 351 352 /** 353 * Perform single panel area update (repaint newly obtained image). 354 */ 355 private void update() { 356 357 // do nothing when updater not running, when webcam is closed, or 358 // panel repainting is paused 359 360 if (!running.get() || !webcam.isOpen() || paused) { 361 return; 362 } 363 364 // get new image from webcam 365 366 BufferedImage tmp = webcam.getImage(); 367 if (tmp != null) { 368 errored = false; 369 image = tmp; 370 } 371 372 // and repaint it 373 374 repaintPanel(); 375 } 376 } 377 378 /** 379 * Resource bundle. 380 */ 381 private ResourceBundle rb = null; 382 383 /** 384 * Fit image into panel area. 385 */ 386 private boolean fillArea = false; 387 388 /** 389 * Frames requesting frequency. 390 */ 391 private double frequency = 5; // FPS 392 393 /** 394 * Is frames requesting frequency limited? If true, images will be fetched 395 * in configured time intervals. If false, images will be fetched as fast as 396 * camera can serve them. 397 */ 398 private boolean frequencyLimit = false; 399 400 /** 401 * Display FPS. 402 */ 403 private boolean frequencyDisplayed = false; 404 405 /** 406 * Webcam object used to fetch images. 407 */ 408 private final Webcam webcam; 409 410 /** 411 * Repainter is used to fetch images from camera and force panel repaint 412 * when image is ready. 413 */ 414 private final ImageUpdater updater; 415 416 /** 417 * Image currently being displayed. 418 */ 419 private BufferedImage image = null; 420 421 /** 422 * Webcam is currently starting. 423 */ 424 private volatile boolean starting = false; 425 426 /** 427 * Painting is paused. 428 */ 429 private volatile boolean paused = false; 430 431 /** 432 * Is there any problem with webcam? 433 */ 434 private volatile boolean errored = false; 435 436 /** 437 * Webcam has been started. 438 */ 439 private final AtomicBoolean started = new AtomicBoolean(false); 440 441 /** 442 * Default painter. 443 */ 444 private final Painter defaultPainter = new DefaultPainter(); 445 446 /** 447 * Painter used to draw image in panel. 448 * 449 * @see #setPainter(Painter) 450 * @see #getPainter() 451 */ 452 private Painter painter = defaultPainter; 453 454 /** 455 * Preferred panel size. 456 */ 457 private Dimension defaultSize = null; 458 459 /** 460 * Creates webcam panel and automatically start webcam. 461 * 462 * @param webcam the webcam to be used to fetch images 463 */ 464 public WebcamPanel(Webcam webcam) { 465 this(webcam, true); 466 } 467 468 /** 469 * Creates new webcam panel which display image from camera in you your 470 * Swing application. 471 * 472 * @param webcam the webcam to be used to fetch images 473 * @param start true if webcam shall be automatically started 474 */ 475 public WebcamPanel(Webcam webcam, boolean start) { 476 this(webcam, null, start); 477 } 478 479 /** 480 * Creates new webcam panel which display image from camera in you your 481 * Swing application. If panel size argument is null, then image size will 482 * be used. If you would like to fill panel area with image even if its size 483 * is different, then you can use {@link WebcamPanel#setFillArea(boolean)} 484 * method to configure this. 485 * 486 * @param webcam the webcam to be used to fetch images 487 * @param size the size of panel 488 * @param start true if webcam shall be automatically started 489 * @see WebcamPanel#setFillArea(boolean) 490 */ 491 public WebcamPanel(Webcam webcam, Dimension size, boolean start) { 492 493 if (webcam == null) { 494 throw new IllegalArgumentException(String.format("Webcam argument in %s constructor cannot be null!", getClass().getSimpleName())); 495 } 496 497 this.defaultSize = size; 498 this.webcam = webcam; 499 this.updater = new ImageUpdater(); 500 this.rb = WebcamUtils.loadRB(WebcamPanel.class, getLocale()); 501 502 addPropertyChangeListener("locale", this); 503 504 if (size == null) { 505 Dimension r = webcam.getViewSize(); 506 if (r == null) { 507 r = webcam.getViewSizes()[0]; 508 } 509 setPreferredSize(r); 510 } else { 511 setPreferredSize(size); 512 } 513 514 if (start) { 515 start(); 516 } 517 } 518 519 /** 520 * Set new painter. Painter is a class which pains image visible when 521 * 522 * @param painter the painter object to be set 523 */ 524 public void setPainter(Painter painter) { 525 this.painter = painter; 526 } 527 528 /** 529 * Get painter used to draw image in webcam panel. 530 * 531 * @return Painter object 532 */ 533 public Painter getPainter() { 534 return painter; 535 } 536 537 @Override 538 protected void paintComponent(Graphics g) { 539 Graphics2D g2 = (Graphics2D) g; 540 if (image == null) { 541 painter.paintPanel(this, g2); 542 } else { 543 painter.paintImage(this, image, g2); 544 } 545 } 546 547 /** 548 * Open webcam and start rendering. 549 */ 550 public void start() { 551 552 if (!started.compareAndSet(false, true)) { 553 return; 554 } 555 556 webcam.addWebcamListener(this); 557 558 LOG.debug("Starting panel rendering and trying to open attached webcam"); 559 560 updater.start(); 561 562 starting = true; 563 564 try { 565 if (!webcam.isOpen()) { 566 errored = !webcam.open(); 567 } 568 } catch (WebcamException e) { 569 errored = true; 570 throw e; 571 } finally { 572 starting = false; 573 repaintPanel(); 574 } 575 576 } 577 578 /** 579 * Stop rendering and close webcam. 580 */ 581 public void stop() { 582 583 if (!started.compareAndSet(true, false)) { 584 return; 585 } 586 587 webcam.removeWebcamListener(this); 588 589 LOG.debug("Stopping panel rendering and closing attached webcam"); 590 591 try { 592 updater.stop(); 593 } catch (InterruptedException e) { 594 throw new RuntimeException(e); 595 } 596 597 image = null; 598 599 try { 600 if (webcam.isOpen()) { 601 errored = !webcam.close(); 602 } 603 } catch (WebcamException e) { 604 errored = true; 605 throw e; 606 } finally { 607 repaintPanel(); 608 } 609 } 610 611 /** 612 * Repaint panel in Swing asynchronous manner. 613 */ 614 private void repaintPanel() { 615 SwingUtilities.invokeLater(repaint); 616 } 617 618 /** 619 * Pause rendering. 620 */ 621 public void pause() { 622 if (paused) { 623 return; 624 } 625 626 LOG.debug("Pausing panel rendering"); 627 628 paused = true; 629 } 630 631 /** 632 * Resume rendering. 633 */ 634 public void resume() { 635 636 if (!paused) { 637 return; 638 } 639 640 LOG.debug("Resuming panel rendering"); 641 642 paused = false; 643 } 644 645 /** 646 * Is frequency limit enabled? 647 * 648 * @return True or false 649 */ 650 public boolean isFPSLimited() { 651 return frequencyLimit; 652 } 653 654 /** 655 * Enable or disable frequency limit. Frequency limit should be used for 656 * <b>all IP cameras working in pull mode</b> (to save number of HTTP 657 * requests). If true, images will be fetched in configured time intervals. 658 * If false, images will be fetched as fast as camera can serve them. 659 * 660 * @param frequencyLimit 661 */ 662 public void setFPSLimited(boolean frequencyLimit) { 663 this.frequencyLimit = frequencyLimit; 664 } 665 666 /** 667 * Get rendering frequency in FPS (equivalent to Hz). 668 * 669 * @return Rendering frequency 670 */ 671 public double getFPSLimit() { 672 return frequency; 673 } 674 675 /** 676 * Set rendering frequency (in Hz or FPS). Minimum frequency is 0.016 (1 677 * frame per minute) and maximum is 25 (25 frames per second). 678 * 679 * @param fps the frequency 680 */ 681 public void setFPSLimit(double fps) { 682 if (fps > MAX_FREQUENCY) { 683 fps = MAX_FREQUENCY; 684 } 685 if (fps < MIN_FREQUENCY) { 686 fps = MIN_FREQUENCY; 687 } 688 this.frequency = fps; 689 } 690 691 public boolean isFPSDisplayed() { 692 return frequencyDisplayed; 693 } 694 695 public void setFPSDisplayed(boolean displayed) { 696 this.frequencyDisplayed = displayed; 697 } 698 699 /** 700 * Is webcam panel repainting starting. 701 * 702 * @return True if panel is starting 703 */ 704 public boolean isStarting() { 705 return starting; 706 } 707 708 /** 709 * Is webcam panel repainting started. 710 * 711 * @return True if panel repainting has been started 712 */ 713 public boolean isStarted() { 714 return started.get(); 715 } 716 717 /** 718 * Image will be resized to fill panel area if true. If false then image 719 * will be rendered as it was obtained from webcam instance. 720 * 721 * @param fillArea shall image be resided to fill panel area 722 */ 723 public void setFillArea(boolean fillArea) { 724 this.fillArea = fillArea; 725 } 726 727 /** 728 * Get value of fill area setting. Image will be resized to fill panel area 729 * if true. If false then image will be rendered as it was obtained from 730 * webcam instance. 731 * 732 * @return True if image is being resized, false otherwise 733 */ 734 public boolean isFillArea() { 735 return fillArea; 736 } 737 738 public Painter getDefaultPainter() { 739 return defaultPainter; 740 } 741 742 @Override 743 public void propertyChange(PropertyChangeEvent evt) { 744 Locale lc = (Locale) evt.getNewValue(); 745 if (lc != null) { 746 rb = WebcamUtils.loadRB(WebcamPanel.class, lc); 747 } 748 } 749 750 @Override 751 public void webcamOpen(WebcamEvent we) { 752 753 // if default size has not been provided, then use the one from webcam 754 // device (this will be current webcam resolution) 755 756 if (defaultSize == null) { 757 setPreferredSize(webcam.getViewSize()); 758 } 759 } 760 761 @Override 762 public void webcamClosed(WebcamEvent we) { 763 stop(); 764 } 765 766 @Override 767 public void webcamDisposed(WebcamEvent we) { 768 stop(); 769 } 770 771 @Override 772 public void webcamImageObtained(WebcamEvent we) { 773 // do nothing 774 } 775}