/* HBJob+HBJobConversion.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+HBJobConversion.h"
#import "HBAudioDefaults.h"
#import "HBAudioTrack.h"
#import "HBSubtitlesTrack.h"
#import "HBChapter.h"
#import "HBTitle+Private.h"
#import "HBMutablePreset.h"
#import "HBRange.h"
#import "HBVideo.h"
#import "HBPicture.h"
#import "HBFilters.h"
#import "HBAudio.h"
#import "HBSubtitles.h"
@implementation HBJob (HBJobConversion)
- (NSDictionary *)jobDict
{
NSAssert(self.title, @"HBJob: calling jobDict without a valid title loaded");
HBMutablePreset *preset = [[HBMutablePreset alloc] init];
[self writeToPreset:preset];
return [self.title jobSettingsWithPreset:preset];
}
/**
* Prepares a hb_job_t
*/
- (hb_job_t *)hb_job
{
NSAssert(self.title, @"HBJob: calling hb_job without a valid title loaded");
NSAssert(self.completeOutputURL, @"HBJob: calling hb_job without a valid destination");
hb_title_t *title = self.title.hb_title;
hb_job_t *job = hb_job_init(title);
hb_job_set_file(job, self.completeOutputURL.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 - start_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 - start_frame;
}
else if (self.range.type == HBRangePreviewIndex)
{
job->start_at_preview = self.range.previewIndex;
job->seek_points = self.range.previewsCount;
job->pts_to_stop = self.range.ptsToStop;
}
// Format (Muxer) and Video Encoder
job->mux = self.container;
job->vcodec = self.video.encoder;
job->mp4_optimize = self.mp4HttpOptimize;
if (self.container & HB_MUX_MASK_MP4)
{
job->align_av_start = self.alignAVStart;
}
// 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 (HBChapter *jobChapter 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, jobChapter.title.UTF8String);
}
i++;
}
}
else
{
job->chapter_markers = 0;
}
if (job->vcodec & HB_VCODEC_H264_MASK)
{
// iPod 5G atom
job->ipod_atom = self.mp4iPodCompatible;
}
if (self.video.twoPass && ((self.video.encoder & HB_VCODEC_X264_MASK) ||
(self.video.encoder & HB_VCODEC_X265_MASK)))
{
job->fastfirstpass = self.video.turboTwoPass;
}
job->twopass = self.video.twoPass;
if (hb_video_encoder_get_presets(self.video.encoder) != NULL)
{
// advanced x264/x265 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;
// we are using the x264/x265 preset system
if ([(tmpString = self.video.completeTune) 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 = HB_INVALID_VIDEO_QUALITY;
job->vbitrate = self.video.avgBitrate;
break;
case 1:
// Constant Quality
job->vquality = self.video.quality;
job->vbitrate = 0;
break;
}
// Map the settings in the dictionaries for the SubtitleList array to match title->list_subtitle
for (HBSubtitlesTrack *subTrack in self.subtitles.tracks)
{
if (subTrack.isEnabled)
{
// Subtract 2 to the source indexes to compensate
// for the none and foreign audio search tracks.
int sourceIdx = ((int)subTrack.sourceTrackIdx) - 2;
// we need to check for the "Foreign Audio Search" which would be have an index of -1
if (sourceIdx == -1)
{
job->indepth_scan = 1;
if (subTrack.burnedIn)
{
job->select_subtitle_config.dest = RENDERSUB;
}
else
{
job->select_subtitle_config.dest = PASSTHRUSUB;
}
job->select_subtitle_config.force = subTrack.forcedOnly;
job->select_subtitle_config.default_track = subTrack.def;
}
else
{
// if we are getting the subtitles from an external file
if (subTrack.type == IMPORTSRT || subTrack.type == IMPORTSSA)
{
hb_subtitle_config_t sub_config;
int type = subTrack.type;
sub_config.name = subTrack.title.UTF8String;
sub_config.offset = subTrack.offset;
// we need to strncpy file name and codeset
sub_config.src_filename = subTrack.fileURL.fileSystemRepresentation;
strncpy(sub_config.src_codeset, subTrack.charCode.UTF8String, 39);
sub_config.src_codeset[39] = 0;
if (!subTrack.burnedIn && hb_subtitle_can_pass(type, job->mux))
{
sub_config.dest = PASSTHRUSUB;
}
else if (hb_subtitle_can_burn(type))
{
sub_config.dest = RENDERSUB;
}
sub_config.force = 0;
sub_config.default_track = subTrack.def;
hb_import_subtitle_add( job, &sub_config, subTrack.isoLanguage.UTF8String, type);
}
else
{
// 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, sourceIdx);
if (subt != NULL)
{
hb_subtitle_config_t sub_config = subt->config;
sub_config.name = subTrack.title.UTF8String;
if (!subTrack.burnedIn && hb_subtitle_can_pass(subt->source, job->mux))
{
sub_config.dest = PASSTHRUSUB;
}
else if (hb_subtitle_can_burn(subt->source))
{
sub_config.dest = RENDERSUB;
}
sub_config.force = subTrack.forcedOnly;
sub_config.default_track = subTrack.def;
hb_subtitle_add(job, &sub_config, sourceIdx);
}
}
}
}
}
// Audio Defaults (Selection Behavior)
job->acodec_copy_mask = 0;
HBAudioDefaults *audioDefaults = self.audio.defaults;
if (audioDefaults.allowAACPassthru)
{
job->acodec_copy_mask |= HB_ACODEC_AAC_PASS;
}
if (audioDefaults.allowAC3Passthru)
{
job->acodec_copy_mask |= HB_ACODEC_AC3_PASS;
}
if (audioDefaults.allowEAC3Passthru)
{
job->acodec_copy_mask |= HB_ACODEC_EAC3_PASS;
}
if (audioDefaults.allowDTSHDPassthru)
{
job->acodec_copy_mask |= HB_ACODEC_DCA_HD_PASS;
}
if (audioDefaults.allowDTSPassthru)
{
job->acodec_copy_mask |= HB_ACODEC_DCA_PASS;
}
if (audioDefaults.allowMP3Passthru)
{
job->acodec_copy_mask |= HB_ACODEC_MP3_PASS;
}
if (audioDefaults.allowTrueHDPassthru)
{
job->acodec_copy_mask |= HB_ACODEC_TRUEHD_PASS;
}
if (audioDefaults.allowFLACPassthru)
{
job->acodec_copy_mask |= HB_ACODEC_FLAC_PASS;
}
job->acodec_fallback = audioDefaults.encoderFallback;
// Audio tracks and mixdowns
// Now lets add our new tracks to the audio list here
for (HBAudioTrack *audioTrack in self.audio.tracks)
{
if (audioTrack.isEnabled)
{
hb_audio_config_t *audio = (hb_audio_config_t *)calloc(1, sizeof(*audio));
hb_audio_config_init(audio);
HBTitleAudioTrack *inputTrack = self.audio.sourceTracks[audioTrack.sourceTrackIdx];
int sampleRateToUse = (audioTrack.sampleRate == 0 ?
inputTrack.sampleRate :
audioTrack.sampleRate);
audio->in.track = (int)audioTrack.sourceTrackIdx - 1;
// We go ahead and assign values to our audio->out.
audio->out.track = audio->in.track;
audio->out.codec = audioTrack.encoder;
audio->out.compression_level = hb_audio_compression_get_default(audio->out.codec);
audio->out.mixdown = audioTrack.mixdown;
audio->out.normalize_mix_level = 0;
audio->out.bitrate = audioTrack.bitRate;
audio->out.samplerate = sampleRateToUse;
audio->out.dither_method = hb_audio_dither_get_default();
audio->out.name = audioTrack.title.UTF8String;
// output is not passthru so apply gain
if (!(audioTrack.encoder & HB_ACODEC_PASS_FLAG))
{
audio->out.gain = audioTrack.gain;
}
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(inputTrack.codec,
inputTrack.codecParam,
audioTrack.encoder))
{
audio->out.dynamic_range_compression = audioTrack.drc;
}
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 isEqualToString:@"off"])
{
int filter_id = HB_FILTER_DETELECINE;
hb_dict_t *filter_dict = hb_generate_filter_settings(filter_id,
self.filters.detelecine.UTF8String,
NULL,
self.filters.detelecineCustomString.UTF8String);
filter = hb_filter_init(filter_id);
hb_add_filter_dict(job, filter, filter_dict);
hb_value_free(&filter_dict);
}
// Comb Detection
if (![self.filters.combDetection isEqualToString:@"off"])
{
int filter_id = HB_FILTER_COMB_DETECT;
hb_dict_t *filter_dict = hb_generate_filter_settings(filter_id,
self.filters.combDetection.UTF8String,
NULL,
self.filters.combDetectionCustomString.UTF8String);
filter = hb_filter_init(filter_id);
hb_add_filter_dict(job, filter, filter_dict);
hb_value_free(&filter_dict);
}
// Deinterlace
if (![self.filters.deinterlace isEqualToString:@"off"])
{
int filter_id = HB_FILTER_DECOMB;
if ([self.filters.deinterlace isEqualToString:@"deinterlace"])
{
filter_id = HB_FILTER_DEINTERLACE;
}
hb_dict_t *filter_dict = hb_generate_filter_settings(filter_id,
self.filters.deinterlacePreset.UTF8String,
NULL,
self.filters.deinterlaceCustomString.UTF8String);
filter = hb_filter_init(filter_id);
hb_add_filter_dict(job, filter, filter_dict);
hb_value_free(&filter_dict);
}
// Denoise
if (![self.filters.denoise isEqualToString:@"off"])
{
int filter_id = HB_FILTER_HQDN3D;
if ([self.filters.denoise isEqualToString:@"nlmeans"])
{
filter_id = HB_FILTER_NLMEANS;
}
hb_dict_t *filter_dict = hb_generate_filter_settings(filter_id,
self.filters.denoisePreset.UTF8String,
self.filters.denoiseTune.UTF8String,
self.filters.denoiseCustomString.UTF8String);
filter = hb_filter_init(filter_id);
hb_add_filter_dict(job, filter, filter_dict);
hb_dict_free(&filter_dict);
}
// Deblock
if (![self.filters.deblock isEqualToString:@"off"])
{
int filter_id = HB_FILTER_DEBLOCK;
hb_dict_t *filter_dict = hb_generate_filter_settings(filter_id,
self.filters.deblock.UTF8String,
self.filters.deblockTune.UTF8String,
self.filters.deblockCustomString.UTF8String);
filter = hb_filter_init(filter_id);
hb_add_filter_dict(job, filter, filter_dict);
hb_value_free(&filter_dict);
}
// Add Crop/Scale filter
filter = hb_filter_init(HB_FILTER_CROP_SCALE);
hb_add_filter( job, filter,
[NSString stringWithFormat:
@"width=%d:height=%d:crop-top=%d:crop-bottom=%d:crop-left=%d:crop-right=%d",
self.picture.width, self.picture.height,
self.picture.cropTop, self.picture.cropBottom,
self.picture.cropLeft, self.picture.cropRight].UTF8String);
// Sharpen
if (![self.filters.sharpen isEqualToString:@"off"])
{
int filter_id = HB_FILTER_UNSHARP;
if ([self.filters.sharpen isEqualToString:@"lapsharp"])
{
filter_id = HB_FILTER_LAPSHARP;
}
hb_dict_t *filter_dict = hb_generate_filter_settings(filter_id,
self.filters.sharpenPreset.UTF8String,
self.filters.sharpenTune.UTF8String,
self.filters.sharpenCustomString.UTF8String);
filter = hb_filter_init(filter_id);
hb_add_filter_dict(job, filter, filter_dict);
hb_dict_free(&filter_dict);
}
// Add grayscale filter
if (self.filters.grayscale)
{
filter = hb_filter_init(HB_FILTER_GRAYSCALE);
hb_add_filter(job, filter, NULL);
}
// Add rotate filter
if (self.filters.rotate || self.filters.flip)
{
int filter_id = HB_FILTER_ROTATE;
hb_dict_t *filter_dict = hb_generate_filter_settings(filter_id,
NULL, NULL,
[NSString stringWithFormat:@"angle=%d:hflip=%d", self.filters.rotate, self.filters.flip].UTF8String);
filter = hb_filter_init(filter_id);
hb_add_filter_dict(job, filter, filter_dict);
hb_dict_free(&filter_dict);
}
// Add framerate shaping filter
filter = hb_filter_init(HB_FILTER_VFR);
hb_add_filter(job, filter, [[NSString stringWithFormat:@"mode=%d:rate=%d/%d",
fps_mode, fps_num, fps_den] UTF8String]);
return job;
}
@end