diff --git a/CHANGELOG.md b/CHANGELOG.md index 33e01507..1b1fe4eb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,19 @@ ### next * Fix Android videos being able to play with background music/audio from other apps. +### Version 4.4.2 +* 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 +* Fix tvOS picture-in-picture compilation regression [#1518](https://github.com/react-native-community/react-native-video/pull/1518) +* fullscreen rotation issues with iOS built-in controls [#1441](https://github.com/react-native-community/react-native-video/pull/1441) +* Fix player freeze when playing audio files on ExoPlayer [#1529](https://github.com/react-native-community/react-native-video/pull/1529) + ### Version 4.4.0 * Fix runtime warning by replacing `UIManager.RCTVideo` with `UIManager.getViewManagerConfig('RCTVideo')` (and ensuring backwards compat) [#1487](https://github.com/react-native-community/react-native-video/pull/1487) * Fix loading package resolved videos when using video-caching [#1438](https://github.com/react-native-community/react-native-video/pull/1438) diff --git a/README.md b/README.md index 93627ae8..90d67522 100644 --- a/README.md +++ b/README.md @@ -302,7 +302,9 @@ var styles = StyleSheet.create({ * [onFullscreenPlayerDidDismiss](#onfullscreenplayerdiddismiss) * [onLoad](#onload) * [onLoadStart](#onloadstart) +* [onReadyForDisplay](#onreadyfordisplay) * [onPictureInPictureStatusChanged](#onpictureinpicturestatuschanged) +* [onPlaybackRateChange](#onplaybackratechange) * [onProgress](#onprogress) * [onSeek](#onseek) * [onRestoreUserInterfaceForPictureInPictureStop](#onrestoreuserinterfaceforpictureinpicturestop) @@ -960,6 +962,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. @@ -976,6 +989,23 @@ isActive: true Platforms: iOS +#### onPlaybackRateChange +Callback function that is called when the rate of playback changes - either paused or starts/resumes. + +Property | Type | Description +--- | --- | --- +playbackRate | number | 0 when playback is paused, 1 when playing at normal speed. Other values when playback is slowed down or sped up + +Example: +``` +{ + playbackRate: 0, // indicates paused +} +``` + +Platforms: all + + #### onProgress Callback function that is called every progressUpdateInterval seconds with info about which position the media is currently playing. diff --git a/Video.js b/Video.js index 81c1b802..474d7b26 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/android-exoplayer/build.gradle b/android-exoplayer/build.gradle index 717d04ad..7fccb220 100644 --- a/android-exoplayer/build.gradle +++ b/android-exoplayer/build.gradle @@ -22,7 +22,7 @@ android { } dependencies { - compileOnly "com.facebook.react:react-native:${safeExtGet('reactNativeVersion', '+')}" + implementation "com.facebook.react:react-native:${safeExtGet('reactNativeVersion', '+')}" implementation('com.google.android.exoplayer:exoplayer:2.9.3') { exclude group: 'com.android.support' } diff --git a/android-exoplayer/src/main/java/com/brentvatne/exoplayer/ReactExoplayerView.java b/android-exoplayer/src/main/java/com/brentvatne/exoplayer/ReactExoplayerView.java index 2ed30269..bea3e20e 100644 --- a/android-exoplayer/src/main/java/com/brentvatne/exoplayer/ReactExoplayerView.java +++ b/android-exoplayer/src/main/java/com/brentvatne/exoplayer/ReactExoplayerView.java @@ -975,16 +975,17 @@ class ReactExoplayerView extends FrameLayout implements groupIndex = getGroupIndexForDefaultLocale(groups); } - if (groupIndex == C.INDEX_UNSET && trackType == C.TRACK_TYPE_VIDEO) { // Video auto - if (groups.length != 0) { - TrackGroup group = groups.get(0); - tracks = new int[group.length]; - groupIndex = 0; - for (int j = 0; j < group.length; j++) { - tracks[j] = j; - } + if (groupIndex == C.INDEX_UNSET && trackType == C.TRACK_TYPE_VIDEO && groups.length != 0) { // Video auto + // Add all tracks as valid options for ABR to choose from + TrackGroup group = groups.get(0); + tracks = new int[group.length]; + groupIndex = 0; + for (int j = 0; j < group.length; j++) { + tracks[j] = j; } - } else if (groupIndex == C.INDEX_UNSET) { + } + + if (groupIndex == C.INDEX_UNSET) { trackSelector.setParameters(disableParameters); return; } diff --git a/android/build.gradle b/android/build.gradle index ff05f873..df1bb2c8 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -21,6 +21,6 @@ android { dependencies { //noinspection GradleDynamicVersion - compileOnly "com.facebook.react:react-native:${safeExtGet('reactNativeVersion', '+')}" + implementation "com.facebook.react:react-native:${safeExtGet('reactNativeVersion', '+')}" implementation 'com.yqritc:android-scalablevideoview:1.0.4' } diff --git a/dom/RCTVideo.js b/dom/RCTVideo.js index 9831b61d..32128973 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.h b/ios/Video/RCTVideo.h index 19d5d19f..26d436c2 100644 --- a/ios/Video/RCTVideo.h +++ b/ios/Video/RCTVideo.h @@ -21,27 +21,27 @@ @interface RCTVideo : UIView #endif -@property (nonatomic, copy) RCTBubblingEventBlock onVideoLoadStart; -@property (nonatomic, copy) RCTBubblingEventBlock onVideoLoad; -@property (nonatomic, copy) RCTBubblingEventBlock onVideoBuffer; -@property (nonatomic, copy) RCTBubblingEventBlock onVideoError; -@property (nonatomic, copy) RCTBubblingEventBlock onVideoProgress; -@property (nonatomic, copy) RCTBubblingEventBlock onBandwidthUpdate; -@property (nonatomic, copy) RCTBubblingEventBlock onVideoSeek; -@property (nonatomic, copy) RCTBubblingEventBlock onVideoEnd; -@property (nonatomic, copy) RCTBubblingEventBlock onTimedMetadata; -@property (nonatomic, copy) RCTBubblingEventBlock onVideoAudioBecomingNoisy; -@property (nonatomic, copy) RCTBubblingEventBlock onVideoFullscreenPlayerWillPresent; -@property (nonatomic, copy) RCTBubblingEventBlock onVideoFullscreenPlayerDidPresent; -@property (nonatomic, copy) RCTBubblingEventBlock onVideoFullscreenPlayerWillDismiss; -@property (nonatomic, copy) RCTBubblingEventBlock onVideoFullscreenPlayerDidDismiss; -@property (nonatomic, copy) RCTBubblingEventBlock onReadyForDisplay; -@property (nonatomic, copy) RCTBubblingEventBlock onPlaybackStalled; -@property (nonatomic, copy) RCTBubblingEventBlock onPlaybackResume; -@property (nonatomic, copy) RCTBubblingEventBlock onPlaybackRateChange; -@property (nonatomic, copy) RCTBubblingEventBlock onVideoExternalPlaybackChange; -@property (nonatomic, copy) RCTBubblingEventBlock onPictureInPictureStatusChanged; -@property (nonatomic, copy) RCTBubblingEventBlock onRestoreUserInterfaceForPictureInPictureStop; +@property (nonatomic, copy) RCTDirectEventBlock onVideoLoadStart; +@property (nonatomic, copy) RCTDirectEventBlock onVideoLoad; +@property (nonatomic, copy) RCTDirectEventBlock onVideoBuffer; +@property (nonatomic, copy) RCTDirectEventBlock onVideoError; +@property (nonatomic, copy) RCTDirectEventBlock onVideoProgress; +@property (nonatomic, copy) RCTDirectEventBlock onBandwidthUpdate; +@property (nonatomic, copy) RCTDirectEventBlock onVideoSeek; +@property (nonatomic, copy) RCTDirectEventBlock onVideoEnd; +@property (nonatomic, copy) RCTDirectEventBlock onTimedMetadata; +@property (nonatomic, copy) RCTDirectEventBlock onVideoAudioBecomingNoisy; +@property (nonatomic, copy) RCTDirectEventBlock onVideoFullscreenPlayerWillPresent; +@property (nonatomic, copy) RCTDirectEventBlock onVideoFullscreenPlayerDidPresent; +@property (nonatomic, copy) RCTDirectEventBlock onVideoFullscreenPlayerWillDismiss; +@property (nonatomic, copy) RCTDirectEventBlock onVideoFullscreenPlayerDidDismiss; +@property (nonatomic, copy) RCTDirectEventBlock onReadyForDisplay; +@property (nonatomic, copy) RCTDirectEventBlock onPlaybackStalled; +@property (nonatomic, copy) RCTDirectEventBlock onPlaybackResume; +@property (nonatomic, copy) RCTDirectEventBlock onPlaybackRateChange; +@property (nonatomic, copy) RCTDirectEventBlock onVideoExternalPlaybackChange; +@property (nonatomic, copy) RCTDirectEventBlock onPictureInPictureStatusChanged; +@property (nonatomic, copy) RCTDirectEventBlock onRestoreUserInterfaceForPictureInPictureStop; - (instancetype)initWithEventDispatcher:(RCTEventDispatcher *)eventDispatcher NS_DESIGNATED_INITIALIZER; diff --git a/ios/Video/RCTVideo.m b/ios/Video/RCTVideo.m index dcadee85..71bf82d8 100644 --- a/ios/Video/RCTVideo.m +++ b/ios/Video/RCTVideo.m @@ -223,6 +223,7 @@ static int const RCTVideoUnset = -1; if (_playInBackground) { // Needed to play sound in background. See https://developer.apple.com/library/ios/qa/qa1668/_index.html [_playerLayer setPlayer:nil]; + [_playerViewController setPlayer:nil]; } } @@ -231,6 +232,7 @@ static int const RCTVideoUnset = -1; [self applyModifiers]; if (_playInBackground) { [_playerLayer setPlayer:_player]; + [_playerViewController setPlayer:_player]; } } @@ -354,8 +356,6 @@ static int const RCTVideoUnset = -1; [self setMaxBitRate:_maxBitRate]; [_player pause]; - [_playerViewController.view removeFromSuperview]; - _playerViewController = nil; if (_playbackRateObserverRegistered) { [_player removeObserver:self forKeyPath:playbackRate context:nil]; @@ -598,7 +598,10 @@ static int const RCTVideoUnset = -1; } 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 +693,6 @@ static int const RCTVideoUnset = -1; _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 +1280,9 @@ static int const RCTVideoUnset = -1; { 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 +1292,8 @@ static int const RCTVideoUnset = -1; [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 +1489,7 @@ static int const RCTVideoUnset = -1; [self removePlayerLayer]; [_playerViewController.contentOverlayView removeObserver:self forKeyPath:@"frame"]; + [_playerViewController removeObserver:self forKeyPath:readyForDisplayKeyPath]; [_playerViewController.view removeFromSuperview]; _playerViewController = nil; diff --git a/ios/Video/RCTVideoManager.m b/ios/Video/RCTVideoManager.m index a608f32e..1614eab1 100644 --- a/ios/Video/RCTVideoManager.m +++ b/ios/Video/RCTVideoManager.m @@ -45,25 +45,25 @@ RCT_EXPORT_VIEW_PROPERTY(filterEnabled, BOOL); RCT_EXPORT_VIEW_PROPERTY(progressUpdateInterval, float); RCT_EXPORT_VIEW_PROPERTY(restoreUserInterfaceForPIPStopCompletionHandler, BOOL); /* Should support: onLoadStart, onLoad, and onError to stay consistent with Image */ -RCT_EXPORT_VIEW_PROPERTY(onVideoLoadStart, RCTBubblingEventBlock); -RCT_EXPORT_VIEW_PROPERTY(onVideoLoad, RCTBubblingEventBlock); -RCT_EXPORT_VIEW_PROPERTY(onVideoBuffer, RCTBubblingEventBlock); -RCT_EXPORT_VIEW_PROPERTY(onVideoError, RCTBubblingEventBlock); -RCT_EXPORT_VIEW_PROPERTY(onVideoProgress, RCTBubblingEventBlock); -RCT_EXPORT_VIEW_PROPERTY(onBandwidthUpdate, RCTBubblingEventBlock); -RCT_EXPORT_VIEW_PROPERTY(onVideoSeek, RCTBubblingEventBlock); -RCT_EXPORT_VIEW_PROPERTY(onVideoEnd, RCTBubblingEventBlock); -RCT_EXPORT_VIEW_PROPERTY(onTimedMetadata, RCTBubblingEventBlock); -RCT_EXPORT_VIEW_PROPERTY(onVideoAudioBecomingNoisy, RCTBubblingEventBlock); -RCT_EXPORT_VIEW_PROPERTY(onVideoFullscreenPlayerWillPresent, RCTBubblingEventBlock); -RCT_EXPORT_VIEW_PROPERTY(onVideoFullscreenPlayerDidPresent, RCTBubblingEventBlock); -RCT_EXPORT_VIEW_PROPERTY(onVideoFullscreenPlayerWillDismiss, RCTBubblingEventBlock); -RCT_EXPORT_VIEW_PROPERTY(onVideoFullscreenPlayerDidDismiss, RCTBubblingEventBlock); -RCT_EXPORT_VIEW_PROPERTY(onReadyForDisplay, RCTBubblingEventBlock); -RCT_EXPORT_VIEW_PROPERTY(onPlaybackStalled, RCTBubblingEventBlock); -RCT_EXPORT_VIEW_PROPERTY(onPlaybackResume, RCTBubblingEventBlock); -RCT_EXPORT_VIEW_PROPERTY(onPlaybackRateChange, RCTBubblingEventBlock); -RCT_EXPORT_VIEW_PROPERTY(onVideoExternalPlaybackChange, RCTBubblingEventBlock); +RCT_EXPORT_VIEW_PROPERTY(onVideoLoadStart, RCTDirectEventBlock); +RCT_EXPORT_VIEW_PROPERTY(onVideoLoad, RCTDirectEventBlock); +RCT_EXPORT_VIEW_PROPERTY(onVideoBuffer, RCTDirectEventBlock); +RCT_EXPORT_VIEW_PROPERTY(onVideoError, RCTDirectEventBlock); +RCT_EXPORT_VIEW_PROPERTY(onVideoProgress, RCTDirectEventBlock); +RCT_EXPORT_VIEW_PROPERTY(onBandwidthUpdate, RCTDirectEventBlock); +RCT_EXPORT_VIEW_PROPERTY(onVideoSeek, RCTDirectEventBlock); +RCT_EXPORT_VIEW_PROPERTY(onVideoEnd, RCTDirectEventBlock); +RCT_EXPORT_VIEW_PROPERTY(onTimedMetadata, RCTDirectEventBlock); +RCT_EXPORT_VIEW_PROPERTY(onVideoAudioBecomingNoisy, RCTDirectEventBlock); +RCT_EXPORT_VIEW_PROPERTY(onVideoFullscreenPlayerWillPresent, RCTDirectEventBlock); +RCT_EXPORT_VIEW_PROPERTY(onVideoFullscreenPlayerDidPresent, RCTDirectEventBlock); +RCT_EXPORT_VIEW_PROPERTY(onVideoFullscreenPlayerWillDismiss, RCTDirectEventBlock); +RCT_EXPORT_VIEW_PROPERTY(onVideoFullscreenPlayerDidDismiss, RCTDirectEventBlock); +RCT_EXPORT_VIEW_PROPERTY(onReadyForDisplay, RCTDirectEventBlock); +RCT_EXPORT_VIEW_PROPERTY(onPlaybackStalled, RCTDirectEventBlock); +RCT_EXPORT_VIEW_PROPERTY(onPlaybackResume, RCTDirectEventBlock); +RCT_EXPORT_VIEW_PROPERTY(onPlaybackRateChange, RCTDirectEventBlock); +RCT_EXPORT_VIEW_PROPERTY(onVideoExternalPlaybackChange, RCTDirectEventBlock); RCT_REMAP_METHOD(save, options:(NSDictionary *)options reactTag:(nonnull NSNumber *)reactTag @@ -79,8 +79,8 @@ RCT_REMAP_METHOD(save, } }]; } -RCT_EXPORT_VIEW_PROPERTY(onPictureInPictureStatusChanged, RCTBubblingEventBlock); -RCT_EXPORT_VIEW_PROPERTY(onRestoreUserInterfaceForPictureInPictureStop, RCTBubblingEventBlock); +RCT_EXPORT_VIEW_PROPERTY(onPictureInPictureStatusChanged, RCTDirectEventBlock); +RCT_EXPORT_VIEW_PROPERTY(onRestoreUserInterfaceForPictureInPictureStop, RCTDirectEventBlock); - (NSDictionary *)constantsToExport { diff --git a/package.json b/package.json index 8e5e3ddc..d86ed89f 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "react-native-video", - "version": "4.3.1", + "version": "4.4.2", "description": "A