1 /* 2 * Copyright (c) 2010, 2015, Oracle and/or its affiliates. All rights reserved. 3 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. 4 * 5 * This code is free software; you can redistribute it and/or modify it 6 * under the terms of the GNU General Public License version 2 only, as 7 * published by the Free Software Foundation. Oracle designates this 8 * particular file as subject to the "Classpath" exception as provided 9 * by Oracle in the LICENSE file that accompanied this code. 10 * 11 * This code is distributed in the hope that it will be useful, but WITHOUT 12 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or 13 * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License 14 * version 2 for more details (a copy is included in the LICENSE file that 15 * accompanied this code). 16 * 17 * You should have received a copy of the GNU General Public License version 18 * 2 along with this work; if not, write to the Free Software Foundation, 19 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. 20 * 21 * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA 22 * or visit www.oracle.com if you need additional information or have any 23 * questions. 24 */ 25 26 package javafx.scene.media; 27 28 import java.lang.ref.WeakReference; 29 import java.util.HashSet; 30 import java.util.Iterator; 31 import java.util.Set; 32 import java.util.Timer; 33 import java.util.TimerTask; 34 import java.util.List; 35 import java.util.ListIterator; 36 import java.util.ArrayList; 37 38 import javafx.application.Platform; 39 import javafx.beans.NamedArg; 40 import javafx.beans.property.BooleanProperty; 41 import javafx.beans.property.BooleanPropertyBase; 42 import javafx.beans.property.DoubleProperty; 43 import javafx.beans.property.DoublePropertyBase; 44 import javafx.beans.property.IntegerProperty; 45 import javafx.beans.property.IntegerPropertyBase; 46 import javafx.beans.property.ObjectProperty; 47 import javafx.beans.property.ObjectPropertyBase; 48 import javafx.beans.property.SimpleObjectProperty; 49 import javafx.collections.MapChangeListener; 50 import javafx.collections.ObservableMap; 51 import javafx.util.Duration; 52 import javafx.util.Pair; 53 54 import com.sun.javafx.tk.TKPulseListener; 55 import com.sun.javafx.tk.Toolkit; 56 import com.sun.media.jfxmedia.MediaManager; 57 import com.sun.media.jfxmedia.control.VideoDataBuffer; 58 import com.sun.media.jfxmedia.effects.AudioSpectrum; 59 import com.sun.media.jfxmedia.events.AudioSpectrumEvent; 60 import com.sun.media.jfxmedia.events.BufferListener; 61 import com.sun.media.jfxmedia.events.BufferProgressEvent; 62 import com.sun.media.jfxmedia.events.MarkerEvent; 63 import com.sun.media.jfxmedia.events.MarkerListener; 64 import com.sun.media.jfxmedia.events.NewFrameEvent; 65 import com.sun.media.jfxmedia.events.PlayerStateEvent; 66 import com.sun.media.jfxmedia.events.PlayerStateListener; 67 import com.sun.media.jfxmedia.events.PlayerTimeListener; 68 import com.sun.media.jfxmedia.events.VideoTrackSizeListener; 69 import com.sun.media.jfxmedia.locator.Locator; 70 import java.util.*; 71 import javafx.beans.property.ReadOnlyDoubleProperty; 72 import javafx.beans.property.ReadOnlyDoubleWrapper; 73 import javafx.beans.property.ReadOnlyIntegerProperty; 74 import javafx.beans.property.ReadOnlyIntegerWrapper; 75 import javafx.beans.property.ReadOnlyObjectProperty; 76 import javafx.beans.property.ReadOnlyObjectWrapper; 77 import javafx.event.EventHandler; 78 79 /** 80 * The <code>MediaPlayer</code> class provides the controls for playing media. 81 * It is used in combination with the {@link Media} and {@link MediaView} 82 * classes to display and control media playback. <code>MediaPlayer</code> does 83 * not contain any visual elements so must be used with the {@link MediaView} 84 * class to view any video track which may be present. 85 * 86 * <p><code>MediaPlayer</code> provides the {@link #pause()}, {@link #play()}, 87 * {@link #stop()} and {@link #seek(javafx.util.Duration) seek()} controls as 88 * well as the {@link #rateProperty rate} and {@link #autoPlayProperty autoPlay} 89 * properties which apply to all types of media. It also provides the 90 * {@link #balanceProperty balance}, {@link #muteProperty mute}, and 91 * {@link #volumeProperty volume} properties which control audio playback 92 * characteristics. Further control over audio quality may be attained via the 93 * {@link AudioEqualizer} associated with the player. Frequency descriptors of 94 * audio playback may be observed by registering an {@link AudioSpectrumListener}. 95 * Information about playback position, rate, and buffering may be obtained from 96 * the {@link #currentTimeProperty currentTime}, 97 * {@link #currentRateProperty currentRate}, and 98 * {@link #bufferProgressTimeProperty bufferProgressTime} 99 * properties, respectively. Media marker notifications are received by an event 100 * handler registered as the {@link #onMarkerProperty onMarker} property.</p> 101 * 102 * <p>For finite duration media, playback may be positioned at any point in time 103 * between <code>0.0</code> and the duration of the media. <code>MediaPlayer</code> 104 * refines this definition by adding the {@link #startTimeProperty startTime} and 105 * {@link #stopTimeProperty stopTime} 106 * properties which in effect define a virtual media source with time position 107 * constrained to <code>[startTime,stopTime]</code>. Media playback 108 * commences at <code>startTime</code> and continues to <code>stopTime</code>. 109 * The interval defined by these two endpoints is termed a <i>cycle</i> with 110 * duration being the difference of the stop and start times. This cycle 111 * may be set to repeat a specific or indefinite number of times. The total 112 * duration of media playback is then the product of the cycle duration and the 113 * number of times the cycle is played. If the stop time of the cycle is reached 114 * and the cycle is to be played again, the event handler registered with the 115 * {@link #onRepeatProperty onRepeat} property is invoked. If the stop time is reached and 116 * the cycle is <i>not</i> to be repeated, then the event handler registered 117 * with the {@link #onEndOfMediaProperty onEndOfMedia} property is invoked. A zero-relative index of 118 * which cycle is presently being played is maintained by {@link #currentCountProperty currentCount}. 119 * </p> 120 * 121 * <p>The operation of a <code>MediaPlayer</code> is inherently asynchronous. 122 * A player is not prepared to respond to commands quasi-immediately until 123 * its status has transitioned to {@link Status#READY}, which in 124 * effect generally occurs when media pre-roll completes. Some requests made of 125 * a player prior to its status being <code>READY</code> will however take 126 * effect when that status is entered. These include invoking {@link #play()} 127 * without an intervening invocation of {@link #pause()} or {@link #stop()} 128 * before the <code>READY</code> transition, as well as setting any of the 129 * {@link #autoPlayProperty autoPlay}, {@link #balanceProperty balance}, 130 * {@link #muteProperty mute}, {@link #rateProperty rate}, 131 * {@link #startTimeProperty startTime}, {@link #stopTimeProperty stopTime}, and 132 * {@link #volumeProperty volume} properties.</p> 133 * 134 * <p>The {@link #statusProperty status} 135 * property may be monitored to make the application aware of player status 136 * changes, and callback functions may be registered via properties such as 137 * {@link #onReadyProperty onReady} if an action should be taken when a particular status is 138 * entered. There are also {@link #errorProperty error} and {@link #onErrorProperty onError} properties which 139 * respectively enable monitoring when an error occurs and taking a specified 140 * action in response thereto.</p> 141 * 142 * <p>The same <code>MediaPlayer</code> object may be shared among multiple 143 * <code>MediaView</code>s. This will not affect the player itself. In 144 * particular, the property settings of the view will not have any effect on 145 * media playback.</p> 146 * @see Media 147 * @see MediaView 148 * @since JavaFX 2.0 149 */ 150 public final class MediaPlayer { 151 152 /** 153 * Enumeration describing the different status values of a {@link MediaPlayer}. 154 * 155 * The principal <code>MediaPlayer</code> status transitions are given in the 156 * following table: 157 * <table border="1" summary="MediaPlayer status transition table"> 158 * <tr> 159 * <th>Current \ Next</th><th>READY</th><th>PAUSED</th> 160 * <th>PLAYING</th><th>STALLED</th><th>STOPPED</th> 161 * </tr> 162 * <tr> 163 * <td><b>UNKNOWN</b></td><td>pre-roll</td><td></td><td></td><td></td><td></td> 164 * </tr> 165 * <tr> 166 * <td><b>READY</b><td></td></td><td></td><td>autoplay; play()</td><td></td><td></td> 167 * </tr> 168 * <tr> 169 * <td><b>PAUSED</b><td></td></td><td></td><td>play()</td><td></td><td>stop()</td> 170 * </tr> 171 * <tr> 172 * <td><b>PLAYING</b><td></td></td><td>pause()</td><td></td><td>buffering data</td><td>stop()</td> 173 * </tr> 174 * <tr> 175 * <td><b>STALLED</b><td></td></td><td>pause()</td><td>data buffered</td><td></td><td>stop()</td> 176 * </tr> 177 * <tr> 178 * <td><b>STOPPED</b><td></td></td><td>pause()</td><td>play()</td><td></td><td></td> 179 * </tr> 180 * </table> 181 * </p> 182 * <p>The table rows represent the current state of the player and the columns 183 * the next state of the player. The cell at the intersection of a given row 184 * and column lists the events which can cause a transition from the row 185 * state to the column state. An empty cell represents an impossible transition. 186 * The transitions to <code>UNKNOWN</code> and to and from <code>HALTED</code> 187 * status are intentionally not tabulated. <code>UNKNOWN</code> is the initial 188 * status of the player before the media source is pre-rolled and cannot be 189 * entered once exited. <code>HALTED</code> is a terminal status entered when 190 * an error occurs and may be transitioned into from any other status but not 191 * exited. 192 * </p> 193 * <p> 194 * The principal <code>MediaPlayer</code> status values and transitions are 195 * depicted in the following diagram: 196 * <br/><br/> 197 * <img src="doc-files/mediaplayerstatus.png" alt="MediaPlayer status diagram"/> 198 * </p> 199 * <p> 200 * Reaching the end of the media (or the 201 * {@link #stopTimeProperty stopTime} if this is defined) while playing does not cause the 202 * status to change from <code>PLAYING</code>. Therefore, for example, if 203 * the media is played to its end and then a manual seek to an earlier 204 * time within the media is performed, playing will continue from the 205 * new media time. 206 * </p> 207 * @since JavaFX 2.0 208 */ 209 public enum Status { 210 211 /** 212 * State of the player immediately after creation. While in this state, 213 * property values are not reliable and should not be considered. 214 * Additionally, commands sent to the player while in this state will be 215 * buffered until the media is fully loaded and ready to play. 216 */ 217 UNKNOWN, 218 /** 219 * State of the player once it is prepared to play. 220 * This state is entered only once when the movie is loaded and pre-rolled. 221 */ 222 READY, 223 /** 224 * State of the player when playback is paused. Requesting the player 225 * to play again will cause it to continue where it left off. 226 */ 227 PAUSED, 228 /** 229 * State of the player when it is currently playing. 230 */ 231 PLAYING, 232 /** 233 * State of the player when playback has stopped. Requesting the player 234 * to play again will cause it to start playback from the beginning. 235 */ 236 STOPPED, 237 /** 238 * State of the player when data coming into the buffer has slowed or 239 * stopped and the playback buffer does not have enough data to continue 240 * playing. Playback will continue automatically when enough data are 241 * buffered to resume playback. If paused or stopped in this state, then 242 * buffering will continue but playback will not resume automatically 243 * when sufficient data are buffered. 244 */ 245 STALLED, 246 /** 247 * State of the player when a critical error has occurred. This state 248 * indicates playback can never continue again with this player. The 249 * player is no longer functional and a new player should be created. 250 */ 251 HALTED, 252 /** 253 * State of the player after dispose() method is invoked. This state indicates 254 * player is disposed, all resources are free and player SHOULD NOT be used again. 255 * <code>Media</code> and <code>MediaView</code> objects associated with disposed player can be reused. 256 * @since JavaFX 8.0 257 */ 258 DISPOSED 259 }; 260 261 /** 262 * A value representing an effectively infinite number of playback cycles. 263 * When {@link #cycleCountProperty cycleCount} is set to this value, the player 264 * will replay the <code>Media</code> until stopped or paused. 265 */ 266 public static final int INDEFINITE = -1; // Note: this is a count, not a Duration. 267 268 private static final double RATE_MIN = 0.0; 269 private static final double RATE_MAX = 8.0; 270 271 private static final int AUDIOSPECTRUM_THRESHOLD_MAX = 0; // dB 272 273 private static final double AUDIOSPECTRUM_INTERVAL_MIN = 0.000000001; // seconds 274 275 private static final int AUDIOSPECTRUM_NUMBANDS_MIN = 2; 276 277 // The underlying player 278 private com.sun.media.jfxmedia.MediaPlayer jfxPlayer; 279 // Need package getter for MediaView 280 com.sun.media.jfxmedia.MediaPlayer retrieveJfxPlayer() { 281 synchronized (disposeLock) { 282 return jfxPlayer; 283 } 284 } 285 286 private MapChangeListener<String,Duration> markerMapListener = null; 287 private MarkerListener markerEventListener = null; 288 289 private PlayerStateListener stateListener = null; 290 private PlayerTimeListener timeListener = null; 291 private VideoTrackSizeListener sizeListener = null; 292 private com.sun.media.jfxmedia.events.MediaErrorListener errorListener = null; 293 private BufferListener bufferListener = null; 294 private com.sun.media.jfxmedia.events.AudioSpectrumListener spectrumListener = null; 295 private RendererListener rendererListener = null; 296 297 // Store requested operations sent before we receive the onReady event 298 private boolean rateChangeRequested = false; 299 private boolean volumeChangeRequested = false; 300 private boolean balanceChangeRequested = false; 301 private boolean startTimeChangeRequested = false; 302 private boolean stopTimeChangeRequested = false; 303 private boolean muteChangeRequested = false; 304 private boolean playRequested = false; 305 private boolean audioSpectrumNumBandsChangeRequested = false; 306 private boolean audioSpectrumIntervalChangeRequested = false; 307 private boolean audioSpectrumThresholdChangeRequested = false; 308 private boolean audioSpectrumEnabledChangeRequested = false; 309 310 private MediaTimerTask mediaTimerTask = null; 311 private double prevTimeMs = -1.0; 312 private boolean isUpdateTimeEnabled = false; 313 private BufferProgressEvent lastBufferEvent = null; 314 private Duration startTimeAtStop = null; 315 private boolean isEOS = false; 316 317 private final Object disposeLock = new Object(); 318 319 private final static int DEFAULT_SPECTRUM_BAND_COUNT = 128; 320 private final static double DEFAULT_SPECTRUM_INTERVAL = 0.1; 321 private final static int DEFAULT_SPECTRUM_THRESHOLD = -60; 322 323 // views to be notified on media change 324 private final Set<WeakReference<MediaView>> viewRefs = 325 new HashSet<WeakReference<MediaView>>(); 326 327 /** 328 * The read-only {@link AudioEqualizer} associated with this player. The 329 * equalizer is enabled by default. 330 */ 331 private AudioEqualizer audioEqualizer; 332 333 private static double clamp(double dvalue, double dmin, double dmax) { 334 if (dmin != Double.MIN_VALUE && dvalue < dmin) { 335 return dmin; 336 } else if (dmax != Double.MAX_VALUE && dvalue > dmax) { 337 return dmax; 338 } else { 339 return dvalue; 340 } 341 } 342 343 private static int clamp(int ivalue, int imin, int imax) { 344 if (imin != Integer.MIN_VALUE && ivalue < imin) { 345 return imin; 346 } else if (imax != Integer.MAX_VALUE && ivalue > imax) { 347 return imax; 348 } else { 349 return ivalue; 350 } 351 } 352 353 /** 354 * Retrieve the {@link AudioEqualizer} associated with this player. 355 * @return the <code>AudioEqualizer</code> or <code>null</code> if player is disposed. 356 */ 357 public final AudioEqualizer getAudioEqualizer() { 358 synchronized (disposeLock) { 359 if (getStatus() == Status.DISPOSED) { 360 return null; 361 } 362 363 if (audioEqualizer == null) { 364 audioEqualizer = new AudioEqualizer(); 365 if (jfxPlayer != null) { 366 audioEqualizer.setAudioEqualizer(jfxPlayer.getEqualizer()); 367 } 368 audioEqualizer.setEnabled(true); 369 } 370 return audioEqualizer; 371 } 372 } 373 374 /** 375 * Create a player for a specific media. This is the only way to associate 376 * a <code>Media</code> object with a <code>MediaPlayer</code>: once the 377 * player is created it cannot be changed. Errors which occur synchronously 378 * within the constructor will cause exceptions to be thrown. Errors which 379 * occur asynchronously will cause the {@link #errorProperty error} property to be set and 380 * consequently any {@link #onErrorProperty onError} callback to be invoked. 381 * 382 * <p>When created, the {@link #statusProperty status} of the player will be {@link Status#UNKNOWN}. 383 * Once the <code>status</code> has transitioned to {@link Status#READY} the 384 * player will be in a usable condition. The amount of time between player 385 * creation and its entering <code>READY</code> status may vary depending, 386 * for example, on whether the media is being read over a network connection 387 * or from a local file system. 388 * 389 * @param media The media to play. 390 * @throws NullPointerException if media is <code>null</code>. 391 * @throws MediaException if any synchronous errors occur within the 392 * constructor. 393 */ 394 public MediaPlayer(@NamedArg("media") Media media) { 395 if (null == media) { 396 throw new NullPointerException("media == null!"); 397 } 398 399 this.media = media; 400 401 // So we can get errors during initialization from other threads (Ex. HLS). 402 errorListener = new _MediaErrorListener(); 403 MediaManager.addMediaErrorListener(errorListener); 404 405 try { 406 // Init MediaPlayer. Run on separate thread if locator can block. 407 Locator locator = media.retrieveJfxLocator(); 408 if (locator.canBlock()) { 409 InitMediaPlayer initMediaPlayer = new InitMediaPlayer(); 410 Thread t = new Thread(initMediaPlayer); 411 t.setDaemon(true); 412 t.start(); 413 } else { 414 init(); 415 } 416 } catch (com.sun.media.jfxmedia.MediaException e) { 417 throw MediaException.exceptionToMediaException(e); 418 } catch (MediaException e) { 419 throw e; 420 } 421 } 422 423 void registerListeners() { 424 synchronized (disposeLock) { 425 if (getStatus() == Status.DISPOSED) { 426 return; 427 } 428 429 if (jfxPlayer != null) { 430 // Register jfxPlayer for dispose. It will be disposed when FX MediaPlayer does not have 431 // any strong references. 432 MediaManager.registerMediaPlayerForDispose(this, jfxPlayer); 433 434 jfxPlayer.addMediaErrorListener(errorListener); 435 436 jfxPlayer.addMediaTimeListener(timeListener); 437 jfxPlayer.addVideoTrackSizeListener(sizeListener); 438 jfxPlayer.addBufferListener(bufferListener); 439 jfxPlayer.addMarkerListener(markerEventListener); 440 jfxPlayer.addAudioSpectrumListener(spectrumListener); 441 jfxPlayer.getVideoRenderControl().addVideoRendererListener(rendererListener); 442 jfxPlayer.addMediaPlayerListener(stateListener); 443 } 444 445 if (null != rendererListener) { 446 // add a stage listener, this will be called before scene listeners 447 // so we can make sure the dirty bits are set correctly before PG sync 448 Toolkit.getToolkit().addStageTkPulseListener(rendererListener); 449 } 450 } 451 } 452 453 private void init() throws MediaException { 454 try { 455 // Create a new player 456 Locator locator = media.retrieveJfxLocator(); 457 458 // This call will block until we connected or fail to connect. 459 // Call it here, so we do not block while initializing and holding locks like disposeLock. 460 locator.waitForReadySignal(); 461 462 synchronized (disposeLock) { 463 if (getStatus() == Status.DISPOSED) { 464 return; 465 } 466 467 jfxPlayer = MediaManager.getPlayer(locator); 468 469 if (jfxPlayer != null) { 470 // Register media player with shutdown hook. 471 MediaPlayerShutdownHook.addMediaPlayer(this); 472 473 // Make sure we start with a known state 474 jfxPlayer.setBalance((float) getBalance()); 475 jfxPlayer.setMute(isMute()); 476 jfxPlayer.setVolume((float) getVolume()); 477 478 // Create listeners for the Player's event 479 sizeListener = new _VideoTrackSizeListener(); 480 stateListener = new _PlayerStateListener(); 481 timeListener = new _PlayerTimeListener(); 482 bufferListener = new _BufferListener(); 483 markerEventListener = new _MarkerListener(); 484 spectrumListener = new _SpectrumListener(); 485 rendererListener = new RendererListener(); 486 } 487 488 // Listen to Media.getMarkers() so as to propagate updates of the 489 // map to the implementation layer. 490 markerMapListener = new MarkerMapChangeListener(); 491 ObservableMap<String, Duration> markers = media.getMarkers(); 492 markers.addListener(markerMapListener); 493 494 // Propagate to the implementation layer any markers already in 495 // Media.getMarkers(). 496 com.sun.media.jfxmedia.Media jfxMedia = jfxPlayer.getMedia(); 497 for (Map.Entry<String, Duration> entry : markers.entrySet()) { 498 String markerName = entry.getKey(); 499 if (markerName != null) { 500 Duration markerTime = entry.getValue(); 501 if (markerTime != null) { 502 double msec = markerTime.toMillis(); 503 if (msec >= 0.0) { 504 jfxMedia.addMarker(markerName, msec / 1000.0); 505 } 506 } 507 } 508 } 509 } 510 } catch (com.sun.media.jfxmedia.MediaException e) { 511 throw MediaException.exceptionToMediaException(e); 512 } 513 514 // Register for the Player's event 515 Platform.runLater(() -> { 516 registerListeners(); 517 }); 518 } 519 520 private class InitMediaPlayer implements Runnable { 521 522 @Override 523 public void run() { 524 try { 525 init(); 526 } catch (com.sun.media.jfxmedia.MediaException e) { 527 handleError(MediaException.exceptionToMediaException(e)); 528 } catch (MediaException e) { 529 // Check media object for error. If it is connection related, then Media object will have better error message 530 if (media.getError() != null) { 531 handleError(media.getError()); 532 } else { 533 handleError(e); 534 } 535 } catch (Exception e) { 536 handleError(new MediaException(MediaException.Type.UNKNOWN, e.getMessage())); 537 } 538 } 539 } 540 541 /** 542 * Observable property set to a <code>MediaException</code> if an error occurs. 543 */ 544 private ReadOnlyObjectWrapper<MediaException> error; 545 546 private void setError(MediaException value) { 547 if (getError() == null) { 548 errorPropertyImpl().set(value); 549 } 550 } 551 552 /** 553 * Retrieve the value of the {@link #errorProperty error} property or <code>null</code> 554 * if there is no error. 555 * @return a <code>MediaException</code> or <code>null</code>. 556 */ 557 public final MediaException getError() { 558 return error == null ? null : error.get(); 559 } 560 561 public ReadOnlyObjectProperty<MediaException> errorProperty() { 562 return errorPropertyImpl().getReadOnlyProperty(); 563 } 564 565 private ReadOnlyObjectWrapper<MediaException> errorPropertyImpl() { 566 if (error == null) { 567 error = new ReadOnlyObjectWrapper<MediaException>() { 568 569 @Override 570 protected void invalidated() { 571 if (getOnError() != null) { 572 Platform.runLater(getOnError()); 573 } 574 } 575 576 @Override 577 public Object getBean() { 578 return MediaPlayer.this; 579 } 580 581 @Override 582 public String getName() { 583 return "error"; 584 } 585 }; 586 } 587 return error; 588 } 589 590 /** 591 * Event handler invoked when an error occurs. 592 */ 593 private ObjectProperty<Runnable> onError; 594 595 /** 596 * Sets the event handler to be called when an error occurs. 597 * @param value the event handler or <code>null</code>. 598 */ 599 public final void setOnError(Runnable value) { 600 onErrorProperty().set(value); 601 } 602 603 /** 604 * Retrieves the event handler for errors. 605 * @return the event handler. 606 */ 607 public final Runnable getOnError() { 608 return onError == null ? null : onError.get(); 609 } 610 611 public ObjectProperty<Runnable> onErrorProperty() { 612 if (onError == null) { 613 onError = new ObjectPropertyBase<Runnable>() { 614 615 @Override 616 protected void invalidated() { 617 /* 618 * if we have an existing error condition schedule the handler to be 619 * called immediately. This way the client app does not have to perform 620 * an explicit error check. 621 */ 622 if (get() != null && getError() != null) { 623 Platform.runLater(get()); 624 } 625 } 626 627 @Override 628 public Object getBean() { 629 return MediaPlayer.this; 630 } 631 632 @Override 633 public String getName() { 634 return "onError"; 635 } 636 }; 637 } 638 return onError; 639 } 640 641 /** 642 * The parent {@link Media} object; read-only. 643 * 644 * @see Media 645 */ 646 private Media media; 647 648 /** 649 * Retrieves the {@link Media} instance being played. 650 * @return the <code>Media</code> object. 651 */ 652 public final Media getMedia() { 653 return media; 654 } 655 656 /** 657 * Whether playing should start as soon as possible. For a new player this 658 * will occur once the player has reached the READY state. The default 659 * value is <code>false</code>. 660 * 661 * @see MediaPlayer.Status 662 */ 663 private BooleanProperty autoPlay; 664 665 /** 666 * Sets the {@link #autoPlayProperty autoPlay} property value. 667 * @param value whether to enable auto-playback 668 */ 669 public final void setAutoPlay(boolean value) { 670 autoPlayProperty().set(value); 671 } 672 673 /** 674 * Retrieves the {@link #autoPlayProperty autoPlay} property value. 675 * @return the value. 676 */ 677 public final boolean isAutoPlay() { 678 return autoPlay == null ? false : autoPlay.get(); 679 } 680 681 public BooleanProperty autoPlayProperty() { 682 if (autoPlay == null) { 683 autoPlay = new BooleanPropertyBase() { 684 685 @Override 686 protected void invalidated() { 687 if (autoPlay.get()) { 688 play(); 689 } else { 690 playRequested = false; 691 } 692 } 693 694 @Override 695 public Object getBean() { 696 return MediaPlayer.this; 697 } 698 699 @Override 700 public String getName() { 701 return "autoPlay"; 702 } 703 }; 704 } 705 return autoPlay; 706 } 707 708 private boolean playerReady; 709 710 /** 711 * Starts playing the media. If previously paused, then playback resumes 712 * where it was paused. If playback was stopped, playback starts 713 * from the {@link #startTimeProperty startTime}. When playing actually starts the 714 * {@link #statusProperty status} will be set to {@link Status#PLAYING}. 715 */ 716 public void play() { 717 synchronized (disposeLock) { 718 if (getStatus() != Status.DISPOSED) { 719 if (playerReady) { 720 jfxPlayer.play(); 721 } else { 722 playRequested = true; 723 } 724 } 725 } 726 } 727 728 /** 729 * Pauses the player. Once the player is actually paused the {@link #statusProperty status} 730 * will be set to {@link Status#PAUSED}. 731 */ 732 public void pause() { 733 synchronized (disposeLock) { 734 if (getStatus() != Status.DISPOSED) { 735 if (playerReady) { 736 jfxPlayer.pause(); 737 } else { 738 playRequested = false; 739 } 740 } 741 } 742 } 743 744 /** 745 * Stops playing the media. This operation resets playback to 746 * {@link #startTimeProperty startTime}, and resets 747 * {@link #currentCountProperty currentCount} to zero. Once the player is actually 748 * stopped, the {@link #statusProperty status} will be set to {@link Status#STOPPED}. The 749 * only transitions out of <code>STOPPED</code> status are to 750 * {@link Status#PAUSED} and {@link Status#PLAYING} which occur after 751 * invoking {@link #pause()} or {@link #play()}, respectively. 752 * While stopped, the player will not respond to playback position changes 753 * requested by {@link #seek(javafx.util.Duration)}. 754 */ 755 public void stop() { 756 synchronized (disposeLock) { 757 if (getStatus() != Status.DISPOSED) { 758 if (playerReady) { 759 jfxPlayer.stop(); 760 setCurrentCount(0); 761 destroyMediaTimer(); // Stop media timer 762 } else { 763 playRequested = false; 764 } 765 } 766 } 767 } 768 769 /** 770 * The rate at which the media should be played. For example, a rate of 771 * <code>1.0</code> plays the media at its normal (encoded) playback rate, 772 * <code>2.0</code> plays back at twice the normal rate, etc. The currently 773 * supported range of rates is <code>[0.0, 8.0]</code>. The default 774 * value is <code>1.0</code>. 775 */ 776 private DoubleProperty rate; 777 778 /** 779 * Sets the playback rate to the supplied value. Its effect will be clamped 780 * to the range <code>[0.0, 8.0]</code>. 781 * Invoking this method will have no effect if media duration is {@link Duration#INDEFINITE}. 782 * @param value the playback rate 783 */ 784 public final void setRate(double value) { 785 rateProperty().set(value); 786 } 787 788 /** 789 * Retrieves the playback rate. 790 * @return the playback rate 791 */ 792 public final double getRate() { 793 return rate == null ? 1.0 : rate.get(); 794 } 795 796 public DoubleProperty rateProperty() { 797 if (rate == null) { 798 rate = new DoublePropertyBase(1.0) { 799 800 @Override 801 protected void invalidated() { 802 synchronized (disposeLock) { 803 if (getStatus() != Status.DISPOSED) { 804 if (playerReady) { 805 if (jfxPlayer.getDuration() != Double.POSITIVE_INFINITY) { 806 jfxPlayer.setRate((float) clamp(rate.get(), RATE_MIN, RATE_MAX)); 807 } 808 } else { 809 rateChangeRequested = true; 810 } 811 } 812 } 813 } 814 815 @Override 816 public Object getBean() { 817 return MediaPlayer.this; 818 } 819 820 @Override 821 public String getName() { 822 return "rate"; 823 } 824 }; 825 } 826 return rate; 827 } 828 829 /** 830 * The current rate of playback regardless of settings. For example, if 831 * <code>rate</code> is set to 1.0 and the player is paused or stalled, 832 * then <code>currentRate</code> will be zero. 833 */ 834 // FIXME: we should see if we can track rate in the native player instead 835 private ReadOnlyDoubleWrapper currentRate; 836 837 private void setCurrentRate(double value) { 838 currentRatePropertyImpl().set(value); 839 } 840 841 /** 842 * Retrieves the current playback rate. 843 * @return the current rate 844 */ 845 public final double getCurrentRate() { 846 return currentRate == null ? 0.0 : currentRate.get(); 847 } 848 849 public ReadOnlyDoubleProperty currentRateProperty() { 850 return currentRatePropertyImpl().getReadOnlyProperty(); 851 } 852 853 private ReadOnlyDoubleWrapper currentRatePropertyImpl() { 854 if (currentRate == null) { 855 currentRate = new ReadOnlyDoubleWrapper(this, "currentRate"); 856 } 857 return currentRate; 858 } 859 860 /** 861 * The volume at which the media should be played. The range of effective 862 * values is <code>[0.0 1.0]</code> where <code>0.0</code> is inaudible 863 * and <code>1.0</code> is full volume, which is the default. 864 */ 865 private DoubleProperty volume; 866 867 /** 868 * Sets the audio playback volume. Its effect will be clamped to the range 869 * <code>[0.0, 1.0]</code>. 870 * 871 * @param value the volume 872 */ 873 public final void setVolume(double value) { 874 volumeProperty().set(value); 875 } 876 877 /** 878 * Retrieves the audio playback volume. The default value is <code>1.0</code>. 879 * @return the audio volume 880 */ 881 public final double getVolume() { 882 return volume == null ? 1.0 : volume.get(); 883 } 884 885 public DoubleProperty volumeProperty() { 886 if (volume == null) { 887 volume = new DoublePropertyBase(1.0) { 888 889 @Override 890 protected void invalidated() { 891 synchronized (disposeLock) { 892 if (getStatus() != Status.DISPOSED) { 893 if (playerReady) { 894 jfxPlayer.setVolume((float) clamp(volume.get(), 0.0, 1.0)); 895 } else { 896 volumeChangeRequested = true; 897 } 898 } 899 } 900 } 901 902 @Override 903 public Object getBean() { 904 return MediaPlayer.this; 905 } 906 907 @Override 908 public String getName() { 909 return "volume"; 910 } 911 }; 912 } 913 return volume; 914 } 915 916 /** 917 * The balance, or left-right setting, of the audio output. The range of 918 * effective values is <code>[-1.0, 1.0]</code> with <code>-1.0</code> 919 * being full left, <code>0.0</code> center, and <code>1.0</code> full right. 920 * The default value is <code>0.0</code>. 921 */ 922 private DoubleProperty balance; 923 924 /** 925 * Sets the audio balance. Its effect will be clamped to the range 926 * <code>[-1.0, 1.0]</code>. 927 * @param value the balance 928 */ 929 public final void setBalance(double value) { 930 balanceProperty().set(value); 931 } 932 933 /** 934 * Retrieves the audio balance. 935 * @return the audio balance 936 */ 937 public final double getBalance() { 938 return balance == null ? 0.0F : balance.get(); 939 } 940 941 public DoubleProperty balanceProperty() { 942 if (balance == null) { 943 balance = new DoublePropertyBase() { 944 945 @Override 946 protected void invalidated() { 947 synchronized (disposeLock) { 948 if (getStatus() != Status.DISPOSED) { 949 if (playerReady) { 950 jfxPlayer.setBalance((float) clamp(balance.get(), -1.0, 1.0)); 951 } else { 952 balanceChangeRequested = true; 953 } 954 } 955 } 956 } 957 958 @Override 959 public Object getBean() { 960 return MediaPlayer.this; 961 } 962 963 @Override 964 public String getName() { 965 return "balance"; 966 } 967 }; 968 } 969 return balance; 970 } 971 972 /** 973 * Behaviorally clamp the start and stop times. The parameters are clamped 974 * to the range <code>[0.0, duration]</code>. If the duration is not 975 * known, {@link Double#MAX_VALUE} is used instead. Furthermore, if the 976 * separately clamped values satisfy 977 * <code>startTime > stopTime</code> 978 * then <code>stopTime</code> is clamped as 979 * <code>stopTime ≥ startTime</code>. 980 * 981 * @param startValue the new start time. 982 * @param stopValue the new stop time. 983 * @return the clamped times in seconds as <code>{actualStart, actualStop}</code>. 984 */ 985 private double[] calculateStartStopTimes(Duration startValue, Duration stopValue) { 986 // Derive start time in seconds. 987 double newStart; 988 if (startValue == null || startValue.lessThan(Duration.ZERO) 989 || startValue.equals(Duration.UNKNOWN)) { 990 newStart = 0.0; 991 } else if (startValue.equals(Duration.INDEFINITE)) { 992 newStart = Double.MAX_VALUE; 993 } else { 994 newStart = startValue.toMillis() / 1000.0; 995 } 996 997 // Derive stop time in seconds. 998 double newStop; 999 if (stopValue == null || stopValue.equals(Duration.UNKNOWN) 1000 || stopValue.equals(Duration.INDEFINITE)) { 1001 newStop = Double.MAX_VALUE; 1002 } else if (stopValue.lessThan(Duration.ZERO)) { 1003 newStop = 0.0; 1004 } else { 1005 newStop = stopValue.toMillis() / 1000.0; 1006 } 1007 1008 // Derive the duration in seconds. 1009 Duration mediaDuration = media.getDuration(); 1010 double duration = mediaDuration == Duration.UNKNOWN ? 1011 Double.MAX_VALUE : mediaDuration.toMillis()/1000.0; 1012 1013 // Clamp the start and stop times to [0,duration]. 1014 double actualStart = clamp(newStart, 0.0, duration); 1015 double actualStop = clamp(newStop, 0.0, duration); 1016 1017 // Restrict actual stop time to [startTime,duration]. 1018 if (actualStart > actualStop) { 1019 actualStop = actualStart; 1020 } 1021 1022 return new double[] {actualStart, actualStop}; 1023 } 1024 1025 /** 1026 * Set the effective start and stop times on the underlying player, 1027 * clamping as needed. 1028 * 1029 * @param startValue the new start time. 1030 * @param stopValue the new stop time. 1031 */ 1032 private void setStartStopTimes(Duration startValue, boolean isStartValueSet, Duration stopValue, boolean isStopValueSet) { 1033 if (jfxPlayer.getDuration() == Double.POSITIVE_INFINITY) { 1034 return; 1035 } 1036 1037 // Clamp the start and stop times to values in seconds. 1038 double[] startStop = calculateStartStopTimes(startValue, stopValue); 1039 1040 // Set the start and stop times on the underlying player. 1041 if (isStartValueSet) { 1042 jfxPlayer.setStartTime(startStop[0]); 1043 if (getStatus() == Status.READY || getStatus() == Status.PAUSED) { 1044 Platform.runLater(() -> { 1045 setCurrentTime(getStartTime()); 1046 }); 1047 } 1048 } 1049 if (isStopValueSet) { 1050 jfxPlayer.setStopTime(startStop[1]); 1051 } 1052 } 1053 1054 /** 1055 * The time offset where media should start playing, or restart from when 1056 * repeating. When playback is stopped, the current time is reset to this 1057 * value. If this value is positive, then the first time the media is 1058 * played there might be a delay before playing begins unless the play 1059 * position can be set to an arbitrary time within the media. This could 1060 * occur for example for a video which does not contain a lookup table 1061 * of the offsets of intra-frames in the video stream. In such a case the 1062 * video frames would need to be skipped over until the position of the 1063 * first intra-frame before the start time was reached. The default value is 1064 * <code>Duration.ZERO</code>. 1065 * 1066 * <p>Constraints: <code>0 ≤ startTime < {@link #stopTimeProperty stopTime}</code> 1067 */ 1068 private ObjectProperty<Duration> startTime; 1069 1070 /** 1071 * Sets the start time. Its effect will be clamped to 1072 * the range <code>[{@link Duration#ZERO}, {@link #stopTimeProperty stopTime})</code>. 1073 * Invoking this method will have no effect if media duration is {@link Duration#INDEFINITE}. 1074 * 1075 * @param value the start time 1076 */ 1077 public final void setStartTime(Duration value) { 1078 startTimeProperty().set(value); 1079 } 1080 1081 /** 1082 * Retrieves the start time. The default value is <code>Duration.ZERO</code>. 1083 * @return the start time 1084 */ 1085 public final Duration getStartTime() { 1086 return startTime == null ? Duration.ZERO : startTime.get(); 1087 } 1088 1089 public ObjectProperty<Duration> startTimeProperty() { 1090 if (startTime == null) { 1091 startTime = new ObjectPropertyBase<Duration>() { 1092 1093 @Override 1094 protected void invalidated() { 1095 synchronized (disposeLock) { 1096 if (getStatus() != Status.DISPOSED) { 1097 if (playerReady) { 1098 setStartStopTimes(startTime.get(), true, getStopTime(), false); 1099 } else { 1100 startTimeChangeRequested = true; 1101 } 1102 calculateCycleDuration(); 1103 } 1104 } 1105 } 1106 1107 @Override 1108 public Object getBean() { 1109 return MediaPlayer.this; 1110 } 1111 1112 @Override 1113 public String getName() { 1114 return "startTime"; 1115 } 1116 }; 1117 } 1118 return startTime; 1119 } 1120 /** 1121 * The time offset where media should stop playing or restart when repeating. 1122 * The default value is <code>{@link #getMedia()}.getDuration()</code>. 1123 * 1124 * <p>Constraints: <code>{@link #startTimeProperty startTime} < stopTime ≤ {@link Media#durationProperty Media.duration}</code> 1125 */ 1126 private ObjectProperty<Duration> stopTime; 1127 1128 /** 1129 * Sets the stop time. Its effect will be clamped to 1130 * the range <code>({@link #startTimeProperty startTime}, {@link Media#durationProperty Media.duration}]</code>. 1131 * Invoking this method will have no effect if media duration is {@link Duration#INDEFINITE}. 1132 * 1133 * @param value the stop time 1134 */ 1135 public final void setStopTime (Duration value) { 1136 stopTimeProperty().set(value); 1137 } 1138 1139 /** 1140 * Retrieves the stop time. The default value is 1141 * <code>{@link #getMedia()}.getDuration()</code>. Note that 1142 * <code>{@link Media#durationProperty Media.duration}</code> may have the value 1143 * <code>Duration.UNKNOWN</code> if media initialization is not complete. 1144 * @return the stop time 1145 */ 1146 public final Duration getStopTime() { 1147 return stopTime == null ? media.getDuration() : stopTime.get(); 1148 } 1149 1150 public ObjectProperty<Duration> stopTimeProperty() { 1151 if (stopTime == null) { 1152 stopTime = new ObjectPropertyBase<Duration>() { 1153 1154 @Override 1155 protected void invalidated() { 1156 synchronized (disposeLock) { 1157 if (getStatus() != Status.DISPOSED) { 1158 if (playerReady) { 1159 setStartStopTimes(getStartTime(), false, stopTime.get(), true); 1160 } else { 1161 stopTimeChangeRequested = true; 1162 } 1163 calculateCycleDuration(); 1164 } 1165 } 1166 } 1167 1168 @Override 1169 public Object getBean() { 1170 return MediaPlayer.this; 1171 } 1172 1173 @Override 1174 public String getName() { 1175 return "stopTime"; 1176 } 1177 }; 1178 } 1179 return stopTime; 1180 } 1181 1182 /** 1183 * The amount of time between the {@link #startTimeProperty startTime} and 1184 * {@link #stopTimeProperty stopTime} 1185 * of this player. For the total duration of the Media use the 1186 * {@link Media#durationProperty Media.duration} property. 1187 */ 1188 private ReadOnlyObjectWrapper<Duration> cycleDuration; 1189 1190 1191 private void setCycleDuration(Duration value) { 1192 cycleDurationPropertyImpl().set(value); 1193 } 1194 1195 /** 1196 * Retrieves the cycle duration in seconds. 1197 * @return the cycle duration 1198 */ 1199 public final Duration getCycleDuration() { 1200 return cycleDuration == null ? Duration.UNKNOWN : cycleDuration.get(); 1201 } 1202 1203 public ReadOnlyObjectProperty<Duration> cycleDurationProperty() { 1204 return cycleDurationPropertyImpl().getReadOnlyProperty(); 1205 } 1206 1207 private ReadOnlyObjectWrapper<Duration> cycleDurationPropertyImpl() { 1208 if (cycleDuration == null) { 1209 cycleDuration = new ReadOnlyObjectWrapper<Duration>(this, "cycleDuration"); 1210 } 1211 return cycleDuration; 1212 } 1213 1214 // recalculate cycleDuration based on startTime, stopTime and Media.duration 1215 // if any are UNKNOWN then this is UNKNOWN 1216 private void calculateCycleDuration() { 1217 Duration endTime; 1218 Duration mediaDuration = media.getDuration(); 1219 1220 if (!getStopTime().isUnknown()) { 1221 endTime = getStopTime(); 1222 } else { 1223 endTime = mediaDuration; 1224 } 1225 if (endTime.greaterThan(mediaDuration)) { 1226 endTime = mediaDuration; 1227 } 1228 1229 // filter bad values 1230 if (endTime.isUnknown() || getStartTime().isUnknown() || getStartTime().isIndefinite()) { 1231 if (!getCycleDuration().isUnknown()) 1232 setCycleDuration(Duration.UNKNOWN); 1233 } 1234 1235 setCycleDuration(endTime.subtract(getStartTime())); 1236 calculateTotalDuration(); // since it's dependent on cycle duration 1237 } 1238 /** 1239 * The total amount of play time if allowed to play until finished. If 1240 * <code>cycleCount</code> is set to <code>INDEFINITE</code> then this will 1241 * also be INDEFINITE. If the Media duration is UNKNOWN, then this will 1242 * likewise be UNKNOWN. Otherwise, total duration will be the product of 1243 * cycleDuration and cycleCount. 1244 */ 1245 private ReadOnlyObjectWrapper<Duration> totalDuration; 1246 1247 1248 private void setTotalDuration(Duration value) { 1249 totalDurationPropertyImpl().set(value); 1250 } 1251 1252 /** 1253 * Retrieves the total playback duration including all cycles (repetitions). 1254 * @return the total playback duration 1255 */ 1256 public final Duration getTotalDuration() { 1257 return totalDuration == null ? Duration.UNKNOWN : totalDuration.get(); 1258 } 1259 1260 public ReadOnlyObjectProperty<Duration> totalDurationProperty() { 1261 return totalDurationPropertyImpl().getReadOnlyProperty(); 1262 } 1263 1264 private ReadOnlyObjectWrapper<Duration> totalDurationPropertyImpl() { 1265 if (totalDuration == null) { 1266 totalDuration = new ReadOnlyObjectWrapper<Duration>(this, "totalDuration"); 1267 } 1268 return totalDuration; 1269 } 1270 private void calculateTotalDuration() { 1271 if (getCycleCount() == INDEFINITE) { 1272 setTotalDuration(Duration.INDEFINITE); 1273 } else if (getCycleDuration().isUnknown()) { 1274 setTotalDuration(Duration.UNKNOWN); 1275 } else { 1276 setTotalDuration(getCycleDuration().multiply((double)getCycleCount())); 1277 } 1278 } 1279 1280 /** 1281 * The current media playback time. This property is read-only: use 1282 * {@link #seek(javafx.util.Duration)} to change playback to a different 1283 * stream position. 1284 * 1285 */ 1286 private ReadOnlyObjectWrapper<Duration> currentTime; 1287 1288 1289 private void setCurrentTime(Duration value) { 1290 currentTimePropertyImpl().set(value); 1291 } 1292 1293 /** 1294 * Retrieves the current media time. 1295 * @return the current media time 1296 */ 1297 public final Duration getCurrentTime() { 1298 synchronized (disposeLock) { 1299 if (getStatus() == Status.DISPOSED) { 1300 return Duration.ZERO; 1301 } 1302 1303 if (getStatus() == Status.STOPPED) { 1304 return Duration.millis(getStartTime().toMillis()); 1305 } 1306 1307 if (isEOS) { 1308 Duration duration = media.getDuration(); 1309 Duration stopTime = getStopTime(); 1310 if (stopTime != Duration.UNKNOWN && duration != Duration.UNKNOWN) { 1311 if (stopTime.greaterThan(duration)) { 1312 return Duration.millis(duration.toMillis()); 1313 } else { 1314 return Duration.millis(stopTime.toMillis()); 1315 } 1316 } 1317 } 1318 1319 // Query the property value. This is necessary even if the returned 1320 // value is not used below as setting the property value in 1321 // setCurrentTime() as is done in updateTime() which is called by the 1322 // MediaTimer will not trigger invalidation events unless the previous 1323 // value of the property has been retrieved via get(). 1324 Duration theCurrentTime = currentTimeProperty().get(); 1325 1326 // Query the implementation layer for a more accurate value of the time. 1327 // The MediaTimer only updates the property at a fixed interval and 1328 // the present method might be called too far away from a timer update. 1329 if (playerReady) { 1330 double timeSeconds = jfxPlayer.getPresentationTime(); 1331 if (timeSeconds >= 0.0) { 1332 theCurrentTime = Duration.seconds(timeSeconds); 1333 // We do not set the currentTime property value here as doing so 1334 // could result in an infinite loop if getCurrentTime() is for 1335 // example being invoked by an Invaludation listener of 1336 // currentTime, for example in response to MediaTimer calling 1337 // updateTime(). 1338 } 1339 } 1340 1341 return theCurrentTime; 1342 } 1343 } 1344 1345 public ReadOnlyObjectProperty<Duration> currentTimeProperty() { 1346 return currentTimePropertyImpl().getReadOnlyProperty(); 1347 } 1348 1349 private ReadOnlyObjectWrapper<Duration> currentTimePropertyImpl() { 1350 if (currentTime == null) { 1351 currentTime = new ReadOnlyObjectWrapper<Duration>(this, "currentTime"); 1352 currentTime.setValue(Duration.ZERO); 1353 updateTime(); 1354 } 1355 return currentTime; 1356 } 1357 1358 /** 1359 * Seeks the player to a new playback time. Invoking this method will have 1360 * no effect while the player status is {@link Status#STOPPED} or media duration is {@link Duration#INDEFINITE}. 1361 * 1362 * <p>The behavior of <code>seek()</code> is constrained as follows where 1363 * <i>start time</i> and <i>stop time</i> indicate the effective lower and 1364 * upper bounds, respectively, of media playback: 1365 * <table border="1"> 1366 * <tr><th>seekTime</th><th>seek position</th></tr> 1367 * <tr><td><code>null</code></td><td>no change</td></tr> 1368 * <tr><td>{@link Duration#UNKNOWN}</td><td>no change</td></tr> 1369 * <tr><td>{@link Duration#INDEFINITE}</td><td>stop time</td></tr> 1370 * <tr><td>seekTime < start time</td><td>start time</td></tr> 1371 * <tr><td>seekTime > stop time</td><td>stop time</td></tr> 1372 * <tr><td>start time ≤ seekTime ≤ stop time</td><td>seekTime</td></tr> 1373 * </table> 1374 * </p> 1375 * 1376 * @param seekTime the requested playback time 1377 */ 1378 public void seek(Duration seekTime) { 1379 synchronized (disposeLock) { 1380 if (getStatus() == Status.DISPOSED) { 1381 return; 1382 } 1383 1384 // Seek only if the player is ready and the seekTime is valid. 1385 if (playerReady && seekTime != null && !seekTime.isUnknown()) { 1386 if (jfxPlayer.getDuration() == Double.POSITIVE_INFINITY) { 1387 return; 1388 } 1389 1390 // Determine the seek position in seconds. 1391 double seekSeconds; 1392 1393 // Duration.INDEFINITE means seek to end. 1394 if (seekTime.isIndefinite()) { 1395 // Determine the effective duration. 1396 Duration duration = media.getDuration(); 1397 if (duration == null 1398 || duration.isUnknown() 1399 || duration.isIndefinite()) { 1400 duration = Duration.millis(Double.MAX_VALUE); 1401 } 1402 1403 // Convert the duration to seconds. 1404 seekSeconds = duration.toMillis() / 1000.0; 1405 } else { 1406 // Convert the parameter to seconds. 1407 seekSeconds = seekTime.toMillis() / 1000.0; 1408 1409 // Clamp the seconds if needed. 1410 double[] startStop = calculateStartStopTimes(getStartTime(), getStopTime()); 1411 if (seekSeconds < startStop[0]) { 1412 seekSeconds = startStop[0]; 1413 } else if (seekSeconds > startStop[1]) { 1414 seekSeconds = startStop[1]; 1415 } 1416 } 1417 1418 if (!isUpdateTimeEnabled) { 1419 // Change time update flag to true amd current rate to rate 1420 // if status is PLAYING and current time is in range. 1421 Status playerStatus = getStatus(); 1422 if ((playerStatus == MediaPlayer.Status.PLAYING 1423 || playerStatus == MediaPlayer.Status.PAUSED) 1424 && getStartTime().toSeconds() <= seekSeconds 1425 && seekSeconds <= getStopTime().toSeconds()) { 1426 isEOS = false; 1427 isUpdateTimeEnabled = true; 1428 setCurrentRate(getRate()); 1429 } 1430 } 1431 1432 // Perform the seek. 1433 jfxPlayer.seek(seekSeconds); 1434 } 1435 } 1436 } 1437 /** 1438 * The current state of the MediaPlayer. 1439 */ 1440 private ReadOnlyObjectWrapper<Status> status; 1441 1442 private void setStatus(Status value) { 1443 statusPropertyImpl().set(value); 1444 } 1445 1446 /** 1447 * Retrieves the current player status. 1448 * @return the playback status 1449 */ 1450 public final Status getStatus() { 1451 return status == null ? Status.UNKNOWN : status.get(); 1452 } 1453 1454 public ReadOnlyObjectProperty<Status> statusProperty() { 1455 return statusPropertyImpl().getReadOnlyProperty(); 1456 } 1457 1458 private ReadOnlyObjectWrapper<Status> statusPropertyImpl() { 1459 if (status == null) { 1460 status = new ReadOnlyObjectWrapper<Status>() { 1461 1462 @Override 1463 protected void invalidated() { 1464 // use status changes to update currentRate 1465 if (get() == Status.PLAYING) { 1466 setCurrentRate(getRate()); 1467 } else { 1468 setCurrentRate(0.0); 1469 } 1470 1471 // Signal status updates 1472 if (get() == Status.READY) { 1473 if (getOnReady() != null) { 1474 Platform.runLater(getOnReady()); 1475 } 1476 } else if (get() == Status.PLAYING) { 1477 if (getOnPlaying() != null) { 1478 Platform.runLater(getOnPlaying()); 1479 } 1480 } else if (get() == Status.PAUSED) { 1481 if (getOnPaused() != null) { 1482 Platform.runLater(getOnPaused()); 1483 } 1484 } else if (get() == Status.STOPPED) { 1485 if (getOnStopped() != null) { 1486 Platform.runLater(getOnStopped()); 1487 } 1488 } else if (get() == Status.STALLED) { 1489 if (getOnStalled() != null) { 1490 Platform.runLater(getOnStalled()); 1491 } 1492 } 1493 } 1494 1495 @Override 1496 public Object getBean() { 1497 return MediaPlayer.this; 1498 } 1499 1500 @Override 1501 public String getName() { 1502 return "status"; 1503 } 1504 }; 1505 } 1506 return status; 1507 } 1508 /** 1509 * The current buffer position indicating how much media can be played 1510 * without stalling the <code>MediaPlayer</code>. This is applicable to 1511 * buffered streams such as those reading from network connections as 1512 * opposed for example to local files. 1513 * 1514 * <p>Seeking to a position beyond <code>bufferProgressTime</code> might 1515 * cause a slight pause in playback until an amount of data sufficient to 1516 * permit playback resumption has been buffered. 1517 */ 1518 private ReadOnlyObjectWrapper<Duration> bufferProgressTime; 1519 1520 private void setBufferProgressTime(Duration value) { 1521 bufferProgressTimePropertyImpl().set(value); 1522 } 1523 1524 /** 1525 * Retrieves the {@link #bufferProgressTimeProperty bufferProgressTime} value. 1526 * @return the buffer progress time 1527 */ 1528 public final Duration getBufferProgressTime() { 1529 return bufferProgressTime == null ? null : bufferProgressTime.get(); 1530 } 1531 1532 public ReadOnlyObjectProperty<Duration> bufferProgressTimeProperty() { 1533 return bufferProgressTimePropertyImpl().getReadOnlyProperty(); 1534 } 1535 1536 private ReadOnlyObjectWrapper<Duration> bufferProgressTimePropertyImpl() { 1537 if (bufferProgressTime == null) { 1538 bufferProgressTime = new ReadOnlyObjectWrapper<Duration>(this, "bufferProgressTime"); 1539 } 1540 return bufferProgressTime; 1541 } 1542 /** 1543 * The number of times the media will be played. By default, 1544 * <code>cycleCount</code> is set to <code>1</code> 1545 * meaning the media will only be played once. Setting <code>cycleCount</code> 1546 * to a value greater than 1 will cause the media to play the given number 1547 * of times or until stopped. If set to {@link #INDEFINITE INDEFINITE}, 1548 * playback will repeat until stop() or pause() is called. 1549 * 1550 * <p>constraints: <code>cycleCount ≥ 1</code> 1551 */ 1552 private IntegerProperty cycleCount; 1553 1554 /** 1555 * Sets the cycle count. Its effect will be constrained to 1556 * <code>[1,{@link Integer#MAX_VALUE}]</code>. 1557 * Invoking this method will have no effect if media duration is {@link Duration#INDEFINITE}. 1558 * @param value the cycle count 1559 */ 1560 public final void setCycleCount(int value) { 1561 cycleCountProperty().set(value); 1562 } 1563 1564 /** 1565 * Retrieves the cycle count. 1566 * @return the cycle count. 1567 */ 1568 public final int getCycleCount() { 1569 return cycleCount == null ? 1 : cycleCount.get(); 1570 } 1571 1572 public IntegerProperty cycleCountProperty() { 1573 if (cycleCount == null) { 1574 cycleCount = new IntegerPropertyBase(1) { 1575 1576 @Override 1577 public Object getBean() { 1578 return MediaPlayer.this; 1579 } 1580 1581 @Override 1582 public String getName() { 1583 return "cycleCount"; 1584 } 1585 }; 1586 } 1587 return cycleCount; 1588 } 1589 /** 1590 * The number of completed playback cycles. On the first pass, 1591 * the value should be 0. On the second pass, the value should be 1 and 1592 * so on. It is incremented at the end of each cycle just prior to seeking 1593 * back to {@link #startTimeProperty startTime}, i.e., when {@link #stopTimeProperty stopTime} or the 1594 * end of media has been reached. 1595 */ 1596 private ReadOnlyIntegerWrapper currentCount; 1597 1598 1599 private void setCurrentCount(int value) { 1600 currentCountPropertyImpl().set(value); 1601 } 1602 1603 /** 1604 * Retrieves the index of the current cycle. 1605 * @return the current cycle index 1606 */ 1607 public final int getCurrentCount() { 1608 return currentCount == null ? 0 : currentCount.get(); 1609 } 1610 1611 public ReadOnlyIntegerProperty currentCountProperty() { 1612 return currentCountPropertyImpl().getReadOnlyProperty(); 1613 } 1614 1615 private ReadOnlyIntegerWrapper currentCountPropertyImpl() { 1616 if (currentCount == null) { 1617 currentCount = new ReadOnlyIntegerWrapper(this, "currentCount"); 1618 } 1619 return currentCount; 1620 } 1621 /** 1622 * Whether the player audio is muted. A value of <code>true</code> indicates 1623 * that audio is <i>not</i> being produced. The value of this property has 1624 * no effect on {@link #volumeProperty volume}, i.e., if the audio is muted and then 1625 * un-muted, audio playback will resume at the same audible level provided 1626 * of course that the <code>volume</code> property has not been modified 1627 * meanwhile. The default value is <code>false</code>. 1628 * @see #volume 1629 */ 1630 private BooleanProperty mute; 1631 1632 /** 1633 * Sets the value of {@link #muteProperty}. 1634 * @param value the <code>mute</code> setting 1635 */ 1636 public final void setMute (boolean value) { 1637 muteProperty().set(value); 1638 } 1639 1640 /** 1641 * Retrieves the {@link #muteProperty} value. 1642 * @return the mute setting 1643 */ 1644 public final boolean isMute() { 1645 return mute == null ? false : mute.get(); 1646 } 1647 1648 public BooleanProperty muteProperty() { 1649 if (mute == null) { 1650 mute = new BooleanPropertyBase() { 1651 1652 @Override 1653 protected void invalidated() { 1654 synchronized (disposeLock) { 1655 if (getStatus() != Status.DISPOSED) { 1656 if (playerReady) { 1657 jfxPlayer.setMute(get()); 1658 } else { 1659 muteChangeRequested = true; 1660 } 1661 } 1662 } 1663 } 1664 1665 @Override 1666 public Object getBean() { 1667 return MediaPlayer.this; 1668 } 1669 1670 @Override 1671 public String getName() { 1672 return "mute"; 1673 } 1674 }; 1675 } 1676 return mute; 1677 } 1678 1679 /** 1680 * Event handler invoked when the player <code>currentTime</code> reaches a 1681 * media marker. 1682 */ 1683 private ObjectProperty<EventHandler<MediaMarkerEvent>> onMarker; 1684 1685 /** 1686 * Sets the marker event handler. 1687 * @param onMarker the marker event handler. 1688 */ 1689 public final void setOnMarker(EventHandler<MediaMarkerEvent> onMarker) { 1690 onMarkerProperty().set(onMarker); 1691 } 1692 1693 /** 1694 * Retrieves the marker event handler. 1695 * @return the marker event handler. 1696 */ 1697 public final EventHandler<MediaMarkerEvent> getOnMarker() { 1698 return onMarker == null ? null : onMarker.get(); 1699 } 1700 1701 public ObjectProperty<EventHandler<MediaMarkerEvent>> onMarkerProperty() { 1702 if (onMarker == null) { 1703 onMarker = new SimpleObjectProperty<EventHandler<MediaMarkerEvent>>(this, "onMarker"); 1704 } 1705 return onMarker; 1706 } 1707 1708 void addView(MediaView view) { 1709 WeakReference<MediaView> vref = new WeakReference<MediaView>(view); 1710 synchronized (viewRefs) { 1711 viewRefs.add(vref); 1712 } 1713 } 1714 1715 void removeView(MediaView view) { 1716 synchronized (viewRefs) { 1717 for (WeakReference<MediaView> vref : viewRefs) { 1718 MediaView v = vref.get(); 1719 if (v != null && v.equals(view)) { 1720 viewRefs.remove(vref); 1721 } 1722 } 1723 } 1724 } 1725 1726 // This function sets the player's error property on the UI thread. 1727 void handleError(final MediaException error) { 1728 Platform.runLater(() -> { 1729 setError(error); 1730 1731 // Propogate errors that related to media to media object 1732 if (error.getType() == MediaException.Type.MEDIA_CORRUPTED 1733 || error.getType() == MediaException.Type.MEDIA_UNSUPPORTED 1734 || error.getType() == MediaException.Type.MEDIA_INACCESSIBLE 1735 || error.getType() == MediaException.Type.MEDIA_UNAVAILABLE) { 1736 media._setError(error.getType(), error.getMessage()); 1737 } 1738 }); 1739 } 1740 1741 void createMediaTimer() { 1742 synchronized (MediaTimerTask.timerLock) { 1743 if (mediaTimerTask == null) { 1744 mediaTimerTask = new MediaTimerTask(this); 1745 mediaTimerTask.start(); 1746 } 1747 isUpdateTimeEnabled = true; 1748 } 1749 } 1750 1751 void destroyMediaTimer() { 1752 synchronized (MediaTimerTask.timerLock) { 1753 if (mediaTimerTask != null) { 1754 isUpdateTimeEnabled = false; 1755 mediaTimerTask.stop(); 1756 mediaTimerTask = null; 1757 } 1758 } 1759 } 1760 1761 // Called periodically to update the currentTime 1762 void updateTime() { 1763 if (playerReady && isUpdateTimeEnabled && jfxPlayer != null) { 1764 double timeSeconds = jfxPlayer.getPresentationTime(); 1765 if (timeSeconds >= 0.0) { 1766 double newTimeMs = timeSeconds*1000.0; 1767 1768 if (Double.compare(newTimeMs, prevTimeMs) != 0) { 1769 setCurrentTime(Duration.millis(newTimeMs)); 1770 prevTimeMs = newTimeMs; 1771 } 1772 } 1773 } 1774 } 1775 1776 void loopPlayback() { 1777 seek (getStartTime()); 1778 } 1779 1780 // handleRequestedChanges() is called to update jfxPlayer's properties once 1781 // MediaPlayer gets the onReady event from jfxPlayer. Before onReady, calls to 1782 // update MediaPlayer's properties to not correspond to calls to update jfxPlayer's 1783 // properties. Once we get onReady(), we must then go and update all of jfxPlayer's 1784 // proprties. 1785 void handleRequestedChanges() { 1786 if (rateChangeRequested) { 1787 if (jfxPlayer.getDuration() != Double.POSITIVE_INFINITY) { 1788 jfxPlayer.setRate((float)clamp(getRate(), RATE_MIN, RATE_MAX)); 1789 } 1790 rateChangeRequested = false; 1791 } 1792 1793 if (volumeChangeRequested) { 1794 jfxPlayer.setVolume((float)clamp(getVolume(), 0.0, 1.0)); 1795 volumeChangeRequested = false; 1796 } 1797 1798 if (balanceChangeRequested) { 1799 jfxPlayer.setBalance((float)clamp(getBalance(), -1.0, 1.0)); 1800 balanceChangeRequested = false; 1801 } 1802 1803 if (startTimeChangeRequested || stopTimeChangeRequested) { 1804 setStartStopTimes(getStartTime(), startTimeChangeRequested, getStopTime(), stopTimeChangeRequested); 1805 startTimeChangeRequested = stopTimeChangeRequested = false; 1806 } 1807 1808 if (muteChangeRequested) { 1809 jfxPlayer.setMute(isMute()); 1810 muteChangeRequested = false; 1811 } 1812 1813 if (audioSpectrumNumBandsChangeRequested) { 1814 jfxPlayer.getAudioSpectrum().setBandCount(clamp(getAudioSpectrumNumBands(), AUDIOSPECTRUM_NUMBANDS_MIN, Integer.MAX_VALUE)); 1815 audioSpectrumNumBandsChangeRequested = false; 1816 } 1817 1818 if (audioSpectrumIntervalChangeRequested) { 1819 jfxPlayer.getAudioSpectrum().setInterval(clamp(getAudioSpectrumInterval(), AUDIOSPECTRUM_INTERVAL_MIN, Double.MAX_VALUE)); 1820 audioSpectrumIntervalChangeRequested = false; 1821 } 1822 1823 if (audioSpectrumThresholdChangeRequested) { 1824 jfxPlayer.getAudioSpectrum().setSensitivityThreshold(clamp(getAudioSpectrumThreshold(), Integer.MIN_VALUE, AUDIOSPECTRUM_THRESHOLD_MAX)); 1825 audioSpectrumThresholdChangeRequested = false; 1826 } 1827 1828 if (audioSpectrumEnabledChangeRequested) { 1829 boolean enabled = (getAudioSpectrumListener() != null); 1830 jfxPlayer.getAudioSpectrum().setEnabled(enabled); 1831 audioSpectrumEnabledChangeRequested = false; 1832 } 1833 1834 if (playRequested) { 1835 jfxPlayer.play(); 1836 playRequested = false; 1837 } 1838 } 1839 1840 //************************************************************************************************* 1841 //********** Player event-handling 1842 //************************************************************************************************* 1843 1844 void preReady() { 1845 // Notify MediaView that we ready 1846 synchronized (viewRefs) { 1847 for (WeakReference<MediaView> vref : viewRefs) { 1848 MediaView v = vref.get(); 1849 if (v != null) { 1850 v._mediaPlayerOnReady(); 1851 } 1852 } 1853 } 1854 1855 // Update AudioEqaualizer if needed 1856 if (audioEqualizer != null) { 1857 audioEqualizer.setAudioEqualizer(jfxPlayer.getEqualizer()); 1858 } 1859 1860 // Update duration 1861 double durationSeconds = jfxPlayer.getDuration(); 1862 Duration duration; 1863 if (durationSeconds >= 0.0 && !Double.isNaN(durationSeconds)) { 1864 duration = Duration.millis(durationSeconds * 1000.0); 1865 } else { 1866 duration = Duration.UNKNOWN; 1867 } 1868 1869 playerReady = true; 1870 1871 media.setDuration(duration); 1872 media._updateMedia(jfxPlayer.getMedia()); 1873 1874 //***** Sync up the player with the desired properties if they were called 1875 // before onReady() 1876 handleRequestedChanges(); 1877 1878 // update cycle/total durations 1879 calculateCycleDuration(); 1880 1881 // Set BufferProgressTime 1882 if (lastBufferEvent != null && duration.toMillis() > 0.0) { 1883 double position = lastBufferEvent.getBufferPosition(); 1884 double stop = lastBufferEvent.getBufferStop(); 1885 final double bufferedTime = position / stop * duration.toMillis(); 1886 lastBufferEvent = null; 1887 setBufferProgressTime(Duration.millis(bufferedTime)); 1888 } 1889 1890 setStatus(Status.READY); 1891 } 1892 /** 1893 * Event handler invoked when the player <code>currentTime</code> reaches 1894 * <code>stopTime</code>. 1895 */ 1896 private ObjectProperty<Runnable> onEndOfMedia; 1897 1898 /** 1899 * Sets the end of media event handler. 1900 * @param value the event handler or <code>null</code>. 1901 */ 1902 public final void setOnEndOfMedia(Runnable value) { 1903 onEndOfMediaProperty().set(value); 1904 } 1905 1906 /** 1907 * Retrieves the end of media event handler. 1908 * @return the event handler or <code>null</code>. 1909 */ 1910 public final Runnable getOnEndOfMedia() { 1911 return onEndOfMedia == null ? null : onEndOfMedia.get(); 1912 } 1913 1914 public ObjectProperty<Runnable> onEndOfMediaProperty() { 1915 if (onEndOfMedia == null) { 1916 onEndOfMedia = new SimpleObjectProperty<Runnable>(this, "onEndOfMedia"); 1917 } 1918 return onEndOfMedia; 1919 } 1920 1921 /** 1922 * Event handler invoked when the status changes to 1923 * <code>READY</code>. 1924 */ 1925 private ObjectProperty<Runnable> onReady; // Player is ready and media has prerolled 1926 1927 /** 1928 * Sets the {@link Status#READY} event handler. 1929 * @param value the event handler or <code>null</code>. 1930 */ 1931 public final void setOnReady(Runnable value) { 1932 onReadyProperty().set(value); 1933 } 1934 1935 /** 1936 * Retrieves the {@link Status#READY} event handler. 1937 * @return the event handler or <code>null</code>. 1938 */ 1939 public final Runnable getOnReady() { 1940 return onReady == null ? null : onReady.get(); 1941 } 1942 1943 public ObjectProperty<Runnable> onReadyProperty() { 1944 if (onReady == null) { 1945 onReady = new SimpleObjectProperty<Runnable>(this, "onReady"); 1946 } 1947 return onReady; 1948 } 1949 1950 /** 1951 * Event handler invoked when the status changes to 1952 * <code>PLAYING</code>. 1953 */ 1954 private ObjectProperty<Runnable> onPlaying; // Media has reached its end. 1955 1956 /** 1957 * Sets the {@link Status#PLAYING} event handler. 1958 * @param value the event handler or <code>null</code>. 1959 */ 1960 public final void setOnPlaying(Runnable value) { 1961 onPlayingProperty().set(value); 1962 } 1963 1964 /** 1965 * Retrieves the {@link Status#PLAYING} event handler. 1966 * @return the event handler or <code>null</code>. 1967 */ 1968 public final Runnable getOnPlaying() { 1969 return onPlaying == null ? null : onPlaying.get(); 1970 } 1971 1972 public ObjectProperty<Runnable> onPlayingProperty() { 1973 if (onPlaying == null) { 1974 onPlaying = new SimpleObjectProperty<Runnable>(this, "onPlaying"); 1975 } 1976 return onPlaying; 1977 } 1978 1979 /** 1980 * Event handler invoked when the status changes to <code>PAUSED</code>. 1981 */ 1982 private ObjectProperty<Runnable> onPaused; // Media has reached its end. 1983 1984 /** 1985 * Sets the {@link Status#PAUSED} event handler. 1986 * @param value the event handler or <code>null</code>. 1987 */ 1988 public final void setOnPaused(Runnable value) { 1989 onPausedProperty().set(value); 1990 } 1991 1992 /** 1993 * Retrieves the {@link Status#PAUSED} event handler. 1994 * @return the event handler or <code>null</code>. 1995 */ 1996 public final Runnable getOnPaused() { 1997 return onPaused == null ? null : onPaused.get(); 1998 } 1999 2000 public ObjectProperty<Runnable> onPausedProperty() { 2001 if (onPaused == null) { 2002 onPaused = new SimpleObjectProperty<Runnable>(this, "onPaused"); 2003 } 2004 return onPaused; 2005 } 2006 2007 /** 2008 * Event handler invoked when the status changes to 2009 * <code>STOPPED</code>. 2010 */ 2011 private ObjectProperty<Runnable> onStopped; // Media has reached its end. 2012 2013 /** 2014 * Sets the {@link Status#STOPPED} event handler. 2015 * @param value the event handler or <code>null</code>. 2016 */ 2017 public final void setOnStopped(Runnable value) { 2018 onStoppedProperty().set(value); 2019 } 2020 2021 /** 2022 * Retrieves the {@link Status#STOPPED} event handler. 2023 * @return the event handler or <code>null</code>. 2024 */ 2025 public final Runnable getOnStopped() { 2026 return onStopped == null ? null : onStopped.get(); 2027 } 2028 2029 public ObjectProperty<Runnable> onStoppedProperty() { 2030 if (onStopped == null) { 2031 onStopped = new SimpleObjectProperty<Runnable>(this, "onStopped"); 2032 } 2033 return onStopped; 2034 } 2035 2036 /** 2037 * Event handler invoked when the status changes to <code>HALTED</code>. 2038 */ 2039 private ObjectProperty<Runnable> onHalted; // Media caught an irrecoverable error. 2040 2041 /** 2042 * Sets the {@link Status#HALTED} event handler. 2043 * @param value the event handler or <code>null</code>. 2044 */ 2045 public final void setOnHalted(Runnable value) { 2046 onHaltedProperty().set(value); 2047 } 2048 2049 /** 2050 * Retrieves the {@link Status#HALTED} event handler. 2051 * @return the event handler or <code>null</code>. 2052 */ 2053 public final Runnable getOnHalted() { 2054 return onHalted == null ? null : onHalted.get(); 2055 } 2056 2057 public ObjectProperty<Runnable> onHaltedProperty() { 2058 if (onHalted == null) { 2059 onHalted = new SimpleObjectProperty<Runnable>(this, "onHalted"); 2060 } 2061 return onHalted; 2062 } 2063 /** 2064 * Event handler invoked when the player <code>currentTime</code> reaches 2065 * <code>stopTime</code> and <i>will be</i> repeating. This callback is made 2066 * prior to seeking back to <code>startTime</code>. 2067 * 2068 * @see cycleCount 2069 */ 2070 private ObjectProperty<Runnable> onRepeat; 2071 2072 /** 2073 * Sets the repeat event handler. 2074 * @param value the event handler or <code>null</code>. 2075 */ 2076 public final void setOnRepeat(Runnable value) { 2077 onRepeatProperty().set(value); 2078 } 2079 2080 /** 2081 * Retrieves the repeat event handler. 2082 * @return the event handler or <code>null</code>. 2083 */ 2084 public final Runnable getOnRepeat() { 2085 return onRepeat == null ? null : onRepeat.get(); 2086 } 2087 2088 public ObjectProperty<Runnable> onRepeatProperty() { 2089 if (onRepeat == null) { 2090 onRepeat = new SimpleObjectProperty<Runnable>(this, "onRepeat"); 2091 } 2092 return onRepeat; 2093 } 2094 2095 /** 2096 * Event handler invoked when the status changes to 2097 * <code>STALLED</code>. 2098 */ 2099 private ObjectProperty<Runnable> onStalled; 2100 2101 /** 2102 * Sets the {@link Status#STALLED} event handler. 2103 * @param value the event handler or <code>null</code>. 2104 */ 2105 public final void setOnStalled(Runnable value) { 2106 onStalledProperty().set(value); 2107 } 2108 2109 /** 2110 * Retrieves the {@link Status#STALLED} event handler. 2111 * @return the event handler or <code>null</code>. 2112 */ 2113 public final Runnable getOnStalled() { 2114 return onStalled == null ? null : onStalled.get(); 2115 } 2116 2117 public ObjectProperty<Runnable> onStalledProperty() { 2118 if (onStalled == null) { 2119 onStalled = new SimpleObjectProperty<Runnable>(this, "onStalled"); 2120 } 2121 return onStalled; 2122 } 2123 2124 /**************************************************************************** 2125 * AudioSpectrum API 2126 ***************************************************************************/ 2127 2128 /** 2129 * The number of bands in the audio spectrum. The default value is 128; minimum 2130 * is 2. The frequency range of the audio signal will be divided into the 2131 * specified number of frequency bins. For example, a typical digital music 2132 * signal has a frequency range of <code>[0.0, 22050]</code> Hz. If the 2133 * number of spectral bands were in this case set to 10, the width of each 2134 * frequency bin in the spectrum would be <code>2205</code> Hz with the 2135 * lower bound of the lowest frequency bin equal to <code>0.0</code>. 2136 */ 2137 private IntegerProperty audioSpectrumNumBands; 2138 2139 /** 2140 * Sets the number of bands in the audio spectrum. 2141 * @param value the number of spectral bands; <code>value</code>must be ≥ 2 2142 */ 2143 public final void setAudioSpectrumNumBands(int value) { 2144 audioSpectrumNumBandsProperty().setValue(value); 2145 } 2146 2147 /** 2148 * Retrieves the number of bands in the audio spectrum. 2149 * @return the number of spectral bands. 2150 */ 2151 public final int getAudioSpectrumNumBands() { 2152 return audioSpectrumNumBandsProperty().getValue(); 2153 } 2154 2155 public IntegerProperty audioSpectrumNumBandsProperty() { 2156 if (audioSpectrumNumBands == null) { 2157 audioSpectrumNumBands = new IntegerPropertyBase(DEFAULT_SPECTRUM_BAND_COUNT) { 2158 2159 @Override 2160 protected void invalidated() { 2161 synchronized (disposeLock) { 2162 if (getStatus() != Status.DISPOSED) { 2163 if (playerReady) { 2164 jfxPlayer.getAudioSpectrum().setBandCount(clamp(audioSpectrumNumBands.get(), AUDIOSPECTRUM_NUMBANDS_MIN, Integer.MAX_VALUE)); 2165 } else { 2166 audioSpectrumNumBandsChangeRequested = true; 2167 } 2168 } 2169 } 2170 } 2171 2172 @Override 2173 public Object getBean() { 2174 return MediaPlayer.this; 2175 } 2176 2177 @Override 2178 public String getName() { 2179 return "audioSpectrumNumBands"; 2180 } 2181 }; 2182 } 2183 return audioSpectrumNumBands; 2184 } 2185 2186 /** 2187 * The interval between spectrum updates in seconds. The default is 2188 * <code>0.1</code> seconds. 2189 */ 2190 private DoubleProperty audioSpectrumInterval; 2191 2192 /** 2193 * Sets the value of the audio spectrum notification interval in seconds. 2194 * @param value a positive value specifying the spectral update interval 2195 */ 2196 public final void setAudioSpectrumInterval(double value) { 2197 audioSpectrumIntervalProperty().set(value); 2198 } 2199 2200 /** 2201 * Retrieves the value of the audio spectrum notification interval in seconds. 2202 * @return the spectral update interval 2203 */ 2204 public final double getAudioSpectrumInterval() { 2205 return audioSpectrumIntervalProperty().get(); 2206 } 2207 2208 public DoubleProperty audioSpectrumIntervalProperty() { 2209 if (audioSpectrumInterval == null) { 2210 audioSpectrumInterval = new DoublePropertyBase(DEFAULT_SPECTRUM_INTERVAL) { 2211 2212 @Override 2213 protected void invalidated() { 2214 synchronized (disposeLock) { 2215 if (getStatus() != Status.DISPOSED) { 2216 if (playerReady) { 2217 jfxPlayer.getAudioSpectrum().setInterval(clamp(audioSpectrumInterval.get(), AUDIOSPECTRUM_INTERVAL_MIN, Double.MAX_VALUE)); 2218 } else { 2219 audioSpectrumIntervalChangeRequested = true; 2220 } 2221 } 2222 } 2223 } 2224 2225 @Override 2226 public Object getBean() { 2227 return MediaPlayer.this; 2228 } 2229 2230 @Override 2231 public String getName() { 2232 return "audioSpectrumInterval"; 2233 } 2234 }; 2235 } 2236 return audioSpectrumInterval; 2237 } 2238 2239 /** 2240 * The sensitivity threshold in decibels; must be non-positive. Values below 2241 * this threshold with respect to the peak frequency in the given spectral 2242 * band will be set to the value of the threshold. The default value is 2243 * -60 dB. 2244 */ 2245 private IntegerProperty audioSpectrumThreshold; 2246 2247 /** 2248 * Sets the audio spectrum threshold in decibels. 2249 * @param value the spectral threshold in dB; must be ≤ <code>0</code>. 2250 */ 2251 public final void setAudioSpectrumThreshold(int value) { 2252 audioSpectrumThresholdProperty().set(value); 2253 } 2254 2255 /** 2256 * Retrieves the audio spectrum threshold in decibels. 2257 * @return the spectral threshold in dB 2258 */ 2259 public final int getAudioSpectrumThreshold() { 2260 return audioSpectrumThresholdProperty().get(); 2261 } 2262 2263 public IntegerProperty audioSpectrumThresholdProperty() { 2264 if (audioSpectrumThreshold == null) { 2265 audioSpectrumThreshold = new IntegerPropertyBase(DEFAULT_SPECTRUM_THRESHOLD) { 2266 2267 @Override 2268 protected void invalidated() { 2269 synchronized (disposeLock) { 2270 if (getStatus() != Status.DISPOSED) { 2271 if (playerReady) { 2272 jfxPlayer.getAudioSpectrum().setSensitivityThreshold(clamp(audioSpectrumThreshold.get(), Integer.MIN_VALUE, AUDIOSPECTRUM_THRESHOLD_MAX)); 2273 } else { 2274 audioSpectrumThresholdChangeRequested = true; 2275 } 2276 } 2277 } 2278 } 2279 2280 @Override 2281 public Object getBean() { 2282 return MediaPlayer.this; 2283 } 2284 2285 @Override 2286 public String getName() { 2287 return "audioSpectrumThreshold"; 2288 } 2289 }; 2290 } 2291 return audioSpectrumThreshold; 2292 } 2293 2294 /** 2295 * A listener for audio spectrum updates. When the listener is registered, 2296 * audio spectrum computation is enabled; upon removing the listener, 2297 * computation is disabled. Only a single listener may be registered, so if 2298 * multiple observers are required, events must be forwarded. 2299 * 2300 * <p>An <code>AudioSpectrumListener</code> may be useful for example to 2301 * plot the frequency spectrum of the audio being played or to generate 2302 * waveforms for a music visualizer. 2303 */ 2304 private ObjectProperty<AudioSpectrumListener> audioSpectrumListener; 2305 2306 /** 2307 * Sets the listener of the audio spectrum. 2308 * @param listener the spectral listener or <code>null</code>. 2309 */ 2310 public final void setAudioSpectrumListener(AudioSpectrumListener listener) { 2311 audioSpectrumListenerProperty().set(listener); 2312 } 2313 2314 /** 2315 * Retrieves the listener of the audio spectrum. 2316 * @return the spectral listener or <code>null</code> 2317 */ 2318 public final AudioSpectrumListener getAudioSpectrumListener() { 2319 return audioSpectrumListenerProperty().get(); 2320 } 2321 2322 public ObjectProperty<AudioSpectrumListener> audioSpectrumListenerProperty() { 2323 if (audioSpectrumListener == null) { 2324 audioSpectrumListener = new ObjectPropertyBase<AudioSpectrumListener>() { 2325 2326 @Override 2327 protected void invalidated() { 2328 synchronized (disposeLock) { 2329 if (getStatus() != Status.DISPOSED) { 2330 if (playerReady) { 2331 boolean enabled = (audioSpectrumListener.get() != null); 2332 jfxPlayer.getAudioSpectrum().setEnabled(enabled); 2333 } else { 2334 audioSpectrumEnabledChangeRequested = true; 2335 } 2336 } 2337 } 2338 } 2339 2340 @Override 2341 public Object getBean() { 2342 return MediaPlayer.this; 2343 } 2344 2345 @Override 2346 public String getName() { 2347 return "audioSpectrumListener"; 2348 } 2349 }; 2350 } 2351 return audioSpectrumListener; 2352 } 2353 2354 /** 2355 * Free all resources associated with player. Player SHOULD NOT be used after this function is called. 2356 * Player will transition to {@link Status.DISPOSED} after this method is done. This method can be called 2357 * anytime and regarding current player status. 2358 * @since JavaFX 8.0 2359 */ 2360 public synchronized void dispose() { 2361 synchronized (disposeLock) { 2362 setStatus(Status.DISPOSED); 2363 2364 destroyMediaTimer(); 2365 2366 if (audioEqualizer != null) { 2367 audioEqualizer.setAudioEqualizer(null); 2368 audioEqualizer = null; 2369 } 2370 2371 if (jfxPlayer != null) { 2372 jfxPlayer.dispose(); 2373 synchronized (renderLock) { 2374 if (rendererListener != null) { 2375 Toolkit.getToolkit().removeStageTkPulseListener(rendererListener); 2376 rendererListener = null; 2377 } 2378 } 2379 jfxPlayer = null; 2380 } 2381 } 2382 } 2383 2384 /**************************************************************************** 2385 * Listeners section 2386 *************************************************************************** 2387 * Listener of modifications to the marker map in the public Media API. 2388 * Changes to this map are propagated to the implementation layer. 2389 */ 2390 private class MarkerMapChangeListener implements MapChangeListener<String, Duration> { 2391 @Override 2392 public void onChanged(Change<? extends String, ? extends Duration> change) { 2393 synchronized (disposeLock) { 2394 if (getStatus() != Status.DISPOSED) { 2395 String key = change.getKey(); 2396 // Reject null-named markers. 2397 if (key == null) { 2398 return; 2399 } 2400 com.sun.media.jfxmedia.Media jfxMedia = jfxPlayer.getMedia(); 2401 if (change.wasAdded()) { 2402 if (change.wasRemoved()) { 2403 // The remove and add marker calls eventually go to native code 2404 // so we can't depend on the Java Map behavior or replacing a 2405 // key-value pair when the key is already in the Map. Instead we 2406 // explicitly remove the old entry and add the new one. 2407 jfxMedia.removeMarker(key); 2408 } 2409 Duration value = change.getValueAdded(); 2410 // Reject null- or negative-valued marker times. 2411 if (value != null && value.greaterThanOrEqualTo(Duration.ZERO)) { 2412 jfxMedia.addMarker(key, change.getValueAdded().toMillis() / 1000.0); 2413 } 2414 } else if (change.wasRemoved()) { 2415 jfxMedia.removeMarker(key); 2416 } 2417 } 2418 } 2419 } 2420 } 2421 2422 /** 2423 * Listener of marker events emitted by the implementation layer. The 2424 * CURRENT_MARKER property is updated to the most recently received event. 2425 */ 2426 private class _MarkerListener implements MarkerListener { 2427 2428 @Override 2429 public void onMarker(final MarkerEvent evt) { 2430 Platform.runLater(() -> { 2431 Duration markerTime = Duration.millis(evt.getPresentationTime() * 1000.0); 2432 if (getOnMarker() != null) { 2433 getOnMarker().handle(new MediaMarkerEvent(new Pair<String, Duration>(evt.getMarkerName(), markerTime))); 2434 } 2435 }); 2436 } 2437 } 2438 2439 private class _PlayerStateListener implements PlayerStateListener { 2440 @Override 2441 public void onReady(PlayerStateEvent evt) { 2442 //System.out.println("** MediaPlayerFX received onReady!"); 2443 Platform.runLater(() -> { 2444 synchronized (disposeLock) { 2445 if (getStatus() == Status.DISPOSED) { 2446 return; 2447 } 2448 2449 preReady(); 2450 } 2451 }); 2452 } 2453 2454 @Override 2455 public void onPlaying(PlayerStateEvent evt) { 2456 //System.err.println("** MediaPlayerFX received onPlaying!"); 2457 startTimeAtStop = null; 2458 2459 Platform.runLater(() -> { 2460 createMediaTimer(); 2461 setStatus(Status.PLAYING); 2462 }); 2463 } 2464 2465 @Override 2466 public void onPause(PlayerStateEvent evt) { 2467 //System.err.println("** MediaPlayerFX received onPause!"); 2468 2469 Platform.runLater(() -> { 2470 // Disable updating currentTime. 2471 isUpdateTimeEnabled = false; 2472 2473 setStatus(Status.PAUSED); 2474 }); 2475 2476 if (startTimeAtStop != null && startTimeAtStop != getStartTime()) { 2477 startTimeAtStop = null; 2478 Platform.runLater(() -> { 2479 setCurrentTime(getStartTime()); 2480 }); 2481 } 2482 } 2483 2484 @Override 2485 public void onStop(PlayerStateEvent evt) { 2486 //System.err.println("** MediaPlayerFX received onStop!"); 2487 Platform.runLater(() -> { 2488 // Destroy media time and update current time 2489 destroyMediaTimer(); 2490 startTimeAtStop = getStartTime(); 2491 setCurrentTime(getStartTime()); 2492 setStatus(Status.STOPPED); 2493 }); 2494 } 2495 2496 @Override 2497 public void onStall(PlayerStateEvent evt) { 2498 //System.err.println("** MediaPlayerFX received onStall!"); 2499 Platform.runLater(() -> { 2500 // Disable updating currentTime. 2501 isUpdateTimeEnabled = false; 2502 2503 setStatus(Status.STALLED); 2504 }); 2505 } 2506 2507 void handleFinish() { 2508 //System.err.println("** MediaPlayerFX handleFinish"); 2509 2510 // Increment number of times media has played. 2511 setCurrentCount(getCurrentCount() + 1); 2512 2513 // Rewind and play from the beginning if the number 2514 // of repeats has yet to be reached. 2515 if ((getCurrentCount() < getCycleCount()) || (getCycleCount() == INDEFINITE)) { 2516 if (getOnEndOfMedia() != null) { 2517 Platform.runLater(getOnEndOfMedia()); 2518 } 2519 2520 loopPlayback(); 2521 2522 if (getOnRepeat() != null) { 2523 Platform.runLater(getOnRepeat()); 2524 } 2525 } else { 2526 // Player status remains PLAYING. 2527 2528 // Disable updating currentTime. 2529 isUpdateTimeEnabled = false; 2530 2531 // Set current rate to zero. 2532 setCurrentRate(0.0); 2533 2534 // Set EOS flag 2535 isEOS = true; 2536 2537 if (getOnEndOfMedia() != null) { 2538 Platform.runLater(getOnEndOfMedia()); 2539 } 2540 } 2541 } 2542 2543 @Override 2544 public void onFinish(PlayerStateEvent evt) { 2545 //System.err.println("** MediaPlayerFX received onFinish!"); 2546 startTimeAtStop = null; 2547 2548 Platform.runLater(() -> { 2549 handleFinish(); 2550 }); 2551 } 2552 2553 @Override 2554 public void onHalt(final PlayerStateEvent evt) { 2555 Platform.runLater(() -> { 2556 setStatus(Status.HALTED); 2557 handleError(MediaException.haltException(evt.getMessage())); 2558 2559 // Disable updating currentTime. 2560 isUpdateTimeEnabled = false; 2561 }); 2562 } 2563 } 2564 2565 private class _PlayerTimeListener implements PlayerTimeListener { 2566 double theDuration; 2567 2568 void handleDurationChanged() { 2569 media.setDuration(Duration.millis(theDuration * 1000.0)); 2570 } 2571 2572 @Override 2573 public void onDurationChanged(final double duration) { 2574 //System.err.println("** MediaPlayerFX received onDurationChanged!"); 2575 Platform.runLater(() -> { 2576 theDuration = duration; 2577 handleDurationChanged(); 2578 }); 2579 } 2580 } 2581 2582 private class _VideoTrackSizeListener implements VideoTrackSizeListener { 2583 int trackWidth; 2584 int trackHeight; 2585 2586 @Override 2587 public void onSizeChanged(final int width, final int height) { 2588 Platform.runLater(() -> { 2589 if (media != null) { 2590 trackWidth = width; 2591 trackHeight = height; 2592 setSize(); 2593 } 2594 }); 2595 } 2596 2597 void setSize() { 2598 media.setWidth(trackWidth); 2599 media.setHeight(trackHeight); 2600 2601 synchronized (viewRefs) { 2602 for (WeakReference<MediaView> vref : viewRefs) { 2603 MediaView v = vref.get(); 2604 if (v != null) { 2605 v.notifyMediaSizeChange(); 2606 } 2607 } 2608 } 2609 } 2610 } 2611 2612 private class _MediaErrorListener implements com.sun.media.jfxmedia.events.MediaErrorListener { 2613 @Override 2614 public void onError(Object source, int errorCode, String message) { 2615 MediaException error = MediaException.getMediaException(source, errorCode, message); 2616 2617 handleError(error); 2618 } 2619 } 2620 2621 private class _BufferListener implements BufferListener { 2622 double bufferedTime; // time in ms 2623 2624 @Override 2625 public void onBufferProgress(BufferProgressEvent evt) { 2626 if (media != null) { 2627 if (evt.getDuration() > 0.0) { 2628 double position = evt.getBufferPosition(); //Must assign. I don't know how to convert integer to number otherwise. 2629 double stop = evt.getBufferStop(); 2630 bufferedTime = position/stop * evt.getDuration()*1000.0; 2631 lastBufferEvent = null; 2632 2633 Platform.runLater(() -> { 2634 setBufferProgressTime(Duration.millis(bufferedTime)); 2635 }); 2636 } else { 2637 lastBufferEvent = evt; 2638 } 2639 } 2640 } 2641 } 2642 2643 private class _SpectrumListener implements com.sun.media.jfxmedia.events.AudioSpectrumListener { 2644 private float[] magnitudes; 2645 private float[] phases; 2646 2647 @Override public void onAudioSpectrumEvent(final AudioSpectrumEvent evt) { 2648 Platform.runLater(() -> { 2649 AudioSpectrumListener listener = getAudioSpectrumListener(); 2650 if (listener != null) { 2651 listener.spectrumDataUpdate(evt.getTimestamp(), 2652 evt.getDuration(), 2653 magnitudes = evt.getSource().getMagnitudes(magnitudes), 2654 phases = evt.getSource().getPhases(phases)); 2655 } 2656 }); 2657 } 2658 } 2659 2660 private final Object renderLock = new Object(); 2661 private VideoDataBuffer currentRenderFrame; 2662 private VideoDataBuffer nextRenderFrame; 2663 2664 // NGMediaView will call this to get the frame to render 2665 /** 2666 * WARNING: You must call releaseFrame() on the returned frame when you are 2667 * finished with it or a massive memory leak will occur. 2668 * 2669 * @return the current frame to be used for rendering, or null if not in a render cycle 2670 * @treatAsPrivate implementation detail 2671 * @deprecated This is an internal API that is not intended for use and will be removed in the next version 2672 */ 2673 @Deprecated 2674 public VideoDataBuffer impl_getLatestFrame() { 2675 synchronized (renderLock) { 2676 if (null != currentRenderFrame) { 2677 currentRenderFrame.holdFrame(); 2678 } 2679 return currentRenderFrame; 2680 } 2681 } 2682 2683 private class RendererListener implements 2684 com.sun.media.jfxmedia.events.VideoRendererListener, 2685 TKPulseListener 2686 { 2687 boolean updateMediaViews; 2688 2689 @Override 2690 public void videoFrameUpdated(NewFrameEvent nfe) { 2691 VideoDataBuffer vdb = nfe.getFrameData(); 2692 if (null != vdb) { 2693 2694 Duration frameTS = new Duration(vdb.getTimestamp() * 1000); 2695 Duration stopTime = getStopTime(); 2696 if (frameTS.greaterThanOrEqualTo(getStartTime()) && (stopTime.isUnknown() || frameTS.lessThanOrEqualTo(stopTime))) { 2697 updateMediaViews = true; 2698 2699 synchronized (renderLock) { 2700 vdb.holdFrame(); 2701 2702 // currentRenderFrame must not be touched, queue this one for later 2703 if (null != nextRenderFrame) { 2704 nextRenderFrame.releaseFrame(); 2705 } 2706 nextRenderFrame = vdb; 2707 } 2708 // make sure we get the next pulse so we can update our textures 2709 Toolkit.getToolkit().requestNextPulse(); 2710 } else { 2711 vdb.releaseFrame(); 2712 } 2713 } 2714 } 2715 2716 @Override 2717 public void releaseVideoFrames() { 2718 synchronized (renderLock) { 2719 if (null != currentRenderFrame) { 2720 currentRenderFrame.releaseFrame(); 2721 currentRenderFrame = null; 2722 } 2723 2724 if (null != nextRenderFrame) { 2725 nextRenderFrame.releaseFrame(); 2726 nextRenderFrame = null; 2727 } 2728 } 2729 } 2730 2731 @Override 2732 public void pulse() { 2733 if (updateMediaViews) { 2734 updateMediaViews = false; 2735 2736 /* swap in the next frame if there is one 2737 * this should be done exactly once per render cycle so that all 2738 * views display the same image. 2739 */ 2740 synchronized (renderLock) { 2741 if (null != nextRenderFrame) { 2742 if (null != currentRenderFrame) { 2743 currentRenderFrame.releaseFrame(); 2744 } 2745 currentRenderFrame = nextRenderFrame; 2746 nextRenderFrame = null; 2747 } 2748 } 2749 2750 // tell all media views that their content needs to be redrawn 2751 synchronized (viewRefs) { 2752 Iterator<WeakReference<MediaView>> iter = viewRefs.iterator(); 2753 while (iter.hasNext()) { 2754 MediaView view = iter.next().get(); 2755 if (null != view) { 2756 view.notifyMediaFrameUpdated(); 2757 } else { 2758 iter.remove(); 2759 } 2760 } 2761 } 2762 } 2763 } 2764 } 2765 } 2766 2767 class MediaPlayerShutdownHook implements Runnable { 2768 2769 private final static List<WeakReference<MediaPlayer>> playerRefs = new ArrayList<WeakReference<MediaPlayer>>(); 2770 private static boolean isShutdown = false; 2771 2772 static { 2773 Toolkit.getToolkit().addShutdownHook(new MediaPlayerShutdownHook()); 2774 } 2775 2776 public static void addMediaPlayer(MediaPlayer player) { 2777 synchronized (playerRefs) { 2778 if (isShutdown) { 2779 com.sun.media.jfxmedia.MediaPlayer jfxPlayer = player.retrieveJfxPlayer(); 2780 if (jfxPlayer != null) { 2781 jfxPlayer.dispose(); 2782 } 2783 } else { 2784 for (ListIterator<WeakReference<MediaPlayer>> it = playerRefs.listIterator(); it.hasNext();) { 2785 MediaPlayer l = it.next().get(); 2786 if (l == null) { 2787 it.remove(); 2788 } 2789 } 2790 2791 playerRefs.add(new WeakReference<MediaPlayer>(player)); 2792 } 2793 } 2794 } 2795 2796 @Override 2797 public void run() { 2798 synchronized (playerRefs) { 2799 for (ListIterator<WeakReference<MediaPlayer>> it = playerRefs.listIterator(); it.hasNext();) { 2800 MediaPlayer player = it.next().get(); 2801 if (player != null) { 2802 player.destroyMediaTimer(); 2803 com.sun.media.jfxmedia.MediaPlayer jfxPlayer = player.retrieveJfxPlayer(); 2804 if (jfxPlayer != null) { 2805 jfxPlayer.dispose(); 2806 } 2807 } else { 2808 it.remove(); 2809 } 2810 } 2811 2812 isShutdown = true; 2813 } 2814 } 2815 } 2816 2817 class MediaTimerTask extends TimerTask { 2818 2819 private Timer mediaTimer = null; 2820 static final Object timerLock = new Object(); 2821 private WeakReference<MediaPlayer> playerRef; 2822 2823 MediaTimerTask(MediaPlayer player) { 2824 playerRef = new WeakReference<MediaPlayer>(player); 2825 } 2826 2827 void start() { 2828 if (mediaTimer == null) { 2829 mediaTimer = new Timer(true); 2830 mediaTimer.scheduleAtFixedRate(this, 0, 100 /* period ms*/); 2831 } 2832 } 2833 2834 void stop() { 2835 if (mediaTimer != null) { 2836 mediaTimer.cancel(); 2837 mediaTimer = null; 2838 } 2839 } 2840 2841 @Override 2842 public void run() { 2843 synchronized (timerLock) { 2844 final MediaPlayer player = playerRef.get(); 2845 if (player != null) { 2846 2847 Platform.runLater(() -> { 2848 synchronized (timerLock) { 2849 player.updateTime(); 2850 } 2851 }); 2852 } else { 2853 cancel(); 2854 } 2855 } 2856 } 2857 }