介绍
- FFmpeg已经提供对 VideoToolBox 的编解码支持;主要涉及到的文件有videotoolbox.c、videotoolbox.h、videotoolboxenc.c、ffmepg_videotoolbox.c。
- 在编译 FFmpeg 源码时,想要支持VideoToolBox,在 configure 时,需要--enable-videotoolbox 命令。
- 命令行ffmpeg -hwaccels查看支持哪些硬编码器。
- ffmpeg 支持 videotoolbox h264 和 h265 的编码,即 h264_videotoolbox、hevc_videotoolbox。
FFmpeg
- FFmpeg 是一个可以处理音视频的软件,功能非常强大,主要包括,编解码转换,封装格式转换,滤镜特效。
- FFmpeg支持各种网络协议,支持 RTMP ,RTSP,HLS 等高层协议的推拉流,也支持更底层的TCP/UDP 协议推拉流。
- FFmpeg 可以在 Windows,Linux,Mac,iOS,Android等操作系统上运行。
- FFmpeg 是 " Fast Forward mpeg " 的缩写;
- FFMPEG从功能上划分为几个模块,分别为核心工具(libutils)、媒体格式(libavformat)、编解码(libavcodec)、设备(libavdevice)和后处理(libavfilter, libswscale, libpostproc),分别负责提供公用的功能函数、实现多媒体文件的读包和写包、完成音视频的编解码、管理音视频设备的操作以及进行音视频后处理。
VideoToolBox
- VideoToolBox是一个优化的视频编解码器框架,由苹果公司开发并针对iOS和macOS平台进行优化,作为现代移动应用程序中不可或缺的组成部分之一,它被用于H.264解码和编码,HEVC解码和编码,以及MPEG-2解码和编码,同时还支持对Core Audio和Core Video的访问。
- VideoToolBox的优点是高效性、易用性;在iOS和macOS设备上,它的编解码速度比其他框架要快得多;此外,它为开发人员提供了各种功能,包括修改视频帧速率,更改编码格式等等。
FFmpeg 硬编码 VideoToolBox 流程
- 可以看出,FFmpeg 与 VideoToolBox之间的交互,主要通过三个函数指针 init、encode2、close 来完成;
- 从整体流程分析,VideoToolBox 的工作流程是:
创建 一个压缩会话 ;
添加会话属性 ;
编码视频帧、接受视频编码回调 ;
强制完成一些或者全部未处理的视频帧;
释放压缩会话、释放内存资源。 - init模块核心函数是 vtenc_configure_encode();
- encode2模块核心函数是vtenc_send_frame();
- close 模块的核心函数是VTCompressionSessionCompleteFrames();
h264_videotoolbox
- VideoToolBox 的h264硬编码通过三个结构体h264_options 、h264_videotoolbox_class 、ff_h264_videotoolbox_encoder来完成与 FFmpeg 的交互。
- h264_options主要涉及的是内部参数,例如 profile、level、熵编码选择等。
- h264_videotoolbox_class来定义 h264的私有类,指定编码类型和编码参数。
- ff_h264_videotoolbox_encoder是具体的对外与 FFmpeg 的交互结构体,完成h264硬编码。
c
static const AVOption h264_options[] = {
{ "profile", "Profile", OFFSET(profile), AV_OPT_TYPE_INT, { .i64 = H264_PROF_AUTO }, H264_PROF_AUTO, H264_PROF_COUNT, VE, "profile" },
{ "baseline", "Baseline Profile", 0, AV_OPT_TYPE_CONST, { .i64 = H264_PROF_BASELINE }, INT_MIN, INT_MAX, VE, "profile" },
{ "main", "Main Profile", 0, AV_OPT_TYPE_CONST, { .i64 = H264_PROF_MAIN }, INT_MIN, INT_MAX, VE, "profile" },
{ "high", "High Profile", 0, AV_OPT_TYPE_CONST, { .i64 = H264_PROF_HIGH }, INT_MIN, INT_MAX, VE, "profile" },
{ "extended", "Extend Profile", 0, AV_OPT_TYPE_CONST, { .i64 = H264_PROF_EXTENDED }, INT_MIN, INT_MAX, VE, "profile" },
{ "level", "Level", OFFSET(level), AV_OPT_TYPE_INT, { .i64 = 0 }, 0, 52, VE, "level" },
{ "1.3", "Level 1.3, only available with Baseline Profile", 0, AV_OPT_TYPE_CONST, { .i64 = 13 }, INT_MIN, INT_MAX, VE, "level" },
{ "3.0", "Level 3.0", 0, AV_OPT_TYPE_CONST, { .i64 = 30 }, INT_MIN, INT_MAX, VE, "level" },
{ "3.1", "Level 3.1", 0, AV_OPT_TYPE_CONST, { .i64 = 31 }, INT_MIN, INT_MAX, VE, "level" },
{ "3.2", "Level 3.2", 0, AV_OPT_TYPE_CONST, { .i64 = 32 }, INT_MIN, INT_MAX, VE, "level" },
{ "4.0", "Level 4.0", 0, AV_OPT_TYPE_CONST, { .i64 = 40 }, INT_MIN, INT_MAX, VE, "level" },
{ "4.1", "Level 4.1", 0, AV_OPT_TYPE_CONST, { .i64 = 41 }, INT_MIN, INT_MAX, VE, "level" },
{ "4.2", "Level 4.2", 0, AV_OPT_TYPE_CONST, { .i64 = 42 }, INT_MIN, INT_MAX, VE, "level" },
{ "5.0", "Level 5.0", 0, AV_OPT_TYPE_CONST, { .i64 = 50 }, INT_MIN, INT_MAX, VE, "level" },
{ "5.1", "Level 5.1", 0, AV_OPT_TYPE_CONST, { .i64 = 51 }, INT_MIN, INT_MAX, VE, "level" },
{ "5.2", "Level 5.2", 0, AV_OPT_TYPE_CONST, { .i64 = 52 }, INT_MIN, INT_MAX, VE, "level" },
{ "coder", "Entropy coding", OFFSET(entropy), AV_OPT_TYPE_INT, { .i64 = VT_ENTROPY_NOT_SET }, VT_ENTROPY_NOT_SET, VT_CABAC, VE, "coder" },
{ "cavlc", "CAVLC entropy coding", 0, AV_OPT_TYPE_CONST, { .i64 = VT_CAVLC }, INT_MIN, INT_MAX, VE, "coder" },
{ "vlc", "CAVLC entropy coding", 0, AV_OPT_TYPE_CONST, { .i64 = VT_CAVLC }, INT_MIN, INT_MAX, VE, "coder" },
{ "cabac", "CABAC entropy coding", 0, AV_OPT_TYPE_CONST, { .i64 = VT_CABAC }, INT_MIN, INT_MAX, VE, "coder" },
{ "ac", "CABAC entropy coding", 0, AV_OPT_TYPE_CONST, { .i64 = VT_CABAC }, INT_MIN, INT_MAX, VE, "coder" },
{ "a53cc", "Use A53 Closed Captions (if available)", OFFSET(a53_cc), AV_OPT_TYPE_BOOL, {.i64 = 1}, 0, 1, VE },
COMMON_OPTIONS
{ NULL },
};
static const AVClass h264_videotoolbox_class = {
.class_name = "h264_videotoolbox",
.item_name = av_default_item_name,
.option = h264_options,
.version = LIBAVUTIL_VERSION_INT,
};
AVCodec ff_h264_videotoolbox_encoder = {
.name = "h264_videotoolbox",
.long_name = NULL_IF_CONFIG_SMALL("VideoToolbox H.264 Encoder"),
.type = AVMEDIA_TYPE_VIDEO,
.id = AV_CODEC_ID_H264,
.priv_data_size = sizeof(VTEncContext),
.pix_fmts = avc_pix_fmts,
.init = vtenc_init,
.encode2 = vtenc_frame,
.close = vtenc_close,
.capabilities = AV_CODEC_CAP_DELAY,
.priv_class = &h264_videotoolbox_class,
.caps_internal = FF_CODEC_CAP_INIT_THREADSAFE |
FF_CODEC_CAP_INIT_CLEANUP,
};
hevc_videotoolbox
- VideoToolBox 的HEVC硬编码通过三个结构体hevc_options 、hevc_videotoolbox_class 、ff_hevc_videotoolbox_encoder来完成与 FFmpeg 的交互。
- hevc_options主要涉及的是内部参数,例如 profile的选择。
- hevc_videotoolbox_class来定义 HEVC的私有类,指定编码类型和编码参数。
- ff_hevc_videotoolbox_encoder是具体的对外与 FFmpeg 的交互结构体,完成HEVC硬编码。
c
static const AVOption hevc_options[] = {
{ "profile", "Profile", OFFSET(profile), AV_OPT_TYPE_INT, { .i64 = HEVC_PROF_AUTO }, HEVC_PROF_AUTO, HEVC_PROF_COUNT, VE, "profile" },
{ "main", "Main Profile", 0, AV_OPT_TYPE_CONST, { .i64 = HEVC_PROF_MAIN }, INT_MIN, INT_MAX, VE, "profile" },
{ "main10", "Main10 Profile", 0, AV_OPT_TYPE_CONST, { .i64 = HEVC_PROF_MAIN10 }, INT_MIN, INT_MAX, VE, "profile" },
COMMON_OPTIONS
{ NULL },
};
static const AVClass hevc_videotoolbox_class = {
.class_name = "hevc_videotoolbox",
.item_name = av_default_item_name,
.option = hevc_options,
.version = LIBAVUTIL_VERSION_INT,
};
AVCodec ff_hevc_videotoolbox_encoder = {
.name = "hevc_videotoolbox",
.long_name = NULL_IF_CONFIG_SMALL("VideoToolbox H.265 Encoder"),
.type = AVMEDIA_TYPE_VIDEO,
.id = AV_CODEC_ID_HEVC,
.priv_data_size = sizeof(VTEncContext),
.pix_fmts = hevc_pix_fmts,
.init = vtenc_init,
.encode2 = vtenc_frame,
.close = vtenc_close,
.capabilities = AV_CODEC_CAP_DELAY | AV_CODEC_CAP_HARDWARE,
.priv_class = &hevc_videotoolbox_class,
.caps_internal = FF_CODEC_CAP_INIT_THREADSAFE |
FF_CODEC_CAP_INIT_CLEANUP,
.wrapper_name = "videotoolbox",
};
核心模块介绍
.init
- .init模块完成初始化工作,对应的函数是vtenc_init();函数内部主要完成了线程初始化、配置编码器、检索属性以及 B 帧的相关处理。
c
static av_cold int vtenc_init(AVCodecContext *avctx)
{
VTEncContext *vtctx = avctx->priv_data;
CFBooleanRef has_b_frames_cfbool;
int status;
pthread_once(&once_ctrl, loadVTEncSymbols);
pthread_mutex_init(&vtctx->lock, NULL);
pthread_cond_init(&vtctx->cv_sample_sent, NULL);
vtctx->session = NULL;
status = vtenc_configure_encoder(avctx);
if (status) return status;
status = VTSessionCopyProperty(vtctx->session,
kVTCompressionPropertyKey_AllowFrameReordering,
kCFAllocatorDefault,
&has_b_frames_cfbool);
if (!status && has_b_frames_cfbool) {
//Some devices don't output B-frames for main profile, even if requested.
vtctx->has_b_frames = CFBooleanGetValue(has_b_frames_cfbool);
CFRelease(has_b_frames_cfbool);
}
avctx->has_b_frames = vtctx->has_b_frames;
return 0;
}
- vtenc_configure_encoder()函数是 init 模块的核心函数,主要完成编码器的配置工作;根据编码器类型(h264/HEVC)来配置 profile、level、熵编码等信息;此外还会选择裁剪信息、传递函数、YCbCr 矩阵、颜色原色以及额外信息;最后调用vtenc_create_encoder()完成编码器的创建;
c
static int vtenc_configure_encoder(AVCodecContext *avctx)
{
CFMutableDictionaryRef enc_info;
CFMutableDictionaryRef pixel_buffer_info;
CMVideoCodecType codec_type;
VTEncContext *vtctx = avctx->priv_data;
CFStringRef profile_level;
CFNumberRef gamma_level = NULL;
int status;
codec_type = get_cm_codec_type(avctx->codec_id);
if (!codec_type) {
av_log(avctx, AV_LOG_ERROR, "Error: no mapping for AVCodecID %d\n", avctx->codec_id);
return AVERROR(EINVAL);
}
vtctx->codec_id = avctx->codec_id;
if (vtctx->codec_id == AV_CODEC_ID_H264) {
vtctx->get_param_set_func = CMVideoFormatDescriptionGetH264ParameterSetAtIndex;
vtctx->has_b_frames = avctx->max_b_frames > 0;
if(vtctx->has_b_frames && vtctx->profile == H264_PROF_BASELINE){
av_log(avctx, AV_LOG_WARNING, "Cannot use B-frames with baseline profile. Output will not contain B-frames.\n");
vtctx->has_b_frames = false;
}
if (vtctx->entropy == VT_CABAC && vtctx->profile == H264_PROF_BASELINE) {
av_log(avctx, AV_LOG_WARNING, "CABAC entropy requires 'main' or 'high' profile, but baseline was requested. Encode will not use CABAC entropy.\n");
vtctx->entropy = VT_ENTROPY_NOT_SET;
}
if (!get_vt_h264_profile_level(avctx, &profile_level)) return AVERROR(EINVAL);
} else {
vtctx->get_param_set_func = compat_keys.CMVideoFormatDescriptionGetHEVCParameterSetAtIndex;
if (!vtctx->get_param_set_func) return AVERROR(EINVAL);
if (!get_vt_hevc_profile_level(avctx, &profile_level)) return AVERROR(EINVAL);
}
enc_info = CFDictionaryCreateMutable(
kCFAllocatorDefault,
20,
&kCFCopyStringDictionaryKeyCallBacks,
&kCFTypeDictionaryValueCallBacks
);
if (!enc_info) return AVERROR(ENOMEM);
#if !TARGET_OS_IPHONE
if(vtctx->require_sw) {
CFDictionarySetValue(enc_info,
compat_keys.kVTVideoEncoderSpecification_EnableHardwareAcceleratedVideoEncoder,
kCFBooleanFalse);
} else if (!vtctx->allow_sw) {
CFDictionarySetValue(enc_info,
compat_keys.kVTVideoEncoderSpecification_RequireHardwareAcceleratedVideoEncoder,
kCFBooleanTrue);
} else {
CFDictionarySetValue(enc_info,
compat_keys.kVTVideoEncoderSpecification_EnableHardwareAcceleratedVideoEncoder,
kCFBooleanTrue);
}
#endif
if (avctx->pix_fmt != AV_PIX_FMT_VIDEOTOOLBOX) {
status = create_cv_pixel_buffer_info(avctx, &pixel_buffer_info);
if (status)
goto init_cleanup;
} else {
pixel_buffer_info = NULL;
}
vtctx->dts_delta = vtctx->has_b_frames ? -1 : 0;
get_cv_transfer_function(avctx, &vtctx->transfer_function, &gamma_level);
get_cv_ycbcr_matrix(avctx, &vtctx->ycbcr_matrix);
get_cv_color_primaries(avctx, &vtctx->color_primaries);
if (avctx->flags & AV_CODEC_FLAG_GLOBAL_HEADER) {
status = vtenc_populate_extradata(avctx,
codec_type,
profile_level,
gamma_level,
enc_info,
pixel_buffer_info);
if (status)
goto init_cleanup;
}
status = vtenc_create_encoder(avctx,
codec_type,
profile_level,
gamma_level,
enc_info,
pixel_buffer_info,
&vtctx->session);
init_cleanup:
if (gamma_level)
CFRelease(gamma_level);
if (pixel_buffer_info)
CFRelease(pixel_buffer_info);
CFRelease(enc_info);
return status;
}
- vtenc_create_encoder()完成编码器创建工作;调用VTCompressionSessionCreate()创建压缩帧实例,接着会创建码率/码控等各类对象,并配置相应属性;最后,(可选)调用VTCompressionSessionPrepareToEncodeFrames()完成编码前的合理资源分配。
.encode2
- .encode2模块完成具体的编码工作,对应的函数是 vtenc_frame();判断 AVFrame里是否有帧数据,有数据就调用vtenc_send_frame()完成具体的编码,没有就 flush 下;然后调用 vtenc_q_pop()完成线程相关操作;最后利用vtenc_cm_to_avpacket()得到数据包信息,如 SEI、pts、dts 等。
c
static av_cold int vtenc_frame(
AVCodecContext *avctx,
AVPacket *pkt,
const AVFrame *frame,
int *got_packet)
{
VTEncContext *vtctx = avctx->priv_data;
bool get_frame;
int status;
CMSampleBufferRef buf = NULL;
ExtraSEI *sei = NULL;
if (frame) {
status = vtenc_send_frame(avctx, vtctx, frame);
if (status) {
status = AVERROR_EXTERNAL;
goto end_nopkt;
}
if (vtctx->frame_ct_in == 0) {
vtctx->first_pts = frame->pts;
} else if(vtctx->frame_ct_in == 1 && vtctx->has_b_frames) {
vtctx->dts_delta = frame->pts - vtctx->first_pts;
}
vtctx->frame_ct_in++;
} else if(!vtctx->flushing) {
vtctx->flushing = true;
status = VTCompressionSessionCompleteFrames(vtctx->session,
kCMTimeIndefinite);
if (status) {
av_log(avctx, AV_LOG_ERROR, "Error flushing frames: %d\n", status);
status = AVERROR_EXTERNAL;
goto end_nopkt;
}
}
*got_packet = 0;
get_frame = vtctx->dts_delta >= 0 || !frame;
if (!get_frame) {
status = 0;
goto end_nopkt;
}
status = vtenc_q_pop(vtctx, !frame, &buf, &sei);
if (status) goto end_nopkt;
if (!buf) goto end_nopkt;
status = vtenc_cm_to_avpacket(avctx, buf, pkt, sei);
if (sei) {
if (sei->data) av_free(sei->data);
av_free(sei);
}
CFRelease(buf);
if (status) goto end_nopkt;
*got_packet = 1;
return 0;
end_nopkt:
av_packet_unref(pkt);
return status;
}
- vtenc_send_frame()完成编码核心工作;内部主要调用 VideoToolBox 的核心 API函数VTCompressionSessionEncodeFrame()完成具体的编码工作。
c
static int vtenc_send_frame(AVCodecContext *avctx,
VTEncContext *vtctx,
const AVFrame *frame)
{
CMTime time;
CFDictionaryRef frame_dict;
CVPixelBufferRef cv_img = NULL;
AVFrameSideData *side_data = NULL;
ExtraSEI *sei = NULL;
int status = create_cv_pixel_buffer(avctx, frame, &cv_img);
if (status) return status;
status = create_encoder_dict_h264(frame, &frame_dict);
if (status) {
CFRelease(cv_img);
return status;
}
side_data = av_frame_get_side_data(frame, AV_FRAME_DATA_A53_CC);
if (vtctx->a53_cc && side_data && side_data->size) {
sei = av_mallocz(sizeof(*sei));
if (!sei) {
av_log(avctx, AV_LOG_ERROR, "Not enough memory for closed captions, skipping\n");
} else {
int ret = ff_alloc_a53_sei(frame, 0, &sei->data, &sei->size);
if (ret < 0) {
av_log(avctx, AV_LOG_ERROR, "Not enough memory for closed captions, skipping\n");
av_free(sei);
sei = NULL;
}
}
}
time = CMTimeMake(frame->pts * avctx->time_base.num, avctx->time_base.den);
status = VTCompressionSessionEncodeFrame(
vtctx->session,
cv_img,
time,
kCMTimeInvalid,
frame_dict,
sei,
NULL
);
if (frame_dict) CFRelease(frame_dict);
CFRelease(cv_img);
if (status) {
av_log(avctx, AV_LOG_ERROR, "Error: cannot encode frame: %d\n", status);
return AVERROR_EXTERNAL;
}
return 0;
}
.close
- .close 模块完成关闭回收工作,对应的函数是 vtenc_close();内部主要进行线程的销毁、强制完成一些或全部未处理的视频帧、清除帧队列、释放资源的工作。
c
static av_cold int vtenc_close(AVCodecContext *avctx)
{
VTEncContext *vtctx = avctx->priv_data;
pthread_cond_destroy(&vtctx->cv_sample_sent);
pthread_mutex_destroy(&vtctx->lock);
if(!vtctx->session) return 0;
VTCompressionSessionCompleteFrames(vtctx->session,
kCMTimeIndefinite);
clear_frame_queue(vtctx);
CFRelease(vtctx->session);
vtctx->session = NULL;
if (vtctx->color_primaries) {
CFRelease(vtctx->color_primaries);
vtctx->color_primaries = NULL;
}
if (vtctx->transfer_function) {
CFRelease(vtctx->transfer_function);
vtctx->transfer_function = NULL;
}
if (vtctx->ycbcr_matrix) {
CFRelease(vtctx->ycbcr_matrix);
vtctx->ycbcr_matrix = NULL;
}
return 0;
}