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}