001/**
002 * Copyright (c) 2011, The University of Southampton and the individual contributors.
003 * All rights reserved.
004 *
005 * Redistribution and use in source and binary forms, with or without modification,
006 * are permitted provided that the following conditions are met:
007 *
008 *   *  Redistributions of source code must retain the above copyright notice,
009 *      this list of conditions and the following disclaimer.
010 *
011 *   *  Redistributions in binary form must reproduce the above copyright notice,
012 *      this list of conditions and the following disclaimer in the documentation
013 *      and/or other materials provided with the distribution.
014 *
015 *   *  Neither the name of the University of Southampton nor the names of its
016 *      contributors may be used to endorse or promote products derived from this
017 *      software without specific prior written permission.
018 *
019 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
020 * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
021 * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
022 * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
023 * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
024 * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
025 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
026 * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
027 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
028 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
029 */
030package org.openimaj.video;
031
032import java.awt.Color;
033import java.awt.Dimension;
034import java.awt.Graphics;
035import java.awt.GridBagConstraints;
036import java.awt.GridBagLayout;
037import java.awt.Insets;
038import java.awt.event.MouseAdapter;
039import java.awt.event.MouseEvent;
040import java.awt.image.BufferedImage;
041import java.io.IOException;
042import java.lang.reflect.InvocationTargetException;
043import java.lang.reflect.Method;
044import java.util.ArrayList;
045import java.util.HashMap;
046import java.util.Map;
047
048import javax.imageio.ImageIO;
049import javax.swing.BorderFactory;
050import javax.swing.ImageIcon;
051import javax.swing.JFrame;
052import javax.swing.JLabel;
053import javax.swing.JPanel;
054import javax.swing.JProgressBar;
055
056import org.openimaj.audio.AudioStream;
057import org.openimaj.content.animation.animator.LinearTimeBasedIntegerValueAnimator;
058import org.openimaj.image.DisplayUtilities.ImageComponent;
059import org.openimaj.image.Image;
060import org.openimaj.video.timecode.HrsMinSecFrameTimecode;
061
062/**
063 * This class is an extension of the {@link VideoDisplay} class that provides
064 * GUI elements for starting, stopping, pausing and rewinding video.
065 * <p>
066 * The class relies on the underlying {@link VideoDisplay} to actually provide
067 * the main functionality for video playing and indeed still allows its methods
068 * to be used. This class then provides a simple API for starting, pausing and
069 * stopping video.
070 * <p>
071 * Unlike {@link VideoDisplay}, the VideoPlayer class does not create a frame
072 * when the {@link #createVideoPlayer(Video)} methods are called. Use the
073 * {@link #showFrame()} method to produce a visible frame.
074 *
075 * @author David Dupplaw (dpd@ecs.soton.ac.uk)
076 * @created 10 Aug 2012
077 * @version $Author$, $Revision$, $Date$
078 * @param <T>
079 *            The type of the video frame
080 */
081public class VideoPlayer<T extends Image<?, T>> extends VideoDisplay<T>
082                implements
083                        VideoDisplayStateListener
084{
085        /**
086         * The video player components encapsulates the buttons and their
087         * functionalities, as well as animating buttons, etc.
088         *
089         * @author David Dupplaw (dpd@ecs.soton.ac.uk)
090         * @created 10 Aug 2012
091         * @version $Author$, $Revision$, $Date$
092         */
093        protected class VideoPlayerComponent extends JPanel {
094                /** */
095                private static final long serialVersionUID = 1L;
096
097                /**
098                 * This class represents the widgets in the video player
099                 *
100                 * @author David Dupplaw (dpd@ecs.soton.ac.uk)
101                 * @created 10 Aug 2012
102                 * @version $Author$, $Revision$, $Date$
103                 */
104                protected class ButtonsPanel extends JPanel implements VideoDisplayListener<T> {
105                        /** */
106                        private static final long serialVersionUID = 1L;
107
108                        /* The graphic for the play button */
109                        private final static String PLAY = "/play.png";
110                        private final static String STOP = "/stop.png";
111                        private final static String PAUSE = "/pause.png";
112                        private final static String STEP_BACK = "/step-backward.png";
113                        private final static String STEP_FORWARD = "/step-forward.png";
114
115                        /** A map that makes it easier to replace buttons */
116                        private final Map<String, String> buttonsMap = new HashMap<String, String>();
117
118                        /** The default list of buttons in order of their display */
119                        private String[] buttons = null;
120
121                        /** The methods to use for each of the buttons */
122                        private Method[] methods = null;
123
124                        /** Insets */
125                        private final int inset = 2;
126
127                        /** Progress bar */
128                        private final JProgressBar progress = new JProgressBar(0, 100);
129
130                        /** The background image */
131                        private BufferedImage img = null;
132
133                        /** Label showing the current position */
134                        private final JLabel label = new JLabel("0:00:00/0:00:00");
135
136                        /**
137                         * Construct a new buttons panel
138                         */
139                        public ButtonsPanel() {
140                                // We will only allow these methods to be called
141                                this.buttonsMap.put("play", ButtonsPanel.PLAY);
142                                this.buttonsMap.put("stop", ButtonsPanel.STOP);
143                                this.buttonsMap.put("pause", ButtonsPanel.PAUSE);
144                                this.buttonsMap.put("stepBack", ButtonsPanel.STEP_BACK);
145                                this.buttonsMap.put("stepForward", ButtonsPanel.STEP_FORWARD);
146
147                                try {
148                                        this.img = ImageIO.read(this.getClass().getResource(
149                                                        "/brushed-metal.png"));
150                                } catch (final IOException e) {
151                                        e.printStackTrace();
152                                }
153
154                                // Set up the methods list (calls init())
155                                this.setButtons(new String[] { "pause", "play", "stop" });
156
157                                this.setPreferredSize(new Dimension(
158                                                (100 + this.inset) * this.buttons.length,
159                                                100 + this.inset));
160                                this.setSize(this.getPreferredSize());
161
162                                VideoPlayer.this.addVideoListener(this);
163
164                        }
165
166                        /**
167                         * Set the list of buttons available on the player. The array of
168                         * strings should match the names of methods in the
169                         * {@link VideoPlayer} class for navigating the video. That is
170                         * {@link VideoPlayer#pause()}, {@link VideoPlayer#stop()},
171                         * {@link VideoPlayer#play()}, {@link VideoPlayer#stepBack()} or
172                         * {@link VideoPlayer#stepForward()}. The order specifies the order
173                         * they will be shown in the player.
174                         *
175                         * @param buttons
176                         *            The order of the buttons
177                         */
178                        public void setButtons(final String[] buttons) {
179                                this.buttons = buttons;
180
181                                final ArrayList<Method> methodsList = new ArrayList<Method>();
182                                for (final String button : buttons) {
183                                        // Make sure we're only allowing the methods predetermined
184                                        // by us, so not any old method could be put in.
185                                        if (this.buttonsMap.get(button) != null) {
186                                                try {
187                                                        methodsList.add(VideoPlayer.this.getClass().getMethod(button));
188                                                } catch (final SecurityException e) {
189                                                        e.printStackTrace();
190                                                } catch (final NoSuchMethodException e) {
191                                                        e.printStackTrace();
192                                                }
193                                        }
194                                }
195
196                                this.methods = methodsList.toArray(new Method[0]);
197                                this.init();
198                        }
199
200                        /**
201                         *
202                         */
203                        private void init() {
204                                this.removeAll();
205                                this.setLayout(new GridBagLayout());
206                                this.setOpaque(false);
207
208                                final GridBagConstraints gbc = new GridBagConstraints();
209                                gbc.fill = GridBagConstraints.HORIZONTAL;
210                                gbc.weightx = gbc.weighty = 0;
211                                gbc.gridx = gbc.gridy = 1;
212                                gbc.insets = new Insets(this.inset, this.inset, this.inset, this.inset);
213
214                                // ------------------------------------------------------------
215                                // Progress bar
216                                // ------------------------------------------------------------
217                                gbc.gridy = 0;
218                                gbc.weightx = 1;
219                                gbc.gridwidth = this.buttons.length;
220                                this.add(this.progress, gbc);
221                                this.progress.addMouseListener(new MouseAdapter() {
222                                        @Override
223                                        public void mouseClicked(final MouseEvent e) {
224                                                System.out.println("Clicked at " + e.getX());
225                                                VideoPlayer.this.setPosition(e.getX() * 100 /
226                                                                ButtonsPanel.this.getWidth());
227                                        }
228                                });
229
230                                // ------------------------------------------------------------
231                                // Navigation Buttons
232                                // ------------------------------------------------------------
233                                final JPanel buttonsPanel = new JPanel(new GridBagLayout());
234                                buttonsPanel.setBorder(BorderFactory.createEmptyBorder());
235                                buttonsPanel.setOpaque(false);
236
237                                gbc.weightx = gbc.weighty = 0;
238                                gbc.gridx = gbc.gridy = 1;
239                                gbc.gridwidth = 1;
240                                for (int i = 0; i < this.buttons.length; i++) {
241                                        final String b = this.buttons[i];
242                                        final ImageIcon buttonIcon = new ImageIcon(this.getClass()
243                                                        .getResource(this.buttonsMap.get(b)));
244                                        final JLabel button = new JLabel(buttonIcon);
245                                        button.setBorder(BorderFactory.createEmptyBorder());
246                                        final int j = i;
247                                        button.addMouseListener(new MouseAdapter() {
248                                                @Override
249                                                public void mouseClicked(final MouseEvent e) {
250                                                        try {
251                                                                ButtonsPanel.this.methods[j].invoke(
252                                                                                VideoPlayer.this);
253                                                        } catch (final IllegalArgumentException e1) {
254                                                                e1.printStackTrace();
255                                                        } catch (final IllegalAccessException e1) {
256                                                                e1.printStackTrace();
257                                                        } catch (final InvocationTargetException e1) {
258                                                                e1.printStackTrace();
259                                                        }
260                                                };
261
262                                                @Override
263                                                public void mouseEntered(final MouseEvent e) {
264                                                        button.setBorder(BorderFactory.createLineBorder(Color.yellow));
265                                                };
266
267                                                @Override
268                                                public void mouseExited(final MouseEvent e) {
269                                                        button.setBorder(BorderFactory.createEmptyBorder());
270                                                };
271                                        });
272                                        buttonsPanel.add(button, gbc);
273                                        gbc.gridx++;
274                                }
275                                buttonsPanel.add(this.label, gbc);
276
277                                gbc.gridy = 2;
278                                gbc.gridx = 1;
279                                this.add(buttonsPanel, gbc);
280                        }
281
282                        @Override
283                        public void paint(final Graphics g) {
284                                g.drawImage(this.img, 0, 0, null);
285                                super.paint(g);
286                        }
287
288                        /**
289                         * Set the progress (0-100)
290                         *
291                         * @param pc
292                         *            The %age value
293                         */
294                        public void setProgress(final double pc) {
295                                this.progress.setValue((int) pc);
296                        }
297
298                        @Override
299                        public void afterUpdate(final VideoDisplay<T> display) {
300                                this.setProgress(display.getPosition());
301
302                                // The end timecode
303                                final HrsMinSecFrameTimecode end = new HrsMinSecFrameTimecode(
304                                                VideoPlayer.this.getVideo().countFrames(),
305                                                VideoPlayer.this.getVideo().getFPS());
306
307                                final HrsMinSecFrameTimecode current = new HrsMinSecFrameTimecode(
308                                                VideoPlayer.this.getVideo().currentFrame,
309                                                VideoPlayer.this.getVideo().getFPS());
310
311                                this.label.setText(current.toString() + " / " + end.toString());
312                        }
313
314                        @Override
315                        public void beforeUpdate(final T frame) {
316                        }
317                }
318
319                /**
320                 * Class used to animate the buttons panel on and off the screen.
321                 *
322                 * @author David Dupplaw (dpd@ecs.soton.ac.uk)
323                 * @created 14 Aug 2012
324                 * @version $Author$, $Revision$, $Date$
325                 */
326                public class AnimatorThread implements Runnable {
327                        public boolean stopNow = false;
328                        public boolean buttonValue;
329
330                        /**
331                         * Create a new animator thread. If the thread succeeds the
332                         * showButtons value will be set to the tf value given.
333                         *
334                         * @param tf
335                         *            Whether the buttons are shown (TRUE) or hidden
336                         */
337                        public AnimatorThread(final boolean tf) {
338                                this.buttonValue = tf;
339                        }
340
341                        @Override
342                        public void run() {
343                                // Animate the buttons
344                                while (!this.stopNow && VideoPlayerComponent.this.animator != null &&
345                                                !VideoPlayerComponent.this.animator.isComplete())
346                                {
347                                        VideoPlayerComponent.this.bp.setBounds(
348                                                        VideoPlayerComponent.this.bp.getBounds().x,
349                                                        VideoPlayerComponent.this.animator.nextValue(),
350                                                        VideoPlayerComponent.this.bp.getBounds().width,
351                                                        VideoPlayerComponent.this.bp.getBounds().height);
352                                        try {
353                                                // Sleep for 40ms - animates at roughly 25fps
354                                                Thread.sleep(40);
355                                        } catch (final InterruptedException e) {
356                                        }
357                                }
358
359                                if (!this.stopNow)
360                                        VideoPlayerComponent.this.showButtons = this.buttonValue;
361                        }
362                }
363
364                /** The buttons panel */
365                private ButtonsPanel bp = null;
366
367                /** Whether to show the buttons */
368                private boolean showButtons = true;
369
370                /** The current mode of the buttons */
371                private Mode currentMode = Mode.PLAY;
372
373                /** The animator used to animate the buttons */
374                private LinearTimeBasedIntegerValueAnimator animator = null;
375
376                /** The animator thread */
377                private AnimatorThread animatorThread = null;
378
379                /**
380                 * Create a new player component using the display component
381                 *
382                 * @param ic
383                 *            The video display component
384                 */
385                public VideoPlayerComponent(final ImageComponent ic) {
386                        try {
387                                this.init(ic);
388                        } catch (final SecurityException e) {
389                                e.printStackTrace();
390                        } catch (final NoSuchMethodException e) {
391                                e.printStackTrace();
392                        }
393                }
394
395                /**
396                 * Set up the widgets
397                 *
398                 * @param ic
399                 *            The video display component
400                 * @throws NoSuchMethodException
401                 * @throws SecurityException
402                 */
403                private void init(final ImageComponent ic)
404                                throws SecurityException, NoSuchMethodException
405                {
406                        this.setLayout(null);
407
408                        // Add the buttons
409                        this.bp = new ButtonsPanel();
410                        this.add(this.bp);
411
412                        // Add the video
413                        this.add(ic);
414
415                        // Set the size of the components based on the video component
416                        this.setPreferredSize(ic.getSize());
417                        this.setSize(ic.getSize());
418
419                        // Position the buttons panel
420                        this.bp.setBounds(0, this.getHeight() - this.bp.getSize().height,
421                                        this.getWidth(),
422                                        this.bp.getSize().height);
423
424                        this.showButtons = true;
425
426                        // Add a mouse listener to toggle the button display.
427                        final MouseAdapter ma = new MouseAdapter() {
428                                @Override
429                                public void mouseEntered(final MouseEvent e) {
430                                        VideoPlayerComponent.this.setShowButtons(true);
431                                };
432
433                                @Override
434                                public void mouseExited(final MouseEvent e) {
435                                        if (!VideoPlayerComponent.this.getVisibleRect().contains(
436                                                        e.getPoint()))
437                                        {
438                                                VideoPlayerComponent.this.setShowButtons(false);
439                                        }
440                                };
441                        };
442                        ic.addMouseListener(ma);
443                        this.bp.addMouseListener(ma);
444                }
445
446                /**
447                 * Reset the button states to the current state of the video player
448                 */
449                public void updateButtonStates() {
450                        // If we're changing mode
451                        if (this.currentMode != VideoPlayer.this.getMode()) {
452                                // Pop the buttons up if the mode changes.
453                                this.showButtons = true;
454
455                                // TODO: Update the graphics depending on the mode
456                                switch (VideoPlayer.this.getMode()) {
457                                case PLAY:
458                                        break;
459                                case STOP:
460                                        break;
461                                case PAUSE:
462                                        break;
463                                default:
464                                        break;
465                                }
466
467                                // Update the buttons to reflect the current video player mode
468                                this.currentMode = VideoPlayer.this.getMode();
469                        }
470                }
471
472                /**
473                 * Set whether the buttons are in view or not.
474                 *
475                 * @param tf
476                 *            TRUE to show the buttons
477                 */
478                public void setShowButtons(final boolean tf) {
479                        // Only need to do anything if the buttons are different to what
480                        // we want.
481                        if (tf != this.showButtons) {
482                                // Kill the current thread if there is one
483                                if (this.animatorThread != null) {
484                                        this.animatorThread.stopNow = true;
485                                        this.animatorThread = null;
486                                }
487
488                                // Create an animator to animate the buttons over 1/2 second
489                                // Animates from the current position to either off the screen
490                                // or on the screen depending on the value of tf
491                                this.animator = new LinearTimeBasedIntegerValueAnimator(
492                                                this.bp.getBounds().y,
493                                                this.getHeight() - (tf ? this.bp.getSize().height : 0),
494                                                500);
495
496                                // Start the thread
497                                this.animatorThread = new AnimatorThread(tf);
498                                new Thread(this.animatorThread).start();
499
500                                this.showButtons = tf;
501                        }
502                }
503        }
504
505        /** The frame showing the player */
506        private JFrame frame = null;
507
508        /** The player component */
509        private VideoPlayerComponent component = null;
510
511        /**
512         * Create the video player to play the given video.
513         *
514         * @param v
515         *            The video to play
516         */
517        public VideoPlayer(final Video<T> v) {
518                this(v, null, new ImageComponent());
519        }
520
521        /**
522         * Create the video player to play the given video.
523         *
524         * @param v
525         *            The video to play
526         * @param audio
527         *            The audio to play
528         */
529        public VideoPlayer(final Video<T> v, final AudioStream audio) {
530                this(v, audio, new ImageComponent());
531        }
532
533        /**
534         * Created the video player for the given video on the given image
535         * component.
536         *
537         * @param v
538         *            The video
539         * @param audio
540         *            The audio
541         * @param screen
542         *            The screen to draw the video to.
543         */
544        protected VideoPlayer(final Video<T> v, final AudioStream audio, final ImageComponent screen) {
545                super(v, audio, screen);
546
547                screen.setSize(v.getWidth(), v.getHeight());
548                screen.setPreferredSize(new Dimension(v.getWidth(), v.getHeight()));
549                screen.setAllowZoom(false);
550                screen.setAllowPanning(false);
551                screen.setTransparencyGrid(false);
552                screen.setShowPixelColours(false);
553                screen.setShowXYPosition(false);
554
555                this.component = new VideoPlayerComponent(screen);
556                this.component.setShowButtons(false);
557                this.addVideoDisplayStateListener(this);
558        }
559
560        /**
561         * Creates a new video player in a new thread and starts it running
562         * (initially in pause mode).
563         *
564         * @param video
565         *            The video
566         * @return The video player
567         */
568        public static <T extends Image<?, T>> VideoPlayer<T> createVideoPlayer(
569                        final Video<T> video)
570        {
571                final VideoPlayer<T> vp = new VideoPlayer<T>(video);
572                new Thread(vp).start();
573                return vp;
574        }
575
576        /**
577         * Creates a new video player in a new thread and starts it running
578         * (initially in pause mode).
579         *
580         * @param video
581         *            The video
582         * @param audio
583         *            The udio
584         * @return The video player
585         */
586        public static <T extends Image<?, T>> VideoPlayer<T> createVideoPlayer(
587                        final Video<T> video, final AudioStream audio)
588        {
589                final VideoPlayer<T> vp = new VideoPlayer<T>(video, audio);
590                new Thread(vp).start();
591                return vp;
592        }
593
594        /**
595         * Shows the video player in a frame. If a frame already exists it will be
596         * made visible.
597         *
598         * @return Returns the frame shown
599         */
600        public JFrame showFrame() {
601                if (this.frame == null) {
602                        this.frame = new JFrame();
603                        this.frame.add(this.component);
604                        this.frame.pack();
605                }
606
607                this.frame.setVisible(true);
608                return this.frame;
609        }
610
611        /**
612         * Returns a JPanel video player which can be incorporated into other GUIs.
613         *
614         * @return A VideoPlayer in a JPanel
615         */
616        public JPanel getVideoPlayerPanel() {
617                return this.component;
618        }
619
620        /**
621         * Play the video.
622         */
623        public void play() {
624                this.setMode(Mode.PLAY);
625        }
626
627        /**
628         * Stop the video
629         */
630        public void stop() {
631                this.setMode(Mode.STOP);
632        }
633
634        /**
635         * Pause the video
636         */
637        public void pause() {
638                this.setMode(Mode.PAUSE);
639        }
640
641        /**
642         * Step back a frame.
643         */
644        public void stepBack() {
645
646        }
647
648        /**
649         * Step forward a frame.
650         */
651        public void stepForward() {
652
653        }
654
655        /**
656         * {@inheritDoc}
657         *
658         * @see org.openimaj.video.VideoDisplayStateListener#videoStopped(org.openimaj.video.VideoDisplay)
659         */
660        @Override
661        public void videoStopped(final VideoDisplay<?> v) {
662                // If this is called it means the video mode was changed and the video
663                // has stopped playing. We must let our buttons know that this has
664                // happened.
665                this.component.updateButtonStates();
666        }
667
668        /**
669         * {@inheritDoc}
670         *
671         * @see org.openimaj.video.VideoDisplayStateListener#videoPlaying(org.openimaj.video.VideoDisplay)
672         */
673        @Override
674        public void videoPlaying(final VideoDisplay<?> v) {
675                // If this is called it means the video mode was changed and the video
676                // has started playing. We must let our buttons know that this has
677                // happened.
678                this.component.updateButtonStates();
679        }
680
681        /**
682         * {@inheritDoc}
683         *
684         * @see org.openimaj.video.VideoDisplayStateListener#videoPaused(org.openimaj.video.VideoDisplay)
685         */
686        @Override
687        public void videoPaused(final VideoDisplay<?> v) {
688                // If this is called it means the video mode was changed and the video
689                // has been paused. We must let our buttons know that this has happened.
690                this.component.updateButtonStates();
691        }
692
693        /**
694         * {@inheritDoc}
695         *
696         * @see org.openimaj.video.VideoDisplayStateListener#videoStateChanged(org.openimaj.video.VideoDisplay.Mode,
697         *      org.openimaj.video.VideoDisplay)
698         */
699        @Override
700        public void videoStateChanged(
701                        final org.openimaj.video.VideoDisplay.Mode mode,
702                        final VideoDisplay<?> v)
703        {
704                // As we've implemented the other methods in this listener, so
705                // we don't need to implement this one too.
706        }
707
708        /**
709         * Set the buttons to show on this video player. Available buttons are:
710         * <p>
711         * <ul>
712         * <li>play</li>
713         * <li>stop</li>
714         * <li>pause</li>
715         * <li>stepBack</li>
716         * <li>stepForward</li>
717         * </ul>
718         * <p>
719         * Buttons not from this list will be ignored.
720         * <p>
721         * The order of the array will determine the order of the buttons shown on
722         * the player.
723         *
724         * @param buttons
725         *            The buttons to show on the player.
726         */
727        public void setButtons(final String[] buttons) {
728                this.component.bp.setButtons(buttons);
729        }
730}