开发环境
- FFmpeg 3.4 window 动态库
- window
- Visual Studio 2022
用的开发环境可以在前面的文章中找的哦,依然是在前篇博客里面工程里面增加代码,接着 使用FFmpeg 做音频数据重采样 文章继续开发
开发过程
1. 在FileDecode的AVOpenFile方法里面添加代码
int FileDecode::AVOpenFile(std::string filename)
{
#ifdef WRITE_DECODED_PCM_FILE
outdecodedfile = fopen("decode.pcm", "wb");
if (!outdecodedfile) {
std::cout << "open out put file failed";
}
#endif
#ifdef WRITE_DECODED_YUV_FILE
outdecodedYUVfile = fopen("decoded_video.yuv", "wb");
if (!outdecodedYUVfile) {
std::cout << "open out put YUV file failed";
}
#endif
int openInputResult = avformat_open_input(&formatCtx, filename.c_str(), NULL, NULL);
if (openInputResult != 0) {
std::cout << "open input failed" << std::endl;
return -1;
}
if (avformat_find_stream_info(formatCtx, NULL) < 0) {
std::cout << "find stram info faild" << std::endl;
return -1;
}
av_dump_format(formatCtx, 0, filename.c_str(), 0);
audioStream = av_find_best_stream(formatCtx, AVMEDIA_TYPE_AUDIO, -1, -1, NULL, 0);
if (audioStream < 0) {
std::cout << "av find best audio stream failed" << std::endl;
return -1;
}
videoStream = av_find_best_stream(formatCtx, AVMEDIA_TYPE_VIDEO, -1, -1, NULL, 0);
if (videoStream < 0) {
std::cout << "av find best video stream failed" << std::endl;
return -1;
}
return 0;
}
- 增加了 outdecodedYUVfile 文件的创建,用来存放解码后的视频源数据
- videoStream 视频流解析
2. 在FileDecode的创建 OpenVideoDecode方法
方法的作用查找和打开一个视频解码器
int FileDecode::OpenVideoDecode() {
videoCodecCtx = formatCtx->streams[videoStream]->codec;
AVCodec* codec = avcodec_find_decoder(videoCodecCtx->codec_id);
if (codec == NULL) {
std::cout << "cannot find video codec id: " << videoCodecCtx->codec_id << std::endl;
return -1;
}
// Open codec
AVDictionary* dict = NULL;
int codecOpenResult = avcodec_open2(videoCodecCtx, codec, &dict);
if (codecOpenResult < 0) {
std::cout << "open video decode faild" << std::endl;
return -1;
}
return 0;
}
videoCodecCtx里面存储了解码器的上下文,后面会用到里面的信息
3. 在FileDecode的Decode() 方法里面增加对视频包的处理
int FileDecode::Decode()
{
AVPacket avpkt;
av_init_packet(&avpkt);
do {
if (av_read_frame(formatCtx, &avpkt) < 0) {
//没有读到数据,说明结束了
return 0;
}
if (avpkt.stream_index == audioStream)
{
//std::cout << "read one audio frame" << std::endl;
DecodeAudio(&avpkt);
av_packet_unref(&avpkt);
}
else if(avpkt.stream_index == videoStream) {
DecodeVideo(&avpkt);
av_packet_unref(&avpkt);
continue;
}
} while (avpkt.data == NULL);
}
DecodeVideo 就是对视频的流解码处理
int FileDecode::DecodeVideo(AVPacket* originalPacket)
{
int ret = avcodec_send_packet(videoCodecCtx, originalPacket);
if (ret < 0)
{
return -1;
}
AVFrame* frame = av_frame_alloc();
ret = avcodec_receive_frame(videoCodecCtx, frame);
if (ret == AVERROR(EAGAIN) || ret == AVERROR_EOF) {
return -2;
}
else if (ret < 0) {
std::cout << "error decoding";
return -1;
}
#ifdef WRITE_DECODED_YUV_FILE
enum AVPixelFormat pix_fmt = videoCodecCtx->pix_fmt;
bool isPlanar = is_planar_yuv(pix_fmt);
int wrapy = frame->linesize[0];
int wrapu = frame->linesize[1];
int wrapv = frame->linesize[2];
int xsize = frame->width;
int ysize = frame->height;
if (pix_fmt == AV_PIX_FMT_YUV420P) {
fwrite(frame->data[0], 1, wrapy * ysize, outdecodedYUVfile); // Y
fwrite(frame->data[2], 1, wrapv * ysize / 2, outdecodedYUVfile); // V
fwrite(frame->data[1], 1, wrapu * ysize / 2, outdecodedYUVfile); // U
}
#endif // WRITE_DECODED_PCM_FILE
av_frame_free(&frame);
}
同样的,解码过程是由 avcodec_send_packet 和 avcodec_receive_frame 完成。送入AVPacket ,得到AVFrame 解码后的数据,然后将Frame里面的yuv数据写入文件。
宏定义判断条件里面的代码,就是用来把AVFrame* 里面解码出来的数据包写入文件,这里会涉及一个知识点,yuv,我们的文件是h264视频编码,也是目前主流的视频编码格式。它解码出来的视频原始数据格式是yuv,yuv有比较多的格式。这次我们只重点将一个,后面可以专题来讲着个知识。
yuv 讲解
YUV
格式有两大类:planar
和packed
- 对于
planar
的YUV
格式,先连续存储所有像素点的Y
,紧接着存储所有像素点的U
,随后是所有像素点的V
就好比 YYYYYYYYYYYYYYYYYYYYYUUUUUUUUUUUUUUUVVVVVVVVVVV,存储planar格式的数据的时候,比如AVFrame->data 里面有数组长度为3,AVFrame->data[0],AVFrame->data[1],AVFrame->data[2]分别指向buf里面存储的y,u,v的起点。说到道理大家明白了和音频一样,AVframe->data 是个指针指向作用,实际是一个以一维数组存储所有数据 - 对于
packed
的YUV
格式,每个像素点的Y,U,V
是连续交叉存储的,比如 YYYYUV YYYYUV。所以 AVFrame->data[0] 就指向了全部数据,frame->linesize[0] 也是全部数据的长度。这种情况 frame->data[i] 都等于 AVFrame->data[0]。 frame->linesize[i] 都等于 frame->linesize[0],可以通过这个来判断yuv平面类型
int is_planar_yuv(const AVFrame *frame) {
if (!frame)
return -1; // Invalid frame
// Check if linesize array contains different values
for (int i = 1; i < AV_NUM_DATA_POINTERS; i++) {
if (frame->linesize[i] != frame->linesize[0])
return 1; // Planar layout
}
// Check if data pointers are different
for (int i = 1; i < AV_NUM_DATA_POINTERS; i++) {
if (frame->data[i] != frame->data[0])
return 1; // Planar layout
}
return 0; // Non-planar layout
}
在我的代码里面,弄得更加简单一点,常用的就那几种,直接写上就可以了,严格的话,就用上面的
bool FileDecode::is_planar_yuv(enum AVPixelFormat pix_fmt) {
// Check if the pixel format corresponds to planar layout
switch (pix_fmt) {
case AV_PIX_FMT_YUV420P:
case AV_PIX_FMT_YUV422P:
case AV_PIX_FMT_YUV444P:
// Add more planar pixel formats here if needed
return 1; // Planar layout
default:
return 0; // Non-planar layout
}
}
YUV 4:4:4
采样,每一个Y对应一组UV分量。YUV 4:2:2
采样,每两个Y共用一组UV分量。YUV 4:2:0
采样,每四个Y共用一组UV分量
在我这儿测试例子里面,我用的文件是 AV_PIX_FMT_YUV420P 的,是planar layout 格式的。 所以就从frame->data的三个指针位置去取
通过上面表述,我们就可以计算一个帧视频数据有多少个字节,加入是yuv420格式的,960*400分辨率的。那么它的y占用960×400个字节,u占用 960×400/4个, v占用 960×400/4。一张yuv图一共占用960×400×3/2个字节。
回到上面工程源码:
int wrapy = frame->linesize[0];
int wrapu = frame->linesize[1];
int wrapv = frame->linesize[2];
int xsize = frame->width;
int ysize = frame->height;
if (pix_fmt == AV_PIX_FMT_YUV420P) {
fwrite(frame->data[0], 1, wrapy * ysize, outdecodedYUVfile); // Y
fwrite(frame->data[2], 1, wrapv * ysize / 2, outdecodedYUVfile); // V
fwrite(frame->data[1], 1, wrapu * ysize / 2, outdecodedYUVfile); // U
}
xszie 就是宽度 960,ysize 就是400。那wrapy是什么呢,这里涉及一个字节对齐。
在这个图里面,有效数据宽高是960*400,但是为了计算机存储,宽度多了4个字节,后边部分,其实是冗余数据。那么这个wrapy就是960,xsize是964,同理高度也可以有这样的值。所有在拷贝数据的时候,我们只拷贝960部分的。
不知是否发现AVFrame->data 拷贝到文件数据最终格式就是 YYYYUV YYYYUV YYYYUV。 这其实就是packed 格式的。
4.运行验证
执行程序后,会生成一个 decoded_video.yuv 文件,这个文件是很大的,这也证明了为什么视频数据编码压缩。我用下面这个工具来验证播放。
工具->参数设置,可以设置yuv的格式,分辨率可以自定义,按照实际情况填写
5. 补充
注意要关闭和释放相关参数
void FileDecode::Close()
{
if (swrResample) {
swrResample->Close();
}
#ifdef WRITE_DECODED_PCM_FILE
fclose(outdecodedfile);
#endif // WRITE_DECODED_PCM_FILE
#ifdef WRITE_DECODED_YUV_FILE
fclose(outdecodedYUVfile);
#endif // WRITE_DECODED_PCM_FILE
if (audioCodecCtx) {
avcodec_close(audioCodecCtx); // 注意这里要用关闭,不要用下面free,会不够彻底,导致avformat_close_input崩溃
//avcodec_free_context(&audioCodecCtx);
}
if (videoCodecCtx) {
avcodec_close(videoCodecCtx);
//avcodec_free_context(&videoCodecCtx);
}
if (formatCtx) {
avformat_close_input(&formatCtx);
}
}
这里之前的的有个bug,avcodec_close 替代 avcodec_free_context,才可以正常关闭解码器,不然后面释放format的时候会报错
6.总结
视频解码写文件流程和音频类似,难点在于yuv格式的理解,这也是音视频知识的一个重要点。需要的话,可以自行去仔细学,以后有机会,我也会详细讲解一次
7. 其他:
-
仓库: FFmpegExample
-
讲解视频地址:视频讲解地址
-
联系我:
- 邮箱: gu19860621@163.com
- 微信: p13071210551