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