在ffmpeg中,使用AVPacket结构体表示视频文件中的压缩数据,也就是还未进行解压缩的原始视频帧与音频帧。
整体代码如下:
#include<iostream>
#define __STDC_CONSTANT_MACROS
extern "C"
{
#include "libavcodec/avcodec.h"
#include "include/libavformat/avformat.h"
#include "include/libswscale/swscale.h"
#include "include/libavdevice/avdevice.h"
}
using namespace std;
AVFormatContext* myFFMpegDemux(const char* path, AVFormatContext* ic);//打开视频文件
void myFFMpegShow(AVFormatContext* ic);//显示视频文件信息
const AVCodec* myFFMpegFindVideoCodec(AVFormatContext* ic, const AVCodec* vcodec, int* videoindex);//寻找视频流解码器
const AVCodec* myFFMpegFindAudioCodec(AVFormatContext* ic, const AVCodec* acodec, int* audioindex);//寻找音频流解码器
void myFFMpegOpenVideoCodec(AVFormatContext* ic, AVCodecContext *vc, int videoindex);//启动视频流解码器
void myFFMpegOpenAudioCodec(AVFormatContext* ic, AVCodecContext *ac, int audioindex);//启动音频流解码器
int main(int argc, char *argv[])
{
const char* path = "ds.mov";//记录视频源文件的路径,这里视频文件ds.mov直接放在项目工程里面了,所以可以直接用视频名称
//如果视频不在项目工程里面,路径的书写格式举例:D:\\code of visual studio\\ffmpegTest\\ffmpegTest\\ds.mov
//路径要使用“\\”,不然会被视为转义字符
cout << "TEST DEMUX" << endl;
//初始化封装库
//在新版本中av_register_all()被弃用了,可以根据代码里有无此函数判断ffmpeg版本
//初始化网络库(可以打开rtsp,rtmp,http协议的流媒体视频)
avformat_network_init();
//解封装上下文
AVFormatContext* ic = nullptr;//将其地址做为输入,会申请一块空间,将这块空间的地址赋给ic
//解封装上下文AVFormatContext,是存储音视频封装格式中包含信息的结构体。
//对视频进行解封装
ic = myFFMpegDemux(path,ic);
//获取并显示视频流与音频流的信息
//myFFMpegShow(ic);
//初始化解码器信息
const AVCodec* vcodec = nullptr;
const AVCodec* acodec = nullptr;
//寻找合适的视频解码器与音频解码器
int videoindex = -1;
int audioindex = -1;
vcodec = myFFMpegFindVideoCodec(ic, vcodec,&videoindex);
acodec = myFFMpegFindAudioCodec(ic, acodec, &audioindex);
//创建解码器上下文
AVCodecContext *vc = avcodec_alloc_context3(vcodec);
myFFMpegOpenVideoCodec(ic, vc, videoindex);
AVCodecContext *ac = avcodec_alloc_context3(acodec);
myFFMpegOpenAudioCodec(ic, ac, audioindex);
AVPacket *packet;//创建packet指针
packet = av_packet_alloc();//初始化
cout << "av_read_frame start!" << endl;
int ret = 0;
int packetCount = 0;//当前的帧数
int packetShowCount = 10;//设定的最多显示帧数
while (true)//开始循环
{
ret = av_read_frame(ic, packet);//读取一个packet结构体
if (ret < 0) {
cout << "av_read_frame end!" << endl;
break;
}
if (packetCount++ < packetShowCount) {
if (packet->stream_index == audioindex) {//如果读到的是音频帧
cout << "audio 显示时间戳" << packet->pts << endl;
cout << "audio 解码时间戳" << packet->dts << endl;
cout << "audio 压缩编码数据大小" << packet->size << endl;
cout << "audio 数据的偏移地址" << packet->pos << endl;
cout << "audio 时长:" << packet->duration * av_q2d(ic->streams[audioindex]->time_base) << endl;
}
else if (packet->stream_index == videoindex) {//如果读到的是视频帧
cout << "video 显示时间戳" << packet->pts << endl;
cout << "video 解码时间戳" << packet->dts << endl;
cout << "video 压缩编码数据大小" << packet->size << endl;
cout << "video 数据的偏移地址" << packet->pos << endl;
cout << "video 时长:" << packet->duration * av_q2d(ic->streams[videoindex]->time_base) << endl;
}
else {
cout << "unknown stream_index:" << packet->stream_index;//未能识别类型的帧
}
}
av_packet_unref(packet);
}
//资源回收
if (ic) {//如果封装上下文仍存在
avformat_close_input(&ic);//释放资源,指针置零
ic = nullptr;
avcodec_close(vc);
avcodec_close(ac);
vcodec = nullptr;
acodec = nullptr;
vc = nullptr;
ac = nullptr;
}
return 0;
}
AVFormatContext* myFFMpegDemux(const char* path,AVFormatContext* ic) {
//对视频文件进行解封装操作
int re = avformat_open_input(&ic, path, 0, nullptr);//0表示自动选择解封装器,设置一个返回值知道有无错误
if (re != 0)//如果返回值不是0,说明打开出现错误
{
char buf[1024] = { 0 };
av_strerror(re, buf, sizeof(buf) - 1);//记录错误
cout << "open" << path << "failed!:" << buf << endl;//提示错误
return nullptr;
}
cout << "open " << path << " success!" << endl;//提示成功
return ic;
}
void myFFMpegShow(AVFormatContext* ic) {
avformat_find_stream_info(ic, 0);
//自行计算视频的总时长,以毫秒为单位,AV_TIME_BASE为1秒时长
int total = ic->duration / (AV_TIME_BASE / 1000);
cout << "total ms = " << total << endl;
//定义了秒,小时,分钟
int total_seconds, hour, minute, second;
total_seconds = (ic->duration) / AV_TIME_BASE;
hour = total_seconds / 3600;
minute = (total_seconds % 3600) / 60;
second = (total_seconds % 60);
cout << "total duration " <<"hours: "<< hour <<"minutes: "<< minute <<"seconds: "<< second << endl;
//获取视频流的详细信息,包括视频流与音频流
av_dump_format(ic, 1, "2", 0);
for (int i = 0; i < ic->nb_streams; i++)//对视频中所有的流进行遍历
{
AVStream* as = ic->streams[i];
//音频
if (as->codecpar->codec_type == AVMEDIA_TYPE_AUDIO)
{
cout << i << "音频" << endl;
cout << "simple_rate" << as->codecpar->sample_rate << endl;
cout << "format" << as->codecpar->format << endl;
cout << "channels" << as->codecpar->channels << endl;
cout << "codec_id" << as->codecpar->codec_id << endl;
}
//视频
else if (as->codecpar->codec_type == AVMEDIA_TYPE_VIDEO)
{
cout << i << "视频" << endl;
cout << "simple_rate" << as->codecpar->sample_rate << endl;
cout << "format" << as->codecpar->format << endl;
cout << "channels" << as->codecpar->channels << endl;
cout << "codec_id" << as->codecpar->codec_id << endl;
}
}
}
const AVCodec* myFFMpegFindVideoCodec(AVFormatContext* ic, const AVCodec* vcodec,int* videoindex) {
//找到视频流在streams[i]中的编号i
*videoindex = av_find_best_stream(ic, AVMEDIA_TYPE_VIDEO, -1, -1, &vcodec, 0);
//找合适的视频解码器,得到流对应的解码器编号
vcodec = avcodec_find_decoder(ic->streams[*videoindex]->codecpar->codec_id);
//如果没有找到流对应的解码器,需要判断一下
if (!vcodec)
{
cout << "can't find the codec id" << ic->streams[*videoindex]->codecpar->codec_id << endl;
return nullptr;
}
cout << " find the video codec id" << ic->streams[*videoindex]->codecpar->codec_id << endl;
return vcodec;
}
const AVCodec* myFFMpegFindAudioCodec(AVFormatContext* ic,const AVCodec* acodec,int* audioindex) {
//找到音频流在streams[i]中的编号i
*audioindex = av_find_best_stream(ic, AVMEDIA_TYPE_AUDIO, -1, -1, &acodec, 0);
//找合适的音频解码器,得到流对应的解码器编号
acodec = avcodec_find_decoder(ic->streams[*audioindex]->codecpar->codec_id);
//如果没有找到流对应的解码器,需要判断一下
if (!acodec) {
cout << "can't find the codec id" << ic->streams[*audioindex]->codecpar->codec_id << endl;
return nullptr;
}
cout << " find the audio codec id" << ic->streams[*audioindex]->codecpar->codec_id << endl;
return acodec;
}
void myFFMpegOpenVideoCodec(AVFormatContext* ic, AVCodecContext *vc, int videoindex) {
//配置解码器上下文参数
avcodec_parameters_to_context(vc, ic->streams[videoindex]->codecpar);
//八线程解码
vc->thread_count = 8;
//打开解码器上下文
int re = avcodec_open2(vc, 0, 0);
if (re != 0)
{
char buf[1024] = { 0 };
av_strerror(re, buf, sizeof(buf) - 1);
cout << "open vedio codec failed!" << buf << endl;
return ;
}
cout << "video codec success!" << endl;
}
void myFFMpegOpenAudioCodec(AVFormatContext* ic, AVCodecContext *ac, int audioindex) {
//配置解码器上下文参数
avcodec_parameters_to_context(ac, ic->streams[audioindex]->codecpar);
//八线程解码
ac->thread_count = 8;
//打开解码器上下文
int re = avcodec_open2(ac, 0, 0);
if (re != 0)
{
char buf[1024] = { 0 };
av_strerror(re, buf, sizeof(buf) - 1);
cout << "open audio codec failed!" << buf << endl;
return;
}
cout << "audio codec success!" << endl;
}
读取压缩帧的核心代码是:
ret = av_read_frame(ic, packet);//读取一个packet结构体
上句代码实际上实现了对AVPacket *类型变量packet的更新,变量packet始终为当前读取出的最新的一个帧,当然,该帧可能是视频帧也可能是音频帧。
if (packet->stream_index == audioindex) {//如果读到的是音频帧
cout << "audio 显示时间戳" << packet->pts << endl;
cout << "audio 解码时间戳" << packet->dts << endl;
cout << "audio 压缩编码数据大小" << packet->size << endl;
cout << "audio 数据的偏移地址" << packet->pos << endl;
cout << "audio 时长:" << packet->duration * av_q2d(ic->streams[audioindex]->time_base) << endl;
}
通过packet->stream_index变量可以判断该帧是视频帧还是音频帧,随后显示该帧的各种信息。
百度百科里这么说的:
数字电视系统码流分析中,对PES进行分析,打包的基本码流(PES)是非定长的,一般是一个存取单元的长度,一个存取单元为一个视频帧,也可以是一个音频帧。为实现解码的同步,每段之前还要插入相应的时间标记和相关的标志符,解码时间标签(DTS,decoding time stamp)、显示时间标签(PTS,presentation time stamp)、以及段内信息类型和用户类型等标志信息。
DTS:decoding time stamp 解码时间戳
PTS:presentation time stamp 显示时间戳
掏出《音视频开发进阶指南》,里面这样说的:PTS决定这帧数据什么时候显示给用户,DTS决定该帧数据什么被解码,如果视频里各个帧的编码是按照显示顺序依次进行的,那么解码和显示的时间应该是一致的。但是实际上大多数编码解码标准中,编码顺序和输入顺序并不一致,所以需要两种时间戳。
pos表示的是流中字节的位置,这个数据并不是很重要。
运行结果如下图所示:
从运行结果中也可以看到一些需要说明一下的内容。
可以看到在输出内容中,通常是一个视频帧两个音频帧为周期进行循环,这是由视频的编码格式决定的,说明该种编码方式为一个视频帧后方跟随两个音频帧。
此外,对于音频流,一个AVPacket可能包含多个AVFrame,而视频流中一个只AVPacket包含AVFrame。
每一个视频帧的时长和音频帧的时长都是有固定长度的,这也体现了编码的规范性。