导读
在前面我们介绍了FFmpeg的解封装,并且实现了提取视频文件中的音频流和视频流单独输出,使用ffplay播放验证, 今天我们使用FFmpeg解码视频流,将视频解码为YUV并输出到文件,然后使用ffplay播放YUV图像。
关于YUV的相关知识,之前笔者也有过一些笔记,但是写的比较简单,大家可以网上找找更加详细的资料: 音视频基础知识-YUV图像
关于使用FFmpeg进行视频解码的文章,之前也写过类似的文章《Android使用ffmpeg解码视频为YUV》 但是在这篇文章中有一个错误的点就是写入的YUV的方法不是通用的,对于一些视频解码出来的YUV,按照文章中的方法写入可能会有播放花屏,甚至无法播放的情况。对于这点如果有误人子弟的话, 笔者深感抱歉,在这里说明一下,笔者发表的这些博客仅作为笔记或交流需要,不具备权威性,观点总结仅限于自己的理解,不保证所有的准确性哈。。。
AVFrame介绍
相对于解封装而言,视频解码时我们需要用到一个新的结构体AVFrame。
AVFrame可以说是一个与AVPacket相对应的结构体,既然AVPacket表示的是音视频包解码前或编码后的数据,那么AVFrame就是音视频包解码后或编码前的原始数据包。
AVFrame内部包含了一个视频帧或音频帧所持续播放的时间,播放的时机等时间信息,同时还包含了采样率,采样格式、图片格式、帧类型等相关信息。
在FFmpeg中我们使用av_frame_alloc() 分配一个AVFrame,使用av_frame_free 释放一个AVFrame,使用函数av_frame_get_buffer 为AVFrame内部分配数据缓冲区。
视频解码
回顾之前的解封装的一张图:
视频的解码阶段就是发生在函数av_read_frame 之后,如果读取到的资源包是视频类型的则送进解码器进行解码。
我们来看看另外一张图,这张图主要介绍了解码视频过程中用到的一些结构体的功能:
下面简单介绍一下视频解码的两个重要操作步骤:
1、配置解码器
配置解码器这个步骤又可以拆分为四个小步骤:
a、查找解码器 b、分配解码器上下文 c、按照视频流信息配置解码参数到解码器上下文 d、打开解码器
这四个小步骤对于的FFmpeg的API分别是:
// 查找解码器
avcodec_find_decoder 或 avcodec_find_decoder
// 分配解码器上下文
avcodec_alloc_context3
//按照视频流信息配置解码参数到解码器上下文
avcodec_parameters_to_context
//打开解码器
avcodec_open2
2、发送解码包及获取解码YUV数据帧
解码阶段主要用到的两个关键API是avcodec_send_packet 和avcodec_receive_frame 其中avcodec_send_packet 表示发送一个视频数据包到解码器,然后使用avcodec_receive_frame 接收 解码数据帧(也就是YUV数据)。avcodec_send_packet 和avcodec_receive_frame 并不是一一对应的调用关系,而是一个avcodec_send_packet 的调用,可能会对应n个avcodec_receive_frame 函数的 调用。因为解码器内部是有缓存和参考帧的,并不是每送进去一个数据包就能解码出一帧数据,可能出现送进去几个数据包,但是暂时没有数据帧解码输出的情况,也可能会出现某个时间点送进去一个数据包,然后会输出n个数据帧的情况。
主要代码如下:
VideoDecoder.h
#include <string>
class VideoDecoder {
public:
VideoDecoder();
~VideoDecoder();
void decode_video(std::string media_path, std::string yuv_path);
};
以下是实现文件:
VideoDecoder.cpp
#include "VideoDecoder.h"
#include <iostream>
extern "C"{
#include <libavformat/avformat.h>
#include <libavcodec/avcodec.h>
#include <libavutil/avutil.h>
#include <libavutil/log.h>
}
VideoDecoder::VideoDecoder() {
}
VideoDecoder::~VideoDecoder() {
}
void VideoDecoder::decode_video(std::string media_path, std::string yuv_path) {
AVFormatContext *avFormatContext = nullptr;
AVCodecContext *avCodecContext = nullptr;
avFormatContext = avformat_alloc_context();
avformat_open_input(&avFormatContext,media_path.c_str(), nullptr,nullptr);
av_dump_format(avFormatContext,0,media_path.c_str(),0);
int video_index = av_find_best_stream(avFormatContext,AVMEDIA_TYPE_VIDEO,-1,-1, nullptr,0);
if(video_index < 0){
std::cout << "没有找到视频" << std::endl;
}
const AVCodec *avCodec = avcodec_find_decoder(avFormatContext->streams[video_index]->codecpar->codec_id);
avCodecContext = avcodec_alloc_context3(avCodec);
avcodec_parameters_to_context(avCodecContext,avFormatContext->streams[video_index]->codecpar);
int ret = avcodec_open2(avCodecContext,avCodec, nullptr);
if(ret < 0){
std::cout << "解码器打开失败" << std::endl;
}
FILE *yuv_file = fopen(yuv_path.c_str(),"wb");
AVPacket *avPacket = av_packet_alloc();
AVFrame *avFrame = av_frame_alloc();
while (true){
ret = av_read_frame(avFormatContext,avPacket);
if(ret < 0){
std::cout << "文件读取完毕" << std::endl;
break;
} else if(video_index == avPacket->stream_index){
ret = avcodec_send_packet(avCodecContext,avPacket);
if(ret < 0){
std::cout << "视频发送解码失败:" << av_err2str(ret) << std::endl;
}
while (true){
ret = avcodec_receive_frame(avCodecContext,avFrame);
if (ret == AVERROR(EAGAIN) || ret == AVERROR_EOF) {
std::cout << "avcodec_receive_frame:" << av_err2str(ret) << std::endl;
break;
} else if (ret < 0) {
std::cout << "视频解码失败:" << std::endl;
return;
} else{
std::cout << "写入YUV文件avFrame->linesize[0]:" << avFrame->linesize[0] << "avFrame->width:" << avFrame->width << std::endl;
std::cout << "avFrame->format:" << avFrame->format << std::endl;
// 播放 ffplay -i YUV文件路径 -pixel_format yuv420p -framerate 25 -video_size 640x480
// frame->linesize[1] 对齐的问题
// 正确写法 linesize[]代表每行的字节数量,所以每行的偏移是linesize[]
// 成员data是个指针数组,每个成员所指向的就是yuv三个分量的实体数据了,成员linesize是指对应于每一行的大小,为什么需要这个变量,是因为在YUV格式和RGB格式时,每行的大小不一定等于图像的宽度
//
for(int j=0; j<avFrame->height; j++)
fwrite(avFrame->data[0] + j * avFrame->linesize[0], 1, avFrame->width, yuv_file);
for(int j=0; j<avFrame->height/2; j++)
fwrite(avFrame->data[1] + j * avFrame->linesize[1], 1, avFrame->width/2, yuv_file);
for(int j=0; j<avFrame->height/2; j++)
fwrite(avFrame->data[2] + j * avFrame->linesize[2], 1, avFrame->width/2, yuv_file);
// 错误写法 用source.200kbps.766x322_10s.h264测试时可以看出该种方法是错误的
// 如果frame.width == avFrame->linesize[0] 则可以用这种方式写入
// 写入y分量
// fwrite(avFrame->data[0], 1, avFrame->width * avFrame->height, yuv_file);//Y
// // 写入u分量
// fwrite(avFrame->data[1], 1, (avFrame->width) *(avFrame->height)/4,yuv_file);//U:宽高均是Y的一半
// // 写入v分量
// fwrite(avFrame->data[2], 1, (avFrame->width) *(avFrame->height)/4,yuv_file);//V:宽高均是Y的一半
}
}
}
av_packet_unref(avPacket);
}
fflush(yuv_file);
av_packet_free(&avPacket);
av_frame_free(&avFrame);
if(nullptr != yuv_file) {
fclose(yuv_file);
yuv_file = nullptr;
}
}
对于解码出来的YUV输出文件,我们可以使用ffplay命令来进行播放:
// 其中 640x480 需要替换成自己解码的视频的真实宽高
ffplay -i YUV文件路径 -pixel_format yuv420p -framerate 25 -video_size 640x480
针对导读中提到的YUV非通用写法,笔者在代码中做了简单的注释。更多的资料可以查询关于FFmpeg内存对齐的问题,例如针对图像来说并不是像素对齐而是字节对齐的。
注意
在上面的例子中,视频文件读取完毕之后并没有对编码器内部进行数据冲刷,可能会导致视频的最后几帧丢失的情况,更加规范的写法应该在文件读取完毕之后再次调用函数avcodec_send_packet 但是需要传入空的视频数据包, 然后通过循环调用avcodec_receive_frame 将解码器中缓存的数据帧全部获取出来。
还有别忘了释放资源。。。
推荐阅读
FFmpeg连载1-开发环境搭建 FFmpeg连载2-分离视频和音频
关注我,一起进步,人生不止coding!!!
|