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 = 5792962512394656227L; 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 private static final PanelThreadFactory THREAD_FACTORY = new PanelThreadFactory(); 210 211 /** 212 * Scheduled executor acting as timer. 213 */ 214 private ScheduledExecutorService executor = null; 215 216 /** 217 * Image updater reads images from camera and force panel to be repainted. 218 * 219 * @author Bartosz Firyn (SarXos) 220 */ 221 private class ImageUpdater implements Runnable { 222 223 /** 224 * Repainter updates panel when it is being started. 225 * 226 * @author Bartosz Firyn (sarxos) 227 */ 228 private class RepaintScheduler extends Thread { 229 230 public RepaintScheduler() { 231 setUncaughtExceptionHandler(WebcamExceptionHandler.getInstance()); 232 setName(String.format("repaint-scheduler-%s", webcam.getName())); 233 setDaemon(true); 234 } 235 236 @Override 237 public void run() { 238 239 if (!running.get()) { 240 return; 241 } 242 243 repaint(); 244 245 while (starting) { 246 try { 247 Thread.sleep(50); 248 } catch (InterruptedException e) { 249 throw new RuntimeException(e); 250 } 251 } 252 253 if (webcam.isOpen()) { 254 if (isFPSLimited()) { 255 executor.scheduleAtFixedRate(updater, 0, (long) (1000 / frequency), TimeUnit.MILLISECONDS); 256 } else { 257 executor.scheduleWithFixedDelay(updater, 100, 1, TimeUnit.MILLISECONDS); 258 } 259 } else { 260 executor.schedule(this, 500, TimeUnit.MILLISECONDS); 261 } 262 } 263 264 } 265 266 private Thread scheduler = new RepaintScheduler(); 267 268 private AtomicBoolean running = new AtomicBoolean(false); 269 270 public void start() { 271 if (running.compareAndSet(false, true)) { 272 executor = Executors.newScheduledThreadPool(1, THREAD_FACTORY); 273 scheduler.start(); 274 } 275 } 276 277 public void stop() { 278 if (running.compareAndSet(true, false)) { 279 executor.shutdown(); 280 } 281 } 282 283 @Override 284 public void run() { 285 286 if (!running.get()) { 287 return; 288 } 289 290 if (!webcam.isOpen()) { 291 return; 292 } 293 294 if (paused) { 295 return; 296 } 297 298 BufferedImage tmp = null; 299 try { 300 tmp = webcam.getImage(); 301 } catch (Throwable t) { 302 LOG.error("Exception when getting image", t); 303 } 304 305 if (tmp != null) { 306 image = tmp; 307 } 308 309 repaint(); 310 } 311 } 312 313 /** 314 * Resource bundle. 315 */ 316 private ResourceBundle rb = null; 317 318 /** 319 * Fit image into panel area. 320 */ 321 private boolean fillArea = false; 322 323 /** 324 * Frames requesting frequency. 325 */ 326 private double frequency = 5; // FPS 327 328 /** 329 * Is frames requesting frequency limited? If true, images will be fetched 330 * in configured time intervals. If false, images will be fetched as fast as 331 * camera can serve them. 332 */ 333 private boolean frequencyLimit = false; 334 335 /** 336 * Display FPS. 337 */ 338 private boolean frequencyDisplayed = false; 339 340 /** 341 * Webcam object used to fetch images. 342 */ 343 private Webcam webcam = null; 344 345 /** 346 * Image currently being displayed. 347 */ 348 private BufferedImage image = null; 349 350 /** 351 * Repainter is used to fetch images from camera and force panel repaint 352 * when image is ready. 353 */ 354 private volatile ImageUpdater updater = null; 355 356 /** 357 * Webcam is currently starting. 358 */ 359 private volatile boolean starting = false; 360 361 /** 362 * Painting is paused. 363 */ 364 private volatile boolean paused = false; 365 366 /** 367 * Is there any problem with webcam? 368 */ 369 private volatile boolean errored = false; 370 371 /** 372 * Webcam has been started. 373 */ 374 private AtomicBoolean started = new AtomicBoolean(false); 375 376 /** 377 * Painter used to draw image in panel. 378 * 379 * @see #setPainter(Painter) 380 * @see #getPainter() 381 */ 382 private Painter painter = new DefaultPainter(); 383 384 /** 385 * Preferred panel size. 386 */ 387 private Dimension size = null; 388 389 /** 390 * Creates webcam panel and automatically start webcam. 391 * 392 * @param webcam the webcam to be used to fetch images 393 */ 394 public WebcamPanel(Webcam webcam) { 395 this(webcam, true); 396 } 397 398 /** 399 * Creates new webcam panel which display image from camera in you your 400 * Swing application. 401 * 402 * @param webcam the webcam to be used to fetch images 403 * @param start true if webcam shall be automatically started 404 */ 405 public WebcamPanel(Webcam webcam, boolean start) { 406 this(webcam, null, start); 407 } 408 409 /** 410 * Creates new webcam panel which display image from camera in you your 411 * Swing application. If panel size argument is null, then image size will 412 * be used. If you would like to fill panel area with image even if its size 413 * is different, then you can use {@link WebcamPanel#setFillArea(boolean)} 414 * method to configure this. 415 * 416 * @param webcam the webcam to be used to fetch images 417 * @param size the size of panel 418 * @param start true if webcam shall be automatically started 419 * @see WebcamPanel#setFillArea(boolean) 420 */ 421 public WebcamPanel(Webcam webcam, Dimension size, boolean start) { 422 423 if (webcam == null) { 424 throw new IllegalArgumentException(String.format("Webcam argument in %s constructor cannot be null!", getClass().getSimpleName())); 425 } 426 427 this.size = size; 428 this.webcam = webcam; 429 this.webcam.addWebcamListener(this); 430 431 rb = WebcamUtils.loadRB(WebcamPanel.class, getLocale()); 432 433 addPropertyChangeListener("locale", this); 434 435 if (size == null) { 436 Dimension r = webcam.getViewSize(); 437 if (r == null) { 438 r = webcam.getViewSizes()[0]; 439 } 440 setPreferredSize(r); 441 } else { 442 setPreferredSize(size); 443 } 444 445 if (start) { 446 start(); 447 } 448 } 449 450 /** 451 * Set new painter. Painter is a class which pains image visible when 452 * 453 * @param painter the painter object to be set 454 */ 455 public void setPainter(Painter painter) { 456 this.painter = painter; 457 } 458 459 /** 460 * Get painter used to draw image in webcam panel. 461 * 462 * @return Painter object 463 */ 464 public Painter getPainter() { 465 return painter; 466 } 467 468 @Override 469 protected void paintComponent(Graphics g) { 470 Graphics2D g2 = (Graphics2D) g; 471 if (image == null) { 472 painter.paintPanel(this, g2); 473 } else { 474 painter.paintImage(this, image, g2); 475 } 476 } 477 478 @Override 479 public void webcamOpen(WebcamEvent we) { 480 481 // start image updater (i.e. start panel repainting) 482 if (updater == null) { 483 updater = new ImageUpdater(); 484 updater.start(); 485 } 486 487 // copy size from webcam only if default size has not been provided 488 if (size == null) { 489 setPreferredSize(webcam.getViewSize()); 490 } 491 } 492 493 @Override 494 public void webcamClosed(WebcamEvent we) { 495 stop(); 496 } 497 498 @Override 499 public void webcamDisposed(WebcamEvent we) { 500 webcamClosed(we); 501 } 502 503 @Override 504 public void webcamImageObtained(WebcamEvent we) { 505 // do nothing 506 } 507 508 /** 509 * Open webcam and start rendering. 510 */ 511 public void start() { 512 513 if (!started.compareAndSet(false, true)) { 514 return; 515 } 516 517 LOG.debug("Starting panel rendering and trying to open attached webcam"); 518 519 starting = true; 520 521 if (updater == null) { 522 updater = new ImageUpdater(); 523 } 524 525 updater.start(); 526 527 try { 528 errored = !webcam.open(); 529 } catch (WebcamException e) { 530 errored = true; 531 throw e; 532 } finally { 533 starting = false; 534 } 535 } 536 537 /** 538 * Stop rendering and close webcam. 539 */ 540 public void stop() { 541 if (!started.compareAndSet(true, false)) { 542 return; 543 } 544 545 LOG.debug("Stopping panel rendering and closing attached webcam"); 546 547 updater.stop(); 548 updater = null; 549 550 image = null; 551 552 try { 553 errored = !webcam.close(); 554 } catch (WebcamException e) { 555 errored = true; 556 throw e; 557 } 558 } 559 560 /** 561 * Pause rendering. 562 */ 563 public void pause() { 564 if (paused) { 565 return; 566 } 567 568 LOG.debug("Pausing panel rendering"); 569 570 paused = true; 571 } 572 573 /** 574 * Resume rendering. 575 */ 576 public void resume() { 577 578 if (!paused) { 579 return; 580 } 581 582 LOG.debug("Resuming panel rendering"); 583 584 paused = false; 585 } 586 587 /** 588 * Is frequency limit enabled? 589 * 590 * @return True or false 591 */ 592 public boolean isFPSLimited() { 593 return frequencyLimit; 594 } 595 596 /** 597 * Enable or disable frequency limit. Frequency limit should be used for 598 * <b>all IP cameras working in pull mode</b> (to save number of HTTP 599 * requests). If true, images will be fetched in configured time intervals. 600 * If false, images will be fetched as fast as camera can serve them. 601 * 602 * @param frequencyLimit 603 */ 604 public void setFPSLimited(boolean frequencyLimit) { 605 this.frequencyLimit = frequencyLimit; 606 } 607 608 /** 609 * Get rendering frequency in FPS (equivalent to Hz). 610 * 611 * @return Rendering frequency 612 */ 613 public double getFPS() { 614 return frequency; 615 } 616 617 /** 618 * Set rendering frequency (in Hz or FPS). Minimum frequency is 0.016 (1 619 * frame per minute) and maximum is 25 (25 frames per second). 620 * 621 * @param frequency the frequency 622 */ 623 public void setFPS(double frequency) { 624 if (frequency > MAX_FREQUENCY) { 625 frequency = MAX_FREQUENCY; 626 } 627 if (frequency < MIN_FREQUENCY) { 628 frequency = MIN_FREQUENCY; 629 } 630 this.frequency = frequency; 631 } 632 633 public boolean isFPSDisplayed() { 634 return frequencyDisplayed; 635 } 636 637 public void setFPSDisplayed(boolean displayed) { 638 this.frequencyDisplayed = displayed; 639 } 640 641 /** 642 * Is webcam starting. 643 * 644 * @return True if panel is starting 645 */ 646 public boolean isStarting() { 647 return starting; 648 } 649 650 /** 651 * Image will be resized to fill panel area if true. If false then image 652 * will be rendered as it was obtained from webcam instance. 653 * 654 * @param fillArea shall image be resided to fill panel area 655 */ 656 public void setFillArea(boolean fillArea) { 657 this.fillArea = fillArea; 658 } 659 660 /** 661 * Get value of fill area setting. Image will be resized to fill panel area 662 * if true. If false then image will be rendered as it was obtained from 663 * webcam instance. 664 * 665 * @return True if image is being resized, false otherwise 666 */ 667 public boolean isFillArea() { 668 return fillArea; 669 } 670 671 @Override 672 public void propertyChange(PropertyChangeEvent evt) { 673 Locale lc = (Locale) evt.getNewValue(); 674 if (lc != null) { 675 rb = WebcamUtils.loadRB(WebcamPanel.class, lc); 676 } 677 } 678 }