Webrtc新增FFmpeg视频编解码模块
1 整体描述2 编码器初始化实现3 编码器编码实现4 解码器初始化实现5 解码器解码实现
1 整体描述
目前webrtc内置的视频编解码器包括:VP8、VP9、AV1和H264。一般情况下载pc端基本可以满足大部分的需求,但是有时候为了进行编解码器的扩展包括支持H265或者是支持硬件编解码以提升效率时需要新增编解码模块。
2 新增外部编码器
编码器实现的要点包括两个部分:
一是需要实现以VideoEncoder为基类的编码器对象,核心API实现如下:
(1)初始化编码器,将编码参数传入进行初始化。
virtual int InitEncode(const VideoCodec* codec_settings,
const VideoEncoder::Settings& settings);
(2)回调函数的注册,用于编码后数据的回传。
virtual int32_t RegisterEncodeCompleteCallback(
EncodedImageCallback* callback) = 0;
(3)编码函数,将视频帧数据传入到解码器中,这个函数是实际编码接口;外部数据输入包括VideoFrame数据帧一般是kI420数据和帧类型(I帧、P帧…)
virtual int32_t Encode(const VideoFrame& frame,
const std::vector<VideoFrameType>* frame_types) = 0;
(4) QOS相关控制,设置码率和帧率等操作。
void SetRates(const RateControlParameters& parameters) override;
二是需要实现EncodedImageCallback回调基类来处理,编码后的数据回传到视频引擎。核心函数就是将编码后的数据转成EncodedImage结构回传到视频引擎。
2.1 编码器初始化
编码器的初始化主要是对编码器的基础参数包括分辨率,码率和荷载等基本数据,我们在完成编码器的初始化基本需要完成编码对象的创建和编码通道的创建等操作。这部分工作主要是在InitEncode完成,需要注意的是初始化函数可能被调用多次,因此这里需要保护防止多次创建造成,内存泄漏和系统资源冲突等问题。
以FFmpeg初始化编码器为例,基本需要包括四部分一是进行编码器的选择,根据codec类型, 寻找并创建编码器的上下文;第二是编码参数的初始化,根据上层传入的用户配置参数及自身对编码器的理解, 对所使用编码器参数进行的配置,可以进行参数的优化提高编码质量和用户体验;第三是编码帧的初始化,最后是开启编码器。
//编码器
AVCodec *avcodec_find_encoder(enum AVCodecID id);
//上下文获取
AVCodecContext *avcodec_alloc_context3(const AVCodec *codec);
//初始化视频帧
AVFrame *avcodec_alloc_frame(void);
//开启编码器
int avcodec_open(AVCodecContext *avctx, AVCodec *codec);
2.2 编码实现
编码阶段需要理解两方面数据,一方面是输入数据VideoFrame为原始的图像帧,一般情况为I420,我们需要进行内部转换转成FFmpeg编码的输入参数。
以FFmpeg为例,解码过程分为三步:一是将输入数据转换为I420数据,然后利用类内部接口获取YUV数据并组装成编码器需要的数据;
二是编码数据,一般是调用FFmpeg编码函数编码,内部会进一步根据不同编码类型进行编码;
三是编码结束后,需要根据重新将编码后的数据进行组装得到一个EncodedImage结构体,并且调用回调函数将结果返回给视频引擎处理。
//将原始视频数据,转成I420格式
input_frame.video_frame_buffer()->ToI420()
//获取YUV数据
frame_buffer->DataY();
//图片填充
int avpicture_fill(AVPicture *picture, const uint8_t *ptr,
enum AVPixelFormat pix_fmt, int width, int height);
//视频编码
int avcodec_encode_video(AVCodecContext *avctx, uint8_t *buf, int buf_size,
const AVFrame *pict);
//回调,数据返回
Encoded(EncodedImage& encodedImage,
const CodecSpecificInfo* codecSpecificInfo = NULL,
const RTPFragmentationHeader* fragmentation = NULL) = 0;
3 新增外部解码器
解码器实现与编码器类似,实现的要点包括两个部分:
一是需要实现以VideoDecoder为基类的解码器对象,核心API实现如下:
(1)初始化解码器,将解码参数传入进行初始化。
virtual bool Configure(const Settings& settings) = 0;
(2)回调函数的注册,用于解码后数据的回传。
virtual int32_t Decode(const EncodedImage& input_image,
bool missing_frames,
int64_t render_time_ms) = 0;
(3)解码函数,将编码视频帧数据传入到解码器中,这个函数是实际解码接口;外部数据输入包括EncodedImage数据帧,渲染时间和是否丢帧等。
virtual int32_t Decode(const EncodedImage& input_image,
bool missing_frames,
int64_t render_time_ms) = 0;
(4) 与视频编码不一样的配置函数,包括视频的解码缓冲区大小和渲染分辨率的设置。
//设置解码缓冲区大小
void set_buffer_pool_size(absl::optional<int> value);
//设置渲染分辨率
void set_max_render_resolution(RenderResolution value);
二是需要实现DecodedImageCallback回调基类来处理,解码后的数据回传到视频引擎。核心函数就是将编码后的数据转成原始视频帧VideoFrame结构回传到视频引擎。
3.1 解码器初始化
解码器的初始化主要是对解码器的基础参数包括分辨率,码率和荷载等基本数据,我们在完成解码器的初始化基本需要完成解码对象的创建和解码通道的创建等操作。这部分工作主要是在Configure完成,需要注意的是初始化函数可能被调用多次,因此这里需要保护防止多次创建造成,内存泄漏和系统资源冲突等问题。
以FFmpeg初始化解码器为例,基本需要包括四部分第一是解码参数的初始化,根据上层传入的用户配置参数及自身对解码器的理解, 对所使用解码器参数进行的配置,可以进行参数的优化提高解码质量和用户体验;二是进行解码器的选择,根据codec类型, 寻找并创建解码器的上下文;第三是开启解码器,传入上下文配置和解码器;第四部分是分配一个解码帧,用于后续解码使用。
//上下文获取
AVCodecContext *avcodec_alloc_context3(const AVCodec *codec);
//解码器
AVCodec *avcodec_find_decoder(enum AVCodecID id);
//开启解码器
int avcodec_open2(AVCodecContext *avctx, const AVCodec *codec, AVDictionary **options);
//初始化视频帧
AVFrame *avcodec_alloc_frame(void);
3.2 解码实现
解码阶段需要理解两方面数据,一方面是输入数据EncodedImage为编码后的数据,需要转换会解码器需要的帧结构,然后进行解码操作;第二是解码后数据解释为VideoFrame帧后回调给视频引擎。
以FFmpeg为例,解码过程分为四步:一是将EncodedImage结构视频帧转换为AVPacket结构;第二是将配置上下文和AVPacket传入解码器解码;第三是获取解码后视频帧存入AVFrame中;第四是需要根据重新将解码后的数据进行组装得到一个VideoFrame结构体,并且调用回调函数将结果返回给视频引擎处理。
//分配一个packet空间
AVPacket *av_packet_alloc(void);
//将编码数据加入到解码队列
int avcodec_send_packet(AVCodecContext *avctx, const AVPacket *avpkt);
//获取解码后数据帧
int avcodec_receive_frame(AVCodecContext *avctx, AVFrame *frame);
//释放packet
void av_packet_free(AVPacket **pkt);
4 总结
webrtc编解码模块设计有较强的对称性,其实在发送和接收,音频和视频,编码和解码等等都在设计上比较接近,因此需要在学习的时候相互借鉴和理解。整个编解码模块的添加相对难度较低,主要是理解输入和输出的数据结构,可以将webrtc的数据结构转换为编解码器内部能够理解的数据,达到将数据编解码的目的。基本的设计理念就是定义好标准的输入和输出,屏蔽内部的处理细节,达到模块之间的相互独立。
更多推荐
所有评论(0)