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