/* HBSubtitles.m $
This file is part of the HandBrake source code.
Homepage: .
It may be used under the terms of the GNU General Public License. */
#import "HBSubtitles.h"
#import "HBSubtitlesDefaults.h"
#import "HBSubtitlesTrack.h"
#import "HBJob.h"
#import "HBJob+HBJobConversion.h"
#import "HBTitle.h"
#import "HBCodingUtilities.h"
#import "HBUtilities.h"
#import "HBJob+Private.h"
#import "HBSecurityAccessToken.h"
#include "common.h"
extern NSString *keySubTrackName;
extern NSString *keySubTrackLanguageIsoCode;
extern NSString *keySubTrackType;
extern NSString *keySubTrackSrtFileURL;
extern NSString *keySubTrackSrtFileURLBookmark;
#define NONE_TRACK_INDEX 0
#define FOREIGN_TRACK_INDEX 1
@interface HBSubtitles ()
@property (nonatomic, readwrite) NSArray *sourceTracks;
@property (nonatomic, readonly) NSMutableArray *tokens;
@property (nonatomic, readwrite) NSInteger *accessCount;
@property (nonatomic, readwrite, weak) HBJob *job;
@property (nonatomic, readwrite) int container;
/// Used to aovid circular dependecy validation.
@property (nonatomic, readwrite) BOOL validating;
@end
@implementation HBSubtitles
- (instancetype)initWithJob:(HBJob *)job
{
self = [super init];
if (self)
{
_job = job;
_container = HB_MUX_MP4;
_tracks = [[NSMutableArray alloc] init];
_defaults = [[HBSubtitlesDefaults alloc] init];
_tokens = [NSMutableArray array];
NSMutableArray *sourceTracks = [job.title.subtitlesTracks mutableCopy];
NSMutableSet *forcedSourceNamesArray = [NSMutableSet set];
int foreignAudioType = VOBSUB;
for (NSDictionary *dict in _sourceTracks)
{
enum subsource source = [dict[keySubTrackType] intValue];
NSString *name = @(hb_subsource_name(source));
// if the subtitle track can be forced, add its source name to the array
if (hb_subtitle_can_force(source) && name.length)
{
[forcedSourceNamesArray addObject:name];
}
}
// now set the name of the Foreign Audio Search track
NSMutableString *foreignAudioSearchTrackName = [@"Foreign Audio Search (Bitmap)" mutableCopy];
if (forcedSourceNamesArray.count)
{
[foreignAudioSearchTrackName appendFormat:@" ("];
for (NSString *name in forcedSourceNamesArray)
{
[foreignAudioSearchTrackName appendFormat:@"%@, ", name];
}
[foreignAudioSearchTrackName deleteCharactersInRange:NSMakeRange(foreignAudioSearchTrackName.length - 2, 2)];
[foreignAudioSearchTrackName appendFormat:@")"];
}
// Add the none and foreign track to the source array
NSDictionary *none = @{ keySubTrackName: NSLocalizedString(@"None", nil)};
[sourceTracks insertObject:none atIndex:0];
NSDictionary *foreign = @{ keySubTrackName: [foreignAudioSearchTrackName copy],
keySubTrackType: @(foreignAudioType) };
[sourceTracks insertObject:foreign atIndex:1];
_sourceTracks = [sourceTracks copy];
}
return self;
}
#pragma mark - Data Source
- (NSDictionary *)sourceTrackAtIndex:(NSUInteger)idx;
{
return self.sourceTracks[idx];
}
- (NSArray *)sourceTracksArray
{
NSMutableArray *sourceNames = [NSMutableArray array];
for (NSDictionary *track in self.sourceTracks)
{
[sourceNames addObject:track[keySubTrackName]];
}
return sourceNames;
}
#pragma mark - Delegate
- (void)track:(HBSubtitlesTrack *)track didChangeSourceFrom:(NSUInteger)oldSourceIdx;
{
// If the source was changed to None, remove the track
if (track.sourceTrackIdx == NONE_TRACK_INDEX)
{
NSUInteger idx = [self.tracks indexOfObject:track];
[self removeObjectFromTracksAtIndex:idx];
}
// If the source was changed to Foreign Audio Track,
// insert it at top if it wasn't already there
else if (track.sourceTrackIdx == FOREIGN_TRACK_INDEX)
{
NSUInteger idx = [self.tracks indexOfObject:track];
if (idx != 0)
{
[self removeObjectFromTracksAtIndex:idx];
if (self.tracks[0].sourceTrackIdx != FOREIGN_TRACK_INDEX)
{
[self insertObject:track inTracksAtIndex:0];
}
}
}
// Else add a new None track
if (oldSourceIdx == NONE_TRACK_INDEX)
{
[self addNoneTrack];
}
[self validatePassthru];
}
- (BOOL)canSetBurnedInOption:(HBSubtitlesTrack *)track
{
BOOL result = YES;
for (HBSubtitlesTrack *subTrack in self.tracks)
{
if (subTrack != track && subTrack.isEnabled
&& subTrack.sourceTrackIdx > FOREIGN_TRACK_INDEX && !subTrack.canPassthru)
{
result = NO;
}
}
return result;
}
- (void)didSetBurnedInOption:(HBSubtitlesTrack *)track
{
if (self.validating == NO && track.sourceTrackIdx != FOREIGN_TRACK_INDEX)
{
self.validating = YES;
NSUInteger idx = [self.tracks indexOfObject:track];
[self validateBurned:idx];
self.validating = NO;
}
}
- (void)didSetDefaultOption:(HBSubtitlesTrack *)track
{
if (self.validating == NO && track.sourceTrackIdx != FOREIGN_TRACK_INDEX)
{
self.validating = YES;
NSUInteger idx = [self.tracks indexOfObject:track];
[self validateDefault:idx];
self.validating = NO;
}
}
- (void)addNoneTrack
{
HBSubtitlesTrack *track = [self trackFromSourceTrackIndex:NONE_TRACK_INDEX];
[self addTrack:track];
}
#pragma mark - Public methods
- (void)addAllTracks
{
[self removeTracksAtIndexes:[NSIndexSet indexSetWithIndexesInRange:NSMakeRange(0, self.tracks.count)]];
// Add the remainings tracks
for (NSUInteger idx = 1; idx < self.sourceTracksArray.count; idx++) {
[self addTrack:[self trackFromSourceTrackIndex:idx]];
}
[self addNoneTrack];
[self validatePassthru];
}
- (void)removeAll
{
[self removeTracksAtIndexes:[NSIndexSet indexSetWithIndexesInRange:NSMakeRange(0, self.tracks.count)]];
[self addNoneTrack];
}
- (void)reloadDefaults
{
[self addDefaultTracksFromJobSettings:self.job.jobDict];
}
- (void)addSrtTrackFromURL:(NSURL *)srtURL
{
#ifdef __SANDBOX_ENABLED__
// Create the security scoped bookmark
NSData *bookmark = [HBUtilities bookmarkFromURL:srtURL
options:NSURLBookmarkCreationWithSecurityScope |
NSURLBookmarkCreationSecurityScopeAllowOnlyReadAccess];
#endif
// Create a new entry for the subtitle source array so it shows up in our subtitle source list
NSMutableArray *sourceTrack = [self.sourceTracks mutableCopy];
#ifdef __SANDBOX_ENABLED__
[sourceTrack addObject:@{keySubTrackName: srtURL.lastPathComponent,
keySubTrackType: @(SRTSUB),
keySubTrackSrtFileURL: srtURL,
keySubTrackSrtFileURLBookmark: bookmark}];
#else
[sourceTrack addObject:@{keySubTrackName: srtURL.lastPathComponent,
keySubTrackType: @(SRTSUB),
keySubTrackSrtFileURL: srtURL}];
#endif
self.sourceTracks = [sourceTrack copy];
HBSubtitlesTrack *track = [self trackFromSourceTrackIndex:self.sourceTracksArray.count - 1];
[self insertObject:track inTracksAtIndex:[self countOfTracks] - 1];
}
- (void)setContainer:(int)container
{
_container = container;
for (HBSubtitlesTrack *track in self.tracks)
{
track.container = container;
}
if (!(self.undo.isUndoing || self.undo.isRedoing))
{
[self validatePassthru];
}
}
- (void)setUndo:(NSUndoManager *)undo
{
_undo = undo;
for (HBSubtitlesTrack *track in self.tracks)
{
track.undo = undo;
}
self.defaults.undo = undo;
}
- (void)setDefaults:(HBSubtitlesDefaults *)defaults
{
if (defaults != _defaults)
{
[[self.undo prepareWithInvocationTarget:self] setDefaults:_defaults];
}
_defaults = defaults;
_defaults.undo = self.undo;
}
/**
* Convenience method to add a track to subtitlesArray.
*
* @param track the track to add.
*/
- (void)addTrack:(HBSubtitlesTrack *)newTrack
{
[self insertObject:newTrack inTracksAtIndex:[self countOfTracks]];
}
/**
* Creates a new track dictionary from a source track.
*
* @param index the index of the source track in the subtitlesSourceArray
*/
- (HBSubtitlesTrack *)trackFromSourceTrackIndex:(NSInteger)index
{
HBSubtitlesTrack *track = [[HBSubtitlesTrack alloc] initWithTrackIdx:index container:self.container
dataSource:self delegate:self];
track.undo = self.undo;
return track;
}
#pragma mark - Defaults
- (void)addDefaultTracksFromJobSettings:(NSDictionary *)settings
{
NSMutableArray *tracks = [NSMutableArray array];
NSArray *> *settingsTracks = settings[@"Subtitle"][@"SubtitleList"];
NSDictionary *search = settings[@"Subtitle"][@"Search"];
// Reinitialize the configured list of audio tracks
[self removeTracksAtIndexes:[NSIndexSet indexSetWithIndexesInRange:NSMakeRange(0, self.tracks.count)]];
// Add the foreign audio search pass
if ([search[@"Enable"] boolValue])
{
HBSubtitlesTrack *track = [self trackFromSourceTrackIndex:FOREIGN_TRACK_INDEX];
track.burnedIn = [search[@"Burn"] boolValue];
track.forcedOnly = [search[@"Forced"] boolValue];
track.def = [search[@"Default"] boolValue];
[tracks addObject:track];
}
// Add the tracks
for (NSDictionary *trackDict in settingsTracks)
{
HBSubtitlesTrack *track = [self trackFromSourceTrackIndex:[trackDict[@"Track"] unsignedIntegerValue] + 2];
track.burnedIn = [trackDict[@"Burn"] boolValue];
track.forcedOnly = [trackDict[@"Forced"] boolValue];
[tracks addObject:track];
}
[self insertTracks:tracks atIndexes:[NSIndexSet indexSetWithIndexesInRange:NSMakeRange(0, tracks.count)]];
// Add an None item
[self addNoneTrack];
}
#pragma mark - Validation
/**
* Checks whether any subtitles in the list cannot be passed through.
* Set the first of any such subtitles to burned-in, remove the others.
*/
- (void)validatePassthru
{
BOOL convertToBurnInUsed = NO;
NSMutableIndexSet *tracksToDelete = [[NSMutableIndexSet alloc] init];
// convert any non-None incompatible tracks to burn-in or remove them
NSUInteger idx = 0;
for (HBSubtitlesTrack *track in self.tracks)
{
if (track.isEnabled && track.sourceTrackIdx > FOREIGN_TRACK_INDEX && !track.canPassthru)
{
if (convertToBurnInUsed == NO)
{
//we haven't set any track to burned-in yet, so we can
track.burnedIn = YES;
convertToBurnInUsed = YES; //remove any additional tracks
}
else
{
//we already have a burned-in track, we must remove others
[tracksToDelete addIndex:idx];
}
}
idx++;
}
//if we converted a track to burned-in, unset it for tracks that support passthru
if (convertToBurnInUsed == YES)
{
for (HBSubtitlesTrack *track in self.tracks)
{
if (track.isEnabled && track.sourceTrackIdx > FOREIGN_TRACK_INDEX && track.canPassthru)
{
track.burnedIn = NO;
}
}
}
// Delete the tracks
NSUInteger currentIndex = [tracksToDelete lastIndex];
while (currentIndex != NSNotFound)
{
[self removeObjectFromTracksAtIndex:currentIndex];
currentIndex = [tracksToDelete indexLessThanIndex:currentIndex];
}
}
- (void)validateBurned:(NSUInteger)index
{
[self.tracks enumerateObjectsUsingBlock:^(HBSubtitlesTrack *track, NSUInteger idx, BOOL *stop)
{
if (idx != index && track.sourceTrackIdx != FOREIGN_TRACK_INDEX)
{
track.burnedIn = NO;
}
}];
}
- (void)validateDefault:(NSUInteger)index
{
[self.tracks enumerateObjectsUsingBlock:^(HBSubtitlesTrack *obj, NSUInteger idx, BOOL *stop)
{
if (idx != index && obj.sourceTrackIdx != FOREIGN_TRACK_INDEX)
{
obj.def = NO;
}
}];
}
- (BOOL)startAccessingSecurityScopedResource
{
#ifdef __SANDBOX_ENABLED__
if (self.accessCount == 0)
{
for (NSDictionary *sourceTrack in self.sourceTracks)
{
if (sourceTrack[keySubTrackSrtFileURLBookmark])
{
[self.tokens addObject:[HBSecurityAccessToken tokenWithObject:sourceTrack[keySubTrackSrtFileURL]]];
}
}
}
self.accessCount += 1;
return YES;
#else
return NO;
#endif
}
- (void)stopAccessingSecurityScopedResource
{
#ifdef __SANDBOX_ENABLED__
self.accessCount -= 1;
NSAssert(self.accessCount >= 0, @"[HBSubtitles stopAccessingSecurityScopedResource:] unbalanced call");
if (self.accessCount == 0)
{
[self.tokens removeAllObjects];
}
#endif
}
#pragma mark - NSCopying
- (instancetype)copyWithZone:(NSZone *)zone
{
HBSubtitles *copy = [[[self class] alloc] init];
if (copy)
{
copy->_container = _container;
copy->_sourceTracks = [_sourceTracks copy];
copy->_tracks = [[NSMutableArray alloc] init];
for (HBSubtitlesTrack *track in _tracks)
{
HBSubtitlesTrack *trackCopy = [track copy];
[copy->_tracks addObject:trackCopy];
trackCopy.dataSource = copy;
trackCopy.delegate = copy;
}
copy->_defaults = [_defaults copy];
copy->_tokens = [NSMutableArray array];
}
return copy;
}
#pragma mark - NSCoding
+ (BOOL)supportsSecureCoding
{
return YES;
}
- (void)encodeWithCoder:(NSCoder *)coder
{
[coder encodeInt:1 forKey:@"HBSubtitlesVersion"];
encodeInt(_container);
encodeObject(_sourceTracks);
encodeObject(_tracks);
encodeObject(_defaults);
}
- (instancetype)initWithCoder:(NSCoder *)decoder
{
self = [super init];
_tokens = [NSMutableArray array];
decodeInt(_container);
decodeCollectionOfObjects3(_sourceTracks, NSArray, NSDictionary, NSURL, NSData);
#ifdef __SANDBOX_ENABLED__
NSMutableArray *sourceTracks = [_sourceTracks mutableCopy];
for (NSDictionary *sourceTrack in _sourceTracks)
{
if (sourceTrack[keySubTrackSrtFileURLBookmark])
{
NSMutableDictionary *copy = [sourceTrack mutableCopy];
NSURL *srtURL = [HBUtilities URLFromBookmark:sourceTrack[keySubTrackSrtFileURLBookmark]];
if (srtURL)
{
copy[keySubTrackSrtFileURL] = srtURL;
}
[sourceTracks addObject:copy];
}
else
{
[sourceTracks addObject:sourceTrack];
}
}
_sourceTracks = [sourceTracks copy];
#endif
decodeCollectionOfObjects(_tracks, NSMutableArray, HBSubtitlesTrack);
for (HBSubtitlesTrack *track in _tracks)
{
track.dataSource = self;
track.delegate = self;
}
decodeObject(_defaults, HBSubtitlesDefaults);
return self;
}
#pragma mark - Presets
- (void)writeToPreset:(HBMutablePreset *)preset
{
[self.defaults writeToPreset:preset];
}
- (void)applyPreset:(HBPreset *)preset jobSettings:(NSDictionary *)settings
{
[self.defaults applyPreset:preset jobSettings:settings];
[self addDefaultTracksFromJobSettings:settings];
}
#pragma mark -
#pragma mark KVC
- (NSUInteger)countOfTracks
{
return self.tracks.count;
}
- (HBSubtitlesTrack *)objectInTracksAtIndex:(NSUInteger)index
{
return self.tracks[index];
}
- (void)insertObject:(HBSubtitlesTrack *)track inTracksAtIndex:(NSUInteger)index;
{
[[self.undo prepareWithInvocationTarget:self] removeObjectFromTracksAtIndex:index];
[self.tracks insertObject:track atIndex:index];
}
- (void)insertTracks:(NSArray *)array atIndexes:(NSIndexSet *)indexes
{
[[self.undo prepareWithInvocationTarget:self] removeTracksAtIndexes:indexes];
[self.tracks insertObjects:array atIndexes:indexes];
}
- (void)removeObjectFromTracksAtIndex:(NSUInteger)index
{
HBSubtitlesTrack *track = self.tracks[index];
[[self.undo prepareWithInvocationTarget:self] insertObject:track inTracksAtIndex:index];
[self.tracks removeObjectAtIndex:index];
}
- (void)removeTracksAtIndexes:(NSIndexSet *)indexes
{
NSArray *tracks = [self.tracks objectsAtIndexes:indexes];
[[self.undo prepareWithInvocationTarget:self] insertTracks:tracks atIndexes:indexes];
[self.tracks removeObjectsAtIndexes:indexes];
}
@end