HandBrake/macosx/HBJob+HBJobConversion.m

682 lines
24 KiB
Objective-C

/* HBJob+HBJobConversion.m $
This file is part of the HandBrake source code.
Homepage: <http://handbrake.fr/>.
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"
#import "NSDictionary+HBAdditions.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.destinationURL, @"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.destinationURL.fileSystemRepresentation);
if (self.hwDecodeUsage == HBJobHardwareDecoderUsageFullPathOnly)
{
job->hw_decode = HB_DECODE_VIDEOTOOLBOX;
}
else if (self.hwDecodeUsage == HBJobHardwareDecoderUsageAlways)
{
job->hw_decode = HB_DECODE_VIDEOTOOLBOX | HB_DECODE_FORCE_HW;
}
// 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 + 1;
}
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;
if (self.video.colorRange != HBVideoColorRangeAuto)
{
job->color_range = (int)self.video.colorRange;
}
job->optimize = self.optimize;
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 (self.metadataPassthru == NO && job->metadata)
{
if (job->metadata->dict)
{
hb_dict_clear(job->metadata->dict);
}
if (job->metadata->list_coverart)
{
int count = hb_list_count(job->metadata->list_coverart);
for (int i = 0; i < count; i++)
{
hb_metadata_rem_coverart(job->metadata, 0);
}
}
}
if (job->vcodec & HB_VCODEC_H264_MASK)
{
// iPod 5G atom
job->ipod_atom = self.mp4iPodCompatible;
}
if (self.video.multiPass && ((self.video.encoder & HB_VCODEC_X264_MASK) ||
(self.video.encoder & HB_VCODEC_X265_MASK)))
{
job->fastanalysispass = self.video.turboMultiPass;
}
job->multipass = self.video.multiPass;
switch (self.video.passthruHDRDynamicMetadata)
{
case HBVideoHDRDynamicMetadataPassthruOff:
job->passthru_dynamic_hdr_metadata = HB_HDR_DYNAMIC_METADATA_NONE;
break;
case HBVideoHDRDynamicMetadataPassthruHDR10Plus:
job->passthru_dynamic_hdr_metadata = HB_HDR_DYNAMIC_METADATA_HDR10PLUS;
break;
case HBVideoHDRDynamicMetadataPassthruDolbyVision:
job->passthru_dynamic_hdr_metadata = HB_HDR_DYNAMIC_METADATA_DOVI;
break;
case HBVideoHDRDynamicMetadataPassthruAll:
default:
job->passthru_dynamic_hdr_metadata = HB_HDR_DYNAMIC_METADATA_ALL;
break;
}
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.parNum;
job->par.den = self.picture.parDen;
// 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)
{
if (subTrack.fileURL)
{
hb_subtitle_config_t sub_config = {0};
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;
if (subTrack.charCode)
{
size_t len = sizeof(sub_config.src_codeset) - 1;
strncpy(sub_config.src_codeset, subTrack.charCode.UTF8String, len);
sub_config.src_codeset[len] = 0;
}
if (!subTrack.burnedIn && hb_subtitle_can_pass(subTrack.type, job->mux))
{
sub_config.dest = PASSTHRUSUB;
}
else if (hb_subtitle_can_burn(subTrack.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, subTrack.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.allowMP2Passthru)
{
job->acodec_copy_mask |= HB_ACODEC_MP2_PASS;
}
if (audioDefaults.allowMP3Passthru)
{
job->acodec_copy_mask |= HB_ACODEC_MP3_PASS;
}
if (audioDefaults.allowVorbisPassthru)
{
job->acodec_copy_mask |= HB_ACODEC_VORBIS_PASS;
}
if (audioDefaults.allowOpusPassthru)
{
job->acodec_copy_mask |= HB_ACODEC_OPUS_PASS;
}
if (audioDefaults.allowTrueHDPassthru)
{
job->acodec_copy_mask |= HB_ACODEC_TRUEHD_PASS;
}
if (audioDefaults.allowALACPassthru)
{
job->acodec_copy_mask |= HB_ACODEC_ALAC_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 = {0};
hb_audio_config_init(&audio);
HBTitleAudioTrack *inputTrack = self.audio.sourceTracks[audioTrack.sourceTrackIdx];
int sampleRateToUse = (audioTrack.sampleRate == 0 ?
inputTrack.sampleRate :
audioTrack.sampleRate);
audio.index = (int)audioTrack.sourceTrackIdx - 1;
// We go ahead and assign values to our audio->out.<properties>
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);
hb_audio_config_close(&audio);
}
}
// Now lets call the filters if applicable.
hb_filter_object_t *filter;
// Detelecine
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_YADIF;
}
else if ([self.filters.deinterlace isEqualToString:@"bwdif"])
{
filter_id = HB_FILTER_BWDIF;
}
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);
}
// 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]);
// 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);
}
// 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);
}
// Chroma Smooth
if (![self.filters.chromaSmooth isEqualToString:@"off"])
{
int filter_id = HB_FILTER_CHROMA_SMOOTH;
hb_dict_t *filter_dict = hb_generate_filter_settings(filter_id,
self.filters.chromaSmooth.UTF8String,
self.filters.chromaSmoothTune.UTF8String,
self.filters.chromaSmoothCustomString.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);
}
// Grayscale
if (self.filters.grayscale)
{
filter = hb_filter_init(HB_FILTER_GRAYSCALE);
hb_add_filter(job, filter, NULL);
}
// Rotate
if (self.picture.angle || self.picture.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.picture.angle, self.picture.flip].UTF8String);
filter = hb_filter_init(filter_id);
hb_add_filter_dict(job, filter, filter_dict);
hb_dict_free(&filter_dict);
}
// Pad
if (self.picture.padMode != HBPicturePadModeNone)
{
int filter_id = HB_FILTER_PAD;
NSString *color;
switch (self.picture.padColorMode) {
case HBPicturePadColorModeBlack:
color = @"black";
break;
case HBPicturePadColorModeDarkGray:
color = @"darkslategray";
break;
case HBPicturePadColorModeGray:
color = @"slategray";
break;
case HBPicturePadColorModeWhite:
color = @"white";
break;
case HBPicturePadColorModeCustom:
color = self.picture.padColorCustom;
break;
}
NSString *settings = [NSString stringWithFormat:@"color=%@:top=%d:bottom=%d:left=%d:right=%d",
color, self.picture.padTop, self.picture.padBottom, self.picture.padLeft, self.picture.padRight];
hb_dict_t *filter_dict = hb_generate_filter_settings(filter_id, NULL, NULL, settings.UTF8String);
filter = hb_filter_init(filter_id);
hb_add_filter_dict(job, filter, filter_dict);
hb_dict_free(&filter_dict);
}
// Colorspace
if (![self.filters.colorspace isEqualToString:@"off"])
{
int filter_id = HB_FILTER_COLORSPACE;
hb_dict_t *filter_dict = hb_generate_filter_settings(filter_id,
self.filters.colorspace.UTF8String,
NULL,
self.filters.colorspaceCustomString.UTF8String);
filter = hb_filter_init(filter_id);
hb_add_filter_dict(job, filter, filter_dict);
hb_value_free(&filter_dict);
}
return job;
}
@end