Merge pull request #2679 from nbennink/fix/disable-sideloaded-texttracks

fix(texttracks): unable to disable sideloaded texttracks in the AVPlayer
This commit is contained in:
Eran Hammer 2022-06-23 14:30:03 -07:00 committed by GitHub
commit 41731a8117
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 54 additions and 13 deletions

View File

@ -12,11 +12,13 @@ enum RCTPlayerOperations {
static func setSideloadedText(player:AVPlayer?, textTracks:[TextTrack]?, criteria:SelectedTrackCriteria?) { static func setSideloadedText(player:AVPlayer?, textTracks:[TextTrack]?, criteria:SelectedTrackCriteria?) {
let type = criteria?.type let type = criteria?.type
let textTracks:[TextTrack]! = textTracks ?? RCTVideoUtils.getTextTrackInfo(player) let textTracks:[TextTrack]! = textTracks ?? RCTVideoUtils.getTextTrackInfo(player)
let trackCount:Int! = player?.currentItem?.tracks.count ?? 0
// The first few tracks will be audio & video track // The first few tracks will be audio & video track
let firstTextIndex:Int = 0 var firstTextIndex:Int = 0
for firstTextIndex in 0..<(player?.currentItem?.tracks.count ?? 0) { for i in 0..<(trackCount) {
if player?.currentItem?.tracks[firstTextIndex].assetTrack?.hasMediaCharacteristic(.legible) ?? false { if player?.currentItem?.tracks[i].assetTrack?.hasMediaCharacteristic(.legible) ?? false {
firstTextIndex = i
break break
} }
} }
@ -24,7 +26,8 @@ enum RCTPlayerOperations {
var selectedTrackIndex:Int = RCTVideoUnset var selectedTrackIndex:Int = RCTVideoUnset
if (type == "disabled") { if (type == "disabled") {
// Do nothing. We want to ensure option is nil // Select the last text index which is the disabled text track
selectedTrackIndex = trackCount - firstTextIndex
} else if (type == "language") { } else if (type == "language") {
let selectedValue = criteria?.value as? String let selectedValue = criteria?.value as? String
for i in 0..<textTracks.count { for i in 0..<textTracks.count {
@ -53,7 +56,7 @@ enum RCTPlayerOperations {
// in the situation that a selected text track is not available (eg. specifies a textTrack not available) // in the situation that a selected text track is not available (eg. specifies a textTrack not available)
if (type != "disabled") && selectedTrackIndex == RCTVideoUnset { if (type != "disabled") && selectedTrackIndex == RCTVideoUnset {
let captioningMediaCharacteristics = MACaptionAppearanceCopyPreferredCaptioningMediaCharacteristics(.user) as! CFArray let captioningMediaCharacteristics = MACaptionAppearanceCopyPreferredCaptioningMediaCharacteristics(.user)
let captionSettings = captioningMediaCharacteristics as? [AnyHashable] let captionSettings = captioningMediaCharacteristics as? [AnyHashable]
if ((captionSettings?.contains(AVMediaCharacteristic.transcribesSpokenDialogForAccessibility)) != nil) { if ((captionSettings?.contains(AVMediaCharacteristic.transcribesSpokenDialogForAccessibility)) != nil) {
selectedTrackIndex = 0 // If we can't find a match, use the first available track selectedTrackIndex = 0 // If we can't find a match, use the first available track
@ -68,7 +71,7 @@ enum RCTPlayerOperations {
} }
} }
for i in firstTextIndex..<(player?.currentItem?.tracks.count ?? 0) { for i in firstTextIndex..<(trackCount) {
var isEnabled = false var isEnabled = false
if selectedTrackIndex != RCTVideoUnset { if selectedTrackIndex != RCTVideoUnset {
isEnabled = i == selectedTrackIndex + firstTextIndex isEnabled = i == selectedTrackIndex + firstTextIndex

View File

@ -37,16 +37,17 @@ enum RCTVideoUtils {
return 0 return 0
} }
static func urlFilePath(filepath:NSString!) -> NSURL! { static func urlFilePath(filepath:NSString!, searchPath:FileManager.SearchPathDirectory) -> NSURL! {
if filepath.contains("file://") { if filepath.contains("file://") {
return NSURL(string: filepath as String) return NSURL(string: filepath as String)
} }
// if no file found, check if the file exists in the Document directory // if no file found, check if the file exists in the Document directory
let paths:[String]! = NSSearchPathForDirectoriesInDomains(.documentDirectory, .userDomainMask, true) let paths:[String]! = NSSearchPathForDirectoriesInDomains(searchPath, .userDomainMask, true)
var relativeFilePath:String! = filepath.lastPathComponent var relativeFilePath:String! = filepath.lastPathComponent
// the file may be multiple levels below the documents directory // the file may be multiple levels below the documents directory
let fileComponents:[String]! = filepath.components(separatedBy: "Documents/") let directoryString:String! = searchPath == .cachesDirectory ? "Library/Caches/" : "Documents";
let fileComponents:[String]! = filepath.components(separatedBy: directoryString)
if fileComponents.count > 1 { if fileComponents.count > 1 {
relativeFilePath = fileComponents[1] relativeFilePath = fileComponents[1]
} }
@ -192,6 +193,7 @@ enum RCTVideoUtils {
static func getValidTextTracks(asset:AVAsset, assetOptions:NSDictionary?, mixComposition:AVMutableComposition, textTracks:[TextTrack]?) -> [TextTrack] { static func getValidTextTracks(asset:AVAsset, assetOptions:NSDictionary?, mixComposition:AVMutableComposition, textTracks:[TextTrack]?) -> [TextTrack] {
let videoAsset:AVAssetTrack! = asset.tracks(withMediaType: AVMediaType.video).first let videoAsset:AVAssetTrack! = asset.tracks(withMediaType: AVMediaType.video).first
var validTextTracks:[TextTrack] = [] var validTextTracks:[TextTrack] = []
if let textTracks = textTracks, textTracks.count > 0 { if let textTracks = textTracks, textTracks.count > 0 {
for i in 0..<textTracks.count { for i in 0..<textTracks.count {
var textURLAsset:AVURLAsset! var textURLAsset:AVURLAsset!
@ -199,7 +201,9 @@ enum RCTVideoUtils {
if textUri.lowercased().hasPrefix("http") { if textUri.lowercased().hasPrefix("http") {
textURLAsset = AVURLAsset(url: NSURL(string: textUri)! as URL, options:(assetOptions as! [String : Any])) textURLAsset = AVURLAsset(url: NSURL(string: textUri)! as URL, options:(assetOptions as! [String : Any]))
} else { } else {
textURLAsset = AVURLAsset(url: RCTVideoUtils.urlFilePath(filepath: textUri as NSString?) as URL, options:nil) let isDisabledTrack:Bool! = textTracks[i].type == "disabled"
let searchPath:FileManager.SearchPathDirectory = isDisabledTrack ? .cachesDirectory : .documentDirectory;
textURLAsset = AVURLAsset(url: RCTVideoUtils.urlFilePath(filepath: textUri as NSString?, searchPath: searchPath) as URL, options:nil)
} }
let textTrackAsset:AVAssetTrack! = textURLAsset.tracks(withMediaType: AVMediaType.text).first let textTrackAsset:AVAssetTrack! = textURLAsset.tracks(withMediaType: AVMediaType.text).first
if (textTrackAsset == nil) {continue} // fix when there's no textTrackAsset if (textTrackAsset == nil) {continue} // fix when there's no textTrackAsset
@ -215,9 +219,43 @@ enum RCTVideoUtils {
} }
} }
} }
let emptyVttFile:TextTrack? = self.createEmptyVttFile()
if (emptyVttFile != nil) {
validTextTracks.append(emptyVttFile!)
}
return validTextTracks return validTextTracks
} }
/*
* Create an useless / almost empty VTT file in the list with available tracks. This track gets selected when you give type: "disabled" as the selectedTextTrack
* This is needed because there is a bug where sideloaded texttracks cannot be disabled in the AVPlayer. Loading this VTT file instead solves that problem.
* For more info see: https://github.com/react-native-community/react-native-video/issues/1144
*/
static func createEmptyVttFile() -> TextTrack? {
let fileManager = FileManager.default
let cachesDirectoryUrl = fileManager.urls(for: .cachesDirectory, in: .userDomainMask)[0]
let filePath = cachesDirectoryUrl.appendingPathComponent("empty.vtt").path
if !fileManager.fileExists(atPath: filePath) {
let stringToWrite = "WEBVTT\n\n1\n99:59:59.000 --> 99:59:59.001\n."
do {
try stringToWrite.write(to: URL(fileURLWithPath: filePath), atomically: true, encoding: String.Encoding.utf8)
} catch {
return nil
}
}
return TextTrack([
"language": "disabled",
"title": "EmptyVttFile",
"type": "text/vtt",
"uri": filePath,
])
}
static func delay(seconds: Int = 0) -> Promise<Void> { static func delay(seconds: Int = 0) -> Promise<Void> {
return Promise<Void>(on: .global()) { fulfill, reject in return Promise<Void>(on: .global()) { fulfill, reject in
DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + Double(Int64(seconds)) / Double(NSEC_PER_SEC), execute: { DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + Double(Int64(seconds)) / Double(NSEC_PER_SEC), execute: {

View File

@ -5,7 +5,7 @@ import React
class RCTVideoManager: RCTViewManager { class RCTVideoManager: RCTViewManager {
override func view() -> UIView { override func view() -> UIView {
return RCTVideo(eventDispatcher: bridge.eventDispatcher()) return RCTVideo(eventDispatcher: bridge.eventDispatcher() as! RCTEventDispatcher)
} }
func methodQueue() -> DispatchQueue { func methodQueue() -> DispatchQueue {