Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: implement startPosition #3355

Merged
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -661,7 +662,7 @@ private DrmSessionManager initializePlayerDrm(ReactExoplayerView self) {

private void initializePlayerSource(ReactExoplayerView self, DrmSessionManager drmSessionManager) {
ArrayList<MediaSource> 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)
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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");
}
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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<String, String> headers) {
public void setSrc(final Uri uri, final long startPositionMs, final long cropStartMs, final long cropEndMs, final String extension, Map<String, String> 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 =
Expand All @@ -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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,8 +36,9 @@ public class ReactExoplayerViewManager extends ViewGroupManager<ReactExoplayerVi
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_SRC_START_POSITION = "startPosition";
private static final String PROP_SRC_CROP_START = "cropStart";
private static final String PROP_SRC_CROP_END = "cropEnd";
private static final String PROP_AD_TAG_URL = "adTagUrl";
private static final String PROP_SRC_TYPE = "type";
private static final String PROP_DRM = "drm";
Expand Down Expand Up @@ -75,6 +76,7 @@ public class ReactExoplayerViewManager extends ViewGroupManager<ReactExoplayerVi
private static final String PROP_MIN_LOAD_RETRY_COUNT = "minLoadRetryCount";
private static final String PROP_MAXIMUM_BIT_RATE = "maxBitRate";
private static final String PROP_PLAY_IN_BACKGROUND = "playInBackground";
private static final String PROP_START_POSITION = "startPosition";
private static final String PROP_CONTENT_START_TIME = "contentStartTime";
private static final String PROP_DISABLE_FOCUS = "disableFocus";
private static final String PROP_DISABLE_BUFFERING = "disableBuffering";
Expand Down Expand Up @@ -151,8 +153,9 @@ public void setDRM(final ReactExoplayerView videoView, @Nullable ReadableMap drm
public void setSrc(final ReactExoplayerView videoView, @Nullable ReadableMap src) {
Context context = videoView.getContext().getApplicationContext();
String uriString = ReactBridgeUtils.safeGetString(src, PROP_SRC_URI, null);
int startTimeMs = ReactBridgeUtils.safeGetInt(src, PROP_SRC_START_TIME, -1);
int endTimeMs = ReactBridgeUtils.safeGetInt(src, PROP_SRC_END_TIME, -1);
int startPositionMs = ReactBridgeUtils.safeGetInt(src, PROP_START_POSITION, -1);
int cropStartMs = ReactBridgeUtils.safeGetInt(src, PROP_SRC_CROP_START, -1);
int cropEndMs = ReactBridgeUtils.safeGetInt(src, PROP_SRC_CROP_END, -1);
String extension = ReactBridgeUtils.safeGetString(src, PROP_SRC_TYPE, null);

Map<String, String> headers = src.hasKey(PROP_SRC_HEADERS) ? ReactBridgeUtils.toStringMap(src.getMap(PROP_SRC_HEADERS)) : new HashMap<>();
Expand All @@ -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(
Expand Down
18 changes: 13 additions & 5 deletions docs/pages/component/props.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

good catch !


### `debug`

Expand Down Expand Up @@ -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
Expand Down
15 changes: 9 additions & 6 deletions ios/Video/DataStructures/VideoSource.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,9 @@ struct VideoSource {
let isAsset: Bool
let shouldCache: Bool
let requestHeaders: Dictionary<String,Any>?
let startTime: Int64?
let endTime: Int64?
let startPosition: Int64?
let cropStart: Int64?
let cropEnd: Int64?
// Custom Metadata
let title: String?
let subtitle: String?
Expand All @@ -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
Expand All @@ -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<String,Any>
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
Expand Down
8 changes: 4 additions & 4 deletions ios/Video/Features/RCTVideoUtils.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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?
Expand All @@ -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
Expand Down
20 changes: 17 additions & 3 deletions ios/Video/RCTVideo.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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?
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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)
Expand All @@ -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)
Expand Down Expand Up @@ -601,6 +606,7 @@ class RCTVideo: UIView, RCTVideoPlayerViewControllerDelegate, RCTPlayerObserverH
_pendingSeek = false
}


@objc
func setRate(_ rate:Float) {
_rate = rate
Expand Down Expand Up @@ -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
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is it really need to force _startPosition to -1 ?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was assuming that the playback would work fine, then an error would occur, and then it would work again.

When the source is unchanged and the lifecycle is as follows,

(1) readyToPlay → (2) failed → (3) readyToPlay

I forced it to -1 because I would expect startPosition to not work in state (3).

But I'm not very experienced with iOS, and I don't know much about it. If my assumptions are wrong, feel free to let me know and I'll correct above code.

}

if _videoLoadStarted {
let audioTracks = RCTVideoUtils.getAudioTrackInfo(_player)
let textTracks = RCTVideoUtils.getTextTrackInfo(_player).map(\.json)
Expand Down
7 changes: 4 additions & 3 deletions src/Video.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -147,9 +147,10 @@ const Video = forwardRef<VideoRef, ReactVideoProps>(
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,
Expand Down
5 changes: 3 additions & 2 deletions src/VideoNativeComponent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
5 changes: 3 additions & 2 deletions src/types/video.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down