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