diff --git a/CHANGELOG.md b/CHANGELOG.md index e1704a3540..8668a91585 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,8 @@ * Change compileOnly to implementation on gradle (for newer gradle versions and react-native 0.59 support) [#1592](https://github.com/react-native-community/react-native-video/pull/1592) * Replaced RCTBubblingEventBlock events by RCTDirectEventBlock to avoid event name collisions [#1625](https://github.com/react-native-community/react-native-video/pull/1625) * 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) * Fix background audio stopping on iOS when using `controls` [#1614](https://github.com/react-native-community/react-native-video/pull/1614) ### Version 4.4.1 diff --git a/README.md b/README.md index b5f99f218b..c8242d5ab8 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,17 @@ 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) +* Android ExoPlayer [STATE_READY](https://exoplayer.dev/doc/reference/com/google/android/exoplayer2/Player.html#STATE_READY) + +Platforms: Android ExoPlayer, Android MediaPlayer, iOS, Web + #### onPictureInPictureStatusChanged Callback function that is called when picture in picture becomes active or inactive. diff --git a/Video.js b/Video.js index 81c1b80202..474d7b269d 100644 --- a/Video.js +++ b/Video.js @@ -20,7 +20,7 @@ export default class Video extends Component { super(props); this.state = { - showPoster: true, + showPoster: !!props.poster }; } @@ -86,6 +86,12 @@ export default class Video extends Component { this._root = component; }; + _hidePoster = () => { + if (this.state.showPoster) { + this.setState({showPoster: false}); + } + } + _onLoadStart = (event) => { if (this.props.onLoadStart) { this.props.onLoadStart(event.nativeEvent); @@ -93,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); } @@ -117,10 +127,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); } @@ -163,6 +169,7 @@ export default class Video extends Component { }; _onReadyForDisplay = (event) => { + this._hidePoster(); if (this.props.onReadyForDisplay) { this.props.onReadyForDisplay(event.nativeEvent); } @@ -181,10 +188,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); } @@ -308,15 +311,16 @@ export default class Video extends Component { }; return ( - - - {this.props.poster && - this.state.showPoster && ( - - - - )} - + + + {this.state.showPoster && ( + + )} + ); } } 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 = { diff --git a/ios/Video/RCTVideo.m b/ios/Video/RCTVideo.m index 271433b3b8..71bf82d855 100644 --- a/ios/Video/RCTVideo.m +++ b/ios/Video/RCTVideo.m @@ -356,8 +356,6 @@ - (void)setSrc:(NSDictionary *)source [self setMaxBitRate:_maxBitRate]; [_player pause]; - [_playerViewController.view removeFromSuperview]; - _playerViewController = nil; if (_playbackRateObserverRegistered) { [_player removeObserver:self forKeyPath:playbackRate context:nil]; @@ -600,7 +598,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]) { @@ -692,12 +693,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) { @@ -1285,7 +1280,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]; @@ -1295,6 +1292,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]; } @@ -1490,6 +1489,7 @@ - (void)removeFromSuperview [self removePlayerLayer]; [_playerViewController.contentOverlayView removeObserver:self forKeyPath:@"frame"]; + [_playerViewController removeObserver:self forKeyPath:readyForDisplayKeyPath]; [_playerViewController.view removeFromSuperview]; _playerViewController = nil;