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    import javax.swing.SwingUtilities;
025    
026    import org.slf4j.Logger;
027    import org.slf4j.LoggerFactory;
028    
029    
030    /**
031     * Simply implementation of JPanel allowing users to render pictures taken with
032     * webcam.
033     * 
034     * @author Bartosz Firyn (SarXos)
035     */
036    public class WebcamPanel extends JPanel implements WebcamListener, PropertyChangeListener {
037    
038            /**
039             * Interface of the painter used to draw image in panel.
040             * 
041             * @author Bartosz Firyn (SarXos)
042             */
043            public static interface Painter {
044    
045                    /**
046                     * Paints panel without image.
047                     * 
048                     * @param g2 the graphics 2D object used for drawing
049                     */
050                    void paintPanel(WebcamPanel panel, Graphics2D g2);
051    
052                    /**
053                     * Paints webcam image in panel.
054                     * 
055                     * @param g2 the graphics 2D object used for drawing
056                     */
057                    void paintImage(WebcamPanel panel, BufferedImage image, Graphics2D g2);
058            }
059    
060            /**
061             * Default painter used to draw image in panel.
062             * 
063             * @author Bartosz Firyn (SarXos)
064             */
065            public class DefaultPainter implements Painter {
066    
067                    private String name = null;
068    
069                    @Override
070                    public void paintPanel(WebcamPanel owner, Graphics2D g2) {
071    
072                            assert owner != null;
073                            assert g2 != null;
074    
075                            g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
076                            g2.setBackground(Color.BLACK);
077                            g2.fillRect(0, 0, getWidth(), getHeight());
078    
079                            int cx = (getWidth() - 70) / 2;
080                            int cy = (getHeight() - 40) / 2;
081    
082                            g2.setStroke(new BasicStroke(2));
083                            g2.setColor(Color.LIGHT_GRAY);
084                            g2.fillRoundRect(cx, cy, 70, 40, 10, 10);
085                            g2.setColor(Color.WHITE);
086                            g2.fillOval(cx + 5, cy + 5, 30, 30);
087                            g2.setColor(Color.LIGHT_GRAY);
088                            g2.fillOval(cx + 10, cy + 10, 20, 20);
089                            g2.setColor(Color.WHITE);
090                            g2.fillOval(cx + 12, cy + 12, 16, 16);
091                            g2.fillRoundRect(cx + 50, cy + 5, 15, 10, 5, 5);
092                            g2.fillRect(cx + 63, cy + 25, 7, 2);
093                            g2.fillRect(cx + 63, cy + 28, 7, 2);
094                            g2.fillRect(cx + 63, cy + 31, 7, 2);
095    
096                            g2.setColor(Color.DARK_GRAY);
097                            g2.setStroke(new BasicStroke(3));
098                            g2.drawLine(0, 0, getWidth(), getHeight());
099                            g2.drawLine(0, getHeight(), getWidth(), 0);
100    
101                            String str = null;
102    
103                            final String strInitDevice = rb.getString("INITIALIZING_DEVICE");
104                            final String strNoImage = rb.getString("NO_IMAGE");
105                            final String strDeviceError = rb.getString("DEVICE_ERROR");
106    
107                            if (!errored) {
108                                    str = starting ? strInitDevice : strNoImage;
109                            } else {
110                                    str = strDeviceError;
111                            }
112    
113                            FontMetrics metrics = g2.getFontMetrics(getFont());
114                            int w = metrics.stringWidth(str);
115                            int h = metrics.getHeight();
116    
117                            int x = (getWidth() - w) / 2;
118                            int y = cy - h;
119    
120                            g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_OFF);
121                            g2.setFont(getFont());
122                            g2.setColor(Color.WHITE);
123                            g2.drawString(str, x, y);
124    
125                            if (name == null) {
126                                    name = webcam.getName();
127                            }
128    
129                            str = name;
130    
131                            w = metrics.stringWidth(str);
132                            h = metrics.getHeight();
133    
134                            g2.drawString(str, (getWidth() - w) / 2, cy - 2 * h);
135                    }
136    
137                    @Override
138                    public void paintImage(WebcamPanel owner, BufferedImage image, Graphics2D g2) {
139    
140                            int w = getWidth();
141                            int h = getHeight();
142    
143                            if (fillArea && image.getWidth() != w && image.getHeight() != h) {
144    
145                                    BufferedImage resized = new BufferedImage(w, h, BufferedImage.TYPE_3BYTE_BGR);
146                                    Graphics2D gr = resized.createGraphics();
147                                    gr.setComposite(AlphaComposite.Src);
148                                    gr.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BILINEAR);
149                                    gr.setRenderingHint(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY);
150                                    gr.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
151                                    gr.drawImage(image, 0, 0, w, h, null);
152                                    gr.dispose();
153                                    resized.flush();
154    
155                                    image = resized;
156                            }
157    
158                            g2.drawImage(image, 0, 0, null);
159    
160                            if (isFPSDisplayed()) {
161    
162                                    String str = String.format("FPS: %.1f", webcam.getFPS());
163    
164                                    int x = 5;
165                                    int y = getHeight() - 5;
166    
167                                    g2.setFont(getFont());
168                                    g2.setColor(Color.BLACK);
169                                    g2.drawString(str, x + 1, y + 1);
170                                    g2.setColor(Color.WHITE);
171                                    g2.drawString(str, x, y);
172                            }
173                    }
174            }
175    
176            private static final class PanelThreadFactory implements ThreadFactory {
177    
178                    private static final AtomicInteger number = new AtomicInteger(0);
179    
180                    @Override
181                    public Thread newThread(Runnable r) {
182                            Thread t = new Thread(r, String.format("webcam-panel-scheduled-executor-%d", number.incrementAndGet()));
183                            t.setUncaughtExceptionHandler(WebcamExceptionHandler.getInstance());
184                            t.setDaemon(true);
185                            return t;
186                    }
187    
188            }
189    
190            /**
191             * This runnable will do nothing more than repaint panel.
192             */
193            private static final class SwingRepainter implements Runnable {
194    
195                    private WebcamPanel panel = null;
196    
197                    public SwingRepainter(WebcamPanel panel) {
198                            this.panel = panel;
199                    }
200    
201                    @Override
202                    public void run() {
203                            panel.repaint();
204                    }
205            }
206    
207            /**
208             * S/N used by Java to serialize beans.
209             */
210            private static final long serialVersionUID = 1L;
211    
212            /**
213             * Logger.
214             */
215            private static final Logger LOG = LoggerFactory.getLogger(WebcamPanel.class);
216    
217            /**
218             * Minimum FPS frequency.
219             */
220            public static final double MIN_FREQUENCY = 0.016; // 1 frame per minute
221    
222            /**
223             * Maximum FPS frequency.
224             */
225            private static final double MAX_FREQUENCY = 50; // 50 frames per second
226    
227            /**
228             * Thread factory used by execution service.
229             */
230            private static final ThreadFactory THREAD_FACTORY = new PanelThreadFactory();
231    
232            /**
233             * This runnable will do nothing more than repaint panel.
234             */
235            private final Runnable repaint = new SwingRepainter(this);
236    
237            /**
238             * Scheduled executor acting as timer.
239             */
240            private ScheduledExecutorService executor = null;
241    
242            /**
243             * Image updater reads images from camera and force panel to be repainted.
244             * 
245             * @author Bartosz Firyn (SarXos)
246             */
247            private class ImageUpdater implements Runnable {
248    
249                    /**
250                     * Repaint scheduler schedule panel updates.
251                     * 
252                     * @author Bartosz Firyn (sarxos)
253                     */
254                    private class RepaintScheduler extends Thread {
255    
256                            /**
257                             * Repaint scheduler schedule panel updates.
258                             */
259                            public RepaintScheduler() {
260                                    setUncaughtExceptionHandler(WebcamExceptionHandler.getInstance());
261                                    setName(String.format("repaint-scheduler-%s", webcam.getName()));
262                                    setDaemon(true);
263                            }
264    
265                            @Override
266                            public void run() {
267    
268                                    // do nothing when not running
269                                    if (!running.get()) {
270                                            return;
271                                    }
272    
273                                    repaintPanel();
274    
275                                    // loop when starting, to wait for images
276                                    while (starting) {
277                                            try {
278                                                    Thread.sleep(50);
279                                            } catch (InterruptedException e) {
280                                                    throw new RuntimeException(e);
281                                            }
282                                    }
283    
284                                    // schedule update when webcam is open, otherwise schedule
285                                    // second scheduler execution
286    
287                                    if (webcam.isOpen()) {
288    
289                                            // FPS limit means that panel rendering frequency is limited
290                                            // to the specific value and panel will not be rendered more
291                                            // often then specific value
292    
293                                            // TODO: rename FPS value in panel to rendering frequency
294    
295                                            if (isFPSLimited()) {
296                                                    executor.scheduleAtFixedRate(updater, 0, (long) (1000 / frequency), TimeUnit.MILLISECONDS);
297                                            } else {
298                                                    executor.scheduleWithFixedDelay(updater, 100, 1, TimeUnit.MILLISECONDS);
299                                            }
300                                    } else {
301                                            executor.schedule(this, 500, TimeUnit.MILLISECONDS);
302                                    }
303                            }
304    
305                    }
306    
307                    /**
308                     * Update scheduler thread.
309                     */
310                    private Thread scheduler = new RepaintScheduler();
311    
312                    /**
313                     * Is repainter running?
314                     */
315                    private AtomicBoolean running = new AtomicBoolean(false);
316    
317                    /**
318                     * Start repainter. Can be invoked many times, but only first call will
319                     * take effect.
320                     */
321                    public void start() {
322                            if (running.compareAndSet(false, true)) {
323                                    executor = Executors.newScheduledThreadPool(1, THREAD_FACTORY);
324                                    scheduler.start();
325                            }
326                    }
327    
328                    /**
329                     * Stop repainter. Can be invoked many times, but only first call will
330                     * take effect.
331                     * 
332                     * @throws InterruptedException
333                     */
334                    public void stop() throws InterruptedException {
335                            if (running.compareAndSet(true, false)) {
336                                    executor.shutdown();
337                                    executor.awaitTermination(5000, TimeUnit.MILLISECONDS);
338                                    scheduler.join();
339                            }
340                    }
341    
342                    @Override
343                    public void run() {
344                            try {
345                                    update();
346                            } catch (Throwable t) {
347                                    errored = true;
348                                    WebcamExceptionHandler.handle(t);
349                            }
350                    }
351    
352                    /**
353                     * Perform single panel area update (repaint newly obtained image).
354                     */
355                    private void update() {
356    
357                            // do nothing when updater not running, when webcam is closed, or
358                            // panel repainting is paused
359    
360                            if (!running.get() || !webcam.isOpen() || paused) {
361                                    return;
362                            }
363    
364                            // get new image from webcam
365    
366                            BufferedImage tmp = webcam.getImage();
367                            if (tmp != null) {
368                                    errored = false;
369                                    image = tmp;
370                            }
371    
372                            // and repaint it
373    
374                            repaintPanel();
375                    }
376            }
377    
378            /**
379             * Resource bundle.
380             */
381            private ResourceBundle rb = null;
382    
383            /**
384             * Fit image into panel area.
385             */
386            private boolean fillArea = false;
387    
388            /**
389             * Frames requesting frequency.
390             */
391            private double frequency = 5; // FPS
392    
393            /**
394             * Is frames requesting frequency limited? If true, images will be fetched
395             * in configured time intervals. If false, images will be fetched as fast as
396             * camera can serve them.
397             */
398            private boolean frequencyLimit = false;
399    
400            /**
401             * Display FPS.
402             */
403            private boolean frequencyDisplayed = false;
404    
405            /**
406             * Webcam object used to fetch images.
407             */
408            private final Webcam webcam;
409    
410            /**
411             * Repainter is used to fetch images from camera and force panel repaint
412             * when image is ready.
413             */
414            private final ImageUpdater updater;
415    
416            /**
417             * Image currently being displayed.
418             */
419            private BufferedImage image = null;
420    
421            /**
422             * Webcam is currently starting.
423             */
424            private volatile boolean starting = false;
425    
426            /**
427             * Painting is paused.
428             */
429            private volatile boolean paused = false;
430    
431            /**
432             * Is there any problem with webcam?
433             */
434            private volatile boolean errored = false;
435    
436            /**
437             * Webcam has been started.
438             */
439            private final AtomicBoolean started = new AtomicBoolean(false);
440    
441            /**
442             * Default painter.
443             */
444            private final Painter defaultPainter = new DefaultPainter();
445    
446            /**
447             * Painter used to draw image in panel.
448             * 
449             * @see #setPainter(Painter)
450             * @see #getPainter()
451             */
452            private Painter painter = defaultPainter;
453    
454            /**
455             * Preferred panel size.
456             */
457            private Dimension defaultSize = null;
458    
459            /**
460             * Creates webcam panel and automatically start webcam.
461             * 
462             * @param webcam the webcam to be used to fetch images
463             */
464            public WebcamPanel(Webcam webcam) {
465                    this(webcam, true);
466            }
467    
468            /**
469             * Creates new webcam panel which display image from camera in you your
470             * Swing application.
471             * 
472             * @param webcam the webcam to be used to fetch images
473             * @param start true if webcam shall be automatically started
474             */
475            public WebcamPanel(Webcam webcam, boolean start) {
476                    this(webcam, null, start);
477            }
478    
479            /**
480             * Creates new webcam panel which display image from camera in you your
481             * Swing application. If panel size argument is null, then image size will
482             * be used. If you would like to fill panel area with image even if its size
483             * is different, then you can use {@link WebcamPanel#setFillArea(boolean)}
484             * method to configure this.
485             * 
486             * @param webcam the webcam to be used to fetch images
487             * @param size the size of panel
488             * @param start true if webcam shall be automatically started
489             * @see WebcamPanel#setFillArea(boolean)
490             */
491            public WebcamPanel(Webcam webcam, Dimension size, boolean start) {
492    
493                    if (webcam == null) {
494                            throw new IllegalArgumentException(String.format("Webcam argument in %s constructor cannot be null!", getClass().getSimpleName()));
495                    }
496    
497                    this.defaultSize = size;
498                    this.webcam = webcam;
499                    this.updater = new ImageUpdater();
500                    this.rb = WebcamUtils.loadRB(WebcamPanel.class, getLocale());
501    
502                    addPropertyChangeListener("locale", this);
503    
504                    if (size == null) {
505                            Dimension r = webcam.getViewSize();
506                            if (r == null) {
507                                    r = webcam.getViewSizes()[0];
508                            }
509                            setPreferredSize(r);
510                    } else {
511                            setPreferredSize(size);
512                    }
513    
514                    if (start) {
515                            start();
516                    }
517            }
518    
519            /**
520             * Set new painter. Painter is a class which pains image visible when
521             * 
522             * @param painter the painter object to be set
523             */
524            public void setPainter(Painter painter) {
525                    this.painter = painter;
526            }
527    
528            /**
529             * Get painter used to draw image in webcam panel.
530             * 
531             * @return Painter object
532             */
533            public Painter getPainter() {
534                    return painter;
535            }
536    
537            @Override
538            protected void paintComponent(Graphics g) {
539                    Graphics2D g2 = (Graphics2D) g;
540                    if (image == null) {
541                            painter.paintPanel(this, g2);
542                    } else {
543                            painter.paintImage(this, image, g2);
544                    }
545            }
546    
547            /**
548             * Open webcam and start rendering.
549             */
550            public void start() {
551    
552                    if (!started.compareAndSet(false, true)) {
553                            return;
554                    }
555    
556                    webcam.addWebcamListener(this);
557    
558                    LOG.debug("Starting panel rendering and trying to open attached webcam");
559    
560                    updater.start();
561    
562                    starting = true;
563    
564                    try {
565                            if (!webcam.isOpen()) {
566                                    errored = !webcam.open();
567                            }
568                    } catch (WebcamException e) {
569                            errored = true;
570                            throw e;
571                    } finally {
572                            starting = false;
573                            repaintPanel();
574                    }
575    
576            }
577    
578            /**
579             * Stop rendering and close webcam.
580             */
581            public void stop() {
582    
583                    if (!started.compareAndSet(true, false)) {
584                            return;
585                    }
586    
587                    webcam.removeWebcamListener(this);
588    
589                    LOG.debug("Stopping panel rendering and closing attached webcam");
590    
591                    try {
592                            updater.stop();
593                    } catch (InterruptedException e) {
594                            throw new RuntimeException(e);
595                    }
596    
597                    image = null;
598    
599                    try {
600                            if (webcam.isOpen()) {
601                                    errored = !webcam.close();
602                            }
603                    } catch (WebcamException e) {
604                            errored = true;
605                            throw e;
606                    } finally {
607                            repaintPanel();
608                    }
609            }
610    
611            /**
612             * Repaint panel in Swing asynchronous manner.
613             */
614            private void repaintPanel() {
615                    SwingUtilities.invokeLater(repaint);
616            }
617    
618            /**
619             * Pause rendering.
620             */
621            public void pause() {
622                    if (paused) {
623                            return;
624                    }
625    
626                    LOG.debug("Pausing panel rendering");
627    
628                    paused = true;
629            }
630    
631            /**
632             * Resume rendering.
633             */
634            public void resume() {
635    
636                    if (!paused) {
637                            return;
638                    }
639    
640                    LOG.debug("Resuming panel rendering");
641    
642                    paused = false;
643            }
644    
645            /**
646             * Is frequency limit enabled?
647             * 
648             * @return True or false
649             */
650            public boolean isFPSLimited() {
651                    return frequencyLimit;
652            }
653    
654            /**
655             * Enable or disable frequency limit. Frequency limit should be used for
656             * <b>all IP cameras working in pull mode</b> (to save number of HTTP
657             * requests). If true, images will be fetched in configured time intervals.
658             * If false, images will be fetched as fast as camera can serve them.
659             * 
660             * @param frequencyLimit
661             */
662            public void setFPSLimited(boolean frequencyLimit) {
663                    this.frequencyLimit = frequencyLimit;
664            }
665    
666            /**
667             * Get rendering frequency in FPS (equivalent to Hz).
668             * 
669             * @return Rendering frequency
670             */
671            public double getFPSLimit() {
672                    return frequency;
673            }
674    
675            /**
676             * Set rendering frequency (in Hz or FPS). Minimum frequency is 0.016 (1
677             * frame per minute) and maximum is 25 (25 frames per second).
678             * 
679             * @param fps the frequency
680             */
681            public void setFPSLimit(double fps) {
682                    if (fps > MAX_FREQUENCY) {
683                            fps = MAX_FREQUENCY;
684                    }
685                    if (fps < MIN_FREQUENCY) {
686                            fps = MIN_FREQUENCY;
687                    }
688                    this.frequency = fps;
689            }
690    
691            public boolean isFPSDisplayed() {
692                    return frequencyDisplayed;
693            }
694    
695            public void setFPSDisplayed(boolean displayed) {
696                    this.frequencyDisplayed = displayed;
697            }
698    
699            /**
700             * Is webcam panel repainting starting.
701             * 
702             * @return True if panel is starting
703             */
704            public boolean isStarting() {
705                    return starting;
706            }
707    
708            /**
709             * Is webcam panel repainting started.
710             * 
711             * @return True if panel repainting has been started
712             */
713            public boolean isStarted() {
714                    return started.get();
715            }
716    
717            /**
718             * Image will be resized to fill panel area if true. If false then image
719             * will be rendered as it was obtained from webcam instance.
720             * 
721             * @param fillArea shall image be resided to fill panel area
722             */
723            public void setFillArea(boolean fillArea) {
724                    this.fillArea = fillArea;
725            }
726    
727            /**
728             * Get value of fill area setting. Image will be resized to fill panel area
729             * if true. If false then image will be rendered as it was obtained from
730             * webcam instance.
731             * 
732             * @return True if image is being resized, false otherwise
733             */
734            public boolean isFillArea() {
735                    return fillArea;
736            }
737    
738            public Painter getDefaultPainter() {
739                    return defaultPainter;
740            }
741    
742            @Override
743            public void propertyChange(PropertyChangeEvent evt) {
744                    Locale lc = (Locale) evt.getNewValue();
745                    if (lc != null) {
746                            rb = WebcamUtils.loadRB(WebcamPanel.class, lc);
747                    }
748            }
749    
750            @Override
751            public void webcamOpen(WebcamEvent we) {
752    
753                    // if default size has not been provided, then use the one from webcam
754                    // device (this will be current webcam resolution)
755    
756                    if (defaultSize == null) {
757                            setPreferredSize(webcam.getViewSize());
758                    }
759            }
760    
761            @Override
762            public void webcamClosed(WebcamEvent we) {
763                    stop();
764            }
765    
766            @Override
767            public void webcamDisposed(WebcamEvent we) {
768                    stop();
769            }
770    
771            @Override
772            public void webcamImageObtained(WebcamEvent we) {
773                    // do nothing
774            }
775    }