From c9ecc8e9e03cb68b82ffba6f105f7e282d389de1 Mon Sep 17 00:00:00 2001 From: nbennink Date: Thu, 12 May 2022 14:45:02 +0200 Subject: [PATCH] fix(texttracks): unable to disable sideloaded texttracks in the AVPlayer --- ios/Video/RCTVideo.m | 73 ++++++++++++++++++++++++++++++++++++-------- 1 file changed, 60 insertions(+), 13 deletions(-) diff --git a/ios/Video/RCTVideo.m b/ios/Video/RCTVideo.m index cd01cc4e..b690eb4c 100644 --- a/ios/Video/RCTVideo.m +++ b/ios/Video/RCTVideo.m @@ -65,7 +65,7 @@ static int const RCTVideoUnset = -1; BOOL _paused; BOOL _repeat; BOOL _allowsExternalPlayback; - NSArray * _textTracks; + NSMutableArray * _textTracks; NSDictionary * _selectedTextTrack; NSDictionary * _selectedAudioTrack; BOOL _playbackStalled; @@ -97,7 +97,7 @@ static int const RCTVideoUnset = -1; { if ((self = [super init])) { _eventDispatcher = eventDispatcher; - _automaticallyWaitsToMinimizeStalling = YES; + _automaticallyWaitsToMinimizeStalling = YES; _playbackRateObserverRegistered = NO; _isExternalPlaybackActiveObserverRegistered = NO; _playbackStalled = NO; @@ -418,16 +418,18 @@ static int const RCTVideoUnset = -1; _drm = drm; } -- (NSURL*) urlFilePath:(NSString*) filepath { +- (NSURL*) urlFilePath:(NSString*) filepath searchPath:(enum NSSearchPathDirectory) searchPath { if ([filepath containsString:@"file://"]) { return [NSURL URLWithString:filepath]; } - // if no file found, check if the file exists in the Document directory - NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES); + // if no file found, check if the file exists in the given searchPath + NSArray *paths = NSSearchPathForDirectoriesInDomains(searchPath, NSUserDomainMask, YES); NSString* relativeFilePath = [filepath lastPathComponent]; - // the file may be multiple levels below the documents directory - NSArray* fileComponents = [filepath componentsSeparatedByString:@"Documents/"]; + + // the file may be multiple levels below the in the given searchPath + NSString *directoryString = searchPath == NSCachesDirectory ? @"Library/Caches/" : @"Documents/"; + NSArray *fileComponents = [filepath componentsSeparatedByString:directoryString]; if (fileComponents.count > 1) { relativeFilePath = [fileComponents objectAtIndex:1]; } @@ -467,13 +469,22 @@ static int const RCTVideoUnset = -1; error:nil]; NSMutableArray* validTextTracks = [NSMutableArray array]; + + NSMutableDictionary *emptyVttTrack = [self createDisabledVttFile]; + if (emptyVttTrack) [_textTracks addObject:emptyVttTrack]; + for (int i = 0; i < _textTracks.count; ++i) { AVURLAsset *textURLAsset; NSString *textUri = [_textTracks objectAtIndex:i][@"uri"]; if ([[textUri lowercaseString] hasPrefix:@"http"]) { textURLAsset = [AVURLAsset URLAssetWithURL:[NSURL URLWithString:textUri] options:assetOptions]; } else { - textURLAsset = [AVURLAsset URLAssetWithURL:[self urlFilePath:textUri] options:nil]; + BOOL isDisabledTrack = [[_textTracks objectAtIndex:i][@"language"] isEqualToString:@"disabled"]; + + // Search the track to disabled subtitles in the Caches directory, search every other local file in the Documents directory + NSSearchPathDirectory searchPath = isDisabledTrack ? NSCachesDirectory : NSDocumentDirectory; + NSURL *assetUrl = [self urlFilePath:textUri searchPath:searchPath]; + textURLAsset = [AVURLAsset URLAssetWithURL:assetUrl options:nil]; } AVAssetTrack *textTrackAsset = [textURLAsset tracksWithMediaType:AVMediaTypeText].firstObject; if (!textTrackAsset) continue; // fix when there's no textTrackAsset @@ -493,6 +504,36 @@ static int const RCTVideoUnset = -1; handler([AVPlayerItem playerItemWithAsset:mixComposition]); } +/* + * 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 + */ +- (NSMutableDictionary*)createDisabledVttFile { + NSError *error; + NSMutableDictionary *emptyVTTDictionary = nil; + + // Write the file into the Caches directory because that directory is available on iOS and tvOS + NSString *filePath = [[NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES) firstObject] stringByAppendingPathComponent:@"empty.vtt"]; + + if (![[NSFileManager defaultManager] isReadableFileAtPath:filePath]){ + // WebVTT should have at least 1 cue to be valid. That's why a small dot is visible for 1 ms at the 99:59:59.000 timestamp + NSString *stringToWrite = @"WEBVTT\n\n1\n99:59:59.000 --> 99:59:59.001\n."; + + [stringToWrite writeToFile:filePath atomically:YES encoding:NSUTF8StringEncoding error:&error]; + } + + if (!error){ + emptyVTTDictionary = [[NSMutableDictionary alloc] init]; + emptyVTTDictionary[@"language"] = @"disabled"; + emptyVTTDictionary[@"title"] = @"EmptyVttFile"; + emptyVTTDictionary[@"uri"] = filePath; + emptyVTTDictionary[@"type"] = @"text/vtt"; + } + + return emptyVTTDictionary; +} + - (void)playerItemForSource:(NSDictionary *)source withCallback:(void(^)(AVPlayerItem *))handler { bool isNetwork = [RCTConvert BOOL:[source objectForKey:@"isNetwork"]]; @@ -1037,8 +1078,8 @@ static int const RCTVideoUnset = -1; - (void)setAutomaticallyWaitsToMinimizeStalling:(BOOL)waits { - _automaticallyWaitsToMinimizeStalling = waits; - _player.automaticallyWaitsToMinimizeStalling = waits; + _automaticallyWaitsToMinimizeStalling = waits; + _player.automaticallyWaitsToMinimizeStalling = waits; } @@ -1183,9 +1224,10 @@ static int const RCTVideoUnset = -1; } int selectedTrackIndex = RCTVideoUnset; - + if ([type isEqualToString:@"disabled"]) { - // Do nothing. We want to ensure option is nil + // Enable the empty texttrack + [_player.currentItem.tracks[_player.currentItem.tracks.count - 1] setEnabled:YES]; } else if ([type isEqualToString:@"language"]) { NSString *selectedValue = _selectedTextTrack[@"value"]; for (int i = 0; i < textTracks.count; ++i) { @@ -1324,17 +1366,22 @@ static int const RCTVideoUnset = -1; mediaSelectionGroupForMediaCharacteristic:AVMediaCharacteristicLegible]; for (int i = 0; i < group.options.count; ++i) { AVMediaSelectionOption *currentOption = [group.options objectAtIndex:i]; + NSString *language = [currentOption extendedLanguageTag] ? [currentOption extendedLanguageTag] : @""; + + // Ignore the texttrack with language disabled. This is not a real texttrack and can be ignored + if ([language isEqualToString:@"disabled"]) continue; + NSString *title = @""; NSArray *values = [[currentOption commonMetadata] valueForKey:@"value"]; if (values.count > 0) { title = [values objectAtIndex:0]; } - NSString *language = [currentOption extendedLanguageTag] ? [currentOption extendedLanguageTag] : @""; NSDictionary *textTrack = @{ @"index": [NSNumber numberWithInt:i], @"title": title, @"language": language }; + [textTracks addObject:textTrack]; } return textTracks;