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 }