fix: refactor side loaded text tracks management (#4158)
* fix: refactor side loaded text tracks management More textTracks in source. android/ios: ensure text tracks are not selected by default android/ios make textTrack field not nullable clean up doc check compatibility with the old api Add comments on deprecated JS apis Apply API change on basic sample * chore: fix linter * fix(ios): fix build with caching & remove warnings
This commit is contained in:
parent
7118ba6819
commit
84a27f3d9f
@ -11,12 +11,18 @@ import com.facebook.react.bridge.ReadableMap
|
||||
class SideLoadedTextTrackList {
|
||||
var tracks = ArrayList<SideLoadedTextTrack>()
|
||||
|
||||
/** return true if this and src are equals */
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (other == null || other !is SideLoadedTextTrackList) return false
|
||||
return tracks == other.tracks
|
||||
}
|
||||
|
||||
companion object {
|
||||
fun parse(src: ReadableArray?): SideLoadedTextTrackList? {
|
||||
if (src == null) {
|
||||
return null
|
||||
}
|
||||
var sideLoadedTextTrackList = SideLoadedTextTrackList()
|
||||
val sideLoadedTextTrackList = SideLoadedTextTrackList()
|
||||
for (i in 0 until src.size()) {
|
||||
val textTrack: ReadableMap = src.getMap(i)
|
||||
sideLoadedTextTrackList.tracks.add(SideLoadedTextTrack.parse(textTrack))
|
||||
|
@ -62,6 +62,11 @@ class Source {
|
||||
*/
|
||||
var cmcdProps: CMCDProps? = null
|
||||
|
||||
/**
|
||||
* The list of sideLoaded text tracks
|
||||
*/
|
||||
var sideLoadedTextTracks: SideLoadedTextTrackList? = null
|
||||
|
||||
override fun hashCode(): Int = Objects.hash(uriString, uri, startPositionMs, cropStartMs, cropEndMs, extension, metadata, headers)
|
||||
|
||||
/** return true if this and src are equals */
|
||||
@ -74,7 +79,8 @@ class Source {
|
||||
startPositionMs == other.startPositionMs &&
|
||||
extension == other.extension &&
|
||||
drmProps == other.drmProps &&
|
||||
cmcdProps == other.cmcdProps
|
||||
cmcdProps == other.cmcdProps &&
|
||||
sideLoadedTextTracks == other.sideLoadedTextTracks
|
||||
)
|
||||
}
|
||||
|
||||
@ -139,6 +145,7 @@ class Source {
|
||||
private const val PROP_SRC_DRM = "drm"
|
||||
private const val PROP_SRC_CMCD = "cmcd"
|
||||
private const val PROP_SRC_TEXT_TRACKS_ALLOW_CHUNKLESS_PREPARATION = "textTracksAllowChunklessPreparation"
|
||||
private const val PROP_SRC_TEXT_TRACKS = "textTracks"
|
||||
|
||||
@SuppressLint("DiscouragedApi")
|
||||
private fun getUriFromAssetId(context: Context, uriString: String): Uri? {
|
||||
@ -198,6 +205,7 @@ class Source {
|
||||
source.drmProps = parse(safeGetMap(src, PROP_SRC_DRM))
|
||||
source.cmcdProps = CMCDProps.parse(safeGetMap(src, PROP_SRC_CMCD))
|
||||
source.textTracksAllowChunklessPreparation = safeGetBool(src, PROP_SRC_TEXT_TRACKS_ALLOW_CHUNKLESS_PREPARATION, true)
|
||||
source.sideLoadedTextTracks = SideLoadedTextTrackList.parse(safeGetArray(src, PROP_SRC_TEXT_TRACKS))
|
||||
|
||||
val propSrcHeadersArray = safeGetArray(src, PROP_SRC_HEADERS)
|
||||
if (propSrcHeadersArray != null) {
|
||||
|
@ -3,7 +3,6 @@ package com.brentvatne.common.toolbox
|
||||
import com.facebook.react.bridge.Dynamic
|
||||
import com.facebook.react.bridge.ReadableArray
|
||||
import com.facebook.react.bridge.ReadableMap
|
||||
import java.util.HashMap
|
||||
|
||||
/*
|
||||
* Toolbox to safe parsing of <Video props
|
||||
@ -54,6 +53,17 @@ object ReactBridgeUtils {
|
||||
|
||||
@JvmStatic fun safeGetFloat(map: ReadableMap?, key: String?): Float = safeGetFloat(map, key, 0.0f)
|
||||
|
||||
@JvmStatic fun safeParseInt(value: String?, default: Int): Int {
|
||||
if (value == null) {
|
||||
return default
|
||||
}
|
||||
return try {
|
||||
value.toInt()
|
||||
} catch (e: java.lang.Exception) {
|
||||
default
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* toStringMap converts a [ReadableMap] into a HashMap.
|
||||
*
|
||||
|
@ -108,7 +108,6 @@ import com.brentvatne.common.api.ControlsConfig;
|
||||
import com.brentvatne.common.api.DRMProps;
|
||||
import com.brentvatne.common.api.ResizeMode;
|
||||
import com.brentvatne.common.api.SideLoadedTextTrack;
|
||||
import com.brentvatne.common.api.SideLoadedTextTrackList;
|
||||
import com.brentvatne.common.api.Source;
|
||||
import com.brentvatne.common.api.SubtitleStyle;
|
||||
import com.brentvatne.common.api.TimedMetadata;
|
||||
@ -116,6 +115,7 @@ import com.brentvatne.common.api.Track;
|
||||
import com.brentvatne.common.api.VideoTrack;
|
||||
import com.brentvatne.common.react.VideoEventEmitter;
|
||||
import com.brentvatne.common.toolbox.DebugLog;
|
||||
import com.brentvatne.common.toolbox.ReactBridgeUtils;
|
||||
import com.brentvatne.react.BuildConfig;
|
||||
import com.brentvatne.react.R;
|
||||
import com.brentvatne.react.ReactNativeVideoManager;
|
||||
@ -230,9 +230,8 @@ public class ReactExoplayerView extends FrameLayout implements
|
||||
private String audioTrackValue;
|
||||
private String videoTrackType;
|
||||
private String videoTrackValue;
|
||||
private String textTrackType;
|
||||
private String textTrackType = "disabled";
|
||||
private String textTrackValue;
|
||||
private SideLoadedTextTrackList textTracks;
|
||||
private boolean disableFocus;
|
||||
private boolean focusable = true;
|
||||
private BufferingStrategy.BufferingStrategyEnum bufferingStrategy;
|
||||
@ -1126,11 +1125,11 @@ public class ReactExoplayerView extends FrameLayout implements
|
||||
|
||||
private ArrayList<MediaSource> buildTextSources() {
|
||||
ArrayList<MediaSource> textSources = new ArrayList<>();
|
||||
if (textTracks == null) {
|
||||
if (source.getSideLoadedTextTracks() == null) {
|
||||
return textSources;
|
||||
}
|
||||
|
||||
for (SideLoadedTextTrack track : textTracks.getTracks()) {
|
||||
for (SideLoadedTextTrack track : source.getSideLoadedTextTracks().getTracks()) {
|
||||
MediaSource textSource = buildTextSource(track.getTitle(),
|
||||
track.getUri(),
|
||||
track.getType(),
|
||||
@ -1844,11 +1843,6 @@ public class ReactExoplayerView extends FrameLayout implements
|
||||
adLanguage = language;
|
||||
}
|
||||
|
||||
public void setTextTracks(SideLoadedTextTrackList textTracks) {
|
||||
this.textTracks = textTracks;
|
||||
reloadSource(); // FIXME Shall be moved inside source
|
||||
}
|
||||
|
||||
private void reloadSource() {
|
||||
playerNeedsSource = true;
|
||||
initializePlayer();
|
||||
@ -1928,64 +1922,67 @@ public class ReactExoplayerView extends FrameLayout implements
|
||||
}
|
||||
}
|
||||
} else if ("index".equals(type)) {
|
||||
int iValue = Integer.parseInt(value);
|
||||
|
||||
if (trackType == C.TRACK_TYPE_VIDEO && groups.length == 1) {
|
||||
groupIndex = 0;
|
||||
if (iValue < groups.get(groupIndex).length) {
|
||||
tracks.set(0, iValue);
|
||||
int iValue = ReactBridgeUtils.safeParseInt(value, -1);
|
||||
if (iValue != -1) {
|
||||
if (trackType == C.TRACK_TYPE_VIDEO && groups.length == 1) {
|
||||
groupIndex = 0;
|
||||
if (iValue < groups.get(groupIndex).length) {
|
||||
tracks.set(0, iValue);
|
||||
}
|
||||
} else if (iValue < groups.length) {
|
||||
groupIndex = iValue;
|
||||
}
|
||||
} else if (iValue < groups.length) {
|
||||
groupIndex = iValue;
|
||||
}
|
||||
} else if ("resolution".equals(type)) {
|
||||
int height = Integer.parseInt(value);
|
||||
for (int i = 0; i < groups.length; ++i) { // Search for the exact height
|
||||
TrackGroup group = groups.get(i);
|
||||
Format closestFormat = null;
|
||||
int closestTrackIndex = -1;
|
||||
boolean usingExactMatch = false;
|
||||
for (int j = 0; j < group.length; j++) {
|
||||
Format format = group.getFormat(j);
|
||||
if (format.height == height) {
|
||||
groupIndex = i;
|
||||
tracks.set(0, j);
|
||||
closestFormat = null;
|
||||
closestTrackIndex = -1;
|
||||
usingExactMatch = true;
|
||||
break;
|
||||
} else if (isUsingContentResolution) {
|
||||
// When using content resolution rather than ads, we need to try and find the closest match if there is no exact match
|
||||
if (closestFormat != null) {
|
||||
if ((format.bitrate > closestFormat.bitrate || format.height > closestFormat.height) && format.height < height) {
|
||||
// Higher quality match
|
||||
int height = ReactBridgeUtils.safeParseInt(value, -1);
|
||||
if (height != -1) {
|
||||
for (int i = 0; i < groups.length; ++i) { // Search for the exact height
|
||||
TrackGroup group = groups.get(i);
|
||||
Format closestFormat = null;
|
||||
int closestTrackIndex = -1;
|
||||
boolean usingExactMatch = false;
|
||||
for (int j = 0; j < group.length; j++) {
|
||||
Format format = group.getFormat(j);
|
||||
if (format.height == height) {
|
||||
groupIndex = i;
|
||||
tracks.set(0, j);
|
||||
closestFormat = null;
|
||||
closestTrackIndex = -1;
|
||||
usingExactMatch = true;
|
||||
break;
|
||||
} else if (isUsingContentResolution) {
|
||||
// When using content resolution rather than ads, we need to try and find the closest match if there is no exact match
|
||||
if (closestFormat != null) {
|
||||
if ((format.bitrate > closestFormat.bitrate || format.height > closestFormat.height) && format.height < height) {
|
||||
// Higher quality match
|
||||
closestFormat = format;
|
||||
closestTrackIndex = j;
|
||||
}
|
||||
} else if (format.height < height) {
|
||||
closestFormat = format;
|
||||
closestTrackIndex = j;
|
||||
}
|
||||
} else if(format.height < height) {
|
||||
closestFormat = format;
|
||||
closestTrackIndex = j;
|
||||
}
|
||||
}
|
||||
}
|
||||
// This is a fallback if the new period contains only higher resolutions than the user has selected
|
||||
if (closestFormat == null && isUsingContentResolution && !usingExactMatch) {
|
||||
// No close match found - so we pick the lowest quality
|
||||
int minHeight = Integer.MAX_VALUE;
|
||||
for (int j = 0; j < group.length; j++) {
|
||||
Format format = group.getFormat(j);
|
||||
if (format.height < minHeight) {
|
||||
minHeight = format.height;
|
||||
groupIndex = i;
|
||||
tracks.set(0, j);
|
||||
// This is a fallback if the new period contains only higher resolutions than the user has selected
|
||||
if (closestFormat == null && isUsingContentResolution && !usingExactMatch) {
|
||||
// No close match found - so we pick the lowest quality
|
||||
int minHeight = Integer.MAX_VALUE;
|
||||
for (int j = 0; j < group.length; j++) {
|
||||
Format format = group.getFormat(j);
|
||||
if (format.height < minHeight) {
|
||||
minHeight = format.height;
|
||||
groupIndex = i;
|
||||
tracks.set(0, j);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// Selecting the closest match found
|
||||
if (closestFormat != null && closestTrackIndex != -1) {
|
||||
// We found the closest match instead of an exact one
|
||||
groupIndex = i;
|
||||
tracks.set(0, closestTrackIndex);
|
||||
// Selecting the closest match found
|
||||
if (closestFormat != null && closestTrackIndex != -1) {
|
||||
// We found the closest match instead of an exact one
|
||||
groupIndex = i;
|
||||
tracks.set(0, closestTrackIndex);
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (trackType == C.TRACK_TYPE_TEXT && Util.SDK_INT > 18) { // Text default
|
||||
|
@ -8,7 +8,6 @@ import com.brentvatne.common.api.BufferConfig
|
||||
import com.brentvatne.common.api.BufferingStrategy
|
||||
import com.brentvatne.common.api.ControlsConfig
|
||||
import com.brentvatne.common.api.ResizeMode
|
||||
import com.brentvatne.common.api.SideLoadedTextTrackList
|
||||
import com.brentvatne.common.api.Source
|
||||
import com.brentvatne.common.api.SubtitleStyle
|
||||
import com.brentvatne.common.api.ViewType
|
||||
@ -16,7 +15,6 @@ import com.brentvatne.common.react.EventTypes
|
||||
import com.brentvatne.common.toolbox.DebugLog
|
||||
import com.brentvatne.common.toolbox.ReactBridgeUtils
|
||||
import com.brentvatne.react.ReactNativeVideoManager
|
||||
import com.facebook.react.bridge.ReadableArray
|
||||
import com.facebook.react.bridge.ReadableMap
|
||||
import com.facebook.react.uimanager.ThemedReactContext
|
||||
import com.facebook.react.uimanager.ViewGroupManager
|
||||
@ -38,7 +36,6 @@ class ReactExoplayerViewManager(private val config: ReactExoplayerConfig) : View
|
||||
private const val PROP_SELECTED_TEXT_TRACK = "selectedTextTrack"
|
||||
private const val PROP_SELECTED_TEXT_TRACK_TYPE = "type"
|
||||
private const val PROP_SELECTED_TEXT_TRACK_VALUE = "value"
|
||||
private const val PROP_TEXT_TRACKS = "textTracks"
|
||||
private const val PROP_PAUSED = "paused"
|
||||
private const val PROP_MUTED = "muted"
|
||||
private const val PROP_AUDIO_OUTPUT = "audioOutput"
|
||||
@ -180,12 +177,6 @@ class ReactExoplayerViewManager(private val config: ReactExoplayerConfig) : View
|
||||
videoView.setSelectedTextTrack(typeString, value)
|
||||
}
|
||||
|
||||
@ReactProp(name = PROP_TEXT_TRACKS)
|
||||
fun setTextTracks(videoView: ReactExoplayerView, textTracks: ReadableArray?) {
|
||||
val sideLoadedTextTracks = SideLoadedTextTrackList.parse(textTracks)
|
||||
videoView.setTextTracks(sideLoadedTextTracks)
|
||||
}
|
||||
|
||||
@ReactProp(name = PROP_PAUSED, defaultBoolean = false)
|
||||
fun setPaused(videoView: ReactExoplayerView, paused: Boolean) {
|
||||
videoView.setPausedModifier(paused)
|
||||
|
@ -848,6 +848,46 @@ source={{
|
||||
}}
|
||||
```
|
||||
|
||||
#### `textTracks`
|
||||
|
||||
<PlatformsList types={['Android', 'iOS', 'visionOS']} />
|
||||
|
||||
Load one or more "sidecar" text tracks. This takes an array of objects representing each track. Each object should have the format:
|
||||
|
||||
> ⚠️ This feature does not work with HLS playlists (e.g m3u8) on iOS
|
||||
|
||||
| Property | Description |
|
||||
| -------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| title | Descriptive name for the track |
|
||||
| language | 2 letter [ISO 639-1 code](https://en.wikipedia.org/wiki/List_of_ISO_639-1_codes) representing the language |
|
||||
| type | Mime type of the track _ TextTrackType.SRT - SubRip (.srt) _ TextTrackType.TTML - TTML (.ttml) \* TextTrackType.VTT - WebVTT (.vtt)iOS only supports VTT, Android supports all 3 |
|
||||
| uri | URL for the text track. Currently, only tracks hosted on a webserver are supported |
|
||||
|
||||
On iOS, sidecar text tracks are only supported for individual files, not HLS playlists. For HLS, you should include the text tracks as part of the playlist.
|
||||
|
||||
Note: Due to iOS limitations, sidecar text tracks are not compatible with Airplay. If textTracks are specified, AirPlay support will be automatically disabled.
|
||||
|
||||
Example:
|
||||
|
||||
```javascript
|
||||
import { TextTrackType }, Video from 'react-native-video';
|
||||
|
||||
textTracks={[
|
||||
{
|
||||
title: "English CC",
|
||||
language: "en",
|
||||
type: TextTrackType.VTT, // "text/vtt"
|
||||
uri: "https://bitdash-a.akamaihd.net/content/sintel/subtitles/subtitles_en.vtt"
|
||||
},
|
||||
{
|
||||
title: "Spanish Subtitles",
|
||||
language: "es",
|
||||
type: TextTrackType.SRT, // "application/x-subrip"
|
||||
uri: "https://durian.blender.org/wp-content/content/subtitles/sintel_es.srt"
|
||||
}
|
||||
]}
|
||||
```
|
||||
|
||||
### `subtitleStyle`
|
||||
|
||||
<PlatformsList types={['Android', 'iOS']} />
|
||||
@ -892,6 +932,9 @@ This prop can be changed on runtime.
|
||||
|
||||
### `textTracks`
|
||||
|
||||
> [!WARNING]
|
||||
> deprecated, use source.textTracks instead. changing text tracks will restart playback
|
||||
|
||||
<PlatformsList types={['Android', 'iOS', 'visionOS']} />
|
||||
|
||||
Load one or more "sidecar" text tracks. This takes an array of objects representing each track. Each object should have the format:
|
||||
|
@ -241,7 +241,6 @@ const VideoPlayer: FC<Props> = ({}) => {
|
||||
showNotificationControls={showNotificationControls}
|
||||
ref={videoRef}
|
||||
source={currentSrc as ReactVideoSource}
|
||||
textTracks={additional?.textTracks}
|
||||
adTagUrl={additional?.adTagUrl}
|
||||
drm={additional?.drm}
|
||||
style={viewStyle}
|
||||
|
@ -15,4 +15,8 @@ struct SelectedTrackCriteria {
|
||||
self.type = json["type"] as? String ?? ""
|
||||
self.value = json["value"] as? String
|
||||
}
|
||||
|
||||
static func none() -> SelectedTrackCriteria {
|
||||
return SelectedTrackCriteria(["type": "none", "value": ""])
|
||||
}
|
||||
}
|
||||
|
@ -11,6 +11,7 @@ struct VideoSource {
|
||||
let customMetadata: CustomMetadata?
|
||||
/* DRM */
|
||||
let drm: DRMParams?
|
||||
var textTracks: [TextTrack] = []
|
||||
|
||||
let json: NSDictionary?
|
||||
|
||||
@ -52,5 +53,8 @@ struct VideoSource {
|
||||
self.cropEnd = (json["cropEnd"] as? Float64).flatMap { Int64(round($0)) }
|
||||
self.customMetadata = CustomMetadata(json["metadata"] as? NSDictionary)
|
||||
self.drm = DRMParams(json["drm"] as? NSDictionary)
|
||||
self.textTracks = (json["textTracks"] as? NSArray)?.map { trackDict in
|
||||
return TextTrack(trackDict as? NSDictionary)
|
||||
} ?? []
|
||||
}
|
||||
}
|
||||
|
@ -42,9 +42,8 @@ class RCTVideo: UIView, RCTVideoPlayerViewControllerDelegate, RCTPlayerObserverH
|
||||
private var _repeat = false
|
||||
private var _isPlaying = false
|
||||
private var _allowsExternalPlayback = true
|
||||
private var _textTracks: [TextTrack] = []
|
||||
private var _selectedTextTrackCriteria: SelectedTrackCriteria?
|
||||
private var _selectedAudioTrackCriteria: SelectedTrackCriteria?
|
||||
private var _selectedTextTrackCriteria: SelectedTrackCriteria = .none()
|
||||
private var _selectedAudioTrackCriteria: SelectedTrackCriteria = .none()
|
||||
private var _playbackStalled = false
|
||||
private var _playInBackground = false
|
||||
private var _preventsDisplaySleepDuringVideoPlayback = true
|
||||
@ -428,7 +427,7 @@ class RCTVideo: UIView, RCTVideoPlayerViewControllerDelegate, RCTPlayerObserverH
|
||||
|
||||
if let uri = source.uri, uri.starts(with: "ph://") {
|
||||
let photoAsset = await RCTVideoUtils.preparePHAsset(uri: uri)
|
||||
return await playerItemPrepareText(asset: photoAsset, assetOptions: nil, uri: source.uri ?? "")
|
||||
return await playerItemPrepareText(source: source, asset: photoAsset, assetOptions: nil, uri: source.uri ?? "")
|
||||
}
|
||||
|
||||
guard let assetResult = RCTVideoUtils.prepareAsset(source: source),
|
||||
@ -454,8 +453,8 @@ class RCTVideo: UIView, RCTVideoPlayerViewControllerDelegate, RCTPlayerObserverH
|
||||
}
|
||||
|
||||
#if USE_VIDEO_CACHING
|
||||
if _videoCache.shouldCache(source: source, textTracks: _textTracks) {
|
||||
return try await _videoCache.playerItemForSourceUsingCache(uri: source.uri, assetOptions: assetOptions)
|
||||
if _videoCache.shouldCache(source: source) {
|
||||
return try await _videoCache.playerItemForSourceUsingCache(source: source, assetOptions: assetOptions)
|
||||
}
|
||||
#endif
|
||||
|
||||
@ -470,7 +469,7 @@ class RCTVideo: UIView, RCTVideoPlayerViewControllerDelegate, RCTPlayerObserverH
|
||||
)
|
||||
}
|
||||
|
||||
return await playerItemPrepareText(asset: asset, assetOptions: assetOptions, uri: source.uri ?? "")
|
||||
return await playerItemPrepareText(source: source, asset: asset, assetOptions: assetOptions, uri: source.uri ?? "")
|
||||
}
|
||||
|
||||
func setupPlayer(playerItem: AVPlayerItem) async throws {
|
||||
@ -600,8 +599,8 @@ class RCTVideo: UIView, RCTVideoPlayerViewControllerDelegate, RCTPlayerObserverH
|
||||
_localSourceEncryptionKeyScheme = keyScheme
|
||||
}
|
||||
|
||||
func playerItemPrepareText(asset: AVAsset!, assetOptions: NSDictionary?, uri: String) async -> AVPlayerItem {
|
||||
if self._textTracks.isEmpty == true || (uri.hasSuffix(".m3u8")) {
|
||||
func playerItemPrepareText(source: VideoSource, asset: AVAsset!, assetOptions: NSDictionary?, uri: String) async -> AVPlayerItem {
|
||||
if source.textTracks.isEmpty != true || uri.hasSuffix(".m3u8") {
|
||||
return await self.playerItemPropegateMetadata(AVPlayerItem(asset: asset))
|
||||
}
|
||||
|
||||
@ -612,15 +611,15 @@ class RCTVideo: UIView, RCTVideoPlayerViewControllerDelegate, RCTPlayerObserverH
|
||||
asset: asset,
|
||||
assetOptions: assetOptions,
|
||||
mixComposition: mixComposition,
|
||||
textTracks: self._textTracks
|
||||
textTracks: source.textTracks
|
||||
)
|
||||
|
||||
if validTextTracks.isEmpty {
|
||||
DebugLog("Strange state, not valid textTrack")
|
||||
}
|
||||
|
||||
if validTextTracks.count != self._textTracks.count {
|
||||
self.setTextTracks(validTextTracks)
|
||||
if validTextTracks.count != source.textTracks.count {
|
||||
setSelectedTextTrack(_selectedTextTrackCriteria)
|
||||
}
|
||||
|
||||
return await self.playerItemPropegateMetadata(AVPlayerItem(asset: mixComposition))
|
||||
@ -935,10 +934,7 @@ class RCTVideo: UIView, RCTVideoPlayerViewControllerDelegate, RCTPlayerObserverH
|
||||
setMaxBitRate(_maxBitRate)
|
||||
}
|
||||
|
||||
if _selectedTextTrackCriteria != nil {
|
||||
setSelectedTextTrack(_selectedTextTrackCriteria)
|
||||
}
|
||||
|
||||
setSelectedTextTrack(_selectedTextTrackCriteria)
|
||||
setAudioOutput(_audioOutput)
|
||||
setSelectedAudioTrack(_selectedAudioTrackCriteria)
|
||||
setResizeMode(_resizeMode)
|
||||
@ -959,7 +955,7 @@ class RCTVideo: UIView, RCTVideoPlayerViewControllerDelegate, RCTPlayerObserverH
|
||||
}
|
||||
|
||||
func setSelectedAudioTrack(_ selectedAudioTrack: SelectedTrackCriteria?) {
|
||||
_selectedAudioTrackCriteria = selectedAudioTrack
|
||||
_selectedAudioTrackCriteria = selectedAudioTrack ?? SelectedTrackCriteria.none()
|
||||
Task {
|
||||
await RCTPlayerOperations.setMediaSelectionTrackForCharacteristic(player: _player, characteristic: AVMediaCharacteristic.audible,
|
||||
criteria: _selectedAudioTrackCriteria)
|
||||
@ -972,9 +968,10 @@ class RCTVideo: UIView, RCTVideoPlayerViewControllerDelegate, RCTPlayerObserverH
|
||||
}
|
||||
|
||||
func setSelectedTextTrack(_ selectedTextTrack: SelectedTrackCriteria?) {
|
||||
_selectedTextTrackCriteria = selectedTextTrack
|
||||
if !_textTracks.isEmpty { // sideloaded text tracks
|
||||
RCTPlayerOperations.setSideloadedText(player: _player, textTracks: _textTracks, criteria: _selectedTextTrackCriteria)
|
||||
_selectedTextTrackCriteria = selectedTextTrack ?? SelectedTrackCriteria.none()
|
||||
guard let source = _source else { return }
|
||||
if !source.textTracks.isEmpty { // sideloaded text tracks
|
||||
RCTPlayerOperations.setSideloadedText(player: _player, textTracks: source.textTracks, criteria: _selectedTextTrackCriteria)
|
||||
} else { // text tracks included in the HLS playlist§
|
||||
Task {
|
||||
await RCTPlayerOperations.setMediaSelectionTrackForCharacteristic(player: _player, characteristic: AVMediaCharacteristic.legible,
|
||||
@ -983,21 +980,6 @@ class RCTVideo: UIView, RCTVideoPlayerViewControllerDelegate, RCTPlayerObserverH
|
||||
}
|
||||
}
|
||||
|
||||
@objc
|
||||
func setTextTracks(_ textTracks: [NSDictionary]?) {
|
||||
setTextTracks(textTracks?.map { TextTrack($0) })
|
||||
}
|
||||
|
||||
func setTextTracks(_ textTracks: [TextTrack]?) {
|
||||
if textTracks == nil {
|
||||
_textTracks = []
|
||||
} else {
|
||||
_textTracks = textTracks!
|
||||
}
|
||||
// in case textTracks was set after selectedTextTrack
|
||||
if _selectedTextTrackCriteria != nil { setSelectedTextTrack(_selectedTextTrackCriteria) }
|
||||
}
|
||||
|
||||
@objc
|
||||
func setChapters(_ chapters: [NSDictionary]?) {
|
||||
setChapters(chapters?.map { Chapter($0) })
|
||||
@ -1307,9 +1289,8 @@ class RCTVideo: UIView, RCTVideoPlayerViewControllerDelegate, RCTPlayerObserverH
|
||||
_playerItem = nil
|
||||
_source = nil
|
||||
_chapters = nil
|
||||
_textTracks = []
|
||||
_selectedTextTrackCriteria = nil
|
||||
_selectedAudioTrackCriteria = nil
|
||||
_selectedTextTrackCriteria = SelectedTrackCriteria.none()
|
||||
_selectedAudioTrackCriteria = SelectedTrackCriteria.none()
|
||||
_presentingViewController = nil
|
||||
|
||||
ReactNativeVideoManager.shared.onInstanceRemoved(id: instanceId, player: _player as Any)
|
||||
@ -1419,7 +1400,7 @@ class RCTVideo: UIView, RCTVideoPlayerViewControllerDelegate, RCTPlayerObserverH
|
||||
|
||||
func handleReadyToPlay() {
|
||||
guard let _playerItem else { return }
|
||||
|
||||
guard let source = _source else { return }
|
||||
Task {
|
||||
if self._pendingSeek {
|
||||
self.setSeek(NSNumber(value: self._pendingSeekTime), NSNumber(value: 100))
|
||||
@ -1475,7 +1456,7 @@ class RCTVideo: UIView, RCTVideoPlayerViewControllerDelegate, RCTPlayerObserverH
|
||||
"orientation": orientation,
|
||||
],
|
||||
"audioTracks": audioTracks,
|
||||
"textTracks": extractJsonWithIndex(from: _textTracks) ?? textTracks.map(\.json),
|
||||
"textTracks": extractJsonWithIndex(from: source.textTracks) ?? textTracks.map(\.json),
|
||||
"target": self.reactTag as Any])
|
||||
}
|
||||
|
||||
@ -1672,10 +1653,11 @@ class RCTVideo: UIView, RCTVideoPlayerViewControllerDelegate, RCTPlayerObserverH
|
||||
}
|
||||
|
||||
func handleTracksChange(playerItem _: AVPlayerItem, change _: NSKeyValueObservedChange<[AVPlayerItemTrack]>) {
|
||||
guard let source = _source else { return }
|
||||
if onTextTracks != nil {
|
||||
Task {
|
||||
let textTracks = await RCTVideoUtils.getTextTrackInfo(self._player)
|
||||
self.onTextTracks?(["textTracks": extractJsonWithIndex(from: _textTracks) ?? textTracks.compactMap(\.json)])
|
||||
self.onTextTracks?(["textTracks": extractJsonWithIndex(from: source.textTracks) ?? textTracks.compactMap(\.json)])
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -4,20 +4,20 @@ import Foundation
|
||||
|
||||
class RCTVideoCachingHandler: NSObject, DVAssetLoaderDelegatesDelegate {
|
||||
private var _videoCache: RCTVideoCache! = RCTVideoCache.sharedInstance()
|
||||
var playerItemPrepareText: ((AVAsset?, NSDictionary?, String) async -> AVPlayerItem)?
|
||||
var playerItemPrepareText: ((VideoSource, AVAsset?, NSDictionary?, String) async -> AVPlayerItem)?
|
||||
|
||||
override init() {
|
||||
super.init()
|
||||
}
|
||||
|
||||
func shouldCache(source: VideoSource, textTracks: [TextTrack]?) -> Bool {
|
||||
if source.isNetwork && source.shouldCache && ((textTracks == nil) || (textTracks!.isEmpty)) {
|
||||
func shouldCache(source: VideoSource) -> Bool {
|
||||
if source.isNetwork && source.shouldCache && source.textTracks.isEmpty {
|
||||
/* The DVURLAsset created by cache doesn't have a tracksWithMediaType property, so trying
|
||||
* to bring in the text track code will crash. I suspect this is because the asset hasn't fully loaded.
|
||||
* Until this is fixed, we need to bypass caching when text tracks are specified.
|
||||
*/
|
||||
DebugLog("""
|
||||
Caching is not supported for uri '\(source.uri)' because text tracks are not compatible with the cache.
|
||||
Caching is not supported for uri '\(source.uri ?? "NO URI")' because text tracks are not compatible with the cache.
|
||||
Checkout https://github.com/react-native-community/react-native-video/blob/master/docs/caching.md
|
||||
""")
|
||||
return true
|
||||
@ -25,7 +25,8 @@ class RCTVideoCachingHandler: NSObject, DVAssetLoaderDelegatesDelegate {
|
||||
return false
|
||||
}
|
||||
|
||||
func playerItemForSourceUsingCache(uri: String!, assetOptions options: NSDictionary!) async throws -> AVPlayerItem {
|
||||
func playerItemForSourceUsingCache(source: VideoSource, assetOptions options: NSDictionary) async throws -> AVPlayerItem {
|
||||
let uri = source.uri!
|
||||
let url = URL(string: uri)
|
||||
let (videoCacheStatus, cachedAsset) = await getItemForUri(uri)
|
||||
|
||||
@ -36,33 +37,33 @@ class RCTVideoCachingHandler: NSObject, DVAssetLoaderDelegatesDelegate {
|
||||
switch videoCacheStatus {
|
||||
case .missingFileExtension:
|
||||
DebugLog("""
|
||||
Could not generate cache key for uri '\(uri ?? "NO_URI")'.
|
||||
Could not generate cache key for uri '\(uri)'.
|
||||
It is currently not supported to cache urls that do not include a file extension.
|
||||
The video file will not be cached.
|
||||
Checkout https://github.com/react-native-community/react-native-video/blob/master/docs/caching.md
|
||||
""")
|
||||
let asset: AVURLAsset! = AVURLAsset(url: url!, options: options as! [String: Any])
|
||||
return await playerItemPrepareText(asset, options, "")
|
||||
let asset: AVURLAsset! = AVURLAsset(url: url!, options: options as? [String: Any])
|
||||
return await playerItemPrepareText(source, asset, options, "")
|
||||
|
||||
case .unsupportedFileExtension:
|
||||
DebugLog("""
|
||||
Could not generate cache key for uri '\(uri ?? "NO_URI")'.
|
||||
Could not generate cache key for uri '\(uri)'.
|
||||
The file extension of that uri is currently not supported.
|
||||
The video file will not be cached.
|
||||
Checkout https://github.com/react-native-community/react-native-video/blob/master/docs/caching.md
|
||||
""")
|
||||
let asset: AVURLAsset! = AVURLAsset(url: url!, options: options as! [String: Any])
|
||||
return await playerItemPrepareText(asset, options, "")
|
||||
let asset: AVURLAsset! = AVURLAsset(url: url!, options: options as? [String: Any])
|
||||
return await playerItemPrepareText(source, asset, options, "")
|
||||
|
||||
default:
|
||||
if let cachedAsset {
|
||||
DebugLog("Playing back uri '\(uri ?? "NO_URI")' from cache")
|
||||
DebugLog("Playing back uri '\(uri)' from cache")
|
||||
// See note in playerItemForSource about not being able to support text tracks & caching
|
||||
return AVPlayerItem(asset: cachedAsset)
|
||||
}
|
||||
}
|
||||
|
||||
let asset: DVURLAsset! = DVURLAsset(url: url, options: options as! [String: Any], networkTimeout: 10000)
|
||||
let asset: DVURLAsset! = DVURLAsset(url: url, options: options as? [String: Any], networkTimeout: 10000)
|
||||
asset.loaderDelegate = self
|
||||
|
||||
/* More granular code to have control over the DVURLAsset
|
||||
|
@ -166,6 +166,7 @@ const Video = forwardRef<VideoRef, ReactVideoProps>(
|
||||
);
|
||||
|
||||
const selectedDrm = source.drm || drm;
|
||||
const _textTracks = source.textTracks || textTracks;
|
||||
const _drm = !selectedDrm
|
||||
? undefined
|
||||
: {
|
||||
@ -218,10 +219,11 @@ const Video = forwardRef<VideoRef, ReactVideoProps>(
|
||||
metadata: resolvedSource.metadata,
|
||||
drm: _drm,
|
||||
cmcd: _cmcd,
|
||||
textTracks: _textTracks,
|
||||
textTracksAllowChunklessPreparation:
|
||||
resolvedSource.textTracksAllowChunklessPreparation,
|
||||
};
|
||||
}, [drm, source]);
|
||||
}, [drm, source, textTracks]);
|
||||
|
||||
const _selectedTextTrack = useMemo(() => {
|
||||
if (!selectedTextTrack) {
|
||||
@ -727,7 +729,6 @@ const Video = forwardRef<VideoRef, ReactVideoProps>(
|
||||
restoreUserInterfaceForPIPStopCompletionHandler={
|
||||
_restoreUserInterfaceForPIPStopCompletionHandler
|
||||
}
|
||||
textTracks={textTracks}
|
||||
selectedTextTrack={_selectedTextTrack}
|
||||
selectedAudioTrack={_selectedAudioTrack}
|
||||
selectedVideoTrack={_selectedVideoTrack}
|
||||
|
@ -42,6 +42,7 @@ export type VideoSrc = Readonly<{
|
||||
drm?: Drm;
|
||||
cmcd?: NativeCmcdConfiguration; // android
|
||||
textTracksAllowChunklessPreparation?: boolean; // android
|
||||
textTracks?: TextTracks;
|
||||
}>;
|
||||
|
||||
type DRMType = WithDefault<string, 'widevine'>;
|
||||
@ -317,7 +318,6 @@ export interface VideoNativeProps extends ViewProps {
|
||||
automaticallyWaitsToMinimizeStalling?: boolean;
|
||||
shutterColor?: Int32;
|
||||
audioOutput?: WithDefault<string, 'speaker'>;
|
||||
textTracks?: TextTracks;
|
||||
selectedTextTrack?: SelectedTextTrack;
|
||||
selectedAudioTrack?: SelectedAudioTrack;
|
||||
selectedVideoTrack?: SelectedVideoTrack; // android
|
||||
|
@ -35,6 +35,7 @@ export type ReactVideoSourceProperties = {
|
||||
drm?: Drm;
|
||||
cmcd?: Cmcd; // android
|
||||
textTracksAllowChunklessPreparation?: boolean;
|
||||
textTracks?: TextTracks;
|
||||
};
|
||||
|
||||
export type ReactVideoSource = Readonly<
|
||||
@ -254,7 +255,7 @@ export type ControlsStyles = {
|
||||
|
||||
export interface ReactVideoProps extends ReactVideoEvents, ViewProps {
|
||||
source?: ReactVideoSource;
|
||||
/** @deprecated */
|
||||
/** @deprecated Use source.drm */
|
||||
drm?: Drm;
|
||||
style?: StyleProp<ViewStyle>;
|
||||
adTagUrl?: string;
|
||||
@ -302,12 +303,13 @@ export interface ReactVideoProps extends ReactVideoEvents, ViewProps {
|
||||
selectedVideoTrack?: SelectedVideoTrack; // android
|
||||
subtitleStyle?: SubtitleStyle; // android
|
||||
shutterColor?: string; // Android
|
||||
/** @deprecated Use source.textTracks */
|
||||
textTracks?: TextTracks;
|
||||
testID?: string;
|
||||
viewType?: ViewType;
|
||||
/** @deprecated */
|
||||
/** @deprecated Use viewType */
|
||||
useTextureView?: boolean; // Android
|
||||
/** @deprecated */
|
||||
/** @deprecated Use viewType*/
|
||||
useSecureView?: boolean; // Android
|
||||
volume?: number;
|
||||
localSourceEncryptionKeyScheme?: string;
|
||||
|
Loading…
Reference in New Issue
Block a user