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