/*
* Copyright (c) 2010, 2015, 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
* 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.
MediaPlayer
status transitions are given in the
* following table:
* Current \ Next | READY | PAUSED | *PLAYING | STALLED | STOPPED | *
---|---|---|---|---|---|
UNKNOWN | pre-roll | * | |||
READY | autoplay; play() | * | |||
PAUSED | play() | stop() | *|||
PLAYING | pause() | buffering data | stop() | *||
STALLED | pause() | data buffered | stop() | *||
STOPPED | pause() | 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:
*
*
*
* 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.
*
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 MapChangeListenerAudioEqualizer
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 Constraints: Constraints: The behavior of Seeking to a position beyond constraints: An 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();
ObservableMapMediaException
if an error occurs.
*/
private ReadOnlyObjectWrappernull
* if there is no error.
* @return a MediaException
or null
.
*/
public final MediaException getError() {
return error == null ? null : error.get();
}
public ReadOnlyObjectPropertynull
.
*/
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 ObjectPropertyMedia
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
.
*
* 0 ≤ startTime < {@link #stopTimeProperty stopTime}
*/
private ObjectProperty[{@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{@link #getMedia()}.getDuration()
.
*
* {@link #startTimeProperty startTime} < stopTime ≤ {@link Media#durationProperty Media.duration}
*/
private ObjectProperty({@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 ObjectPropertycycleCount
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 ReadOnlyObjectWrapperseek()
is constrained as follows where
* start time and stop time indicate the effective lower and
* upper bounds, respectively, of media playback:
*
*
*
* 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 MediaPlayer
. This is applicable to
* buffered streams such as those reading from network connections as
* opposed for example to local files.
*
* bufferProgressTime
might
* cause a slight pause in playback until an amount of data sufficient to
* permit playback resumption has been buffered.
*/
private ReadOnlyObjectWrappercycleCount
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.
*
* 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 ObjectPropertycurrentTime
reaches
* stopTime
.
*/
private ObjectPropertynull
.
*/
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 ObjectPropertyREADY
.
*/
private ObjectPropertynull
.
*/
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 ObjectPropertyPLAYING
.
*/
private ObjectPropertynull
.
*/
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 ObjectPropertyPAUSED
.
*/
private ObjectPropertynull
.
*/
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 ObjectPropertySTOPPED
.
*/
private ObjectPropertynull
.
*/
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 ObjectPropertyHALTED
.
*/
private ObjectPropertynull
.
*/
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 ObjectPropertycurrentTime
reaches
* stopTime
and will be repeating. This callback is made
* prior to seeking back to startTime
.
*
* @see cycleCount
*/
private ObjectPropertynull
.
*/
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 ObjectPropertySTALLED
.
*/
private ObjectPropertynull
.
*/
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[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.
*
* 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 ObjectPropertynull
.
*/
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