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