diff --git a/android/src/main/java/com/brentvatne/exoplayer/ReactExoplayerView.java b/android/src/main/java/com/brentvatne/exoplayer/ReactExoplayerView.java index 903705ca41..d9fc534be0 100644 --- a/android/src/main/java/com/brentvatne/exoplayer/ReactExoplayerView.java +++ b/android/src/main/java/com/brentvatne/exoplayer/ReactExoplayerView.java @@ -192,8 +192,9 @@ public 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 long startPositionMs = -1; + private long cropStartMs = -1; + private long cropEndMs = -1; private String extension; private boolean repeat; private String audioTrackType; @@ -661,7 +662,7 @@ private DrmSessionManager initializePlayerDrm(ReactExoplayerView self) { private void initializePlayerSource(ReactExoplayerView self, DrmSessionManager drmSessionManager) { ArrayList mediaSourceList = buildTextSources(); - MediaSource videoSource = buildMediaSource(self.srcUri, self.extension, drmSessionManager, startTimeMs, endTimeMs); + MediaSource videoSource = buildMediaSource(self.srcUri, self.extension, drmSessionManager, cropStartMs, cropEndMs); MediaSource mediaSourceWithAds = null; if (adTagUrl != null) { MediaSource.Factory mediaSourceFactory = new DefaultMediaSourceFactory(mediaDataSourceFactory) @@ -702,7 +703,12 @@ private void initializePlayerSource(ReactExoplayerView self, DrmSessionManager d if (haveResumePosition) { player.seekTo(resumeWindow, resumePosition); } - player.prepare(mediaSource, !haveResumePosition, false); + if (startPositionMs >= 0) { + player.setMediaSource(mediaSource, startPositionMs); + } else { + player.setMediaSource(mediaSource, !haveResumePosition); + } + player.prepare(); playerNeedsSource = false; reLayout(exoPlayerView); @@ -760,7 +766,7 @@ private DrmSessionManager buildDrmSessionManager(UUID uuid, String licenseUrl, S } } - private MediaSource buildMediaSource(Uri uri, String overrideExtension, DrmSessionManager drmSessionManager, long startTimeMs, long endTimeMs) { + private MediaSource buildMediaSource(Uri uri, String overrideExtension, DrmSessionManager drmSessionManager, long cropStartMs, long cropEndMs) { if (uri == null) { throw new IllegalStateException("Invalid video uri"); } @@ -821,12 +827,12 @@ private MediaSource buildMediaSource(Uri uri, String overrideExtension, DrmSessi ) .createMediaSource(mediaItem); - 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); + if (cropStartMs >= 0 && cropEndMs >= 0) { + return new ClippingMediaSource(mediaSource, cropStartMs * 1000, cropEndMs * 1000); + } else if (cropStartMs >= 0) { + return new ClippingMediaSource(mediaSource, cropStartMs * 1000, TIME_END_OF_SOURCE); + } else if (cropEndMs >= 0) { + return new ClippingMediaSource(mediaSource, 0, cropEndMs * 1000); } return mediaSource; @@ -1500,13 +1506,14 @@ public void onMetadata(@NonNull Metadata metadata) { // ReactExoplayerViewManager public api - public void setSrc(final Uri uri, final long startTimeMs, final long endTimeMs, final String extension, Map headers) { + public void setSrc(final Uri uri, final long startPositionMs, final long cropStartMs, final long cropEndMs, final String extension, Map headers) { if (uri != null) { - boolean isSourceEqual = uri.equals(srcUri) && startTimeMs == this.startTimeMs && endTimeMs == this.endTimeMs; + boolean isSourceEqual = uri.equals(srcUri) && cropStartMs == this.cropStartMs && cropEndMs == this.cropEndMs; hasDrmFailed = false; this.srcUri = uri; - this.startTimeMs = startTimeMs; - this.endTimeMs = endTimeMs; + this.startPositionMs = startPositionMs; + this.cropStartMs = cropStartMs; + this.cropEndMs = cropEndMs; this.extension = extension; this.requestHeaders = headers; this.mediaDataSourceFactory = @@ -1524,8 +1531,9 @@ public void clearSrc() { player.stop(); player.clearMediaItems(); this.srcUri = null; - this.startTimeMs = -1; - this.endTimeMs = -1; + this.startPositionMs = -1; + this.cropStartMs = -1; + this.cropEndMs = -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 7da5c19cba..7cf6cbb24d 100644 --- a/android/src/main/java/com/brentvatne/exoplayer/ReactExoplayerViewManager.java +++ b/android/src/main/java/com/brentvatne/exoplayer/ReactExoplayerViewManager.java @@ -36,8 +36,9 @@ public class ReactExoplayerViewManager extends ViewGroupManager headers = src.hasKey(PROP_SRC_HEADERS) ? ReactBridgeUtils.toStringMap(src.getMap(PROP_SRC_HEADERS)) : new HashMap<>(); @@ -166,7 +169,7 @@ public void setSrc(final ReactExoplayerView videoView, @Nullable ReadableMap src Uri srcUri = Uri.parse(uriString); if (srcUri != null) { - videoView.setSrc(srcUri, startTimeMs, endTimeMs, extension, headers); + videoView.setSrc(srcUri, startPositionMs, cropStartMs, cropEndMs, extension, headers); } } else { int identifier = context.getResources().getIdentifier( diff --git a/docs/pages/component/props.md b/docs/pages/component/props.md index 0783367cce..7b27244344 100644 --- a/docs/pages/component/props.md +++ b/docs/pages/component/props.md @@ -160,10 +160,12 @@ Note on iOS, controls are always shown when in fullscreen mode. Note on Android, native controls are available by default. If needed, you can also add your controls or use a package like [react-native-video-controls](https://github.com/itsnubix/react-native-video-controls) or [react-native-media-console](https://github.com/criszz77/react-native-media-console), see [Useful Side Project](/projects). +Platforms: Android, iOS + ### `contentStartTime` The start time in ms for SSAI content. This determines at what time to load the video info like resolutions. Use this only when you have SSAI stream where ads resolution is not the same as content resolution. -Platforms: Android, iOS +Platforms: Android ### `debug` @@ -656,18 +658,24 @@ type: 'mpd' }} The following other types are supported on some platforms, but aren't fully documented yet: `content://, ms-appx://, ms-appdata://, assets-library://` +#### Start playback at a specific point in time + +Provide an optional `startPosition` for video. Value is in milliseconds. If the `cropStart` prop is applied, it will be applied from that point forward. +(If it is negative or undefined or null, it is ignored) + +Platforms: Android, iOS #### 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. +Provide an optional `cropStart` and/or `cropEnd` for the video. Value is in milliseconds. Useful when you want to play only a portion of a large video. Example ```javascript -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', cropStart: 36012, cropEnd: 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', cropStart: 36012 }} -source={{ uri: 'https://bitdash-a.akamaihd.net/content/sintel/hls/playlist.m3u8', endTime: 48500 }} +source={{ uri: 'https://bitdash-a.akamaihd.net/content/sintel/hls/playlist.m3u8', cropEnd: 48500 }} ``` Platforms: iOS, Android diff --git a/ios/Video/DataStructures/VideoSource.swift b/ios/Video/DataStructures/VideoSource.swift index 59dca89f54..368310b5ca 100644 --- a/ios/Video/DataStructures/VideoSource.swift +++ b/ios/Video/DataStructures/VideoSource.swift @@ -6,8 +6,9 @@ struct VideoSource { let isAsset: Bool let shouldCache: Bool let requestHeaders: Dictionary? - let startTime: Int64? - let endTime: Int64? + let startPosition: Int64? + let cropStart: Int64? + let cropEnd: Int64? // Custom Metadata let title: String? let subtitle: String? @@ -25,8 +26,9 @@ struct VideoSource { self.isAsset = false self.shouldCache = false self.requestHeaders = nil - self.startTime = nil - self.endTime = nil + self.startPosition = nil + self.cropStart = nil + self.cropEnd = nil self.title = nil self.subtitle = nil self.description = nil @@ -40,8 +42,9 @@ 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 + self.startPosition = json["startPosition"] as? Int64 + self.cropStart = json["cropStart"] as? Int64 + self.cropEnd = json["cropEnd"] as? Int64 self.title = json["title"] as? String self.subtitle = json["subtitle"] as? String self.description = json["description"] as? String diff --git a/ios/Video/Features/RCTVideoUtils.swift b/ios/Video/Features/RCTVideoUtils.swift index 3e0ef3bb36..bde3f309dd 100644 --- a/ios/Video/Features/RCTVideoUtils.swift +++ b/ios/Video/Features/RCTVideoUtils.swift @@ -19,8 +19,8 @@ enum RCTVideoUtils { return 0 } - if (source?.startTime != nil && source?.endTime != nil) { - return NSNumber(value: (Float64(source?.endTime ?? 0) - Float64(source?.startTime ?? 0)) / 1000) + if (source?.cropStart != nil && source?.cropEnd != nil) { + return NSNumber(value: (Float64(source?.cropEnd ?? 0) - Float64(source?.cropStart ?? 0)) / 1000) } var effectiveTimeRange:CMTimeRange? @@ -35,8 +35,8 @@ 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)) + if (source?.cropStart != nil) { + return NSNumber(value: (playableDuration - Float64(source?.cropStart ?? 0) / 1000)) } return playableDuration as NSNumber diff --git a/ios/Video/RCTVideo.swift b/ios/Video/RCTVideo.swift index 3cf6a019a4..dfdde5c566 100644 --- a/ios/Video/RCTVideo.swift +++ b/ios/Video/RCTVideo.swift @@ -66,6 +66,7 @@ class RCTVideo: UIView, RCTVideoPlayerViewControllerDelegate, RCTPlayerObserverH private var _filterEnabled:Bool = false private var _presentingViewController:UIViewController? private var _pictureInPictureEnabled = false + private var _startPosition:Float64 = -1 /* IMA Ads */ private var _adTagUrl:String? @@ -251,8 +252,8 @@ class RCTVideo: UIView, RCTVideoPlayerViewControllerDelegate, RCTPlayerObserverH } var currentTime = _player?.currentTime() - if (currentTime != nil && _source?.startTime != nil) { - currentTime = CMTimeSubtract(currentTime!, CMTimeMake(value: _source?.startTime ?? 0, timescale: 1000)) + if (currentTime != nil && _source?.cropStart != nil) { + currentTime = CMTimeSubtract(currentTime!, CMTimeMake(value: _source?.cropStart ?? 0, timescale: 1000)) } let currentPlaybackTime = _player?.currentItem?.currentDate() let duration = CMTimeGetSeconds(playerDuration) @@ -316,6 +317,10 @@ class RCTVideo: UIView, RCTVideoPlayerViewControllerDelegate, RCTPlayerObserverH throw NSError(domain: "", code: 0, userInfo: nil) } + if let startPosition = self._source?.startPosition { + self._startPosition = Float64(startPosition) / 1000 + } + #if USE_VIDEO_CACHING if self._videoCache.shouldCache(source:source, textTracks:self._textTracks) { return self._videoCache.playerItemForSourceUsingCache(uri: source.uri, assetOptions:assetOptions) @@ -341,7 +346,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.setPlaybackRange(playerItem, withVideoStart: self._source?.cropStart, withVideoEnd: self._source?.cropEnd) self.setFilter(self._filterName) if let maxBitRate = self._maxBitRate { self._playerItem?.preferredPeakBitRate = Double(maxBitRate) @@ -601,6 +606,7 @@ class RCTVideo: UIView, RCTVideoPlayerViewControllerDelegate, RCTPlayerObserverH _pendingSeek = false } + @objc func setRate(_ rate:Float) { _rate = rate @@ -1177,6 +1183,14 @@ class RCTVideo: UIView, RCTVideoPlayerViewControllerDelegate, RCTPlayerObserverH _pendingSeek = false } + if _startPosition >= 0 { + setSeek([ + "time": NSNumber(value: _startPosition), + "tolerance": NSNumber(value: 100) + ]) + _startPosition = -1 + } + if _videoLoadStarted { let audioTracks = RCTVideoUtils.getAudioTrackInfo(_player) let textTracks = RCTVideoUtils.getTextTrackInfo(_player).map(\.json) diff --git a/src/Video.tsx b/src/Video.tsx index a0193a8737..7f99651e7d 100644 --- a/src/Video.tsx +++ b/src/Video.tsx @@ -147,9 +147,10 @@ const Video = forwardRef( type: resolvedSource.type || '', mainVer: resolvedSource.mainVer || 0, patchVer: resolvedSource.patchVer || 0, - requestHeaders: resolvedSource?.headers || {}, - startTime: resolvedSource.startTime || 0, - endTime: resolvedSource.endTime, + requestHeaders: resolvedSource.headers || {}, + startPosition: resolvedSource.startPosition ?? -1, + cropStart: resolvedSource.cropStart || 0, + cropEnd: resolvedSource.cropEnd, title: resolvedSource.title, subtitle: resolvedSource.subtitle, description: resolvedSource.description, diff --git a/src/VideoNativeComponent.ts b/src/VideoNativeComponent.ts index c13991c9d3..bcafd137b6 100644 --- a/src/VideoNativeComponent.ts +++ b/src/VideoNativeComponent.ts @@ -23,8 +23,9 @@ type VideoSrc = Readonly<{ mainVer?: number; patchVer?: number; requestHeaders?: Headers; - startTime?: number; - endTime?: number; + startPosition?: number; + cropStart?: number; + cropEnd?: number; title?: string; subtitle?: string; description?: string; diff --git a/src/types/video.ts b/src/types/video.ts index 013ba24505..34b0b3d5f1 100644 --- a/src/types/video.ts +++ b/src/types/video.ts @@ -15,8 +15,9 @@ export type ReactVideoSourceProperties = { mainVer?: number; patchVer?: number; headers?: Headers; - startTime?: number; - endTime?: number; + startPosition?: number; + cropStart?: number; + cropEnd?: number; title?: string; subtitle?: string; description?: string;