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 ≤ 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 ≤ 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 }