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