mirror of https://github.com/HandBrake/HandBrake
2058 lines
69 KiB
C
2058 lines
69 KiB
C
/* encvt.c
|
|
|
|
Copyright (c) 2003-2025 HandBrake Team
|
|
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 v2.
|
|
For full terms see the file COPYING file or visit http://www.gnu.org/licenses/gpl-2.0.html
|
|
*/
|
|
|
|
#include <VideoToolbox/VideoToolbox.h>
|
|
#include <CoreMedia/CoreMedia.h>
|
|
#include <CoreVideo/CoreVideo.h>
|
|
#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;
|
|
}
|