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