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