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