PatTheMav 566ce79b0f mac-videotoolbox: Enable CBR and CRF on hardware encoders only
Both rate control methods only work on hardware encoders and will error
out when selected for software encoders, so we shouldn't show them there

Co-Authored-By: Sebastian Beckmann <beckmann.sebastian@outlook.de>
2022-07-11 23:45:43 +02:00

1110 lines
31 KiB
C

#include <obs-module.h>
#include <util/darray.h>
#include <util/platform.h>
#include <obs-avc.h>
#include <CoreFoundation/CoreFoundation.h>
#include <VideoToolbox/VideoToolbox.h>
#include <VideoToolbox/VTVideoEncoderList.h>
#include <CoreMedia/CoreMedia.h>
#include <util/apple/cfstring-utils.h>
#include <assert.h>
#define VT_LOG(level, format, ...) \
blog(level, "[VideoToolbox encoder]: " format, ##__VA_ARGS__)
#define VT_LOG_ENCODER(encoder, level, format, ...) \
blog(level, "[VideoToolbox %s: 'h264']: " format, \
obs_encoder_get_name(encoder), ##__VA_ARGS__)
#define VT_BLOG(level, format, ...) \
VT_LOG_ENCODER(enc->encoder, level, format, ##__VA_ARGS__)
struct vt_encoder_type_data {
const char *disp_name;
const char *id;
bool hardware_accelerated;
};
struct vt_encoder {
obs_encoder_t *encoder;
const char *vt_encoder_id;
uint32_t width;
uint32_t height;
uint32_t keyint;
uint32_t fps_num;
uint32_t fps_den;
const char *rate_control;
uint32_t bitrate;
float quality;
bool limit_bitrate;
uint32_t rc_max_bitrate;
float rc_max_bitrate_window;
const char *profile;
bool bframes;
enum video_format obs_pix_fmt;
int vt_pix_fmt;
enum video_colorspace colorspace;
bool fullrange;
VTCompressionSessionRef session;
CMSimpleQueueRef queue;
bool hw_enc;
DARRAY(uint8_t) packet_data;
DARRAY(uint8_t) extra_data;
};
static void log_osstatus(int log_level, struct vt_encoder *enc,
const char *context, OSStatus code)
{
char *c_str = NULL;
CFErrorRef err = CFErrorCreate(kCFAllocatorDefault,
kCFErrorDomainOSStatus, code, NULL);
CFStringRef str = CFErrorCopyDescription(err);
c_str = cfstr_copy_cstr(str, kCFStringEncodingUTF8);
if (c_str) {
if (enc)
VT_BLOG(log_level, "Error in %s: %s", context, c_str);
else
VT_LOG(log_level, "Error in %s: %s", context, c_str);
}
bfree(c_str);
CFRelease(str);
CFRelease(err);
}
static CFStringRef obs_to_vt_profile(const char *profile)
{
if (strcmp(profile, "baseline") == 0)
return kVTProfileLevel_H264_Baseline_AutoLevel;
else if (strcmp(profile, "main") == 0)
return kVTProfileLevel_H264_Main_AutoLevel;
else if (strcmp(profile, "high") == 0)
return kVTProfileLevel_H264_High_AutoLevel;
else
return kVTProfileLevel_H264_Main_AutoLevel;
}
static CFStringRef obs_to_vt_colorspace(enum video_colorspace cs)
{
if (cs == VIDEO_CS_709)
return kCVImageBufferYCbCrMatrix_ITU_R_709_2;
else if (cs == VIDEO_CS_601)
return kCVImageBufferYCbCrMatrix_ITU_R_601_4;
return NULL;
}
#define STATUS_CHECK(c) \
code = c; \
if (code) { \
log_osstatus(LOG_ERROR, enc, #c, code); \
goto fail; \
}
#define SESSION_CHECK(x) \
if ((code = (x)) != noErr) \
return code;
static OSStatus session_set_prop_float(VTCompressionSessionRef session,
CFStringRef key, float val)
{
CFNumberRef n = CFNumberCreate(NULL, kCFNumberFloat32Type, &val);
OSStatus code = VTSessionSetProperty(session, key, n);
CFRelease(n);
return code;
}
static OSStatus session_set_prop_int(VTCompressionSessionRef session,
CFStringRef key, int32_t val)
{
CFNumberRef n = CFNumberCreate(NULL, kCFNumberSInt32Type, &val);
OSStatus code = VTSessionSetProperty(session, key, n);
CFRelease(n);
return code;
}
static OSStatus session_set_prop_str(VTCompressionSessionRef session,
CFStringRef key, char *val)
{
CFStringRef s = CFStringCreateWithFileSystemRepresentation(NULL, val);
OSStatus code = VTSessionSetProperty(session, key, s);
CFRelease(s);
return code;
}
static OSStatus session_set_prop(VTCompressionSessionRef session,
CFStringRef key, CFTypeRef val)
{
return VTSessionSetProperty(session, key, val);
}
static OSStatus session_set_bitrate(VTCompressionSessionRef session,
const char *rate_control, int new_bitrate,
float quality, bool limit_bitrate,
int max_bitrate, float max_bitrate_window)
{
OSStatus code;
bool can_limit_bitrate;
CFStringRef compressionPropertyKey;
if (strcmp(rate_control, "CBR") == 0) {
compressionPropertyKey =
kVTCompressionPropertyKey_AverageBitRate;
can_limit_bitrate = true;
if (__builtin_available(macOS 13.0, *)) {
#if __MAC_OS_X_VERSION_MAX_ALLOWED >= 130000
#ifdef __aarch64__
if (true) {
#else
if (os_get_emulation_status() == true) {
#endif
compressionPropertyKey =
kVTCompressionPropertyKey_ConstantBitRate;
can_limit_bitrate = false;
} else {
VT_LOG(LOG_WARNING,
"CBR support for VideoToolbox encoder requires Apple Silicon. "
"Will use ABR instead.");
}
#else
VT_LOG(LOG_WARNING,
"CBR support for VideoToolbox not available in this build of OBS. "
"Will use ABR instead.");
#endif
} else {
VT_LOG(LOG_WARNING,
"CBR support for VideoToolbox encoder requires macOS 13 or newer. "
"Will use ABR instead.");
}
} else if (strcmp(rate_control, "ABR") == 0) {
compressionPropertyKey =
kVTCompressionPropertyKey_AverageBitRate;
can_limit_bitrate = true;
} else if (strcmp(rate_control, "CRF") == 0) {
#ifdef __aarch64__
if (true) {
#else
if (os_get_emulation_status() == true) {
#endif
compressionPropertyKey =
kVTCompressionPropertyKey_Quality;
SESSION_CHECK(session_set_prop_float(
session, compressionPropertyKey, quality));
} else {
VT_LOG(LOG_WARNING,
"CRF support for VideoToolbox encoder requires Apple Silicon. "
"Will use ABR instead.");
compressionPropertyKey =
kVTCompressionPropertyKey_AverageBitRate;
}
can_limit_bitrate = true;
} else {
VT_LOG(LOG_ERROR,
"Selected rate control method is not supported: %s",
rate_control);
return kVTParameterErr;
}
if (compressionPropertyKey != kVTCompressionPropertyKey_Quality) {
SESSION_CHECK(session_set_prop_int(
session, compressionPropertyKey, new_bitrate * 1000));
}
if (limit_bitrate && can_limit_bitrate) {
int32_t cpb_size = max_bitrate * 125 * max_bitrate_window;
CFNumberRef cf_cpb_size =
CFNumberCreate(NULL, kCFNumberIntType, &cpb_size);
CFNumberRef cf_cpb_window_s = CFNumberCreate(
NULL, kCFNumberFloatType, &max_bitrate_window);
CFMutableArrayRef rate_control = CFArrayCreateMutable(
kCFAllocatorDefault, 2, &kCFTypeArrayCallBacks);
CFArrayAppendValue(rate_control, cf_cpb_size);
CFArrayAppendValue(rate_control, cf_cpb_window_s);
code = session_set_prop(
session, kVTCompressionPropertyKey_DataRateLimits,
rate_control);
CFRelease(cf_cpb_size);
CFRelease(cf_cpb_window_s);
CFRelease(rate_control);
if (code == kVTPropertyNotSupportedErr) {
log_osstatus(LOG_WARNING, NULL,
"setting DataRateLimits on session", code);
return noErr;
}
}
return noErr;
}
static OSStatus session_set_colorspace(VTCompressionSessionRef session,
enum video_colorspace cs)
{
CFStringRef matrix = obs_to_vt_colorspace(cs);
OSStatus code;
if (matrix != NULL) {
SESSION_CHECK(session_set_prop(
session, kVTCompressionPropertyKey_ColorPrimaries,
kCVImageBufferColorPrimaries_ITU_R_709_2));
SESSION_CHECK(session_set_prop(
session, kVTCompressionPropertyKey_TransferFunction,
kCVImageBufferTransferFunction_ITU_R_709_2));
SESSION_CHECK(session_set_prop(
session, kVTCompressionPropertyKey_YCbCrMatrix,
matrix));
}
return noErr;
}
#undef SESSION_CHECK
void sample_encoded_callback(void *data, void *source, OSStatus status,
VTEncodeInfoFlags info_flags,
CMSampleBufferRef buffer)
{
UNUSED_PARAMETER(status);
UNUSED_PARAMETER(info_flags);
CMSimpleQueueRef queue = data;
CVPixelBufferRef pixbuf = source;
if (buffer != NULL) {
CFRetain(buffer);
CMSimpleQueueEnqueue(queue, buffer);
}
CFRelease(pixbuf);
}
#define ENCODER_ID kVTVideoEncoderSpecification_EncoderID
#define ENABLE_HW_ACCEL \
kVTVideoEncoderSpecification_EnableHardwareAcceleratedVideoEncoder
#define REQUIRE_HW_ACCEL \
kVTVideoEncoderSpecification_RequireHardwareAcceleratedVideoEncoder
static inline CFMutableDictionaryRef
create_encoder_spec(const char *vt_encoder_id)
{
CFMutableDictionaryRef encoder_spec = CFDictionaryCreateMutable(
kCFAllocatorDefault, 3, &kCFTypeDictionaryKeyCallBacks,
&kCFTypeDictionaryValueCallBacks);
CFStringRef id =
CFStringCreateWithFileSystemRepresentation(NULL, vt_encoder_id);
CFDictionaryAddValue(encoder_spec, ENCODER_ID, id);
CFRelease(id);
CFDictionaryAddValue(encoder_spec, ENABLE_HW_ACCEL, kCFBooleanTrue);
CFDictionaryAddValue(encoder_spec, REQUIRE_HW_ACCEL, kCFBooleanFalse);
return encoder_spec;
}
#undef ENCODER_ID
#undef REQUIRE_HW_ACCEL
#undef ENABLE_HW_ACCEL
static inline CFMutableDictionaryRef create_pixbuf_spec(struct vt_encoder *enc)
{
CFMutableDictionaryRef pixbuf_spec = CFDictionaryCreateMutable(
kCFAllocatorDefault, 3, &kCFTypeDictionaryKeyCallBacks,
&kCFTypeDictionaryValueCallBacks);
CFNumberRef n =
CFNumberCreate(NULL, kCFNumberSInt32Type, &enc->vt_pix_fmt);
CFDictionaryAddValue(pixbuf_spec, kCVPixelBufferPixelFormatTypeKey, n);
CFRelease(n);
n = CFNumberCreate(NULL, kCFNumberSInt32Type, &enc->width);
CFDictionaryAddValue(pixbuf_spec, kCVPixelBufferWidthKey, n);
CFRelease(n);
n = CFNumberCreate(NULL, kCFNumberSInt32Type, &enc->height);
CFDictionaryAddValue(pixbuf_spec, kCVPixelBufferHeightKey, n);
CFRelease(n);
return pixbuf_spec;
}
static bool create_encoder(struct vt_encoder *enc)
{
OSStatus code;
VTCompressionSessionRef s;
CFDictionaryRef encoder_spec = create_encoder_spec(enc->vt_encoder_id);
CFDictionaryRef pixbuf_spec = create_pixbuf_spec(enc);
STATUS_CHECK(VTCompressionSessionCreate(
kCFAllocatorDefault, enc->width, enc->height,
kCMVideoCodecType_H264, encoder_spec, pixbuf_spec, NULL,
&sample_encoded_callback, enc->queue, &s));
CFRelease(encoder_spec);
CFRelease(pixbuf_spec);
CFBooleanRef b = NULL;
code = VTSessionCopyProperty(
s,
kVTCompressionPropertyKey_UsingHardwareAcceleratedVideoEncoder,
NULL, &b);
if (code == noErr && (enc->hw_enc = CFBooleanGetValue(b)))
VT_BLOG(LOG_INFO, "session created with hardware encoding");
else
enc->hw_enc = false;
if (b != NULL)
CFRelease(b);
STATUS_CHECK(session_set_prop_int(
s, kVTCompressionPropertyKey_MaxKeyFrameIntervalDuration,
enc->keyint));
STATUS_CHECK(session_set_prop_int(
s, kVTCompressionPropertyKey_MaxKeyFrameInterval,
enc->keyint * ((float)enc->fps_num / enc->fps_den)));
STATUS_CHECK(session_set_prop_float(
s, kVTCompressionPropertyKey_ExpectedFrameRate,
(float)enc->fps_num / enc->fps_den));
STATUS_CHECK(session_set_prop(
s, kVTCompressionPropertyKey_AllowFrameReordering,
enc->bframes ? kCFBooleanTrue : kCFBooleanFalse));
// This can fail depending on hardware configuration
code = session_set_prop(s, kVTCompressionPropertyKey_RealTime,
kCFBooleanFalse);
if (code != noErr)
log_osstatus(
LOG_WARNING, enc,
"setting kVTCompressionPropertyKey_RealTime failed, "
"frame delay might be increased",
code);
STATUS_CHECK(session_set_prop(s, kVTCompressionPropertyKey_ProfileLevel,
obs_to_vt_profile(enc->profile)));
STATUS_CHECK(session_set_bitrate(s, enc->rate_control, enc->bitrate,
enc->quality, enc->limit_bitrate,
enc->rc_max_bitrate,
enc->rc_max_bitrate_window));
STATUS_CHECK(session_set_colorspace(s, enc->colorspace));
STATUS_CHECK(VTCompressionSessionPrepareToEncodeFrames(s));
enc->session = s;
return true;
fail:
if (encoder_spec != NULL)
CFRelease(encoder_spec);
if (pixbuf_spec != NULL)
CFRelease(pixbuf_spec);
return false;
}
static void vt_destroy(void *data)
{
struct vt_encoder *enc = data;
if (enc) {
if (enc->session != NULL) {
VTCompressionSessionInvalidate(enc->session);
CFRelease(enc->session);
}
da_free(enc->packet_data);
da_free(enc->extra_data);
bfree(enc);
}
}
static void dump_encoder_info(struct vt_encoder *enc)
{
VT_BLOG(LOG_INFO,
"settings:\n"
"\tvt_encoder_id %s\n"
"\trate_control: %s\n"
"\tbitrate: %d (kbps)\n"
"\tquality: %f\n"
"\tfps_num: %d\n"
"\tfps_den: %d\n"
"\twidth: %d\n"
"\theight: %d\n"
"\tkeyint: %d (s)\n"
"\tlimit_bitrate: %s\n"
"\trc_max_bitrate: %d (kbps)\n"
"\trc_max_bitrate_window: %f (s)\n"
"\thw_enc: %s\n"
"\tprofile: %s\n",
enc->vt_encoder_id, enc->rate_control, enc->bitrate,
enc->quality, enc->fps_num, enc->fps_den, enc->width,
enc->height, enc->keyint, enc->limit_bitrate ? "on" : "off",
enc->rc_max_bitrate, enc->rc_max_bitrate_window,
enc->hw_enc ? "on" : "off",
(enc->profile != NULL && !!strlen(enc->profile)) ? enc->profile
: "default");
}
static void vt_video_info(void *data, struct video_scale_info *info)
{
struct vt_encoder *enc = data;
if (info->format == VIDEO_FORMAT_I420) {
enc->obs_pix_fmt = info->format;
enc->vt_pix_fmt =
enc->fullrange
? kCVPixelFormatType_420YpCbCr8PlanarFullRange
: kCVPixelFormatType_420YpCbCr8Planar;
return;
}
if (info->format == VIDEO_FORMAT_I444)
VT_BLOG(LOG_WARNING, "I444 color format not supported");
// Anything else, return default
enc->obs_pix_fmt = VIDEO_FORMAT_NV12;
enc->vt_pix_fmt =
enc->fullrange
? kCVPixelFormatType_420YpCbCr8BiPlanarFullRange
: kCVPixelFormatType_420YpCbCr8BiPlanarVideoRange;
info->format = enc->obs_pix_fmt;
}
static void update_params(struct vt_encoder *enc, obs_data_t *settings)
{
video_t *video = obs_encoder_video(enc->encoder);
const struct video_output_info *voi = video_output_get_info(video);
struct video_scale_info info = {.format = voi->format};
enc->fullrange = voi->range == VIDEO_RANGE_FULL;
// also sets the enc->vt_pix_fmt
vt_video_info(enc, &info);
enc->colorspace = voi->colorspace;
enc->width = obs_encoder_get_width(enc->encoder);
enc->height = obs_encoder_get_height(enc->encoder);
enc->fps_num = voi->fps_num;
enc->fps_den = voi->fps_den;
enc->keyint = (uint32_t)obs_data_get_int(settings, "keyint_sec");
enc->rate_control = obs_data_get_string(settings, "rate_control");
enc->bitrate = (uint32_t)obs_data_get_int(settings, "bitrate");
enc->quality = ((float)obs_data_get_int(settings, "quality")) / 100;
enc->profile = obs_data_get_string(settings, "profile");
enc->limit_bitrate = obs_data_get_bool(settings, "limit_bitrate");
enc->rc_max_bitrate = obs_data_get_int(settings, "max_bitrate");
enc->rc_max_bitrate_window =
obs_data_get_double(settings, "max_bitrate_window");
enc->bframes = obs_data_get_bool(settings, "bframes");
}
static bool vt_update(void *data, obs_data_t *settings)
{
struct vt_encoder *enc = data;
uint32_t old_bitrate = enc->bitrate;
bool old_limit_bitrate = enc->limit_bitrate;
update_params(enc, settings);
if (old_bitrate == enc->bitrate &&
old_limit_bitrate == enc->limit_bitrate)
return true;
OSStatus code = session_set_bitrate(enc->session, enc->rate_control,
enc->bitrate, enc->quality,
enc->limit_bitrate,
enc->rc_max_bitrate,
enc->rc_max_bitrate_window);
if (code != noErr)
VT_BLOG(LOG_WARNING, "Failed to set bitrate to session");
dump_encoder_info(enc);
return true;
}
static void *vt_create(obs_data_t *settings, obs_encoder_t *encoder)
{
struct vt_encoder *enc = bzalloc(sizeof(struct vt_encoder));
OSStatus code;
enc->encoder = encoder;
enc->vt_encoder_id = obs_encoder_get_id(encoder);
update_params(enc, settings);
STATUS_CHECK(CMSimpleQueueCreate(NULL, 100, &enc->queue));
if (!create_encoder(enc))
goto fail;
dump_encoder_info(enc);
return enc;
fail:
vt_destroy(enc);
return NULL;
}
static const uint8_t annexb_startcode[4] = {0, 0, 0, 1};
static void packet_put(struct darray *packet, const uint8_t *buf, size_t size)
{
darray_push_back_array(sizeof(uint8_t), packet, buf, size);
}
static void packet_put_startcode(struct darray *packet, int size)
{
assert(size == 3 || size == 4);
packet_put(packet, &annexb_startcode[4 - size], size);
}
static void convert_block_nals_to_annexb(struct vt_encoder *enc,
struct darray *packet,
CMBlockBufferRef block,
int nal_length_bytes)
{
size_t block_size;
uint8_t *block_buf;
CMBlockBufferGetDataPointer(block, 0, NULL, &block_size,
(char **)&block_buf);
size_t bytes_remaining = block_size;
while (bytes_remaining > 0) {
uint32_t nal_size;
if (nal_length_bytes == 1)
nal_size = block_buf[0];
else if (nal_length_bytes == 2)
nal_size = CFSwapInt16BigToHost(
((uint16_t *)block_buf)[0]);
else if (nal_length_bytes == 4)
nal_size = CFSwapInt32BigToHost(
((uint32_t *)block_buf)[0]);
else
return;
bytes_remaining -= nal_length_bytes;
block_buf += nal_length_bytes;
if (bytes_remaining < nal_size) {
VT_BLOG(LOG_ERROR, "invalid nal block");
return;
}
packet_put_startcode(packet, 3);
packet_put(packet, block_buf, nal_size);
bytes_remaining -= nal_size;
block_buf += nal_size;
}
}
static bool handle_keyframe(struct vt_encoder *enc,
CMFormatDescriptionRef format_desc,
size_t param_count, struct darray *packet,
struct darray *extra_data)
{
OSStatus code;
const uint8_t *param;
size_t param_size;
for (size_t i = 0; i < param_count; i++) {
code = CMVideoFormatDescriptionGetH264ParameterSetAtIndex(
format_desc, i, &param, &param_size, NULL, NULL);
if (code != noErr) {
log_osstatus(LOG_ERROR, enc,
"getting NAL parameter "
"at index",
code);
return false;
}
packet_put_startcode(packet, 4);
packet_put(packet, param, param_size);
}
// if we were passed an extra_data array, fill it with
// SPS, PPS, etc.
if (extra_data != NULL)
packet_put(extra_data, packet->array, packet->num);
return true;
}
static bool convert_sample_to_annexb(struct vt_encoder *enc,
struct darray *packet,
struct darray *extra_data,
CMSampleBufferRef buffer, bool keyframe)
{
OSStatus code;
CMFormatDescriptionRef format_desc =
CMSampleBufferGetFormatDescription(buffer);
size_t param_count;
int nal_length_bytes;
code = CMVideoFormatDescriptionGetH264ParameterSetAtIndex(
format_desc, 0, NULL, NULL, &param_count, &nal_length_bytes);
// it is not clear what errors this function can return
// so we check the two most reasonable
if (code == kCMFormatDescriptionBridgeError_InvalidParameter ||
code == kCMFormatDescriptionError_InvalidParameter) {
VT_BLOG(LOG_WARNING, "assuming 2 parameter sets "
"and 4 byte NAL length header");
param_count = 2;
nal_length_bytes = 4;
} else if (code != noErr) {
log_osstatus(LOG_ERROR, enc,
"getting parameter count from sample", code);
return false;
}
if (keyframe &&
!handle_keyframe(enc, format_desc, param_count, packet, extra_data))
return false;
CMBlockBufferRef block = CMSampleBufferGetDataBuffer(buffer);
convert_block_nals_to_annexb(enc, packet, block, nal_length_bytes);
return true;
}
static bool is_sample_keyframe(CMSampleBufferRef buffer)
{
CFArrayRef attachments =
CMSampleBufferGetSampleAttachmentsArray(buffer, false);
if (attachments != NULL) {
CFDictionaryRef attachment;
CFBooleanRef has_dependencies;
attachment =
(CFDictionaryRef)CFArrayGetValueAtIndex(attachments, 0);
has_dependencies = (CFBooleanRef)CFDictionaryGetValue(
attachment, kCMSampleAttachmentKey_DependsOnOthers);
return has_dependencies == kCFBooleanFalse;
}
return false;
}
static bool parse_sample(struct vt_encoder *enc, CMSampleBufferRef buffer,
struct encoder_packet *packet, CMTime off)
{
int type;
CMTime pts = CMSampleBufferGetPresentationTimeStamp(buffer);
CMTime dts = CMSampleBufferGetDecodeTimeStamp(buffer);
if (CMTIME_IS_INVALID(dts))
dts = pts;
// imitate x264's negative dts when bframes might have pts < dts
else if (enc->bframes)
dts = CMTimeSubtract(dts, off);
pts = CMTimeMultiply(pts, enc->fps_num);
dts = CMTimeMultiply(dts, enc->fps_num);
bool keyframe = is_sample_keyframe(buffer);
da_resize(enc->packet_data, 0);
// If we are still looking for extra data
struct darray *extra_data = NULL;
if (enc->extra_data.num == 0)
extra_data = &enc->extra_data.da;
if (!convert_sample_to_annexb(enc, &enc->packet_data.da, extra_data,
buffer, keyframe))
goto fail;
packet->type = OBS_ENCODER_VIDEO;
packet->pts = (int64_t)(CMTimeGetSeconds(pts));
packet->dts = (int64_t)(CMTimeGetSeconds(dts));
packet->data = enc->packet_data.array;
packet->size = enc->packet_data.num;
packet->keyframe = keyframe;
// VideoToolbox produces packets with priority lower than the RTMP code
// expects, which causes it to be unable to recover from frame drops.
// Fix this by manually adjusting the priority.
uint8_t *start = enc->packet_data.array;
uint8_t *end = start + enc->packet_data.num;
start = (uint8_t *)obs_avc_find_startcode(start, end);
while (true) {
while (start < end && !*(start++))
;
if (start == end)
break;
type = start[0] & 0x1F;
if (type == OBS_NAL_SLICE_IDR || type == OBS_NAL_SLICE) {
uint8_t prev_type = (start[0] >> 5) & 0x3;
start[0] &= ~(3 << 5);
if (type == OBS_NAL_SLICE_IDR)
start[0] |= OBS_NAL_PRIORITY_HIGHEST << 5;
else if (type == OBS_NAL_SLICE &&
prev_type != OBS_NAL_PRIORITY_DISPOSABLE)
start[0] |= OBS_NAL_PRIORITY_HIGH << 5;
else
start[0] |= prev_type << 5;
}
start = (uint8_t *)obs_avc_find_startcode(start, end);
}
CFRelease(buffer);
return true;
fail:
CFRelease(buffer);
return false;
}
bool get_cached_pixel_buffer(struct vt_encoder *enc, CVPixelBufferRef *buf)
{
OSStatus code;
CVPixelBufferPoolRef pool =
VTCompressionSessionGetPixelBufferPool(enc->session);
if (!pool)
return kCVReturnError;
CVPixelBufferRef pixbuf;
STATUS_CHECK(CVPixelBufferPoolCreatePixelBuffer(NULL, pool, &pixbuf));
// Why aren't these already set on the pixel buffer?
// I would have expected pixel buffers from the session's
// pool to have the correct color space stuff set
CFStringRef matrix = obs_to_vt_colorspace(enc->colorspace);
CVBufferSetAttachment(pixbuf, kCVImageBufferYCbCrMatrixKey, matrix,
kCVAttachmentMode_ShouldPropagate);
CVBufferSetAttachment(pixbuf, kCVImageBufferColorPrimariesKey,
kCVImageBufferColorPrimaries_ITU_R_709_2,
kCVAttachmentMode_ShouldPropagate);
CVBufferSetAttachment(pixbuf, kCVImageBufferTransferFunctionKey,
kCVImageBufferTransferFunction_ITU_R_709_2,
kCVAttachmentMode_ShouldPropagate);
*buf = pixbuf;
return true;
fail:
return false;
}
static bool vt_encode(void *data, struct encoder_frame *frame,
struct encoder_packet *packet, bool *received_packet)
{
struct vt_encoder *enc = data;
OSStatus code;
CMTime dur = CMTimeMake(enc->fps_den, enc->fps_num);
CMTime off = CMTimeMultiply(dur, 2);
CMTime pts = CMTimeMake(frame->pts, enc->fps_num);
CVPixelBufferRef pixbuf = NULL;
if (!get_cached_pixel_buffer(enc, &pixbuf)) {
VT_BLOG(LOG_ERROR, "Unable to create pixel buffer");
goto fail;
}
STATUS_CHECK(CVPixelBufferLockBaseAddress(pixbuf, 0));
for (int i = 0; i < MAX_AV_PLANES; i++) {
if (frame->data[i] == NULL)
break;
uint8_t *p = (uint8_t *)CVPixelBufferGetBaseAddressOfPlane(
pixbuf, i);
uint8_t *f = frame->data[i];
size_t plane_linesize =
CVPixelBufferGetBytesPerRowOfPlane(pixbuf, i);
size_t plane_height = CVPixelBufferGetHeightOfPlane(pixbuf, i);
for (size_t j = 0; j < plane_height; j++) {
memcpy(p, f, frame->linesize[i]);
p += plane_linesize;
f += frame->linesize[i];
}
}
STATUS_CHECK(CVPixelBufferUnlockBaseAddress(pixbuf, 0));
STATUS_CHECK(VTCompressionSessionEncodeFrame(enc->session, pixbuf, pts,
dur, NULL, pixbuf, NULL));
CMSampleBufferRef buffer =
(CMSampleBufferRef)CMSimpleQueueDequeue(enc->queue);
// No samples waiting in the queue
if (buffer == NULL)
return true;
*received_packet = true;
return parse_sample(enc, buffer, packet, off);
fail:
return false;
}
#undef STATUS_CHECK
#undef CFNUM_INT
static bool vt_extra_data(void *data, uint8_t **extra_data, size_t *size)
{
struct vt_encoder *enc = (struct vt_encoder *)data;
*extra_data = enc->extra_data.array;
*size = enc->extra_data.num;
return true;
}
static const char *vt_getname(void *data)
{
struct vt_encoder_type_data *type_data = data;
if (strcmp("Apple H.264 (HW)", type_data->disp_name) == 0) {
return obs_module_text("VTH264EncHW");
} else if (strcmp("Apple H.264 (SW)", type_data->disp_name) == 0) {
return obs_module_text("VTH264EncSW");
}
return type_data->disp_name;
}
#define TEXT_VT_ENCODER obs_module_text("VTEncoder")
#define TEXT_BITRATE obs_module_text("Bitrate")
#define TEXT_QUALITY obs_module_text("Quality")
#define TEXT_USE_MAX_BITRATE obs_module_text("UseMaxBitrate")
#define TEXT_MAX_BITRATE obs_module_text("MaxBitrate")
#define TEXT_MAX_BITRATE_WINDOW obs_module_text("MaxBitrateWindow")
#define TEXT_KEYINT_SEC obs_module_text("KeyframeIntervalSec")
#define TEXT_PROFILE obs_module_text("Profile")
#define TEXT_NONE obs_module_text("None")
#define TEXT_DEFAULT obs_module_text("DefaultEncoder")
#define TEXT_BFRAMES obs_module_text("UseBFrames")
#define TEXT_RATE_CONTROL obs_module_text("RateControl")
static bool rate_control_limit_bitrate_modified(obs_properties_t *ppts,
obs_property_t *p,
obs_data_t *settings)
{
bool has_bitrate = true;
bool can_limit_bitrate = true;
bool use_limit_bitrate = obs_data_get_bool(settings, "limit_bitrate");
const char *rate_control =
obs_data_get_string(settings, "rate_control");
if (strcmp(rate_control, "CBR") == 0) {
can_limit_bitrate = false;
has_bitrate = true;
} else if (strcmp(rate_control, "CRF") == 0) {
can_limit_bitrate = true;
has_bitrate = false;
} else if (strcmp(rate_control, "ABR") == 0) {
can_limit_bitrate = true;
has_bitrate = true;
}
p = obs_properties_get(ppts, "limit_bitrate");
obs_property_set_visible(p, can_limit_bitrate);
p = obs_properties_get(ppts, "max_bitrate");
obs_property_set_visible(p, can_limit_bitrate && use_limit_bitrate);
p = obs_properties_get(ppts, "max_bitrate_window");
obs_property_set_visible(p, can_limit_bitrate && use_limit_bitrate);
p = obs_properties_get(ppts, "bitrate");
obs_property_set_visible(p, has_bitrate);
p = obs_properties_get(ppts, "quality");
obs_property_set_visible(p, !has_bitrate);
return true;
}
static obs_properties_t *vt_properties(void *unused, void *data)
{
UNUSED_PARAMETER(unused);
struct vt_encoder_type_data *type_data = data;
obs_properties_t *props = obs_properties_create();
obs_property_t *p;
p = obs_properties_add_list(props, "rate_control", TEXT_RATE_CONTROL,
OBS_COMBO_TYPE_LIST,
OBS_COMBO_FORMAT_STRING);
if (__builtin_available(macOS 13.0, *))
if (type_data->hardware_accelerated
#ifndef __aarch64__
&& (os_get_emulation_status() == true)
#endif
)
obs_property_list_add_string(p, "CBR", "CBR");
obs_property_list_add_string(p, "ABR", "ABR");
if (type_data->hardware_accelerated
#ifndef __aarch64__
&& (os_get_emulation_status() == true)
#endif
)
obs_property_list_add_string(p, "CRF", "CRF");
obs_property_set_modified_callback(p,
rate_control_limit_bitrate_modified);
p = obs_properties_add_int(props, "bitrate", TEXT_BITRATE, 50, 10000000,
50);
obs_property_int_set_suffix(p, " Kbps");
obs_properties_add_int_slider(props, "quality", TEXT_QUALITY, 0, 100,
1);
p = obs_properties_add_bool(props, "limit_bitrate",
TEXT_USE_MAX_BITRATE);
obs_property_set_modified_callback(p,
rate_control_limit_bitrate_modified);
p = obs_properties_add_int(props, "max_bitrate", TEXT_MAX_BITRATE, 50,
10000000, 50);
obs_property_int_set_suffix(p, " Kbps");
obs_properties_add_float(props, "max_bitrate_window",
TEXT_MAX_BITRATE_WINDOW, 0.10f, 10.0f, 0.25f);
obs_properties_add_int(props, "keyint_sec", TEXT_KEYINT_SEC, 0, 20, 1);
p = obs_properties_add_list(props, "profile", TEXT_PROFILE,
OBS_COMBO_TYPE_LIST,
OBS_COMBO_FORMAT_STRING);
obs_property_list_add_string(p, TEXT_NONE, "");
obs_property_list_add_string(p, "baseline", "baseline");
obs_property_list_add_string(p, "main", "main");
obs_property_list_add_string(p, "high", "high");
obs_properties_add_bool(props, "bframes", TEXT_BFRAMES);
return props;
}
static void vt_defaults(obs_data_t *settings, void *data)
{
struct vt_encoder_type_data *type_data = data;
obs_data_set_default_string(settings, "rate_control", "ABR");
if (__builtin_available(macOS 13.0, *))
if (type_data->hardware_accelerated
#ifndef __aarch64__
&& (os_get_emulation_status() == true)
#endif
)
obs_data_set_default_string(settings, "rate_control",
"CBR");
obs_data_set_default_int(settings, "bitrate", 2500);
obs_data_set_default_int(settings, "quality", 60);
obs_data_set_default_bool(settings, "limit_bitrate", false);
obs_data_set_default_int(settings, "max_bitrate", 2500);
obs_data_set_default_double(settings, "max_bitrate_window", 1.5f);
obs_data_set_default_int(settings, "keyint_sec", 0);
obs_data_set_default_string(settings, "profile", "");
obs_data_set_default_bool(settings, "bframes", true);
}
static void vt_free_type_data(void *data)
{
struct vt_encoder_type_data *type_data = data;
bfree((char *)type_data->disp_name);
bfree((char *)type_data->id);
bfree(type_data);
}
OBS_DECLARE_MODULE()
OBS_MODULE_USE_DEFAULT_LOCALE("mac-videotoolbox", "en-US")
bool obs_module_load(void)
{
struct obs_encoder_info info = {
.type = OBS_ENCODER_VIDEO,
.codec = "h264",
.get_name = vt_getname,
.create = vt_create,
.destroy = vt_destroy,
.encode = vt_encode,
.update = vt_update,
.get_properties2 = vt_properties,
.get_defaults2 = vt_defaults,
.get_video_info = vt_video_info,
.get_extra_data = vt_extra_data,
.free_type_data = vt_free_type_data,
.caps = OBS_ENCODER_CAP_DYN_BITRATE,
};
CFArrayRef encoder_list;
VTCopyVideoEncoderList(NULL, &encoder_list);
CFIndex size = CFArrayGetCount(encoder_list);
for (CFIndex i = 0; i < size; i++) {
CFDictionaryRef encoder_dict =
CFArrayGetValueAtIndex(encoder_list, i);
#define VT_DICTSTR(key, name) \
CFStringRef name##_ref = CFDictionaryGetValue(encoder_dict, key); \
CFIndex name##_len = CFStringGetLength(name##_ref); \
char *name = bzalloc(name##_len + 1); \
CFStringGetFileSystemRepresentation(name##_ref, name, name##_len);
VT_DICTSTR(kVTVideoEncoderList_CodecName, codec_name);
if (strcmp("H.264", codec_name) != 0) {
bfree(codec_name);
continue;
}
bfree(codec_name);
VT_DICTSTR(kVTVideoEncoderList_EncoderID, id);
VT_DICTSTR(kVTVideoEncoderList_DisplayName, disp_name);
CFBooleanRef hardware_ref = CFDictionaryGetValue(
encoder_dict,
kVTVideoEncoderList_IsHardwareAccelerated);
bool hardware_accelerated = false;
if (hardware_ref)
hardware_accelerated = CFBooleanGetValue(hardware_ref);
info.id = id;
struct vt_encoder_type_data *type_data =
bzalloc(sizeof(struct vt_encoder_type_data));
type_data->disp_name = disp_name;
type_data->id = id;
type_data->hardware_accelerated = hardware_accelerated;
info.type_data = type_data;
obs_register_encoder(&info);
#undef VT_DICTSTR
}
CFRelease(encoder_list);
VT_LOG(LOG_INFO, "Adding VideoToolbox encoders");
return true;
}