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,&nbsp;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,&nbsp;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&nbsp;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,&nbsp;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,&nbsp;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,&nbsp;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,&nbsp;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&nbsp;&gt;&nbsp;stopTime</code>
 978      * then <code>stopTime</code> is clamped as
 979      * <code>stopTime&nbsp;&ge;&nbsp;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,&nbsp;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&nbsp;&le;&nbsp;startTime&nbsp;&lt;&nbsp;{@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},&nbsp;{@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}&nbsp;&lt;&nbsp;stopTime&nbsp;&le;&nbsp;{@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},&nbsp;{@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&nbsp;&lt;&nbsp;start time</td><td>start time</td></tr>
1371      * <tr><td>seekTime&nbsp;&gt;&nbsp;stop time</td><td>stop time</td></tr>
1372      * <tr><td>start time&nbsp;&le;&nbsp;seekTime&nbsp;&le;&nbsp;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&nbsp;&ge;&nbsp;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,&nbsp;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 &ge; 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 &le; <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 }