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