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