Merge remote-tracking branch 'upstream/master'

This commit is contained in:
Craig Martin 2023-07-27 16:24:11 -04:00
commit dc3e83a3d5
18 changed files with 212 additions and 111 deletions

View File

@ -10,37 +10,24 @@ assignees: ''
# Bug # Bug
<!-- <!--
Before opening a ticket Very important, before opening a ticket:
* Ensure the issue has not been already reported 1) Ensure the issue has not been already reported
* Please test using the latest release of the library, as maybe said bug has been already fixed. 2) lease test using the latest release (including 6.0.0 preRelease) of the library, as maybe the bug has been already fixed.
* Provide a clear and concise description of what the bug is. 3) please don't use emulator to reproduce issues. if you have strange or sporadic error check if this is not devices specific (understand that this library is just a binding to player).
* If the library has multiple install methods, describe installation method (e.g., pod, not pod, with jetifier etc) 4) ensure you cannot solve the issue by yourself using following guide: https://github.com/react-native-video/react-native-video/blob/master/docs/DEBUGGING.md
* Include screenshots if needed.
--> -->
## Platform ## Platform
<!--
Platform where your bug is happening.
-->
Which player are you experiencing the problem on: Which player are you experiencing the problem on:
* iOS * iOS
* Android * Android
* Windows UWP * Windows
* Windows WPF
## Environment info ## Environment info
<!-- <!-- This fields are mandatory -->
Run `react-native info` in your terminal and copy the results here. Also, include the *precise* version number of this library that you are using in the project
-->
React native info output:
```bash
// paste it here
```
Library version: x.x.x Library version: x.x.x
Device:
## Steps To Reproduce ## Steps To Reproduce

15
API.md
View File

