当前位置:网站首页>FFmpeg连载3-视频解码
FFmpeg连载3-视频解码
2022-04-21 21:34:00 【FlyerGo】
导读
在前面我们介绍了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!!!

版权声明
本文为[FlyerGo]所创,转载请带上原文链接,感谢
https://blog.csdn.net/u012944685/article/details/124311167
边栏推荐
- [spark] (task5) fundamentals of sparkml (classification | clustering model)
- 移动Web开发之rem实际开发适配方案
- Smart face recognition 3 -- pytoch builds its own facenet face recognition platform
- 短视频直播模式让偏远地区农产品“走出去”
- Bailian3722 因子问题【枚举】
- Smart face recognition 1 - keras builds its own facenet face recognition platform
- 奇安信冬奥“零事故”终端安全分享会:成功解决6641次攻击事件
- [ZigBee wireless communication module step by step] zigbee3 0 module to establish remote network control method
- Bailian3722 factor problem [enumeration]
- Yuanxin technology seeks to be listed again: the amount of loss soars, and the R & D expense rate is less than 2%. How to break through?
猜你喜欢

【迷人的爪哇】——数据类型和变量

Microsoft | multilingual molecular representation learning through comparative learning pre training

短视频直播模式让偏远地区农产品“走出去”

02.GCC编译器的使用

Eeasybi report system data source selection code development manual

分析师认为三星Galaxy Z Fold 4和Z Flip 4可能比其前代产品更便宜

【zigbee无线通信模块步步详解】ZigBee3.0模块建立远程网络控制方法

Reading breaks ten thousand "volumes": National Reading insight 2022

细粒度情感分析实战

Intelligent target detection 50 - tensorflow2 uses mobilenet series (V1, V2, V3) to build yolov4 target detection platform
随机推荐
(valid for personal test) Oracle can't use backspace key to backspace the command line under Linux, and can't use up and down keys to switch the historical command. There will be garbled code
Workflow process setting customization development
Gan remake 2, which seems to be fun -- keras built srgan platform to improve the super-resolution of pictures
Bailian4004 digit combination [recursion + DP]
[common shortcut keys]
Bailian4105 拯救公主【BFS】
Anxun cup 2021_ Crypto_ Reappearance
尿素期货怎么做才安全?尿素期货套期保值会有什么好处?
奇安信冬奥“零事故”终端安全分享会:成功解决6641次攻击事件
Those things about SAP - Career - 36 - from the subject of "fixed assets liquidation"
使用Flutter给照片调个颜色滤镜动画
Reflex WMS study experience shelf principle
2022安全员-A证考试练习题及在线模拟考试
Application case of Lora wireless data transmission module of Internet of things: lorawan gateway communication technology
CTF Crypto中涉及的AES题目
华云积极响应合肥高新区抗疫号召:践行社会责任 贡献科技企业力量
Eeasybi report system data source selection code development manual
841. 字符串哈希 (字符串哈希模板)
2022R1快开门式压力容器操作操作证考试题库模拟考试平台操作
JVM 从入门到放弃之 ZGC 垃圾收集器精讲