前言
本文是针对前文对WebRTC-H265的软解码入口文件补充解释,建议了解前面文章的内容再阅读本篇内容
我们看到的ffmpeg静态库调用的编译入口文件,我们考虑篇幅的问题,将其放置在这里去进行去了解一下这个入口文件的构成
首先我们将整体代码放置在此处先
#include <libavcodec/avcodec.h>
#include <libavutil/imgutils.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
// 定义一个函数指针类型,用于回调 YUV 数据
typedef void(*OnBuffer)(unsigned char* data_y, int size, int pts);
AVCodec *codec = NULL;
AVCodecContext *dec_ctx = NULL;
AVCodecParserContext *parser_ctx = NULL;
AVPacket *pkt = NULL;
AVFrame *frame = NULL;
OnBuffer decoder_callback = NULL;
void output_yuv_buffer(AVFrame *frame) {
int width, height, frame_size;
uint8_t *yuv_buffer = NULL;
width = frame->width;
height = frame->height;
// 根据格式,获取buffer大小
frame_size = av_image_get_buffer_size(frame->format, width, height, 1);
// 分配内存
yuv_buffer = (uint8_t *)av_mallocz(frame_size * sizeof(uint8_t));
// 将frame数据按照yuv的格式依次填充到bufferr中。下面的步骤可以用工具函数av_image_copy_to_buffer代替。
int i, j, k;
// Y
for(i = 0; i < height; i++) {
memcpy(yuv_buffer + width*i,
frame->data[0]+frame->linesize[0]*i,
width);
}
for(j = 0; j < height / 2; j++) {
memcpy(yuv_buffer + width * i + width / 2 * j,
frame->data[1] + frame->linesize[1] * j,
width / 2);
}
for(k =0; k < height / 2; k++) {
memcpy(yuv_buffer + width * i + width / 2 * j + width / 2 * k,
frame->data[2] + frame->linesize[2] * k,
width / 2);
}
// 通过之前传入的回调函数发给js
decoder_callback(yuv_buffer, frame_size, frame->pts);
av_free(yuv_buffer);
}
int decode_packet(AVCodecContext* ctx, AVFrame* frame, AVPacket* pkt)
{
int ret = 0;
// 发送packet到解码器
ret = avcodec_send_packet(dec_ctx, pkt);
if (ret < 0) {
return ret;
}
// 从解码器接收frame
while (ret >= 0) {
ret = avcodec_receive_frame(dec_ctx, frame);
if (ret == AVERROR(EAGAIN) || ret == AVERROR_EOF) {
break;
} else if (ret < 0) {
// handle error
break;
}
// 输出yuv buffer数据
output_yuv_buffer(frame);
}
return ret;
}
void init_decoder(OnBuffer callback) {
// 找到hevc解码器
codec = avcodec_find_decoder(AV_CODEC_ID_HEVC);
// 初始化对应的解析器
parser_ctx = av_parser_init(codec->id);
// 初始化上下文
dec_ctx = avcodec_alloc_context3(codec);
// 打开decoder
avcodec_open2(dec_ctx, codec, NULL);
// 分配一个frame内存,并指明yuv 420p格式
frame = av_frame_alloc();
frame->format = AV_PIX_FMT_YUV420P;
// 分配一个pkt内存
pkt = av_packet_alloc();
// 暂存回调
decoder_callback = callback;
}
void decode_buffer(uint8_t* buffer, size_t data_size) { // 入参是js传入的uint8array数据以及数据长度
while (data_size > 0) {
// 从buffer中解析出packet
int size = av_parser_parse2(parser_ctx, dec_ctx, &pkt->data, &pkt->size,
buffer, data_size, AV_NOPTS_VALUE, AV_NOPTS_VALUE, 0);
if (size < 0) {
break;
}
buffer += size;
data_size -= size;
if (pkt->size) {
// 解码packet
decode_packet(dec_ctx, frame, pkt);
}
}
}
// 主函数
int main(int argc, char** argv) {
return 0;
}
按照结构去划分,这里面的代码一共分为以下模块
-
头部文件的引用
-
全局变量的声明
-
核心的解码函数
-
入口函数
我们分别来进行解析一下
头部文件的引用
#include <libavcodec/avcodec.h>
#include <libavutil/imgutils.h>
#include <stdio.h> // 输入输出功能
#include <stdlib.h> // 提供通用的实用函数,例如动态内存分配(malloc、free)、程序退出(exit)、随机数生成(rand)等。
#include <string.h> // 提供字符串处理函数,例如字符串复制(strcpy)、比较(strcmp)、长度计算(strlen)等。
这里面一共引入五个内容,我们关注一下前面两个头文件,他是ffmpeg提供的两个我们用于解码的文件
libavcodec/avcodec.h
这个文件是ffmpeg的核心头文件之一,里面包含了我们由前文编译得到的编解码的API,用于处理音视频的编解码操作。(ffmpeg里面经常可以看到很多av开头的变量名和文件名,这里的av代表的audio和video的合并缩写)
libavutil/imgutils.h
这个文件的作用是ffmpeg里面的一些公共方法的使用头文件
全局变量的声明
// 定义一个函数指针类型,用于回调 YUV 数据
typedef void(*OnBuffer)(unsigned char* data_y, int size, int pts);
AVCodec *codec = NULL; // 编解码器的xin'xi
AVCodecContext *dec_ctx = NULL; // 编解码器的上下文
AVCodecParserContext *parser_ctx = NULL; // 解析器的上下文
AVPacket *pkt = NULL; // ffmpeg的包概念,代表一个比特流数据包
AVFrame *frame = NULL; // 解码后得到的原始数据帧
OnBuffer decoder_callback = NULL;
要了解一下上面的全局变量的作用,我们得了解一下ffmpeg的解码流程是如何的进行的
我们从DataChannel消息,每一个数据包的都是有NALU头部,NALU的负载(视频本身的内容)组成的原始码流,通过上文我们知道他是一个二进制的数据流。我们需要使用AVCodecParserContext去解析成AVpacket包供AVCodecContext去解码成原始帧AVFrame。
核心解码函数
各个函数调用的逻辑
init_decoder
void init_decoder(OnBuffer callback) {
// 找到hevc解码器, hevc就是h265英文代指
codec = avcodec_find_decoder(AV_CODEC_ID_HEVC);
// 初始化对应的解析器
parser_ctx = av_parser_init(codec->id);
// 初始化上下文
dec_ctx = avcodec_alloc_context3(codec);
// 打开decoder
avcodec_open2(dec_ctx, codec, NULL);
// 分配一个frame内存,并指明yuv 420p格式
frame = av_frame_alloc();
frame->format = AV_PIX_FMT_YUV420P;
// 分配一个pkt内存
pkt = av_packet_alloc();
// 暂存回调
decoder_callback = callback;
}
这里的av开头的方法出自于FFmpeg库。详细的解释看建议看官方文档和博客了解。
这个方法在worker线程的软编码器代码初始化就会被调用。通过外部声明一个函数后作为参数callback传入。
这里声明callback变量作为入参传入内部代码函数指针,这里的addFunction方法是由Emscripten提供的,详细可以看这里的文档。
通过addFunction这个方法,我们我们声明的handleYUVData作为一个函数指针传入到wasm作为一个函数指针调用。viii 这里代表的意思,第一个参数代表void,代表函数无返回,i代表的32位的int类型。
这里的调用还可以看到前面多了一个下横线,这个是Emscripten在导出WASM的时候的默认行为,用于区分导出的函数和其他符号。
decode_buffer
void decode_buffer(uint8_t* buffer, size_t data_size) { // 入参是js传入的uint8array数据以及数据长度
while (data_size > 0) {
// 从buffer中解析出packet
int size = av_parser_parse2(parser_ctx, dec_ctx, &pkt->data, &pkt->size,
buffer, data_size, AV_NOPTS_VALUE, AV_NOPTS_VALUE, 0);
if (size < 0) {
break;
}
buffer += size;
data_size -= size;
if (pkt->size) {
// 解码packet
decode_packet(dec_ctx, frame, pkt);
}
}
}
这里调用了av_parser_parse2方法,我们看一下官方文档是怎么定义的
一共接收9个参数,我们分别解析一下
| s | AVCodecParserContext 的指针,前面我们已经初始化了 |
|---|---|
| avctx | AVCodecContext的指针,前面我们已经初始化了 |
| poutbuf | 这里要传入一个无符号8位的整数指针,当解码后,解码数据将会放在这个指针对应的内存位置 |
| poutbuf_size | 这里传入一个整形的指针,当解码之后会将解码之后的数据大小放在该指针位置 |
| buf | 无符号8位的整数指针,需要解码的数据,该数据就是从外部dc传入的数据 |
| buf_size | 解码数据对应的大小 |
| pts | input presentation timestamp.展示时间戳 |
| dts | input decoding timestamp.解码时间戳 |
| pos | input byte position in stream.字节位置 |
调用这个函数之后,我们定义的AVPackage的变量pkt就会被赋值,得到这个值之后我们就可以到下一步去进一步解码了
decode_packet
int decode_packet(AVCodecContext* ctx, AVFrame* frame, AVPacket* pkt)
{
int ret = 0;
// 发送packet到解码器
ret = avcodec_send_packet(dec_ctx, pkt);
if (ret < 0) {
return ret;
}
// 从解码器接收frame
while (ret >= 0) {
ret = avcodec_receive_frame(dec_ctx, frame);
if (ret == AVERROR(EAGAIN) || ret == AVERROR_EOF) {
break;
} else if (ret < 0) {
// handle error
break;
}
// 输出yuv buffer数据
output_yuv_buffer(frame);
}
return ret;
}
avcodec_send_packet 这个方法我们看一下官方文档
这个方法接受两个指针变量。方法的作用是提供原始的数据包供给一个解码器。方法返回一个整形,如果整形为0的时候代表成功,ffmpege将错误码都设置了成负数。如果不为负数,我们将可以从解码器去获取AVFrame。
通过调用avcodec_receive_frame,我们可以实现该目的
两个入参,一个是解码器的指针,一个是AVFrame的指针,如果返回的状态吗不为负数,我们的frame形参将会赋值一个新的原始帧数据。
拿到AVFrame之后,我们就可以将其解码为YUV数据了
output_yuv_buffer
接着我们看一下YUV的数据怎么通过AVFrame来进行获取。首先我们可能需要先了解一下YUV的基础概念。可以先看看参考文档的YUV的几种基本格式。我们这边的使用的YUV的格式是较为普遍的YUV420格式。
在这个格式下,YUV的采样是根据,Y分量逐行采样,U和V分量采用隔行二分之一的采样。这样的采样相对于全采样可以减少一半的数据大小,这是我们为什么选择将他作为我们传输格式的原因,可以减少不少带宽。
我们在前面初始化的确定了YUV的解码格式是 AV_PIX_FMT_YUV420P 这个格式的最后字母P代表了YUV的存储格式为平面格式,在这个格式下,YUV三个分量会采用三个平面planner来分别存储在AVFrame内部的data字段下。例如,AVFrame->data[0] 存储 Y 分量,AVFrame->data[1] 存储 U 分量,AVFrame->data[2] 存储 V 分量。详细可以参考
我们的目标是将这个存储格式的420P的AVFrame变量,还原成正常内存需要的数据。参考下图
接下来我们看一下我们的函数是怎么写的
void output_yuv_buffer(AVFrame *frame) {
int width, height, frame_size;
uint8_t *yuv_buffer = NULL;
width = frame->width;
height = frame->height;
// 根据格式,获取buffer大小
frame_size = av_image_get_buffer_size(frame->format, width, height, 1);
// 分配内存
yuv_buffer = (uint8_t *)av_mallocz(frame_size * sizeof(uint8_t));
// 将frame数据按照yuv的格式依次填充到bufferr中。下面的步骤可以用工具函数av_image_copy_to_buffer代替。
int i, j, k;
// Y
for(i = 0; i < height; i++) {
memcpy(yuv_buffer + width*i,
frame->data[0]+frame->linesize[0]*i,
width);
}
for(j = 0; j < height / 2; j++) {
memcpy(yuv_buffer + width * i + width / 2 * j,
frame->data[1] + frame->linesize[1] * j,
width / 2);
}
for(k =0; k < height / 2; k++) {
memcpy(yuv_buffer + width * i + width / 2 * j + width / 2 * k,
frame->data[2] + frame->linesize[2] * k,
width / 2);
}
// 通过之前传入的回调函数发给js
decoder_callback(yuv_buffer, frame_size, frame->pts);
av_free(yuv_buffer);
}
这个函数的接受一个类型为AVFrame的入参,并在前面初始化了变量,width和height代表帧的尺寸,frame_size代表的是转成buffer需要的内存大小。yuv_buffer是我们的输出产物变量,用来存储连续的YUV数据
这里的三个for循环就是用于给YUV各自做赋值使用的,linesize代表的是每一行分量对应的字节数。memcpy是C的一个标准函数,作用是拷贝内存区域至另外一个内存区域。
void *memcpy(void *dest, const void *src, size_t n);
参数说明
-
dest: 目标内存地址,数据将被复制到这里。 -
src: 源内存地址,数据将从这里复制。 -
n: 要复制的字节数。
这里取第一个for循环来作为说明。
for(i = 0; i < height; i++) { // 取高度的值目的是为了隔行来进行处理
memcpy(yuv_buffer + width*i,
frame->data[0]+frame->linesize[0]*i,
width);
}
目标内存地址取值是 yuv_buffer + width * i。 这里yuv_buffer是内存的起始地址。i的第一个循环是0.那么这里的值就会等于 yuv_buffer。我们将首地址来作为Y分量的拷贝地址,源内存地址取的是 frame->data[0] + frame -> linesize[0]*i。第一个循环下,第一个地址取的也是data0。第三个参数取值取的是width,也就是单一行像素的大小了。一轮循环之后,我们就得到了上图中的Y部分的图片了
接下来的UV部分也是同理,就不赘述。