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
<!--
Before opening a ticket
* 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.
* Provide a clear and concise description of what the bug is.
* If the library has multiple install methods, describe installation method (e.g., pod, not pod, with jetifier etc)
* Include screenshots if needed.
Very important, before opening a ticket:
1) Ensure the issue has not been already reported
2) lease test using the latest release (including 6.0.0 preRelease) of the library, as maybe the bug has been already fixed.
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).
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
-->
## Platform
<!--
Platform where your bug is happening.
-->
Which player are you experiencing the problem on:
* iOS
* Android
* Windows UWP
* Windows WPF
* Windows
## Environment info
<!--
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
```
<!-- This fields are mandatory -->
Library version: x.x.x
Device:
## 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://`
##### 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

View File

@ -2,6 +2,12 @@
- 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
- 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,
patchVer: source.patchVer || 0,
requestHeaders: source.headers ? this.stringsOnlyObject(source.headers) : {},
startTime: source.startTime || 0,
endTime: source.endTime
},
onVideoLoadStart: this._onLoadStart,
onVideoPlaybackStateChanged: this._onPlaybackStateChanged,
@ -414,13 +416,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,
@ -558,24 +553,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');

View File

@ -17,6 +17,7 @@ def configStringPath = (
).md5()
android {
namespace 'com.brentvatne.react'
compileSdkVersion safeExtGet('compileSdkVersion', 31)
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_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;
@ -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.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;
@ -210,6 +212,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;
@ -717,7 +721,8 @@ class ReactExoplayerView extends FrameLayout implements
private void initializePlayerSource(ReactExoplayerView self, DrmSessionManager drmSessionManager) {
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;
if (adTagUrl != null) {
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) {
throw new IllegalStateException("Invalid video uri");
}
@ -831,7 +837,7 @@ class ReactExoplayerView extends FrameLayout implements
}
MediaItem mediaItem = mediaItemBuilder.build();
MediaSource mediaSource = null;
DrmSessionManagerProvider drmProvider = null;
if (drmSessionManager != null) {
drmProvider = new DrmSessionManagerProvider() {
@ -845,35 +851,49 @@ class ReactExoplayerView extends FrameLayout implements
}
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<MediaSource> buildTextSources() {
@ -1530,11 +1550,15 @@ class ReactExoplayerView extends FrameLayout implements
// 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) {
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,
@ -1552,6 +1576,8 @@ class ReactExoplayerView extends FrameLayout implements
player.stop();
player.clearMediaItems();
this.srcUri = null;
this.startTimeMs = -1;
this.endTimeMs = -1;
this.extension = null;
this.requestHeaders = null;
this.mediaDataSourceFactory = null;

View File

@ -28,9 +28,10 @@ import javax.annotation.Nullable;
public class ReactExoplayerViewManager extends ViewGroupManager<ReactExoplayerView> {
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";
@ -152,6 +153,8 @@ public class ReactExoplayerViewManager extends ViewGroupManager<ReactExoplayerVi
public void setSrc(final ReactExoplayerView videoView, @Nullable ReadableMap src) {
Context context = videoView.getContext().getApplicationContext();
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;
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);
if (srcUri != null) {
videoView.setSrc(srcUri, extension, headers);
videoView.setSrc(srcUri, startTimeMs, endTimeMs, extension, headers);
}
} else {
int identifier = context.getResources().getIdentifier(

View File

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

View File

@ -6,6 +6,8 @@ struct VideoSource {
let isAsset: Bool
let shouldCache: Bool
let requestHeaders: Dictionary<String,Any>?
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<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 {
private var _video:RCTVideo
private weak var _video: RCTVideo?
/* Entry point for the SDK. Used to make ad requests. */
private var adsLoader: IMAAdsLoader!
@ -23,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())
@ -54,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
@ -71,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()
@ -98,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

View File

@ -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)
}
}
}

View File

@ -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.playAndRecord
} 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).")
}
}
}

View File

@ -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
}
}

View File

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

View File

@ -60,6 +60,7 @@ 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?
@ -214,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)
@ -232,7 +236,7 @@ class RCTVideo: UIView, RCTVideoPlayerViewControllerDelegate, RCTPlayerObserverH
#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,
@ -244,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)
@ -301,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)
@ -462,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
@ -510,6 +507,11 @@ class RCTVideo: UIView, RCTVideoPlayerViewControllerDelegate, RCTPlayerObserverH
applyModifiers()
}
@objc
func isMuted() -> Bool {
return _muted
}
@objc
func setMuted(_ muted:Bool) {
_muted = muted
@ -561,6 +563,18 @@ 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() {
@ -666,7 +680,13 @@ class RCTVideo: UIView, RCTVideoPlayerViewControllerDelegate, RCTPlayerObserverH
self.onVideoFullscreenPlayerWillPresent?(["target": reactTag as Any])
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._fullscreenPlayerPresented = fullscreen
self._playerViewController?.autorotate = self._fullscreenAutorotate
@ -678,8 +698,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)
})
}
}
@ -1025,7 +1045,10 @@ class RCTVideo: UIView, RCTVideoPlayerViewControllerDelegate, RCTPlayerObserverH
}
if _pendingSeek {
setCurrentTime(_pendingSeekTime)
setSeek([
"time": NSNumber(value: _pendingSeekTime),
"tolerance": NSNumber(value: 100)
])
_pendingSeek = false
}
@ -1104,12 +1127,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) {
RCTLog("in fullscreen")
if (!_fullscreenUncontrolPlayerPresented) {
_fullscreenUncontrolPlayerPresented = true;
self.reactViewController().view.frame = UIScreen.main.bounds
self.reactViewController().view.setNeedsLayout()
} else {NSLog("not fullscreen")}
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()
}
}

View File

@ -28,7 +28,6 @@ RCT_EXPORT_VIEW_PROPERTY(ignoreSilentSwitch, NSString);
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);

View File

@ -2,7 +2,7 @@ import AVKit
class RCTVideoPlayerViewController: AVPlayerViewController {
var rctDelegate:RCTVideoPlayerViewControllerDelegate!
weak var rctDelegate: RCTVideoPlayerViewControllerDelegate?
// Optional paramters
var preferredOrientation:String?
@ -19,11 +19,9 @@ class RCTVideoPlayerViewController: AVPlayerViewController {
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

View File

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