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
14 changed files with 179 additions and 131 deletions

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