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