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    }