diff --git a/CHANGELOG.md b/CHANGELOG.md index a91fd638..a7e9c1c6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,7 +1,22 @@ ## Changelog -### next +### Version 4.4.4 +* Handle racing conditions when props are setted on exoplayer + +### Version 4.4.3 +* Fix mute/unmute when controls are present (iOS) [#1654](https://github.com/react-native-community/react-native-video/pull/1654) +* Fix Android videos being able to play with background music/audio from other apps. +* Fixed memory leak on iOS when using `controls` [#1647](https://github.com/react-native-community/react-native-video/pull/1647) +* (Android) Update gradle and target SDK [#1629](https://github.com/react-native-community/react-native-video/pull/1629) +* Fix iOS stressed mount/unmount crash [#1646](https://github.com/react-native-community/react-native-video/pull/1646) + +### 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) diff --git a/README.md b/README.md index 20c4f9e7..2c40b188 100644 --- a/README.md +++ b/README.md @@ -130,11 +130,11 @@ From version >= 5.0.0, you have to apply this changes: dependencies { ... compile project(':react-native-video') -+ implementation "androidx.appcompat:appcompat:1.0.0" -- implementation "com.android.support:appcompat-v7:${rootProject.ext.supportLibVersion}" + implementation "com.android.support:appcompat-v7:${rootProject.ext.supportLibVersion}" } ``` + #### **android/gradle.properties** Migrating to AndroidX (needs version >= 5.0.0): @@ -320,6 +320,7 @@ var styles = StyleSheet.create({ * [onFullscreenPlayerDidDismiss](#onfullscreenplayerdiddismiss) * [onLoad](#onload) * [onLoadStart](#onloadstart) +* [onReadyForDisplay](#onreadyfordisplay) * [onPictureInPictureStatusChanged](#onpictureinpicturestatuschanged) * [onPlaybackRateChange](#onplaybackratechange) * [onProgress](#onprogress) @@ -387,6 +388,13 @@ For Android MediaPlayer, you will need to build your own controls or use a packa Platforms: Android ExoPlayer, iOS, react-native-dom +#### disableFocus +Determines whether video audio should override background music/audio in Android devices. +* ** false (default)** - Override background audio/music +* **true** - Let background audio/music from other apps play + +Platforms: Android Exoplayer + #### filter Add video filter * **FilterType.NONE (default)** - No Filter @@ -972,6 +980,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 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 5290c88c..5baf953a 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/ExoPlayerView.java b/android-exoplayer/src/main/java/com/brentvatne/exoplayer/ExoPlayerView.java index 9f3d09be..fc966005 100644 --- a/android-exoplayer/src/main/java/com/brentvatne/exoplayer/ExoPlayerView.java +++ b/android-exoplayer/src/main/java/com/brentvatne/exoplayer/ExoPlayerView.java @@ -2,7 +2,7 @@ package com.brentvatne.exoplayer; import android.annotation.TargetApi; import android.content.Context; -import androidx.core.content.ContextCompat; +import android.support.v4.content.ContextCompat; import android.util.AttributeSet; import android.util.Log; import android.view.Gravity; 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 175ef84f..1edb8c7a 100644 --- a/android-exoplayer/src/main/java/com/brentvatne/exoplayer/ReactExoplayerView.java +++ b/android-exoplayer/src/main/java/com/brentvatne/exoplayer/ReactExoplayerView.java @@ -144,6 +144,7 @@ class ReactExoplayerView extends FrameLayout implements private boolean playInBackground = false; private Map requestHeaders; private boolean mReportBandwidth = false; + private boolean controls; // \ End props // React @@ -267,6 +268,7 @@ class ReactExoplayerView extends FrameLayout implements * Toggling the visibility of the player control view */ private void togglePlayerControlVisibility() { + if(player == null) return; reLayout(playerControlView); if (playerControlView.isVisible()) { playerControlView.hide(); @@ -312,10 +314,15 @@ class ReactExoplayerView extends FrameLayout implements * Adding Player control to the frame layout */ private void addPlayerControl() { + if(player == null) return; LayoutParams layoutParams = new LayoutParams( LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT); playerControlView.setLayoutParams(layoutParams); + int indexOfPC = indexOfChild(playerControlView); + if (indexOfPC != -1) { + removeViewAt(indexOfPC); + } addView(playerControlView, 1, layoutParams); } @@ -333,53 +340,62 @@ class ReactExoplayerView extends FrameLayout implements } private void initializePlayer() { - if (player == null) { - TrackSelection.Factory videoTrackSelectionFactory = new AdaptiveTrackSelection.Factory(BANDWIDTH_METER); - trackSelector = new DefaultTrackSelector(videoTrackSelectionFactory); - trackSelector.setParameters(trackSelector.buildUponParameters() + ReactExoplayerView self = this; + // This ensures all props have been setted, to avoid async racing conditions. + new Handler().postDelayed(new Runnable() { + @Override + public void run() { + if (player == null) { + TrackSelection.Factory videoTrackSelectionFactory = new AdaptiveTrackSelection.Factory(BANDWIDTH_METER); + trackSelector = new DefaultTrackSelector(videoTrackSelectionFactory); + trackSelector.setParameters(trackSelector.buildUponParameters() .setMaxVideoBitrate(maxBitRate == 0 ? Integer.MAX_VALUE : maxBitRate)); - DefaultAllocator allocator = new DefaultAllocator(true, C.DEFAULT_BUFFER_SEGMENT_SIZE); - DefaultLoadControl defaultLoadControl = new DefaultLoadControl(allocator, minBufferMs, maxBufferMs, bufferForPlaybackMs, bufferForPlaybackAfterRebufferMs, -1, true); - player = ExoPlayerFactory.newSimpleInstance(getContext(), trackSelector, defaultLoadControl); - player.addListener(this); - player.setMetadataOutput(this); - exoPlayerView.setPlayer(player); - audioBecomingNoisyReceiver.setListener(this); - BANDWIDTH_METER.addEventListener(new Handler(), this); - setPlayWhenReady(!isPaused); - playerNeedsSource = true; + DefaultAllocator allocator = new DefaultAllocator(true, C.DEFAULT_BUFFER_SEGMENT_SIZE); + DefaultLoadControl defaultLoadControl = new DefaultLoadControl(allocator, minBufferMs, maxBufferMs, bufferForPlaybackMs, bufferForPlaybackAfterRebufferMs, -1, true); + player = ExoPlayerFactory.newSimpleInstance(getContext(), trackSelector, defaultLoadControl); + player.addListener(self); + player.setMetadataOutput(self); + exoPlayerView.setPlayer(player); + audioBecomingNoisyReceiver.setListener(self); + BANDWIDTH_METER.addEventListener(new Handler(), self); + setPlayWhenReady(!isPaused); + playerNeedsSource = true; - PlaybackParameters params = new PlaybackParameters(rate, 1f); - player.setPlaybackParameters(params); - } - if (playerNeedsSource && srcUri != null) { - ArrayList mediaSourceList = buildTextSources(); - MediaSource videoSource = buildMediaSource(srcUri, extension); - MediaSource mediaSource; - if (mediaSourceList.size() == 0) { - mediaSource = videoSource; - } else { - mediaSourceList.add(0, videoSource); - MediaSource[] textSourceArray = mediaSourceList.toArray( - new MediaSource[mediaSourceList.size()] - ); - mediaSource = new MergingMediaSource(textSourceArray); + PlaybackParameters params = new PlaybackParameters(rate, 1f); + player.setPlaybackParameters(params); + } + if (playerNeedsSource && srcUri != null) { + ArrayList mediaSourceList = buildTextSources(); + MediaSource videoSource = buildMediaSource(srcUri, extension); + MediaSource mediaSource; + if (mediaSourceList.size() == 0) { + mediaSource = videoSource; + } else { + mediaSourceList.add(0, videoSource); + MediaSource[] textSourceArray = mediaSourceList.toArray( + new MediaSource[mediaSourceList.size()] + ); + mediaSource = new MergingMediaSource(textSourceArray); + } + + boolean haveResumePosition = resumeWindow != C.INDEX_UNSET; + if (haveResumePosition) { + player.seekTo(resumeWindow, resumePosition); + } + player.prepare(mediaSource, !haveResumePosition, false); + playerNeedsSource = false; + + eventEmitter.loadStart(); + loadVideoStarted = true; + } + + // Initializing the playerControlView + initializePlayerControl(); + setControls(controls); + applyModifiers(); } - - boolean haveResumePosition = resumeWindow != C.INDEX_UNSET; - if (haveResumePosition) { - player.seekTo(resumeWindow, resumePosition); - } - player.prepare(mediaSource, !haveResumePosition, false); - playerNeedsSource = false; - - eventEmitter.loadStart(); - loadVideoStarted = true; - } - - // Initializing the playerControlView - initializePlayerControl(); + }, 1); } private MediaSource buildMediaSource(Uri uri, String overrideExtension) { @@ -439,8 +455,8 @@ class ReactExoplayerView extends FrameLayout implements updateResumePosition(); player.release(); player.setMetadataOutput(null); - player = null; trackSelector = null; + player = null; } progressHandler.removeMessages(SHOW_PROGRESS); themedReactContext.removeLifecycleEventListener(this); @@ -449,7 +465,7 @@ class ReactExoplayerView extends FrameLayout implements } private boolean requestAudioFocus() { - if (disableFocus) { + if (disableFocus || srcUri == null) { return true; } int result = audioManager.requestAudioFocus(this, @@ -894,6 +910,10 @@ class ReactExoplayerView extends FrameLayout implements exoPlayerView.setResizeMode(resizeMode); } + private void applyModifiers() { + setRepeatModifier(repeat); + } + public void setRepeatModifier(boolean repeat) { if (player != null) { if (repeat) { @@ -906,6 +926,7 @@ class ReactExoplayerView extends FrameLayout implements } public void setSelectedTrack(int trackType, String type, Dynamic value) { + if (player == null) return; int rendererIndex = getTrackRendererIndex(trackType); if (rendererIndex == C.INDEX_UNSET) { return; @@ -1156,10 +1177,15 @@ class ReactExoplayerView extends FrameLayout implements * @param controls Controls prop, if true enable controls, if false disable them */ public void setControls(boolean controls) { - if (controls && exoPlayerView != null) { + this.controls = controls; + if (player == null || exoPlayerView == null) return; + if (controls) { addPlayerControl(); - } else if (getChildAt(1) instanceof PlayerControlView && exoPlayerView != null) { - removeViewAt(1); + } else { + int indexOfPC = indexOfChild(playerControlView); + if (indexOfPC != -1) { + removeViewAt(indexOfPC); + } } } } diff --git a/android-exoplayer/src/main/java/com/brentvatne/exoplayer/ResizeMode.java b/android-exoplayer/src/main/java/com/brentvatne/exoplayer/ResizeMode.java index ceea420c..002c32ad 100644 --- a/android-exoplayer/src/main/java/com/brentvatne/exoplayer/ResizeMode.java +++ b/android-exoplayer/src/main/java/com/brentvatne/exoplayer/ResizeMode.java @@ -1,6 +1,6 @@ package com.brentvatne.exoplayer; -import androidx.annotation.IntDef; +import android.support.annotation.IntDef; import java.lang.annotation.Retention; diff --git a/android-exoplayer/src/main/java/com/brentvatne/exoplayer/VideoEventEmitter.java b/android-exoplayer/src/main/java/com/brentvatne/exoplayer/VideoEventEmitter.java index 24d51e02..00f51c94 100644 --- a/android-exoplayer/src/main/java/com/brentvatne/exoplayer/VideoEventEmitter.java +++ b/android-exoplayer/src/main/java/com/brentvatne/exoplayer/VideoEventEmitter.java @@ -1,6 +1,6 @@ package com.brentvatne.exoplayer; -import androidx.annotation.StringDef; +import android.support.annotation.StringDef; import android.view.View; import com.facebook.react.bridge.Arguments; diff --git a/android/build.gradle b/android/build.gradle index fbe5fad6..2fb8dfd2 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/examples/basic/android/gradle.properties b/examples/basic/android/gradle.properties index e77af9f2..1fd964e9 100644 --- a/examples/basic/android/gradle.properties +++ b/examples/basic/android/gradle.properties @@ -18,5 +18,3 @@ # org.gradle.parallel=true android.useDeprecatedNdk=true -android.useAndroidX=true -android.enableJetifier=true \ No newline at end of file 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..4710afaa 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]; @@ -578,27 +578,11 @@ static int const RCTVideoUnset = -1; - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context { - // when controls==true, this is a hack to reset the rootview when rotation happens in fullscreen - if (object == _playerViewController.contentOverlayView) { - if ([keyPath isEqualToString:@"frame"]) { - - CGRect oldRect = [change[NSKeyValueChangeOldKey] CGRectValue]; - CGRect newRect = [change[NSKeyValueChangeNewKey] CGRectValue]; - - if (!CGRectEqualToRect(oldRect, newRect)) { - if (CGRectEqualToRect(newRect, [UIScreen mainScreen].bounds)) { - NSLog(@"in fullscreen"); - } else NSLog(@"not fullscreen"); - - [self.reactViewController.view setFrame:[UIScreen mainScreen].bounds]; - [self.reactViewController.view setNeedsLayout]; - } - - return; - } 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 +674,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) { @@ -716,7 +694,25 @@ static int const RCTVideoUnset = -1; @"target": self.reactTag}); } } - } else { + } else if (object == _playerViewController.contentOverlayView) { + // when controls==true, this is a hack to reset the rootview when rotation happens in fullscreen + if ([keyPath isEqualToString:@"frame"]) { + + CGRect oldRect = [change[NSKeyValueChangeOldKey] CGRectValue]; + CGRect newRect = [change[NSKeyValueChangeNewKey] CGRectValue]; + + if (!CGRectEqualToRect(oldRect, newRect)) { + if (CGRectEqualToRect(newRect, [UIScreen mainScreen].bounds)) { + NSLog(@"in fullscreen"); + } else NSLog(@"not fullscreen"); + + [self.reactViewController.view setFrame:[UIScreen mainScreen].bounds]; + [self.reactViewController.view setNeedsLayout]; + } + + return; + } + } else if ([super respondsToSelector:@selector(observeValueForKeyPath:ofObject:change:context:)]) { [super observeValueForKeyPath:keyPath ofObject:object change:change context:context]; } } @@ -961,7 +957,9 @@ static int const RCTVideoUnset = -1; - (void)applyModifiers { if (_muted) { - [_player setVolume:0]; + if (!_controls) { + [_player setVolume:0]; + } [_player setMuted:YES]; } else { [_player setVolume:_volume]; @@ -1283,7 +1281,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 +1293,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,7 +1490,10 @@ static int const RCTVideoUnset = -1; [self removePlayerLayer]; [_playerViewController.contentOverlayView removeObserver:self forKeyPath:@"frame"]; + [_playerViewController removeObserver:self forKeyPath:readyForDisplayKeyPath]; [_playerViewController.view removeFromSuperview]; + _playerViewController.rctDelegate = nil; + _playerViewController.player = nil; _playerViewController = nil; [self removePlayerTimeObserver]; 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 7b33428a..b1af1564 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "react-native-video", - "version": "4.4.1", + "version": "4.4.4", "description": "A