/* * Copyright (c) 2010, 2016, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it * under the terms of the GNU General Public License version 2 only, as * published by the Free Software Foundation. Oracle designates this * particular file as subject to the "Classpath" exception as provided * by Oracle in the LICENSE file that accompanied this code. * * This code is distributed in the hope that it will be useful, but WITHOUT * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License * version 2 for more details (a copy is included in the LICENSE file that * accompanied this code). * * You should have received a copy of the GNU General Public License version * 2 along with this work; if not, write to the Free Software Foundation, * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. * * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA * or visit www.oracle.com if you need additional information or have any * questions. */ package javafx.scene.media; import java.lang.ref.WeakReference; import java.util.HashSet; import java.util.Iterator; import java.util.Set; import java.util.Timer; import java.util.TimerTask; import java.util.List; import java.util.ListIterator; import java.util.ArrayList; import javafx.application.Platform; import javafx.beans.NamedArg; import javafx.beans.property.BooleanProperty; import javafx.beans.property.BooleanPropertyBase; import javafx.beans.property.DoubleProperty; import javafx.beans.property.DoublePropertyBase; import javafx.beans.property.IntegerProperty; import javafx.beans.property.IntegerPropertyBase; import javafx.beans.property.ObjectProperty; import javafx.beans.property.ObjectPropertyBase; import javafx.beans.property.SimpleObjectProperty; import javafx.collections.MapChangeListener; import javafx.collections.ObservableMap; import javafx.util.Duration; import javafx.util.Pair; import com.sun.javafx.tk.TKPulseListener; import com.sun.javafx.tk.Toolkit; import com.sun.media.jfxmedia.MediaManager; import com.sun.media.jfxmedia.control.VideoDataBuffer; import com.sun.media.jfxmedia.effects.AudioSpectrum; import com.sun.media.jfxmedia.events.AudioSpectrumEvent; import com.sun.media.jfxmedia.events.BufferListener; import com.sun.media.jfxmedia.events.BufferProgressEvent; import com.sun.media.jfxmedia.events.MarkerEvent; import com.sun.media.jfxmedia.events.MarkerListener; import com.sun.media.jfxmedia.events.NewFrameEvent; import com.sun.media.jfxmedia.events.PlayerStateEvent; import com.sun.media.jfxmedia.events.PlayerStateListener; import com.sun.media.jfxmedia.events.PlayerTimeListener; import com.sun.media.jfxmedia.events.VideoTrackSizeListener; import com.sun.media.jfxmedia.locator.Locator; import java.util.*; import javafx.beans.property.ReadOnlyDoubleProperty; import javafx.beans.property.ReadOnlyDoubleWrapper; import javafx.beans.property.ReadOnlyIntegerProperty; import javafx.beans.property.ReadOnlyIntegerWrapper; import javafx.beans.property.ReadOnlyObjectProperty; import javafx.beans.property.ReadOnlyObjectWrapper; import javafx.event.EventHandler; /** * The MediaPlayer class provides the controls for playing media. * It is used in combination with the {@link Media} and {@link MediaView} * classes to display and control media playback. MediaPlayer does * not contain any visual elements so must be used with the {@link MediaView} * class to view any video track which may be present. * *

MediaPlayer provides the {@link #pause()}, {@link #play()}, * {@link #stop()} and {@link #seek(javafx.util.Duration) seek()} controls as * well as the {@link #rateProperty rate} and {@link #autoPlayProperty autoPlay} * properties which apply to all types of media. It also provides the * {@link #balanceProperty balance}, {@link #muteProperty mute}, and * {@link #volumeProperty volume} properties which control audio playback * characteristics. Further control over audio quality may be attained via the * {@link AudioEqualizer} associated with the player. Frequency descriptors of * audio playback may be observed by registering an {@link AudioSpectrumListener}. * Information about playback position, rate, and buffering may be obtained from * the {@link #currentTimeProperty currentTime}, * {@link #currentRateProperty currentRate}, and * {@link #bufferProgressTimeProperty bufferProgressTime} * properties, respectively. Media marker notifications are received by an event * handler registered as the {@link #onMarkerProperty onMarker} property.

* *

For finite duration media, playback may be positioned at any point in time * between 0.0 and the duration of the media. MediaPlayer * refines this definition by adding the {@link #startTimeProperty startTime} and * {@link #stopTimeProperty stopTime} * properties which in effect define a virtual media source with time position * constrained to [startTime,stopTime]. Media playback * commences at startTime and continues to stopTime. * The interval defined by these two endpoints is termed a cycle with * duration being the difference of the stop and start times. This cycle * may be set to repeat a specific or indefinite number of times. The total * duration of media playback is then the product of the cycle duration and the * number of times the cycle is played. If the stop time of the cycle is reached * and the cycle is to be played again, the event handler registered with the * {@link #onRepeatProperty onRepeat} property is invoked. If the stop time is reached and * the cycle is not to be repeated, then the event handler registered * with the {@link #onEndOfMediaProperty onEndOfMedia} property is invoked. A zero-relative index of * which cycle is presently being played is maintained by {@link #currentCountProperty currentCount}. *

* *

The operation of a MediaPlayer is inherently asynchronous. * A player is not prepared to respond to commands quasi-immediately until * its status has transitioned to {@link Status#READY}, which in * effect generally occurs when media pre-roll completes. Some requests made of * a player prior to its status being READY will however take * effect when that status is entered. These include invoking {@link #play()} * without an intervening invocation of {@link #pause()} or {@link #stop()} * before the READY transition, as well as setting any of the * {@link #autoPlayProperty autoPlay}, {@link #balanceProperty balance}, * {@link #muteProperty mute}, {@link #rateProperty rate}, * {@link #startTimeProperty startTime}, {@link #stopTimeProperty stopTime}, and * {@link #volumeProperty volume} properties.

* *

The {@link #statusProperty status} * property may be monitored to make the application aware of player status * changes, and callback functions may be registered via properties such as * {@link #onReadyProperty onReady} if an action should be taken when a particular status is * entered. There are also {@link #errorProperty error} and {@link #onErrorProperty onError} properties which * respectively enable monitoring when an error occurs and taking a specified * action in response thereto.

* *

The same MediaPlayer object may be shared among multiple * MediaViews. This will not affect the player itself. In * particular, the property settings of the view will not have any effect on * media playback.

* @see Media * @see MediaView * @since JavaFX 2.0 */ public final class MediaPlayer { /** * Enumeration describing the different status values of a {@link MediaPlayer}. * * The principal MediaPlayer status transitions are given in the * following table: * * * * * * * * * * * * * * * * * * * * * * * *
Current \ NextREADYPAUSEDPLAYINGSTALLEDSTOPPED
UNKNOWNpre-roll
READYautoplay; play()
PAUSEDplay()stop()
PLAYINGpause()buffering datastop()
STALLEDpause()data bufferedstop()
STOPPEDpause()play()
*

*

The table rows represent the current state of the player and the columns * the next state of the player. The cell at the intersection of a given row * and column lists the events which can cause a transition from the row * state to the column state. An empty cell represents an impossible transition. * The transitions to UNKNOWN and to and from HALTED * status are intentionally not tabulated. UNKNOWN is the initial * status of the player before the media source is pre-rolled and cannot be * entered once exited. HALTED is a terminal status entered when * an error occurs and may be transitioned into from any other status but not * exited. *

*

* The principal MediaPlayer status values and transitions are * depicted in the following diagram: *

* MediaPlayer status diagram *

*

* Reaching the end of the media (or the * {@link #stopTimeProperty stopTime} if this is defined) while playing does not cause the * status to change from PLAYING. Therefore, for example, if * the media is played to its end and then a manual seek to an earlier * time within the media is performed, playing will continue from the * new media time. *

* @since JavaFX 2.0 */ public enum Status { /** * State of the player immediately after creation. While in this state, * property values are not reliable and should not be considered. * Additionally, commands sent to the player while in this state will be * buffered until the media is fully loaded and ready to play. */ UNKNOWN, /** * State of the player once it is prepared to play. * This state is entered only once when the movie is loaded and pre-rolled. */ READY, /** * State of the player when playback is paused. Requesting the player * to play again will cause it to continue where it left off. */ PAUSED, /** * State of the player when it is currently playing. */ PLAYING, /** * State of the player when playback has stopped. Requesting the player * to play again will cause it to start playback from the beginning. */ STOPPED, /** * State of the player when data coming into the buffer has slowed or * stopped and the playback buffer does not have enough data to continue * playing. Playback will continue automatically when enough data are * buffered to resume playback. If paused or stopped in this state, then * buffering will continue but playback will not resume automatically * when sufficient data are buffered. */ STALLED, /** * State of the player when a critical error has occurred. This state * indicates playback can never continue again with this player. The * player is no longer functional and a new player should be created. */ HALTED, /** * State of the player after dispose() method is invoked. This state indicates * player is disposed, all resources are free and player SHOULD NOT be used again. * Media and MediaView objects associated with disposed player can be reused. * @since JavaFX 8.0 */ DISPOSED }; /** * A value representing an effectively infinite number of playback cycles. * When {@link #cycleCountProperty cycleCount} is set to this value, the player * will replay the Media until stopped or paused. */ public static final int INDEFINITE = -1; // Note: this is a count, not a Duration. private static final double RATE_MIN = 0.0; private static final double RATE_MAX = 8.0; private static final int AUDIOSPECTRUM_THRESHOLD_MAX = 0; // dB private static final double AUDIOSPECTRUM_INTERVAL_MIN = 0.000000001; // seconds private static final int AUDIOSPECTRUM_NUMBANDS_MIN = 2; // The underlying player private com.sun.media.jfxmedia.MediaPlayer jfxPlayer; // Need package getter for MediaView com.sun.media.jfxmedia.MediaPlayer retrieveJfxPlayer() { synchronized (disposeLock) { return jfxPlayer; } } private MapChangeListener markerMapListener = null; private MarkerListener markerEventListener = null; private PlayerStateListener stateListener = null; private PlayerTimeListener timeListener = null; private VideoTrackSizeListener sizeListener = null; private com.sun.media.jfxmedia.events.MediaErrorListener errorListener = null; private BufferListener bufferListener = null; private com.sun.media.jfxmedia.events.AudioSpectrumListener spectrumListener = null; private RendererListener rendererListener = null; // Store requested operations sent before we receive the onReady event private boolean rateChangeRequested = false; private boolean volumeChangeRequested = false; private boolean balanceChangeRequested = false; private boolean startTimeChangeRequested = false; private boolean stopTimeChangeRequested = false; private boolean muteChangeRequested = false; private boolean playRequested = false; private boolean audioSpectrumNumBandsChangeRequested = false; private boolean audioSpectrumIntervalChangeRequested = false; private boolean audioSpectrumThresholdChangeRequested = false; private boolean audioSpectrumEnabledChangeRequested = false; private MediaTimerTask mediaTimerTask = null; private double prevTimeMs = -1.0; private boolean isUpdateTimeEnabled = false; private BufferProgressEvent lastBufferEvent = null; private Duration startTimeAtStop = null; private boolean isEOS = false; private final Object disposeLock = new Object(); private final static int DEFAULT_SPECTRUM_BAND_COUNT = 128; private final static double DEFAULT_SPECTRUM_INTERVAL = 0.1; private final static int DEFAULT_SPECTRUM_THRESHOLD = -60; // views to be notified on media change private final Set> viewRefs = new HashSet>(); /** * The read-only {@link AudioEqualizer} associated with this player. The * equalizer is enabled by default. */ private AudioEqualizer audioEqualizer; private static double clamp(double dvalue, double dmin, double dmax) { if (dmin != Double.MIN_VALUE && dvalue < dmin) { return dmin; } else if (dmax != Double.MAX_VALUE && dvalue > dmax) { return dmax; } else { return dvalue; } } private static int clamp(int ivalue, int imin, int imax) { if (imin != Integer.MIN_VALUE && ivalue < imin) { return imin; } else if (imax != Integer.MAX_VALUE && ivalue > imax) { return imax; } else { return ivalue; } } /** * Retrieve the {@link AudioEqualizer} associated with this player. * @return the AudioEqualizer or null if player is disposed. */ public final AudioEqualizer getAudioEqualizer() { synchronized (disposeLock) { if (getStatus() == Status.DISPOSED) { return null; } if (audioEqualizer == null) { audioEqualizer = new AudioEqualizer(); if (jfxPlayer != null) { audioEqualizer.setAudioEqualizer(jfxPlayer.getEqualizer()); } audioEqualizer.setEnabled(true); } return audioEqualizer; } } /** * Create a player for a specific media. This is the only way to associate * a Media object with a MediaPlayer: once the * player is created it cannot be changed. Errors which occur synchronously * within the constructor will cause exceptions to be thrown. Errors which * occur asynchronously will cause the {@link #errorProperty error} property to be set and * consequently any {@link #onErrorProperty onError} callback to be invoked. * *

When created, the {@link #statusProperty status} of the player will be {@link Status#UNKNOWN}. * Once the status has transitioned to {@link Status#READY} the * player will be in a usable condition. The amount of time between player * creation and its entering READY status may vary depending, * for example, on whether the media is being read over a network connection * or from a local file system. * * @param media The media to play. * @throws NullPointerException if media is null. * @throws MediaException if any synchronous errors occur within the * constructor. */ public MediaPlayer(@NamedArg("media") Media media) { if (null == media) { throw new NullPointerException("media == null!"); } this.media = media; // So we can get errors during initialization from other threads (Ex. HLS). errorListener = new _MediaErrorListener(); MediaManager.addMediaErrorListener(errorListener); try { // Init MediaPlayer. Run on separate thread if locator can block. Locator locator = media.retrieveJfxLocator(); if (locator.canBlock()) { InitMediaPlayer initMediaPlayer = new InitMediaPlayer(); Thread t = new Thread(initMediaPlayer); t.setDaemon(true); t.start(); } else { init(); } } catch (com.sun.media.jfxmedia.MediaException e) { throw MediaException.exceptionToMediaException(e); } catch (MediaException e) { throw e; } } void registerListeners() { synchronized (disposeLock) { if (getStatus() == Status.DISPOSED) { return; } if (jfxPlayer != null) { // Register jfxPlayer for dispose. It will be disposed when FX MediaPlayer does not have // any strong references. MediaManager.registerMediaPlayerForDispose(this, jfxPlayer); jfxPlayer.addMediaErrorListener(errorListener); jfxPlayer.addMediaTimeListener(timeListener); jfxPlayer.addVideoTrackSizeListener(sizeListener); jfxPlayer.addBufferListener(bufferListener); jfxPlayer.addMarkerListener(markerEventListener); jfxPlayer.addAudioSpectrumListener(spectrumListener); jfxPlayer.getVideoRenderControl().addVideoRendererListener(rendererListener); jfxPlayer.addMediaPlayerListener(stateListener); } if (null != rendererListener) { // add a stage listener, this will be called before scene listeners // so we can make sure the dirty bits are set correctly before PG sync Toolkit.getToolkit().addStageTkPulseListener(rendererListener); } } } private void init() throws MediaException { try { // Create a new player Locator locator = media.retrieveJfxLocator(); // This call will block until we connected or fail to connect. // Call it here, so we do not block while initializing and holding locks like disposeLock. locator.waitForReadySignal(); synchronized (disposeLock) { if (getStatus() == Status.DISPOSED) { return; } jfxPlayer = MediaManager.getPlayer(locator); if (jfxPlayer != null) { // Register media player with shutdown hook. MediaPlayerShutdownHook.addMediaPlayer(this); // Make sure we start with a known state jfxPlayer.setBalance((float) getBalance()); jfxPlayer.setMute(isMute()); jfxPlayer.setVolume((float) getVolume()); // Create listeners for the Player's event sizeListener = new _VideoTrackSizeListener(); stateListener = new _PlayerStateListener(); timeListener = new _PlayerTimeListener(); bufferListener = new _BufferListener(); markerEventListener = new _MarkerListener(); spectrumListener = new _SpectrumListener(); rendererListener = new RendererListener(); } // Listen to Media.getMarkers() so as to propagate updates of the // map to the implementation layer. markerMapListener = new MarkerMapChangeListener(); ObservableMap markers = media.getMarkers(); markers.addListener(markerMapListener); // Propagate to the implementation layer any markers already in // Media.getMarkers(). com.sun.media.jfxmedia.Media jfxMedia = jfxPlayer.getMedia(); for (Map.Entry entry : markers.entrySet()) { String markerName = entry.getKey(); if (markerName != null) { Duration markerTime = entry.getValue(); if (markerTime != null) { double msec = markerTime.toMillis(); if (msec >= 0.0) { jfxMedia.addMarker(markerName, msec / 1000.0); } } } } } } catch (com.sun.media.jfxmedia.MediaException e) { throw MediaException.exceptionToMediaException(e); } // Register for the Player's event Platform.runLater(() -> { registerListeners(); }); } private class InitMediaPlayer implements Runnable { @Override public void run() { try { init(); } catch (com.sun.media.jfxmedia.MediaException e) { handleError(MediaException.exceptionToMediaException(e)); } catch (MediaException e) { // Check media object for error. If it is connection related, then Media object will have better error message if (media.getError() != null) { handleError(media.getError()); } else { handleError(e); } } catch (Exception e) { handleError(new MediaException(MediaException.Type.UNKNOWN, e.getMessage())); } } } /** * Observable property set to a MediaException if an error occurs. */ private ReadOnlyObjectWrapper error; private void setError(MediaException value) { if (getError() == null) { errorPropertyImpl().set(value); } } /** * Retrieve the value of the {@link #errorProperty error} property or null * if there is no error. * @return a MediaException or null. */ public final MediaException getError() { return error == null ? null : error.get(); } public ReadOnlyObjectProperty errorProperty() { return errorPropertyImpl().getReadOnlyProperty(); } private ReadOnlyObjectWrapper errorPropertyImpl() { if (error == null) { error = new ReadOnlyObjectWrapper() { @Override protected void invalidated() { if (getOnError() != null) { Platform.runLater(getOnError()); } } @Override public Object getBean() { return MediaPlayer.this; } @Override public String getName() { return "error"; } }; } return error; } /** * Event handler invoked when an error occurs. */ private ObjectProperty onError; /** * Sets the event handler to be called when an error occurs. * @param value the event handler or null. */ public final void setOnError(Runnable value) { onErrorProperty().set(value); } /** * Retrieves the event handler for errors. * @return the event handler. */ public final Runnable getOnError() { return onError == null ? null : onError.get(); } public ObjectProperty onErrorProperty() { if (onError == null) { onError = new ObjectPropertyBase() { @Override protected void invalidated() { /* * if we have an existing error condition schedule the handler to be * called immediately. This way the client app does not have to perform * an explicit error check. */ if (get() != null && getError() != null) { Platform.runLater(get()); } } @Override public Object getBean() { return MediaPlayer.this; } @Override public String getName() { return "onError"; } }; } return onError; } /** * The parent {@link Media} object; read-only. * * @see Media */ private Media media; /** * Retrieves the {@link Media} instance being played. * @return the Media object. */ public final Media getMedia() { return media; } /** * Whether playing should start as soon as possible. For a new player this * will occur once the player has reached the READY state. The default * value is false. * * @see MediaPlayer.Status */ private BooleanProperty autoPlay; /** * Sets the {@link #autoPlayProperty autoPlay} property value. * @param value whether to enable auto-playback */ public final void setAutoPlay(boolean value) { autoPlayProperty().set(value); } /** * Retrieves the {@link #autoPlayProperty autoPlay} property value. * @return the value. */ public final boolean isAutoPlay() { return autoPlay == null ? false : autoPlay.get(); } public BooleanProperty autoPlayProperty() { if (autoPlay == null) { autoPlay = new BooleanPropertyBase() { @Override protected void invalidated() { if (autoPlay.get()) { play(); } else { playRequested = false; } } @Override public Object getBean() { return MediaPlayer.this; } @Override public String getName() { return "autoPlay"; } }; } return autoPlay; } private boolean playerReady; /** * Starts playing the media. If previously paused, then playback resumes * where it was paused. If playback was stopped, playback starts * from the {@link #startTimeProperty startTime}. When playing actually starts the * {@link #statusProperty status} will be set to {@link Status#PLAYING}. */ public void play() { synchronized (disposeLock) { if (getStatus() != Status.DISPOSED) { if (playerReady) { jfxPlayer.play(); } else { playRequested = true; } } } } /** * Pauses the player. Once the player is actually paused the {@link #statusProperty status} * will be set to {@link Status#PAUSED}. */ public void pause() { synchronized (disposeLock) { if (getStatus() != Status.DISPOSED) { if (playerReady) { jfxPlayer.pause(); } else { playRequested = false; } } } } /** * Stops playing the media. This operation resets playback to * {@link #startTimeProperty startTime}, and resets * {@link #currentCountProperty currentCount} to zero. Once the player is actually * stopped, the {@link #statusProperty status} will be set to {@link Status#STOPPED}. The * only transitions out of STOPPED status are to * {@link Status#PAUSED} and {@link Status#PLAYING} which occur after * invoking {@link #pause()} or {@link #play()}, respectively. * While stopped, the player will not respond to playback position changes * requested by {@link #seek(javafx.util.Duration)}. */ public void stop() { synchronized (disposeLock) { if (getStatus() != Status.DISPOSED) { if (playerReady) { jfxPlayer.stop(); setCurrentCount(0); destroyMediaTimer(); // Stop media timer } else { playRequested = false; } } } } /** * The rate at which the media should be played. For example, a rate of * 1.0 plays the media at its normal (encoded) playback rate, * 2.0 plays back at twice the normal rate, etc. The currently * supported range of rates is [0.0, 8.0]. The default * value is 1.0. */ private DoubleProperty rate; /** * Sets the playback rate to the supplied value. Its effect will be clamped * to the range [0.0, 8.0]. * Invoking this method will have no effect if media duration is {@link Duration#INDEFINITE}. * @param value the playback rate */ public final void setRate(double value) { rateProperty().set(value); } /** * Retrieves the playback rate. * @return the playback rate */ public final double getRate() { return rate == null ? 1.0 : rate.get(); } public DoubleProperty rateProperty() { if (rate == null) { rate = new DoublePropertyBase(1.0) { @Override protected void invalidated() { synchronized (disposeLock) { if (getStatus() != Status.DISPOSED) { if (playerReady) { if (jfxPlayer.getDuration() != Double.POSITIVE_INFINITY) { jfxPlayer.setRate((float) clamp(rate.get(), RATE_MIN, RATE_MAX)); } } else { rateChangeRequested = true; } } } } @Override public Object getBean() { return MediaPlayer.this; } @Override public String getName() { return "rate"; } }; } return rate; } /** * The current rate of playback regardless of settings. For example, if * rate is set to 1.0 and the player is paused or stalled, * then currentRate will be zero. */ // FIXME: we should see if we can track rate in the native player instead private ReadOnlyDoubleWrapper currentRate; private void setCurrentRate(double value) { currentRatePropertyImpl().set(value); } /** * Retrieves the current playback rate. * @return the current rate */ public final double getCurrentRate() { return currentRate == null ? 0.0 : currentRate.get(); } public ReadOnlyDoubleProperty currentRateProperty() { return currentRatePropertyImpl().getReadOnlyProperty(); } private ReadOnlyDoubleWrapper currentRatePropertyImpl() { if (currentRate == null) { currentRate = new ReadOnlyDoubleWrapper(this, "currentRate"); } return currentRate; } /** * The volume at which the media should be played. The range of effective * values is [0.0 1.0] where 0.0 is inaudible * and 1.0 is full volume, which is the default. */ private DoubleProperty volume; /** * Sets the audio playback volume. Its effect will be clamped to the range * [0.0, 1.0]. * * @param value the volume */ public final void setVolume(double value) { volumeProperty().set(value); } /** * Retrieves the audio playback volume. The default value is 1.0. * @return the audio volume */ public final double getVolume() { return volume == null ? 1.0 : volume.get(); } public DoubleProperty volumeProperty() { if (volume == null) { volume = new DoublePropertyBase(1.0) { @Override protected void invalidated() { synchronized (disposeLock) { if (getStatus() != Status.DISPOSED) { if (playerReady) { jfxPlayer.setVolume((float) clamp(volume.get(), 0.0, 1.0)); } else { volumeChangeRequested = true; } } } } @Override public Object getBean() { return MediaPlayer.this; } @Override public String getName() { return "volume"; } }; } return volume; } /** * The balance, or left-right setting, of the audio output. The range of * effective values is [-1.0, 1.0] with -1.0 * being full left, 0.0 center, and 1.0 full right. * The default value is 0.0. */ private DoubleProperty balance; /** * Sets the audio balance. Its effect will be clamped to the range * [-1.0, 1.0]. * @param value the balance */ public final void setBalance(double value) { balanceProperty().set(value); } /** * Retrieves the audio balance. * @return the audio balance */ public final double getBalance() { return balance == null ? 0.0F : balance.get(); } public DoubleProperty balanceProperty() { if (balance == null) { balance = new DoublePropertyBase() { @Override protected void invalidated() { synchronized (disposeLock) { if (getStatus() != Status.DISPOSED) { if (playerReady) { jfxPlayer.setBalance((float) clamp(balance.get(), -1.0, 1.0)); } else { balanceChangeRequested = true; } } } } @Override public Object getBean() { return MediaPlayer.this; } @Override public String getName() { return "balance"; } }; } return balance; } /** * Behaviorally clamp the start and stop times. The parameters are clamped * to the range [0.0, duration]. If the duration is not * known, {@link Double#MAX_VALUE} is used instead. Furthermore, if the * separately clamped values satisfy * startTime > stopTime * then stopTime is clamped as * stopTime ≥ startTime. * * @param startValue the new start time. * @param stopValue the new stop time. * @return the clamped times in seconds as {actualStart, actualStop}. */ private double[] calculateStartStopTimes(Duration startValue, Duration stopValue) { // Derive start time in seconds. double newStart; if (startValue == null || startValue.lessThan(Duration.ZERO) || startValue.equals(Duration.UNKNOWN)) { newStart = 0.0; } else if (startValue.equals(Duration.INDEFINITE)) { newStart = Double.MAX_VALUE; } else { newStart = startValue.toMillis() / 1000.0; } // Derive stop time in seconds. double newStop; if (stopValue == null || stopValue.equals(Duration.UNKNOWN) || stopValue.equals(Duration.INDEFINITE)) { newStop = Double.MAX_VALUE; } else if (stopValue.lessThan(Duration.ZERO)) { newStop = 0.0; } else { newStop = stopValue.toMillis() / 1000.0; } // Derive the duration in seconds. Duration mediaDuration = media.getDuration(); double duration = mediaDuration == Duration.UNKNOWN ? Double.MAX_VALUE : mediaDuration.toMillis()/1000.0; // Clamp the start and stop times to [0,duration]. double actualStart = clamp(newStart, 0.0, duration); double actualStop = clamp(newStop, 0.0, duration); // Restrict actual stop time to [startTime,duration]. if (actualStart > actualStop) { actualStop = actualStart; } return new double[] {actualStart, actualStop}; } /** * Set the effective start and stop times on the underlying player, * clamping as needed. * * @param startValue the new start time. * @param stopValue the new stop time. */ private void setStartStopTimes(Duration startValue, boolean isStartValueSet, Duration stopValue, boolean isStopValueSet) { if (jfxPlayer.getDuration() == Double.POSITIVE_INFINITY) { return; } // Clamp the start and stop times to values in seconds. double[] startStop = calculateStartStopTimes(startValue, stopValue); // Set the start and stop times on the underlying player. if (isStartValueSet) { jfxPlayer.setStartTime(startStop[0]); if (getStatus() == Status.READY || getStatus() == Status.PAUSED) { Platform.runLater(() -> { setCurrentTime(getStartTime()); }); } } if (isStopValueSet) { jfxPlayer.setStopTime(startStop[1]); } } /** * The time offset where media should start playing, or restart from when * repeating. When playback is stopped, the current time is reset to this * value. If this value is positive, then the first time the media is * played there might be a delay before playing begins unless the play * position can be set to an arbitrary time within the media. This could * occur for example for a video which does not contain a lookup table * of the offsets of intra-frames in the video stream. In such a case the * video frames would need to be skipped over until the position of the * first intra-frame before the start time was reached. The default value is * Duration.ZERO. * *

Constraints: 0 ≤ startTime < {@link #stopTimeProperty stopTime} */ private ObjectProperty startTime; /** * Sets the start time. Its effect will be clamped to * the range [{@link Duration#ZERO}, {@link #stopTimeProperty stopTime}). * Invoking this method will have no effect if media duration is {@link Duration#INDEFINITE}. * * @param value the start time */ public final void setStartTime(Duration value) { startTimeProperty().set(value); } /** * Retrieves the start time. The default value is Duration.ZERO. * @return the start time */ public final Duration getStartTime() { return startTime == null ? Duration.ZERO : startTime.get(); } public ObjectProperty startTimeProperty() { if (startTime == null) { startTime = new ObjectPropertyBase() { @Override protected void invalidated() { synchronized (disposeLock) { if (getStatus() != Status.DISPOSED) { if (playerReady) { setStartStopTimes(startTime.get(), true, getStopTime(), false); } else { startTimeChangeRequested = true; } calculateCycleDuration(); } } } @Override public Object getBean() { return MediaPlayer.this; } @Override public String getName() { return "startTime"; } }; } return startTime; } /** * The time offset where media should stop playing or restart when repeating. * The default value is {@link #getMedia()}.getDuration(). * *

Constraints: {@link #startTimeProperty startTime} < stopTime ≤ {@link Media#durationProperty Media.duration} */ private ObjectProperty stopTime; /** * Sets the stop time. Its effect will be clamped to * the range ({@link #startTimeProperty startTime}, {@link Media#durationProperty Media.duration}]. * Invoking this method will have no effect if media duration is {@link Duration#INDEFINITE}. * * @param value the stop time */ public final void setStopTime (Duration value) { stopTimeProperty().set(value); } /** * Retrieves the stop time. The default value is * {@link #getMedia()}.getDuration(). Note that * {@link Media#durationProperty Media.duration} may have the value * Duration.UNKNOWN if media initialization is not complete. * @return the stop time */ public final Duration getStopTime() { return stopTime == null ? media.getDuration() : stopTime.get(); } public ObjectProperty stopTimeProperty() { if (stopTime == null) { stopTime = new ObjectPropertyBase() { @Override protected void invalidated() { synchronized (disposeLock) { if (getStatus() != Status.DISPOSED) { if (playerReady) { setStartStopTimes(getStartTime(), false, stopTime.get(), true); } else { stopTimeChangeRequested = true; } calculateCycleDuration(); } } } @Override public Object getBean() { return MediaPlayer.this; } @Override public String getName() { return "stopTime"; } }; } return stopTime; } /** * The amount of time between the {@link #startTimeProperty startTime} and * {@link #stopTimeProperty stopTime} * of this player. For the total duration of the Media use the * {@link Media#durationProperty Media.duration} property. */ private ReadOnlyObjectWrapper cycleDuration; private void setCycleDuration(Duration value) { cycleDurationPropertyImpl().set(value); } /** * Retrieves the cycle duration in seconds. * @return the cycle duration */ public final Duration getCycleDuration() { return cycleDuration == null ? Duration.UNKNOWN : cycleDuration.get(); } public ReadOnlyObjectProperty cycleDurationProperty() { return cycleDurationPropertyImpl().getReadOnlyProperty(); } private ReadOnlyObjectWrapper cycleDurationPropertyImpl() { if (cycleDuration == null) { cycleDuration = new ReadOnlyObjectWrapper(this, "cycleDuration"); } return cycleDuration; } // recalculate cycleDuration based on startTime, stopTime and Media.duration // if any are UNKNOWN then this is UNKNOWN private void calculateCycleDuration() { Duration endTime; Duration mediaDuration = media.getDuration(); if (!getStopTime().isUnknown()) { endTime = getStopTime(); } else { endTime = mediaDuration; } if (endTime.greaterThan(mediaDuration)) { endTime = mediaDuration; } // filter bad values if (endTime.isUnknown() || getStartTime().isUnknown() || getStartTime().isIndefinite()) { if (!getCycleDuration().isUnknown()) setCycleDuration(Duration.UNKNOWN); } setCycleDuration(endTime.subtract(getStartTime())); calculateTotalDuration(); // since it's dependent on cycle duration } /** * The total amount of play time if allowed to play until finished. If * cycleCount is set to INDEFINITE then this will * also be INDEFINITE. If the Media duration is UNKNOWN, then this will * likewise be UNKNOWN. Otherwise, total duration will be the product of * cycleDuration and cycleCount. */ private ReadOnlyObjectWrapper totalDuration; private void setTotalDuration(Duration value) { totalDurationPropertyImpl().set(value); } /** * Retrieves the total playback duration including all cycles (repetitions). * @return the total playback duration */ public final Duration getTotalDuration() { return totalDuration == null ? Duration.UNKNOWN : totalDuration.get(); } public ReadOnlyObjectProperty totalDurationProperty() { return totalDurationPropertyImpl().getReadOnlyProperty(); } private ReadOnlyObjectWrapper totalDurationPropertyImpl() { if (totalDuration == null) { totalDuration = new ReadOnlyObjectWrapper(this, "totalDuration"); } return totalDuration; } private void calculateTotalDuration() { if (getCycleCount() == INDEFINITE) { setTotalDuration(Duration.INDEFINITE); } else if (getCycleDuration().isUnknown()) { setTotalDuration(Duration.UNKNOWN); } else { setTotalDuration(getCycleDuration().multiply((double)getCycleCount())); } } /** * The current media playback time. This property is read-only: use * {@link #seek(javafx.util.Duration)} to change playback to a different * stream position. * */ private ReadOnlyObjectWrapper currentTime; private void setCurrentTime(Duration value) { currentTimePropertyImpl().set(value); } /** * Retrieves the current media time. * @return the current media time */ public final Duration getCurrentTime() { synchronized (disposeLock) { if (getStatus() == Status.DISPOSED) { return Duration.ZERO; } if (getStatus() == Status.STOPPED) { return Duration.millis(getStartTime().toMillis()); } if (isEOS) { Duration duration = media.getDuration(); Duration stopTime = getStopTime(); if (stopTime != Duration.UNKNOWN && duration != Duration.UNKNOWN) { if (stopTime.greaterThan(duration)) { return Duration.millis(duration.toMillis()); } else { return Duration.millis(stopTime.toMillis()); } } } // Query the property value. This is necessary even if the returned // value is not used below as setting the property value in // setCurrentTime() as is done in updateTime() which is called by the // MediaTimer will not trigger invalidation events unless the previous // value of the property has been retrieved via get(). Duration theCurrentTime = currentTimeProperty().get(); // Query the implementation layer for a more accurate value of the time. // The MediaTimer only updates the property at a fixed interval and // the present method might be called too far away from a timer update. if (playerReady) { double timeSeconds = jfxPlayer.getPresentationTime(); if (timeSeconds >= 0.0) { theCurrentTime = Duration.seconds(timeSeconds); // We do not set the currentTime property value here as doing so // could result in an infinite loop if getCurrentTime() is for // example being invoked by an Invaludation listener of // currentTime, for example in response to MediaTimer calling // updateTime(). } } return theCurrentTime; } } public ReadOnlyObjectProperty currentTimeProperty() { return currentTimePropertyImpl().getReadOnlyProperty(); } private ReadOnlyObjectWrapper currentTimePropertyImpl() { if (currentTime == null) { currentTime = new ReadOnlyObjectWrapper(this, "currentTime"); currentTime.setValue(Duration.ZERO); updateTime(); } return currentTime; } /** * Seeks the player to a new playback time. Invoking this method will have * no effect while the player status is {@link Status#STOPPED} or media duration is {@link Duration#INDEFINITE}. * *

The behavior of seek() is constrained as follows where * start time and stop time indicate the effective lower and * upper bounds, respectively, of media playback: * * * * * * * * *
seekTimeseek position
nullno change
{@link Duration#UNKNOWN}no change
{@link Duration#INDEFINITE}stop time
seekTime < start timestart time
seekTime > stop timestop time
start time ≤ seekTime ≤ stop timeseekTime
*

* * @param seekTime the requested playback time */ public void seek(Duration seekTime) { synchronized (disposeLock) { if (getStatus() == Status.DISPOSED) { return; } // Seek only if the player is ready and the seekTime is valid. if (playerReady && seekTime != null && !seekTime.isUnknown()) { if (jfxPlayer.getDuration() == Double.POSITIVE_INFINITY) { return; } // Determine the seek position in seconds. double seekSeconds; // Duration.INDEFINITE means seek to end. if (seekTime.isIndefinite()) { // Determine the effective duration. Duration duration = media.getDuration(); if (duration == null || duration.isUnknown() || duration.isIndefinite()) { duration = Duration.millis(Double.MAX_VALUE); } // Convert the duration to seconds. seekSeconds = duration.toMillis() / 1000.0; } else { // Convert the parameter to seconds. seekSeconds = seekTime.toMillis() / 1000.0; // Clamp the seconds if needed. double[] startStop = calculateStartStopTimes(getStartTime(), getStopTime()); if (seekSeconds < startStop[0]) { seekSeconds = startStop[0]; } else if (seekSeconds > startStop[1]) { seekSeconds = startStop[1]; } } if (!isUpdateTimeEnabled) { // Change time update flag to true amd current rate to rate // if status is PLAYING and current time is in range. Status playerStatus = getStatus(); if ((playerStatus == MediaPlayer.Status.PLAYING || playerStatus == MediaPlayer.Status.PAUSED) && getStartTime().toSeconds() <= seekSeconds && seekSeconds <= getStopTime().toSeconds()) { isEOS = false; isUpdateTimeEnabled = true; setCurrentRate(getRate()); } } // Perform the seek. jfxPlayer.seek(seekSeconds); } } } /** * The current state of the MediaPlayer. */ private ReadOnlyObjectWrapper status; private void setStatus(Status value) { statusPropertyImpl().set(value); } /** * Retrieves the current player status. * @return the playback status */ public final Status getStatus() { return status == null ? Status.UNKNOWN : status.get(); } public ReadOnlyObjectProperty statusProperty() { return statusPropertyImpl().getReadOnlyProperty(); } private ReadOnlyObjectWrapper statusPropertyImpl() { if (status == null) { status = new ReadOnlyObjectWrapper() { @Override protected void invalidated() { // use status changes to update currentRate if (get() == Status.PLAYING) { setCurrentRate(getRate()); } else { setCurrentRate(0.0); } // Signal status updates if (get() == Status.READY) { if (getOnReady() != null) { Platform.runLater(getOnReady()); } } else if (get() == Status.PLAYING) { if (getOnPlaying() != null) { Platform.runLater(getOnPlaying()); } } else if (get() == Status.PAUSED) { if (getOnPaused() != null) { Platform.runLater(getOnPaused()); } } else if (get() == Status.STOPPED) { if (getOnStopped() != null) { Platform.runLater(getOnStopped()); } } else if (get() == Status.STALLED) { if (getOnStalled() != null) { Platform.runLater(getOnStalled()); } } } @Override public Object getBean() { return MediaPlayer.this; } @Override public String getName() { return "status"; } }; } return status; } /** * The current buffer position indicating how much media can be played * without stalling the MediaPlayer. This is applicable to * buffered streams such as those reading from network connections as * opposed for example to local files. * *

Seeking to a position beyond bufferProgressTime might * cause a slight pause in playback until an amount of data sufficient to * permit playback resumption has been buffered. */ private ReadOnlyObjectWrapper bufferProgressTime; private void setBufferProgressTime(Duration value) { bufferProgressTimePropertyImpl().set(value); } /** * Retrieves the {@link #bufferProgressTimeProperty bufferProgressTime} value. * @return the buffer progress time */ public final Duration getBufferProgressTime() { return bufferProgressTime == null ? null : bufferProgressTime.get(); } public ReadOnlyObjectProperty bufferProgressTimeProperty() { return bufferProgressTimePropertyImpl().getReadOnlyProperty(); } private ReadOnlyObjectWrapper bufferProgressTimePropertyImpl() { if (bufferProgressTime == null) { bufferProgressTime = new ReadOnlyObjectWrapper(this, "bufferProgressTime"); } return bufferProgressTime; } /** * The number of times the media will be played. By default, * cycleCount is set to 1 * meaning the media will only be played once. Setting cycleCount * to a value greater than 1 will cause the media to play the given number * of times or until stopped. If set to {@link #INDEFINITE INDEFINITE}, * playback will repeat until stop() or pause() is called. * *

constraints: cycleCount ≥ 1 */ private IntegerProperty cycleCount; /** * Sets the cycle count. Its effect will be constrained to * [1,{@link Integer#MAX_VALUE}]. * Invoking this method will have no effect if media duration is {@link Duration#INDEFINITE}. * @param value the cycle count */ public final void setCycleCount(int value) { cycleCountProperty().set(value); } /** * Retrieves the cycle count. * @return the cycle count. */ public final int getCycleCount() { return cycleCount == null ? 1 : cycleCount.get(); } public IntegerProperty cycleCountProperty() { if (cycleCount == null) { cycleCount = new IntegerPropertyBase(1) { @Override public Object getBean() { return MediaPlayer.this; } @Override public String getName() { return "cycleCount"; } }; } return cycleCount; } /** * The number of completed playback cycles. On the first pass, * the value should be 0. On the second pass, the value should be 1 and * so on. It is incremented at the end of each cycle just prior to seeking * back to {@link #startTimeProperty startTime}, i.e., when {@link #stopTimeProperty stopTime} or the * end of media has been reached. */ private ReadOnlyIntegerWrapper currentCount; private void setCurrentCount(int value) { currentCountPropertyImpl().set(value); } /** * Retrieves the index of the current cycle. * @return the current cycle index */ public final int getCurrentCount() { return currentCount == null ? 0 : currentCount.get(); } public ReadOnlyIntegerProperty currentCountProperty() { return currentCountPropertyImpl().getReadOnlyProperty(); } private ReadOnlyIntegerWrapper currentCountPropertyImpl() { if (currentCount == null) { currentCount = new ReadOnlyIntegerWrapper(this, "currentCount"); } return currentCount; } /** * Whether the player audio is muted. A value of true indicates * that audio is not being produced. The value of this property has * no effect on {@link #volumeProperty volume}, i.e., if the audio is muted and then * un-muted, audio playback will resume at the same audible level provided * of course that the volume property has not been modified * meanwhile. The default value is false. * @see #volume */ private BooleanProperty mute; /** * Sets the value of {@link #muteProperty}. * @param value the mute setting */ public final void setMute (boolean value) { muteProperty().set(value); } /** * Retrieves the {@link #muteProperty} value. * @return the mute setting */ public final boolean isMute() { return mute == null ? false : mute.get(); } public BooleanProperty muteProperty() { if (mute == null) { mute = new BooleanPropertyBase() { @Override protected void invalidated() { synchronized (disposeLock) { if (getStatus() != Status.DISPOSED) { if (playerReady) { jfxPlayer.setMute(get()); } else { muteChangeRequested = true; } } } } @Override public Object getBean() { return MediaPlayer.this; } @Override public String getName() { return "mute"; } }; } return mute; } /** * Event handler invoked when the player currentTime reaches a * media marker. */ private ObjectProperty> onMarker; /** * Sets the marker event handler. * @param onMarker the marker event handler. */ public final void setOnMarker(EventHandler onMarker) { onMarkerProperty().set(onMarker); } /** * Retrieves the marker event handler. * @return the marker event handler. */ public final EventHandler getOnMarker() { return onMarker == null ? null : onMarker.get(); } public ObjectProperty> onMarkerProperty() { if (onMarker == null) { onMarker = new SimpleObjectProperty>(this, "onMarker"); } return onMarker; } void addView(MediaView view) { WeakReference vref = new WeakReference(view); synchronized (viewRefs) { viewRefs.add(vref); } } void removeView(MediaView view) { synchronized (viewRefs) { for (WeakReference vref : viewRefs) { MediaView v = vref.get(); if (v != null && v.equals(view)) { viewRefs.remove(vref); } } } } // This function sets the player's error property on the UI thread. void handleError(final MediaException error) { Platform.runLater(() -> { setError(error); // Propogate errors that related to media to media object if (error.getType() == MediaException.Type.MEDIA_CORRUPTED || error.getType() == MediaException.Type.MEDIA_UNSUPPORTED || error.getType() == MediaException.Type.MEDIA_INACCESSIBLE || error.getType() == MediaException.Type.MEDIA_UNAVAILABLE) { media._setError(error.getType(), error.getMessage()); } }); } void createMediaTimer() { synchronized (MediaTimerTask.timerLock) { if (mediaTimerTask == null) { mediaTimerTask = new MediaTimerTask(this); mediaTimerTask.start(); } isUpdateTimeEnabled = true; } } void destroyMediaTimer() { synchronized (MediaTimerTask.timerLock) { if (mediaTimerTask != null) { isUpdateTimeEnabled = false; mediaTimerTask.stop(); mediaTimerTask = null; } } } // Called periodically to update the currentTime void updateTime() { if (playerReady && isUpdateTimeEnabled && jfxPlayer != null) { double timeSeconds = jfxPlayer.getPresentationTime(); if (timeSeconds >= 0.0) { double newTimeMs = timeSeconds*1000.0; if (Double.compare(newTimeMs, prevTimeMs) != 0) { setCurrentTime(Duration.millis(newTimeMs)); prevTimeMs = newTimeMs; } } } } void loopPlayback() { seek (getStartTime()); } // handleRequestedChanges() is called to update jfxPlayer's properties once // MediaPlayer gets the onReady event from jfxPlayer. Before onReady, calls to // update MediaPlayer's properties to not correspond to calls to update jfxPlayer's // properties. Once we get onReady(), we must then go and update all of jfxPlayer's // proprties. void handleRequestedChanges() { if (rateChangeRequested) { if (jfxPlayer.getDuration() != Double.POSITIVE_INFINITY) { jfxPlayer.setRate((float)clamp(getRate(), RATE_MIN, RATE_MAX)); } rateChangeRequested = false; } if (volumeChangeRequested) { jfxPlayer.setVolume((float)clamp(getVolume(), 0.0, 1.0)); volumeChangeRequested = false; } if (balanceChangeRequested) { jfxPlayer.setBalance((float)clamp(getBalance(), -1.0, 1.0)); balanceChangeRequested = false; } if (startTimeChangeRequested || stopTimeChangeRequested) { setStartStopTimes(getStartTime(), startTimeChangeRequested, getStopTime(), stopTimeChangeRequested); startTimeChangeRequested = stopTimeChangeRequested = false; } if (muteChangeRequested) { jfxPlayer.setMute(isMute()); muteChangeRequested = false; } if (audioSpectrumNumBandsChangeRequested) { jfxPlayer.getAudioSpectrum().setBandCount(clamp(getAudioSpectrumNumBands(), AUDIOSPECTRUM_NUMBANDS_MIN, Integer.MAX_VALUE)); audioSpectrumNumBandsChangeRequested = false; } if (audioSpectrumIntervalChangeRequested) { jfxPlayer.getAudioSpectrum().setInterval(clamp(getAudioSpectrumInterval(), AUDIOSPECTRUM_INTERVAL_MIN, Double.MAX_VALUE)); audioSpectrumIntervalChangeRequested = false; } if (audioSpectrumThresholdChangeRequested) { jfxPlayer.getAudioSpectrum().setSensitivityThreshold(clamp(getAudioSpectrumThreshold(), Integer.MIN_VALUE, AUDIOSPECTRUM_THRESHOLD_MAX)); audioSpectrumThresholdChangeRequested = false; } if (audioSpectrumEnabledChangeRequested) { boolean enabled = (getAudioSpectrumListener() != null); jfxPlayer.getAudioSpectrum().setEnabled(enabled); audioSpectrumEnabledChangeRequested = false; } if (playRequested) { jfxPlayer.play(); playRequested = false; } } //************************************************************************************************* //********** Player event-handling //************************************************************************************************* void preReady() { // Notify MediaView that we ready synchronized (viewRefs) { for (WeakReference vref : viewRefs) { MediaView v = vref.get(); if (v != null) { v._mediaPlayerOnReady(); } } } // Update AudioEqaualizer if needed if (audioEqualizer != null) { audioEqualizer.setAudioEqualizer(jfxPlayer.getEqualizer()); } // Update duration double durationSeconds = jfxPlayer.getDuration(); Duration duration; if (durationSeconds >= 0.0 && !Double.isNaN(durationSeconds)) { duration = Duration.millis(durationSeconds * 1000.0); } else { duration = Duration.UNKNOWN; } playerReady = true; media.setDuration(duration); media._updateMedia(jfxPlayer.getMedia()); //***** Sync up the player with the desired properties if they were called // before onReady() handleRequestedChanges(); // update cycle/total durations calculateCycleDuration(); // Set BufferProgressTime if (lastBufferEvent != null && duration.toMillis() > 0.0) { double position = lastBufferEvent.getBufferPosition(); double stop = lastBufferEvent.getBufferStop(); final double bufferedTime = position / stop * duration.toMillis(); lastBufferEvent = null; setBufferProgressTime(Duration.millis(bufferedTime)); } setStatus(Status.READY); } /** * Event handler invoked when the player currentTime reaches * stopTime. */ private ObjectProperty onEndOfMedia; /** * Sets the end of media event handler. * @param value the event handler or null. */ public final void setOnEndOfMedia(Runnable value) { onEndOfMediaProperty().set(value); } /** * Retrieves the end of media event handler. * @return the event handler or null. */ public final Runnable getOnEndOfMedia() { return onEndOfMedia == null ? null : onEndOfMedia.get(); } public ObjectProperty onEndOfMediaProperty() { if (onEndOfMedia == null) { onEndOfMedia = new SimpleObjectProperty(this, "onEndOfMedia"); } return onEndOfMedia; } /** * Event handler invoked when the status changes to * READY. */ private ObjectProperty onReady; // Player is ready and media has prerolled /** * Sets the {@link Status#READY} event handler. * @param value the event handler or null. */ public final void setOnReady(Runnable value) { onReadyProperty().set(value); } /** * Retrieves the {@link Status#READY} event handler. * @return the event handler or null. */ public final Runnable getOnReady() { return onReady == null ? null : onReady.get(); } public ObjectProperty onReadyProperty() { if (onReady == null) { onReady = new SimpleObjectProperty(this, "onReady"); } return onReady; } /** * Event handler invoked when the status changes to * PLAYING. */ private ObjectProperty onPlaying; // Media has reached its end. /** * Sets the {@link Status#PLAYING} event handler. * @param value the event handler or null. */ public final void setOnPlaying(Runnable value) { onPlayingProperty().set(value); } /** * Retrieves the {@link Status#PLAYING} event handler. * @return the event handler or null. */ public final Runnable getOnPlaying() { return onPlaying == null ? null : onPlaying.get(); } public ObjectProperty onPlayingProperty() { if (onPlaying == null) { onPlaying = new SimpleObjectProperty(this, "onPlaying"); } return onPlaying; } /** * Event handler invoked when the status changes to PAUSED. */ private ObjectProperty onPaused; // Media has reached its end. /** * Sets the {@link Status#PAUSED} event handler. * @param value the event handler or null. */ public final void setOnPaused(Runnable value) { onPausedProperty().set(value); } /** * Retrieves the {@link Status#PAUSED} event handler. * @return the event handler or null. */ public final Runnable getOnPaused() { return onPaused == null ? null : onPaused.get(); } public ObjectProperty onPausedProperty() { if (onPaused == null) { onPaused = new SimpleObjectProperty(this, "onPaused"); } return onPaused; } /** * Event handler invoked when the status changes to * STOPPED. */ private ObjectProperty onStopped; // Media has reached its end. /** * Sets the {@link Status#STOPPED} event handler. * @param value the event handler or null. */ public final void setOnStopped(Runnable value) { onStoppedProperty().set(value); } /** * Retrieves the {@link Status#STOPPED} event handler. * @return the event handler or null. */ public final Runnable getOnStopped() { return onStopped == null ? null : onStopped.get(); } public ObjectProperty onStoppedProperty() { if (onStopped == null) { onStopped = new SimpleObjectProperty(this, "onStopped"); } return onStopped; } /** * Event handler invoked when the status changes to HALTED. */ private ObjectProperty onHalted; // Media caught an irrecoverable error. /** * Sets the {@link Status#HALTED} event handler. * @param value the event handler or null. */ public final void setOnHalted(Runnable value) { onHaltedProperty().set(value); } /** * Retrieves the {@link Status#HALTED} event handler. * @return the event handler or null. */ public final Runnable getOnHalted() { return onHalted == null ? null : onHalted.get(); } public ObjectProperty onHaltedProperty() { if (onHalted == null) { onHalted = new SimpleObjectProperty(this, "onHalted"); } return onHalted; } /** * Event handler invoked when the player currentTime reaches * stopTime and will be repeating. This callback is made * prior to seeking back to startTime. * * @see cycleCount */ private ObjectProperty onRepeat; /** * Sets the repeat event handler. * @param value the event handler or null. */ public final void setOnRepeat(Runnable value) { onRepeatProperty().set(value); } /** * Retrieves the repeat event handler. * @return the event handler or null. */ public final Runnable getOnRepeat() { return onRepeat == null ? null : onRepeat.get(); } public ObjectProperty onRepeatProperty() { if (onRepeat == null) { onRepeat = new SimpleObjectProperty(this, "onRepeat"); } return onRepeat; } /** * Event handler invoked when the status changes to * STALLED. */ private ObjectProperty onStalled; /** * Sets the {@link Status#STALLED} event handler. * @param value the event handler or null. */ public final void setOnStalled(Runnable value) { onStalledProperty().set(value); } /** * Retrieves the {@link Status#STALLED} event handler. * @return the event handler or null. */ public final Runnable getOnStalled() { return onStalled == null ? null : onStalled.get(); } public ObjectProperty onStalledProperty() { if (onStalled == null) { onStalled = new SimpleObjectProperty(this, "onStalled"); } return onStalled; } /**************************************************************************** * AudioSpectrum API ***************************************************************************/ /** * The number of bands in the audio spectrum. The default value is 128; minimum * is 2. The frequency range of the audio signal will be divided into the * specified number of frequency bins. For example, a typical digital music * signal has a frequency range of [0.0, 22050] Hz. If the * number of spectral bands were in this case set to 10, the width of each * frequency bin in the spectrum would be 2205 Hz with the * lower bound of the lowest frequency bin equal to 0.0. */ private IntegerProperty audioSpectrumNumBands; /** * Sets the number of bands in the audio spectrum. * @param value the number of spectral bands; valuemust be ≥ 2 */ public final void setAudioSpectrumNumBands(int value) { audioSpectrumNumBandsProperty().setValue(value); } /** * Retrieves the number of bands in the audio spectrum. * @return the number of spectral bands. */ public final int getAudioSpectrumNumBands() { return audioSpectrumNumBandsProperty().getValue(); } public IntegerProperty audioSpectrumNumBandsProperty() { if (audioSpectrumNumBands == null) { audioSpectrumNumBands = new IntegerPropertyBase(DEFAULT_SPECTRUM_BAND_COUNT) { @Override protected void invalidated() { synchronized (disposeLock) { if (getStatus() != Status.DISPOSED) { if (playerReady) { jfxPlayer.getAudioSpectrum().setBandCount(clamp(audioSpectrumNumBands.get(), AUDIOSPECTRUM_NUMBANDS_MIN, Integer.MAX_VALUE)); } else { audioSpectrumNumBandsChangeRequested = true; } } } } @Override public Object getBean() { return MediaPlayer.this; } @Override public String getName() { return "audioSpectrumNumBands"; } }; } return audioSpectrumNumBands; } /** * The interval between spectrum updates in seconds. The default is * 0.1 seconds. */ private DoubleProperty audioSpectrumInterval; /** * Sets the value of the audio spectrum notification interval in seconds. * @param value a positive value specifying the spectral update interval */ public final void setAudioSpectrumInterval(double value) { audioSpectrumIntervalProperty().set(value); } /** * Retrieves the value of the audio spectrum notification interval in seconds. * @return the spectral update interval */ public final double getAudioSpectrumInterval() { return audioSpectrumIntervalProperty().get(); } public DoubleProperty audioSpectrumIntervalProperty() { if (audioSpectrumInterval == null) { audioSpectrumInterval = new DoublePropertyBase(DEFAULT_SPECTRUM_INTERVAL) { @Override protected void invalidated() { synchronized (disposeLock) { if (getStatus() != Status.DISPOSED) { if (playerReady) { jfxPlayer.getAudioSpectrum().setInterval(clamp(audioSpectrumInterval.get(), AUDIOSPECTRUM_INTERVAL_MIN, Double.MAX_VALUE)); } else { audioSpectrumIntervalChangeRequested = true; } } } } @Override public Object getBean() { return MediaPlayer.this; } @Override public String getName() { return "audioSpectrumInterval"; } }; } return audioSpectrumInterval; } /** * The sensitivity threshold in decibels; must be non-positive. Values below * this threshold with respect to the peak frequency in the given spectral * band will be set to the value of the threshold. The default value is * -60 dB. */ private IntegerProperty audioSpectrumThreshold; /** * Sets the audio spectrum threshold in decibels. * @param value the spectral threshold in dB; must be ≤ 0. */ public final void setAudioSpectrumThreshold(int value) { audioSpectrumThresholdProperty().set(value); } /** * Retrieves the audio spectrum threshold in decibels. * @return the spectral threshold in dB */ public final int getAudioSpectrumThreshold() { return audioSpectrumThresholdProperty().get(); } public IntegerProperty audioSpectrumThresholdProperty() { if (audioSpectrumThreshold == null) { audioSpectrumThreshold = new IntegerPropertyBase(DEFAULT_SPECTRUM_THRESHOLD) { @Override protected void invalidated() { synchronized (disposeLock) { if (getStatus() != Status.DISPOSED) { if (playerReady) { jfxPlayer.getAudioSpectrum().setSensitivityThreshold(clamp(audioSpectrumThreshold.get(), Integer.MIN_VALUE, AUDIOSPECTRUM_THRESHOLD_MAX)); } else { audioSpectrumThresholdChangeRequested = true; } } } } @Override public Object getBean() { return MediaPlayer.this; } @Override public String getName() { return "audioSpectrumThreshold"; } }; } return audioSpectrumThreshold; } /** * A listener for audio spectrum updates. When the listener is registered, * audio spectrum computation is enabled; upon removing the listener, * computation is disabled. Only a single listener may be registered, so if * multiple observers are required, events must be forwarded. * *

An AudioSpectrumListener may be useful for example to * plot the frequency spectrum of the audio being played or to generate * waveforms for a music visualizer. */ private ObjectProperty audioSpectrumListener; /** * Sets the listener of the audio spectrum. * @param listener the spectral listener or null. */ public final void setAudioSpectrumListener(AudioSpectrumListener listener) { audioSpectrumListenerProperty().set(listener); } /** * Retrieves the listener of the audio spectrum. * @return the spectral listener or null */ public final AudioSpectrumListener getAudioSpectrumListener() { return audioSpectrumListenerProperty().get(); } public ObjectProperty audioSpectrumListenerProperty() { if (audioSpectrumListener == null) { audioSpectrumListener = new ObjectPropertyBase() { @Override protected void invalidated() { synchronized (disposeLock) { if (getStatus() != Status.DISPOSED) { if (playerReady) { boolean enabled = (audioSpectrumListener.get() != null); jfxPlayer.getAudioSpectrum().setEnabled(enabled); } else { audioSpectrumEnabledChangeRequested = true; } } } } @Override public Object getBean() { return MediaPlayer.this; } @Override public String getName() { return "audioSpectrumListener"; } }; } return audioSpectrumListener; } /** * Free all resources associated with player. Player SHOULD NOT be used after this function is called. * Player will transition to {@link Status} after this method is done. This method can be called * anytime and regarding current player status. * @since JavaFX 8.0 */ public synchronized void dispose() { synchronized (disposeLock) { setStatus(Status.DISPOSED); destroyMediaTimer(); if (audioEqualizer != null) { audioEqualizer.setAudioEqualizer(null); audioEqualizer = null; } if (jfxPlayer != null) { jfxPlayer.dispose(); synchronized (renderLock) { if (rendererListener != null) { Toolkit.getToolkit().removeStageTkPulseListener(rendererListener); rendererListener = null; } } jfxPlayer = null; } } } /**************************************************************************** * Listeners section *************************************************************************** * Listener of modifications to the marker map in the public Media API. * Changes to this map are propagated to the implementation layer. */ private class MarkerMapChangeListener implements MapChangeListener { @Override public void onChanged(Change change) { synchronized (disposeLock) { if (getStatus() != Status.DISPOSED) { String key = change.getKey(); // Reject null-named markers. if (key == null) { return; } com.sun.media.jfxmedia.Media jfxMedia = jfxPlayer.getMedia(); if (change.wasAdded()) { if (change.wasRemoved()) { // The remove and add marker calls eventually go to native code // so we can't depend on the Java Map behavior or replacing a // key-value pair when the key is already in the Map. Instead we // explicitly remove the old entry and add the new one. jfxMedia.removeMarker(key); } Duration value = change.getValueAdded(); // Reject null- or negative-valued marker times. if (value != null && value.greaterThanOrEqualTo(Duration.ZERO)) { jfxMedia.addMarker(key, change.getValueAdded().toMillis() / 1000.0); } } else if (change.wasRemoved()) { jfxMedia.removeMarker(key); } } } } } /** * Listener of marker events emitted by the implementation layer. The * CURRENT_MARKER property is updated to the most recently received event. */ private class _MarkerListener implements MarkerListener { @Override public void onMarker(final MarkerEvent evt) { Platform.runLater(() -> { Duration markerTime = Duration.millis(evt.getPresentationTime() * 1000.0); if (getOnMarker() != null) { getOnMarker().handle(new MediaMarkerEvent(new Pair(evt.getMarkerName(), markerTime))); } }); } } private class _PlayerStateListener implements PlayerStateListener { @Override public void onReady(PlayerStateEvent evt) { //System.out.println("** MediaPlayerFX received onReady!"); Platform.runLater(() -> { synchronized (disposeLock) { if (getStatus() == Status.DISPOSED) { return; } preReady(); } }); } @Override public void onPlaying(PlayerStateEvent evt) { //System.err.println("** MediaPlayerFX received onPlaying!"); startTimeAtStop = null; Platform.runLater(() -> { createMediaTimer(); setStatus(Status.PLAYING); }); } @Override public void onPause(PlayerStateEvent evt) { //System.err.println("** MediaPlayerFX received onPause!"); Platform.runLater(() -> { // Disable updating currentTime. isUpdateTimeEnabled = false; setStatus(Status.PAUSED); }); if (startTimeAtStop != null && startTimeAtStop != getStartTime()) { startTimeAtStop = null; Platform.runLater(() -> { setCurrentTime(getStartTime()); }); } } @Override public void onStop(PlayerStateEvent evt) { //System.err.println("** MediaPlayerFX received onStop!"); Platform.runLater(() -> { // Destroy media time and update current time destroyMediaTimer(); startTimeAtStop = getStartTime(); setCurrentTime(getStartTime()); setStatus(Status.STOPPED); }); } @Override public void onStall(PlayerStateEvent evt) { //System.err.println("** MediaPlayerFX received onStall!"); Platform.runLater(() -> { // Disable updating currentTime. isUpdateTimeEnabled = false; setStatus(Status.STALLED); }); } void handleFinish() { //System.err.println("** MediaPlayerFX handleFinish"); // Increment number of times media has played. setCurrentCount(getCurrentCount() + 1); // Rewind and play from the beginning if the number // of repeats has yet to be reached. if ((getCurrentCount() < getCycleCount()) || (getCycleCount() == INDEFINITE)) { if (getOnEndOfMedia() != null) { Platform.runLater(getOnEndOfMedia()); } loopPlayback(); if (getOnRepeat() != null) { Platform.runLater(getOnRepeat()); } } else { // Player status remains PLAYING. // Disable updating currentTime. isUpdateTimeEnabled = false; // Set current rate to zero. setCurrentRate(0.0); // Set EOS flag isEOS = true; if (getOnEndOfMedia() != null) { Platform.runLater(getOnEndOfMedia()); } } } @Override public void onFinish(PlayerStateEvent evt) { //System.err.println("** MediaPlayerFX received onFinish!"); startTimeAtStop = null; Platform.runLater(() -> { handleFinish(); }); } @Override public void onHalt(final PlayerStateEvent evt) { Platform.runLater(() -> { setStatus(Status.HALTED); handleError(MediaException.haltException(evt.getMessage())); // Disable updating currentTime. isUpdateTimeEnabled = false; }); } } private class _PlayerTimeListener implements PlayerTimeListener { double theDuration; void handleDurationChanged() { media.setDuration(Duration.millis(theDuration * 1000.0)); } @Override public void onDurationChanged(final double duration) { //System.err.println("** MediaPlayerFX received onDurationChanged!"); Platform.runLater(() -> { theDuration = duration; handleDurationChanged(); }); } } private class _VideoTrackSizeListener implements VideoTrackSizeListener { int trackWidth; int trackHeight; @Override public void onSizeChanged(final int width, final int height) { Platform.runLater(() -> { if (media != null) { trackWidth = width; trackHeight = height; setSize(); } }); } void setSize() { media.setWidth(trackWidth); media.setHeight(trackHeight); synchronized (viewRefs) { for (WeakReference vref : viewRefs) { MediaView v = vref.get(); if (v != null) { v.notifyMediaSizeChange(); } } } } } private class _MediaErrorListener implements com.sun.media.jfxmedia.events.MediaErrorListener { @Override public void onError(Object source, int errorCode, String message) { MediaException error = MediaException.getMediaException(source, errorCode, message); handleError(error); } } private class _BufferListener implements BufferListener { double bufferedTime; // time in ms @Override public void onBufferProgress(BufferProgressEvent evt) { if (media != null) { if (evt.getDuration() > 0.0) { double position = evt.getBufferPosition(); //Must assign. I don't know how to convert integer to number otherwise. double stop = evt.getBufferStop(); bufferedTime = position/stop * evt.getDuration()*1000.0; lastBufferEvent = null; Platform.runLater(() -> { setBufferProgressTime(Duration.millis(bufferedTime)); }); } else { lastBufferEvent = evt; } } } } private class _SpectrumListener implements com.sun.media.jfxmedia.events.AudioSpectrumListener { private float[] magnitudes; private float[] phases; @Override public void onAudioSpectrumEvent(final AudioSpectrumEvent evt) { Platform.runLater(() -> { AudioSpectrumListener listener = getAudioSpectrumListener(); if (listener != null) { listener.spectrumDataUpdate(evt.getTimestamp(), evt.getDuration(), magnitudes = evt.getSource().getMagnitudes(magnitudes), phases = evt.getSource().getPhases(phases)); } }); } } private final Object renderLock = new Object(); private VideoDataBuffer currentRenderFrame; private VideoDataBuffer nextRenderFrame; // NGMediaView will call this to get the frame to render /** * WARNING: You must call releaseFrame() on the returned frame when you are * finished with it or a massive memory leak will occur. * * @return the current frame to be used for rendering, or null if not in a render cycle */ VideoDataBuffer getLatestFrame() { synchronized (renderLock) { if (null != currentRenderFrame) { currentRenderFrame.holdFrame(); } return currentRenderFrame; } } private class RendererListener implements com.sun.media.jfxmedia.events.VideoRendererListener, TKPulseListener { boolean updateMediaViews; @Override public void videoFrameUpdated(NewFrameEvent nfe) { VideoDataBuffer vdb = nfe.getFrameData(); if (null != vdb) { Duration frameTS = new Duration(vdb.getTimestamp() * 1000); Duration stopTime = getStopTime(); if (frameTS.greaterThanOrEqualTo(getStartTime()) && (stopTime.isUnknown() || frameTS.lessThanOrEqualTo(stopTime))) { updateMediaViews = true; synchronized (renderLock) { vdb.holdFrame(); // currentRenderFrame must not be touched, queue this one for later if (null != nextRenderFrame) { nextRenderFrame.releaseFrame(); } nextRenderFrame = vdb; } // make sure we get the next pulse so we can update our textures Toolkit.getToolkit().requestNextPulse(); } else { vdb.releaseFrame(); } } } @Override public void releaseVideoFrames() { synchronized (renderLock) { if (null != currentRenderFrame) { currentRenderFrame.releaseFrame(); currentRenderFrame = null; } if (null != nextRenderFrame) { nextRenderFrame.releaseFrame(); nextRenderFrame = null; } } } @Override public void pulse() { if (updateMediaViews) { updateMediaViews = false; /* swap in the next frame if there is one * this should be done exactly once per render cycle so that all * views display the same image. */ synchronized (renderLock) { if (null != nextRenderFrame) { if (null != currentRenderFrame) { currentRenderFrame.releaseFrame(); } currentRenderFrame = nextRenderFrame; nextRenderFrame = null; } } // tell all media views that their content needs to be redrawn synchronized (viewRefs) { Iterator> iter = viewRefs.iterator(); while (iter.hasNext()) { MediaView view = iter.next().get(); if (null != view) { view.notifyMediaFrameUpdated(); } else { iter.remove(); } } } } } } } class MediaPlayerShutdownHook implements Runnable { private final static List> playerRefs = new ArrayList>(); private static boolean isShutdown = false; static { Toolkit.getToolkit().addShutdownHook(new MediaPlayerShutdownHook()); } public static void addMediaPlayer(MediaPlayer player) { synchronized (playerRefs) { if (isShutdown) { com.sun.media.jfxmedia.MediaPlayer jfxPlayer = player.retrieveJfxPlayer(); if (jfxPlayer != null) { jfxPlayer.dispose(); } } else { for (ListIterator> it = playerRefs.listIterator(); it.hasNext();) { MediaPlayer l = it.next().get(); if (l == null) { it.remove(); } } playerRefs.add(new WeakReference(player)); } } } @Override public void run() { synchronized (playerRefs) { for (ListIterator> it = playerRefs.listIterator(); it.hasNext();) { MediaPlayer player = it.next().get(); if (player != null) { player.destroyMediaTimer(); com.sun.media.jfxmedia.MediaPlayer jfxPlayer = player.retrieveJfxPlayer(); if (jfxPlayer != null) { jfxPlayer.dispose(); } } else { it.remove(); } } isShutdown = true; } } } class MediaTimerTask extends TimerTask { private Timer mediaTimer = null; static final Object timerLock = new Object(); private WeakReference playerRef; MediaTimerTask(MediaPlayer player) { playerRef = new WeakReference(player); } void start() { if (mediaTimer == null) { mediaTimer = new Timer(true); mediaTimer.scheduleAtFixedRate(this, 0, 100 /* period ms*/); } } void stop() { if (mediaTimer != null) { mediaTimer.cancel(); mediaTimer = null; } } @Override public void run() { synchronized (timerLock) { final MediaPlayer player = playerRef.get(); if (player != null) { Platform.runLater(() -> { synchronized (timerLock) { player.updateTime(); } }); } else { cancel(); } } } }