/* encvt.c Copyright (c) 2003-2025 HandBrake Team This file is part of the HandBrake source code Homepage: . It may be used under the terms of the GNU General Public License v2. For full terms see the file COPYING file or visit http://www.gnu.org/licenses/gpl-2.0.html */ #include #include #include #include "libavutil/avutil.h" #include "handbrake/handbrake.h" #include "handbrake/dovi_common.h" #include "handbrake/hdr10plus.h" #include "handbrake/nal_units.h" #include "handbrake/extradata.h" #include "handbrake/bitstream.h" #include "vt_common.h" #include "cv_utils.h" int encvt_init(hb_work_object_t *, hb_job_t *); int encvt_work(hb_work_object_t *, hb_buffer_t **, hb_buffer_t **); void encvt_close(hb_work_object_t *); hb_work_object_t hb_encvt = { WORK_ENCVT, "VideoToolbox encoder (Apple)", encvt_init, encvt_work, encvt_close }; #define FRAME_INFO_SIZE 1024 #define FRAME_INFO_MASK (FRAME_INFO_SIZE - 1) struct hb_work_private_s { hb_job_t *job; CMFormatDescriptionRef format; VTCompressionSessionRef session; CMSimpleQueueRef queue; hb_chapter_queue_t *chapter_queue; // DTS calculation int frameno_in; int frameno_out; int64_t dts_delay; struct { int64_t start; } frame_info[FRAME_INFO_SIZE]; // Multipass VTMultiPassStorageRef passStorage; CMItemCount timeRangeCount; const CMTimeRange *timeRangeArray; int remainingPasses; uint8 nal_length_size; struct hb_vt_param { CMVideoCodecType codec; uint64_t registryID; OSType inputPixFmt; OSType encoderPixFmt; int32_t timescale; double quality; int averageBitRate; double expectedFrameRate; int profile; CFStringRef profileLevel; int maxAllowedFrameQP; int minAllowedFrameQP; int maxReferenceBufferCount; int maxFrameDelayCount; int maxKeyFrameInterval; int lookAheadFrameCount; CFBooleanRef allowFrameReordering; CFBooleanRef allowTemporalCompression; CFBooleanRef disableSpatialAdaptiveQP; CFBooleanRef prioritizeEncodingSpeedOverQuality; CFBooleanRef preserveDynamicHDRMetadata; struct { int maxrate; int bufsize; } vbv; struct { int prim; int matrix; int transfer; int chromaLocation; CFDataRef masteringDisplay; CFDataRef contentLightLevel; CFDataRef ambientViewingEnviroment; } color; SInt32 width; SInt32 height; struct { SInt32 num; SInt32 den; } par; enum { HB_VT_FIELDORDER_PROGRESSIVE = 0, HB_VT_FIELDORDER_TFF, HB_VT_FIELDORDER_BFF, } fieldDetail; struct { CFStringRef entropyMode; int maxSliceBytes; } h264; } settings; CFDictionaryRef attachments; }; void hb_vt_param_default(struct hb_vt_param *param) { param->quality = -1; param->vbv.maxrate = 0; param->vbv.bufsize = 0; param->maxAllowedFrameQP = -1; param->minAllowedFrameQP = -1; param->maxReferenceBufferCount = -1; param->maxFrameDelayCount = kVTUnlimitedFrameDelayCount; param->lookAheadFrameCount = -1; param->allowFrameReordering = kCFBooleanTrue; param->allowTemporalCompression = kCFBooleanTrue; param->disableSpatialAdaptiveQP = kCFBooleanFalse; param->prioritizeEncodingSpeedOverQuality = kCFBooleanFalse; param->preserveDynamicHDRMetadata = kCFBooleanFalse; param->fieldDetail = HB_VT_FIELDORDER_PROGRESSIVE; } // Used to pass the compression // session to the next job typedef struct vt_interjob_s { VTCompressionSessionRef session; VTMultiPassStorageRef passStorage; CMSimpleQueueRef queue; CMFormatDescriptionRef format; int areBframes; } vt_interjob_t; enum { HB_VT_H264_PROFILE_BASELINE = 0, HB_VT_H264_PROFILE_MAIN, HB_VT_H264_PROFILE_HIGH, HB_VT_H264_PROFILE_NB, }; static struct { const char *name; const CFStringRef level[HB_VT_H264_PROFILE_NB]; } hb_vt_h264_levels[] = { { "auto", { CFSTR("H264_Baseline_AutoLevel"), CFSTR("H264_Main_AutoLevel"), CFSTR("H264_High_AutoLevel"), }, }, { "1.3", { CFSTR("H264_Baseline_1_3"), CFSTR("H264_Baseline_1_3"), CFSTR("H264_Baseline_1_3"), }, }, { "3.0", { CFSTR("H264_Baseline_3_0"), CFSTR("H264_Main_3_0" ), CFSTR("H264_High_3_0" ), }, }, { "3.1", { CFSTR("H264_Baseline_3_1"), CFSTR("H264_Main_3_1" ), CFSTR("H264_High_3_1" ), }, }, { "3.2", { CFSTR("H264_Baseline_3_2"), CFSTR("H264_Main_3_2" ), CFSTR("H264_High_3_2" ), }, }, { "4.0", { CFSTR("H264_Baseline_4_0"), CFSTR("H264_Main_4_0" ), CFSTR("H264_High_4_0" ), }, }, { "4.1", { CFSTR("H264_Baseline_4_1"), CFSTR("H264_Main_4_1" ), CFSTR("H264_High_4_1" ), }, }, { "4.2", { CFSTR("H264_Baseline_4_2"), CFSTR("H264_Main_4_2" ), CFSTR("H264_High_4_2" ), }, }, { "5.0", { CFSTR("H264_Baseline_5_0"), CFSTR("H264_Main_5_0" ), CFSTR("H264_High_5_0" ), }, }, { "5.1", { CFSTR("H264_Baseline_5_1"), CFSTR("H264_Main_5_1" ), CFSTR("H264_High_5_1" ), }, }, { "5.2", { CFSTR("H264_Baseline_5_2"), CFSTR("H264_Main_5_2" ), CFSTR("H264_High_5_2" ), }, }, { NULL, { NULL, NULL, NULL, }, }, }; enum { HB_VT_H265_PROFILE_MAIN = 0, HB_VT_H265_PROFILE_MAIN_10, HB_VT_H265_PROFILE_MAIN_422_10, HB_VT_H265_PROFILE_NB, }; static struct { const char *name; const CFStringRef level[HB_VT_H265_PROFILE_NB]; } hb_vt_h265_levels[] = { { "auto", { CFSTR("HEVC_Main_AutoLevel"), CFSTR("HEVC_Main10_AutoLevel"), CFSTR("HEVC_Main42210_AutoLevel") } } }; static void hb_vt_save_frame_info(hb_work_private_t *pv, hb_buffer_t *in) { int i = pv->frameno_in & FRAME_INFO_MASK; pv->frame_info[i].start = in->s.start; } static int64_t hb_vt_get_frame_start(hb_work_private_t *pv, int64_t frameno) { int i = frameno & FRAME_INFO_MASK; return pv->frame_info[i].start; } static void hb_vt_compute_dts_offset(hb_work_private_t *pv, hb_buffer_t *buf) { if (pv->job->areBframes && pv->frameno_in == pv->job->areBframes) { pv->dts_delay = buf->s.start; pv->job->init_delay = pv->dts_delay; } } static void hb_vt_check_result(OSStatus err, CFStringRef propertyKey) { if (err != noErr) { char valBuf[256]; Boolean haveStr = CFStringGetCString(propertyKey, valBuf, 256, kCFStringEncodingUTF8); if (haveStr) { hb_log("VTSessionSetProperty: %s failed (%d)", valBuf, err); } else { hb_log("VTSessionSetProperty: failed (%d)", err); } } } static OSStatus hb_vt_set_property(VTSessionRef session, CFStringRef propertyKey, CFTypeRef propertyValue) { OSStatus err = VTSessionSetProperty(session, propertyKey, propertyValue); hb_vt_check_result(err, propertyKey); return err; } static int hb_vt_get_nal_length_size(CMSampleBufferRef sampleBuffer, CMVideoCodecType codec) { CMFormatDescriptionRef format = CMSampleBufferGetFormatDescription(sampleBuffer); int isize = 0; if (format) { if (codec == kCMVideoCodecType_H264) { CMVideoFormatDescriptionGetH264ParameterSetAtIndex(format, 0, NULL, NULL, NULL, &isize); } else if (codec == kCMVideoCodecType_HEVC) { CMVideoFormatDescriptionGetHEVCParameterSetAtIndex(format, 0, NULL, NULL, NULL, &isize); } } return isize; } static void hb_vt_add_dynamic_hdr_metadata(CMSampleBufferRef sampleBuffer, hb_buffer_t *buf) { for (int i = 0; i < buf->nb_side_data; i++) { const AVFrameSideData *side_data = buf->side_data[i]; if (side_data->type == AV_FRAME_DATA_DYNAMIC_HDR_PLUS) { uint8_t *payload = NULL; uint32_t playload_size = 0; hb_dynamic_hdr10_plus_to_itu_t_t35((AVDynamicHDRPlus *)side_data->data, &payload, &playload_size); if (!playload_size) { continue; } CFDataRef data = CFDataCreate(kCFAllocatorDefault, payload, playload_size); if (data) { CMSetAttachment(sampleBuffer, CFSTR("HB_HDR_PLUS"), data, kCVAttachmentMode_ShouldPropagate); CFRelease(data); } av_freep(&payload); } if (side_data->type == AV_FRAME_DATA_DOVI_RPU_BUFFER) { CFDataRef data = CFDataCreate(kCFAllocatorDefault, side_data->data, side_data->size); if (data) { CMSetAttachment(sampleBuffer, CFSTR("HB_DOVI_RPU"), data, kCVAttachmentMode_ShouldPropagate); CFRelease(data); } } } } static CFDataRef hb_vt_mastering_display_xlat(hb_mastering_display_metadata_t mastering) { CFMutableDataRef data = CFDataCreateMutable(kCFAllocatorDefault, 24); const int chromaDen = 50000; const int lumaDen = 10000; uint16_t display_primaries_gx = CFSwapInt16HostToBig(hb_rescale_rational(mastering.display_primaries[1][0], chromaDen)); uint16_t display_primaries_gy = CFSwapInt16HostToBig(hb_rescale_rational(mastering.display_primaries[1][1], chromaDen)); uint16_t display_primaries_bx = CFSwapInt16HostToBig(hb_rescale_rational(mastering.display_primaries[2][0], chromaDen)); uint16_t display_primaries_by = CFSwapInt16HostToBig(hb_rescale_rational(mastering.display_primaries[2][1], chromaDen)); uint16_t display_primaries_rx = CFSwapInt16HostToBig(hb_rescale_rational(mastering.display_primaries[0][0], chromaDen)); uint16_t display_primaries_ry = CFSwapInt16HostToBig(hb_rescale_rational(mastering.display_primaries[0][1], chromaDen)); uint16_t white_point_x = CFSwapInt16HostToBig(hb_rescale_rational(mastering.white_point[0], chromaDen)); uint16_t white_point_y = CFSwapInt16HostToBig(hb_rescale_rational(mastering.white_point[1], chromaDen)); uint32_t max_display_mastering_luminance = CFSwapInt32HostToBig(hb_rescale_rational(mastering.max_luminance, lumaDen)); uint32_t min_display_mastering_luminance = CFSwapInt32HostToBig(hb_rescale_rational(mastering.min_luminance, lumaDen)); CFDataAppendBytes(data, (UInt8 *)&display_primaries_gx, 2); CFDataAppendBytes(data, (UInt8 *)&display_primaries_gy, 2); CFDataAppendBytes(data, (UInt8 *)&display_primaries_bx, 2); CFDataAppendBytes(data, (UInt8 *)&display_primaries_by, 2); CFDataAppendBytes(data, (UInt8 *)&display_primaries_rx, 2); CFDataAppendBytes(data, (UInt8 *)&display_primaries_ry, 2); CFDataAppendBytes(data, (UInt8 *)&white_point_x, 2); CFDataAppendBytes(data, (UInt8 *)&white_point_y, 2); CFDataAppendBytes(data, (UInt8 *)&max_display_mastering_luminance, 4); CFDataAppendBytes(data, (UInt8 *)&min_display_mastering_luminance, 4); return data; } static CFDataRef hb_vt_content_light_level_xlat(hb_content_light_metadata_t coll) { CFMutableDataRef data = CFDataCreateMutable(kCFAllocatorDefault, 4); uint16_t MaxCLL = CFSwapInt16HostToBig(coll.max_cll); uint16_t MaxFALL = CFSwapInt16HostToBig(coll.max_fall); CFDataAppendBytes(data, (UInt8 *)&MaxCLL, 2); CFDataAppendBytes(data, (UInt8 *)&MaxFALL, 2); return data; } static CFDataRef hb_vt_ambient_viewing_enviroment_xlat(hb_ambient_viewing_environment_metadata_t ambient) { CFMutableDataRef data = CFDataCreateMutable(kCFAllocatorDefault, 8); uint32_t ambient_illuminance = CFSwapInt32HostToBig(hb_rescale_rational(ambient.ambient_illuminance, 10000)); uint16_t ambient_light_x = CFSwapInt16HostToBig(hb_rescale_rational(ambient.ambient_light_x, 50000)); uint16_t ambient_light_y = CFSwapInt16HostToBig(hb_rescale_rational(ambient.ambient_light_y, 50000)); CFDataAppendBytes(data, (UInt8 *)&ambient_illuminance, 4); CFDataAppendBytes(data, (UInt8 *)&ambient_light_x, 2); CFDataAppendBytes(data, (UInt8 *)&ambient_light_y, 2); return data; } static OSType hb_vt_encoder_pixel_format_xlat(int vcodec, int profile, int color_range) { int pix_fmt = AV_PIX_FMT_NV12; switch (vcodec) { case HB_VCODEC_VT_H264: case HB_VCODEC_VT_H265: pix_fmt = hb_vt_get_best_pix_fmt(vcodec, "auto"); break; case HB_VCODEC_VT_H265_10BIT: switch (profile) { case HB_VT_H265_PROFILE_MAIN_10: pix_fmt = hb_vt_get_best_pix_fmt(vcodec, "main-10"); break; case HB_VT_H265_PROFILE_MAIN_422_10: pix_fmt = hb_vt_get_best_pix_fmt(vcodec, "main422-10"); break; } break; default: hb_log("encvt_Init: unknown codec"); } return hb_cv_get_pixel_format(pix_fmt, color_range); } static CFDictionaryRef hb_vt_attachments_xlat(hb_job_t *job) { CFMutableDictionaryRef mutable_attachments = CFDictionaryCreateMutable(NULL, 0, &kCFTypeDictionaryKeyCallBacks, &kCFTypeDictionaryValueCallBacks); hb_cv_add_color_tag(mutable_attachments, job->color_prim, job->color_transfer, job->color_matrix, job->chroma_location); CFDictionaryRef attachments = CFDictionaryCreateCopy(NULL, mutable_attachments); CFRelease(mutable_attachments); return attachments; } static int hb_vt_settings_xlat(hb_work_private_t *pv, hb_job_t *job) { // Set global default values. hb_vt_param_default(&pv->settings); pv->settings.codec = job->vcodec == HB_VCODEC_VT_H264 ? kCMVideoCodecType_H264 : kCMVideoCodecType_HEVC; pv->settings.inputPixFmt = hb_cv_get_pixel_format(job->output_pix_fmt, job->color_range); pv->settings.timescale = 90000; // Set the preset if (job->encoder_preset != NULL && *job->encoder_preset != '\0') { if (!strcasecmp(job->encoder_preset, "speed")) { pv->settings.prioritizeEncodingSpeedOverQuality = kCFBooleanTrue; } } // Set the profile and level before initializing the session if (job->encoder_profile != NULL && *job->encoder_profile != '\0') { if (job->vcodec == HB_VCODEC_VT_H264) { if (!strcasecmp(job->encoder_profile, "baseline")) { pv->settings.profile = HB_VT_H264_PROFILE_BASELINE; } else if (!strcasecmp(job->encoder_profile, "main") || !strcasecmp(job->encoder_profile, "auto")) { pv->settings.profile = HB_VT_H264_PROFILE_MAIN; } else if (!strcasecmp(job->encoder_profile, "high")) { pv->settings.profile = HB_VT_H264_PROFILE_HIGH; } else { hb_error("encvt_Init: invalid profile '%s'", job->encoder_profile); return -1; } } else if (job->vcodec == HB_VCODEC_VT_H265) { if (!strcasecmp(job->encoder_profile, "main") || !strcasecmp(job->encoder_profile, "auto")) { pv->settings.profile = HB_VT_H265_PROFILE_MAIN; } else { hb_error("encvt_Init: invalid profile '%s'", job->encoder_profile); return -1; } } else if (job->vcodec == HB_VCODEC_VT_H265_10BIT) { if (!strcasecmp(job->encoder_profile, "main10") || !strcasecmp(job->encoder_profile, "auto")) { pv->settings.profile = HB_VT_H265_PROFILE_MAIN_10; } else if (!strcasecmp(job->encoder_profile, "main422-10")) { pv->settings.profile = HB_VT_H265_PROFILE_MAIN_422_10; } else { hb_error("encvt_Init: invalid profile '%s'", job->encoder_profile); return -1; } } } else { if (job->vcodec == HB_VCODEC_VT_H264) { pv->settings.profile = HB_VT_H264_PROFILE_HIGH; } else if (job->vcodec == HB_VCODEC_VT_H265) { pv->settings.profile = HB_VT_H265_PROFILE_MAIN; } else if (job->vcodec == HB_VCODEC_VT_H265_10BIT) { pv->settings.profile = HB_VT_H265_PROFILE_MAIN_10; } } pv->settings.encoderPixFmt = hb_vt_encoder_pixel_format_xlat(job->vcodec, pv->settings.profile, job->color_range); if (job->encoder_level != NULL && *job->encoder_level != '\0' && job->vcodec == HB_VCODEC_VT_H264) { int i; for (i = 0; hb_vt_h264_levels[i].name != NULL; i++) { if (!strcasecmp(job->encoder_level, hb_vt_h264_levels[i].name)) { pv->settings.profileLevel = hb_vt_h264_levels[i].level[pv->settings.profile]; break; } } if (hb_vt_h264_levels[i].name == NULL) { hb_error("encvt_Init: invalid level '%s'", job->encoder_level); *job->die = 1; return -1; } } else { if (job->vcodec == HB_VCODEC_VT_H264) { pv->settings.profileLevel = hb_vt_h264_levels[0].level[pv->settings.profile]; } else if (job->vcodec == HB_VCODEC_VT_H265 || job->vcodec == HB_VCODEC_VT_H265_10BIT) { pv->settings.profileLevel = hb_vt_h265_levels[0].level[pv->settings.profile]; } } // Compute the frame rate and output bit rate pv->settings.expectedFrameRate = (double)job->vrate.num / (double)job->vrate.den; if (job->vquality > HB_INVALID_VIDEO_QUALITY) { pv->settings.quality = job->vquality / 100; hb_log("encvt_Init: encoding with constant quality %f", pv->settings.quality * 100); } else if (job->vbitrate > 0) { pv->settings.averageBitRate = job->vbitrate * 1000; hb_log("encvt_Init: encoding with output bitrate %d Kbps", pv->settings.averageBitRate / 1000); } else { hb_error("encvt_Init: invalid rate control (bitrate %d, quality %f)", job->vbitrate, job->vquality); } int fps = pv->settings.expectedFrameRate + 0.5; pv->settings.width = job->width; pv->settings.height = job->height; pv->settings.par.num = job->par.num; pv->settings.par.den = job->par.den; pv->settings.maxKeyFrameInterval = fps * 10 + 1; pv->settings.color.prim = hb_output_color_prim(job); pv->settings.color.transfer = hb_output_color_transfer(job); pv->settings.color.matrix = hb_output_color_matrix(job); pv->settings.color.chromaLocation = job->chroma_location; // HDR10 Static metadata if (job->color_transfer == HB_COLR_TRA_SMPTEST2084) { // Mastering display metadata if (job->mastering.has_primaries && job->mastering.has_luminance) { pv->settings.color.masteringDisplay = hb_vt_mastering_display_xlat(job->mastering); } // Content light level if (job->coll.max_cll && job->coll.max_fall) { pv->settings.color.contentLightLevel = hb_vt_content_light_level_xlat(job->coll); } } if (job->ambient.ambient_illuminance.num && job->ambient.ambient_illuminance.den) { pv->settings.color.ambientViewingEnviroment = hb_vt_ambient_viewing_enviroment_xlat(job->ambient); } return 0; } static int hb_vt_parse_options(hb_work_private_t *pv, hb_job_t *job) { hb_dict_t *opts = NULL; if (job->encoder_options != NULL && *job->encoder_options) { opts = hb_encopts_to_dict(job->encoder_options, job->vcodec); } hb_dict_iter_t iter; for (iter = hb_dict_iter_init(opts); iter != HB_DICT_ITER_DONE; iter = hb_dict_iter_next(opts, iter)) { const char *key = hb_dict_iter_key(iter); hb_value_t *value = hb_dict_iter_value(iter); if (!strcmp(key, "keyint")) { int keyint = hb_value_get_int(value); if (keyint >= 0) { pv->settings.maxKeyFrameInterval = keyint; } } else if (!strcmp(key, "bframes")) { int enabled = hb_value_get_bool(value); pv->settings.allowFrameReordering = enabled ? kCFBooleanTrue : kCFBooleanFalse; } else if (!strcmp(key, "cabac")) { int enabled = hb_value_get_bool(value); pv->settings.h264.entropyMode = enabled ? kVTH264EntropyMode_CABAC : kVTH264EntropyMode_CAVLC; } else if (!strcmp(key, "vbv-bufsize")) { int bufsize = hb_value_get_int(value); if (bufsize > 0) { pv->settings.vbv.bufsize = bufsize; } } else if (!strcmp(key, "vbv-maxrate")) { int maxrate = hb_value_get_int(value); if (maxrate > 0) { pv->settings.vbv.maxrate = maxrate; } } else if (!strcmp(key, "slice-max-size")) { int slice_max_size = hb_value_get_bool(value); if (slice_max_size > 0) { pv->settings.h264.maxSliceBytes = slice_max_size; } } else if (!strcmp(key, "max-frame-delay")) { int maxdelay = hb_value_get_bool(value); if (maxdelay > 0) { pv->settings.maxFrameDelayCount = maxdelay; } } else if (!strcmp(key, "gpu-registryid")) { uint64_t registryID = hb_value_get_int(value); if (registryID > 0) { pv->settings.registryID = registryID; } } else if (!strcmp(key, "qpmin")) { int qpmin = hb_value_get_int(value); if (qpmin >= 0) { pv->settings.minAllowedFrameQP = qpmin; } } else if (!strcmp(key, "qpmax")) { int qpmax = hb_value_get_int(value); if (qpmax >= 0) { pv->settings.maxAllowedFrameQP = qpmax; } } else if (!strcmp(key, "ref")) { int ref = hb_value_get_int(value); if (ref >= 0) { pv->settings.maxReferenceBufferCount = ref; } } else if (!strcmp(key, "look-ahead-frame-count")) { int lookaheadframe = hb_value_get_int(value); if (lookaheadframe >= 0) { pv->settings.lookAheadFrameCount = lookaheadframe; } } else if (!strcmp(key, "disable-spatial-adaptive-qp")) { int disabled = hb_value_get_bool(value); pv->settings.disableSpatialAdaptiveQP = disabled ? kCFBooleanTrue : kCFBooleanFalse; } else { hb_log("encvt_Init: unknown option '%s'", key); } } hb_dict_free(&opts); // Sanitize interframe settings switch (pv->settings.maxKeyFrameInterval) { case 1: pv->settings.allowTemporalCompression = kCFBooleanFalse; case 2: pv->settings.allowFrameReordering = kCFBooleanFalse; default: break; } switch (pv->settings.maxFrameDelayCount) { case 0: pv->settings.allowTemporalCompression = kCFBooleanFalse; case 1: pv->settings.allowFrameReordering = kCFBooleanFalse; default: break; } if (pv->settings.lookAheadFrameCount > pv->settings.maxFrameDelayCount && pv->settings.maxFrameDelayCount != kVTUnlimitedFrameDelayCount) { pv->settings.lookAheadFrameCount = pv->settings.maxFrameDelayCount; } if (pv->settings.quality == 1 || pv->passStorage) { pv->settings.lookAheadFrameCount = -1; } return 0; } static void hb_vt_set_data_rate_limits(VTCompressionSessionRef session, int bufsize, int maxrate) { float seconds = ((float)bufsize / (float)maxrate); int bytes = maxrate * 125 * seconds; CFNumberRef size = CFNumberCreate(kCFAllocatorDefault, kCFNumberIntType, &bytes); CFNumberRef duration = CFNumberCreate(kCFAllocatorDefault, kCFNumberFloatType, &seconds); CFMutableArrayRef dataRateLimits = CFArrayCreateMutable(kCFAllocatorDefault, 2, &kCFTypeArrayCallBacks); CFArrayAppendValue(dataRateLimits, size); CFArrayAppendValue(dataRateLimits, duration); hb_vt_set_property(session, kVTCompressionPropertyKey_DataRateLimits, dataRateLimits); CFRelease(size); CFRelease(duration); CFRelease(dataRateLimits); } static CVPixelBufferRef hb_vt_get_pix_buf(hb_work_private_t *pv, hb_buffer_t *buf) { CVPixelBufferRef pix_buf = NULL; if (pv->job->hw_pix_fmt == AV_PIX_FMT_VIDEOTOOLBOX) { pix_buf = hb_cv_get_pixel_buffer(buf); if (pix_buf) { CVPixelBufferRetain(pix_buf); } } else { int numberOfPlanes = pv->settings.inputPixFmt == kCVPixelFormatType_420YpCbCr8Planar || pv->settings.inputPixFmt == kCVPixelFormatType_420YpCbCr8PlanarFullRange ? 3 : 2; void *planeBaseAddress[3] = {buf->plane[0].data, buf->plane[1].data, buf->plane[2].data}; size_t planeWidth[3] = {buf->plane[0].width, buf->plane[1].width, buf->plane[2].width}; size_t planeHeight[3] = {buf->plane[0].height, buf->plane[1].height, buf->plane[2].height}; size_t planeBytesPerRow[3] = {buf->plane[0].stride, buf->plane[1].stride, buf->plane[2].stride}; OSStatus err = CVPixelBufferCreateWithPlanarBytes( kCFAllocatorDefault, buf->f.width, buf->f.height, pv->settings.inputPixFmt, buf->data, 0, numberOfPlanes, planeBaseAddress, planeWidth, planeHeight, planeBytesPerRow, NULL, buf, NULL, &pix_buf); if (err) { pix_buf = NULL; } } return pix_buf; } static void hb_vt_insert_dynamic_metadata(hb_work_private_t *pv, CMSampleBufferRef sampleBuffer, hb_buffer_t **buf) { if (pv->job->passthru_dynamic_hdr_metadata == 0) { return; } if (pv->nal_length_size == 0) { pv->nal_length_size = hb_vt_get_nal_length_size(sampleBuffer, pv->settings.codec); } if (pv->nal_length_size > 4) { hb_log("VTCompressionSession: unknown nal length size"); return; } hb_buffer_t *buf_in = *buf; hb_sei_t seis[4]; size_t seis_count = 0; if (buf_in->s.frametype == HB_FRAME_IDR) { if (pv->settings.color.contentLightLevel) { const uint8_t *coll_data = CFDataGetBytePtr(pv->settings.color.contentLightLevel); size_t coll_size = CFDataGetLength(pv->settings.color.contentLightLevel); seis[seis_count].type = HB_CONTENT_LIGHT_LEVEL_INFO; seis[seis_count].payload = coll_data; seis[seis_count].payload_size = coll_size; seis_count += 1; } if (pv->settings.color.masteringDisplay) { const uint8_t *mastering_data = CFDataGetBytePtr(pv->settings.color.masteringDisplay); size_t mastering_size = CFDataGetLength(pv->settings.color.masteringDisplay); seis[seis_count].type = HB_MASTERING_DISPLAY_INFO; seis[seis_count].payload = mastering_data; seis[seis_count].payload_size = mastering_size; seis_count += 1; } } if (pv->job->passthru_dynamic_hdr_metadata & HB_HDR_DYNAMIC_METADATA_HDR10PLUS) { CFDataRef hdrPlus = CMGetAttachment(sampleBuffer, CFSTR("HB_HDR_PLUS"), NULL); if (hdrPlus != NULL) { const uint8_t *sei_data = CFDataGetBytePtr(hdrPlus); size_t sei_size = CFDataGetLength(hdrPlus); seis[seis_count].type = HB_USER_DATA_REGISTERED_ITU_T_T35; seis[seis_count].payload = sei_data; seis[seis_count].payload_size = sei_size; seis_count += 1; } } hb_nal_t nals[1]; size_t nals_count = 0; if (pv->job->passthru_dynamic_hdr_metadata & HB_HDR_DYNAMIC_METADATA_DOVI) { CFDataRef rpu = CMGetAttachment(sampleBuffer, CFSTR("HB_DOVI_RPU"), NULL); if (rpu != NULL) { const uint8_t *rpu_data = CFDataGetBytePtr(rpu); size_t rpu_size = CFDataGetLength(rpu); nals[nals_count].type = HB_HEVC_NAL_UNIT_UNSPECIFIED; nals[nals_count].payload = rpu_data; nals[nals_count].payload_size = rpu_size; nals_count += 1; } } if (seis_count || nals_count) { hb_buffer_t *out = hb_isomp4_hevc_nal_bitstream_insert_payloads(buf_in->data, buf_in->size, seis, seis_count, nals, nals_count, pv->nal_length_size); if (out) { out->f = buf_in->f; out->s = buf_in->s; hb_buffer_close(buf); *buf = out; } } } static void hb_vt_set_frametype(CMSampleBufferRef sampleBuffer, hb_buffer_t *buf) { buf->s.frametype = HB_FRAME_IDR; buf->s.flags |= HB_FLAG_FRAMETYPE_REF; CFArrayRef attachmentsArray = CMSampleBufferGetSampleAttachmentsArray(sampleBuffer, 0); if (CFArrayGetCount(attachmentsArray)) { CFDictionaryRef dict = CFArrayGetValueAtIndex(attachmentsArray, 0); CFBooleanRef notSync; if (CFDictionaryGetValueIfPresent(dict, kCMSampleAttachmentKey_NotSync,(const void **) ¬Sync)) { Boolean notSyncValue = CFBooleanGetValue(notSync); if (notSyncValue) { CFBooleanRef b; if (CFDictionaryGetValueIfPresent(dict, kCMSampleAttachmentKey_PartialSync, NULL)) { buf->s.frametype = HB_FRAME_I; } else if (CFDictionaryGetValueIfPresent(dict, kCMSampleAttachmentKey_IsDependedOnByOthers,(const void **) &b)) { Boolean bv = CFBooleanGetValue(b); if (bv) { buf->s.frametype = HB_FRAME_P; } else { buf->s.frametype = HB_FRAME_B; buf->s.flags &= ~HB_FLAG_FRAMETYPE_REF; } } else { buf->s.frametype = HB_FRAME_P; } } } } } static void hb_vt_set_timestamps(hb_work_private_t *pv, CMSampleBufferRef sampleBuffer, hb_buffer_t *buf) { CMTime presentationTimeStamp = CMSampleBufferGetPresentationTimeStamp(sampleBuffer); CMTime duration = CMSampleBufferGetDuration(sampleBuffer); buf->s.duration = duration.value; buf->s.start = presentationTimeStamp.value; buf->s.stop = presentationTimeStamp.value + buf->s.duration; // Use the cached frame info to get the start time of Nth frame // Note that start Nth frame != start time this buffer since the // output buffers have rearranged start times. if (pv->frameno_out < pv->job->areBframes) { buf->s.renderOffset = hb_vt_get_frame_start(pv, pv->frameno_out) - pv->dts_delay; } else { buf->s.renderOffset = hb_vt_get_frame_start(pv, pv->frameno_out - pv->job->areBframes); } pv->frameno_out++; } static hb_buffer_t * hb_vt_get_buf(CMSampleBufferRef sampleBuffer, hb_work_private_t *pv) { CMItemCount samplesNum = CMSampleBufferGetNumSamples(sampleBuffer); if (samplesNum > 1) { hb_log("VTCompressionSession: more than 1 sample in sampleBuffer = %ld", samplesNum); } hb_buffer_t *buf = NULL; CMBlockBufferRef buffer = CMSampleBufferGetDataBuffer(sampleBuffer); if (buffer) { size_t sampleSize = CMBlockBufferGetDataLength(buffer); Boolean isContiguous = CMBlockBufferIsRangeContiguous(buffer, 0, sampleSize); if (isContiguous) { size_t lengthAtOffsetOut, totalLengthOut; char * _Nullable dataPointerOut; buf = hb_buffer_wrapper_init(); OSStatus err = CMBlockBufferGetDataPointer(buffer, 0, &lengthAtOffsetOut, &totalLengthOut, &dataPointerOut); if (err != kCMBlockBufferNoErr) { hb_log("VTCompressionSession: CMBlockBufferGetDataPointer error"); } buf->data = (uint8_t *)dataPointerOut; buf->size = totalLengthOut; buf->storage = sampleBuffer; buf->storage_type = COREMEDIA; CFRetain(sampleBuffer); } else { buf = hb_buffer_init(sampleSize); OSStatus err = CMBlockBufferCopyDataBytes(buffer, 0, sampleSize, buf->data); if (err != kCMBlockBufferNoErr) { hb_log("VTCompressionSession: CMBlockBufferCopyDataBytes error"); } } hb_vt_set_frametype(sampleBuffer, buf); hb_vt_set_timestamps(pv, sampleBuffer, buf); hb_vt_insert_dynamic_metadata(pv, sampleBuffer, &buf); if (buf->s.frametype == HB_FRAME_IDR) { hb_chapter_dequeue(pv->chapter_queue, buf); } } return buf; } void hb_vt_compression_output_callback( void *outputCallbackRefCon, void *sourceFrameRefCon, OSStatus status, VTEncodeInfoFlags infoFlags, CMSampleBufferRef sampleBuffer) { OSStatus err; if (sourceFrameRefCon) { hb_buffer_t *buf = (hb_buffer_t *)sourceFrameRefCon; if (sampleBuffer) { hb_vt_add_dynamic_hdr_metadata(sampleBuffer, buf); } hb_buffer_close(&buf); } if (status != noErr) { hb_log("VTCompressionSession: hb_vt_compression_output_callback called error"); } else if (sampleBuffer) { CFRetain(sampleBuffer); CMSimpleQueueRef queue = outputCallbackRefCon; err = CMSimpleQueueEnqueue(queue, sampleBuffer); if (err) { hb_log("VTCompressionSession: hb_vt_compression_output_callback queue full"); } } else { hb_log("VTCompressionSession: hb_vt_compression_output_callback sample buffer is NULL"); } } static OSStatus hb_vt_init_session(hb_work_object_t *w, hb_job_t *job, hb_work_private_t *pv, int cookieOnly) { OSStatus err = noErr; CFNumberRef cfValue = NULL; CFMutableDictionaryRef encoderSpecifications = CFDictionaryCreateMutable( kCFAllocatorDefault, 2, &kCFTypeDictionaryKeyCallBacks, &kCFTypeDictionaryValueCallBacks); CFDictionaryAddValue(encoderSpecifications, kVTVideoEncoderSpecification_RequireHardwareAcceleratedVideoEncoder, kCFBooleanTrue); if (pv->settings.registryID > 0) { cfValue = CFNumberCreate(kCFAllocatorDefault, kCFNumberLongLongType, &pv->settings.registryID); if (__builtin_available(macOS 10.14, *)) { CFDictionaryAddValue(encoderSpecifications, kVTVideoEncoderSpecification_RequiredEncoderGPURegistryID, cfValue); } CFRelease(cfValue); } OSType cv_pix_fmt = pv->settings.encoderPixFmt; CFNumberRef pix_fmt_num = CFNumberCreate(kCFAllocatorDefault, kCFNumberSInt32Type, &cv_pix_fmt); const void *attrs_keys[1] = { kCVPixelBufferPixelFormatTypeKey }; const void *attrs_values[1] = { pix_fmt_num }; CFDictionaryRef imageBufferAttributes = CFDictionaryCreate(kCFAllocatorDefault, attrs_keys, attrs_values, 1, &kCFTypeDictionaryKeyCallBacks, &kCFTypeDictionaryValueCallBacks); CFRelease(pix_fmt_num); CMSimpleQueueCreate(kCFAllocatorDefault, 200, &pv->queue); err = VTCompressionSessionCreate( kCFAllocatorDefault, pv->settings.width, pv->settings.height, pv->settings.codec, encoderSpecifications, imageBufferAttributes, NULL, &hb_vt_compression_output_callback, pv->queue, &pv->session); CFRelease(imageBufferAttributes); if (err != noErr) { hb_log("Error creating a VTCompressionSession err=%"PRId64"", (int64_t)err); CFRelease(encoderSpecifications); return err; } // Print the actual encoderID if (cookieOnly == 0) { CFStringRef encoderID; err = VTSessionCopyProperty(pv->session, kVTCompressionPropertyKey_EncoderID, kCFAllocatorDefault, &encoderID); if (err == noErr) { char valBuf[256]; Boolean haveStr = CFStringGetCString(encoderID, valBuf, 256, kCFStringEncodingUTF8); if (haveStr) { hb_log("encvt_Init: %s", valBuf); } CFRelease(encoderID); } } CFDictionaryRef supportedProps = NULL; err = VTCopySupportedPropertyDictionaryForEncoder(pv->settings.width, pv->settings.height, pv->settings.codec, encoderSpecifications, NULL, &supportedProps); if (err != noErr) { hb_log("Error retrieving the supported property dictionary err=%"PRId64"", (int64_t)err); } CFRelease(encoderSpecifications); // Offline encoders (such as Handbrake) should set RealTime property to False, as it disconnects the relationship // between encoder speed and target video frame rate, explicitly setting RealTime to false encourages VideoToolbox // to use the fastest mode, while adhering to the required output quality/bitrate and favorQualityOverSpeed settings hb_vt_set_property(pv->session, kVTCompressionPropertyKey_RealTime, kCFBooleanFalse); hb_vt_set_property(pv->session, kVTCompressionPropertyKey_AllowTemporalCompression, pv->settings.allowTemporalCompression); hb_vt_set_property(pv->session, kVTCompressionPropertyKey_AllowFrameReordering, pv->settings.allowFrameReordering); cfValue = CFNumberCreate(kCFAllocatorDefault, kCFNumberIntType, &pv->settings.maxKeyFrameInterval); hb_vt_set_property(pv->session, kVTCompressionPropertyKey_MaxKeyFrameInterval, cfValue); CFRelease(cfValue); if (__builtin_available(macOS 11, *)) { if (supportedProps != NULL && CFDictionaryContainsKey(supportedProps, kVTCompressionPropertyKey_PrioritizeEncodingSpeedOverQuality)) { hb_vt_set_property(pv->session, kVTCompressionPropertyKey_PrioritizeEncodingSpeedOverQuality, pv->settings.prioritizeEncodingSpeedOverQuality); } } if (__builtin_available(macOS 12, *)) { if (pv->settings.maxAllowedFrameQP > -1) { cfValue = CFNumberCreate(kCFAllocatorDefault, kCFNumberIntType, &pv->settings.maxAllowedFrameQP); hb_vt_set_property(pv->session, kVTCompressionPropertyKey_MaxAllowedFrameQP, cfValue); CFRelease(cfValue); } } if (__builtin_available(macOS 13, *)) { if (pv->settings.minAllowedFrameQP > -1) { cfValue = CFNumberCreate(kCFAllocatorDefault, kCFNumberIntType, &pv->settings.minAllowedFrameQP); hb_vt_set_property(pv->session, kVTCompressionPropertyKey_MinAllowedFrameQP, cfValue); CFRelease(cfValue); } if (pv->settings.maxReferenceBufferCount > -1) { cfValue = CFNumberCreate(kCFAllocatorDefault, kCFNumberIntType, &pv->settings.maxReferenceBufferCount); hb_vt_set_property(pv->session, kVTCompressionPropertyKey_ReferenceBufferCount, cfValue); CFRelease(cfValue); } } if (pv->settings.maxFrameDelayCount >= 0) { cfValue = CFNumberCreate(kCFAllocatorDefault, kCFNumberIntType, &pv->settings.maxFrameDelayCount); hb_vt_set_property(pv->session, kVTCompressionPropertyKey_MaxFrameDelayCount, cfValue); CFRelease(cfValue); } #if __MAC_OS_X_VERSION_MAX_ALLOWED >= 150000 if (__builtin_available(macOS 15, *)) { // Control spatial adaptation of the quantization parameter (QP) based on per-frame statistics. if (pv->settings.disableSpatialAdaptiveQP == kCFBooleanTrue) { int32_t spatialAdaptiveQP = kVTQPModulationLevel_Disable; cfValue = CFNumberCreate(kCFAllocatorDefault, kCFNumberSInt32Type, &spatialAdaptiveQP); hb_vt_set_property(pv->session, kVTCompressionPropertyKey_SpatialAdaptiveQPLevel, cfValue); CFRelease(cfValue); } // Requests that the encoder retain the specified number of frames during encoding. if (pv->settings.lookAheadFrameCount >= 0) { cfValue = CFNumberCreate(kCFAllocatorDefault, kCFNumberIntType, &pv->settings.lookAheadFrameCount); hb_vt_set_property(pv->session, kVTCompressionPropertyKey_SuggestedLookAheadFrameCount, cfValue); CFRelease(cfValue); } } #endif if ( #if defined(__aarch64__) job->pass_id == HB_PASS_ENCODE && #endif pv->settings.vbv.maxrate > 0 && pv->settings.vbv.bufsize > 0) { hb_vt_set_data_rate_limits(pv->session, pv->settings.vbv.bufsize, pv->settings.vbv.maxrate); } if (pv->settings.fieldDetail != HB_VT_FIELDORDER_PROGRESSIVE) { int count = 2; cfValue = CFNumberCreate(kCFAllocatorDefault, kCFNumberIntType, &count); hb_vt_set_property(pv->session, kVTCompressionPropertyKey_FieldCount, cfValue); CFRelease(cfValue); CFStringRef cfStringValue = NULL; switch (pv->settings.fieldDetail) { case HB_VT_FIELDORDER_BFF: cfStringValue = kCMFormatDescriptionFieldDetail_TemporalBottomFirst; break; case HB_VT_FIELDORDER_TFF: default: cfStringValue = kCMFormatDescriptionFieldDetail_TemporalTopFirst; break; } hb_vt_set_property(pv->session, kVTCompressionPropertyKey_FieldDetail, cfStringValue); } hb_vt_set_property(pv->session, kVTCompressionPropertyKey_ColorPrimaries, hb_cv_colr_pri_xlat(pv->settings.color.prim)); hb_vt_set_property(pv->session, kVTCompressionPropertyKey_TransferFunction, hb_cv_colr_tra_xlat(pv->settings.color.transfer)); CFNumberRef gamma = hb_cv_colr_gamma_xlat(pv->settings.color.transfer); if (gamma) { hb_vt_set_property(pv->session, CFSTR("GammaLevel"), gamma); CFRelease(gamma); } hb_vt_set_property(pv->session, kVTCompressionPropertyKey_YCbCrMatrix, hb_cv_colr_mat_xlat(pv->settings.color.matrix)); hb_vt_set_property(pv->session, CFSTR("ChromaLocationTopField"), hb_cv_chroma_loc_xlat(pv->settings.color.chromaLocation)); hb_vt_set_property(pv->session, CFSTR("ChromaLocationBottomField"), hb_cv_chroma_loc_xlat(pv->settings.color.chromaLocation)); if (supportedProps != NULL && CFDictionaryContainsKey(supportedProps, kVTCompressionPropertyKey_MasteringDisplayColorVolume) && pv->settings.color.masteringDisplay != NULL) { hb_vt_set_property(pv->session, kVTCompressionPropertyKey_MasteringDisplayColorVolume, pv->settings.color.masteringDisplay); } if (supportedProps != NULL && CFDictionaryContainsKey(supportedProps, kVTCompressionPropertyKey_ContentLightLevelInfo) && pv->settings.color.contentLightLevel != NULL) { hb_vt_set_property(pv->session, kVTCompressionPropertyKey_ContentLightLevelInfo, pv->settings.color.contentLightLevel); } if (supportedProps != NULL && CFDictionaryContainsKey(supportedProps, CFSTR("AmbientViewingEnvironment")) && pv->settings.color.ambientViewingEnviroment != NULL) { hb_vt_set_property(pv->session, CFSTR("AmbientViewingEnvironment"), pv->settings.color.ambientViewingEnviroment); } if (__builtin_available(macOS 11.0, *)) { if (pv->settings.codec != kCMVideoCodecType_H264) { // VideoToolbox can generate Dolby Vision 8.4 RPUs for HLG video, // however we preserve the RPUs from the source file, so disable it // to avoid having two sets of RPUs per frame. if (supportedProps != NULL && CFDictionaryContainsKey(supportedProps, kVTCompressionPropertyKey_HDRMetadataInsertionMode)) { hb_vt_set_property(pv->session, kVTCompressionPropertyKey_HDRMetadataInsertionMode, kVTHDRMetadataInsertionMode_None); } if (supportedProps != NULL && CFDictionaryContainsKey(supportedProps, kVTCompressionPropertyKey_PreserveDynamicHDRMetadata)) { hb_vt_set_property(pv->session, kVTCompressionPropertyKey_PreserveDynamicHDRMetadata, pv->settings.preserveDynamicHDRMetadata); } } } CFNumberRef parNum = CFNumberCreate(kCFAllocatorDefault, kCFNumberSInt32Type, &pv->settings.par.num); CFNumberRef parDen = CFNumberCreate(kCFAllocatorDefault, kCFNumberSInt32Type, &pv->settings.par.den); CFMutableDictionaryRef pixelAspectRatio = CFDictionaryCreateMutable(kCFAllocatorDefault, 2, &kCFTypeDictionaryKeyCallBacks, &kCFTypeDictionaryValueCallBacks); CFDictionaryAddValue(pixelAspectRatio, kCMFormatDescriptionKey_PixelAspectRatioHorizontalSpacing, parNum); CFDictionaryAddValue(pixelAspectRatio, kCMFormatDescriptionKey_PixelAspectRatioVerticalSpacing, parDen); hb_vt_set_property(pv->session, kVTCompressionPropertyKey_PixelAspectRatio, pixelAspectRatio); CFRelease(parNum); CFRelease(parDen); CFRelease(pixelAspectRatio); cfValue = CFNumberCreate(kCFAllocatorDefault, kCFNumberDoubleType, &pv->settings.expectedFrameRate); hb_vt_set_property(pv->session, kVTCompressionPropertyKey_ExpectedFrameRate, cfValue); CFRelease(cfValue); if (pv->settings.quality > -1) { cfValue = CFNumberCreate(kCFAllocatorDefault, kCFNumberDoubleType, &pv->settings.quality); hb_vt_set_property(pv->session, kVTCompressionPropertyKey_Quality, cfValue); CFRelease(cfValue); } else { cfValue = CFNumberCreate(kCFAllocatorDefault, kCFNumberIntType, &pv->settings.averageBitRate); hb_vt_set_property(pv->session, kVTCompressionPropertyKey_AverageBitRate, cfValue); CFRelease(cfValue); } hb_vt_set_property(pv->session, kVTCompressionPropertyKey_ProfileLevel, pv->settings.profileLevel); if (pv->settings.codec == kCMVideoCodecType_H264) { if (pv->settings.h264.entropyMode) { hb_vt_set_property(pv->session, kVTCompressionPropertyKey_H264EntropyMode, pv->settings.h264.entropyMode); } if (pv->settings.h264.maxSliceBytes) { cfValue = CFNumberCreate(kCFAllocatorDefault, kCFNumberIntType, &pv->settings.h264.maxSliceBytes); hb_vt_set_property(pv->session, kVTCompressionPropertyKey_MaxH264SliceBytes, cfValue); CFRelease(cfValue); } } if (supportedProps) { CFRelease(supportedProps); } // Multi-pass if (job->pass_id == HB_PASS_ENCODE_ANALYSIS) { char *filename = hb_get_temporary_filename("videotoolbox.log");; CFURLRef url = NULL; if (filename) { url = CFURLCreateFromFileSystemRepresentation(kCFAllocatorDefault, (const UInt8 *)filename, strlen(filename), false); free(filename); } if (url == NULL) { return -1; } err = VTMultiPassStorageCreate(kCFAllocatorDefault, url, kCMTimeRangeInvalid, NULL, &pv->passStorage); CFRelease(url); if (err != noErr) { return err; } hb_vt_set_property(pv->session, kVTCompressionPropertyKey_MultiPassStorage, pv->passStorage); err = VTCompressionSessionBeginPass(pv->session, 0, 0); if (err != noErr) { hb_log("VTCompressionSessionBeginPass failed"); } } err = VTCompressionSessionPrepareToEncodeFrames(pv->session); if (err != noErr) { hb_log("VTCompressionSessionPrepareToEncodeFrames failed"); return err; } CFBooleanRef allowFrameReordering; err = VTSessionCopyProperty(pv->session, kVTCompressionPropertyKey_AllowFrameReordering, kCFAllocatorDefault, &allowFrameReordering); if (err != noErr) { hb_log("VTSessionCopyProperty: kVTCompressionPropertyKey_AllowFrameReordering failed"); } else { if (CFBooleanGetValue(allowFrameReordering)) { // There is no way to know if b-pyramid will be // used or not, to be safe always assume it's enabled job->areBframes = 2; } CFRelease(allowFrameReordering); } return err; } static void hb_vt_set_cookie(hb_work_object_t *w, CMFormatDescriptionRef format) { CFDictionaryRef extentions = CMFormatDescriptionGetExtensions(format); if (!extentions) { hb_log("VTCompressionSession: Format Description Extensions error"); } else { CFStringRef key = CMVideoFormatDescriptionGetCodecType(format) == kCMVideoCodecType_H264 ? CFSTR("avcC") : CFSTR("hvcC"); CFDictionaryRef atoms = CFDictionaryGetValue(extentions, kCMFormatDescriptionExtension_SampleDescriptionExtensionAtoms); if (atoms) { CFDataRef magicCookie = CFDictionaryGetValue(atoms, key); if (magicCookie) { const uint8_t *hvcCAtom = CFDataGetBytePtr(magicCookie); CFIndex size = CFDataGetLength(magicCookie); hb_set_extradata(w->extradata, hvcCAtom, size); } else { hb_log("VTCompressionSession: Magic Cookie error"); } } } } static OSStatus hb_vt_create_cookie(hb_work_object_t *w, hb_job_t *job, hb_work_private_t *pv) { OSStatus err; CVPixelBufferRef pix_buf = NULL; CVPixelBufferPoolRef pool = NULL; err = hb_vt_init_session(w, job, pv, 1); if (err != noErr) { goto fail; } pool = VTCompressionSessionGetPixelBufferPool(pv->session); if (pool == NULL) { hb_log("VTCompressionSession: VTCompressionSessionGetPixelBufferPool error"); err = -1; goto fail; } err = CVPixelBufferPoolCreatePixelBuffer(NULL, pool, &pix_buf); if (kCVReturnSuccess != err) { hb_log("VTCompressionSession: CVPixelBufferPoolCreatePixelBuffer error"); } CMTime pts = CMTimeMake(0, pv->settings.timescale); CMTime duration = CMTimeMake(pv->settings.timescale, pv->settings.timescale); err = VTCompressionSessionEncodeFrame( pv->session, pix_buf, pts, duration, NULL, NULL, NULL); if (noErr != err) { hb_log("VTCompressionSession: VTCompressionSessionEncodeFrame error"); } err = VTCompressionSessionCompleteFrames(pv->session, kCMTimeIndefinite); if (noErr != err) { hb_log("VTCompressionSession: VTCompressionSessionCompleteFrames error"); } CMSampleBufferRef sampleBuffer = (CMSampleBufferRef)CMSimpleQueueDequeue(pv->queue); if (!sampleBuffer) { hb_log("VTCompressionSession: sampleBuffer == NULL"); goto fail; } else { CMFormatDescriptionRef format = CMSampleBufferGetFormatDescription(sampleBuffer); if (!format) { hb_log("VTCompressionSession: Format Description error"); } else { pv->format = format; CFRetain(pv->format); hb_vt_set_cookie(w, format); } CFRelease(sampleBuffer); } fail: CVPixelBufferRelease(pix_buf); VTCompressionSessionInvalidate(pv->session); if (pv->passStorage) { VTMultiPassStorageClose(pv->passStorage); CFRelease(pv->passStorage); } if (pv->session) { CFRelease(pv->session); } if (pv->queue) { CFRelease(pv->queue); } pv->session = NULL; pv->passStorage = NULL; pv->queue = NULL; return err; } static OSStatus hb_vt_reuse_session(hb_work_object_t *w, hb_job_t * job, hb_work_private_t *pv) { OSStatus err = noErr; hb_interjob_t *interjob = hb_interjob_get(job->h); vt_interjob_t *context = interjob->context; hb_vt_set_cookie(w, context->format); pv->session = context->session; pv->passStorage = context->passStorage; pv->queue = context->queue; pv->format = context->format; job->areBframes = context->areBframes; if (err != noErr) { hb_log("Error reusing a VTCompressionSession err=%"PRId64"", (int64_t)err); return err; } // This should tell us the time range the encoder thinks it can enhance in the second pass // currently we ignore this, because it would mean storing the frames from the first pass // somewhere on disk. // And it seems the range is always the entire movie duration. err = VTCompressionSessionGetTimeRangesForNextPass(pv->session, &pv->timeRangeCount, &pv->timeRangeArray); if (err != noErr) { hb_log("Error beginning a VTCompressionSession final pass err=%"PRId64"", (int64_t)err); return err; } hb_log("encvt_Init: starting pass with time ranges: %ld", pv->timeRangeCount); for (CMItemCount i = 0; i < pv->timeRangeCount; i++) { hb_log("encvt_init: %lld, %lld", pv->timeRangeArray[i].start.value, pv->timeRangeArray[i].duration.value); } err = VTCompressionSessionBeginPass(pv->session, kVTCompressionSessionBeginFinalPass, 0); if (err != noErr) { hb_log("Error beginning a VTCompressionSession final pass err=%"PRId64"", (int64_t)err); return err; } free(context); interjob->context = NULL; return err; } int encvt_init(hb_work_object_t *w, hb_job_t *job) { OSStatus err; hb_work_private_t *pv = calloc(1, sizeof(hb_work_private_t)); if (pv == NULL) { *job->die = 1; return -1; } w->private_data = pv; pv->job = job; pv->chapter_queue = hb_chapter_queue_init(); err = hb_vt_settings_xlat(pv, job); if (err != noErr) { *job->die = 1; return -1; } err = hb_vt_parse_options(pv, job); if (err != noErr) { *job->die = 1; return -1; } pv->attachments = hb_vt_attachments_xlat(pv->job); pv->remainingPasses = job->pass_id == HB_PASS_ENCODE_ANALYSIS ? 1 : 0; if (job->pass_id != HB_PASS_ENCODE_FINAL) { err = hb_vt_create_cookie(w, job, pv); if (err != noErr) { hb_log("VTCompressionSession: Magic Cookie Error err=%"PRId64"", (int64_t)err); *job->die = 1; return -1; } // Read the actual level and tier and set // the Dolby Vision level and data limits if (job->passthru_dynamic_hdr_metadata & HB_HDR_DYNAMIC_METADATA_DOVI) { int level_idc, high_tier; hb_parse_h265_extradata(*w->extradata, &level_idc, &high_tier); int pps = (double)job->width * job->height * (job->vrate.num / job->vrate.den); int bitrate = job->vquality == HB_INVALID_VIDEO_QUALITY ? job->vbitrate : -1; // Dolby Vision requires VBV settings to enable HRD // set the max value for the current level or guess one if (pv->settings.vbv.maxrate == 0 || pv->settings.vbv.bufsize == 0) { int max_rate = hb_dovi_max_rate(job->vcodec, job->width, pps, bitrate * 1.5, level_idc, high_tier); pv->settings.vbv.maxrate = max_rate; pv->settings.vbv.bufsize = max_rate; } job->dovi.dv_level = hb_dovi_level(job->width, pps, pv->settings.vbv.maxrate, high_tier); // VideoToolbox CQ seems to not support data rate limits correctly, // just set a high enough level for now, and reset the vbv settings if (job->vquality != HB_INVALID_VIDEO_QUALITY) { pv->settings.vbv.maxrate = 0; pv->settings.vbv.bufsize = 0; hb_log("encvt_Init: data rate limits not supported in CQ mode, Dolby Vision file might be out of specs"); } // Data limits are poorly supported in average mode too, disabling for now else { pv->settings.vbv.maxrate = 0; pv->settings.vbv.bufsize = 0; hb_log("encvt_Init: data rate limits not supported in ABR mode, Dolby Vision file might be out of specs"); } } err = hb_vt_init_session(w, job, pv, 0); if (err != noErr) { hb_log("VTCompressionSession: Error creating a VTCompressionSession err=%"PRId64"", (int64_t)err); *job->die = 1; return -1; } } else { err = hb_vt_reuse_session(w, job, pv); if (err != noErr) { hb_log("VTCompressionSession: Error reusing a VTCompressionSession err=%"PRId64"", (int64_t)err); *job->die = 1; return -1; } } return 0; } void encvt_close(hb_work_object_t * w) { hb_work_private_t *pv = w->private_data; if (pv == NULL) { return; } hb_chapter_queue_close(&pv->chapter_queue); // A cancelled encode doesn't send an EOF, // do some additional cleanups here if (*pv->job->die) { if (pv->session) { VTCompressionSessionCompleteFrames(pv->session, kCMTimeIndefinite); } if (pv->queue) { CMSampleBufferRef sampleBuffer; while ((sampleBuffer = (CMSampleBufferRef)CMSimpleQueueDequeue(pv->queue))) { CFRelease(sampleBuffer); } } } if (pv->remainingPasses == 0 || *pv->job->die) { if (pv->session) { VTCompressionSessionInvalidate(pv->session); CFRelease(pv->session); } if (pv->passStorage) { VTMultiPassStorageClose(pv->passStorage); CFRelease(pv->passStorage); } if (pv->queue) { CFRelease(pv->queue); } if (pv->format) { CFRelease(pv->format); } } if (pv->settings.color.masteringDisplay) { CFRelease(pv->settings.color.masteringDisplay); } if (pv->settings.color.contentLightLevel) { CFRelease(pv->settings.color.contentLightLevel); } if (pv->settings.color.ambientViewingEnviroment) { CFRelease(pv->settings.color.ambientViewingEnviroment); } if (pv->attachments) { CFRelease(pv->attachments); } free(pv); w->private_data = NULL; } static void hb_vt_send(hb_work_private_t *pv, hb_buffer_t *in) { CVPixelBufferRef pix_buf = hb_vt_get_pix_buf(pv, in); if (pix_buf == NULL) { hb_buffer_close(&in); hb_log("VTCompressionSession: CVPixelBuffer error"); } else { CFDictionaryRef frameProperties = NULL; if (in->s.new_chap && pv->job->chapter_markers) { // macOS Sonoma has got an unfixed bug that makes the whole // system crash and restart on M* Ultra if we force a keyframe // on the first frame. So avoid that. if (pv->frameno_in) { // chapters have to start with an IDR frame const void *keys[1] = { kVTEncodeFrameOptionKey_ForceKeyFrame }; const void *values[1] = { kCFBooleanTrue }; frameProperties = CFDictionaryCreate(NULL, keys, values, 1, &kCFTypeDictionaryKeyCallBacks, &kCFTypeDictionaryValueCallBacks); } hb_chapter_enqueue(pv->chapter_queue, in); } hb_cv_set_attachments(pix_buf, pv->attachments); // VideoToolbox DTS are greater than PTS // So we remember the PTS values and compute DTS ourselves. hb_vt_save_frame_info(pv, in); hb_vt_compute_dts_offset(pv, in); pv->frameno_in++; // Send the frame to be encoded OSStatus err = VTCompressionSessionEncodeFrame( pv->session, pix_buf, CMTimeMake(in->s.start, pv->settings.timescale), CMTimeMake(in->s.duration, pv->settings.timescale), frameProperties, in, NULL); CVPixelBufferRelease(pix_buf); if (err) { hb_log("VTCompressionSession: VTCompressionSessionEncodeFrame error"); } if (frameProperties) { CFRelease(frameProperties); } } } static hb_buffer_t * hb_vt_receive(hb_work_private_t *pv) { if (pv->frameno_in <= pv->job->areBframes) { // dts_delay not yet set. Queue up buffers till it is set return NULL; } CMSampleBufferRef sampleBuffer = (CMSampleBufferRef)CMSimpleQueueDequeue(pv->queue); hb_buffer_t *buf_out = NULL; if (sampleBuffer) { buf_out = hb_vt_get_buf(sampleBuffer, pv); CFRelease(sampleBuffer); } return buf_out; } static void hb_vt_encode(hb_work_private_t *pv, hb_buffer_t *in, hb_buffer_list_t *list) { hb_vt_send(pv, in); hb_buffer_t *out; while ((out = hb_vt_receive(pv))) { hb_buffer_list_append(list, out); } } static void hb_vt_flush(hb_work_private_t *pv, hb_buffer_t *in, hb_buffer_list_t *list) { VTCompressionSessionCompleteFrames(pv->session, kCMTimeIndefinite); hb_buffer_t *out; while ((out = hb_vt_receive(pv))) { hb_buffer_list_append(list, out); } // Passthru the EOF to the end of the chain hb_buffer_list_append(list, in); } static void hb_vt_end_pass(hb_work_private_t *pv) { if (pv->job->pass_id == HB_PASS_ENCODE_ANALYSIS) { OSStatus err = noErr; Boolean furtherPassesRequestedOut; err = VTCompressionSessionEndPass(pv->session, &furtherPassesRequestedOut, 0); if (err != noErr) { hb_log("VTCompressionSessionEndPass error"); } if (furtherPassesRequestedOut == false) { hb_log("VTCompressionSessionEndPass: no additional pass requested"); } // Save the sessions and the related context for the next pass vt_interjob_t *context = (vt_interjob_t *)malloc(sizeof(vt_interjob_t)); context->session = pv->session; context->passStorage = pv->passStorage; context->queue = pv->queue; context->format = pv->format; context->areBframes = pv->job->areBframes; hb_interjob_t *interjob = hb_interjob_get(pv->job->h); interjob->context = context; } else if (pv->job->pass_id == HB_PASS_ENCODE_FINAL) { VTCompressionSessionEndPass(pv->session, NULL, 0); } } int encvt_work(hb_work_object_t *w, hb_buffer_t **buf_in, hb_buffer_t **buf_out) { hb_work_private_t *pv = w->private_data; hb_buffer_t *in = *buf_in; hb_buffer_list_t list; // Take ownership of the input buffer, avoid a memcpy *buf_in = NULL; hb_buffer_list_clear(&list); if (in->s.flags & HB_BUF_FLAG_EOF) { // EOF on input. Flush any frames still in the decoder then // send the eof downstream to tell the muxer we're done. hb_vt_flush(pv, in, &list); *buf_out = hb_buffer_list_clear(&list); hb_vt_end_pass(pv); return HB_WORK_DONE; } // Not EOF - encode the packet hb_vt_encode(pv, in, &list); *buf_out = hb_buffer_list_clear(&list); return HB_WORK_OK; }