javafx.scene.media.MediaPlayer Maven / Gradle / Ivy
/*
* Copyright (c) 2010, 2022, 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.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, then the event handler registered with the {@link #onEndOfMediaProperty onEndOfMedia}
* property is invoked regardless of whether the cycle is to be repeated or not.
* 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
* MediaView
s. 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:
*
*
* MediaPlayer Status Transition Table
*
* Current \ Next READY PAUSED
* PLAYING STALLED STOPPED
* DISPOSED
*
*
* UNKNOWN pre-roll dispose()
*
*
* READY autoplay; play() dispose()
*
*
* PAUSED play() stop() dispose()
*
*
* PLAYING pause() buffering data stop() dispose()
*
*
* STALLED pause() data buffered stop() dispose()
*
*
* STOPPED pause() play() dispose()
*
*
* HALTED dispose()
*
*
* 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 HALTED
and from
* DISPOSED
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. DISPOSED
is a terminal status
* entered after dispose() method is invoked and cannot be exited. HALTED
* status entered when a critical error occurs and may be transitioned into
* from any other status except DISPOSED
.
*
*
* The principal MediaPlayer
status values and transitions are
* depicted in the following 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(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:
*
*
* MediaPlayer Seek Table
* seekTime seek position
* null
no change
* {@link Duration#UNKNOWN} no change
* {@link Duration#INDEFINITE} stop time
* seekTime < start time start time
* seekTime > stop time stop time
* start time ≤ seekTime ≤ stop time seekTime
*
*
* @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; value
must 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#DISPOSED} after this method is done. This method can be
* called anytime regardless of 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 extends String, ? extends Duration> 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();
}
}
}
}