diff --git a/API.md b/API.md index d927a3b2ce..d8772410c2 100644 --- a/API.md +++ b/API.md @@ -74,6 +74,17 @@ Video with caching ([more info](docs/caching.md)): end ``` +#### Enable custom feature in podfile file + +##### Google IMA + +Google IMA is the google SDK to support Client Side Ads Integration (CSAI), see [google documentation](https://developers.google.com/interactive-media-ads/docs/sdks/ios/client-side) for more informations. + +To enable google IMA usage define add following line in your podfile: +```podfile +$RNVideoUseGoogleIMA=true +``` + ### tvOS installation @@ -320,6 +331,7 @@ var styles = StyleSheet.create({ | [selectedAudioTrack](#selectedaudiotrack) | Android, iOS | | [selectedTextTrack](#selectedtexttrack) | Android, iOS | | [selectedVideoTrack](#selectedvideotrack) | Android | +| [shutterColor](#shutterColor) | Android | | [source](#source) | All | | [subtitleStyle](#subtitleStyle) | Android | | [textTracks](#texttracks) | Android, iOS | @@ -841,6 +853,17 @@ If a track matching the specified Type (and Value if appropriate) is unavailable Platforms: Android +#### shutterColor +Apply color to shutter view, if you see black flashes before video start then set + +``` +shutterColor='transparent' +``` + +- black (default) + +Platforms: Android + #### source Sets the media source. You can pass an asset loaded via require or an object with a uri. @@ -916,6 +939,21 @@ The following other types are supported on some platforms, but aren't fully docu `content://, ms-appx://, ms-appdata://, assets-library://` +##### Playing only a portion of the video (start & end time) + +Provide an optional `startTime` and/or `endTime` for the video. Value is in milliseconds. Useful when you want to play only a portion of a large video. + +Example +``` +source={{ uri: 'https://bitdash-a.akamaihd.net/content/sintel/hls/playlist.m3u8', startTime: 36012, endTime: 48500 }} + +source={{ uri: 'https://bitdash-a.akamaihd.net/content/sintel/hls/playlist.m3u8', startTime: 36012 }} + +source={{ uri: 'https://bitdash-a.akamaihd.net/content/sintel/hls/playlist.m3u8', endTime: 48500 }} +``` + +Platforms: iOS, Android + #### subtitleStyle Property | Description | Platforms diff --git a/CHANGELOG.md b/CHANGELOG.md index 71b54f1c63..ff6d367d3a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,20 @@ ## Changelog +### Version 6.0.0-alpha.7 +- All: clean JS warnings (https://github.com/react-native-video/react-native-video/pull/3183) +- Android: Add shutterView color configurtion (https://github.com/react-native-video/react-native-video/pull/3179) +- Android: React native 0.73 support (https://github.com/react-native-video/react-native-video/pull/3163) +- Android: Fix memory leaks from AudioManager [#3123](https://github.com/react-native-video/react-native-video/pull/3123) +- Android: Fixed syntax error [#3182](https://github.com/react-native-video/react-native-video/issues/3182) +- iOS: Fix freeze at playback startup (https://github.com/react-native-video/react-native-video/pull/3173) +- iOS: Various safety checks (https://github.com/react-native-video/react-native-video/pull/3168) + +### Version 6.0.0-alpha.6 +- Feature: Video range support [#3030](https://github.com/react-native-video/react-native-video/pull/3030) +- iOS: remove undocumented `currentTime` property [#3064](https://github.com/react-native-video/react-native-video/pull/3064) +- iOS: make sure that the audio in ads is muted when the player is muted. [#3068](https://github.com/react-native-video/react-native-video/pull/3077) +- iOS: make IMA build optionnal + ### Version 6.0.0-alpha.5 - iOS: ensure controls are not displayed when disabled by user [#3017](https://github.com/react-native-video/react-native-video/pull/3017) diff --git a/Video.js b/Video.js index 49c4506ba2..f88c27697e 100644 --- a/Video.js +++ b/Video.js @@ -1,11 +1,11 @@ -import { ImagePropTypes, ViewPropTypes } from 'deprecated-react-native-prop-types'; -import PropTypes from 'prop-types'; import React, { Component } from 'react'; -import { findNodeHandle, Image, NativeModules, Platform, requireNativeComponent, StyleSheet, UIManager, View } from 'react-native'; +import PropTypes from 'prop-types'; +import { StyleSheet, requireNativeComponent, NativeModules, UIManager, View, Image, Platform, findNodeHandle } from 'react-native'; +import { ViewPropTypes, ImagePropTypes } from 'deprecated-react-native-prop-types'; import resolveAssetSource from 'react-native/Libraries/Image/resolveAssetSource'; -import DRMType from './DRMType'; -import FilterType from './FilterType'; import TextTrackType from './TextTrackType'; +import FilterType from './FilterType'; +import DRMType from './DRMType'; import VideoResizeMode from './VideoResizeMode.js'; const styles = StyleSheet.create({ @@ -15,7 +15,7 @@ const styles = StyleSheet.create({ }); const { VideoDecoderProperties } = NativeModules -export { TextTrackType, FilterType, DRMType, VideoDecoderProperties }; +export { TextTrackType, FilterType, DRMType, VideoDecoderProperties } export default class Video extends Component { @@ -348,6 +348,8 @@ export default class Video extends Component { mainVer: source.mainVer || 0, patchVer: source.patchVer || 0, requestHeaders: source.headers ? this.stringsOnlyObject(source.headers) : {}, + startTime: source.startTime || 0, + endTime: source.endTime }, onVideoLoadStart: this._onLoadStart, onVideoPlaybackStateChanged: this._onPlaybackStateChanged, @@ -421,13 +423,6 @@ Video.propTypes = { FilterType.SEPIA, ]), filterEnabled: PropTypes.bool, - /* Native only */ - src: PropTypes.object, - seek: PropTypes.oneOfType([ - PropTypes.number, - PropTypes.object, - ]), - fullscreen: PropTypes.bool, onVideoLoadStart: PropTypes.func, onVideoLoad: PropTypes.func, onVideoBuffer: PropTypes.func, @@ -527,7 +522,6 @@ Video.propTypes = { disableBuffering: PropTypes.bool, controls: PropTypes.bool, audioOnly: PropTypes.bool, - currentTime: PropTypes.number, fullscreenAutorotate: PropTypes.bool, fullscreenOrientation: PropTypes.oneOf(['all', 'landscape', 'portrait']), progressUpdateInterval: PropTypes.number, @@ -541,6 +535,7 @@ Video.propTypes = { useTextureView: PropTypes.bool, useSecureView: PropTypes.bool, hideShutterView: PropTypes.bool, + shutterColor: PropTypes.string, onLoadStart: PropTypes.func, onPlaybackStateChanged: PropTypes.func, onLoad: PropTypes.func, @@ -565,24 +560,12 @@ Video.propTypes = { onAudioFocusChanged: PropTypes.func, onAudioBecomingNoisy: PropTypes.func, onPictureInPictureStatusChanged: PropTypes.func, - needsToRestoreUserInterfaceForPictureInPictureStop: PropTypes.func, onExternalPlaybackChange: PropTypes.func, adTagUrl: PropTypes.string, onReceiveAdEvent: PropTypes.func, /* Required by react-native */ - scaleX: PropTypes.number, - scaleY: PropTypes.number, - translateX: PropTypes.number, - translateY: PropTypes.number, - rotation: PropTypes.number, ...ViewPropTypes, }; -const RCTVideo = requireNativeComponent('RCTVideo', Video, { - nativeOnly: { - src: true, - seek: true, - fullscreen: true, - }, -}); +const RCTVideo = requireNativeComponent('RCTVideo'); diff --git a/android/build.gradle b/android/build.gradle index 0c3969c9ca..de702f92ed 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -17,6 +17,7 @@ def configStringPath = ( ).md5() android { + namespace 'com.brentvatne.react' compileSdkVersion safeExtGet('compileSdkVersion', 31) buildToolsVersion safeExtGet('buildToolsVersion', '30.0.2') diff --git a/android/src/main/java/com/brentvatne/exoplayer/ExoPlayerView.java b/android/src/main/java/com/brentvatne/exoplayer/ExoPlayerView.java index 3b7df5b20b..e0d65f1dc9 100644 --- a/android/src/main/java/com/brentvatne/exoplayer/ExoPlayerView.java +++ b/android/src/main/java/com/brentvatne/exoplayer/ExoPlayerView.java @@ -116,6 +116,10 @@ public void setSubtitleStyle(SubtitleStyle style) { subtitleLayout.setPadding(style.getPaddingLeft(), style.getPaddingTop(), style.getPaddingRight(), style.getPaddingBottom()); } + public void setShutterColor(Integer color) { + shutterView.setBackgroundColor(color); + } + private void updateSurfaceView() { View view; if (!useTextureView || useSecureView) { diff --git a/android/src/main/java/com/brentvatne/exoplayer/ReactExoplayerView.java b/android/src/main/java/com/brentvatne/exoplayer/ReactExoplayerView.java index ed17929968..897d61d93b 100644 --- a/android/src/main/java/com/brentvatne/exoplayer/ReactExoplayerView.java +++ b/android/src/main/java/com/brentvatne/exoplayer/ReactExoplayerView.java @@ -4,6 +4,7 @@ import static com.google.android.exoplayer2.C.CONTENT_TYPE_HLS; import static com.google.android.exoplayer2.C.CONTENT_TYPE_OTHER; import static com.google.android.exoplayer2.C.CONTENT_TYPE_SS; +import static com.google.android.exoplayer2.C.TIME_END_OF_SOURCE; import android.annotation.SuppressLint; import android.app.Activity; @@ -93,6 +94,7 @@ import com.google.android.exoplayer2.ext.ima.ImaAdsLoader; import com.google.android.exoplayer2.source.ads.AdsMediaSource; import com.google.android.exoplayer2.source.DefaultMediaSourceFactory; +import com.google.android.exoplayer2.source.ClippingMediaSource; import com.google.common.collect.ImmutableList; import java.net.CookieHandler; @@ -117,7 +119,6 @@ class ReactExoplayerView extends FrameLayout implements Player.Listener, BandwidthMeter.EventListener, BecomingNoisyListener, - AudioManager.OnAudioFocusChangeListener, DrmSessionEventListener, AdEvent.AdEventListener { @@ -181,6 +182,8 @@ class ReactExoplayerView extends FrameLayout implements // Props from React private int backBufferDurationMs = DefaultLoadControl.DEFAULT_BACK_BUFFER_DURATION_MS; private Uri srcUri; + private long startTimeMs = -1; + private long endTimeMs = -1; private String extension; private boolean repeat; private String audioTrackType; @@ -211,6 +214,7 @@ class ReactExoplayerView extends FrameLayout implements private final ThemedReactContext themedReactContext; private final AudioManager audioManager; private final AudioBecomingNoisyReceiver audioBecomingNoisyReceiver; + private final AudioManager.OnAudioFocusChangeListener audioFocusChangeListener; // store last progress event values to avoid sending unnecessary messages private long lastPos = -1; @@ -266,6 +270,7 @@ public ReactExoplayerView(ThemedReactContext context, ReactExoplayerConfig confi audioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE); themedReactContext.addLifecycleEventListener(this); audioBecomingNoisyReceiver = new AudioBecomingNoisyReceiver(themedReactContext); + audioFocusChangeListener = new OnAudioFocusChangedListener(this); } private boolean isPlayingAd() { @@ -301,9 +306,7 @@ private void createViews() { @Override protected void onAttachedToWindow() { super.onAttachedToWindow(); - // uncommented `initializePlayer()`, because it caused video to restart when it is running - // behind another screen and the user navigated back to the screen where the video runs - // initializePlayer(); + initializePlayer(); } @Override @@ -671,7 +674,7 @@ private DrmSessionManager initializePlayerDrm(ReactExoplayerView self) { private void initializePlayerSource(ReactExoplayerView self, DrmSessionManager drmSessionManager) { ArrayList mediaSourceList = buildTextSources(); - MediaSource videoSource = buildMediaSource(self.srcUri, self.extension, drmSessionManager); + MediaSource videoSource = buildMediaSource(self.srcUri, self.extension, drmSessionManager, startTimeMs, endTimeMs); MediaSource mediaSourceWithAds = null; if (adTagUrl != null) { MediaSource.Factory mediaSourceFactory = new DefaultMediaSourceFactory(mediaDataSourceFactory) @@ -766,7 +769,7 @@ private DrmSessionManager buildDrmSessionManager(UUID uuid, String licenseUrl, S } } - private MediaSource buildMediaSource(Uri uri, String overrideExtension, DrmSessionManager drmSessionManager) { + private MediaSource buildMediaSource(Uri uri, String overrideExtension, DrmSessionManager drmSessionManager, long startTimeMs, long endTimeMs) { if (uri == null) { throw new IllegalStateException("Invalid video uri"); } @@ -783,7 +786,7 @@ private MediaSource buildMediaSource(Uri uri, String overrideExtension, DrmSessi } MediaItem mediaItem = mediaItemBuilder.build(); - + MediaSource mediaSource = null; DrmSessionManagerProvider drmProvider = null; if (drmSessionManager != null) { drmProvider = new DrmSessionManagerProvider() { @@ -797,39 +800,54 @@ public DrmSessionManager get(MediaItem mediaItem) { } switch (type) { case CONTENT_TYPE_SS: - return new SsMediaSource.Factory( + mediaSource = new SsMediaSource.Factory( new DefaultSsChunkSource.Factory(mediaDataSourceFactory), buildDataSourceFactory(false) ).setDrmSessionManagerProvider(drmProvider) .setLoadErrorHandlingPolicy( config.buildLoadErrorHandlingPolicy(minLoadRetryCount) ).createMediaSource(mediaItem); + break; case CONTENT_TYPE_DASH: - return new DashMediaSource.Factory( + mediaSource = new DashMediaSource.Factory( new DefaultDashChunkSource.Factory(mediaDataSourceFactory), buildDataSourceFactory(false) ).setDrmSessionManagerProvider(drmProvider) .setLoadErrorHandlingPolicy( config.buildLoadErrorHandlingPolicy(minLoadRetryCount) ).createMediaSource(mediaItem); + break; case CONTENT_TYPE_HLS: - return new HlsMediaSource.Factory( + mediaSource = new HlsMediaSource.Factory( mediaDataSourceFactory ).setDrmSessionManagerProvider(drmProvider) .setLoadErrorHandlingPolicy( config.buildLoadErrorHandlingPolicy(minLoadRetryCount) ).createMediaSource(mediaItem); + break; case CONTENT_TYPE_OTHER: - return new ProgressiveMediaSource.Factory( + mediaSource = new ProgressiveMediaSource.Factory( mediaDataSourceFactory ).setDrmSessionManagerProvider(drmProvider) .setLoadErrorHandlingPolicy( config.buildLoadErrorHandlingPolicy(minLoadRetryCount) ).createMediaSource(mediaItem); + break; default: { throw new IllegalStateException("Unsupported type: " + type); } } + + if (startTimeMs >= 0 && endTimeMs >= 0) + { + return new ClippingMediaSource(mediaSource, startTimeMs * 1000, endTimeMs * 1000); + } else if (startTimeMs >= 0) { + return new ClippingMediaSource(mediaSource, startTimeMs * 1000, TIME_END_OF_SOURCE); + } else if (endTimeMs >= 0) { + return new ClippingMediaSource(mediaSource, 0, endTimeMs * 1000); + } + + return mediaSource; } private ArrayList buildTextSources() { @@ -886,11 +904,54 @@ private void releasePlayer() { bandwidthMeter.removeEventListener(this); } + private static class OnAudioFocusChangedListener implements AudioManager.OnAudioFocusChangeListener { + private final ReactExoplayerView view; + + private OnAudioFocusChangedListener(ReactExoplayerView view) { + this.view = view; + } + + @Override + public void onAudioFocusChange(int focusChange) { + switch (focusChange) { + case AudioManager.AUDIOFOCUS_LOSS: + view.hasAudioFocus = false; + view.eventEmitter.audioFocusChanged(false); + view.pausePlayback(); + view.audioManager.abandonAudioFocus(this); + break; + case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT: + view.eventEmitter.audioFocusChanged(false); + break; + case AudioManager.AUDIOFOCUS_GAIN: + view.hasAudioFocus = true; + view.eventEmitter.audioFocusChanged(true); + break; + default: + break; + } + + if (view.player != null) { + if (focusChange == AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK) { + // Lower the volume + if (!view.muted) { + view.player.setVolume(view.audioVolume * 0.8f); + } + } else if (focusChange == AudioManager.AUDIOFOCUS_GAIN) { + // Raise it back to normal + if (!view.muted) { + view.player.setVolume(view.audioVolume * 1); + } + } + } + } + } + private boolean requestAudioFocus() { if (disableFocus || srcUri == null || this.hasAudioFocus) { return true; } - int result = audioManager.requestAudioFocus(this, + int result = audioManager.requestAudioFocus(audioFocusChangeListener, AudioManager.STREAM_MUSIC, AudioManager.AUDIOFOCUS_GAIN); return result == AudioManager.AUDIOFOCUS_REQUEST_GRANTED; @@ -956,7 +1017,7 @@ private void onStopPlayback() { if (isFullscreen) { setFullscreen(false); } - audioManager.abandonAudioFocus(this); + audioManager.abandonAudioFocus(audioFocusChangeListener); } private void updateResumePosition() { @@ -994,43 +1055,6 @@ private HttpDataSource.Factory buildHttpDataSourceFactory(boolean useBandwidthMe } - // AudioManager.OnAudioFocusChangeListener implementation - - @Override - public void onAudioFocusChange(int focusChange) { - switch (focusChange) { - case AudioManager.AUDIOFOCUS_LOSS: - this.hasAudioFocus = false; - eventEmitter.audioFocusChanged(false); - pausePlayback(); - audioManager.abandonAudioFocus(this); - break; - case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT: - eventEmitter.audioFocusChanged(false); - break; - case AudioManager.AUDIOFOCUS_GAIN: - this.hasAudioFocus = true; - eventEmitter.audioFocusChanged(true); - break; - default: - break; - } - - if (player != null) { - if (focusChange == AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK) { - // Lower the volume - if (!muted) { - player.setVolume(audioVolume * 0.8f); - } - } else if (focusChange == AudioManager.AUDIOFOCUS_GAIN) { - // Raise it back to normal - if (!muted) { - player.setVolume(audioVolume * 1); - } - } - } - } - // AudioBecomingNoisyListener implementation @Override @@ -1467,18 +1491,20 @@ public void onMetadata(Metadata metadata) { // ReactExoplayerViewManager public api - public void setSrc(final Uri uri, final String extension, Map headers) { + public void setSrc(final Uri uri, final long startTimeMs, final long endTimeMs, final String extension, Map headers) { if (uri != null) { - boolean isSourceEqual = uri.equals(srcUri); + boolean isSourceEqual = uri.equals(srcUri) && startTimeMs == this.startTimeMs && endTimeMs == this.endTimeMs; hasDrmFailed = false; this.srcUri = uri; + this.startTimeMs = startTimeMs; + this.endTimeMs = endTimeMs; this.extension = extension; this.requestHeaders = headers; this.mediaDataSourceFactory = DataSourceUtil.getDefaultDataSourceFactory(this.themedReactContext, bandwidthMeter, this.requestHeaders); - if (!isSourceEqual) { + if (!isSourceEqual) { Log.d("PLAYER", "reload1"); reloadSource(); } @@ -1490,6 +1516,8 @@ public void clearSrc() { player.stop(); player.clearMediaItems(); this.srcUri = null; + this.startTimeMs = -1; + this.endTimeMs = -1; this.extension = null; this.requestHeaders = null; this.mediaDataSourceFactory = null; @@ -2014,6 +2042,10 @@ public void setSubtitleStyle(SubtitleStyle style) { exoPlayerView.setSubtitleStyle(style); } + public void setShutterColor(Integer color) { + exoPlayerView.setShutterColor(color); + } + @Override public void onAdEvent(AdEvent adEvent) { eventEmitter.receiveAdEvent(adEvent.getType().name()); diff --git a/android/src/main/java/com/brentvatne/exoplayer/ReactExoplayerViewManager.java b/android/src/main/java/com/brentvatne/exoplayer/ReactExoplayerViewManager.java index 8da025e74e..ed810ca66a 100644 --- a/android/src/main/java/com/brentvatne/exoplayer/ReactExoplayerViewManager.java +++ b/android/src/main/java/com/brentvatne/exoplayer/ReactExoplayerViewManager.java @@ -1,9 +1,9 @@ package com.brentvatne.exoplayer; +import android.graphics.Color; import android.content.Context; import android.net.Uri; import android.text.TextUtils; -import android.util.Log; import com.facebook.react.bridge.Dynamic; import com.facebook.react.bridge.ReadableArray; @@ -13,7 +13,6 @@ import com.facebook.react.uimanager.ThemedReactContext; import com.facebook.react.uimanager.ViewGroupManager; import com.facebook.react.uimanager.annotations.ReactProp; -import com.facebook.react.bridge.ReactMethod; import com.google.android.exoplayer2.util.Util; import com.google.android.exoplayer2.DefaultLoadControl; import com.google.android.exoplayer2.upstream.RawResourceDataSource; @@ -28,9 +27,10 @@ public class ReactExoplayerViewManager extends ViewGroupManager { private static final String REACT_CLASS = "RCTVideo"; - private static final String PROP_SRC = "src"; private static final String PROP_SRC_URI = "uri"; + private static final String PROP_SRC_START_TIME = "startTime"; + private static final String PROP_SRC_END_TIME = "endTime"; private static final String PROP_AD_TAG_URL = "adTagUrl"; private static final String PROP_SRC_TYPE = "type"; private static final String PROP_DRM = "drm"; @@ -80,8 +80,8 @@ public class ReactExoplayerViewManager extends ViewGroupManager headers = src.hasKey(PROP_SRC_HEADERS) ? toStringMap(src.getMap(PROP_SRC_HEADERS)) : null; @@ -164,7 +166,7 @@ public void setSrc(final ReactExoplayerView videoView, @Nullable ReadableMap src Uri srcUri = Uri.parse(uriString); if (srcUri != null) { - videoView.setSrc(srcUri, extension, headers); + videoView.setSrc(srcUri, startTimeMs, endTimeMs, extension, headers); } } else { int identifier = context.getResources().getIdentifier( @@ -375,6 +377,11 @@ public void setSubtitleStyle(final ReactExoplayerView videoView, @Nullable final videoView.setSubtitleStyle(SubtitleStyle.parse(src)); } + @ReactProp(name = PROP_SHUTTER_COLOR, customType = "Color") + public void setShutterColor(final ReactExoplayerView videoView, final Integer color) { + videoView.setShutterColor(color == null ? Color.BLACK : color); + } + @ReactProp(name = PROP_BUFFER_CONFIG) public void setBufferConfig(final ReactExoplayerView videoView, @Nullable ReadableMap bufferConfig) { int minBufferMs = DefaultLoadControl.DEFAULT_MIN_BUFFER_MS; diff --git a/android/src/main/res/layout/exo_player_control_view.xml b/android/src/main/res/layout/exo_player_control_view.xml index 19440912d3..27d3883e85 100644 --- a/android/src/main/res/layout/exo_player_control_view.xml +++ b/android/src/main/res/layout/exo_player_control_view.xml @@ -14,27 +14,27 @@ android:paddingTop="4dp" android:orientation="horizontal"> - - - - - - @@ -46,7 +46,7 @@ android:gravity="center_vertical" android:orientation="horizontal"> - - false target 'videoplayer' do config = use_native_modules! + # $RNVideoUseGoogleIMA = true + # Flags change depending on the env values. flags = get_default_flags() diff --git a/ios/Video/DataStructures/VideoSource.swift b/ios/Video/DataStructures/VideoSource.swift index 20ab7158d4..7cb7c38bc2 100644 --- a/ios/Video/DataStructures/VideoSource.swift +++ b/ios/Video/DataStructures/VideoSource.swift @@ -6,6 +6,8 @@ struct VideoSource { let isAsset: Bool let shouldCache: Bool let requestHeaders: Dictionary? + let startTime: Int64? + let endTime: Int64? let json: NSDictionary? @@ -18,6 +20,8 @@ struct VideoSource { self.isAsset = false self.shouldCache = false self.requestHeaders = nil + self.startTime = nil + self.endTime = nil return } self.json = json @@ -27,5 +31,7 @@ struct VideoSource { self.isAsset = json["isAsset"] as? Bool ?? false self.shouldCache = json["shouldCache"] as? Bool ?? false self.requestHeaders = json["requestHeaders"] as? Dictionary + self.startTime = json["startTime"] as? Int64 + self.endTime = json["endTime"] as? Int64 } } diff --git a/ios/Video/Features/RCTIMAAdsManager.swift b/ios/Video/Features/RCTIMAAdsManager.swift index 059ec6399e..d069af72b6 100644 --- a/ios/Video/Features/RCTIMAAdsManager.swift +++ b/ios/Video/Features/RCTIMAAdsManager.swift @@ -1,9 +1,10 @@ +#if USE_GOOGLE_IMA import Foundation import GoogleInteractiveMediaAds class RCTIMAAdsManager: NSObject, IMAAdsLoaderDelegate, IMAAdsManagerDelegate { - private var _video:RCTVideo + private weak var _video: RCTVideo? /* Entry point for the SDK. Used to make ad requests. */ private var adsLoader: IMAAdsLoader! @@ -22,6 +23,7 @@ class RCTIMAAdsManager: NSObject, IMAAdsLoaderDelegate, IMAAdsManagerDelegate { } func requestAds() { + guard let _video = _video else {return} // Create ad display container for ad rendering. let adDisplayContainer = IMAAdDisplayContainer(adContainer: _video, viewController: _video.reactViewController()) @@ -53,6 +55,7 @@ class RCTIMAAdsManager: NSObject, IMAAdsLoaderDelegate, IMAAdsManagerDelegate { // MARK: - IMAAdsLoaderDelegate func adsLoader(_ loader: IMAAdsLoader, adsLoadedWith adsLoadedData: IMAAdsLoadedData) { + guard let _video = _video else {return} // Grab the instance of the IMAAdsManager and set yourself as the delegate. adsManager = adsLoadedData.adsManager adsManager?.delegate = self @@ -70,12 +73,17 @@ class RCTIMAAdsManager: NSObject, IMAAdsLoaderDelegate, IMAAdsManagerDelegate { print("Error loading ads: " + adErrorData.adError.message!) } - _video.setPaused(false) + _video?.setPaused(false) } // MARK: - IMAAdsManagerDelegate func adsManager(_ adsManager: IMAAdsManager, didReceive event: IMAAdEvent) { + guard let _video = _video else {return} + // Mute ad if the main player is muted + if (_video.isMuted()) { + adsManager.volume = 0; + } // Play each ad once it has been loaded if event.type == IMAAdEventType.LOADED { adsManager.start() @@ -97,19 +105,19 @@ class RCTIMAAdsManager: NSObject, IMAAdsLoaderDelegate, IMAAdsManagerDelegate { } // Fall back to playing content - _video.setPaused(false) + _video?.setPaused(false) } func adsManagerDidRequestContentPause(_ adsManager: IMAAdsManager) { // Pause the content for the SDK to play ads. - _video.setPaused(true) - _video.setAdPlaying(true) + _video?.setPaused(true) + _video?.setAdPlaying(true) } func adsManagerDidRequestContentResume(_ adsManager: IMAAdsManager) { // Resume the content since the SDK is done playing ads (at least for now). - _video.setAdPlaying(false) - _video.setPaused(false) + _video?.setAdPlaying(false) + _video?.setPaused(false) } // MARK: - Helpers @@ -185,3 +193,4 @@ class RCTIMAAdsManager: NSObject, IMAAdsLoaderDelegate, IMAAdsManagerDelegate { return result; } } +#endif diff --git a/ios/Video/Features/RCTPlayerObserver.swift b/ios/Video/Features/RCTPlayerObserver.swift index 68fc56ddf3..9e798dc3b3 100644 --- a/ios/Video/Features/RCTPlayerObserver.swift +++ b/ios/Video/Features/RCTPlayerObserver.swift @@ -25,7 +25,7 @@ protocol RCTPlayerObserverHandler: RCTPlayerObserverHandlerObjc { class RCTPlayerObserver: NSObject { - var _handlers: RCTPlayerObserverHandler! + weak var _handlers: RCTPlayerObserverHandler? var player:AVPlayer? { willSet { @@ -84,11 +84,13 @@ class RCTPlayerObserver: NSObject { private var _playerViewControllerOverlayFrameObserver:NSKeyValueObservation? deinit { - NotificationCenter.default.removeObserver(_handlers) + if let _handlers = _handlers { + NotificationCenter.default.removeObserver(_handlers) + } } func addPlayerObservers() { - guard let player = player else { + guard let player = player, let _handlers = _handlers else { return } @@ -102,7 +104,7 @@ class RCTPlayerObserver: NSObject { } func addPlayerItemObservers() { - guard let playerItem = playerItem else { return } + guard let playerItem = playerItem, let _handlers = _handlers else { return } _playerItemStatusObserver = playerItem.observe(\.status, options: [.new, .old], changeHandler: _handlers.handlePlayerItemStatusChange) _playerPlaybackBufferEmptyObserver = playerItem.observe(\.isPlaybackBufferEmpty, options: [.new, .old], changeHandler: _handlers.handlePlaybackBufferKeyEmpty) @@ -118,7 +120,7 @@ class RCTPlayerObserver: NSObject { } func addPlayerViewControllerObservers() { - guard let playerViewController = playerViewController else { return } + guard let playerViewController = playerViewController, let _handlers = _handlers else { return } _playerViewControllerReadyForDisplayObserver = playerViewController.observe(\.isReadyForDisplay, options: [.new], changeHandler: _handlers.handleReadyForDisplay) @@ -131,6 +133,7 @@ class RCTPlayerObserver: NSObject { } func addPlayerLayerObserver() { + guard let _handlers = _handlers else {return} _playerLayerReadyForDisplayObserver = playerLayer?.observe(\.isReadyForDisplay, options: [.new], changeHandler: _handlers.handleReadyForDisplay) } @@ -139,6 +142,7 @@ class RCTPlayerObserver: NSObject { } func addPlayerTimeObserver() { + guard let _handlers = _handlers else {return} removePlayerTimeObserver() let progressUpdateIntervalMS:Float64 = _progressUpdateInterval / 1000 // @see endScrubbing in AVPlayerDemoPlaybackViewController.m @@ -174,6 +178,7 @@ class RCTPlayerObserver: NSObject { } func attachPlayerEventListeners() { + guard let _handlers = _handlers else {return} NotificationCenter.default.removeObserver(_handlers, name:NSNotification.Name.AVPlayerItemDidPlayToEndTime, @@ -202,6 +207,8 @@ class RCTPlayerObserver: NSObject { func clearPlayer() { player = nil playerItem = nil - NotificationCenter.default.removeObserver(_handlers) + if let _handlers = _handlers { + NotificationCenter.default.removeObserver(_handlers) + } } } diff --git a/ios/Video/Features/RCTPlayerOperations.swift b/ios/Video/Features/RCTPlayerOperations.swift index 8b76fafc5f..a6156445f4 100644 --- a/ios/Video/Features/RCTPlayerOperations.swift +++ b/ios/Video/Features/RCTPlayerOperations.swift @@ -192,36 +192,51 @@ enum RCTPlayerOperations { } static func configureAudio(ignoreSilentSwitch:String, mixWithOthers:String) { - let session:AVAudioSession! = AVAudioSession.sharedInstance() + let audioSession:AVAudioSession! = AVAudioSession.sharedInstance() var category:AVAudioSession.Category? = nil var options:AVAudioSession.CategoryOptions? = nil - + if (ignoreSilentSwitch == "ignore") { category = AVAudioSession.Category.playback } else if (ignoreSilentSwitch == "obey") { category = AVAudioSession.Category.ambient } - + if (mixWithOthers == "mix") { options = .mixWithOthers } else if (mixWithOthers == "duck") { options = .duckOthers } - + if let category = category, let options = options { do { - try session.setCategory(category, options: options) + try audioSession.setCategory(category, options: options) } catch { + debugPrint("[RCTPlayerOperations] Problem setting up AVAudioSession category and options. Error: \(error).") + // Handle specific set category and option combination error + // setCategory:AVAudioSessionCategoryPlayback withOptions:mixWithOthers || duckOthers + // Failed to set category, error: 'what' Error Domain=NSOSStatusErrorDomain + // https://developer.apple.com/forums/thread/714598 + if #available(iOS 16.0, *) { + do { + debugPrint("[RCTPlayerOperations] Reseting AVAudioSession category to playAndRecord with defaultToSpeaker options.") + try audioSession.setCategory(AVAudioSession.Category.playAndRecord, options: AVAudioSession.CategoryOptions.defaultToSpeaker) + } catch { + debugPrint("[RCTPlayerOperations] Reseting AVAudioSession category and options problem. Error: \(error).") + } + } } } else if let category = category, options == nil { do { - try session.setCategory(category) + try audioSession.setCategory(category) } catch { + debugPrint("[RCTPlayerOperations] Problem setting up AVAudioSession category. Error: \(error).") } } else if category == nil, let options = options { do { - try session.setCategory(session.category, options: options) + try audioSession.setCategory(audioSession.category, options: options) } catch { + debugPrint("[RCTPlayerOperations] Problem setting up AVAudioSession options. Error: \(error).") } } } diff --git a/ios/Video/Features/RCTVideoUtils.swift b/ios/Video/Features/RCTVideoUtils.swift index 3953c9d009..9916db3719 100644 --- a/ios/Video/Features/RCTVideoUtils.swift +++ b/ios/Video/Features/RCTVideoUtils.swift @@ -12,13 +12,17 @@ enum RCTVideoUtils { * * \returns The playable duration of the current player item in seconds. */ - static func calculatePlayableDuration(_ player:AVPlayer?) -> NSNumber { + static func calculatePlayableDuration(_ player:AVPlayer?, withSource source:VideoSource?) -> NSNumber { guard let player = player, let video:AVPlayerItem = player.currentItem, video.status == AVPlayerItem.Status.readyToPlay else { return 0 } + if (source?.startTime != nil && source?.endTime != nil) { + return NSNumber(value: (Float64(source?.endTime ?? 0) - Float64(source?.startTime ?? 0)) / 1000) + } + var effectiveTimeRange:CMTimeRange? for (_, value) in video.loadedTimeRanges.enumerated() { let timeRange:CMTimeRange = value.timeRangeValue @@ -31,6 +35,10 @@ enum RCTVideoUtils { if let effectiveTimeRange = effectiveTimeRange { let playableDuration:Float64 = CMTimeGetSeconds(CMTimeRangeGetEnd(effectiveTimeRange)) if playableDuration > 0 { + if (source?.startTime != nil) { + return NSNumber(value: (playableDuration - Float64(source?.startTime ?? 0) / 1000)) + } + return playableDuration as NSNumber } } diff --git a/ios/Video/RCTVideo-Bridging-Header.h b/ios/Video/RCTVideo-Bridging-Header.h index 77815e4e48..586eec1110 100644 --- a/ios/Video/RCTVideo-Bridging-Header.h +++ b/ios/Video/RCTVideo-Bridging-Header.h @@ -1,5 +1,6 @@ #import #import "RCTVideoSwiftLog.h" +#import "RCTEventDispatcher.h" #if __has_include() #import "RCTVideoCache.h" diff --git a/ios/Video/RCTVideo.swift b/ios/Video/RCTVideo.swift index 870f21d72e..a21fa81b89 100644 --- a/ios/Video/RCTVideo.swift +++ b/ios/Video/RCTVideo.swift @@ -1,7 +1,9 @@ import AVFoundation import AVKit import Foundation +#if USE_GOOGLE_IMA import GoogleInteractiveMediaAds +#endif import React import Promises @@ -14,7 +16,6 @@ class RCTVideo: UIView, RCTVideoPlayerViewControllerDelegate, RCTPlayerObserverH private var _playerLayer:AVPlayerLayer? private var _playerViewController:RCTVideoPlayerViewController? - private var _playerDelegate:RCTPlayerDelegate? private var _videoURL:NSURL? /* DRM */ @@ -58,17 +59,20 @@ class RCTVideo: UIView, RCTVideoPlayerViewControllerDelegate, RCTPlayerObserverH private var _fullscreenAutorotate:Bool = true private var _fullscreenOrientation:String! = "all" private var _fullscreenPlayerPresented:Bool = false + private var _fullscreenUncontrolPlayerPresented:Bool = false // to call events switching full screen mode from player controls private var _filterName:String! private var _filterEnabled:Bool = false private var _presentingViewController:UIViewController? /* IMA Ads */ private var _adTagUrl:String? +#if USE_GOOGLE_IMA private var _imaAdsManager: RCTIMAAdsManager! - private var _didRequestAds:Bool = false - private var _adPlaying:Bool = false /* Playhead used by the SDK to track content video progress and insert mid-rolls. */ private var _contentPlayhead: IMAAVPlayerContentPlayhead? +#endif + private var _didRequestAds:Bool = false + private var _adPlaying:Bool = false private var _resouceLoaderDelegate: RCTResourceLoaderDelegate? private var _playerObserver: RCTPlayerObserver = RCTPlayerObserver() @@ -109,8 +113,9 @@ class RCTVideo: UIView, RCTVideoPlayerViewControllerDelegate, RCTPlayerObserverH init(eventDispatcher:RCTEventDispatcher!) { super.init(frame: CGRect(x: 0, y: 0, width: 100, height: 100)) - +#if USE_GOOGLE_IMA _imaAdsManager = RCTIMAAdsManager(video: self) +#endif _eventDispatcher = eventDispatcher @@ -149,8 +154,9 @@ class RCTVideo: UIView, RCTVideoPlayerViewControllerDelegate, RCTPlayerObserverH required init?(coder aDecoder: NSCoder) { super.init(coder: aDecoder) - +#if USE_GOOGLE_IMA _imaAdsManager = RCTIMAAdsManager(video: self) +#endif } deinit { @@ -169,6 +175,11 @@ class RCTVideo: UIView, RCTVideoPlayerViewControllerDelegate, RCTPlayerObserverH } @objc func applicationDidEnterBackground(notification:NSNotification!) { + if _playInBackground { + // Needed to play sound in background. See https://developer.apple.com/library/ios/qa/qa1668/_index.html + _playerLayer?.player = nil + _playerViewController?.player = nil + } } @objc func applicationWillEnterForeground(notification:NSNotification!) { @@ -204,7 +215,10 @@ class RCTVideo: UIView, RCTVideoPlayerViewControllerDelegate, RCTPlayerObserverH return } - let currentTime = _player?.currentTime() + var currentTime = _player?.currentTime() + if (currentTime != nil && _source?.startTime != nil) { + currentTime = CMTimeSubtract(currentTime!, CMTimeMake(value: _source?.startTime ?? 0, timescale: 1000)) + } let currentPlaybackTime = _player?.currentItem?.currentDate() let duration = CMTimeGetSeconds(playerDuration) let currentTimeSecs = CMTimeGetSeconds(currentTime ?? .zero) @@ -214,13 +228,15 @@ class RCTVideo: UIView, RCTVideoPlayerViewControllerDelegate, RCTPlayerObserverH ]) if currentTimeSecs >= 0 { +#if USE_GOOGLE_IMA if !_didRequestAds && currentTimeSecs >= 0.0001 && _adTagUrl != nil { _imaAdsManager.requestAds() _didRequestAds = true } +#endif onVideoProgress?([ "currentTime": NSNumber(value: Float(currentTimeSecs)), - "playableDuration": RCTVideoUtils.calculatePlayableDuration(_player), + "playableDuration": RCTVideoUtils.calculatePlayableDuration(_player, withSource: _source), "atValue": NSNumber(value: currentTime?.value ?? .zero), "currentPlaybackTime": NSNumber(value: NSNumber(value: floor(currentPlaybackTime?.timeIntervalSince1970 ?? 0 * 1000)).int64Value), "target": reactTag, @@ -232,7 +248,8 @@ class RCTVideo: UIView, RCTVideoPlayerViewControllerDelegate, RCTPlayerObserverH // MARK: - Player and source @objc func setSrc(_ source:NSDictionary!) { - DispatchQueue.global(qos: .default).async { + DispatchQueue.global(qos: .default).async { [weak self] in + guard let self = self else {return} self._source = VideoSource(source) if (self._source?.uri == nil || self._source?.uri == "") { self._player?.replaceCurrentItem(with: nil) @@ -289,6 +306,7 @@ class RCTVideo: UIView, RCTVideoPlayerViewControllerDelegate, RCTPlayerObserverH self._playerItem = playerItem self._playerObserver.playerItem = self._playerItem self.setPreferredForwardBufferDuration(self._preferredForwardBufferDuration) + self.setPlaybackRange(playerItem, withVideoStart: self._source?.startTime, withVideoEnd: self._source?.endTime) self.setFilter(self._filterName) if let maxBitRate = self._maxBitRate { self._playerItem?.preferredPeakBitRate = Double(maxBitRate) @@ -304,13 +322,14 @@ class RCTVideo: UIView, RCTVideoPlayerViewControllerDelegate, RCTPlayerObserverH self.setAutomaticallyWaitsToMinimizeStalling(self._automaticallyWaitsToMinimizeStalling) } +#if USE_GOOGLE_IMA if self._adTagUrl != nil { // Set up your content playhead and contentComplete callback. self._contentPlayhead = IMAAVPlayerContentPlayhead(avPlayer: self._player!) self._imaAdsManager.setUpAdsLoader() } - +#endif //Perform on next run loop, otherwise onVideoLoadStart is nil self.onVideoLoadStart?([ "src": [ @@ -421,7 +440,9 @@ class RCTVideo: UIView, RCTVideoPlayerViewControllerDelegate, RCTPlayerObserverH func setPaused(_ paused:Bool) { if paused { if _adPlaying { +#if USE_GOOGLE_IMA _imaAdsManager.getAdsManager()?.pause() +#endif } else { _player?.pause() _player?.rate = 0.0 @@ -430,7 +451,9 @@ class RCTVideo: UIView, RCTVideoPlayerViewControllerDelegate, RCTPlayerObserverH RCTPlayerOperations.configureAudio(ignoreSilentSwitch:_ignoreSilentSwitch, mixWithOthers:_mixWithOthers) if _adPlaying { +#if USE_GOOGLE_IMA _imaAdsManager.getAdsManager()?.resume() +#endif } else { if #available(iOS 10.0, *), !_automaticallyWaitsToMinimizeStalling { _player?.playImmediately(atRate: _rate) @@ -445,15 +468,6 @@ class RCTVideo: UIView, RCTVideoPlayerViewControllerDelegate, RCTPlayerObserverH _paused = paused } - @objc - func setCurrentTime(_ currentTime:Float) { - let info:NSDictionary = [ - "time": NSNumber(value: currentTime), - "tolerance": NSNumber(value: 100) - ] - setSeek(info) - } - @objc func setSeek(_ info:NSDictionary!) { let seekTime:NSNumber! = info["time"] as! NSNumber @@ -493,6 +507,11 @@ class RCTVideo: UIView, RCTVideoPlayerViewControllerDelegate, RCTPlayerObserverH applyModifiers() } + @objc + func isMuted() -> Bool { + return _muted + } + @objc func setMuted(_ muted:Bool) { _muted = muted @@ -530,9 +549,25 @@ class RCTVideo: UIView, RCTVideoPlayerViewControllerDelegate, RCTPlayerObserverH // Fallback on earlier versions } } + + func setPlaybackRange(_ item:AVPlayerItem!, withVideoStart videoStart:Int64?, withVideoEnd videoEnd:Int64?) { + if (videoStart != nil) { + let start = CMTimeMake(value: videoStart!, timescale: 1000) + item.reversePlaybackEndTime = start + _pendingSeekTime = Float(CMTimeGetSeconds(start)) + _pendingSeek = true + } + if (videoEnd != nil) { + item.forwardPlaybackEndTime = CMTimeMake(value: videoEnd!, timescale: 1000) + } + } func applyModifiers() { + if let video = _player?.currentItem, + video == nil || video.status != AVPlayerItem.Status.readyToPlay { + return + } if _muted { if !_controls { _player?.volume = 0 @@ -638,7 +673,9 @@ class RCTVideo: UIView, RCTVideoPlayerViewControllerDelegate, RCTPlayerObserverH // prevents crash https://github.com/react-native-video/react-native-video/issues/3040 self._playerViewController?.removeFromParent() } - viewController.present(playerViewController, animated:true, completion:{ + + viewController.present(playerViewController, animated:true, completion:{ [weak self] in + guard let self = self else {return} self._playerViewController?.showsPlaybackControls = self._controls self._fullscreenPlayerPresented = fullscreen self._playerViewController?.autorotate = self._fullscreenAutorotate @@ -650,8 +687,8 @@ class RCTVideo: UIView, RCTVideoPlayerViewControllerDelegate, RCTPlayerObserverH } } else if !fullscreen && _fullscreenPlayerPresented, let _playerViewController = _playerViewController { self.videoPlayerViewControllerWillDismiss(playerViewController: _playerViewController) - _presentingViewController?.dismiss(animated: true, completion:{ - self.videoPlayerViewControllerDidDismiss(playerViewController: _playerViewController) + _presentingViewController?.dismiss(animated: true, completion:{[weak self] in + self?.videoPlayerViewControllerDidDismiss(playerViewController: _playerViewController) }) } } @@ -735,10 +772,6 @@ class RCTVideo: UIView, RCTVideoPlayerViewControllerDelegate, RCTPlayerObserverH { self.removePlayerLayer() self.usePlayerViewController() - if (_playerViewController !== nil) { - _playerDelegate = RCTPlayerDelegate(self.onPictureInPictureStatusChanged, self.onVideoFullscreenPlayerWillPresent, self.onVideoFullscreenPlayerWillDismiss, self.onVideoFullscreenPlayerDidDismiss) - _playerViewController!.delegate = _playerDelegate - } } else { @@ -831,11 +864,11 @@ class RCTVideo: UIView, RCTVideoPlayerViewControllerDelegate, RCTPlayerObserverH func setAdTagUrl(_ adTagUrl:String!) { _adTagUrl = adTagUrl } - +#if USE_GOOGLE_IMA func getContentPlayhead() -> IMAAVPlayerContentPlayhead? { return _contentPlayhead } - +#endif func setAdPlaying(_ adPlaying:Bool) { _adPlaying = adPlaying } @@ -1003,7 +1036,10 @@ class RCTVideo: UIView, RCTVideoPlayerViewControllerDelegate, RCTPlayerObserverH } if _pendingSeek { - setCurrentTime(_pendingSeekTime) + setSeek([ + "time": NSNumber(value: _pendingSeekTime), + "tolerance": NSNumber(value: 100) + ]) _pendingSeek = false } @@ -1082,14 +1118,27 @@ class RCTVideo: UIView, RCTVideoPlayerViewControllerDelegate, RCTPlayerObserverH let oldRect = change.oldValue let newRect = change.newValue if !oldRect!.equalTo(newRect!) { + // https://github.com/react-native-video/react-native-video/issues/3085#issuecomment-1557293391 if newRect!.equalTo(UIScreen.main.bounds) { - NSLog("in fullscreen") + RCTLog("in fullscreen") + if (!_fullscreenUncontrolPlayerPresented) { + _fullscreenUncontrolPlayerPresented = true; - self.reactViewController().view.frame = UIScreen.main.bounds - self.reactViewController().view.setNeedsLayout() + self.onVideoFullscreenPlayerWillPresent?(["target": self.reactTag as Any]) + self.onVideoFullscreenPlayerDidPresent?(["target": self.reactTag as Any]) + } } else { NSLog("not fullscreen") + if (_fullscreenUncontrolPlayerPresented) { + _fullscreenUncontrolPlayerPresented = false; + + self.onVideoFullscreenPlayerWillDismiss?(["target": self.reactTag as Any]) + self.onVideoFullscreenPlayerDidDismiss?(["target": self.reactTag as Any]) + } } + + self.reactViewController().view.frame = UIScreen.main.bounds + self.reactViewController().view.setNeedsLayout() } } @@ -1115,10 +1164,11 @@ class RCTVideo: UIView, RCTVideoPlayerViewControllerDelegate, RCTPlayerObserverH @objc func handlePlayerItemDidReachEnd(notification:NSNotification!) { onVideoEnd?(["target": reactTag as Any]) - +#if USE_GOOGLE_IMA if notification.object as? AVPlayerItem == _player?.currentItem { + _imaAdsManager.getAdsLoader()?.contentComplete() } - +#endif if _repeat { let item:AVPlayerItem! = notification.object as? AVPlayerItem item.seek(to: CMTime.zero, completionHandler: nil) diff --git a/ios/Video/RCTVideoManager.m b/ios/Video/RCTVideoManager.m index 03a93263aa..e1cea00034 100644 --- a/ios/Video/RCTVideoManager.m +++ b/ios/Video/RCTVideoManager.m @@ -27,7 +27,6 @@ @interface RCT_EXTERN_MODULE(RCTVideoManager, RCTViewManager) RCT_EXPORT_VIEW_PROPERTY(mixWithOthers, NSString); RCT_EXPORT_VIEW_PROPERTY(rate, float); RCT_EXPORT_VIEW_PROPERTY(seek, NSDictionary); -RCT_EXPORT_VIEW_PROPERTY(currentTime, float); RCT_EXPORT_VIEW_PROPERTY(fullscreen, BOOL); RCT_EXPORT_VIEW_PROPERTY(fullscreenAutorotate, BOOL); RCT_EXPORT_VIEW_PROPERTY(fullscreenOrientation, NSString); diff --git a/ios/Video/RCTVideoPlayerViewController.swift b/ios/Video/RCTVideoPlayerViewController.swift index c2319dd3a3..2b89cd6553 100644 --- a/ios/Video/RCTVideoPlayerViewController.swift +++ b/ios/Video/RCTVideoPlayerViewController.swift @@ -2,7 +2,7 @@ import AVKit class RCTVideoPlayerViewController: AVPlayerViewController { - var rctDelegate:RCTVideoPlayerViewControllerDelegate! + weak var rctDelegate: RCTVideoPlayerViewControllerDelegate? // Optional paramters var preferredOrientation:String? @@ -18,26 +18,15 @@ class RCTVideoPlayerViewController: AVPlayerViewController { return false } - override func viewDidLoad() { - super.viewDidLoad() - NotificationCenter.default.addObserver(self, selector: #selector(deviceOrientationDidChange), name: UIDevice.orientationDidChangeNotification, object: nil) - } - - deinit { - NotificationCenter.default.removeObserver(self, name: UIDevice.orientationDidChangeNotification, object: nil) - } - override func viewDidDisappear(_ animated: Bool) { super.viewDidDisappear(animated) - if rctDelegate != nil { - rctDelegate.videoPlayerViewControllerWillDismiss(playerViewController: self) - rctDelegate.videoPlayerViewControllerDidDismiss(playerViewController: self) - } + rctDelegate?.videoPlayerViewControllerWillDismiss(playerViewController: self) + rctDelegate?.videoPlayerViewControllerDidDismiss(playerViewController: self) } #if !TARGET_OS_TV - + @objc func deviceOrientationDidChange() { let orientation = UIDevice.current.orientation // Handle device orientation change diff --git a/package.json b/package.json index aa671cd92f..6edb11d734 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "react-native-video", - "version": "6.0.0-alpha.5", + "version": "6.0.0-alpha.7.3", "description": "A