Merge pull request #3030 from yavor87/AndroidRangePlayback

Feature: Video range support
This commit is contained in:
Olivier Bouillet 2023-04-05 22:50:21 +02:00 committed by GitHub
commit d4b92dab4a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 88 additions and 15 deletions

15
API.md
View File

@ -927,6 +927,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

@ -1,7 +1,7 @@
## Changelog ## Changelog
### Version 6.0.0-alpha.6 ### 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: 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 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 - iOS: make IMA build optionnal

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,

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;
@ -93,6 +94,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;
@ -181,6 +183,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;
@ -669,7 +673,7 @@ 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)
@ -764,7 +768,7 @@ 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");
} }
@ -781,7 +785,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() {
@ -795,39 +799,54 @@ 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) buildDataSourceFactory(false)
).setDrmSessionManagerProvider(drmProvider) ).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) buildDataSourceFactory(false)
).setDrmSessionManagerProvider(drmProvider) ).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 mediaDataSourceFactory
).setDrmSessionManagerProvider(drmProvider) ).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 mediaDataSourceFactory
).setDrmSessionManagerProvider(drmProvider) ).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() {
@ -1465,11 +1484,13 @@ 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 = this.mediaDataSourceFactory =
@ -1487,6 +1508,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

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

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

@ -213,7 +213,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)
@ -231,7 +234,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,
@ -300,6 +303,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)
@ -543,6 +547,18 @@ class RCTVideo: UIView, RCTVideoPlayerViewControllerDelegate, RCTPlayerObserverH
} }
} }
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() {
if _muted { if _muted {