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