001package com.github.sarxos.webcam;
002
003import java.awt.AlphaComposite;
004import java.awt.BasicStroke;
005import java.awt.Color;
006import java.awt.Dimension;
007import java.awt.FontMetrics;
008import java.awt.Graphics;
009import java.awt.Graphics2D;
010import java.awt.RenderingHints;
011import java.awt.image.BufferedImage;
012import java.beans.PropertyChangeEvent;
013import java.beans.PropertyChangeListener;
014import java.util.Locale;
015import java.util.ResourceBundle;
016import java.util.concurrent.Executors;
017import java.util.concurrent.ScheduledExecutorService;
018import java.util.concurrent.ThreadFactory;
019import java.util.concurrent.TimeUnit;
020import java.util.concurrent.atomic.AtomicBoolean;
021import java.util.concurrent.atomic.AtomicInteger;
022
023import javax.swing.JPanel;
024import javax.swing.SwingUtilities;
025
026import org.slf4j.Logger;
027import 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 */
036public 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}