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