From 6d3936de0bfe877e0918adceaddba120d5917cba Mon Sep 17 00:00:00 2001 From: Jens Andersson Date: Wed, 19 Jun 2019 10:39:35 +0100 Subject: [PATCH 1/7] Fixes bug where poster and video was displayed simultaneously --- Video.js | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/Video.js b/Video.js index 81c1b80202..d7bd51f64d 100644 --- a/Video.js +++ b/Video.js @@ -308,15 +308,16 @@ export default class Video extends Component { }; return ( - - - {this.props.poster && - this.state.showPoster && ( - - - - )} - + + + {this.props.poster && this.state.showPoster && ( + + )} + ); } } From cc7db9149097090a99c1d9579cb661f864375b08 Mon Sep 17 00:00:00 2001 From: Jens Andersson Date: Wed, 19 Jun 2019 12:00:57 +0100 Subject: [PATCH 2/7] Improved handling of poster, fading it out on load --- Video.js | 32 ++++++++++++++++++++------------ 1 file changed, 20 insertions(+), 12 deletions(-) diff --git a/Video.js b/Video.js index d7bd51f64d..890d4ba2cd 100644 --- a/Video.js +++ b/Video.js @@ -1,6 +1,6 @@ import React, {Component} from 'react'; import PropTypes from 'prop-types'; -import {StyleSheet, requireNativeComponent, NativeModules, View, ViewPropTypes, Image, Platform, findNodeHandle} from 'react-native'; +import {StyleSheet, requireNativeComponent, NativeModules, View, ViewPropTypes, Image, Platform, findNodeHandle,Animated} from 'react-native'; import resolveAssetSource from 'react-native/Libraries/Image/resolveAssetSource'; import TextTrackType from './TextTrackType'; import FilterType from './FilterType'; @@ -20,7 +20,8 @@ export default class Video extends Component { super(props); this.state = { - showPoster: true, + showPoster: !!props.poster, + posterFadeAnim: new Animated.Value(1), }; } @@ -86,6 +87,18 @@ export default class Video extends Component { this._root = component; }; + _hidePoster = () => { + Animated.timing( + this.state.posterFadeAnim, + { + toValue: 0, + delay: 200, // Not ideal but need to wait for the first frame to be rendered + duration: 100, + useNativeDriver: true + } + ).start(() => this.setState({showPoster: false})); + } + _onLoadStart = (event) => { if (this.props.onLoadStart) { this.props.onLoadStart(event.nativeEvent); @@ -93,6 +106,9 @@ export default class Video extends Component { }; _onLoad = (event) => { + if (this.state.showPoster) { + this._hidePoster(); + } if (this.props.onLoad) { this.props.onLoad(event.nativeEvent); } @@ -117,10 +133,6 @@ export default class Video extends Component { }; _onSeek = (event) => { - if (this.state.showPoster && !this.props.audioOnly) { - this.setState({showPoster: false}); - } - if (this.props.onSeek) { this.props.onSeek(event.nativeEvent); } @@ -181,10 +193,6 @@ export default class Video extends Component { }; _onPlaybackRateChange = (event) => { - if (this.state.showPoster && event.nativeEvent.playbackRate !== 0 && !this.props.audioOnly) { - this.setState({showPoster: false}); - } - if (this.props.onPlaybackRateChange) { this.props.onPlaybackRateChange(event.nativeEvent); } @@ -314,8 +322,8 @@ export default class Video extends Component { {...nativeProps} style={StyleSheet.absoluteFill} /> - {this.props.poster && this.state.showPoster && ( - + {this.state.showPoster && ( + )} ); From 420332e0780de3028fa04c4117fdeb2c0c2ed7a6 Mon Sep 17 00:00:00 2001 From: Jens Andersson Date: Wed, 19 Jun 2019 14:18:25 +0100 Subject: [PATCH 3/7] Implemented onReadyForDisplay for iOS when using controls --- ios/Video/RCTVideo.m | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/ios/Video/RCTVideo.m b/ios/Video/RCTVideo.m index dcadee85f4..e7f4b5570d 100644 --- a/ios/Video/RCTVideo.m +++ b/ios/Video/RCTVideo.m @@ -354,8 +354,6 @@ - (void)setSrc:(NSDictionary *)source [self setMaxBitRate:_maxBitRate]; [_player pause]; - [_playerViewController.view removeFromSuperview]; - _playerViewController = nil; if (_playbackRateObserverRegistered) { [_player removeObserver:self forKeyPath:playbackRate context:nil]; @@ -598,7 +596,10 @@ - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(N } else return [super observeValueForKeyPath:keyPath ofObject:object change:change context:context]; } - + if([keyPath isEqualToString:readyForDisplayKeyPath] && [change objectForKey:NSKeyValueChangeNewKey] && self.onReadyForDisplay) { + self.onReadyForDisplay(@{@"target": self.reactTag}); + return; + } if (object == _playerItem) { // When timeMetadata is read the event onTimedMetadata is triggered if ([keyPath isEqualToString:timedMetadata]) { @@ -690,12 +691,6 @@ - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(N _playerBufferEmpty = NO; self.onVideoBuffer(@{@"isBuffering": @(NO), @"target": self.reactTag}); } - } else if (object == _playerLayer) { - if([keyPath isEqualToString:readyForDisplayKeyPath] && [change objectForKey:NSKeyValueChangeNewKey]) { - if([change objectForKey:NSKeyValueChangeNewKey] && self.onReadyForDisplay) { - self.onReadyForDisplay(@{@"target": self.reactTag}); - } - } } else if (object == _player) { if([keyPath isEqualToString:playbackRate]) { if(self.onPlaybackRateChange) { @@ -1283,7 +1278,9 @@ - (void)usePlayerViewController { if( _player ) { - _playerViewController = [self createPlayerViewController:_player withPlayerItem:_playerItem]; + if (!_playerViewController) { + _playerViewController = [self createPlayerViewController:_player withPlayerItem:_playerItem]; + } // to prevent video from being animated when resizeMode is 'cover' // resize mode must be set before subview is added [self setResizeMode:_resizeMode]; @@ -1293,6 +1290,8 @@ - (void)usePlayerViewController [viewController addChildViewController:_playerViewController]; [self addSubview:_playerViewController.view]; } + + [_playerViewController addObserver:self forKeyPath:readyForDisplayKeyPath options:NSKeyValueObservingOptionNew context:nil]; [_playerViewController.contentOverlayView addObserver:self forKeyPath:@"frame" options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld context:NULL]; } @@ -1488,6 +1487,7 @@ - (void)removeFromSuperview [self removePlayerLayer]; [_playerViewController.contentOverlayView removeObserver:self forKeyPath:@"frame"]; + [_playerViewController removeObserver:self forKeyPath:readyForDisplayKeyPath]; [_playerViewController.view removeFromSuperview]; _playerViewController = nil; From b193baa8024fde08fa165c3bb66d89bd03c2a07c Mon Sep 17 00:00:00 2001 From: Jens Andersson Date: Wed, 19 Jun 2019 14:19:06 +0100 Subject: [PATCH 4/7] Hide poster at the right time ie in onReadyForDisplay --- Video.js | 23 +++++++---------------- 1 file changed, 7 insertions(+), 16 deletions(-) diff --git a/Video.js b/Video.js index 890d4ba2cd..d9e91cd022 100644 --- a/Video.js +++ b/Video.js @@ -1,6 +1,6 @@ import React, {Component} from 'react'; import PropTypes from 'prop-types'; -import {StyleSheet, requireNativeComponent, NativeModules, View, ViewPropTypes, Image, Platform, findNodeHandle,Animated} from 'react-native'; +import {StyleSheet, requireNativeComponent, NativeModules, View, ViewPropTypes, Image, Platform, findNodeHandle} from 'react-native'; import resolveAssetSource from 'react-native/Libraries/Image/resolveAssetSource'; import TextTrackType from './TextTrackType'; import FilterType from './FilterType'; @@ -20,8 +20,7 @@ export default class Video extends Component { super(props); this.state = { - showPoster: !!props.poster, - posterFadeAnim: new Animated.Value(1), + showPoster: !!props.poster }; } @@ -88,15 +87,7 @@ export default class Video extends Component { }; _hidePoster = () => { - Animated.timing( - this.state.posterFadeAnim, - { - toValue: 0, - delay: 200, // Not ideal but need to wait for the first frame to be rendered - duration: 100, - useNativeDriver: true - } - ).start(() => this.setState({showPoster: false})); + this.setState({showPoster: false}); } _onLoadStart = (event) => { @@ -106,9 +97,6 @@ export default class Video extends Component { }; _onLoad = (event) => { - if (this.state.showPoster) { - this._hidePoster(); - } if (this.props.onLoad) { this.props.onLoad(event.nativeEvent); } @@ -175,6 +163,9 @@ export default class Video extends Component { }; _onReadyForDisplay = (event) => { + if (this.state.showPoster) { + this._hidePoster(); + } if (this.props.onReadyForDisplay) { this.props.onReadyForDisplay(event.nativeEvent); } @@ -323,7 +314,7 @@ export default class Video extends Component { style={StyleSheet.absoluteFill} /> {this.state.showPoster && ( - + )} ); From e5b5bbee7f1005875b049ff52cec512ead2e55e1 Mon Sep 17 00:00:00 2001 From: Jens Andersson Date: Wed, 19 Jun 2019 14:32:44 +0100 Subject: [PATCH 5/7] Updated changelog and readme --- CHANGELOG.md | 2 ++ README.md | 11 +++++++++++ 2 files changed, 13 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index a91fd63833..04567483c2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,8 @@ ### next * Added `onPlaybackRateChange` to README [#1578](https://github.com/react-native-community/react-native-video/pull/1578) +* Added `onReadyForDisplay` to README [#1627](https://github.com/react-native-community/react-native-video/pull/1627) +* Improved handling of poster image. Fixes bug with displaying video and poster simultaneously. [#1627](https://github.com/react-native-community/react-native-video/pull/1627) ### Version 4.4.1 * Fix tvOS picture-in-picture compilation regression [#1518](https://github.com/react-native-community/react-native-video/pull/1518) diff --git a/README.md b/README.md index b5f99f218b..0f0762b083 100644 --- a/README.md +++ b/README.md @@ -302,6 +302,7 @@ var styles = StyleSheet.create({ * [onFullscreenPlayerDidDismiss](#onfullscreenplayerdiddismiss) * [onLoad](#onload) * [onLoadStart](#onloadstart) +* [onReadyForDisplay](#onreadyfordisplay) * [onPictureInPictureStatusChanged](#onpictureinpicturestatuschanged) * [onPlaybackRateChange](#onplaybackratechange) * [onProgress](#onprogress) @@ -954,6 +955,16 @@ Example: Platforms: all +#### onReadyForDisplay +Callback function that is called when the first video frame is ready for display. This is when the poster is removed. + +Payload: none + +* iOS: [readyForDisplay](https://developer.apple.com/documentation/avkit/avplayerviewcontroller/1615830-readyfordisplay?language=objc) +* Android: [MEDIA_INFO_VIDEO_RENDERING_START](https://developer.android.com/reference/android/media/MediaPlayer#MEDIA_INFO_VIDEO_RENDERING_START) + +Platforms: all + #### onPictureInPictureStatusChanged Callback function that is called when picture in picture becomes active or inactive. From fcef441369c1840fe46a3ed0326694c7cd76dd40 Mon Sep 17 00:00:00 2001 From: Jens Andersson Date: Wed, 19 Jun 2019 15:35:49 +0100 Subject: [PATCH 6/7] Added onReadyForDisplay to web/dom --- README.md | 3 ++- dom/RCTVideo.js | 6 ++++++ 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 0f0762b083..c8242d5ab8 100644 --- a/README.md +++ b/README.md @@ -962,8 +962,9 @@ Payload: none * iOS: [readyForDisplay](https://developer.apple.com/documentation/avkit/avplayerviewcontroller/1615830-readyfordisplay?language=objc) * Android: [MEDIA_INFO_VIDEO_RENDERING_START](https://developer.android.com/reference/android/media/MediaPlayer#MEDIA_INFO_VIDEO_RENDERING_START) +* Android ExoPlayer [STATE_READY](https://exoplayer.dev/doc/reference/com/google/android/exoplayer2/Player.html#STATE_READY) -Platforms: all +Platforms: Android ExoPlayer, Android MediaPlayer, iOS, Web #### onPictureInPictureStatusChanged Callback function that is called when picture in picture becomes active or inactive. diff --git a/dom/RCTVideo.js b/dom/RCTVideo.js index 9831b61ddb..321289736c 100644 --- a/dom/RCTVideo.js +++ b/dom/RCTVideo.js @@ -37,6 +37,7 @@ class RCTVideo extends RCTView { this.videoElement = this.initializeVideoElement(); this.videoElement.addEventListener("ended", this.onEnd); this.videoElement.addEventListener("loadeddata", this.onLoad); + this.videoElement.addEventListener("canplay", this.onReadyForDisplay); this.videoElement.addEventListener("loadstart", this.onLoadStart); this.videoElement.addEventListener("pause", this.onPause); this.videoElement.addEventListener("play", this.onPlay); @@ -51,6 +52,7 @@ class RCTVideo extends RCTView { detachFromView(view: UIView) { this.videoElement.removeEventListener("ended", this.onEnd); this.videoElement.removeEventListener("loadeddata", this.onLoad); + this.videoElement.removeEventListener("canplay", this.onReadyForDisplay); this.videoElement.removeEventListener("loadstart", this.onLoadStart); this.videoElement.removeEventListener("pause", this.onPause); this.videoElement.removeEventListener("play", this.onPlay); @@ -203,6 +205,10 @@ class RCTVideo extends RCTView { this.sendEvent("topVideoLoad", payload); } + onReadyForDisplay = () => { + this.sendEvent("onReadyForDisplay"); + } + onLoadStart = () => { const src = this.videoElement.currentSrc; const payload = { From 6ea65833fe907c8fba752d4497bb4c00ba098e41 Mon Sep 17 00:00:00 2001 From: Jens Andersson Date: Thu, 20 Jun 2019 09:24:12 +0100 Subject: [PATCH 7/7] Make sure to hide poster for Windows, even though onReadyForDisplay is not implemented --- Video.js | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/Video.js b/Video.js index d9e91cd022..474d7b269d 100644 --- a/Video.js +++ b/Video.js @@ -87,7 +87,9 @@ export default class Video extends Component { }; _hidePoster = () => { - this.setState({showPoster: false}); + if (this.state.showPoster) { + this.setState({showPoster: false}); + } } _onLoadStart = (event) => { @@ -97,6 +99,10 @@ export default class Video extends Component { }; _onLoad = (event) => { + // Need to hide poster here for windows as onReadyForDisplay is not implemented + if (Platform.OS === 'windows') { + this._hidePoster(); + } if (this.props.onLoad) { this.props.onLoad(event.nativeEvent); } @@ -163,9 +169,7 @@ export default class Video extends Component { }; _onReadyForDisplay = (event) => { - if (this.state.showPoster) { - this._hidePoster(); - } + this._hidePoster(); if (this.props.onReadyForDisplay) { this.props.onReadyForDisplay(event.nativeEvent); }