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 }