介绍
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 -h encoder=hevc_videotoolbox可以查看对应设备的硬编码有哪些编码参数(要是 h264,即 ffmpeg -h encoder=h264_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硬编码。
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硬编码。
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 帧的相关处理。
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) {
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()完成编码器的创建;
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 等。
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()完成具体的编码工作。
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();内部主要进行线程的销毁、强制完成一些或全部未处理的视频帧、清除帧队列、释放资源的工作。
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 ;
}
参考
https://developer.apple.com/documentation/videotoolbox http://ffmpeg.org/