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:
Olivier Bouillet 2024-09-13 10:50:33 +02:00 committed by GitHub
parent 7118ba6819
commit 84a27f3d9f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
14 changed files with 179 additions and 131 deletions

View File

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

View File

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

View File

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

View File

@ -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,8 +1922,8 @@ public class ReactExoplayerView extends FrameLayout implements
}
}
} else if ("index".equals(type)) {
int iValue = Integer.parseInt(value);
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) {
@ -1938,8 +1932,10 @@ public class ReactExoplayerView extends FrameLayout implements
} else if (iValue < groups.length) {
groupIndex = iValue;
}
}
} else if ("resolution".equals(type)) {
int height = Integer.parseInt(value);
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;
@ -1962,7 +1958,7 @@ public class ReactExoplayerView extends FrameLayout implements
closestFormat = format;
closestTrackIndex = j;
}
} else if(format.height < height) {
} else if (format.height < height) {
closestFormat = format;
closestTrackIndex = j;
}
@ -1988,6 +1984,7 @@ public class ReactExoplayerView extends FrameLayout implements
tracks.set(0, closestTrackIndex);
}
}
}
} else if (trackType == C.TRACK_TYPE_TEXT && Util.SDK_INT > 18) { // Text default
// Use system settings if possible
CaptioningManager captioningManager

View File

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

View File

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

View File

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

View File

@ -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": ""])
}
}

View File

@ -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)
} ?? []
}
}

View File

@ -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)
}
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)])
}
}

View File

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

View File

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

View File

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

View File

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