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