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 import javax.swing.SwingUtilities;
025
026 import org.slf4j.Logger;
027 import 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 */
036 public 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 }