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