/* HBJob.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 "HBJob.h"
#import "HBPreset.h"
#import "HBAudio.h"
#import "HBAudioController.h"
#import "HBSubtitlesController.h"
#import "NSCodingMacro.h"
NSString *keyAudioTrackIndex = @"keyAudioTrackIndex";
NSString *keyAudioTrackName = @"keyAudioTrackName";
NSString *keyAudioInputBitrate = @"keyAudioInputBitrate";
NSString *keyAudioInputSampleRate = @"keyAudioInputSampleRate";
NSString *keyAudioInputCodec = @"keyAudioInputCodec";
NSString *keyAudioInputCodecParam = @"keyAudioInputCodecParam";
NSString *keyAudioInputChannelLayout = @"keyAudioInputChannelLayout";
NSString *keyAudioTrackLanguageIsoCode = @"keyAudioTrackLanguageIsoCode";
NSString *HBMixdownChangedNotification = @"HBMixdownChangedNotification";
NSString *HBContainerChangedNotification = @"HBContainerChangedNotification";
NSString *keyContainerTag = @"keyContainerTag";
@implementation HBJob
- (instancetype)initWithTitle:(HBTitle *)title andPreset:(HBPreset *)preset
{
self = [super init];
if (self) {
NSParameterAssert(title);
NSParameterAssert(preset);
_title = title;
_titleIdx = title.hb_title->index;
_fileURL = [[NSURL fileURLWithPath:@(title.hb_title->path)] retain];
_container = HB_MUX_MP4;
_angle = 1;
_audioDefaults = [[HBAudioDefaults alloc] init];
_subtitlesDefaults = [[HBSubtitlesDefaults alloc] init];
_range = [[HBRange alloc] initWithTitle:title];
_video = [[HBVideo alloc] initWithJob:self];
_picture = [[HBPicture alloc] initWithTitle:title];
_filters = [[HBFilters alloc] init];
_audioTracks = [[NSMutableArray alloc] init];
_subtitlesTracks = [[NSMutableArray alloc] init];
_chapterTitles = [title.chapters mutableCopy];
[self applyPreset:preset];
}
return self;
}
- (void)applyPreset:(HBPreset *)preset
{
NSDictionary *content = preset.content;
self.container = hb_container_get_from_name(hb_container_sanitize_name([content[@"FileFormat"] UTF8String]));
// MP4 specifics options.
self.mp4HttpOptimize = [content[@"Mp4HttpOptimize"] boolValue];
self.mp4iPodCompatible = [content[@"Mp4iPodCompatible"] boolValue];
// Chapter Markers
self.chaptersEnabled = [content[@"ChapterMarkers"] boolValue];
[@[self.audioDefaults, self.subtitlesDefaults, self.filters, self.picture, self.video, ] makeObjectsPerformSelector:@selector(applyPreset:)
withObject:content];
}
- (void)applyCurrentSettingsToPreset:(NSMutableDictionary *)dict
{
dict[@"FileFormat"] = @(hb_container_get_name(self.container));
dict[@"ChapterMarkers"] = @(self.chaptersEnabled);
// MP4 specifics options.
dict[@"Mp4HttpOptimize"] = @(self.mp4HttpOptimize);
dict[@"Mp4iPodCompatible"] = @(self.mp4iPodCompatible);
[@[self.video, self.filters, self.picture, self.audioDefaults, self.subtitlesDefaults] makeObjectsPerformSelector:@selector(writeToPreset:)
withObject:dict];
}
- (void)setContainer:(int)container
{
_container = container;
[self.video containerChanged];
/* post a notification for any interested observers to indicate that our video container has changed */
[[NSNotificationCenter defaultCenter] postNotification:
[NSNotification notificationWithName:HBContainerChangedNotification
object:self
userInfo:@{keyContainerTag: @(self.container)}]];
}
- (void)setTitle:(HBTitle *)title
{
_title = title;
self.range.title = title;
self.picture.title = title;
}
+ (NSSet *)keyPathsForValuesAffectingValueForKey:(NSString *)key
{
NSSet *retval = nil;
if ([key isEqualToString:@"mp4OptionsEnabled"])
{
retval = [NSSet setWithObjects:@"container", nil];
}
return retval;
}
- (void)dealloc
{
[_audioTracks release];
[_subtitlesTracks release];
[_fileURL release];
[_destURL release];
[_range release];
[_video release];
[_picture release];
[_filters release];
[_audioDefaults release];
[_subtitlesDefaults release];
[_chapterTitles release];
[super dealloc];
}
/**
* Prepares a hb_job_t
*/
- (hb_job_t *)hb_job
{
NSAssert(self.title, @"HBJob: calling hb_job without a valid title loaded");
hb_title_t *title = self.title.hb_title;
hb_job_t *job = hb_job_init(title);
hb_job_set_file(job, self.destURL.path.fileSystemRepresentation);
// Title Angle for dvdnav
job->angle = self.angle;
if (self.range.type == HBRangeTypeChapters)
{
// Chapter selection
job->chapter_start = self.range.chapterStart + 1;
job->chapter_end = self.range.chapterStop + 1;
}
else if (self.range.type == HBRangeTypeSeconds)
{
// we are pts based start / stop
// Point A to Point B. Time to time in seconds.
// get the start seconds from the start seconds field
int start_seconds = self.range.secondsStart;
job->pts_to_start = start_seconds * 90000LL;
// Stop seconds is actually the duration of encode, so subtract the end seconds from the start seconds
int stop_seconds = self.range.secondsStop;
job->pts_to_stop = stop_seconds * 90000LL;
}
else if (self.range.type == HBRangeTypeFrames)
{
// we are frame based start / stop
//Point A to Point B. Frame to frame
// get the start frame from the start frame field
int start_frame = self.range.frameStart;
job->frame_to_start = start_frame;
// get the frame to stop on from the end frame field
int stop_frame = self.range.frameStop;
job->frame_to_stop = stop_frame;
}
// Format (Muxer) and Video Encoder
job->mux = self.container;
job->vcodec = self.video.encoder;
// We set http optimized mp4 here
job->mp4_optimize = self.mp4HttpOptimize;
// We set the chapter marker extraction here based on the format being
// mpeg4 or mkv and the checkbox being checked.
if (self.chaptersEnabled)
{
job->chapter_markers = 1;
// now lets get our saved chapter names out the array in the queue file
// and insert them back into the title chapter list. We have it here,
// because unless we are inserting chapter markers there is no need to
// spend the overhead of iterating through the chapter names array imo
// Also, note that if for some reason we don't apply chapter names, the
// chapters just come out 001, 002, etc. etc.
int i = 0;
for (NSString *name in self.chapterTitles)
{
hb_chapter_t *chapter = (hb_chapter_t *) hb_list_item(job->list_chapter, i);
if (chapter != NULL)
{
hb_chapter_set_title(chapter, name.UTF8String);
}
i++;
}
}
else
{
job->chapter_markers = 0;
}
if (job->vcodec == HB_VCODEC_X264 || job->vcodec == HB_VCODEC_X265)
{
// iPod 5G atom
job->ipod_atom = self.mp4iPodCompatible;
// set fastfirstpass if 2-pass and Turbo are enabled
if (self.video.twoPass)
{
job->fastfirstpass = self.video.turboTwoPass;
}
// advanced x264 options
NSString *tmpString;
// translate zero-length strings to NULL for libhb
const char *encoder_preset = NULL;
const char *encoder_tune = NULL;
const char *encoder_options = NULL;
const char *encoder_profile = NULL;
const char *encoder_level = NULL;
if (self.video.advancedOptions)
{
// we are using the advanced panel
if ([(tmpString = self.video.videoOptionExtra) length])
{
encoder_options = tmpString.UTF8String;
}
}
else
{
// we are using the x264 preset system
if ([(tmpString = self.video.tune) length])
{
encoder_tune = [tmpString UTF8String];
}
if ([(tmpString = self.video.videoOptionExtra) length])
{
encoder_options = [tmpString UTF8String];
}
if ([(tmpString = self.video.profile) length])
{
encoder_profile = [tmpString UTF8String];
}
if ([(tmpString = self.video.level) length])
{
encoder_level = [tmpString UTF8String];
}
encoder_preset = self.video.preset.UTF8String;
}
hb_job_set_encoder_preset (job, encoder_preset);
hb_job_set_encoder_tune (job, encoder_tune);
hb_job_set_encoder_options(job, encoder_options);
hb_job_set_encoder_profile(job, encoder_profile);
hb_job_set_encoder_level (job, encoder_level);
}
else if (job->vcodec & HB_VCODEC_FFMPEG_MASK)
{
hb_job_set_encoder_options(job, self.video.videoOptionExtra.UTF8String);
}
// Picture Size Settings
job->par.num = self.picture.parWidth;
job->par.den = self.picture.parHeight;
// Video settings
// Framerate
int fps_mode, fps_num, fps_den;
if (self.video.frameRate > 0)
{
// a specific framerate has been chosen
fps_num = 27000000;
fps_den = self.video.frameRate;
if (self.video.frameRateMode == 1)
{
// CFR
fps_mode = 1;
}
else
{
// PFR
fps_mode = 2;
}
}
else
{
// same as source
fps_num = title->vrate.num;
fps_den = title->vrate.den;
if (self.video.frameRateMode == 1)
{
// CFR
fps_mode = 1;
}
else
{
// VFR
fps_mode = 0;
}
}
switch (self.video.qualityType)
{
case 0:
// ABR
job->vquality = -1.0;
job->vbitrate = self.video.avgBitrate;
break;
case 1:
// Constant Quality
job->vquality = self.video.quality;
job->vbitrate = 0;
break;
}
job->grayscale = self.filters.grayscale;
// Map the settings in the dictionaries for the SubtitleList array to match title->list_subtitle
BOOL one_burned = NO;
int i = 0;
for (NSDictionary *subtitleDict in self.subtitlesTracks)
{
int subtitle = [subtitleDict[keySubTrackIndex] intValue];
int force = [subtitleDict[keySubTrackForced] intValue];
int burned = [subtitleDict[keySubTrackBurned] intValue];
int def = [subtitleDict[keySubTrackDefault] intValue];
// if i is 0, then we are in the first item of the subtitles which we need to
// check for the "Foreign Audio Search" which would be keySubTrackIndex of -1
// if we are on the first track and using "Foreign Audio Search"
if (i == 0 && subtitle == -1)
{
job->indepth_scan = 1;
if (burned != 1)
{
job->select_subtitle_config.dest = PASSTHRUSUB;
}
else
{
job->select_subtitle_config.dest = RENDERSUB;
}
job->select_subtitle_config.force = force;
job->select_subtitle_config.default_track = def;
}
else
{
// if we are getting the subtitles from an external srt file
if ([subtitleDict[keySubTrackType] intValue] == SRTSUB)
{
hb_subtitle_config_t sub_config;
sub_config.offset = [subtitleDict[keySubTrackSrtOffset] intValue];
// we need to srncpy file name and codeset
strncpy(sub_config.src_filename, [subtitleDict[keySubTrackSrtFilePath] UTF8String], 255);
sub_config.src_filename[255] = 0;
strncpy(sub_config.src_codeset, [subtitleDict[keySubTrackSrtCharCode] UTF8String], 39);
sub_config.src_codeset[39] = 0;
if (!burned && hb_subtitle_can_pass(SRTSUB, job->mux))
{
sub_config.dest = PASSTHRUSUB;
}
else if (hb_subtitle_can_burn(SRTSUB))
{
// Only allow one subtitle to be burned into the video
if (one_burned)
continue;
one_burned = TRUE;
sub_config.dest = RENDERSUB;
}
sub_config.force = 0;
sub_config.default_track = def;
hb_srt_add( job, &sub_config, [subtitleDict[keySubTrackLanguageIsoCode] UTF8String]);
continue;
}
// We are setting a source subtitle so access the source subtitle info
hb_subtitle_t * subt = (hb_subtitle_t *) hb_list_item(title->list_subtitle, subtitle);
if (subt != NULL)
{
hb_subtitle_config_t sub_config = subt->config;
if (!burned && hb_subtitle_can_pass(subt->source, job->mux))
{
sub_config.dest = PASSTHRUSUB;
}
else if (hb_subtitle_can_burn(subt->source))
{
// Only allow one subtitle to be burned into the video
if (one_burned)
continue;
one_burned = TRUE;
sub_config.dest = RENDERSUB;
}
sub_config.force = force;
sub_config.default_track = def;
hb_subtitle_add(job, &sub_config, subtitle);
}
}
i++;
}
if (one_burned)
{
hb_filter_object_t *filter = hb_filter_init( HB_FILTER_RENDER_SUB );
hb_add_filter( job, filter, [[NSString stringWithFormat:@"%d:%d:%d:%d",
job->crop[0], job->crop[1],
job->crop[2], job->crop[3]] UTF8String] );
}
// Audio Defaults
job->acodec_copy_mask = 0;
if (self.audioDefaults.allowAACPassthru)
{
job->acodec_copy_mask |= HB_ACODEC_FFAAC;
}
if (self.audioDefaults.allowAC3Passthru)
{
job->acodec_copy_mask |= HB_ACODEC_AC3;
}
if (self.audioDefaults.allowDTSHDPassthru)
{
job->acodec_copy_mask |= HB_ACODEC_DCA_HD;
}
if (self.audioDefaults.allowDTSPassthru)
{
job->acodec_copy_mask |= HB_ACODEC_DCA;
}
if (self.audioDefaults.allowMP3Passthru)
{
job->acodec_copy_mask |= HB_ACODEC_MP3;
}
job->acodec_fallback = self.audioDefaults.encoderFallback;
// Audio tracks and mixdowns
// Now lets add our new tracks to the audio list here
for (HBAudio *audioTrack in self.audioTracks)
{
if (audioTrack.enabled)
{
hb_audio_config_t *audio = (hb_audio_config_t *)calloc(1, sizeof(*audio));
hb_audio_config_init(audio);
NSNumber *sampleRateToUse = ([audioTrack.sampleRate[keyAudioSamplerate] intValue] == 0 ?
audioTrack.track[keyAudioInputSampleRate] :
audioTrack.sampleRate[keyAudioSamplerate]);
audio->in.track = [audioTrack.track[keyAudioTrackIndex] intValue] -1;
// We go ahead and assign values to our audio->out.
audio->out.track = audio->in.track;
audio->out.codec = [audioTrack.codec[keyAudioCodec] intValue];
audio->out.compression_level = hb_audio_compression_get_default(audio->out.codec);
audio->out.mixdown = [audioTrack.mixdown[keyAudioMixdown] intValue];
audio->out.normalize_mix_level = 0;
audio->out.bitrate = [audioTrack.bitRate[keyAudioBitrate] intValue];
audio->out.samplerate = [sampleRateToUse intValue];
audio->out.dither_method = hb_audio_dither_get_default();
// output is not passthru so apply gain
if (!([[audioTrack codec][keyAudioCodec] intValue] & HB_ACODEC_PASS_FLAG))
{
audio->out.gain = [audioTrack.gain doubleValue];
}
else
{
// output is passthru - the Gain dial is disabled so don't apply its value
audio->out.gain = 0;
}
if (hb_audio_can_apply_drc([audioTrack.track[keyAudioInputCodec] intValue],
[audioTrack.track[keyAudioInputCodecParam] intValue],
[audioTrack.codec[keyAudioCodec] intValue]))
{
audio->out.dynamic_range_compression = [audioTrack.drc doubleValue];
}
else
{
// source isn't AC3 or output is passthru - the DRC dial is disabled so don't apply its value
audio->out.dynamic_range_compression = 0;
}
hb_audio_add(job, audio);
free(audio);
}
}
// Now lets call the filters if applicable.
// The order of the filters is critical
// Detelecine
hb_filter_object_t *filter;
if (self.filters.detelecine == 1)
{
filter = hb_filter_init(HB_FILTER_DETELECINE);
// use a custom detelecine string
hb_add_filter(job, filter, self.filters.detelecineCustomString.UTF8String);
}
else if (self.filters.detelecine == 2)
{
filter = hb_filter_init(HB_FILTER_DETELECINE);
// Use libhb's default values
hb_add_filter(job, filter, NULL);
}
if (self.filters.useDecomb && self.filters.decomb)
{
// Decomb
filter = hb_filter_init(HB_FILTER_DECOMB);
if (self.filters.decomb == 1)
{
// use a custom decomb string */
hb_add_filter(job, filter, self.filters.decombCustomString.UTF8String);
}
else if (self.filters.decomb == 2)
{
// use libhb defaults
hb_add_filter(job, filter, NULL);
}
else if (self.filters.decomb == 3)
{
// use old defaults (decomb fast)
hb_add_filter(job, filter, "7:2:6:9:1:80");
}
else if (self.filters.decomb == 4)
{
// decomb 3 with bobbing enabled
hb_add_filter(job, filter, "455");
}
}
else if (self.filters.deinterlace)
{
// Deinterlace
filter = hb_filter_init(HB_FILTER_DEINTERLACE);
if (self.filters.deinterlace == 1)
{
// we add the custom string if present
hb_add_filter(job, filter, self.filters.deinterlaceCustomString.UTF8String);
}
else if (self.filters.deinterlace == 2)
{
// Run old deinterlacer fd by default
hb_add_filter(job, filter, "0");
}
else if (self.filters.deinterlace == 3)
{
// Yadif mode 0 (without spatial deinterlacing)
hb_add_filter(job, filter, "1");
}
else if (self.filters.deinterlace == 4)
{
// Yadif (with spatial deinterlacing)
hb_add_filter(job, filter, "3");
}
else if (self.filters.deinterlace == 5)
{
// Yadif (with spatial deinterlacing and bobbing)
hb_add_filter(job, filter, "15");
}
}
// Denoise
if (![self.filters.denoise isEqualToString:@"off"])
{
int filter_id = HB_FILTER_HQDN3D;
if ([self.filters.denoise isEqualToString:@"nlmeans"])
filter_id = HB_FILTER_NLMEANS;
if ([self.filters.denoisePreset isEqualToString:@"none"])
{
const char *filter_str;
filter_str = self.filters.denoiseCustomString.UTF8String;
filter = hb_filter_init(filter_id);
hb_add_filter(job, filter, filter_str);
}
else
{
const char *filter_str, *preset, *tune;
preset = self.filters.denoisePreset.UTF8String;
tune = self.filters.denoiseTune.UTF8String;
filter_str = hb_generate_filter_settings(filter_id, preset, tune);
filter = hb_filter_init(filter_id);
hb_add_filter(job, filter, filter_str);
}
}
// Deblock (uses pp7 default)
// NOTE: even though there is a valid deblock setting of 0 for the filter, for
// the macgui's purposes a value of 0 actually means to not even use the filter
// current hb_filter_deblock.settings valid ranges are from 5 - 15
if (self.filters.deblock != 0)
{
filter = hb_filter_init(HB_FILTER_DEBLOCK);
hb_add_filter(job, filter, [NSString stringWithFormat:@"%ld", (long)self.filters.deblock].UTF8String);
}
// Add Crop/Scale filter
filter = hb_filter_init(HB_FILTER_CROP_SCALE);
hb_add_filter( job, filter, [NSString stringWithFormat:@"%d:%d:%d:%d:%d:%d",
self.picture.width, self.picture.height,
self.picture.cropTop, self.picture.cropBottom,
self.picture.cropLeft, self.picture.cropRight].UTF8String);
// Add framerate shaping filter
filter = hb_filter_init(HB_FILTER_VFR);
hb_add_filter(job, filter, [[NSString stringWithFormat:@"%d:%d:%d",
fps_mode, fps_num, fps_den] UTF8String]);
return job;
}
#pragma mark - NSCopying
- (instancetype)copyWithZone:(NSZone *)zone
{
HBJob *copy = [[[self class] alloc] init];
if (copy)
{
copy->_state = HBJobStateReady;
copy->_titleIdx = _titleIdx;
copy->_pidId = _pidId;
copy->_fileURL = [_fileURL copy];
copy->_destURL = [_destURL copy];
copy->_container = _container;
copy->_angle = _angle;
copy->_mp4HttpOptimize = _mp4HttpOptimize;
copy->_mp4iPodCompatible = _mp4iPodCompatible;
copy->_range = [_range copy];
copy->_video = [_video copy];
copy->_picture = [_picture copy];
copy->_filters = [_filters copy];
// Copy the tracks, but not the last one because it's empty.
copy->_audioTracks = [[NSMutableArray alloc] init];
[_audioTracks enumerateObjectsUsingBlock:^(id obj, NSUInteger idx, BOOL *stop) {
if (idx < _audioTracks.count - 1)
{
[copy->_audioTracks addObject:[[obj copy] autorelease]];
}
}];
copy->_subtitlesTracks = [[NSMutableArray alloc] init];
[_subtitlesTracks enumerateObjectsUsingBlock:^(id obj, NSUInteger idx, BOOL *stop) {
if (idx < _subtitlesTracks.count - 1)
{
[copy->_subtitlesTracks addObject:[[obj copy] autorelease]];
}
}];
copy->_chaptersEnabled = _chaptersEnabled;
copy->_chapterTitles = [[NSMutableArray alloc] initWithArray:_chapterTitles copyItems:YES];
copy->_audioDefaults = [_audioDefaults copy];
copy->_subtitlesDefaults = [_subtitlesDefaults copy];
}
return copy;
}
#pragma mark - NSCoding
- (void)encodeWithCoder:(NSCoder *)coder
{
[coder encodeInt:1 forKey:@"HBVideoVersion"];
encodeInt(_state);
encodeInt(_titleIdx);
encodeInt(_pidId);
encodeObject(_fileURL);
encodeObject(_destURL);
encodeInt(_container);
encodeInt(_angle);
encodeBool(_mp4HttpOptimize);
encodeBool(_mp4iPodCompatible);
encodeObject(_range);
encodeObject(_video);
encodeObject(_picture);
encodeObject(_filters);
encodeObject(_audioTracks);
encodeObject(_subtitlesTracks);
encodeBool(_chaptersEnabled);
encodeObject(_chapterTitles);
encodeObject(_audioDefaults);
encodeObject(_subtitlesDefaults);
}
- (id)initWithCoder:(NSCoder *)decoder
{
self = [super init];
decodeInt(_state);
decodeInt(_titleIdx);
decodeInt(_pidId);
decodeObject(_fileURL);
decodeObject(_destURL);
decodeInt(_container);
decodeInt(_angle);
decodeBool(_mp4HttpOptimize);
decodeBool(_mp4iPodCompatible);
decodeObject(_range);
decodeObject(_video);
decodeObject(_picture);
decodeObject(_filters);
_video.job = self;
decodeObject(_audioTracks);
decodeObject(_subtitlesTracks);
decodeBool(_chaptersEnabled);
decodeObject(_chapterTitles);
decodeObject(_audioDefaults);
decodeObject(_subtitlesDefaults);
return self;
}
@end