diff --git a/API.md b/API.md index 40781558..e926452a 100644 --- a/API.md +++ b/API.md @@ -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://` +##### Playing only a portion of the video (start & end time) + +Provide an optional `startTime` and/or `endTime` for the video. Value is in milliseconds. Useful when you want to play only a portion of a large video. + +Example +``` +source={{ uri: 'https://bitdash-a.akamaihd.net/content/sintel/hls/playlist.m3u8', startTime: 36012, endTime: 48500 }} + +source={{ uri: 'https://bitdash-a.akamaihd.net/content/sintel/hls/playlist.m3u8', startTime: 36012 }} + +source={{ uri: 'https://bitdash-a.akamaihd.net/content/sintel/hls/playlist.m3u8', endTime: 48500 }} +``` + +Platforms: iOS, Android + #### subtitleStyle Property | Description | Platforms diff --git a/CHANGELOG.md b/CHANGELOG.md index 660d5573..b94ab4c3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,7 +1,7 @@ ## Changelog ### 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 diff --git a/Video.js b/Video.js index bd8b93ba..808c0a51 100644 --- a/Video.js +++ b/Video.js @@ -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, diff --git a/android/src/main/java/com/brentvatne/exoplayer/ReactExoplayerView.java b/android/src/main/java/com/brentvatne/exoplayer/ReactExoplayerView.java index 0c0fbc17..ebecbdde 100644 --- a/android/src/main/java/com/brentvatne/exoplayer/ReactExoplayerView.java +++ b/android/src/main/java/com/brentvatne/exoplayer/ReactExoplayerView.java @@ -4,6 +4,7 @@ import static com.google.android.exoplayer2.C.CONTENT_TYPE_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; @@ -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.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; @@ -181,6 +183,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; @@ -669,7 +673,7 @@ class ReactExoplayerView extends FrameLayout implements private void initializePlayerSource(ReactExoplayerView self, DrmSessionManager drmSessionManager) { ArrayList mediaSourceList = buildTextSources(); - MediaSource videoSource = buildMediaSource(self.srcUri, self.extension, drmSessionManager); + MediaSource videoSource = buildMediaSource(self.srcUri, self.extension, drmSessionManager, startTimeMs, endTimeMs); MediaSource mediaSourceWithAds = null; if (adTagUrl != null) { MediaSource.Factory mediaSourceFactory = new DefaultMediaSourceFactory(mediaDataSourceFactory) @@ -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) { throw new IllegalStateException("Invalid video uri"); } @@ -781,7 +785,7 @@ class ReactExoplayerView extends FrameLayout implements } MediaItem mediaItem = mediaItemBuilder.build(); - + MediaSource mediaSource = null; DrmSessionManagerProvider drmProvider = null; if (drmSessionManager != null) { drmProvider = new DrmSessionManagerProvider() { @@ -795,39 +799,54 @@ 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 buildTextSources() { @@ -1465,11 +1484,13 @@ class ReactExoplayerView extends FrameLayout implements // ReactExoplayerViewManager public api - public void setSrc(final Uri uri, final String extension, Map headers) { + public void setSrc(final Uri uri, final long startTimeMs, final long endTimeMs, final String extension, Map headers) { if (uri != null) { - boolean isSourceEqual = uri.equals(srcUri); + boolean isSourceEqual = uri.equals(srcUri) && startTimeMs == this.startTimeMs && endTimeMs == this.endTimeMs; hasDrmFailed = false; this.srcUri = uri; + this.startTimeMs = startTimeMs; + this.endTimeMs = endTimeMs; this.extension = extension; this.requestHeaders = headers; this.mediaDataSourceFactory = @@ -1487,6 +1508,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; diff --git a/android/src/main/java/com/brentvatne/exoplayer/ReactExoplayerViewManager.java b/android/src/main/java/com/brentvatne/exoplayer/ReactExoplayerViewManager.java index 8da025e7..e0468985 100644 --- a/android/src/main/java/com/brentvatne/exoplayer/ReactExoplayerViewManager.java +++ b/android/src/main/java/com/brentvatne/exoplayer/ReactExoplayerViewManager.java @@ -28,9 +28,10 @@ import javax.annotation.Nullable; public class ReactExoplayerViewManager extends ViewGroupManager { private static final String REACT_CLASS = "RCTVideo"; - private static final String PROP_SRC = "src"; private static final String PROP_SRC_URI = "uri"; + private static final String PROP_SRC_START_TIME = "startTime"; + private static final String PROP_SRC_END_TIME = "endTime"; private static final String PROP_AD_TAG_URL = "adTagUrl"; private static final String PROP_SRC_TYPE = "type"; private static final String PROP_DRM = "drm"; @@ -152,6 +153,8 @@ public class ReactExoplayerViewManager extends ViewGroupManager headers = src.hasKey(PROP_SRC_HEADERS) ? toStringMap(src.getMap(PROP_SRC_HEADERS)) : null; @@ -164,7 +167,7 @@ public class ReactExoplayerViewManager extends ViewGroupManager? + let startTime: Int64? + let endTime: Int64? let json: NSDictionary? @@ -18,6 +20,8 @@ struct VideoSource { self.isAsset = false self.shouldCache = false self.requestHeaders = nil + self.startTime = nil + self.endTime = nil return } self.json = json @@ -27,5 +31,7 @@ struct VideoSource { self.isAsset = json["isAsset"] as? Bool ?? false self.shouldCache = json["shouldCache"] as? Bool ?? false self.requestHeaders = json["requestHeaders"] as? Dictionary + self.startTime = json["startTime"] as? Int64 + self.endTime = json["endTime"] as? Int64 } } diff --git a/ios/Video/Features/RCTVideoUtils.swift b/ios/Video/Features/RCTVideoUtils.swift index 3953c9d0..9916db37 100644 --- a/ios/Video/Features/RCTVideoUtils.swift +++ b/ios/Video/Features/RCTVideoUtils.swift @@ -12,13 +12,17 @@ enum RCTVideoUtils { * * \returns The playable duration of the current player item in seconds. */ - static func calculatePlayableDuration(_ player:AVPlayer?) -> NSNumber { + static func calculatePlayableDuration(_ player:AVPlayer?, withSource source:VideoSource?) -> NSNumber { guard let player = player, let video:AVPlayerItem = player.currentItem, video.status == AVPlayerItem.Status.readyToPlay else { return 0 } + if (source?.startTime != nil && source?.endTime != nil) { + return NSNumber(value: (Float64(source?.endTime ?? 0) - Float64(source?.startTime ?? 0)) / 1000) + } + var effectiveTimeRange:CMTimeRange? for (_, value) in video.loadedTimeRanges.enumerated() { let timeRange:CMTimeRange = value.timeRangeValue @@ -31,6 +35,10 @@ enum RCTVideoUtils { if let effectiveTimeRange = effectiveTimeRange { let playableDuration:Float64 = CMTimeGetSeconds(CMTimeRangeGetEnd(effectiveTimeRange)) if playableDuration > 0 { + if (source?.startTime != nil) { + return NSNumber(value: (playableDuration - Float64(source?.startTime ?? 0) / 1000)) + } + return playableDuration as NSNumber } } diff --git a/ios/Video/RCTVideo.swift b/ios/Video/RCTVideo.swift index 004fec1b..264b1de3 100644 --- a/ios/Video/RCTVideo.swift +++ b/ios/Video/RCTVideo.swift @@ -213,7 +213,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) @@ -231,7 +234,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, @@ -300,6 +303,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) @@ -542,6 +546,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() {