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