@ -935,6 +935,21 @@ The following other types are supported on some platforms, but aren't fully docu
`content://, ms-appx://, ms-appdata://, assets-library://` `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 #### subtitleStyle
Property | Description | Platforms Property | Description | Platforms

View File

@ -2,6 +2,12 @@
- Feature: playing audio over earpiece [#2887](https://github.com/react-native-video/react-native-video/issues/2887) - Feature: playing audio over earpiece [#2887](https://github.com/react-native-video/react-native-video/issues/2887)
### 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 ### 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) - iOS: ensure controls are not displayed when disabled by user [#3017](https://github.com/react-native-video/react-native-video/pull/3017)

View File

@ -342,6 +342,8 @@ export default class Video extends Component {
mainVer: source.mainVer || 0, mainVer: source.mainVer || 0,
patchVer: source.patchVer || 0, patchVer: source.patchVer || 0,
requestHeaders: source.headers ? this.stringsOnlyObject(source.headers) : {}, requestHeaders: source.headers ? this.stringsOnlyObject(source.headers) : {},
startTime: source.startTime || 0,
endTime: source.endTime
}, },
onVideoLoadStart: this._onLoadStart, onVideoLoadStart: this._onLoadStart,
onVideoPlaybackStateChanged: this._onPlaybackStateChanged, onVideoPlaybackStateChanged: this._onPlaybackStateChanged,
@ -414,13 +416,6 @@ Video.propTypes = {
FilterType.SEPIA, FilterType.SEPIA,
]), ]),
filterEnabled: PropTypes.bool, filterEnabled: PropTypes.bool,
/* Native only */
src: PropTypes.object,
seek: PropTypes.oneOfType([
PropTypes.number,
PropTypes.object,
]),
fullscreen: PropTypes.bool,
onVideoLoadStart: PropTypes.func, onVideoLoadStart: PropTypes.func,
onVideoLoad: PropTypes.func, onVideoLoad: PropTypes.func,
onVideoBuffer: PropTypes.func, onVideoBuffer: PropTypes.func,
@ -558,24 +553,12 @@ Video.propTypes = {
onAudioFocusChanged: PropTypes.func, onAudioFocusChanged: PropTypes.func,
onAudioBecomingNoisy: PropTypes.func, onAudioBecomingNoisy: PropTypes.func,
onPictureInPictureStatusChanged: PropTypes.func, onPictureInPictureStatusChanged: PropTypes.func,
needsToRestoreUserInterfaceForPictureInPictureStop: PropTypes.func,
onExternalPlaybackChange: PropTypes.func, onExternalPlaybackChange: PropTypes.func,
adTagUrl: PropTypes.string, adTagUrl: PropTypes.string,
onReceiveAdEvent: PropTypes.func, onReceiveAdEvent: PropTypes.func,
/* Required by react-native */ /* Required by react-native */
scaleX: PropTypes.number,
scaleY: PropTypes.number,
translateX: PropTypes.number,
translateY: PropTypes.number,
rotation: PropTypes.number,
...ViewPropTypes, ...ViewPropTypes,
}; };
const RCTVideo = requireNativeComponent('RCTVideo', Video, { const RCTVideo = requireNativeComponent('RCTVideo');
nativeOnly: {
src: true,
seek: true,
fullscreen: true,
},
});

View File

@ -17,6 +17,7 @@ def configStringPath = (
).md5() ).md5()
android { android {
namespace 'com.brentvatne.react'
compileSdkVersion safeExtGet('compileSdkVersion', 31) compileSdkVersion safeExtGet('compileSdkVersion', 31)
buildToolsVersion safeExtGet('buildToolsVersion', '30.0.2') buildToolsVersion safeExtGet('buildToolsVersion', '30.0.2')

View File

@ -4,6 +4,7 @@ import static com.google.android.exoplayer2.C.CONTENT_TYPE_DASH;
import static com.google.android.exoplayer2.C.CONTENT_TYPE_HLS; 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_OTHER;
import static com.google.android.exoplayer2.C.CONTENT_TYPE_SS; 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.annotation.SuppressLint;
import android.app.Activity; import android.app.Activity;
@ -94,6 +95,7 @@ import com.google.android.exoplayer2.source.dash.manifest.Representation;
import com.google.android.exoplayer2.ext.ima.ImaAdsLoader; import com.google.android.exoplayer2.ext.ima.ImaAdsLoader;
import com.google.android.exoplayer2.source.ads.AdsMediaSource; import com.google.android.exoplayer2.source.ads.AdsMediaSource;
import com.google.android.exoplayer2.source.DefaultMediaSourceFactory; import com.google.android.exoplayer2.source.DefaultMediaSourceFactory;
import com.google.android.exoplayer2.source.ClippingMediaSource;
import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableList;
import java.net.CookieHandler; import java.net.CookieHandler;
@ -210,6 +212,8 @@ class ReactExoplayerView extends FrameLayout implements
// Props from React // Props from React
private int backBufferDurationMs = DefaultLoadControl.DEFAULT_BACK_BUFFER_DURATION_MS; private int backBufferDurationMs = DefaultLoadControl.DEFAULT_BACK_BUFFER_DURATION_MS;
private Uri srcUri; private Uri srcUri;
private long startTimeMs = -1;
private long endTimeMs = -1;
private String extension; private String extension;
private boolean repeat; private boolean repeat;
private String audioTrackType; private String audioTrackType;
@ -717,7 +721,8 @@ class ReactExoplayerView extends FrameLayout implements
private void initializePlayerSource(ReactExoplayerView self, DrmSessionManager drmSessionManager) { private void initializePlayerSource(ReactExoplayerView self, DrmSessionManager drmSessionManager) {
ArrayList<MediaSource> mediaSourceList = buildTextSources(); ArrayList<MediaSource> mediaSourceList = buildTextSources();
MediaSource videoSource = buildMediaSource(self.srcUri, self.extension, drmSessionManager); MediaSource videoSource = buildMediaSource(self.srcUri, self.extension, drmSessionManager, startTimeMs,
endTimeMs);
MediaSource mediaSourceWithAds = null; MediaSource mediaSourceWithAds = null;
if (adTagUrl != null) { if (adTagUrl != null) {
MediaSource.Factory mediaSourceFactory = new DefaultMediaSourceFactory(mediaDataSourceFactory) MediaSource.Factory mediaSourceFactory = new DefaultMediaSourceFactory(mediaDataSourceFactory)
@ -815,7 +820,8 @@ class ReactExoplayerView extends FrameLayout implements
} }
} }
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) { if (uri == null) {
throw new IllegalStateException("Invalid video uri"); throw new IllegalStateException("Invalid video uri");
} }
@ -831,7 +837,7 @@ class ReactExoplayerView extends FrameLayout implements
} }
MediaItem mediaItem = mediaItemBuilder.build(); MediaItem mediaItem = mediaItemBuilder.build();
MediaSource mediaSource = null;
DrmSessionManagerProvider drmProvider = null; DrmSessionManagerProvider drmProvider = null;
if (drmSessionManager != null) { if (drmSessionManager != null) {
drmProvider = new DrmSessionManagerProvider() { drmProvider = new DrmSessionManagerProvider() {
@ -845,35 +851,49 @@ class ReactExoplayerView extends FrameLayout implements
} }
switch (type) { switch (type) {
case CONTENT_TYPE_SS: case CONTENT_TYPE_SS:
return new SsMediaSource.Factory( mediaSource = new SsMediaSource.Factory(
new DefaultSsChunkSource.Factory(mediaDataSourceFactory), new DefaultSsChunkSource.Factory(mediaDataSourceFactory),
buildDataSourceFactory(false)).setDrmSessionManagerProvider(drmProvider) buildDataSourceFactory(false)).setDrmSessionManagerProvider(drmProvider)
.setLoadErrorHandlingPolicy( .setLoadErrorHandlingPolicy(
config.buildLoadErrorHandlingPolicy(minLoadRetryCount)) config.buildLoadErrorHandlingPolicy(minLoadRetryCount))
.createMediaSource(mediaItem); .createMediaSource(mediaItem);
break;
case CONTENT_TYPE_DASH: case CONTENT_TYPE_DASH:
return new DashMediaSource.Factory( mediaSource = new DashMediaSource.Factory(
new DefaultDashChunkSource.Factory(mediaDataSourceFactory), new DefaultDashChunkSource.Factory(mediaDataSourceFactory),
buildDataSourceFactory(false)).setDrmSessionManagerProvider(drmProvider) buildDataSourceFactory(false)).setDrmSessionManagerProvider(drmProvider)
.setLoadErrorHandlingPolicy( .setLoadErrorHandlingPolicy(
config.buildLoadErrorHandlingPolicy(minLoadRetryCount)) config.buildLoadErrorHandlingPolicy(minLoadRetryCount))
.createMediaSource(mediaItem); .createMediaSource(mediaItem);
break;
case CONTENT_TYPE_HLS: case CONTENT_TYPE_HLS:
return new HlsMediaSource.Factory( mediaSource = new HlsMediaSource.Factory(
mediaDataSourceFactory).setDrmSessionManagerProvider(drmProvider) mediaDataSourceFactory).setDrmSessionManagerProvider(drmProvider)
.setLoadErrorHandlingPolicy( .setLoadErrorHandlingPolicy(
config.buildLoadErrorHandlingPolicy(minLoadRetryCount)) config.buildLoadErrorHandlingPolicy(minLoadRetryCount))
.createMediaSource(mediaItem); .createMediaSource(mediaItem);
break;
case CONTENT_TYPE_OTHER: case CONTENT_TYPE_OTHER:
return new ProgressiveMediaSource.Factory( mediaSource = new ProgressiveMediaSource.Factory(
mediaDataSourceFactory).setDrmSessionManagerProvider(drmProvider) mediaDataSourceFactory).setDrmSessionManagerProvider(drmProvider)
.setLoadErrorHandlingPolicy( .setLoadErrorHandlingPolicy(
config.buildLoadErrorHandlingPolicy(minLoadRetryCount)) config.buildLoadErrorHandlingPolicy(minLoadRetryCount))
.createMediaSource(mediaItem); .createMediaSource(mediaItem);
break;
default: { default: {
throw new IllegalStateException("Unsupported type: " + type); 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<MediaSource> buildTextSources() { private ArrayList<MediaSource> buildTextSources() {
@ -1530,11 +1550,15 @@ class ReactExoplayerView extends FrameLayout implements
// ReactExoplayerViewManager public api // ReactExoplayerViewManager public api
public void setSrc(final Uri uri, final String extension, Map<String, String> headers) { public void setSrc(final Uri uri, final long startTimeMs, final long endTimeMs, final String extension,
Map<String, String> headers) {
if (uri != null) { if (uri != null) {
boolean isSourceEqual = uri.equals(srcUri); boolean isSourceEqual = uri.equals(srcUri) && startTimeMs == this.startTimeMs
&& endTimeMs == this.endTimeMs;
hasDrmFailed = false; hasDrmFailed = false;
this.srcUri = uri; this.srcUri = uri;
this.startTimeMs = startTimeMs;
this.endTimeMs = endTimeMs;
this.extension = extension; this.extension = extension;
this.requestHeaders = headers; this.requestHeaders = headers;
this.mediaDataSourceFactory = DataSourceUtil.getDefaultDataSourceFactory(this.themedReactContext, this.mediaDataSourceFactory = DataSourceUtil.getDefaultDataSourceFactory(this.themedReactContext,
@ -1552,6 +1576,8 @@ class ReactExoplayerView extends FrameLayout implements
player.stop(); player.stop();
player.clearMediaItems(); player.clearMediaItems();
this.srcUri = null; this.srcUri = null;
this.startTimeMs = -1;
this.endTimeMs = -1;
this.extension = null; this.extension = null;
this.requestHeaders = null; this.requestHeaders = null;
this.mediaDataSourceFactory = null; this.mediaDataSourceFactory = null;

View File

@ -28,9 +28,10 @@ import javax.annotation.Nullable;
public class ReactExoplayerViewManager extends ViewGroupManager<ReactExoplayerView> { public class ReactExoplayerViewManager extends ViewGroupManager<ReactExoplayerView> {
private static final String REACT_CLASS = "RCTVideo"; private static final String REACT_CLASS = "RCTVideo";
private static final String PROP_SRC = "src"; private static final String PROP_SRC = "src";
private static final String PROP_SRC_URI = "uri"; 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_AD_TAG_URL = "adTagUrl";
private static final String PROP_SRC_TYPE = "type"; private static final String PROP_SRC_TYPE = "type";
private static final String PROP_DRM = "drm"; private static final String PROP_DRM = "drm";
@ -152,6 +153,8 @@ public class ReactExoplayerViewManager extends ViewGroupManager<ReactExoplayerVi
public void setSrc(final ReactExoplayerView videoView, @Nullable ReadableMap src) { public void setSrc(final ReactExoplayerView videoView, @Nullable ReadableMap src) {
Context context = videoView.getContext().getApplicationContext(); Context context = videoView.getContext().getApplicationContext();
String uriString = src.hasKey(PROP_SRC_URI) ? src.getString(PROP_SRC_URI) : null; String uriString = src.hasKey(PROP_SRC_URI) ? src.getString(PROP_SRC_URI) : null;
int startTimeMs = src.hasKey(PROP_SRC_START_TIME) ? src.getInt(PROP_SRC_START_TIME) : -1;
int endTimeMs = src.hasKey(PROP_SRC_END_TIME) ? src.getInt(PROP_SRC_END_TIME) : -1;
String extension = src.hasKey(PROP_SRC_TYPE) ? src.getString(PROP_SRC_TYPE) : null; String extension = src.hasKey(PROP_SRC_TYPE) ? src.getString(PROP_SRC_TYPE) : null;
Map<String, String> headers = src.hasKey(PROP_SRC_HEADERS) ? toStringMap(src.getMap(PROP_SRC_HEADERS)) : null; Map<String, String> headers = src.hasKey(PROP_SRC_HEADERS) ? toStringMap(src.getMap(PROP_SRC_HEADERS)) : null;
@ -164,7 +167,7 @@ public class ReactExoplayerViewManager extends ViewGroupManager<ReactExoplayerVi
Uri srcUri = Uri.parse(uriString); Uri srcUri = Uri.parse(uriString);
if (srcUri != null) { if (srcUri != null) {
videoView.setSrc(srcUri, extension, headers); videoView.setSrc(srcUri, startTimeMs, endTimeMs, extension, headers);
} }
} else { } else {
int identifier = context.getResources().getIdentifier( int identifier = context.getResources().getIdentifier(

View File

@ -14,27 +14,27 @@
android:paddingTop="4dp" android:paddingTop="4dp"
android:orientation="horizontal"> android:orientation="horizontal">
<ImageButton android:id="@id/exo_prev" <ImageButton android:id="@+id/exo_prev"
style="@style/ExoMediaButton.Previous"/> style="@style/ExoMediaButton.Previous"/>
<ImageButton android:id="@id/exo_rew" <ImageButton android:id="@+id/exo_rew"
style="@style/ExoMediaButton.Rewind"/> style="@style/ExoMediaButton.Rewind"/>
<FrameLayout <FrameLayout
android:id="@+id/exo_play_pause_container" android:id="@+id/exo_play_pause_container"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_gravity="center"> android:layout_gravity="center">
<ImageButton android:id="@id/exo_play" <ImageButton android:id="@+id/exo_play"
style="@style/ExoMediaButton.Play"/> style="@style/ExoMediaButton.Play"/>
<ImageButton android:id="@id/exo_pause" <ImageButton android:id="@+id/exo_pause"
style="@style/ExoMediaButton.Pause"/> style="@style/ExoMediaButton.Pause"/>
</FrameLayout> </FrameLayout>
<ImageButton android:id="@id/exo_ffwd" <ImageButton android:id="@+id/exo_ffwd"
style="@style/ExoMediaButton.FastForward"/> style="@style/ExoMediaButton.FastForward"/>
<ImageButton android:id="@id/exo_next" <ImageButton android:id="@+id/exo_next"
style="@style/ExoMediaButton.Next"/> style="@style/ExoMediaButton.Next"/>
</LinearLayout> </LinearLayout>
@ -46,7 +46,7 @@
android:gravity="center_vertical" android:gravity="center_vertical"
android:orientation="horizontal"> android:orientation="horizontal">
<TextView android:id="@id/exo_position" <TextView android:id="@+id/exo_position"
android:layout_width="50dp" android:layout_width="50dp"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:textSize="14sp" android:textSize="14sp"
@ -57,12 +57,12 @@
android:textColor="#FFBEBEBE"/> android:textColor="#FFBEBEBE"/>
<com.google.android.exoplayer2.ui.DefaultTimeBar <com.google.android.exoplayer2.ui.DefaultTimeBar
android:id="@id/exo_progress" android:id="@+id/exo_progress"
android:layout_width="0dp" android:layout_width="0dp"
android:layout_weight="1" android:layout_weight="1"
android:layout_height="26dp"/> android:layout_height="26dp"/>
<TextView android:id="@id/exo_duration" <TextView android:id="@+id/exo_duration"
android:layout_width="50dp" android:layout_width="50dp"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:textSize="14sp" android:textSize="14sp"
@ -73,7 +73,7 @@
android:textColor="#FFBEBEBE"/> android:textColor="#FFBEBEBE"/>
<ImageButton <ImageButton
android:id="@id/exo_fullscreen" android:id="@+id/exo_fullscreen"
style="@style/ExoMediaButton.FullScreen" style="@style/ExoMediaButton.FullScreen"
android:layout_width="30dp" android:layout_width="30dp"
android:layout_height="30dp" android:layout_height="30dp"

View File

@ -6,6 +6,8 @@ struct VideoSource {
let isAsset: Bool let isAsset: Bool
let shouldCache: Bool let shouldCache: Bool
let requestHeaders: Dictionary<String,Any>? let requestHeaders: Dictionary<String,Any>?
let startTime: Int64?
let endTime: Int64?
let json: NSDictionary? let json: NSDictionary?
@ -18,6 +20,8 @@ struct VideoSource {
self.isAsset = false self.isAsset = false
self.shouldCache = false self.shouldCache = false
self.requestHeaders = nil self.requestHeaders = nil
self.startTime = nil
self.endTime = nil
return return
} }
self.json = json self.json = json
@ -27,5 +31,7 @@ struct VideoSource {
self.isAsset = json["isAsset"] as? Bool ?? false self.isAsset = json["isAsset"] as? Bool ?? false
self.shouldCache = json["shouldCache"] as? Bool ?? false self.shouldCache = json["shouldCache"] as? Bool ?? false
self.requestHeaders = json["requestHeaders"] as? Dictionary<String,Any> self.requestHeaders = json["requestHeaders"] as? Dictionary<String,Any>
self.startTime = json["startTime"] as? Int64
self.endTime = json["endTime"] as? Int64
} }
} }

View File

@ -4,7 +4,7 @@ import GoogleInteractiveMediaAds
class RCTIMAAdsManager: NSObject, IMAAdsLoaderDelegate, IMAAdsManagerDelegate { class RCTIMAAdsManager: NSObject, IMAAdsLoaderDelegate, IMAAdsManagerDelegate {
private var _video:RCTVideo private weak var _video: RCTVideo?
/* Entry point for the SDK. Used to make ad requests. */ /* Entry point for the SDK. Used to make ad requests. */
private var adsLoader: IMAAdsLoader! private var adsLoader: IMAAdsLoader!
@ -23,6 +23,7 @@ class RCTIMAAdsManager: NSObject, IMAAdsLoaderDelegate, IMAAdsManagerDelegate {
} }
func requestAds() { func requestAds() {
guard let _video = _video else {return}
// Create ad display container for ad rendering. // Create ad display container for ad rendering.
let adDisplayContainer = IMAAdDisplayContainer(adContainer: _video, viewController: _video.reactViewController()) let adDisplayContainer = IMAAdDisplayContainer(adContainer: _video, viewController: _video.reactViewController())
@ -54,6 +55,7 @@ class RCTIMAAdsManager: NSObject, IMAAdsLoaderDelegate, IMAAdsManagerDelegate {
// MARK: - IMAAdsLoaderDelegate // MARK: - IMAAdsLoaderDelegate
func adsLoader(_ loader: IMAAdsLoader, adsLoadedWith adsLoadedData: IMAAdsLoadedData) { 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. // Grab the instance of the IMAAdsManager and set yourself as the delegate.
adsManager = adsLoadedData.adsManager adsManager = adsLoadedData.adsManager
adsManager?.delegate = self adsManager?.delegate = self
@ -71,12 +73,17 @@ class RCTIMAAdsManager: NSObject, IMAAdsLoaderDelegate, IMAAdsManagerDelegate {
print("Error loading ads: " + adErrorData.adError.message!) print("Error loading ads: " + adErrorData.adError.message!)
} }
_video.setPaused(false) _video?.setPaused(false)
} }
// MARK: - IMAAdsManagerDelegate // MARK: - IMAAdsManagerDelegate
func adsManager(_ adsManager: IMAAdsManager, didReceive event: IMAAdEvent) { 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 // Play each ad once it has been loaded
if event.type == IMAAdEventType.LOADED { if event.type == IMAAdEventType.LOADED {
adsManager.start() adsManager.start()
@ -98,19 +105,19 @@ class RCTIMAAdsManager: NSObject, IMAAdsLoaderDelegate, IMAAdsManagerDelegate {
} }
// Fall back to playing content // Fall back to playing content
_video.setPaused(false) _video?.setPaused(false)
} }
func adsManagerDidRequestContentPause(_ adsManager: IMAAdsManager) { func adsManagerDidRequestContentPause(_ adsManager: IMAAdsManager) {
// Pause the content for the SDK to play ads. // Pause the content for the SDK to play ads.
_video.setPaused(true) _video?.setPaused(true)
_video.setAdPlaying(true) _video?.setAdPlaying(true)
} }
func adsManagerDidRequestContentResume(_ adsManager: IMAAdsManager) { func adsManagerDidRequestContentResume(_ adsManager: IMAAdsManager) {
// Resume the content since the SDK is done playing ads (at least for now). // Resume the content since the SDK is done playing ads (at least for now).
_video.setAdPlaying(false) _video?.setAdPlaying(false)
_video.setPaused(false) _video?.setPaused(false)
} }
// MARK: - Helpers // MARK: - Helpers

View File

@ -25,7 +25,7 @@ protocol RCTPlayerObserverHandler: RCTPlayerObserverHandlerObjc {
class RCTPlayerObserver: NSObject { class RCTPlayerObserver: NSObject {
var _handlers: RCTPlayerObserverHandler! weak var _handlers: RCTPlayerObserverHandler?
var player:AVPlayer? { var player:AVPlayer? {
willSet { willSet {
@ -84,11 +84,13 @@ class RCTPlayerObserver: NSObject {
private var _playerViewControllerOverlayFrameObserver:NSKeyValueObservation? private var _playerViewControllerOverlayFrameObserver:NSKeyValueObservation?
deinit { deinit {
NotificationCenter.default.removeObserver(_handlers) if let _handlers = _handlers {
NotificationCenter.default.removeObserver(_handlers)
}
} }
func addPlayerObservers() { func addPlayerObservers() {
guard let player = player else { guard let player = player, let _handlers = _handlers else {
return return
} }
@ -102,7 +104,7 @@ class RCTPlayerObserver: NSObject {
} }
func addPlayerItemObservers() { 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) _playerItemStatusObserver = playerItem.observe(\.status, options: [.new, .old], changeHandler: _handlers.handlePlayerItemStatusChange)
_playerPlaybackBufferEmptyObserver = playerItem.observe(\.isPlaybackBufferEmpty, options: [.new, .old], changeHandler: _handlers.handlePlaybackBufferKeyEmpty) _playerPlaybackBufferEmptyObserver = playerItem.observe(\.isPlaybackBufferEmpty, options: [.new, .old], changeHandler: _handlers.handlePlaybackBufferKeyEmpty)
@ -118,7 +120,7 @@ class RCTPlayerObserver: NSObject {
} }
func addPlayerViewControllerObservers() { 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) _playerViewControllerReadyForDisplayObserver = playerViewController.observe(\.isReadyForDisplay, options: [.new], changeHandler: _handlers.handleReadyForDisplay)
@ -131,6 +133,7 @@ class RCTPlayerObserver: NSObject {
} }
func addPlayerLayerObserver() { func addPlayerLayerObserver() {
guard let _handlers = _handlers else {return}
_playerLayerReadyForDisplayObserver = playerLayer?.observe(\.isReadyForDisplay, options: [.new], changeHandler: _handlers.handleReadyForDisplay) _playerLayerReadyForDisplayObserver = playerLayer?.observe(\.isReadyForDisplay, options: [.new], changeHandler: _handlers.handleReadyForDisplay)
} }
@ -139,6 +142,7 @@ class RCTPlayerObserver: NSObject {
} }
func addPlayerTimeObserver() { func addPlayerTimeObserver() {
guard let _handlers = _handlers else {return}
removePlayerTimeObserver() removePlayerTimeObserver()
let progressUpdateIntervalMS:Float64 = _progressUpdateInterval / 1000 let progressUpdateIntervalMS:Float64 = _progressUpdateInterval / 1000
// @see endScrubbing in AVPlayerDemoPlaybackViewController.m // @see endScrubbing in AVPlayerDemoPlaybackViewController.m
@ -174,6 +178,7 @@ class RCTPlayerObserver: NSObject {
} }
func attachPlayerEventListeners() { func attachPlayerEventListeners() {
guard let _handlers = _handlers else {return}
NotificationCenter.default.removeObserver(_handlers, NotificationCenter.default.removeObserver(_handlers,
name:NSNotification.Name.AVPlayerItemDidPlayToEndTime, name:NSNotification.Name.AVPlayerItemDidPlayToEndTime,
@ -202,6 +207,8 @@ class RCTPlayerObserver: NSObject {
func clearPlayer() { func clearPlayer() {
player = nil player = nil
playerItem = nil playerItem = nil
NotificationCenter.default.removeObserver(_handlers) if let _handlers = _handlers {
NotificationCenter.default.removeObserver(_handlers)
}
} }
} }

View File

@ -192,36 +192,51 @@ enum RCTPlayerOperations {
} }
static func configureAudio(ignoreSilentSwitch:String, mixWithOthers:String) { static func configureAudio(ignoreSilentSwitch:String, mixWithOthers:String) {
let session:AVAudioSession! = AVAudioSession.sharedInstance() let audioSession:AVAudioSession! = AVAudioSession.sharedInstance()
var category:AVAudioSession.Category? = nil var category:AVAudioSession.Category? = nil
var options:AVAudioSession.CategoryOptions? = nil var options:AVAudioSession.CategoryOptions? = nil
if (ignoreSilentSwitch == "ignore") { if (ignoreSilentSwitch == "ignore") {
category = AVAudioSession.Category.playAndRecord category = AVAudioSession.Category.playAndRecord
} else if (ignoreSilentSwitch == "obey") { } else if (ignoreSilentSwitch == "obey") {
category = AVAudioSession.Category.ambient category = AVAudioSession.Category.ambient
} }
if (mixWithOthers == "mix") { if (mixWithOthers == "mix") {
options = .mixWithOthers options = .mixWithOthers
} else if (mixWithOthers == "duck") { } else if (mixWithOthers == "duck") {
options = .duckOthers options = .duckOthers
} }
if let category = category, let options = options { if let category = category, let options = options {
do { do {
try session.setCategory(category, options: options) try audioSession.setCategory(category, options: options)
} catch { } 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 { } else if let category = category, options == nil {
do { do {
try session.setCategory(category) try audioSession.setCategory(category)
} catch { } catch {
debugPrint("[RCTPlayerOperations] Problem setting up AVAudioSession category. Error: \(error).")
} }
} else if category == nil, let options = options { } else if category == nil, let options = options {
do { do {
try session.setCategory(session.category, options: options) try audioSession.setCategory(audioSession.category, options: options)
} catch { } catch {
debugPrint("[RCTPlayerOperations] Problem setting up AVAudioSession options. Error: \(error).")
} }
} }
} }

View File

@ -12,13 +12,17 @@ enum RCTVideoUtils {
* *
* \returns The playable duration of the current player item in seconds. * \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, guard let player = player,
let video:AVPlayerItem = player.currentItem, let video:AVPlayerItem = player.currentItem,
video.status == AVPlayerItem.Status.readyToPlay else { video.status == AVPlayerItem.Status.readyToPlay else {
return 0 return 0
} }
if (source?.startTime != nil && source?.endTime != nil) {
return NSNumber(value: (Float64(source?.endTime ?? 0) - Float64(source?.startTime ?? 0)) / 1000)
}
var effectiveTimeRange:CMTimeRange? var effectiveTimeRange:CMTimeRange?
for (_, value) in video.loadedTimeRanges.enumerated() { for (_, value) in video.loadedTimeRanges.enumerated() {
let timeRange:CMTimeRange = value.timeRangeValue let timeRange:CMTimeRange = value.timeRangeValue
@ -31,6 +35,10 @@ enum RCTVideoUtils {
if let effectiveTimeRange = effectiveTimeRange { if let effectiveTimeRange = effectiveTimeRange {
let playableDuration:Float64 = CMTimeGetSeconds(CMTimeRangeGetEnd(effectiveTimeRange)) let playableDuration:Float64 = CMTimeGetSeconds(CMTimeRangeGetEnd(effectiveTimeRange))
if playableDuration > 0 { if playableDuration > 0 {
if (source?.startTime != nil) {
return NSNumber(value: (playableDuration - Float64(source?.startTime ?? 0) / 1000))
}
return playableDuration as NSNumber return playableDuration as NSNumber
} }
} }

View File

@ -1,5 +1,6 @@
#import <React/RCTViewManager.h> #import <React/RCTViewManager.h>
#import "RCTVideoSwiftLog.h" #import "RCTVideoSwiftLog.h"
#import "RCTEventDispatcher.h"
#if __has_include(<react-native-video/RCTVideoCache.h>) #if __has_include(<react-native-video/RCTVideoCache.h>)
#import "RCTVideoCache.h" #import "RCTVideoCache.h"

View File

@ -60,6 +60,7 @@ class RCTVideo: UIView, RCTVideoPlayerViewControllerDelegate, RCTPlayerObserverH
private var _fullscreenAutorotate:Bool = true private var _fullscreenAutorotate:Bool = true
private var _fullscreenOrientation:String! = "all" private var _fullscreenOrientation:String! = "all"
private var _fullscreenPlayerPresented:Bool = false 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 _filterName:String!
private var _filterEnabled:Bool = false private var _filterEnabled:Bool = false
private var _presentingViewController:UIViewController? private var _presentingViewController:UIViewController?
@ -214,7 +215,10 @@ class RCTVideo: UIView, RCTVideoPlayerViewControllerDelegate, RCTPlayerObserverH
return 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 currentPlaybackTime = _player?.currentItem?.currentDate()
let duration = CMTimeGetSeconds(playerDuration) let duration = CMTimeGetSeconds(playerDuration)
let currentTimeSecs = CMTimeGetSeconds(currentTime ?? .zero) let currentTimeSecs = CMTimeGetSeconds(currentTime ?? .zero)
@ -232,7 +236,7 @@ class RCTVideo: UIView, RCTVideoPlayerViewControllerDelegate, RCTPlayerObserverH
#endif #endif
onVideoProgress?([ onVideoProgress?([
"currentTime": NSNumber(value: Float(currentTimeSecs)), "currentTime": NSNumber(value: Float(currentTimeSecs)),
"playableDuration": RCTVideoUtils.calculatePlayableDuration(_player), "playableDuration": RCTVideoUtils.calculatePlayableDuration(_player, withSource: _source),
"atValue": NSNumber(value: currentTime?.value ?? .zero), "atValue": NSNumber(value: currentTime?.value ?? .zero),
"currentPlaybackTime": NSNumber(value: NSNumber(value: floor(currentPlaybackTime?.timeIntervalSince1970 ?? 0 * 1000)).int64Value), "currentPlaybackTime": NSNumber(value: NSNumber(value: floor(currentPlaybackTime?.timeIntervalSince1970 ?? 0 * 1000)).int64Value),
"target": reactTag, "target": reactTag,
@ -244,7 +248,8 @@ class RCTVideo: UIView, RCTVideoPlayerViewControllerDelegate, RCTPlayerObserverH
// MARK: - Player and source // MARK: - Player and source
@objc @objc
func setSrc(_ source:NSDictionary!) { 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) self._source = VideoSource(source)
if (self._source?.uri == nil || self._source?.uri == "") { if (self._source?.uri == nil || self._source?.uri == "") {
self._player?.replaceCurrentItem(with: nil) self._player?.replaceCurrentItem(with: nil)
@ -301,6 +306,7 @@ class RCTVideo: UIView, RCTVideoPlayerViewControllerDelegate, RCTPlayerObserverH
self._playerItem = playerItem self._playerItem = playerItem
self._playerObserver.playerItem = self._playerItem self._playerObserver.playerItem = self._playerItem
self.setPreferredForwardBufferDuration(self._preferredForwardBufferDuration) self.setPreferredForwardBufferDuration(self._preferredForwardBufferDuration)
self.setPlaybackRange(playerItem, withVideoStart: self._source?.startTime, withVideoEnd: self._source?.endTime)
self.setFilter(self._filterName) self.setFilter(self._filterName)
if let maxBitRate = self._maxBitRate { if let maxBitRate = self._maxBitRate {
self._playerItem?.preferredPeakBitRate = Double(maxBitRate) self._playerItem?.preferredPeakBitRate = Double(maxBitRate)
@ -462,15 +468,6 @@ class RCTVideo: UIView, RCTVideoPlayerViewControllerDelegate, RCTPlayerObserverH
_paused = paused _paused = paused
} }
@objc
func setCurrentTime(_ currentTime:Float) {
let info:NSDictionary = [
"time": NSNumber(value: currentTime),
"tolerance": NSNumber(value: 100)
]
setSeek(info)
}
@objc @objc
func setSeek(_ info:NSDictionary!) { func setSeek(_ info:NSDictionary!) {
let seekTime:NSNumber! = info["time"] as! NSNumber let seekTime:NSNumber! = info["time"] as! NSNumber
@ -510,6 +507,11 @@ class RCTVideo: UIView, RCTVideoPlayerViewControllerDelegate, RCTPlayerObserverH
applyModifiers() applyModifiers()
} }
@objc
func isMuted() -> Bool {
return _muted
}
@objc @objc
func setMuted(_ muted:Bool) { func setMuted(_ muted:Bool) {
_muted = muted _muted = muted
@ -561,6 +563,18 @@ class RCTVideo: UIView, RCTVideoPlayerViewControllerDelegate, RCTPlayerObserverH
// Fallback on earlier versions // 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() { func applyModifiers() {
@ -666,7 +680,13 @@ class RCTVideo: UIView, RCTVideoPlayerViewControllerDelegate, RCTPlayerObserverH
self.onVideoFullscreenPlayerWillPresent?(["target": reactTag as Any]) self.onVideoFullscreenPlayerWillPresent?(["target": reactTag as Any])
if let playerViewController = _playerViewController { if let playerViewController = _playerViewController {
viewController.present(playerViewController, animated:true, completion:{ if(_controls) {
// prevents crash https://github.com/react-native-video/react-native-video/issues/3040
self._playerViewController?.removeFromParent()
}
viewController.present(playerViewController, animated:true, completion:{ [weak self] in
guard let self = self else {return}
self._playerViewController?.showsPlaybackControls = self._controls self._playerViewController?.showsPlaybackControls = self._controls
self._fullscreenPlayerPresented = fullscreen self._fullscreenPlayerPresented = fullscreen
self._playerViewController?.autorotate = self._fullscreenAutorotate self._playerViewController?.autorotate = self._fullscreenAutorotate
@ -678,8 +698,8 @@ class RCTVideo: UIView, RCTVideoPlayerViewControllerDelegate, RCTPlayerObserverH
} }
} else if !fullscreen && _fullscreenPlayerPresented, let _playerViewController = _playerViewController { } else if !fullscreen && _fullscreenPlayerPresented, let _playerViewController = _playerViewController {
self.videoPlayerViewControllerWillDismiss(playerViewController: _playerViewController) self.videoPlayerViewControllerWillDismiss(playerViewController: _playerViewController)
_presentingViewController?.dismiss(animated: true, completion:{ _presentingViewController?.dismiss(animated: true, completion:{[weak self] in
self.videoPlayerViewControllerDidDismiss(playerViewController: _playerViewController) self?.videoPlayerViewControllerDidDismiss(playerViewController: _playerViewController)
}) })
} }
} }
@ -1025,7 +1045,10 @@ class RCTVideo: UIView, RCTVideoPlayerViewControllerDelegate, RCTPlayerObserverH
} }
if _pendingSeek { if _pendingSeek {
setCurrentTime(_pendingSeekTime) setSeek([
"time": NSNumber(value: _pendingSeekTime),
"tolerance": NSNumber(value: 100)
])
_pendingSeek = false _pendingSeek = false
} }
@ -1104,12 +1127,27 @@ class RCTVideo: UIView, RCTVideoPlayerViewControllerDelegate, RCTPlayerObserverH
let oldRect = change.oldValue let oldRect = change.oldValue
let newRect = change.newValue let newRect = change.newValue
if !oldRect!.equalTo(newRect!) { if !oldRect!.equalTo(newRect!) {
// https://github.com/react-native-video/react-native-video/issues/3085#issuecomment-1557293391
if newRect!.equalTo(UIScreen.main.bounds) { if newRect!.equalTo(UIScreen.main.bounds) {
RCTLog("in fullscreen") RCTLog("in fullscreen")
if (!_fullscreenUncontrolPlayerPresented) {
_fullscreenUncontrolPlayerPresented = true;
self.reactViewController().view.frame = UIScreen.main.bounds self.onVideoFullscreenPlayerWillPresent?(["target": self.reactTag as Any])
self.reactViewController().view.setNeedsLayout() self.onVideoFullscreenPlayerDidPresent?(["target": self.reactTag as Any])
} else {NSLog("not fullscreen")} }
} 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()
} }
} }

View File

@ -28,7 +28,6 @@ RCT_EXPORT_VIEW_PROPERTY(ignoreSilentSwitch, NSString);
RCT_EXPORT_VIEW_PROPERTY(mixWithOthers, NSString); RCT_EXPORT_VIEW_PROPERTY(mixWithOthers, NSString);
RCT_EXPORT_VIEW_PROPERTY(rate, float); RCT_EXPORT_VIEW_PROPERTY(rate, float);
RCT_EXPORT_VIEW_PROPERTY(seek, NSDictionary); RCT_EXPORT_VIEW_PROPERTY(seek, NSDictionary);
RCT_EXPORT_VIEW_PROPERTY(currentTime, float);
RCT_EXPORT_VIEW_PROPERTY(fullscreen, BOOL); RCT_EXPORT_VIEW_PROPERTY(fullscreen, BOOL);
RCT_EXPORT_VIEW_PROPERTY(fullscreenAutorotate, BOOL); RCT_EXPORT_VIEW_PROPERTY(fullscreenAutorotate, BOOL);
RCT_EXPORT_VIEW_PROPERTY(fullscreenOrientation, NSString); RCT_EXPORT_VIEW_PROPERTY(fullscreenOrientation, NSString);

View File

@ -2,7 +2,7 @@ import AVKit
class RCTVideoPlayerViewController: AVPlayerViewController { class RCTVideoPlayerViewController: AVPlayerViewController {
var rctDelegate:RCTVideoPlayerViewControllerDelegate! weak var rctDelegate: RCTVideoPlayerViewControllerDelegate?
// Optional paramters // Optional paramters
var preferredOrientation:String? var preferredOrientation:String?
@ -19,11 +19,9 @@ class RCTVideoPlayerViewController: AVPlayerViewController {
override func viewDidDisappear(_ animated: Bool) { override func viewDidDisappear(_ animated: Bool) {
super.viewDidDisappear(animated) super.viewDidDisappear(animated)
if rctDelegate != nil { rctDelegate?.videoPlayerViewControllerWillDismiss(playerViewController: self)
rctDelegate.videoPlayerViewControllerWillDismiss(playerViewController: self) rctDelegate?.videoPlayerViewControllerDidDismiss(playerViewController: self)
rctDelegate.videoPlayerViewControllerDidDismiss(playerViewController: self)
}
} }
#if !TARGET_OS_TV #if !TARGET_OS_TV

View File

@ -1,6 +1,6 @@
{ {
"name": "react-native-video", "name": "react-native-video",
"version": "6.0.0-alpha.5", "version": "6.0.0-alpha.6",
"description": "A <Video /> element for react-native", "description": "A <Video /> element for react-native",
"main": "Video.js", "main": "Video.js",
"license": "MIT", "license": "MIT",
@ -24,7 +24,8 @@
}, },
"scripts": { "scripts": {
"lint": "yarn eslint .", "lint": "yarn eslint .",
"xbasic": "yarn --cwd examples/basic" "xbasic": "yarn --cwd examples/basic",
"test": "echo no test available"
}, },
"files": [ "files": [
"android", "android",