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