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.Dimension;
033import java.awt.image.BufferedImage;
034import java.util.ArrayList;
035import java.util.List;
036
037import javax.swing.JComponent;
038import javax.swing.JFrame;
039
040import org.openimaj.audio.AudioPlayer;
041import org.openimaj.audio.AudioStream;
042import org.openimaj.image.DisplayUtilities;
043import org.openimaj.image.DisplayUtilities.ImageComponent;
044import org.openimaj.image.FImage;
045import org.openimaj.image.Image;
046import org.openimaj.image.ImageUtilities;
047import org.openimaj.time.TimeKeeper;
048import org.openimaj.time.Timecode;
049import org.openimaj.video.timecode.HrsMinSecFrameTimecode;
050
051/**
052 * Basic class for displaying videos.
053 * <p>
054 * {@link VideoDisplayListener}s can be added to be informed when the display is
055 * about to be updated or has just been updated.
056 * {@link VideoDisplayStateListener}s can be added to be informed about when the
057 * playback state of the display changes (e.g. when it entered play or pause
058 * mode). {@link VideoPositionListener}s can be added to be informed when the
059 * video hits the start or end frame.
060 * <p>
061 * The video can be played, paused and stopped. Pause and stop have slightly
062 * different semantics. After pause mode, the playback will continue from the
063 * point of pause; whereas after stop mode, the playback will continue from the
064 * start. Also, when in pause mode, frames are still sent to any listeners at
065 * roughly the frame-rate of the video; compare this to stop mode where no video
066 * events are fired. The default is that when the video comes to its end, the
067 * display is automatically set to stop mode. The action at the end of the video
068 * can be altered with {@link #setEndAction(EndAction)}.
069 * <p>
070 * The VideoDisplay constructor takes an {@link ImageComponent} which is used to
071 * draw the video to. This allows video displays to be integrated into a Swing
072 * UI. Use the {@link #createVideoDisplay(Video)} to have the video display
073 * create an appropriate image component and a basic frame into which to display
074 * the video. There is a {@link #createOffscreenVideoDisplay(Video)} method
075 * which will not display the resulting component.
076 * <p>
077 * The player uses a separate object for controlling the speed of playback. The
078 * {@link TimeKeeper} class is used to generate timestamps which the video
079 * display will do its best to synchronise with. A basic time keeper is
080 * encapsulated in this class ({@link BasicVideoTimeKeeper}) which is used for
081 * video without audio. The timekeeper can be set using
082 * {@link #setTimeKeeper(TimeKeeper)}. As video is read from the video stream,
083 * each frame's timestamp is compared with the current time of the timekeeper.
084 * If the frame should have been shown in the past the video display will
085 * attempt to read video frames until the frame's timestamp is in the future.
086 * Once its in the future it will wait until the frame's timestamp becomes
087 * current (or in the past by a small amount). The frame is then displayed. Note
088 * that in the case of live video, the display does not check to see if the
089 * frame was in the past - it always assumes that {@link Video#getNextFrame()}
090 * will return the latest frame to be displayed.
091 * <p>
092 * The VideoDisplay class can also accept an {@link AudioStream} as input. If
093 * this is supplied, an {@link AudioPlayer} will be instantiated to playback the
094 * audio and this audio player will be designated the {@link TimeKeeper} for the
095 * video playback. That means the audio will control the speed of playback for
096 * the video. An example of playing back a video with sound might look like
097 * this:
098 * <p>
099 *
100 * <pre>
101 * <code>
102 *              XuggleVideo xv = new XuggleVideo( videoFile );
103 *              XuggleAudio xa = new XuggleAudio( videoFile );
104 *              VideoDisplay<MBFImage> vd = VideoDisplay.createVideoDisplay( xv, xa );
105 * </code>
106 * </pre>
107 * <p>
108 *
109 * @author Sina Samangooei (ss@ecs.soton.ac.uk)
110 * @author David Dupplaw (dpd@ecs.soton.ac.uk)
111 * @author Jonathon Hare (jsh2@ecs.soton.ac.uk)
112 *
113 * @param <T>
114 *            the image type of the frames in the video
115 */
116public class VideoDisplay<T extends Image<?, T>> implements Runnable {
117        /**
118         * Enumerator to represent the state of the player.
119         *
120         * @author Sina Samangooei (ss@ecs.soton.ac.uk)
121         * @author David Dupplaw (dpd@ecs.soton.ac.uk)
122         */
123        public enum Mode {
124                /** The video is playing */
125                PLAY,
126
127                /** The video is paused */
128                PAUSE,
129
130                /** The video is stopped */
131                STOP,
132
133                /** The video is seeking */
134                SEEK,
135
136                /** The video is closed */
137                CLOSED;
138        }
139
140        /**
141         * An enumerator for what to do when the video reaches the end.
142         *
143         * @author David Dupplaw (dpd@ecs.soton.ac.uk)
144         * @created 14 Aug 2012
145         * @version $Author$, $Revision$, $Date$
146         */
147        public enum EndAction {
148                /** The video will be switched to STOP mode at the end */
149                STOP_AT_END,
150
151                /** The video will be switched to PAUSE mode at the end */
152                PAUSE_AT_END,
153
154                /** The video will be looped */
155                LOOP,
156
157                /** The player and timekeeper will be CLOSED at the end */
158                CLOSE_AT_END,
159        }
160
161        /**
162         * A timekeeper for videos without audio - uses the system time to keep
163         * track of where in a video a video should be. Also used for live videos
164         * that are to be displayed at a given rate.
165         *
166         * @author David Dupplaw (dpd@ecs.soton.ac.uk)
167         * @created 14 Aug 2012
168         * @version $Author$, $Revision$, $Date$
169         */
170        public class BasicVideoTimeKeeper implements TimeKeeper<Timecode> {
171                /** The current time we'll return */
172                private long currentTime = 0;
173
174                /** The last time the timer was started */
175                private long lastStarted = 0;
176
177                /** The time the timer was paused */
178                private long pausedAt = -1;
179
180                /** The amount of time to offset the timer */
181                private long timeOffset = 0;
182
183                /** Whether the timer is running */
184                private boolean isRunning = false;
185
186                /** The timecode object we'll update */
187                private HrsMinSecFrameTimecode timecode = null;
188
189                /** Whether the timekeeper is for live video or not */
190                private boolean liveVideo = false;
191
192                /**
193                 * Default constructor
194                 *
195                 * @param liveVideo
196                 *            Whether the timekeeper is for a live video or for a video
197                 *            that supports pausing
198                 */
199                public BasicVideoTimeKeeper(final boolean liveVideo) {
200                        this.timecode = new HrsMinSecFrameTimecode(0,
201                                        VideoDisplay.this.video.getFPS());
202                        this.liveVideo = liveVideo;
203                }
204
205                /**
206                 * {@inheritDoc}
207                 *
208                 * @see org.openimaj.time.TimeKeeper#run()
209                 */
210                @Override
211                public void run() {
212                        if (this.lastStarted == 0)
213                                this.lastStarted = System.currentTimeMillis();
214                        else if (this.supportsPause())
215                                this.timeOffset += System.currentTimeMillis() - this.pausedAt;
216
217                        this.isRunning = true;
218                }
219
220                /**
221                 * {@inheritDoc}
222                 *
223                 * @see org.openimaj.time.TimeKeeper#stop()
224                 */
225                @Override
226                public void stop() {
227                        this.isRunning = false;
228                        this.currentTime = 0;
229                }
230
231                /**
232                 * {@inheritDoc}
233                 *
234                 * @see org.openimaj.time.TimeKeeper#getTime()
235                 */
236                @Override
237                public Timecode getTime() {
238                        if (this.isRunning) {
239                                // Update the current time.
240                                this.currentTime = (System.currentTimeMillis() -
241                                                this.lastStarted - this.timeOffset);
242                                this.timecode.setTimecodeInMilliseconds(this.currentTime);
243                        }
244
245                        return this.timecode;
246                }
247
248                /**
249                 * {@inheritDoc}
250                 *
251                 * @see org.openimaj.time.TimeKeeper#supportsPause()
252                 */
253                @Override
254                public boolean supportsPause() {
255                        return !this.liveVideo;
256                }
257
258                /**
259                 * {@inheritDoc}
260                 *
261                 * @see org.openimaj.time.TimeKeeper#supportsSeek()
262                 */
263                @Override
264                public boolean supportsSeek() {
265                        return !this.liveVideo;
266                }
267
268                /**
269                 * {@inheritDoc}
270                 *
271                 * @see org.openimaj.time.TimeKeeper#seek(long)
272                 */
273                @Override
274                public void seek(final long timestamp) {
275                        if (!this.liveVideo)
276                                this.lastStarted = System.currentTimeMillis() - timestamp;
277                }
278
279                /**
280                 * {@inheritDoc}
281                 *
282                 * @see org.openimaj.time.TimeKeeper#reset()
283                 */
284                @Override
285                public void reset() {
286                        this.lastStarted = 0;
287                        this.pausedAt = -1;
288                        this.run();
289                }
290
291                /**
292                 * {@inheritDoc}
293                 *
294                 * @see org.openimaj.time.TimeKeeper#pause()
295                 */
296                @Override
297                public void pause() {
298                        if (!this.liveVideo) {
299                                this.isRunning = false;
300                                this.pausedAt = System.currentTimeMillis();
301                        }
302                }
303
304                /**
305                 * Set the time offset to use in the current time calculation. Can be
306                 * used to force the time keeper to start at a different point in time.
307                 *
308                 * @param timeOffset
309                 *            the new time offset.
310                 */
311                public void setTimeOffset(final long timeOffset) {
312                        this.timeOffset = timeOffset;
313                }
314        }
315
316        /** The default mode is to play the player */
317        private Mode mode = Mode.PLAY;
318
319        /** The screen to show the player in */
320        private final ImageComponent screen;
321
322        /** The video being displayed */
323        private Video<T> video;
324
325        /** The list of video display listeners */
326        private final List<VideoDisplayListener<T>> videoDisplayListeners;
327
328        /** List of state listeners */
329        private final List<VideoDisplayStateListener> stateListeners;
330
331        /** List of position listeners */
332        private final List<VideoPositionListener> positionListeners;
333
334        /** Whether to display the screen */
335        private boolean displayMode = true;
336
337        /** What to do at the end of the video */
338        private EndAction endAction = EndAction.STOP_AT_END;
339
340        /** If audio comes with the video, then we play it with the player */
341        private AudioPlayer audioPlayer = null;
342
343        /** The time keeper to use to synch the video */
344        private TimeKeeper<? extends Timecode> timeKeeper = null;
345
346        /** This is the calculated FPS that the video player is playing at */
347        private double calculatedFPS = 0;
348
349        /** Whether to fire video updates or not */
350        private final boolean fireUpdates = true;
351
352        /** The timestamp of the frame currently being displayed */
353        private long currentFrameTimestamp = 0;
354
355        /** The current frame being displayed */
356        private T currentFrame = null;
357
358        /** A count of the number of frames that have been dropped while playing */
359        private int droppedFrameCount = 0;
360
361        /** Whether to calculate frames per second at each frame */
362        private boolean calculateFPS = true;
363
364        /**
365         * Construct a video display with the given video and frame.
366         *
367         * @param v
368         *            the video
369         * @param screen
370         *            the frame to draw into.
371         */
372        public VideoDisplay(final Video<T> v, final ImageComponent screen) {
373                this(v, null, screen);
374        }
375
376        /**
377         * Construct a video display with the given video and audio
378         *
379         * @param v
380         *            The video
381         * @param a
382         *            The audio
383         * @param screen
384         *            The frame to draw into.
385         */
386        public VideoDisplay(final Video<T> v, final AudioStream a, final ImageComponent screen) {
387                this.video = v;
388
389                // If we're given audio, we create an audio player that will also
390                // act as our synchronisation time keeper.
391                if (a != null) {
392                        this.audioPlayer = new AudioPlayer(a);
393                        this.timeKeeper = this.audioPlayer;
394                }
395                // If no audio is provided, we'll use a basic time keeper
396                else
397                        this.timeKeeper = new BasicVideoTimeKeeper(this.video.countFrames() == -1);
398
399                this.screen = screen;
400                this.videoDisplayListeners = new ArrayList<VideoDisplayListener<T>>();
401                this.stateListeners = new ArrayList<VideoDisplayStateListener>();
402                this.positionListeners = new ArrayList<VideoPositionListener>();
403        }
404
405        @SuppressWarnings("rawtypes")
406        @Override
407        public void run() {
408                BufferedImage bimg = null;
409
410                // Current frame
411                this.currentFrame = this.video.getCurrentFrame();
412                // this.currentFrameTimestamp = this.video.getTimeStamp();
413
414                // We'll estimate each iteration how long we should wait before
415                // trying again.
416                long roughSleepTime = 10;
417
418                // Tolerance is an estimate (it only need be rough) of the time it takes
419                // to get a frame from the video and display it.
420                final long tolerance = 10;
421
422                // Used to calculate the FPS the video's playing at
423                long lastTimestamp = 0, currentTimestamp = 0;
424
425                // Just about the start the video
426                this.fireVideoStartEvent();
427
428                // Start the timekeeper (if we have audio, this will start the
429                // audio playing)
430                new Thread(this.timeKeeper).start();
431
432                // Keep going until the mode becomes closed
433                while (this.mode != Mode.CLOSED) {
434                        // System.out.println( "[Main loop ping: "+this.mode+"]" );
435
436                        // If we're on stop we don't update at all
437                        if (this.mode == Mode.PLAY || this.mode == Mode.PAUSE) {
438                                // Calculate the display's FPS
439                                if (this.calculateFPS) {
440                                        currentTimestamp = System.currentTimeMillis();
441                                        this.calculatedFPS = 1000d / (currentTimestamp - lastTimestamp);
442                                        lastTimestamp = currentTimestamp;
443                                }
444
445                                // We initially set up with the last frame
446                                T nextFrame = this.currentFrame;
447                                long nextFrameTimestamp = this.currentFrameTimestamp;
448
449                                if (this.mode == Mode.PLAY) {
450                                        // We may need to catch up if we're behind in display frames
451                                        // rather than ahead. In which case, we keep skipping frames
452                                        // until we find one that's in the future.
453                                        // We only do this if we're not working on live video. If
454                                        // we're working on live video, then getNextFrame() will
455                                        // always
456                                        // deliver the latest video frame, so we never have to catch
457                                        // up.
458                                        if (this.video.countFrames() != -1 && this.currentFrame != null) {
459                                                final long t = this.timeKeeper.getTime().getTimecodeInMilliseconds();
460                                                // System.out.println( "Should be at "+t );
461                                                int droppedThisRound = -1;
462                                                while (nextFrameTimestamp <= t && nextFrame != null) {
463                                                        // Get the next frame to determine if it's in the
464                                                        // future
465                                                        nextFrame = this.video.getNextFrame();
466                                                        nextFrameTimestamp = this.video.getTimeStamp();
467                                                        // System.out.println("Frame is "+nextFrameTimestamp
468                                                        // );
469                                                        droppedThisRound++;
470                                                }
471                                                this.droppedFrameCount += droppedThisRound;
472                                                // System.out.println(
473                                                // "Dropped "+this.droppedFrameCount+" frames.");
474                                        } else {
475                                                nextFrame = this.video.getNextFrame();
476                                                nextFrameTimestamp = this.video.getTimeStamp();
477                                                if (this.currentFrame == null && (this.timeKeeper instanceof VideoDisplay.BasicVideoTimeKeeper))
478                                                        ((VideoDisplay.BasicVideoTimeKeeper) this.timeKeeper).setTimeOffset(-nextFrameTimestamp);
479                                        }
480
481                                        // We've got to the end of the video. What should we do?
482                                        if (nextFrame == null) {
483                                                // System.out.println( "Video ended" );
484                                                this.processEndAction(this.endAction);
485                                                continue;
486                                        }
487                                }
488
489                                // We process the current frame before we draw it to the screen
490                                if (this.fireUpdates) {
491                                        // nextFrame = this.currentFrame.clone();
492                                        this.fireBeforeUpdate(this.currentFrame);
493
494                                }
495
496                                // Draw the image into the display
497                                if (this.displayMode && this.currentFrame != null) {
498                                        // System.out.println( "Drawing frame");
499                                        this.screen.setImage(bimg = ImageUtilities.createBufferedImageForDisplay(this.currentFrame, bimg));
500                                }
501
502                                // Fire that we've put a frame to the screen
503                                if (this.fireUpdates)
504                                        this.fireVideoUpdate();
505
506                                // Estimate the sleep time for next time
507                                roughSleepTime = (long) (1000 / this.video.getFPS()) - tolerance;
508
509                                if (this.mode == Mode.PLAY) {
510                                        // System.out.println("Next frame: "+nextFrameTimestamp );
511                                        // System.out.println("Current time:
512                                        // "+this.timeKeeper.getTime().getTimecodeInMilliseconds()
513                                        // );
514
515                                        // Wait until the timekeeper says we should be displaying
516                                        // the next frame
517                                        // We also check to see we're still in play mode, as it's
518                                        // in this wait that the state is most likely to get the
519                                        // time
520                                        // to change, so we need to drop out of this loop if it
521                                        // does.
522                                        while (this.timeKeeper.getTime().getTimecodeInMilliseconds() < nextFrameTimestamp
523                                                        && this.mode == Mode.PLAY)
524                                        {
525                                                // System.out.println( "Sleep "+roughSleepTime );
526                                                try {
527                                                        Thread.sleep(Math.max(0, roughSleepTime));
528                                                } catch (final InterruptedException e) {
529                                                }
530                                        }
531
532                                        // The current frame will become what was our next frame
533                                        this.currentFrame = nextFrame;
534                                        this.currentFrameTimestamp = nextFrameTimestamp;
535                                } else {
536                                        // We keep delivering frames at roughly the frame rate
537                                        // when in pause mode.
538                                        try {
539                                                Thread.sleep(Math.max(0, roughSleepTime));
540                                        } catch (final InterruptedException e) {
541                                        }
542                                }
543                        } else {
544                                // In STOP mode, we patiently wait to be played again
545                                try {
546                                        Thread.sleep(500);
547                                } catch (final InterruptedException e) {
548                                }
549                        }
550                }
551
552                /*
553                 * This is the old code, for posterity while( true ) { T currentFrame =
554                 * null; T nextFrame;
555                 *
556                 * if (this.mode == Mode.CLOSED) { this.video.close(); return; }
557                 *
558                 * if( this.mode == Mode.SEEK ) { this.video.seek( this.seekTimestamp );
559                 * this.videoPlayerStartTime = -1; this.mode = Mode.PLAY;
560                 *
561                 * }
562                 *
563                 * if(this.mode == Mode.PLAY) { nextFrame = this.video.getNextFrame(); }
564                 * else { nextFrame = this.video.getCurrentFrame(); }
565                 *
566                 * // If the getNextFrame() returns null then the end of the // video
567                 * may have been reached, so we pause the video. if( nextFrame == null )
568                 * { switch( this.endAction ) { case STOP_AT_END: this.setMode(
569                 * Mode.STOP ); break; case PAUSE_AT_END: this.setMode( Mode.PAUSE );
570                 * break; case LOOP: this.seek( 0 ); break; } } else { currentFrame =
571                 * nextFrame; }
572                 *
573                 * // If we have a frame to draw, then draw it. if( currentFrame != null
574                 * && this.mode != Mode.STOP ) { if( this.videoPlayerStartTime == -1 &&
575                 * this.mode == Mode.PLAY ) { //
576                 * System.out.println("Resseting internal times");
577                 * this.firstFrameTimestamp = this.video.getTimeStamp();
578                 * this.videoPlayerStartTime = System.currentTimeMillis(); //
579                 * System.out.println("First time stamp: " + firstFrameTimestamp); }
580                 * else { // This is based on the Xuggler demo code: //
581                 * http://xuggle.googlecode
582                 * .com/svn/trunk/java/xuggle-xuggler/src/com/xuggle
583                 * /xuggler/demos/DecodeAndPlayVideo.java final long systemDelta =
584                 * System.currentTimeMillis() - this.videoPlayerStartTime; final long
585                 * currentFrameTimestamp = this.video.getTimeStamp(); final long
586                 * videoDelta = currentFrameTimestamp - this.firstFrameTimestamp; final
587                 * long tolerance = 20; final long sleepTime = videoDelta - tolerance -
588                 * systemDelta;
589                 *
590                 * if( sleepTime > 0 ) { try { Thread.sleep( sleepTime ); } catch (final
591                 * InterruptedException e) { return; } } } } final boolean fireUpdates =
592                 * this.videoDisplayListeners.size() != 0; if (toDraw == null) { toDraw
593                 * = currentFrame.clone(); } else{ if(currentFrame!=null)
594                 * toDraw.internalCopy(currentFrame); } if (fireUpdates) {
595                 * this.fireBeforeUpdate(toDraw); }
596                 *
597                 * if( this.displayMode ) { this.screen.setImage( bimg =
598                 * ImageUtilities.createBufferedImageForDisplay( toDraw, bimg ) ); }
599                 *
600                 * if (fireUpdates) { this.fireVideoUpdate(); } }
601                 */
602        }
603
604        /**
605         * Process the end of the video action.
606         *
607         * @param e
608         *            The end action to process
609         */
610        protected void processEndAction(final EndAction e) {
611                this.fireVideoEndEvent();
612
613                switch (e) {
614                // The video needs to loop, so we reset the video, any audio player,
615                // the timekeeper back to zero. We also have to zero the current frame
616                // timestamp so that the main loop will read a new frame.
617                case LOOP:
618                        this.video.reset();
619                        if (this.audioPlayer != null)
620                                this.audioPlayer.reset();
621                        this.timeKeeper.reset();
622                        this.currentFrameTimestamp = 0;
623                        this.fireVideoStartEvent();
624                        break;
625
626                // Pause the video player
627                case PAUSE_AT_END:
628                        this.setMode(Mode.PAUSE);
629                        break;
630
631                // Stop the video player
632                case STOP_AT_END:
633                        this.setMode(Mode.STOP);
634                        break;
635
636                // Close the video player
637                case CLOSE_AT_END:
638                        this.setMode(Mode.CLOSED);
639                        break;
640                }
641        }
642
643        /**
644         * Close the video display. Causes playback to stop, and further events are
645         * ignored.
646         */
647        public synchronized void close() {
648                this.setMode(Mode.CLOSED);
649        }
650
651        /**
652         * Set whether this player is playing, paused or stopped. This method will
653         * also control the state of the timekeeper by calling its run, stop or
654         * reset method.
655         *
656         * @param m
657         *            The new mode
658         */
659        synchronized public void setMode(final Mode m) {
660                // System.out.println( "Mode is: "+this.mode+"; setting to "+m );
661
662                // If we're already closed - stop allowing mode changes
663                if (this.mode == Mode.CLOSED)
664                        return;
665
666                // No change in the mode? Just return
667                if (m == this.mode)
668                        return;
669
670                switch (m) {
671                // -------------------------------------------------
672                case PLAY:
673                        if (this.mode == Mode.STOP)
674                                this.fireVideoStartEvent();
675
676                        // Restart the timekeeper
677                        new Thread(this.timeKeeper).start();
678
679                        // Seed the player with the next frame
680                        this.currentFrame = this.video.getCurrentFrame();
681                        this.currentFrameTimestamp = this.video.getTimeStamp();
682
683                        break;
684                // -------------------------------------------------
685                case STOP:
686                        this.timeKeeper.stop();
687                        this.timeKeeper.reset();
688                        if (this.audioPlayer != null) {
689                                this.audioPlayer.stop();
690                                this.audioPlayer.reset();
691                        }
692                        this.video.reset();
693                        this.currentFrameTimestamp = 0;
694                        break;
695                // -------------------------------------------------
696                case PAUSE:
697                        // If we can pause the timekeeper, that's what
698                        // we'll do. If we can't, then it will have to keep
699                        // running while we pause the video (the video will still get
700                        // paused).
701                        System.out.println("Does timekeeper support pause? " + this.timeKeeper.supportsPause());
702                        if (this.timeKeeper.supportsPause())
703                                this.timeKeeper.pause();
704                        break;
705                // -------------------------------------------------
706                case CLOSED:
707                        // Kill everything (same as stop)
708                        this.timeKeeper.stop();
709                        this.video.close();
710                        break;
711                // -------------------------------------------------
712                default:
713                        break;
714                }
715
716                // Update the mode
717                this.mode = m;
718
719                // Let the listeners know we've changed mode
720                this.fireStateChanged();
721        }
722
723        /**
724         * Returns the current state of the video display.
725         *
726         * @return The current state as a {@link Mode}
727         */
728        protected Mode getMode() {
729                return this.mode;
730        }
731
732        /**
733         * Fire the event to the video listeners that a frame is about to be
734         * displayed on the video.
735         *
736         * @param currentFrame
737         *            The frame that is about to be displayed
738         */
739        protected void fireBeforeUpdate(final T currentFrame) {
740                synchronized (this.videoDisplayListeners) {
741                        for (final VideoDisplayListener<T> vdl : this.videoDisplayListeners) {
742                                vdl.beforeUpdate(currentFrame);
743                        }
744                }
745        }
746
747        /**
748         * Fire the event to the video listeners that a frame has been put on the
749         * display
750         */
751        protected void fireVideoUpdate() {
752                synchronized (this.videoDisplayListeners) {
753                        for (final VideoDisplayListener<T> vdl : this.videoDisplayListeners) {
754                                vdl.afterUpdate(this);
755                        }
756                }
757        }
758
759        /**
760         * Get the frame the video is being drawn to
761         *
762         * @return the frame
763         */
764        public ImageComponent getScreen() {
765                return this.screen;
766        }
767
768        /**
769         * Get the video
770         *
771         * @return the video
772         */
773        public Video<T> getVideo() {
774                return this.video;
775        }
776
777        /**
778         * Change the video that is being displayed by this video display.
779         *
780         * @param newVideo
781         *            The new video to display.
782         */
783        public void changeVideo(final Video<T> newVideo) {
784                this.video = newVideo;
785                this.timeKeeper = new BasicVideoTimeKeeper(newVideo.countFrames() == -1);
786        }
787
788        /**
789         * Add a listener that will get fired as every frame is displayed.
790         *
791         * @param dsl
792         *            the listener
793         */
794        public void addVideoListener(final VideoDisplayListener<T> dsl) {
795                synchronized (this.videoDisplayListeners) {
796                        this.videoDisplayListeners.add(dsl);
797                }
798
799        }
800
801        /**
802         * Add a listener for the state of this player.
803         *
804         * @param vdsl
805         *            The listener to add
806         */
807        public void addVideoDisplayStateListener(final VideoDisplayStateListener vdsl) {
808                this.stateListeners.add(vdsl);
809        }
810
811        /**
812         * Remove a listener from the state of this player
813         *
814         * @param vdsl
815         *            The listener
816         */
817        public void removeVideoDisplayStateListener(final VideoDisplayStateListener vdsl) {
818                this.stateListeners.remove(vdsl);
819        }
820
821        /**
822         * Fire the state changed event
823         */
824        protected void fireStateChanged() {
825                for (final VideoDisplayStateListener s : this.stateListeners) {
826                        s.videoStateChanged(this.mode, this);
827                        switch (this.mode) {
828                        case PAUSE:
829                                s.videoPaused(this);
830                                break;
831                        case PLAY:
832                                s.videoPlaying(this);
833                                break;
834                        case STOP:
835                                s.videoStopped(this);
836                                break;
837                        case CLOSED:
838                                break; // TODO: Need to add more states to video state listener
839                        case SEEK:
840                                break;
841                        default:
842                                break;
843                        }
844                }
845        }
846
847        /**
848         * Add a video position listener to this display
849         *
850         * @param vpl
851         *            The video position listener
852         */
853        public void addVideoPositionListener(final VideoPositionListener vpl) {
854                this.positionListeners.add(vpl);
855        }
856
857        /**
858         * Remove visible panty lines... or video position listeners.
859         *
860         * @param vpl
861         *            The video position listener
862         */
863        public void removeVideoPositionListener(final VideoPositionListener vpl) {
864                this.positionListeners.remove(vpl);
865        }
866
867        /**
868         * Fire the event that says the video is at the start.
869         */
870        protected void fireVideoStartEvent() {
871                for (final VideoPositionListener vpl : this.positionListeners)
872                        vpl.videoAtStart(this);
873        }
874
875        /**
876         * Fire the event that says the video is at the end.
877         */
878        protected void fireVideoEndEvent() {
879                for (final VideoPositionListener vpl : this.positionListeners)
880                        vpl.videoAtEnd(this);
881        }
882
883        /**
884         * Pause or resume the display. This will only have an affect if the video
885         * is not in STOP mode.
886         */
887        public void togglePause() {
888                if (this.mode == Mode.CLOSED)
889                        return;
890
891                if (this.mode == Mode.PLAY)
892                        this.setMode(Mode.PAUSE);
893                else if (this.mode == Mode.PAUSE)
894                        this.setMode(Mode.PLAY);
895        }
896
897        /**
898         * Is the video paused?
899         *
900         * @return true if paused; false otherwise.
901         */
902        public boolean isPaused() {
903                return this.mode == Mode.PAUSE;
904        }
905
906        /**
907         * Returns whether the video is stopped or not.
908         *
909         * @return TRUE if stopped; FALSE otherwise.
910         */
911        public boolean isStopped() {
912                return this.mode == Mode.STOP;
913        }
914
915        /**
916         * Set the action to occur when the video reaches its end. Possible values
917         * are given in the {@link EndAction} enumeration.
918         *
919         * @param action
920         *            The {@link EndAction} action to occur.
921         */
922        public void setEndAction(final EndAction action) {
923                this.endAction = action;
924        }
925
926        /**
927         * Convenience function to create a VideoDisplay from an array of images
928         *
929         * @param images
930         *            the images
931         * @return a VideoDisplay
932         */
933        public static VideoDisplay<FImage> createVideoDisplay(final FImage[] images) {
934                return VideoDisplay.createVideoDisplay(new ArrayBackedVideo<FImage>(images, 30));
935        }
936
937        /**
938         * Convenience function to create a VideoDisplay from a video in a new
939         * window.
940         *
941         * @param <T>
942         *            the image type of the video frames
943         * @param video
944         *            the video
945         * @return a VideoDisplay
946         */
947        public static <T extends Image<?, T>> VideoDisplay<T> createVideoDisplay(final Video<T> video) {
948                final JFrame screen = DisplayUtilities.makeFrame("Video");
949                return VideoDisplay.createVideoDisplay(video, screen);
950        }
951
952        /**
953         * Convenience function to create a VideoDisplay from a video in a new
954         * window.
955         *
956         * @param <T>
957         *            the image type of the video frames
958         * @param video
959         *            the video
960         * @param audio
961         *            the audio stream
962         * @return a VideoDisplay
963         */
964        public static <T extends Image<?, T>> VideoDisplay<T> createVideoDisplay(
965                        final Video<T> video, final AudioStream audio)
966        {
967                final JFrame screen = DisplayUtilities.makeFrame("Video");
968                return VideoDisplay.createVideoDisplay(video, audio, screen);
969        }
970
971        /**
972         * Convenience function to create a VideoDisplay from a video in a new
973         * window.
974         *
975         * @param <T>
976         *            the image type of the video frames
977         * @param video
978         *            The video
979         * @param screen
980         *            The window to draw into
981         * @return a VideoDisplay
982         */
983        public static <T extends Image<?, T>> VideoDisplay<T> createVideoDisplay(
984                        final Video<T> video, final JFrame screen)
985        {
986                return VideoDisplay.createVideoDisplay(video, null, screen);
987        }
988
989        /**
990         * Convenience function to create a VideoDisplay from a video in a new
991         * window.
992         *
993         * @param <T>
994         *            the image type of the video frames
995         * @param video
996         *            The video
997         * @param as
998         *            The audio
999         * @param screen
1000         *            The window to draw into
1001         * @return a VideoDisplay
1002         */
1003        public static <T extends Image<?, T>> VideoDisplay<T> createVideoDisplay(
1004                        final Video<T> video, final AudioStream as, final JFrame screen)
1005        {
1006                final ImageComponent ic = new ImageComponent();
1007                ic.setSize(video.getWidth(), video.getHeight());
1008                ic.setPreferredSize(new Dimension(video.getWidth(), video.getHeight()));
1009                ic.setAllowZoom(false);
1010                ic.setAllowPanning(false);
1011                ic.setTransparencyGrid(false);
1012                ic.setShowPixelColours(false);
1013                ic.setShowXYPosition(false);
1014                screen.getContentPane().add(ic);
1015
1016                screen.pack();
1017                screen.setVisible(true);
1018
1019                final VideoDisplay<T> dv = new VideoDisplay<T>(video, as, ic);
1020
1021                new Thread(dv).start();
1022                return dv;
1023
1024        }
1025
1026        /**
1027         * Convenience function to create a VideoDisplay from a video in a new
1028         * window.
1029         *
1030         * @param <T>
1031         *            the image type of the video frames
1032         * @param video
1033         *            The video
1034         * @param ic
1035         *            The {@link ImageComponent} to draw into
1036         * @return a VideoDisplay
1037         */
1038        public static <T extends Image<?, T>>
1039                        VideoDisplay<T>
1040                        createVideoDisplay(final Video<T> video, final ImageComponent ic)
1041        {
1042                final VideoDisplay<T> dv = new VideoDisplay<T>(video, ic);
1043
1044                new Thread(dv).start();
1045                return dv;
1046
1047        }
1048
1049        /**
1050         * Convenience function to create a VideoDisplay from a video in a new
1051         * window.
1052         *
1053         * @param <T>
1054         *            the image type of the video frames
1055         * @param video
1056         *            the video
1057         * @return a VideoDisplay
1058         */
1059        public static <T extends Image<?, T>> VideoDisplay<T> createOffscreenVideoDisplay(final Video<T> video) {
1060
1061                final VideoDisplay<T> dv = new VideoDisplay<T>(video, null);
1062                dv.displayMode = false;
1063                new Thread(dv).start();
1064                return dv;
1065
1066        }
1067
1068        /**
1069         * Convenience function to create a VideoDisplay from a video in an existing
1070         * component.
1071         *
1072         * @param <T>
1073         *            the image type of the video frames
1074         * @param video
1075         *            The video
1076         * @param comp
1077         *            The {@link JComponent} to draw into
1078         * @return a VideoDisplay
1079         */
1080        public static <T extends Image<?, T>> VideoDisplay<T> createVideoDisplay(final Video<T> video,
1081                        final JComponent comp)
1082        {
1083                final ImageComponent ic = new ImageComponent();
1084                ic.setSize(video.getWidth(), video.getHeight());
1085                ic.setPreferredSize(new Dimension(video.getWidth(), video.getHeight()));
1086                ic.setAllowZoom(false);
1087                ic.setAllowPanning(false);
1088                ic.setTransparencyGrid(false);
1089                ic.setShowPixelColours(false);
1090                ic.setShowXYPosition(false);
1091                comp.add(ic);
1092
1093                final VideoDisplay<T> dv = new VideoDisplay<T>(video, ic);
1094
1095                new Thread(dv).start();
1096                return dv;
1097        }
1098
1099        /**
1100         * Convenience function to create a VideoDisplay from a video in an existing
1101         * component.
1102         *
1103         * @param <T>
1104         *            the image type of the video frames
1105         * @param video
1106         *            The video
1107         * @param audio
1108         *            The audio
1109         * @param comp
1110         *            The {@link JComponent} to draw into
1111         * @return a VideoDisplay
1112         */
1113        public static <T extends Image<?, T>> VideoDisplay<T> createVideoDisplay(final Video<T> video, AudioStream audio,
1114                        final JComponent comp)
1115        {
1116                final ImageComponent ic;
1117                if (video.getWidth() > comp.getPreferredSize().width || video.getHeight() > comp.getPreferredSize().height) {
1118                        ic = new DisplayUtilities.ScalingImageComponent();
1119                        ic.setSize(comp.getSize());
1120                        ic.setPreferredSize(comp.getPreferredSize());
1121                } else {
1122                        ic = new ImageComponent();
1123                        ic.setSize(video.getWidth(), video.getHeight());
1124                        ic.setPreferredSize(new Dimension(video.getWidth(), video.getHeight()));
1125                }
1126                ic.setAllowZoom(false);
1127                ic.setAllowPanning(false);
1128                ic.setTransparencyGrid(false);
1129                ic.setShowPixelColours(false);
1130                ic.setShowXYPosition(false);
1131                comp.add(ic);
1132
1133                final VideoDisplay<T> dv = new VideoDisplay<T>(video, audio, ic);
1134
1135                new Thread(dv).start();
1136                return dv;
1137        }
1138
1139        /**
1140         * Set whether to draw onscreen or not
1141         *
1142         * @param b
1143         *            if true then video is drawn to the screen, otherwise it is not
1144         */
1145        public void displayMode(final boolean b) {
1146                this.displayMode = b;
1147        }
1148
1149        /**
1150         * Seek to a given timestamp in millis.
1151         *
1152         * @param toSeek
1153         *            timestamp to seek to in millis.
1154         */
1155        public void seek(final long toSeek) {
1156                // this.mode = Mode.SEEK;
1157                if (this.timeKeeper.supportsSeek()) {
1158                        this.timeKeeper.seek(toSeek);
1159                        this.video.seek(toSeek);
1160                } else {
1161                        System.out.println("WARNING: Time keeper does not support seek. " +
1162                                        "Not seeking");
1163                }
1164        }
1165
1166        /**
1167         * Returns the position of the play head in this video as a percentage of
1168         * the length of the video. IF the video is a live video, this method will
1169         * always return 0;
1170         *
1171         * @return The percentage through the video.
1172         */
1173        public double getPosition() {
1174                final long nFrames = this.video.countFrames();
1175                if (nFrames == -1)
1176                        return 0;
1177                return this.video.getCurrentFrameIndex() * 100d / nFrames;
1178        }
1179
1180        /**
1181         * Set the position of the play head to the given percentage. If the video
1182         * is a live video this method will have no effect.
1183         *
1184         * @param pc
1185         *            The percentage to set the play head to.
1186         */
1187        public void setPosition(final double pc) {
1188                if (pc > 100 || pc < 0)
1189                        throw new IllegalArgumentException("Percentage must be less than " +
1190                                        "or equals to 100 and greater than or equal 0. Given " + pc);
1191
1192                // If it's a live video we cannot do anything
1193                if (this.video.countFrames() == -1)
1194                        return;
1195
1196                // We have to seek to a millisecond position, so we find out the length
1197                // of the video in ms and then multiply by the percentage
1198                final double nMillis = this.video.countFrames() * this.video.getFPS();
1199                final long msPos = (long) (nMillis * pc / 100d);
1200                System.out.println("msPOs = " + msPos + " (" + pc + "%)");
1201                this.seek(msPos);
1202        }
1203
1204        /**
1205         * Returns the speed at which the display is being updated.
1206         *
1207         * @return The number of frames per second
1208         */
1209        public double getDisplayFPS() {
1210                return this.calculatedFPS;
1211        }
1212
1213        /**
1214         * Set the timekeeper to use for this video.
1215         *
1216         * @param t
1217         *            The timekeeper.
1218         */
1219        public void setTimeKeeper(final TimeKeeper<? extends Timecode> t) {
1220                this.timeKeeper = t;
1221        }
1222
1223        /**
1224         * Returns the number of frames that have been dropped while playing the
1225         * video.
1226         *
1227         * @return The number of dropped frames
1228         */
1229        public int getDroppedFrameCount() {
1230                return this.droppedFrameCount;
1231        }
1232
1233        /**
1234         * Reset the dropped frame count to zero.
1235         */
1236        public void resetDroppedFrameCount() {
1237                this.droppedFrameCount = 0;
1238        }
1239
1240        /**
1241         * Returns whether the frames per second are being calculated at every
1242         * frame. If this returns false, then {@link #getDisplayFPS()} will not
1243         * return a valid value.
1244         *
1245         * @return whether the FPS is being calculated
1246         */
1247        public boolean isCalculateFPS() {
1248                return this.calculateFPS;
1249        }
1250
1251        /**
1252         * Set whether the frames per second display rate will be calculated at
1253         * every frame.
1254         *
1255         * @param calculateFPS
1256         *            TRUE to calculate the FPS; FALSE otherwise.
1257         */
1258        public void setCalculateFPS(final boolean calculateFPS) {
1259                this.calculateFPS = calculateFPS;
1260        }
1261}