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                            try {
289                                    update();
290                            } catch (Throwable t) {
291                                    errored = true;
292                                    WebcamExceptionHandler.handle(t);
293                            }
294                    }
295    
296                    private void update() {
297    
298                            if (!running.get() || !webcam.isOpen() || paused) {
299                                    return;
300                            }
301    
302                            BufferedImage tmp = webcam.getImage();
303                            if (tmp != null) {
304                                    errored = false;
305                                    image = tmp;
306                            }
307    
308                            repaint();
309                    }
310            }
311    
312            /**
313             * Resource bundle.
314             */
315            private ResourceBundle rb = null;
316    
317            /**
318             * Fit image into panel area.
319             */
320            private boolean fillArea = false;
321    
322            /**
323             * Frames requesting frequency.
324             */
325            private double frequency = 5; // FPS
326    
327            /**
328             * Is frames requesting frequency limited? If true, images will be fetched
329             * in configured time intervals. If false, images will be fetched as fast as
330             * camera can serve them.
331             */
332            private boolean frequencyLimit = false;
333    
334            /**
335             * Display FPS.
336             */
337            private boolean frequencyDisplayed = false;
338    
339            /**
340             * Webcam object used to fetch images.
341             */
342            private Webcam webcam = null;
343    
344            /**
345             * Image currently being displayed.
346             */
347            private BufferedImage image = null;
348    
349            /**
350             * Repainter is used to fetch images from camera and force panel repaint
351             * when image is ready.
352             */
353            private volatile ImageUpdater updater = null;
354    
355            /**
356             * Webcam is currently starting.
357             */
358            private volatile boolean starting = false;
359    
360            /**
361             * Painting is paused.
362             */
363            private volatile boolean paused = false;
364    
365            /**
366             * Is there any problem with webcam?
367             */
368            private volatile boolean errored = false;
369    
370            /**
371             * Webcam has been started.
372             */
373            private AtomicBoolean started = new AtomicBoolean(false);
374    
375            private Painter defaultPainter = new DefaultPainter();
376    
377            /**
378             * Painter used to draw image in panel.
379             * 
380             * @see #setPainter(Painter)
381             * @see #getPainter()
382             */
383            private Painter painter = defaultPainter;
384    
385            /**
386             * Preferred panel size.
387             */
388            private Dimension size = null;
389    
390            /**
391             * Creates webcam panel and automatically start webcam.
392             * 
393             * @param webcam the webcam to be used to fetch images
394             */
395            public WebcamPanel(Webcam webcam) {
396                    this(webcam, true);
397            }
398    
399            /**
400             * Creates new webcam panel which display image from camera in you your
401             * Swing application.
402             * 
403             * @param webcam the webcam to be used to fetch images
404             * @param start true if webcam shall be automatically started
405             */
406            public WebcamPanel(Webcam webcam, boolean start) {
407                    this(webcam, null, start);
408            }
409    
410            /**
411             * Creates new webcam panel which display image from camera in you your
412             * Swing application. If panel size argument is null, then image size will
413             * be used. If you would like to fill panel area with image even if its size
414             * is different, then you can use {@link WebcamPanel#setFillArea(boolean)}
415             * method to configure this.
416             * 
417             * @param webcam the webcam to be used to fetch images
418             * @param size the size of panel
419             * @param start true if webcam shall be automatically started
420             * @see WebcamPanel#setFillArea(boolean)
421             */
422            public WebcamPanel(Webcam webcam, Dimension size, boolean start) {
423    
424                    if (webcam == null) {
425                            throw new IllegalArgumentException(String.format("Webcam argument in %s constructor cannot be null!", getClass().getSimpleName()));
426                    }
427    
428                    this.size = size;
429                    this.webcam = webcam;
430                    this.webcam.addWebcamListener(this);
431    
432                    rb = WebcamUtils.loadRB(WebcamPanel.class, getLocale());
433    
434                    addPropertyChangeListener("locale", this);
435    
436                    if (size == null) {
437                            Dimension r = webcam.getViewSize();
438                            if (r == null) {
439                                    r = webcam.getViewSizes()[0];
440                            }
441                            setPreferredSize(r);
442                    } else {
443                            setPreferredSize(size);
444                    }
445    
446                    if (start) {
447                            start();
448                    }
449            }
450    
451            /**
452             * Set new painter. Painter is a class which pains image visible when
453             * 
454             * @param painter the painter object to be set
455             */
456            public void setPainter(Painter painter) {
457                    this.painter = painter;
458            }
459    
460            /**
461             * Get painter used to draw image in webcam panel.
462             * 
463             * @return Painter object
464             */
465            public Painter getPainter() {
466                    return painter;
467            }
468    
469            @Override
470            protected void paintComponent(Graphics g) {
471                    Graphics2D g2 = (Graphics2D) g;
472                    if (image == null) {
473                            painter.paintPanel(this, g2);
474                    } else {
475                            painter.paintImage(this, image, g2);
476                    }
477            }
478    
479            @Override
480            public void webcamOpen(WebcamEvent we) {
481    
482                    // start image updater (i.e. start panel repainting)
483                    if (updater == null) {
484                            updater = new ImageUpdater();
485                            updater.start();
486                    }
487    
488                    // copy size from webcam only if default size has not been provided
489                    if (size == null) {
490                            setPreferredSize(webcam.getViewSize());
491                    }
492            }
493    
494            @Override
495            public void webcamClosed(WebcamEvent we) {
496                    stop();
497            }
498    
499            @Override
500            public void webcamDisposed(WebcamEvent we) {
501                    webcamClosed(we);
502            }
503    
504            @Override
505            public void webcamImageObtained(WebcamEvent we) {
506                    // do nothing
507            }
508    
509            /**
510             * Open webcam and start rendering.
511             */
512            public void start() {
513    
514                    if (!started.compareAndSet(false, true)) {
515                            return;
516                    }
517    
518                    LOG.debug("Starting panel rendering and trying to open attached webcam");
519    
520                    starting = true;
521    
522                    if (updater == null) {
523                            updater = new ImageUpdater();
524                    }
525    
526                    updater.start();
527    
528                    try {
529                            errored = !webcam.open();
530                    } catch (WebcamException e) {
531                            errored = true;
532                            repaint();
533                            throw e;
534                    } finally {
535                            starting = false;
536                    }
537            }
538    
539            /**
540             * Stop rendering and close webcam.
541             */
542            public void stop() {
543    
544                    if (!started.compareAndSet(true, false)) {
545                            return;
546                    }
547    
548                    LOG.debug("Stopping panel rendering and closing attached webcam");
549    
550                    updater.stop();
551                    updater = null;
552    
553                    image = null;
554    
555                    try {
556                            errored = !webcam.close();
557                    } catch (WebcamException e) {
558                            errored = true;
559                            repaint();
560                            throw e;
561                    }
562            }
563    
564            /**
565             * Pause rendering.
566             */
567            public void pause() {
568                    if (paused) {
569                            return;
570                    }
571    
572                    LOG.debug("Pausing panel rendering");
573    
574                    paused = true;
575            }
576    
577            /**
578             * Resume rendering.
579             */
580            public void resume() {
581    
582                    if (!paused) {
583                            return;
584                    }
585    
586                    LOG.debug("Resuming panel rendering");
587    
588                    paused = false;
589            }
590    
591            /**
592             * Is frequency limit enabled?
593             * 
594             * @return True or false
595             */
596            public boolean isFPSLimited() {
597                    return frequencyLimit;
598            }
599    
600            /**
601             * Enable or disable frequency limit. Frequency limit should be used for
602             * <b>all IP cameras working in pull mode</b> (to save number of HTTP
603             * requests). If true, images will be fetched in configured time intervals.
604             * If false, images will be fetched as fast as camera can serve them.
605             * 
606             * @param frequencyLimit
607             */
608            public void setFPSLimited(boolean frequencyLimit) {
609                    this.frequencyLimit = frequencyLimit;
610            }
611    
612            /**
613             * Get rendering frequency in FPS (equivalent to Hz).
614             * 
615             * @return Rendering frequency
616             */
617            public double getFPSLimit() {
618                    return frequency;
619            }
620    
621            /**
622             * Set rendering frequency (in Hz or FPS). Minimum frequency is 0.016 (1
623             * frame per minute) and maximum is 25 (25 frames per second).
624             * 
625             * @param fps the frequency
626             */
627            public void setFPSLimit(double fps) {
628                    if (fps > MAX_FREQUENCY) {
629                            fps = MAX_FREQUENCY;
630                    }
631                    if (fps < MIN_FREQUENCY) {
632                            fps = MIN_FREQUENCY;
633                    }
634                    this.frequency = fps;
635            }
636    
637            public boolean isFPSDisplayed() {
638                    return frequencyDisplayed;
639            }
640    
641            public void setFPSDisplayed(boolean displayed) {
642                    this.frequencyDisplayed = displayed;
643            }
644    
645            /**
646             * Is webcam panel repainting starting.
647             * 
648             * @return True if panel is starting
649             */
650            public boolean isStarting() {
651                    return starting;
652            }
653    
654            /**
655             * Is webcam panel repainting started.
656             * 
657             * @return True if panel repainting has been started
658             */
659            public boolean isStarted() {
660                    return started.get();
661            }
662    
663            /**
664             * Image will be resized to fill panel area if true. If false then image
665             * will be rendered as it was obtained from webcam instance.
666             * 
667             * @param fillArea shall image be resided to fill panel area
668             */
669            public void setFillArea(boolean fillArea) {
670                    this.fillArea = fillArea;
671            }
672    
673            /**
674             * Get value of fill area setting. Image will be resized to fill panel area
675             * if true. If false then image will be rendered as it was obtained from
676             * webcam instance.
677             * 
678             * @return True if image is being resized, false otherwise
679             */
680            public boolean isFillArea() {
681                    return fillArea;
682            }
683    
684            @Override
685            public void propertyChange(PropertyChangeEvent evt) {
686                    Locale lc = (Locale) evt.getNewValue();
687                    if (lc != null) {
688                            rb = WebcamUtils.loadRB(WebcamPanel.class, lc);
689                    }
690            }
691    
692            public Painter getDefaultPainter() {
693                    return defaultPainter;
694            }
695    }