1 /*
   2  * Copyright (c) 2010, 2014, Oracle and/or its affiliates. All rights reserved.
   3  * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
   4  *
   5  * This code is free software; you can redistribute it and/or modify it
   6  * under the terms of the GNU General Public License version 2 only, as
   7  * published by the Free Software Foundation.  Oracle designates this
   8  * particular file as subject to the "Classpath" exception as provided
   9  * by Oracle in the LICENSE file that accompanied this code.
  10  *
  11  * This code is distributed in the hope that it will be useful, but WITHOUT
  12  * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
  13  * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
  14  * version 2 for more details (a copy is included in the LICENSE file that
  15  * accompanied this code).
  16  *
  17  * You should have received a copy of the GNU General Public License version
  18  * 2 along with this work; if not, write to the Free Software Foundation,
  19  * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
  20  *
  21  * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
  22  * or visit www.oracle.com if you need additional information or have any
  23  * questions.
  24  */
  25 
  26 package javafx.scene.media;
  27 
  28 import com.sun.javafx.geom.BaseBounds;
  29 import com.sun.javafx.geom.transform.Affine3D;
  30 import com.sun.javafx.geom.transform.BaseTransform;
  31 import com.sun.javafx.jmx.MXNodeAlgorithm;
  32 import com.sun.javafx.jmx.MXNodeAlgorithmContext;
  33 import com.sun.javafx.scene.DirtyBits;
  34 import com.sun.javafx.scene.NodeHelper;
  35 import com.sun.javafx.scene.media.MediaViewHelper;
  36 import com.sun.javafx.sg.prism.MediaFrameTracker;
  37 import com.sun.javafx.sg.prism.NGNode;
  38 import com.sun.javafx.tk.Toolkit;
  39 import com.sun.media.jfxmediaimpl.HostUtils;
  40 import com.sun.media.jfxmediaimpl.platform.ios.IOSMediaPlayer;
  41 import javafx.application.Platform;
  42 import javafx.beans.InvalidationListener;
  43 import javafx.beans.Observable;
  44 import javafx.beans.property.*;
  45 import javafx.beans.value.ChangeListener;
  46 import javafx.beans.value.ObservableObjectValue;
  47 import javafx.beans.value.ObservableValue;
  48 import javafx.collections.ObservableMap;
  49 import javafx.event.EventHandler;
  50 import javafx.geometry.NodeOrientation;
  51 import javafx.geometry.Rectangle2D;
  52 import javafx.scene.Node;
  53 import javafx.scene.Parent;
  54 
  55 /**
  56  * A {@link Node} that provides a view of {@link Media} being played by a
  57  * {@link MediaPlayer}.
  58  *
  59  * <p>The following code snippet provides a simple example of an
  60  * {@link javafx.application.Application#start(javafx.stage.Stage) Application.start()}
  61  * method which displays a video:
  62  * <code><pre>
  63  * public void start(Stage stage) {
  64  *     // Create and set the Scene.
  65  *     Scene scene = new Scene(new Group(), 540, 209);
  66  *     stage.setScene(scene);
  67  *
  68  *     // Name and display the Stage.
  69  *     stage.setTitle("Hello Media");
  70  *     stage.show();
  71  *
  72  *     // Create the media source.
  73  *     String source = getParameters().getRaw().get(0);
  74  *     Media media = new Media(source);
  75  *
  76  *     // Create the player and set to play automatically.
  77  *     MediaPlayer mediaPlayer = new MediaPlayer(media);
  78  *     mediaPlayer.setAutoPlay(true);
  79  *
  80  *     // Create the view and add it to the Scene.
  81  *     MediaView mediaView = new MediaView(mediaPlayer);
  82  *     ((Group) scene.getRoot()).getChildren().add(mediaView);
  83  * }
  84  * </pre></code>
  85  * The foregoing code will display the video as:
  86  * <br/>
  87  * <br/>
  88  * <img src="doc-files/mediaview.png" alt="Hello Media"/>
  89  * </p>
  90  * @since JavaFX 2.0
  91  */
  92 public class MediaView extends Node {
  93     static {
  94          // This is used by classes in different packages to get access to
  95          // private and package private methods.
  96         MediaViewHelper.setMediaViewAccessor(new MediaViewHelper.MediaViewAccessor() {
  97             @Override
  98             public NGNode doCreatePeer(Node node) {
  99                 return ((MediaView) node).doCreatePeer();
 100             }
 101 
 102             @Override
 103             public void doUpdatePeer(Node node) {
 104                 ((MediaView) node).doUpdatePeer();
 105             }
 106         });
 107     }
 108 
 109     /**
 110      * The name of the property in the {@link ObservableMap} returned by
 111      * {@link #getProperties()}. This value must also be defined as a JVM
 112      * command line definition for the frame rate to be added to the properties.
 113      */
 114     private static final String VIDEO_FRAME_RATE_PROPERTY_NAME = "jfxmedia.decodedVideoFPS";
 115 
 116     private static final String DEFAULT_STYLE_CLASS = "media-view";
 117 
 118     /**
 119      * Inner class used to convert a <code>MediaPlayer</code> error into a
 120      * <code>Bean</code> event.
 121      */
 122     private class MediaErrorInvalidationListener implements InvalidationListener {
 123 
 124         @Override public void invalidated(Observable value) {
 125             ObservableObjectValue<MediaException> errorProperty = (ObservableObjectValue<MediaException>)value;
 126             fireEvent(new MediaErrorEvent(getMediaPlayer(), getMediaView(), errorProperty.get()));
 127         }
 128     }
 129 
 130     /** Listener which converts <code>MediaPlayer</code> errors to events. */
 131     private InvalidationListener errorListener = new MediaErrorInvalidationListener();
 132 
 133     /** Listener which causes the geometry to be updated when the media dimension changes. */
 134     private InvalidationListener mediaDimensionListener = value -> {
 135         NodeHelper.markDirty(this, DirtyBits.NODE_VIEWPORT);
 136         impl_geomChanged();
 137     };
 138 
 139     /** Listener for decoded frame rate. */
 140     private com.sun.media.jfxmedia.events.VideoFrameRateListener decodedFrameRateListener;
 141     private boolean registerVideoFrameRateListener = false;
 142 
 143     /** Creates a decoded frame rate listener. Will return <code>null</code> if
 144      * the security manager does not permit retrieve system properties or if
 145      * VIDEO_FRAME_RATE_PROPERTY_NAME is not set to "true."
 146      */
 147     private com.sun.media.jfxmedia.events.VideoFrameRateListener createVideoFrameRateListener() {
 148         String listenerProp = null;
 149         try {
 150             listenerProp = System.getProperty(VIDEO_FRAME_RATE_PROPERTY_NAME);
 151         } catch (Throwable t) {
 152         }
 153 
 154         if (listenerProp == null || !Boolean.getBoolean(VIDEO_FRAME_RATE_PROPERTY_NAME)) {
 155             return null;
 156         } else {
 157             return videoFrameRate -> {
 158                 Platform.runLater(() -> {
 159                     ObservableMap props = getProperties();
 160                     props.put(VIDEO_FRAME_RATE_PROPERTY_NAME, videoFrameRate);
 161                 });
 162             };
 163         }
 164     }
 165 
 166     /***************************************** iOS specific stuff ***************************/
 167 
 168     private /*volatile*/ boolean mediaPlayerReady;
 169 
 170     private ChangeListener<Parent> parentListener;
 171     private ChangeListener<Boolean> treeVisibleListener;
 172     private ChangeListener<Number> opacityListener;
 173 
 174     private void createListeners() {
 175         parentListener = (ov2, oldParent, newParent) -> {
 176             updateOverlayVisibility();
 177         };
 178 
 179         treeVisibleListener = (ov1, oldVisible, newVisible) -> {
 180             updateOverlayVisibility();
 181         };
 182 
 183         opacityListener = (ov, oldOpacity, newOpacity) -> {
 184             updateOverlayOpacity();
 185         };
 186     }
 187 
 188     private IOSMediaPlayer getIOSPlayer() {
 189         return (IOSMediaPlayer) getMediaPlayer().retrieveJfxPlayer();
 190     }
 191 
 192     private boolean determineVisibility() {
 193         return (getParent() != null && isVisible());
 194     }
 195 
 196     private synchronized void updateOverlayVisibility() {
 197         if (mediaPlayerReady) {
 198             getIOSPlayer().setOverlayVisible(determineVisibility());
 199         }
 200     }
 201 
 202     private synchronized void updateOverlayOpacity() {
 203         if (mediaPlayerReady) {
 204             getIOSPlayer().setOverlayOpacity(getOpacity());
 205         }
 206     }
 207 
 208     private synchronized void updateOverlayX() {
 209         if (mediaPlayerReady) {
 210             getIOSPlayer().setOverlayX(getX());
 211         }
 212     }
 213 
 214     private synchronized void updateOverlayY() {
 215         if (mediaPlayerReady) {
 216             getIOSPlayer().setOverlayY(getY());
 217         }
 218     }
 219 
 220     private synchronized void updateOverlayWidth() {
 221         if (mediaPlayerReady) {
 222             getIOSPlayer().setOverlayWidth(getFitWidth());
 223         }
 224     }
 225 
 226     private synchronized void updateOverlayHeight() {
 227         if (mediaPlayerReady) {
 228             getIOSPlayer().setOverlayHeight(getFitHeight());
 229         }
 230     }
 231 
 232     private synchronized void updateOverlayPreserveRatio() {
 233         if (mediaPlayerReady) {
 234             getIOSPlayer().setOverlayPreserveRatio(isPreserveRatio());
 235         }
 236     }
 237 
 238     private static Affine3D calculateNodeToSceneTransform(Node node) {
 239         final Affine3D transform = new Affine3D();
 240         do {
 241             transform.preConcatenate(node.impl_getLeafTransform());
 242             node = node.getParent();
 243         } while (node != null);
 244 
 245         return transform;
 246     }
 247 
 248     private void updateOverlayTransformDirectly() {
 249         final Affine3D trans = MediaView.calculateNodeToSceneTransform(this);
 250         getIOSPlayer().setOverlayTransform(
 251                 trans.getMxx(), trans.getMxy(), trans.getMxz(), trans.getMxt(),
 252                 trans.getMyx(), trans.getMyy(), trans.getMyz(), trans.getMyt(),
 253                 trans.getMzx(), trans.getMzy(), trans.getMzz(), trans.getMzt());
 254     }
 255 
 256     private synchronized void updateOverlayTransform() {
 257         if (mediaPlayerReady) {
 258             updateOverlayTransformDirectly();
 259         }
 260     }
 261 
 262     private void updateIOSOverlay() {
 263         getIOSPlayer().setOverlayX(getX());
 264         getIOSPlayer().setOverlayY(getY());
 265         getIOSPlayer().setOverlayPreserveRatio(isPreserveRatio());
 266         getIOSPlayer().setOverlayWidth(getFitWidth());
 267         getIOSPlayer().setOverlayHeight(getFitHeight());
 268         getIOSPlayer().setOverlayOpacity(getOpacity());
 269         getIOSPlayer().setOverlayVisible(determineVisibility());
 270         updateOverlayTransformDirectly();
 271     }
 272 
 273     /**
 274      * @treatAsPrivate implementation detail
 275      * @deprecated This is an internal API that is not intended for use and will be removed in the next version
 276      */
 277     @Deprecated
 278     @Override
 279     public void impl_transformsChanged() {
 280         super.impl_transformsChanged();
 281         if (HostUtils.isIOS()) {
 282             updateOverlayTransform();
 283         }
 284     }
 285 
 286     /******************************************* End of iOS specific stuff ***************************/
 287 
 288     /**
 289      * @return reference to MediaView
 290      */
 291     private MediaView getMediaView() {
 292         return this;
 293     }
 294 
 295     {
 296         // To initialize the class helper at the begining each constructor of this class
 297         MediaViewHelper.initHelper(this);
 298     }
 299 
 300     /**
 301      * Creates a <code>MediaView</code> instance with no associated
 302      * {@link MediaPlayer}.
 303      */
 304     public MediaView() {
 305         getStyleClass().add(DEFAULT_STYLE_CLASS);
 306         setSmooth(Toolkit.getToolkit().getDefaultImageSmooth());
 307         decodedFrameRateListener = createVideoFrameRateListener();
 308         setNodeOrientation(NodeOrientation.LEFT_TO_RIGHT);
 309         if (HostUtils.isIOS()) {
 310             createListeners();
 311             parentProperty().addListener(parentListener);
 312             impl_treeVisibleProperty().addListener(treeVisibleListener);
 313             opacityProperty().addListener(opacityListener);
 314         }
 315     }
 316 
 317     /**
 318      * Creates a <code>MediaView</code> instance associated with the specified
 319      * {@link MediaPlayer}. Equivalent to
 320      * <pre><code>
 321      * MediaPlayer player; // initialization omitted
 322      * MediaView view = new MediaView();
 323      * view.setPlayer(player);
 324      * </code></pre>
 325      *
 326      * @param mediaPlayer the {@link MediaPlayer} the playback of which is to be
 327      * viewed via this class.
 328      */
 329     public MediaView(MediaPlayer mediaPlayer) {
 330         this();
 331         setNodeOrientation(NodeOrientation.LEFT_TO_RIGHT);
 332         setMediaPlayer(mediaPlayer);
 333     }
 334     /**
 335      * The <code>mediaPlayer</code> whose output will be handled by this view.
 336      *
 337      * Setting this value does not affect the status of the <code>MediaPlayer</code>,
 338      * e.g., if the <code>MediaPlayer</code> was playing prior to setting
 339      * <code>mediaPlayer</code> then it will continue playing.
 340      *
 341      * @see MediaException
 342      * @see MediaPlayer
 343      */
 344     private ObjectProperty<MediaPlayer> mediaPlayer;
 345 
 346     /**
 347      * Sets the <code>MediaPlayer</code> whose output will be handled by this view.
 348      * @param value the associated <code>MediaPlayer</code>.
 349      */
 350     public final void setMediaPlayer (MediaPlayer value) {
 351         mediaPlayerProperty().set(value);
 352     }
 353 
 354     /**
 355      * Retrieves the <code>MediaPlayer</code> whose output is being handled by
 356      * this view.
 357      * @return the associated <code>MediaPlayer</code>.
 358      */
 359     public final MediaPlayer getMediaPlayer() {
 360         return mediaPlayer == null ? null : mediaPlayer.get();
 361     }
 362 
 363     public final ObjectProperty<MediaPlayer> mediaPlayerProperty() {
 364         if (mediaPlayer == null) {
 365             mediaPlayer = new ObjectPropertyBase<MediaPlayer>() {
 366                 MediaPlayer oldValue = null;
 367                 @Override protected void invalidated() {
 368                     if (oldValue != null) {
 369                         Media media = oldValue.getMedia();
 370                         if (media != null) {
 371                             media.widthProperty().removeListener(mediaDimensionListener);
 372                             media.heightProperty().removeListener(mediaDimensionListener);
 373                         }
 374                         if (decodedFrameRateListener != null && getMediaPlayer().retrieveJfxPlayer() != null) {
 375                             getMediaPlayer().retrieveJfxPlayer().getVideoRenderControl().removeVideoFrameRateListener(decodedFrameRateListener);
 376                         }
 377                         oldValue.errorProperty().removeListener(errorListener);
 378                         oldValue.removeView(getMediaView());
 379                     }
 380 
 381                     //Uncomment the line below to print whether media is using Prism or Swing frame handler.
 382                     //System.err.println(getPGMediaView().getClass().getName());
 383                     //Uncomment the line below to print whether media is using Prism or Swing frame handler.
 384                     //System.err.println(getPGMediaView().getClass().getName());
 385                     MediaPlayer newValue = get();
 386                     if (newValue != null) {
 387                         newValue.addView(getMediaView());
 388                         newValue.errorProperty().addListener(errorListener);
 389                         if (decodedFrameRateListener != null && getMediaPlayer().retrieveJfxPlayer() != null) {
 390                             getMediaPlayer().retrieveJfxPlayer().getVideoRenderControl().addVideoFrameRateListener(decodedFrameRateListener);
 391                         } else if (decodedFrameRateListener != null) {
 392                             registerVideoFrameRateListener = true;
 393                         }
 394                         Media media = newValue.getMedia();
 395                         if (media != null) {
 396                             media.widthProperty().addListener(mediaDimensionListener);
 397                             media.heightProperty().addListener(mediaDimensionListener);
 398                         }
 399                     }
 400                     NodeHelper.markDirty(MediaView.this, DirtyBits.MEDIAVIEW_MEDIA);
 401                     impl_geomChanged();
 402                     oldValue = newValue;
 403                 }
 404                 @Override
 405                 public Object getBean() {
 406                     return MediaView.this;
 407                 }
 408 
 409                 @Override
 410                 public String getName() {
 411                     return "mediaPlayer";
 412                 }
 413             };
 414         }
 415         return mediaPlayer;
 416     }
 417     /**
 418      * Event handler to be invoked whenever an error occurs on this
 419      * <code>MediaView</code>.
 420      *
 421      * @see MediaErrorEvent
 422      */
 423     private ObjectProperty<EventHandler<MediaErrorEvent>> onError;
 424 
 425     /**
 426      * Sets the error event handler.
 427      * @param value the error event handler.
 428      */
 429     public final void setOnError(EventHandler<MediaErrorEvent> value) {
 430         onErrorProperty().set( value);
 431     }
 432 
 433     /**
 434      * Retrieves the error event handler.
 435      * @return the error event handler.
 436      */
 437     public final EventHandler<MediaErrorEvent> getOnError() {
 438         return onError == null ? null : onError.get();
 439     }
 440 
 441     public final ObjectProperty<EventHandler<MediaErrorEvent>> onErrorProperty() {
 442         if (onError == null) {
 443             onError = new ObjectPropertyBase<EventHandler<MediaErrorEvent>>() {
 444 
 445                 @Override
 446                 protected void invalidated() {
 447                     setEventHandler(MediaErrorEvent.MEDIA_ERROR, get());
 448                 }
 449 
 450                 @Override
 451                 public Object getBean() {
 452                     return MediaView.this;
 453                 }
 454 
 455                 @Override
 456                 public String getName() {
 457                     return "onError";
 458                 }
 459             };
 460         }
 461         return onError;
 462     }
 463     /**
 464      * Whether to preserve the aspect ratio (width / height) of the media when
 465      * scaling it to fit the node. If the aspect ratio is not preserved, the
 466      * media will be stretched or sheared in both dimensions to fit the
 467      * dimensions of the node. The default value is <code>true</code>.
 468      */
 469     private BooleanProperty preserveRatio;
 470 
 471     /**
 472      * Sets whether to preserve the media aspect ratio when scaling.
 473      * @param value whether to preserve the media aspect ratio.
 474      */
 475     public final void setPreserveRatio(boolean value) {
 476         preserveRatioProperty().set(value);
 477     };
 478 
 479     /**
 480      * Returns whether the media aspect ratio is preserved when scaling.
 481      * @return whether the media aspect ratio is preserved.
 482      */
 483     public final boolean isPreserveRatio() {
 484         return preserveRatio == null ? true : preserveRatio.get();
 485     }
 486 
 487     public final BooleanProperty preserveRatioProperty() {
 488         if (preserveRatio == null) {
 489             preserveRatio = new BooleanPropertyBase(true) {
 490 
 491                 @Override
 492                 protected void invalidated() {
 493                     if (HostUtils.isIOS()) {
 494                         updateOverlayPreserveRatio();
 495                     }
 496                     else {
 497                         NodeHelper.markDirty(MediaView.this, DirtyBits.NODE_VIEWPORT);
 498                         impl_geomChanged();
 499                     }
 500                 }
 501 
 502                 @Override
 503                 public Object getBean() {
 504                     return MediaView.this;
 505                 }
 506 
 507                 @Override
 508                 public String getName() {
 509                     return "preserveRatio";
 510                 }
 511             };
 512         }
 513         return preserveRatio;
 514     }
 515     /**
 516      * If set to <code>true</code> a better quality filtering
 517      * algorithm will be used when scaling this video to fit within the
 518      * bounding box provided by <code>fitWidth</code> and <code>fitHeight</code> or
 519      * when transforming.
 520      *
 521      * If set to <code>false</code> a faster but lesser quality filtering
 522      * will be used.
 523      *
 524      * The default value depends on platform configuration.
 525      */
 526     private BooleanProperty smooth;
 527 
 528     /**
 529      * Sets whether to smooth the media when scaling.
 530      * @param value whether to smooth the media.
 531      */
 532     public final void setSmooth(boolean value) {
 533         smoothProperty().set(value);
 534     }
 535 
 536     /**
 537      * Returns whether to smooth the media when scaling.
 538      * @return whether to smooth the media
 539      */
 540     public final boolean isSmooth() {
 541         return smooth == null ? false : smooth.get();
 542     }
 543 
 544     public final BooleanProperty smoothProperty() {
 545         if (smooth == null) {
 546             smooth = new BooleanPropertyBase() {
 547 
 548                 @Override
 549                 protected void invalidated() {
 550                     NodeHelper.markDirty(MediaView.this, DirtyBits.NODE_SMOOTH);
 551                 }
 552 
 553                 @Override
 554                 public Object getBean() {
 555                     return MediaView.this;
 556                 }
 557 
 558                 @Override
 559                 public String getName() {
 560                     return "smooth";
 561                 }
 562             };
 563         }
 564         return smooth;
 565     }
 566     // PENDING_DOC_REVIEW
 567     /**
 568      * Defines the current x coordinate of the <code>MediaView</code> origin.
 569      */
 570     private DoubleProperty x;
 571 
 572     /**
 573      * Sets the x coordinate of the <code>MediaView</code> origin.
 574      * @param value the x coordinate of the origin of the view.
 575      */
 576     public final void setX(double value) {
 577         xProperty().set(value);
 578     }
 579 
 580     /**
 581      * Retrieves the x coordinate of the <code>MediaView</code> origin.
 582      * @return the x coordinate of the origin of the view.
 583      */
 584     public final double getX() {
 585         return x == null ? 0.0 : x.get();
 586     }
 587 
 588     public final DoubleProperty xProperty() {
 589         if (x == null) {
 590             x = new DoublePropertyBase() {
 591 
 592                 @Override
 593                 protected void invalidated() {
 594                     if (HostUtils.isIOS()) {
 595                         updateOverlayX();
 596                     }
 597                     else {
 598                         NodeHelper.markDirty(MediaView.this, DirtyBits.NODE_GEOMETRY);
 599                         impl_geomChanged();
 600                     }
 601                 }
 602 
 603                 @Override
 604                 public Object getBean() {
 605                     return MediaView.this;
 606                 }
 607 
 608                 @Override
 609                 public String getName() {
 610                     return "x";
 611                 }
 612             };
 613         }
 614         return x;
 615     }
 616     // PENDING_DOC_REVIEW
 617     /**
 618      * Defines the current y coordinate of the <code>MediaView</code> origin.
 619      */
 620     private DoubleProperty y;
 621 
 622     /**
 623      * Sets the y coordinate of the <code>MediaView</code> origin.
 624      * @param value the y coordinate of the origin of the view.
 625      */
 626     public final void setY(double value) {
 627         yProperty().set(value);
 628     }
 629 
 630     /**
 631      * Retrieves the y coordinate of the <code>MediaView</code> origin.
 632      * @return the y coordinate of the origin of the view.
 633      */
 634     public final double getY() {
 635         return y == null ? 0.0 : y.get();
 636     }
 637 
 638     public final DoubleProperty yProperty() {
 639         if (y == null) {
 640             y = new DoublePropertyBase() {
 641 
 642                 @Override
 643                 protected void invalidated() {
 644                     if (HostUtils.isIOS()) {
 645                         updateOverlayY();
 646                     }
 647                     else {
 648                         NodeHelper.markDirty(MediaView.this, DirtyBits.NODE_GEOMETRY);
 649                         impl_geomChanged();
 650                     }
 651                 }
 652 
 653                 @Override
 654                 public Object getBean() {
 655                     return MediaView.this;
 656                 }
 657 
 658                 @Override
 659                 public String getName() {
 660                     return "y";
 661                 }
 662             };
 663         }
 664         return y;
 665     }
 666     // PENDING_DOC_REVIEW
 667     /**
 668      * Determines the width of the bounding box within which the source media is
 669      * resized as necessary to fit. If <code>value &le; 0</code>, then the width
 670      * of the bounding box will be set to the natural width of the media, but
 671      * <code>fitWidth</code> will be set to the supplied parameter, even if
 672      * non-positive.<p/>
 673      * See {@link #preserveRatioProperty preserveRatio} for information on interaction
 674      * between media views <code>fitWidth</code>, <code>fitHeight</code> and
 675      * <code>preserveRatio</code> attributes.
 676      */
 677     private DoubleProperty fitWidth;
 678 
 679     /**
 680      * Sets the width of the bounding box of the resized media.
 681      * @param value the width of the resized media.
 682      */
 683     public final void setFitWidth(double value) {
 684         fitWidthProperty().set(value);
 685     }
 686 
 687     /**
 688      * Retrieves the width of the bounding box of the resized media.
 689      * @return the height of the resized media.
 690      */
 691     public final double getFitWidth() {
 692         return fitWidth == null ? 0.0 : fitWidth.get();
 693     }
 694 
 695     public final DoubleProperty fitWidthProperty() {
 696         if (fitWidth == null) {
 697             fitWidth = new DoublePropertyBase() {
 698 
 699                 @Override
 700                 protected void invalidated() {
 701                     if (HostUtils.isIOS()) {
 702                         updateOverlayWidth();
 703                     }
 704                     else {
 705                         NodeHelper.markDirty(MediaView.this, DirtyBits.NODE_VIEWPORT);
 706                         impl_geomChanged();
 707                     }
 708                 }
 709 
 710                 @Override
 711                 public Object getBean() {
 712                     return MediaView.this;
 713                 }
 714 
 715                 @Override
 716                 public String getName() {
 717                     return "fitWidth";
 718                 }
 719             };
 720         }
 721         return fitWidth;
 722     }
 723     // PENDING_DOC_REVIEW
 724     /**
 725      * Determines the height of the bounding box within which the source media is
 726      * resized as necessary to fit. If <code>value &le; 0</code>, then the height
 727      * of the bounding box will be set to the natural height of the media, but
 728      * <code>fitHeight</code> will be set to the supplied parameter, even if
 729      * non-positive.<p/>
 730      * See {@link #preserveRatioProperty preserveRatio} for information on interaction
 731      * between media views <code>fitWidth</code>, <code>fitHeight</code> and
 732      * <code>preserveRatio</code> attributes.
 733      */
 734     private DoubleProperty fitHeight;
 735 
 736     /**
 737      * Sets the height of the bounding box of the resized media.
 738      * @param value the height of the resized media.
 739      */
 740     public final void setFitHeight(double value) {
 741         fitHeightProperty().set(value);
 742     };
 743 
 744     /**
 745      * Retrieves the height of the bounding box of the resized media.
 746      * @return the height of the resized media.
 747      */
 748     public final double getFitHeight() {
 749         return fitHeight == null ? 0.0 : fitHeight.get();
 750     }
 751 
 752     public final DoubleProperty fitHeightProperty() {
 753         if (fitHeight == null) {
 754             fitHeight = new DoublePropertyBase() {
 755 
 756                 @Override
 757                 protected void invalidated() {
 758                     if (HostUtils.isIOS()) {
 759                         updateOverlayHeight();
 760                     }
 761                     else {
 762                         NodeHelper.markDirty(MediaView.this, DirtyBits.NODE_VIEWPORT);
 763                         impl_geomChanged();
 764                     }
 765                 }
 766 
 767                 @Override
 768                 public Object getBean() {
 769                     return MediaView.this;
 770                 }
 771 
 772                 @Override
 773                 public String getName() {
 774                     return "fitHeight";
 775                 }
 776             };
 777         }
 778         return fitHeight;
 779     }
 780     // PENDING_DOC_REVIEW
 781     /**
 782      * Specifies a rectangular viewport into the media frame.
 783      * The viewport is a rectangle specified in the coordinates of the media frame.
 784      * The resulting bounds prior to scaling will
 785      * be the size of the viewport. The displayed image will include the
 786      * intersection of the frame and the viewport. The viewport can exceed the
 787      * size of the frame, but only the intersection will be displayed.
 788      * Setting <code>viewport</code> to null will clear the viewport.
 789      */
 790     private ObjectProperty<Rectangle2D> viewport;
 791 
 792     /**
 793      * Sets the rectangular viewport into the media frame.
 794      * @param value the rectangular viewport.
 795      */
 796     public final void setViewport(Rectangle2D value) {
 797         viewportProperty().set(value);
 798     };
 799 
 800     /**
 801      * Retrieves the rectangular viewport into the media frame.
 802      * @return the rectangular viewport.
 803      */
 804     public final Rectangle2D getViewport() {
 805         return viewport == null ? null : viewport.get();
 806     }
 807 
 808     public final ObjectProperty<Rectangle2D> viewportProperty() {
 809         if (viewport == null) {
 810             viewport = new ObjectPropertyBase<Rectangle2D>() {
 811 
 812                 @Override
 813                 protected void invalidated() {
 814                     NodeHelper.markDirty(MediaView.this, DirtyBits.NODE_VIEWPORT);
 815                     impl_geomChanged();
 816                 }
 817 
 818                 @Override
 819                 public Object getBean() {
 820                     return MediaView.this;
 821                 }
 822 
 823                 @Override
 824                 public String getName() {
 825                     return "viewport";
 826                 }
 827             };
 828         }
 829         return viewport;
 830     }
 831 
 832     void notifyMediaChange() {
 833         MediaPlayer player = getMediaPlayer();
 834         if (player != null) {
 835             final NGMediaView peer = NodeHelper.getPeer(this);
 836             peer.setMediaProvider(player);
 837         }
 838 
 839         NodeHelper.markDirty(this, DirtyBits.MEDIAVIEW_MEDIA);
 840         impl_geomChanged();
 841     }
 842 
 843     void notifyMediaSizeChange() {
 844         NodeHelper.markDirty(this, DirtyBits.NODE_VIEWPORT);
 845         impl_geomChanged();
 846     }
 847 
 848     void notifyMediaFrameUpdated() {
 849         decodedFrameCount++;
 850         NodeHelper.markDirty(this, DirtyBits.NODE_CONTENTS);
 851     }
 852 
 853     /*
 854      * Note: This method MUST only be called via its accessor method.
 855      */
 856     private NGNode doCreatePeer() {
 857         NGMediaView peer = new NGMediaView();
 858         // this has to be done on the main toolkit thread...
 859         peer.setFrameTracker(new MediaViewFrameTracker());
 860         return peer;
 861     }
 862 
 863     /**
 864      * @treatAsPrivate implementation detail
 865      * @deprecated This is an internal API that is not intended for use and will be removed in the next version
 866      */
 867     @Deprecated
 868     @Override
 869     public BaseBounds impl_computeGeomBounds(BaseBounds bounds, BaseTransform tx) {
 870 
 871         // need to figure out the width/height to use for computing bounds
 872         Media media = (getMediaPlayer() == null) ? null : getMediaPlayer().getMedia();
 873         double w = media != null ? media.getWidth()  : 0; // if media is null, width will be 0
 874         double h = media != null ? media.getHeight() : 0; // if media is null, height will be 0
 875         double newW = getFitWidth();
 876         double newH = getFitHeight();
 877         final double vw = getViewport() != null ? getViewport().getWidth()  : 0; // if viewport is null, width will be 0
 878         final double vh = getViewport() != null ? getViewport().getHeight() : 0; // if viewport is null, height will be 0
 879 
 880         if (vw > 0 && vh > 0) {
 881             w = vw;
 882             h = vh;
 883         }
 884 
 885         if (getFitWidth() <= 0.0 && getFitHeight() <= 0.0) {
 886             newW = w;
 887             newH = h;
 888         } else if (isPreserveRatio()) {
 889             if (getFitWidth() <= 0.0) {
 890                 newW = h > 0 ? w * (getFitHeight() / h) : 0.0F;
 891                 newH = getFitHeight();
 892             } else if (getFitHeight() <= 0.0) {
 893                 newW = getFitWidth();
 894                 newH = w > 0 ? h * (getFitWidth() / w) : 0.0F;
 895             } else {
 896                 if (w == 0.0) w = getFitWidth();
 897                 if (h == 0.0) h = getFitHeight();
 898                 double scale = Math.min(getFitWidth() / w, getFitHeight() / h);
 899                 newW = w * scale;
 900                 newH = h * scale;
 901             }
 902         } else if (getFitHeight() <= 0.0) {
 903             newH = h;
 904         } else if (getFitWidth() <= 0.0) {
 905             newW = w;
 906         }
 907         if (newH < 1.0F) {
 908             newH = 1.0F;
 909         }
 910         if (newW < 1.0F) {
 911             newW = 1.0F;
 912         }
 913 
 914         w = newW;
 915         h = newH;
 916 
 917         // if the w or h are non-positive, then there is no size
 918         // for the media view
 919         if (w <= 0 || h <= 0) {
 920             return bounds.makeEmpty();
 921         }
 922         bounds = bounds.deriveWithNewBounds((float)getX(), (float)getY(), 0.0f,
 923                 (float)(getX()+w), (float)(getY()+h), 0.0f);
 924         bounds = tx.transform(bounds, bounds);
 925         return bounds;
 926     }
 927 
 928     /**
 929      * @treatAsPrivate implementation detail
 930      * @deprecated This is an internal API that is not intended for use and will be removed in the next version
 931      */
 932     @Deprecated
 933     @Override
 934     protected boolean impl_computeContains(double localX, double localY) {
 935         // Currently this is simply a local bounds test which is already tested
 936         // by the caller (Node.contains()).
 937         return true;
 938     }
 939 
 940     void updateViewport() {
 941 
 942         if (getMediaPlayer() == null) {
 943             return;
 944         }
 945 
 946         final NGMediaView peer = NodeHelper.getPeer(this);
 947         if (getViewport() != null) {
 948             peer.setViewport((float)getFitWidth(), (float)getFitHeight(),
 949                              (float)getViewport().getMinX(), (float)getViewport().getMinY(),
 950                              (float)getViewport().getWidth(), (float)getViewport().getHeight(),
 951                              isPreserveRatio());
 952         } else {
 953             peer.setViewport((float)getFitWidth(), (float)getFitHeight(),
 954                              0.0F, 0.0F, 0.0F, 0.0F,
 955                              isPreserveRatio());
 956         }
 957     }
 958 
 959 
 960     /*
 961      * Note: This method MUST only be called via its accessor method.
 962      */
 963     private void doUpdatePeer() {
 964         final NGMediaView peer = NodeHelper.getPeer(this);
 965         if (NodeHelper.isDirty(this, DirtyBits.NODE_GEOMETRY)) {
 966             peer.setX((float)getX());
 967             peer.setY((float)getY());
 968         }
 969         if (NodeHelper.isDirty(this, DirtyBits.NODE_SMOOTH)) {
 970             peer.setSmooth(isSmooth());
 971         }
 972         if (NodeHelper.isDirty(this, DirtyBits.NODE_VIEWPORT)) {
 973             updateViewport();
 974         }
 975         if (NodeHelper.isDirty(this, DirtyBits.NODE_CONTENTS)) {
 976             peer.renderNextFrame();
 977         }
 978         if (NodeHelper.isDirty(this, DirtyBits.MEDIAVIEW_MEDIA)) {
 979             MediaPlayer player = getMediaPlayer();
 980             if (player != null) {
 981                 peer.setMediaProvider(player);
 982                 updateViewport();
 983             } else {
 984                 peer.setMediaProvider(null);
 985             }
 986         }
 987     }
 988 
 989 
 990     private int decodedFrameCount;
 991     private int renderedFrameCount;
 992 
 993     /**
 994      * @treatAsPrivate implementation detail
 995      * @deprecated This is an internal API that is not intended for use and will be removed in the next version
 996      */
 997     @Deprecated
 998     public void impl_perfReset() {
 999         decodedFrameCount = 0;
1000         renderedFrameCount = 0;
1001     }
1002 
1003     /**
1004      * @return number of frames that have been submitted for rendering
1005      * @treatAsPrivate implementation detail
1006      * @deprecated This is an internal API that is not intended for use and will be removed in the next version
1007      */
1008     @Deprecated
1009     public int impl_perfGetDecodedFrameCount() {
1010         return decodedFrameCount;
1011     }
1012 
1013     /**
1014      * @return number of frames that have been rendered
1015      * @treatAsPrivate implementation detail
1016      * @deprecated This is an internal API that is not intended for use and will be removed in the next version
1017      */
1018     @Deprecated
1019     public int impl_perfGetRenderedFrameCount() {
1020         return renderedFrameCount;
1021     }
1022 
1023     private class MediaViewFrameTracker implements MediaFrameTracker {
1024         @Override
1025         public void incrementDecodedFrameCount(int count) {
1026             decodedFrameCount += count;
1027         }
1028 
1029         @Override
1030         public void incrementRenderedFrameCount(int count) {
1031             renderedFrameCount += count;
1032         }
1033     }
1034 
1035     /**
1036      * @treatAsPrivate implementation detail
1037      * @deprecated This is an internal API that is not intended for use and will be removed in the next version
1038      */
1039     @Deprecated
1040     public Object impl_processMXNode(MXNodeAlgorithm alg, MXNodeAlgorithmContext ctx) {
1041         return alg.processLeafNode(this, ctx);
1042     }
1043 
1044     /**
1045      * Called by MediaPlayer when it becomes ready
1046      */
1047     void _mediaPlayerOnReady() {
1048         if (decodedFrameRateListener != null && getMediaPlayer().retrieveJfxPlayer() != null && registerVideoFrameRateListener) {
1049             getMediaPlayer().retrieveJfxPlayer().getVideoRenderControl().addVideoFrameRateListener(decodedFrameRateListener);
1050             registerVideoFrameRateListener = false;
1051         }
1052         if (HostUtils.isIOS()) {
1053             synchronized (this) {
1054                 updateIOSOverlay();
1055                 mediaPlayerReady = true;
1056             }
1057         }
1058     }
1059 }