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