摘要:音视频文件 = 音频流+视频流+同步信息,音频信号和视频信号通过某种标准格式进行组合生成封装格式,如 .mp4/.avi/.flv/.mkv 等。在底层,只能解析纯视频或者音频数据,不能直接处理封装的文件。所以想将封装的视频文件分离为音频,视频和字幕等其他辅助信息等才能播放音视频文件。
1、预备知识
1.1 解封装流程
调用avformat_open_input() 函数注册解封装上下文AVFormatContext
调用av_find_best_stream() 函数获取音视频索引
调用av_packet_alloc() 为解分装后的数据包AVPacket分配空间
调用av_read_frame() 开始解封装
1.2 解封装相关函数说明
/* 1、初始化相关函数 */
av_register_all(); //注册所有的解封装和加封装格式,在解封装视频文件之前需要调用一次
avformat_open_input(); //打开并解析一个封装,调用之前确保av_register_all已调用
avformat_find_stream_info(); //查找文件格式索引
av_find_best_stream(); //找到音频流或视频流都用这个函数,分开处理音频和视频
avformate_close_input(); //关闭输入文件,并释放封装上下文,将ic置NULL
av_read_frame(); //读取具体的音视频帧数据
/* 2、AVPacket相关函数 */
av_packet_alloc(); //AVPacket空间创建并初始化
av_packet_clone(); //创建空间并将引用计数加1
av_packet_ref(); //手动给引用计数加1,在此之前确保AVPacket已创建
av_packet_unref(); //手动给引用计数减1
av_packet_free(); //清空AVPacket对象,并减引用计数
av_init_packet(); //默认值
av_packet_from_data(); //自定义数据转成AVPacket包
av_copy_packet(); //已抛弃,用av_packet_clone替代
2、完整代码
编译之前需要在官网下载FFmpeg工具,并在工程中指定头文件和库文件目录,以下代码已在VS2019下编译测试OK
#include <iostream>
extern "C" { //在官网下载的FFmpeg包
#include "libavcodec/avcodec.h"
#include "libavformat/avformat.h"
}
#pragma comment(lib, "avcodec.lib")
#pragma comment(lib, "avformat.lib")
#pragma comment(lib, "avutil.lib")
static double r2d(AVRational r) //分数转浮点数
{
return (r.den == 0) ? 0 : ((double)r.num / (double)r.den); //需要防止分母为0
}
int main(void)
{
const char *path = "1.mp4"; //视频文件路径
char errorMsg[1024] = { 0 }; //存放错误信息
AVFormatContext *ic = NULL; //解封装上下文
//==========1、创建解封装上下文==========
av_register_all(); //注册所有加解封装器
int ret = avformat_open_input(&ic, path, 0, NULL); //0表示自动选择解封装器,opts可有可无
if (ret != 0) //判断是否打开成功
{
av_strerror(ret, errorMsg, sizeof(errorMsg) - 1); //获取错误信息
std::cout << "open " << path << " failed! : " << errorMsg << std::endl;
return -1; //打开失败
}
//========== 2、获取流信息 ==========
ret = avformat_find_stream_info(ic, 0); //并不是所有格式文件都需要调用(例如MP4),此处统一调用
//========== 3、获取音视频索引 ==========
int videoStream = av_find_best_stream(ic, AVMEDIA_TYPE_AUDIO, -1, -1, NULL, 0);
int audioStream = av_find_best_stream(ic, AVMEDIA_TYPE_VIDEO, -1, -1, NULL, 0);
//========== 4、开始解封装 ==========
AVPacket* pkt = av_packet_alloc(); //创建AVPacket并初始化
for (;;) //解封装,得到的是压缩后的数据
{
ret = av_read_frame(ic, pkt); //读取具体的音视频帧数据
if (ret != 0) //读取失败或达到文件末尾
{
std::cout << "===========================end===========================\n";
int ms = 3000; //3秒
long long pos = (double)ms / 1000 * r2d(ic->streams[pkt->stream_index]->time_base);
//将索引至于开头pos位置,用视频帧做seek,往后找关键帧
av_seek_frame(ic, videoStream, pos, AVSEEK_FLAG_BACKWARD | AVSEEK_FLAG_FRAME);
break;
}
std::cout << "大小:" << pkt->size;
std::cout << " 显示时间:" << pkt->pts;
std::cout << " 解码时间:" << pkt->dts;
std::cout << " " << pkt->pts * (r2d(ic->streams[pkt->stream_index]->time_base) * 1000); //将pts转换成毫秒,方便做同步
if (pkt->stream_index == videoStream) //视频帧
std::cout << " 图像" << std::endl;
else if (pkt->stream_index == audioStream) //视频帧
std::cout << " 音频" << std::endl;
av_packet_unref(pkt); //清理AVPacket中的所有空间数据,并且将data与size置0
}
av_packet_free(&pkt); //清空AVPacket对象
if (ic) //判断ic是否已经被释放
avformat_close_input(&ic); //释放封装上下文,并把ic置0
return 0;
}
3、视频信息获取
在调用 avformat_find_stream_info() 函数之后,AVFormatContext 里已经存放里视频相关信息,可以用作其他处理,下面展示如何获取一些基本的音视频参数信息。
int printSomeInfo()
{
int videoStream = 0; // 视频流索引
int audioStream = 1; // 音频流索引
int64_t totalMs = ic->duration / (AV_TIME_BASE / 1000); //获取视频总时长,单位ms
av_dump_format(ic, 0, path, 0); //打印分辨率,比特率,帧率等详细信息
std::cout << "总时长:" << totalMs << "ms" << std::endl;
for (unsigned int i = 0; i < ic->nb_streams; ++i) //获取所有流信息,可能包含音频,视频,字幕等流信息
{
AVStream* as = ic->streams[i]; //音频、视频或字幕等流信息
if (as->codecpar->codec_type == AVMEDIA_TYPE_AUDIO) //音频
{
audioStream = i; //音频流索引
std::cout << "*** " << i << "、音频信息 ***" << std::endl;
std::cout << "样本率sample_rate:" << as->codecpar->sample_rate << std::endl;
std::cout << "通道数量channels:" << as->codecpar->channels << std::endl;
std::cout << "样本数frame_size:" << as->codecpar->frame_size << std::endl; //一帧数据存的是单通道样本数量
//帧率fps = sample_rate / frame_size
}
else if (as->codecpar->codec_type == AVMEDIA_TYPE_VIDEO) //视频
{
videoStream = i; //视频流索引
std::cout << "*** " << i << "、视频信息 ***" << std::endl;
std::cout << "宽度width:" << as->codecpar->width << std::endl;
std::cout << "高度height:" << as->codecpar->height << std::endl;
//注意,流媒体数据宽高可能没有,需要解码之后才能获取
std::cout << "帧率fps:" << r2d(as->avg_frame_rate) << std::endl;//音频不存在该参数
}
std::cout << "样本格式format:" << as->codecpar->format << std::endl;
std::cout << "解封装格式codec_id:" << as->codecpar->codec_id << "\n\n";
}
}
4、结构体参数说明
🎵🎵🎵🎵🎵🎵🎵🎵🎵🎵🎵🎵🎵🎵🎵🎵🎵🎵🎵🎵🎵🎵🎵🎵🎵🎵🎵🎵🎵🎵🎵🎵🎵🎵🎵🎵🎵🎵🎵🎵🎵
/* 结构体申明 */
/******************************************************************
* AVFormatContext //加解封装,包含封装参数和AVStream
* AVIOContext *pb; //文件IO上下文
* char filename[1024]; //文件名
* unsigned int nb_streams; //流元素个数,视频文件包含多少个流
* AVStream **streams; //音频和视频信息
* int64_t duration; //整个媒体文件长度
* int64_t bit_rate; //比特率,1s文件的大小
******************************************************************/
/****************************************************************************************
* AVStream //每一个流(音视频流)的参数信息,所有配置信息
* AVCodecContext *codec; //解封装和编码没做隔离,该参数已经过时,不使用
* AVRational time_base; //时间基数,用分数表示,为了保障精度
* int64_t duration; //时间段,该参数不一定有,以AVFormatContext里的为准
* int64_t nb_frames; //帧数
* AVRational avg_frame_rate; //帧率,用分数表示
* AVCodecParameters *codecpar; //音视频参数,用于替代AVCodecContext
****************************************************************************************/
/****************************************************************************************
* AVCodecParameters //音视频参数,存放在AVStream结构体里
* enum AVMediaType codec_type; //编码类型,音频还是视频
* enum AVCodecID codec_id; //编码格式
* uint32_t codec_tag; //编码器标签,用不到
* int format; //像素格式或音频采样格式
* int width; int height; //音视频宽高
* uint64_t channel_layout; //声道
* int channels; int sample_rate; int frame_size; //声道数,样本率,一帧音频大小
****************************************************************************************/
/****************************************************************************************
* AVPacket //具体的解封装完之后的数据包,用av_read_frame()函数读取数据包
* AVBufferRef *buf; //指向的空间用来存储引用计数,代码中基本不用管
* int64_t pts; //显示时间pts * (num / den) 乘以时间基数,值可能特别大
* int64_t dts; //解码时间
* uint8_t *data; int size; //数据缓存和数据大小,用ffmpeg提供的接口声请空间
****************************************************************************************/