1/* HBJob.m $ 2 3 This file is part of the HandBrake source code. 4 Homepage: <http://handbrake.fr/>. 5 It may be used under the terms of the GNU General Public License. */ 6 7#import "HBJob.h" 8#import "HBJob+Private.h" 9#import "HBTitle+Private.h" 10 11#import "HBAudioDefaults.h" 12#import "HBSubtitlesDefaults.h" 13#import "HBMutablePreset.h" 14 15#import "HBCodingUtilities.h" 16#import "HBLocalizationUtilities.h" 17#import "HBUtilities.h" 18#import "HBSecurityAccessToken.h" 19 20#include "handbrake/handbrake.h" 21 22NSString *HBContainerChangedNotification = @"HBContainerChangedNotification"; 23NSString *HBChaptersChangedNotification = @"HBChaptersChangedNotification"; 24 25@interface HBJob () 26 27@property (nonatomic, readonly) NSString *name; 28 29/** 30 Store the security scoped bookmarks, so we don't 31 regenerate it each time 32 */ 33@property (nonatomic, readonly) NSData *fileURLBookmark; 34@property (nonatomic, readwrite) NSData *outputURLFolderBookmark; 35 36/** 37 Keep track of security scoped resources status. 38 */ 39@property (nonatomic, readwrite) HBSecurityAccessToken *fileURLToken; 40@property (nonatomic, readwrite) HBSecurityAccessToken *outputURLToken; 41@property (nonatomic, readwrite) HBSecurityAccessToken *subtitlesToken; 42@property (nonatomic, readwrite) NSInteger accessCount; 43 44@end 45 46@implementation HBJob 47 48- (nullable instancetype)initWithTitle:(HBTitle *)title preset:(HBPreset *)preset 49{ 50 self = [super init]; 51 if (self) { 52 NSParameterAssert(title); 53 NSParameterAssert(preset); 54 55 _title = title; 56 _titleIdx = title.index; 57 58 _name = [title.name copy]; 59 _fileURL = title.url; 60 61 _container = HB_MUX_MP4; 62 _angle = 1; 63 64 _range = [[HBRange alloc] initWithTitle:title]; 65 _video = [[HBVideo alloc] initWithJob:self]; 66 _picture = [[HBPicture alloc] initWithTitle:title]; 67 _filters = [[HBFilters alloc] init]; 68 69 _audio = [[HBAudio alloc] initWithJob:self]; 70 _subtitles = [[HBSubtitles alloc] initWithJob:self]; 71 72 _chapterTitles = [title.chapters copy]; 73 _metadataPassthru = YES; 74 _presetName = @""; 75 76 if ([self applyPreset:preset error:NULL] == NO) 77 { 78 return nil; 79 } 80 } 81 82 return self; 83} 84 85#pragma mark - HBPresetCoding 86 87- (BOOL)applyPreset:(HBPreset *)preset error:(NSError * __autoreleasing *)outError 88{ 89 NSAssert(self.title, @"HBJob: calling applyPreset: without a valid title loaded"); 90 91 NSDictionary *jobSettings = [self.title jobSettingsWithPreset:preset]; 92 93 if (jobSettings) 94 { 95 self.presetName = preset.name; 96 97 self.container = hb_container_get_from_name([preset[@"FileFormat"] UTF8String]); 98 99 // MP4 specifics options. 100 self.mp4HttpOptimize = [preset[@"Mp4HttpOptimize"] boolValue]; 101 self.mp4iPodCompatible = [preset[@"Mp4iPodCompatible"] boolValue]; 102 103 self.alignAVStart = [preset[@"AlignAVStart"] boolValue]; 104 105 self.chaptersEnabled = [preset[@"ChapterMarkers"] boolValue]; 106 self.metadataPassthru = [preset[@"MetadataPassthrough"] boolValue]; 107 108 [self.audio applyPreset:preset jobSettings:jobSettings]; 109 [self.subtitles applyPreset:preset jobSettings:jobSettings]; 110 [self.video applyPreset:preset jobSettings:jobSettings]; 111 [self.picture applyPreset:preset jobSettings:jobSettings]; 112 [self.filters applyPreset:preset jobSettings:jobSettings]; 113 114 return YES; 115 } 116 else 117 { 118 if (outError != NULL) 119 { 120 *outError = [NSError errorWithDomain:@"HBError" code:0 userInfo:@{NSLocalizedDescriptionKey: NSLocalizedString(@"Invalid preset", @"HBJob -> invalid preset"), 121 NSLocalizedRecoverySuggestionErrorKey: NSLocalizedString(@"The preset is not a valid, try to select a different one.", @"Job preset -> invalid preset recovery suggestion")}]; 122 } 123 124 return NO; 125 } 126} 127 128- (void)writeToPreset:(HBMutablePreset *)preset 129{ 130 preset.name = self.presetName; 131 132 preset[@"FileFormat"] = @(hb_container_get_short_name(self.container)); 133 134 // MP4 specifics options. 135 preset[@"Mp4HttpOptimize"] = @(self.mp4HttpOptimize); 136 preset[@"AlignAVStart"] = @(self.alignAVStart); 137 preset[@"Mp4iPodCompatible"] = @(self.mp4iPodCompatible); 138 139 preset[@"ChapterMarkers"] = @(self.chaptersEnabled); 140 preset[@"MetadataPassthrough"] = @(self.metadataPassthru); 141 142 [@[self.video, self.filters, self.picture, self.audio, self.subtitles] makeObjectsPerformSelector:@selector(writeToPreset:) 143 withObject:preset]; 144} 145 146- (void)setUndo:(NSUndoManager *)undo 147{ 148 _undo = undo; 149 [@[self.video, self.range, self.filters, self.picture, self.audio, self.subtitles] makeObjectsPerformSelector:@selector(setUndo:) 150 withObject:_undo]; 151 [self.chapterTitles makeObjectsPerformSelector:@selector(setUndo:) withObject:_undo]; 152} 153 154- (void)setPresetName:(NSString *)presetName 155{ 156 if (![presetName isEqualToString:_presetName]) 157 { 158 [[self.undo prepareWithInvocationTarget:self] setPresetName:_presetName]; 159 } 160 _presetName = [presetName copy]; 161} 162 163- (void)setOutputURL:(NSURL *)outputURL 164{ 165 if (![outputURL isEqualTo:_outputURL]) 166 { 167 [[self.undo prepareWithInvocationTarget:self] setOutputURL:_outputURL]; 168 } 169 _outputURL = [outputURL copy]; 170 171#ifdef __SANDBOX_ENABLED__ 172 // Clear the bookmark to regenerate it later 173 self.outputURLFolderBookmark = nil; 174#endif 175} 176 177- (void)setOutputFileName:(NSString *)outputFileName 178{ 179 if (![outputFileName isEqualTo:_outputFileName]) 180 { 181 [[self.undo prepareWithInvocationTarget:self] setOutputFileName:_outputFileName]; 182 } 183 _outputFileName = [outputFileName copy]; 184} 185 186- (BOOL)validateOutputFileName:(id *)ioValue error:(NSError * __autoreleasing *)outError 187{ 188 BOOL retval = YES; 189 190 if (nil != *ioValue) 191 { 192 NSString *value = *ioValue; 193 194 if ([value rangeOfString:@"/"].location != NSNotFound) 195 { 196 if (outError) 197 { 198 *outError = [NSError errorWithDomain:@"HBError" code:0 userInfo:@{NSLocalizedDescriptionKey: HBKitLocalizedString(@"Invalid name", @"HBJob -> invalid name error description"), 199 NSLocalizedRecoverySuggestionErrorKey: HBKitLocalizedString(@"The file name can't contain the / character.", @"HBJob -> invalid name error recovery suggestion")}]; 200 } 201 return NO; 202 } 203 if (value.length == 0) 204 { 205 if (outError) 206 { 207 *outError = [NSError errorWithDomain:@"HBError" code:0 userInfo:@{NSLocalizedDescriptionKey: HBKitLocalizedString(@"Invalid name", @"HBJob -> invalid name error description"), 208 NSLocalizedRecoverySuggestionErrorKey: HBKitLocalizedString(@"The file name can't be empty.", @"HBJob -> invalid name error recovery suggestion")}]; 209 } 210 return NO; 211 } 212 } 213 214 if (*ioValue == nil) 215 { 216 if (outError) 217 { 218 *outError = [NSError errorWithDomain:@"HBError" code:0 userInfo:@{NSLocalizedDescriptionKey: HBKitLocalizedString(@"Invalid name", @"HBJob -> invalid name error description"), 219 NSLocalizedRecoverySuggestionErrorKey: HBKitLocalizedString(@"The file name can't be empty.", @"HBJob -> invalid name error recovery suggestion")}]; 220 } 221 return NO; 222 } 223 224 return retval; 225} 226 227- (NSURL *)completeOutputURL 228{ 229 return [self.outputURL URLByAppendingPathComponent:self.outputFileName]; 230} 231 232- (void)setContainer:(int)container 233{ 234 if (container != _container) 235 { 236 [[self.undo prepareWithInvocationTarget:self] setContainer:_container]; 237 } 238 239 _container = container; 240 241 [self.audio setContainer:container]; 242 [self.subtitles setContainer:container]; 243 [self.video containerChanged]; 244 245 // post a notification for any interested observers to indicate that our video container has changed 246 [[NSNotificationCenter defaultCenter] postNotificationName:HBContainerChangedNotification object:self]; 247} 248 249- (void)setAngle:(int)angle 250{ 251 if (angle != _angle) 252 { 253 [[self.undo prepareWithInvocationTarget:self] setAngle:_angle]; 254 } 255 _angle = angle; 256} 257 258- (void)setTitle:(HBTitle *)title 259{ 260 _title = title; 261 self.range.title = title; 262} 263 264- (void)setMp4HttpOptimize:(BOOL)mp4HttpOptimize 265{ 266 if (mp4HttpOptimize != _mp4HttpOptimize) 267 { 268 [[self.undo prepareWithInvocationTarget:self] setMp4HttpOptimize:_mp4HttpOptimize]; 269 } 270 _mp4HttpOptimize = mp4HttpOptimize; 271 272 [[NSNotificationCenter defaultCenter] postNotificationName:HBContainerChangedNotification object:self]; 273} 274 275- (void)setAlignAVStart:(BOOL)alignAVStart 276{ 277 if (alignAVStart != _alignAVStart) 278 { 279 [[self.undo prepareWithInvocationTarget:self] setAlignAVStart:_alignAVStart]; 280 } 281 _alignAVStart = alignAVStart; 282 283 [[NSNotificationCenter defaultCenter] postNotificationName:HBContainerChangedNotification object:self]; 284} 285 286- (void)setMp4iPodCompatible:(BOOL)mp4iPodCompatible 287{ 288 if (mp4iPodCompatible != _mp4iPodCompatible) 289 { 290 [[self.undo prepareWithInvocationTarget:self] setMp4iPodCompatible:_mp4iPodCompatible]; 291 } 292 _mp4iPodCompatible = mp4iPodCompatible; 293 294 [[NSNotificationCenter defaultCenter] postNotificationName:HBContainerChangedNotification object:self]; 295} 296 297- (void)setChaptersEnabled:(BOOL)chaptersEnabled 298{ 299 if (chaptersEnabled != _chaptersEnabled) 300 { 301 [[self.undo prepareWithInvocationTarget:self] setChaptersEnabled:_chaptersEnabled]; 302 } 303 _chaptersEnabled = chaptersEnabled; 304 [[NSNotificationCenter defaultCenter] postNotificationName:HBChaptersChangedNotification object:self]; 305} 306 307- (void)setMetadataPassthru:(BOOL)metadataPassthru 308{ 309 if (metadataPassthru != _metadataPassthru) 310 { 311 [[self.undo prepareWithInvocationTarget:self] setMetadataPassthru:_metadataPassthru]; 312 } 313 _metadataPassthru = metadataPassthru; 314 [[NSNotificationCenter defaultCenter] postNotificationName:HBContainerChangedNotification object:self]; 315} 316 317- (NSString *)description 318{ 319 return self.name; 320} 321 322- (void)refreshSecurityScopedResources 323{ 324 if (_fileURLBookmark) 325 { 326 NSURL *resolvedURL = [HBUtilities URLFromBookmark:_fileURLBookmark]; 327 if (resolvedURL) 328 { 329 _fileURL = resolvedURL; 330 } 331 } 332 if (_outputURLFolderBookmark) 333 { 334 NSURL *resolvedURL = [HBUtilities URLFromBookmark:_outputURLFolderBookmark]; 335 if (resolvedURL) 336 { 337 _outputURL = resolvedURL; 338 } 339 } 340 [self.subtitles refreshSecurityScopedResources]; 341} 342 343- (BOOL)startAccessingSecurityScopedResource 344{ 345#ifdef __SANDBOX_ENABLED__ 346 if (self.accessCount == 0) 347 { 348 self.fileURLToken = [HBSecurityAccessToken tokenWithObject:self.fileURL]; 349 self.outputURLToken = [HBSecurityAccessToken tokenWithObject:self.outputURL]; 350 self.subtitlesToken = [HBSecurityAccessToken tokenWithObject:self.subtitles]; 351 } 352 self.accessCount += 1; 353 return YES; 354#else 355 return NO; 356#endif 357} 358 359- (void)stopAccessingSecurityScopedResource 360{ 361#ifdef __SANDBOX_ENABLED__ 362 self.accessCount -= 1; 363 NSAssert(self.accessCount >= 0, @"[HBJob stopAccessingSecurityScopedResource:] unbalanced call"); 364 if (self.accessCount == 0) 365 { 366 self.fileURLToken = nil; 367 self.outputURLToken = nil; 368 self.subtitlesToken = nil; 369 } 370#endif 371} 372 373#pragma mark - NSCopying 374 375- (instancetype)copyWithZone:(NSZone *)zone 376{ 377 HBJob *copy = [[[self class] alloc] init]; 378 379 if (copy) 380 { 381 copy->_name = [_name copy]; 382 copy->_presetName = [_presetName copy]; 383 copy->_titleIdx = _titleIdx; 384 385 copy->_fileURLBookmark = [_fileURLBookmark copy]; 386 copy->_outputURLFolderBookmark = [_outputURLFolderBookmark copy]; 387 388 copy->_fileURL = [_fileURL copy]; 389 copy->_outputURL = [_outputURL copy]; 390 copy->_outputFileName = [_outputFileName copy]; 391 392 copy->_container = _container; 393 copy->_angle = _angle; 394 copy->_mp4HttpOptimize = _mp4HttpOptimize; 395 copy->_mp4iPodCompatible = _mp4iPodCompatible; 396 copy->_alignAVStart = _alignAVStart; 397 398 copy->_range = [_range copy]; 399 copy->_video = [_video copy]; 400 copy->_picture = [_picture copy]; 401 copy->_filters = [_filters copy]; 402 403 copy->_video.job = copy; 404 405 copy->_audio = [_audio copy]; 406 copy->_audio.job = copy; 407 copy->_subtitles = [_subtitles copy]; 408 copy->_subtitles.job = copy; 409 410 copy->_chaptersEnabled = _chaptersEnabled; 411 copy->_chapterTitles = [[NSArray alloc] initWithArray:_chapterTitles copyItems:YES]; 412 413 copy->_metadataPassthru = _metadataPassthru; 414 } 415 416 return copy; 417} 418 419#pragma mark - NSCoding 420 421+ (BOOL)supportsSecureCoding 422{ 423 return YES; 424} 425 426- (void)encodeWithCoder:(NSCoder *)coder 427{ 428 [coder encodeInt:5 forKey:@"HBJobVersion"]; 429 430 encodeObject(_name); 431 encodeObject(_presetName); 432 encodeInt(_titleIdx); 433 434#ifdef __SANDBOX_ENABLED__ 435 if (!_fileURLBookmark) 436 { 437 _fileURLBookmark = [HBUtilities bookmarkFromURL:_fileURL 438 options:NSURLBookmarkCreationWithSecurityScope | 439 NSURLBookmarkCreationSecurityScopeAllowOnlyReadAccess]; 440 } 441 442 encodeObject(_fileURLBookmark); 443 444 if (!_outputURLFolderBookmark) 445 { 446 __attribute__((unused)) HBSecurityAccessToken *token = [HBSecurityAccessToken tokenWithObject:_outputURL]; 447 _outputURLFolderBookmark = [HBUtilities bookmarkFromURL:_outputURL]; 448 token = nil; 449 } 450 451 encodeObject(_outputURLFolderBookmark); 452 453#endif 454 455 encodeObject(_fileURL); 456 encodeObject(_outputURL); 457 encodeObject(_outputFileName); 458 459 encodeInt(_container); 460 encodeInt(_angle); 461 encodeBool(_mp4HttpOptimize); 462 encodeBool(_mp4iPodCompatible); 463 encodeBool(_alignAVStart); 464 465 encodeObject(_range); 466 encodeObject(_video); 467 encodeObject(_picture); 468 encodeObject(_filters); 469 470 encodeObject(_audio); 471 encodeObject(_subtitles); 472 473 encodeBool(_chaptersEnabled); 474 encodeObject(_chapterTitles); 475 476 encodeBool(_metadataPassthru); 477} 478 479- (instancetype)initWithCoder:(NSCoder *)decoder 480{ 481 int version = [decoder decodeIntForKey:@"HBJobVersion"]; 482 483 if (version == 5 && (self = [super init])) 484 { 485 decodeObjectOrFail(_name, NSString); 486 decodeObjectOrFail(_presetName, NSString); 487 decodeInt(_titleIdx); if (_titleIdx < 0) { goto fail; } 488 489#ifdef __SANDBOX_ENABLED__ 490 decodeObject(_fileURLBookmark, NSData) 491 decodeObject(_outputURLFolderBookmark, NSData) 492#endif 493 decodeObjectOrFail(_fileURL, NSURL); 494 decodeObject(_outputURL, NSURL); 495 decodeObject(_outputFileName, NSString); 496 497 decodeInt(_container); if (_container != HB_MUX_MP4 && _container != HB_MUX_MKV && _container != HB_MUX_WEBM) { goto fail; } 498 decodeInt(_angle); if (_angle < 0) { goto fail; } 499 decodeBool(_mp4HttpOptimize); 500 decodeBool(_mp4iPodCompatible); 501 decodeBool(_alignAVStart); 502 503 decodeObjectOrFail(_range, HBRange); 504 decodeObjectOrFail(_video, HBVideo); 505 decodeObjectOrFail(_picture, HBPicture); 506 decodeObjectOrFail(_filters, HBFilters); 507 508 _video.job = self; 509 510 decodeObjectOrFail(_audio, HBAudio); 511 decodeObjectOrFail(_subtitles, HBSubtitles); 512 513 _audio.job = self; 514 _video.job = self; 515 516 decodeBool(_chaptersEnabled); 517 decodeCollectionOfObjectsOrFail(_chapterTitles, NSArray, HBChapter); 518 519 decodeBool(_metadataPassthru); 520 521 return self; 522 } 523 524fail: 525 return nil; 526} 527 528@end 529