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:
@@ -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
|
||||
|
Reference in New Issue
Block a user