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 }