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