整个系列的文档链接如下,贴出来方便大家直接跳转:
音视频ijkplayer源码解析系列4--ijkplayer里面如何使用SDL渲染
# 音视频ijkplayer源码解析系列6--pipeline和node解析
考虑到ijkplayer里面播放流程相对复杂,我们就先从如何从一个视频文件(比如MP4格式),解码拿到一帧一帧的图像的流程开始,逐渐展开讲解ijkplayer。
接下来我们先不考虑ijkplayer,单纯从ffmpeg原生api的角度考虑,如何解码一个视频文件得到图像资源。我之前写了一个小的demo工程,可以参考:示例Demo,注意分支为:feature/ffmpeg-player-demo,直接查看ryan_ffmepg_player.c这个文件即可。
视频流的处理流程为:(MP4文件)解协议->解封装->视频解码->颜色空间转换->渲染。可以参考下面这张图(图片出处)
结合上图我们开始讲解如何解码得到每一帧图像
1、解封装流程
解封装(Demuxing)的概念是什么?就是将输入的封装格式的数据,分离成为音频流压缩编码数据和视频流压缩编码数据。例如FLV 格式的数据,经过解封装操作后,输出 H.264编码的视频码流和 AAC 编码的音频码流。
-
首先我们得先通过
avformat_alloc_context
创建封装格式上下文对象AVFormatContext
-
然后借助
avformat_open_input
我们打开输入文件,开始解封装 -
解封装以后形成的多个stream流数据(比如音频、视频、字幕流)就会被存储到我们在步骤1里创建封装格式上下文对象
AVFormatContext
里了 -
这时候借助
avformat_find_stream_info
函数,我们就可以拿到AVFormatContext
里相关的视频信息了 -
最后在借助
av_find_best_stream
,从AVFormatContext
里面找到我们需要的流数据。比如我们实例需要图像数据其代码就是int videoIndex = av_find_best_stream(pFormatCtx, AVMEDIA_TYPE_VIDEO, -1, -1, NULL, 0);
这样我们就取到了所需要视频流的index
结果上述流程我们不仅生成了包含了音频文件信息的AVFormatContext
,更是拿到了所需要流的index。接下来我们就该初始化这个视频流对应的解码器了。
2、 初始化解码器
借助在刚才的流程中拿到的视频流index,我们就可以拿到视频流中的解码器参数
AVCodecParameters *pCodecpar = pFormatCtx->streams[videoIndex]->codecpar;
接着从解码器参数中的编码id查到对应的解码器
AVCodec *pCodec = avcodec_find_decoder(pCodecpar->codec_id); //寻找合适的解码器
紧接着我们需要开始构造解码器的上下文,否则是无法正常初始化的。流程主要就是先创建上下文,然后用之前拿到的解码器参数初始化上下文
AVCodecContext *pCodecCtx = avcodec_alloc_context3(*pCodec); //为AVCodecContext分配内存
avcodec_parameters_to_context(*pCodecCtx, pCodecpar) < 0 //使用解码器参数初始化上下文
此时我们就可以打开解码器了
avcodec_open2(*pCodecCtx, *pCodec, NULL)
那么此时恭喜你,我们成功打开了解码器,要可以开始正式的解码图像了
3、解码
观察解码用到的函数:
int av_read_frame(AVFormatContext *s, AVPacket *pkt);
我们可以发现,解码函数需要两个入参,第一个参数就是我们在解封装流程
中创建的封装格式上下文,第二个参数就是解码的结束一个Packet(音频数据都是以包(Packet)的形式来传递的),不了解Packet是什么的同学可以回去看我们的第一章。
AVFormatContext我们已经有了,我们还需要一个接受解码结果的AVPacket的对象,所以我们首先需要先创建AVPacket的对象
AVPacket *packet = (AVPacket *) av_malloc(sizeof(AVPacket));
解码的流程其实就是while循环不停的执行av_read_frame,拿到数据包Packet。下面是完整的解码代码,大家可以先看下,紧接着我们就来详细的拆解这个流程。
while (av_read_frame(pFormatCtx, packet) >= 0) {
//只要视频压缩数据(根据流的索引位置判断)
if (packet->stream_index == videoIndex) {
//7.解码一帧视频压缩数据,得到视频像素数据
ret = avcodec_send_packet(pCodecCtx, packet);
if (ret < 0) {
LOGE("%s", "解码错误");
return;
}
got_picture = avcodec_receive_frame(pCodecCtx, pFrame);
LOGE("%s%d", "got_picture:", got_picture);
}
}
//释放资源
av_packet_unref(packet);
}
一个Packet数据包其实是有多个类型的数据的,比如视频、音频,我们需要从中拿到对应的视频数据。在我们拿到一帧packet的时候,是需要根据数据类型分别来做处理的,在我们这个场景下我们只处理图像的,所以会使用if (packet->stream_index == videoIndex)
先前置判断当前数据是视频数据。
这样我们就拿到了视频的Packet,紧接着就是解码流程了
ret = avcodec_send_packet(pCodecCtx, packet);
if (ret < 0) {
LOGE("%s", "解码错误");
return;
}
got_picture = avcodec_receive_frame(pCodecCtx, pFrame);
avcodec_send_packet告诉ffmpeg你可以帮我用这个pCodecCtx
对应的解码器上下文,解码packet
这个数据包了。这时候ffmpeg就开始帮你解码了。
解码成功以后,我们继续调用avcodec_receive_frame,从解码器中拿出解码出来的一帧视频帧。
从这里我们又发现了有需要引入一个新的对象AVFrame
,其实就是一帧音频或者视频图像,只需要在刚才我们创建packet对象的时候,一起创建下AVFrame对象即可AVFrame *pFrame = av_frame_alloc();
至此我们完成了解码,但是解码本身肯定是为了处理或者显示用的,我们可以浅浅的讲其中一个场景:“如何将图像展示在屏幕上”
4、如何将图像展示在屏幕上
我们解码出来的视频格式往往和我们需要的视频格式是不一致的,就比如在android上我们从java层拿到的展示view窗口ANativeWindow_Buffer
他需要的图像格式是RGBA8888的格式,那么我们需要将解码出来的frame进行颜色空间的转换,才能正常显示出来。
刚才解码出frame的while循环里,我们可以执行一下的代码
// 视频缓冲区
ANativeWindow_Buffer native_outBuffer;
//为0说明解码完成,非0正在解码
if (!got_picture) {
// 绘制之前配置nativewindow 设置窗口buffer的大小和格式
ANativeWindow_setBuffersGeometry(nativeWindow,pCodecCtx->width,pCodecCtx->height,WINDOW_FORMAT_RGBA_8888);
//上锁
ANativeWindow_lock(nativeWindow, &native_outBuffer, NULL);
//AVFrame转为像素格式RGBA8888,宽高
//2 6输入、输出数据
//3 7输入、输出画面一行的数据的大小 AVFrame 转换是一行一行转换的
//4 输入数据第一列要转码的位置 从0开始
//5 输入画面的高度
sws_scale(sws_ctx, frame->data, frame->linesize, 0, frame->height,
rgb_frame->data, rgb_frame->linesize);
// rgb_frame是有画面数据
uint8_t *dst= (uint8_t *) native_outBuffer.bits;
// 拿到一行有多少个字节 RGBA
int destStride= native_outBuffer.stride*4;
// RGBA像素数据的首地址
uint8_t* src= rgb_frame->data[0];
// RGBA实际内存一行数量
int srcStride = rgb_frame->linesize[0];
for (int i = 0; i < pCodecCtx->height; ++i) {
// 将rgb_frame中每一行的数据复制给nativewindow
// memcpy函数一定要报这个dst和src的一行数据大小是一致,不然会直接crash
memcpy(dst + i * destStride, src + i * srcStride, srcStride);
}
// 解锁
ANativeWindow_unlockAndPost(nativeWindow);
usleep(1000 * 16);
frame_count++;
LOGI("解码第%d帧, 文件大小%d, frame->linesize: %d, rgb_frame->linesize[0]: %d", frame_count, sizeof(dst), frame->linesize[0], rgb_frame->linesize[0]);
}
}
我们来抽象下整个流程帮助大家理解:
-
设置ANativeWindow的大小、格式并加锁
- 颜色空间转换吧图像格式转换成ANativeWindow的格式
- 转换出来的数据写入ANativeWindow的数据缓冲区
-
绘制完成并解锁,此时刷新数据缓冲区出发绘制