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 methods 068 * to be used. This class then provides a simple API for starting, pausing and 069 * 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> 079 * The type of the video frame 080 */ 081public class VideoPlayer<T extends Image<?, T>> extends VideoDisplay<T> 082 implements 083 VideoDisplayStateListener 084{ 085 /** 086 * The video player components encapsulates the buttons and their 087 * functionalities, as well as animating buttons, etc. 088 * 089 * @author David Dupplaw (dpd@ecs.soton.ac.uk) 090 * @created 10 Aug 2012 091 * @version $Author$, $Revision$, $Date$ 092 */ 093 protected class VideoPlayerComponent extends JPanel { 094 /** */ 095 private static final long serialVersionUID = 1L; 096 097 /** 098 * This class represents the widgets in the video player 099 * 100 * @author David Dupplaw (dpd@ecs.soton.ac.uk) 101 * @created 10 Aug 2012 102 * @version $Author$, $Revision$, $Date$ 103 */ 104 protected class ButtonsPanel extends JPanel implements VideoDisplayListener<T> { 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 // We will only allow these methods to be called 141 this.buttonsMap.put("play", ButtonsPanel.PLAY); 142 this.buttonsMap.put("stop", ButtonsPanel.STOP); 143 this.buttonsMap.put("pause", ButtonsPanel.PAUSE); 144 this.buttonsMap.put("stepBack", ButtonsPanel.STEP_BACK); 145 this.buttonsMap.put("stepForward", ButtonsPanel.STEP_FORWARD); 146 147 try { 148 this.img = ImageIO.read(this.getClass().getResource( 149 "/brushed-metal.png")); 150 } catch (final IOException e) { 151 e.printStackTrace(); 152 } 153 154 // Set up the methods list (calls init()) 155 this.setButtons(new String[] { "pause", "play", "stop" }); 156 157 this.setPreferredSize(new Dimension( 158 (100 + this.inset) * this.buttons.length, 159 100 + this.inset)); 160 this.setSize(this.getPreferredSize()); 161 162 VideoPlayer.this.addVideoListener(this); 163 164 } 165 166 /** 167 * Set the list of buttons available on the player. The array of 168 * strings should match the names of methods in the 169 * {@link VideoPlayer} class for navigating the video. That is 170 * {@link VideoPlayer#pause()}, {@link VideoPlayer#stop()}, 171 * {@link VideoPlayer#play()}, {@link VideoPlayer#stepBack()} or 172 * {@link VideoPlayer#stepForward()}. The order specifies the order 173 * they will be shown in the player. 174 * 175 * @param buttons 176 * The order of the buttons 177 */ 178 public void setButtons(final String[] buttons) { 179 this.buttons = buttons; 180 181 final ArrayList<Method> methodsList = new ArrayList<Method>(); 182 for (final String button : buttons) { 183 // Make sure we're only allowing the methods predetermined 184 // by us, so not any old method could be put in. 185 if (this.buttonsMap.get(button) != null) { 186 try { 187 methodsList.add(VideoPlayer.this.getClass().getMethod(button)); 188 } catch (final SecurityException e) { 189 e.printStackTrace(); 190 } catch (final NoSuchMethodException e) { 191 e.printStackTrace(); 192 } 193 } 194 } 195 196 this.methods = methodsList.toArray(new Method[0]); 197 this.init(); 198 } 199 200 /** 201 * 202 */ 203 private void init() { 204 this.removeAll(); 205 this.setLayout(new GridBagLayout()); 206 this.setOpaque(false); 207 208 final GridBagConstraints gbc = new GridBagConstraints(); 209 gbc.fill = GridBagConstraints.HORIZONTAL; 210 gbc.weightx = gbc.weighty = 0; 211 gbc.gridx = gbc.gridy = 1; 212 gbc.insets = new Insets(this.inset, this.inset, this.inset, this.inset); 213 214 // ------------------------------------------------------------ 215 // Progress bar 216 // ------------------------------------------------------------ 217 gbc.gridy = 0; 218 gbc.weightx = 1; 219 gbc.gridwidth = this.buttons.length; 220 this.add(this.progress, gbc); 221 this.progress.addMouseListener(new MouseAdapter() { 222 @Override 223 public void mouseClicked(final MouseEvent e) { 224 System.out.println("Clicked at " + e.getX()); 225 VideoPlayer.this.setPosition(e.getX() * 100 / 226 ButtonsPanel.this.getWidth()); 227 } 228 }); 229 230 // ------------------------------------------------------------ 231 // Navigation Buttons 232 // ------------------------------------------------------------ 233 final JPanel buttonsPanel = new JPanel(new GridBagLayout()); 234 buttonsPanel.setBorder(BorderFactory.createEmptyBorder()); 235 buttonsPanel.setOpaque(false); 236 237 gbc.weightx = gbc.weighty = 0; 238 gbc.gridx = gbc.gridy = 1; 239 gbc.gridwidth = 1; 240 for (int i = 0; i < this.buttons.length; i++) { 241 final String b = this.buttons[i]; 242 final ImageIcon buttonIcon = new ImageIcon(this.getClass() 243 .getResource(this.buttonsMap.get(b))); 244 final JLabel button = new JLabel(buttonIcon); 245 button.setBorder(BorderFactory.createEmptyBorder()); 246 final int j = i; 247 button.addMouseListener(new MouseAdapter() { 248 @Override 249 public void mouseClicked(final MouseEvent e) { 250 try { 251 ButtonsPanel.this.methods[j].invoke( 252 VideoPlayer.this); 253 } catch (final IllegalArgumentException e1) { 254 e1.printStackTrace(); 255 } catch (final IllegalAccessException e1) { 256 e1.printStackTrace(); 257 } catch (final InvocationTargetException e1) { 258 e1.printStackTrace(); 259 } 260 }; 261 262 @Override 263 public void mouseEntered(final MouseEvent e) { 264 button.setBorder(BorderFactory.createLineBorder(Color.yellow)); 265 }; 266 267 @Override 268 public void mouseExited(final MouseEvent e) { 269 button.setBorder(BorderFactory.createEmptyBorder()); 270 }; 271 }); 272 buttonsPanel.add(button, gbc); 273 gbc.gridx++; 274 } 275 buttonsPanel.add(this.label, gbc); 276 277 gbc.gridy = 2; 278 gbc.gridx = 1; 279 this.add(buttonsPanel, gbc); 280 } 281 282 @Override 283 public void paint(final Graphics g) { 284 g.drawImage(this.img, 0, 0, null); 285 super.paint(g); 286 } 287 288 /** 289 * Set the progress (0-100) 290 * 291 * @param pc 292 * The %age value 293 */ 294 public void setProgress(final double pc) { 295 this.progress.setValue((int) pc); 296 } 297 298 @Override 299 public void afterUpdate(final VideoDisplay<T> display) { 300 this.setProgress(display.getPosition()); 301 302 // The end timecode 303 final HrsMinSecFrameTimecode end = new HrsMinSecFrameTimecode( 304 VideoPlayer.this.getVideo().countFrames(), 305 VideoPlayer.this.getVideo().getFPS()); 306 307 final HrsMinSecFrameTimecode current = new HrsMinSecFrameTimecode( 308 VideoPlayer.this.getVideo().currentFrame, 309 VideoPlayer.this.getVideo().getFPS()); 310 311 this.label.setText(current.toString() + " / " + end.toString()); 312 } 313 314 @Override 315 public void beforeUpdate(final T frame) { 316 } 317 } 318 319 /** 320 * Class used to animate the buttons panel on and off the screen. 321 * 322 * @author David Dupplaw (dpd@ecs.soton.ac.uk) 323 * @created 14 Aug 2012 324 * @version $Author$, $Revision$, $Date$ 325 */ 326 public class AnimatorThread implements Runnable { 327 public boolean stopNow = false; 328 public boolean buttonValue; 329 330 /** 331 * Create a new animator thread. If the thread succeeds the 332 * showButtons value will be set to the tf value given. 333 * 334 * @param tf 335 * Whether the buttons are shown (TRUE) or hidden 336 */ 337 public AnimatorThread(final boolean tf) { 338 this.buttonValue = tf; 339 } 340 341 @Override 342 public void run() { 343 // Animate the buttons 344 while (!this.stopNow && VideoPlayerComponent.this.animator != null && 345 !VideoPlayerComponent.this.animator.isComplete()) 346 { 347 VideoPlayerComponent.this.bp.setBounds( 348 VideoPlayerComponent.this.bp.getBounds().x, 349 VideoPlayerComponent.this.animator.nextValue(), 350 VideoPlayerComponent.this.bp.getBounds().width, 351 VideoPlayerComponent.this.bp.getBounds().height); 352 try { 353 // Sleep for 40ms - animates at roughly 25fps 354 Thread.sleep(40); 355 } catch (final InterruptedException e) { 356 } 357 } 358 359 if (!this.stopNow) 360 VideoPlayerComponent.this.showButtons = this.buttonValue; 361 } 362 } 363 364 /** The buttons panel */ 365 private ButtonsPanel bp = null; 366 367 /** Whether to show the buttons */ 368 private boolean showButtons = true; 369 370 /** The current mode of the buttons */ 371 private Mode currentMode = Mode.PLAY; 372 373 /** The animator used to animate the buttons */ 374 private LinearTimeBasedIntegerValueAnimator animator = null; 375 376 /** The animator thread */ 377 private AnimatorThread animatorThread = null; 378 379 /** 380 * Create a new player component using the display component 381 * 382 * @param ic 383 * The video display component 384 */ 385 public VideoPlayerComponent(final ImageComponent ic) { 386 try { 387 this.init(ic); 388 } catch (final SecurityException e) { 389 e.printStackTrace(); 390 } catch (final NoSuchMethodException e) { 391 e.printStackTrace(); 392 } 393 } 394 395 /** 396 * Set up the widgets 397 * 398 * @param ic 399 * The video display component 400 * @throws NoSuchMethodException 401 * @throws SecurityException 402 */ 403 private void init(final ImageComponent ic) 404 throws SecurityException, NoSuchMethodException 405 { 406 this.setLayout(null); 407 408 // Add the buttons 409 this.bp = new ButtonsPanel(); 410 this.add(this.bp); 411 412 // Add the video 413 this.add(ic); 414 415 // Set the size of the components based on the video component 416 this.setPreferredSize(ic.getSize()); 417 this.setSize(ic.getSize()); 418 419 // Position the buttons panel 420 this.bp.setBounds(0, this.getHeight() - this.bp.getSize().height, 421 this.getWidth(), 422 this.bp.getSize().height); 423 424 this.showButtons = true; 425 426 // Add a mouse listener to toggle the button display. 427 final MouseAdapter ma = new MouseAdapter() { 428 @Override 429 public void mouseEntered(final MouseEvent e) { 430 VideoPlayerComponent.this.setShowButtons(true); 431 }; 432 433 @Override 434 public void mouseExited(final MouseEvent e) { 435 if (!VideoPlayerComponent.this.getVisibleRect().contains( 436 e.getPoint())) 437 { 438 VideoPlayerComponent.this.setShowButtons(false); 439 } 440 }; 441 }; 442 ic.addMouseListener(ma); 443 this.bp.addMouseListener(ma); 444 } 445 446 /** 447 * Reset the button states to the current state of the video player 448 */ 449 public void updateButtonStates() { 450 // If we're changing mode 451 if (this.currentMode != VideoPlayer.this.getMode()) { 452 // Pop the buttons up if the mode changes. 453 this.showButtons = true; 454 455 // TODO: Update the graphics depending on the mode 456 switch (VideoPlayer.this.getMode()) { 457 case PLAY: 458 break; 459 case STOP: 460 break; 461 case PAUSE: 462 break; 463 default: 464 break; 465 } 466 467 // Update the buttons to reflect the current video player mode 468 this.currentMode = VideoPlayer.this.getMode(); 469 } 470 } 471 472 /** 473 * Set whether the buttons are in view or not. 474 * 475 * @param tf 476 * TRUE to show the buttons 477 */ 478 public void setShowButtons(final boolean tf) { 479 // Only need to do anything if the buttons are different to what 480 // we want. 481 if (tf != this.showButtons) { 482 // Kill the current thread if there is one 483 if (this.animatorThread != null) { 484 this.animatorThread.stopNow = true; 485 this.animatorThread = null; 486 } 487 488 // Create an animator to animate the buttons over 1/2 second 489 // Animates from the current position to either off the screen 490 // or on the screen depending on the value of tf 491 this.animator = new LinearTimeBasedIntegerValueAnimator( 492 this.bp.getBounds().y, 493 this.getHeight() - (tf ? this.bp.getSize().height : 0), 494 500); 495 496 // Start the thread 497 this.animatorThread = new AnimatorThread(tf); 498 new Thread(this.animatorThread).start(); 499 500 this.showButtons = tf; 501 } 502 } 503 } 504 505 /** The frame showing the player */ 506 private JFrame frame = null; 507 508 /** The player component */ 509 private VideoPlayerComponent component = null; 510 511 /** 512 * Create the video player to play the given video. 513 * 514 * @param v 515 * The video to play 516 */ 517 public VideoPlayer(final Video<T> v) { 518 this(v, null, new ImageComponent()); 519 } 520 521 /** 522 * Create the video player to play the given video. 523 * 524 * @param v 525 * The video to play 526 * @param audio 527 * The audio to play 528 */ 529 public VideoPlayer(final Video<T> v, final AudioStream audio) { 530 this(v, audio, new ImageComponent()); 531 } 532 533 /** 534 * Created the video player for the given video on the given image 535 * component. 536 * 537 * @param v 538 * The video 539 * @param audio 540 * The audio 541 * @param screen 542 * The screen to draw the video to. 543 */ 544 protected VideoPlayer(final Video<T> v, final AudioStream audio, final ImageComponent screen) { 545 super(v, audio, screen); 546 547 screen.setSize(v.getWidth(), v.getHeight()); 548 screen.setPreferredSize(new Dimension(v.getWidth(), v.getHeight())); 549 screen.setAllowZoom(false); 550 screen.setAllowPanning(false); 551 screen.setTransparencyGrid(false); 552 screen.setShowPixelColours(false); 553 screen.setShowXYPosition(false); 554 555 this.component = new VideoPlayerComponent(screen); 556 this.component.setShowButtons(false); 557 this.addVideoDisplayStateListener(this); 558 } 559 560 /** 561 * Creates a new video player in a new thread and starts it running 562 * (initially in pause mode). 563 * 564 * @param video 565 * The video 566 * @return The video player 567 */ 568 public static <T extends Image<?, T>> VideoPlayer<T> createVideoPlayer( 569 final Video<T> video) 570 { 571 final VideoPlayer<T> vp = new VideoPlayer<T>(video); 572 new Thread(vp).start(); 573 return vp; 574 } 575 576 /** 577 * Creates a new video player in a new thread and starts it running 578 * (initially in pause mode). 579 * 580 * @param video 581 * The video 582 * @param audio 583 * The udio 584 * @return The video player 585 */ 586 public static <T extends Image<?, T>> VideoPlayer<T> createVideoPlayer( 587 final Video<T> video, final AudioStream audio) 588 { 589 final VideoPlayer<T> vp = new VideoPlayer<T>(video, audio); 590 new Thread(vp).start(); 591 return vp; 592 } 593 594 /** 595 * Shows the video player in a frame. If a frame already exists it will be 596 * made visible. 597 * 598 * @return Returns the frame shown 599 */ 600 public JFrame showFrame() { 601 if (this.frame == null) { 602 this.frame = new JFrame(); 603 this.frame.add(this.component); 604 this.frame.pack(); 605 } 606 607 this.frame.setVisible(true); 608 return this.frame; 609 } 610 611 /** 612 * Returns a JPanel video player which can be incorporated into other GUIs. 613 * 614 * @return A VideoPlayer in a JPanel 615 */ 616 public JPanel getVideoPlayerPanel() { 617 return this.component; 618 } 619 620 /** 621 * Play the video. 622 */ 623 public void play() { 624 this.setMode(Mode.PLAY); 625 } 626 627 /** 628 * Stop the video 629 */ 630 public void stop() { 631 this.setMode(Mode.STOP); 632 } 633 634 /** 635 * Pause the video 636 */ 637 public void pause() { 638 this.setMode(Mode.PAUSE); 639 } 640 641 /** 642 * Step back a frame. 643 */ 644 public void stepBack() { 645 646 } 647 648 /** 649 * Step forward a frame. 650 */ 651 public void stepForward() { 652 653 } 654 655 /** 656 * {@inheritDoc} 657 * 658 * @see org.openimaj.video.VideoDisplayStateListener#videoStopped(org.openimaj.video.VideoDisplay) 659 */ 660 @Override 661 public void videoStopped(final VideoDisplay<?> v) { 662 // If this is called it means the video mode was changed and the video 663 // has stopped playing. We must let our buttons know that this has 664 // happened. 665 this.component.updateButtonStates(); 666 } 667 668 /** 669 * {@inheritDoc} 670 * 671 * @see org.openimaj.video.VideoDisplayStateListener#videoPlaying(org.openimaj.video.VideoDisplay) 672 */ 673 @Override 674 public void videoPlaying(final VideoDisplay<?> v) { 675 // If this is called it means the video mode was changed and the video 676 // has started playing. We must let our buttons know that this has 677 // happened. 678 this.component.updateButtonStates(); 679 } 680 681 /** 682 * {@inheritDoc} 683 * 684 * @see org.openimaj.video.VideoDisplayStateListener#videoPaused(org.openimaj.video.VideoDisplay) 685 */ 686 @Override 687 public void videoPaused(final VideoDisplay<?> v) { 688 // If this is called it means the video mode was changed and the video 689 // has been paused. We must let our buttons know that this has happened. 690 this.component.updateButtonStates(); 691 } 692 693 /** 694 * {@inheritDoc} 695 * 696 * @see org.openimaj.video.VideoDisplayStateListener#videoStateChanged(org.openimaj.video.VideoDisplay.Mode, 697 * org.openimaj.video.VideoDisplay) 698 */ 699 @Override 700 public void videoStateChanged( 701 final org.openimaj.video.VideoDisplay.Mode mode, 702 final VideoDisplay<?> v) 703 { 704 // As we've implemented the other methods in this listener, so 705 // we don't need to implement this one too. 706 } 707 708 /** 709 * Set the buttons to show on this video player. Available buttons are: 710 * <p> 711 * <ul> 712 * <li>play</li> 713 * <li>stop</li> 714 * <li>pause</li> 715 * <li>stepBack</li> 716 * <li>stepForward</li> 717 * </ul> 718 * <p> 719 * Buttons not from this list will be ignored. 720 * <p> 721 * The order of the array will determine the order of the buttons shown on 722 * the player. 723 * 724 * @param buttons 725 * The buttons to show on the player. 726 */ 727 public void setButtons(final String[] buttons) { 728 this.component.bp.setButtons(buttons); 729 } 730}