背景
FFmpeg作为应用最为广泛的音视频解决方案,适用范围及广,接口众多,如果是初学,很容易被众多Context晕得云里雾里,本文为小白教程,主要记录怎么讲原始音视频流封装成标准的MP4格式!
网上搜索的资料大部分都是读取其他格式文件编码成H264或者读取H264文件再封装成MP4!
举个栗子,FFmpeg的sample例子muxing.c就是读取一个gif文件,再编码成H264,然后封装成MP4,如果你只想将已有的原始音视频流封装成MP4的话,参考起来略显吃力!
所有了这个教程!封装MP4只需要三步!
第一、打开冰箱(avformat_write_header)
第二、把大象放进去(av_interleaved_write_frame)
第三、关上冰箱(av_write_trailer)
FFmpeg基础知识
此章资料全部摘自大神 雷霄骅,人生苦短,及时行乐!
FFmpeg常见的结构体
AVFormatContext:统领全局的基本结构体。主要用于处理封装格式(FLV/MKV/RMVB等)。
AVIOContext:输入输出对应的结构体,用于输入输出(读写文件,RTMP协议等)。
AVStream,AVCodecContext:视音频流对应的结构体,用于视音频编解码。
AVFrame:存储非压缩的数据(视频对应RGB/YUV像素数据,音频对应PCM采样数据)
AVPacket:存储压缩数据(视频对应H.264等码流数据,音频对应AAC/MP3等码流数据)
他们之间的关系如下图所示
环境
操作系统:window10
ffmpeg 版本:ffmpeg-4.2.2-win64
视频源文件格式:HEVC(H265)
音频源文件格式:AAC
FFmpeg 封装MP4完整步骤解析
- ## 新建FFmpeg全局Context
创建全局Context指针;指定源文件格式
/* allocate the output media context */
avformat_alloc_output_context2(&pFormatCtx, NULL, NULL, m_Filename);
if (!pFormatCtx) {
HERR("Failed to alloc mp4 avformat ctx !");
return HS_FAILED;
}
/ * Source stream format */
pFormatCtx->oformat->video_codec = AV_CODEC_ID_HEVC;
pFormatCtx->oformat->audio_codec = AV_CODEC_ID_AAC;
添加视频和音频文件流信息
重点解释一下time_base, time_base是时间戳单位,很重要!
ffmpeg定义:#define AV_TIME_BASE 1000000
FFmpeg里使用的基础时间单位是1/1000000,也就是1us(微妙),所有我们所有时间戳都转成微妙数量级!
这里音频和视频使用的不同的表现方式!
视频time_base设置成1微妙,视频帧率25fps即1000000/25=40*1000us,视频时间戳就是这个单位累加;
音频帧率非整数,所以将音频time_base设置成一个音频帧的时间!即10241000/DEFAULT_AUDIO_SAMPLE_RATE1000;所以音频帧就是从零开始累加!
pVStream = avformat_new_stream(pFormatCtx, NULL);
if (!pVStream) {
HERR("Could not allocate pVStream");
return -1;
}
pAStream = avformat_new_stream(pFormatCtx, NULL);
if (!pVStream) {
HERR("Could not allocate pAStream");
return -1;
}
/* Video Stream info */
pVStream->codecpar->codec_id = AV_CODEC_ID_HEVC;
pVStream->codecpar->codec_type = AVMEDIA_TYPE_VIDEO;
pVStream->codecpar->width = DEFAULT_VIDOE_WIDTH;
pVStream->codecpar->height = DEFAULT_VIDOE_HIGHT;
pVStream->time_base.num = 1;
pVStream->time_base.den = 1000000;
/* Audio Stream info */
pAStream->codecpar->codec_id = AV_CODEC_ID_AAC;
pAStream->codecpar->codec_type = AVMEDIA_TYPE_AUDIO;
pAStream->codecpar->channels = 2;
pAStream->codecpar->sample_rate = DEFAULT_AUDIO_SAMPLE_RATE;
pAStream->time_base.num = 1;
pAStream->time_base.den = 1024*1000/DEFAULT_AUDIO_SAMPLE_RATE*1000;
新建Mp4文件
pFmtOutCtx = pFormatCtx->oformat;
/* open the output file, if needed */
if (!(pFmtOutCtx->flags & AVFMT_NOFILE)) {
ret = avio_open(&pFormatCtx->pb, m_Filename, AVIO_FLAG_WRITE);
if (ret < 0) {
HERR("Could not open '%s'", m_Filename);
return HS_FAILED;
}
}
写Mp4封装头信息
这里的实现就是遍历pFormatCtx里的streams的信息添加到MP4头基本信息里
/* Write the stream header, if any. */
ret = avformat_write_header(pFormatCtx, NULL);
if (ret < 0) {
char buf[64] = { 0 };
av_strerror(ret, buf, 64);
HERR("Error occurred when opening output file: %d %s", ret, buf);
return HS_FAILED;
}
写入源文件
这里需要指定时间戳PTS具体原因已经在第二步详细说明!
FFmpeg使用AVPacket表示一帧数据,关键参数如下:
stream_index:流索引,就是第二步添加的Stream,一般是从0开始
pts/dts:视频时间戳和解码时间戳
duration:帧持续时间,这里指的是本帧数据,不是全部文件
flags:标记I帧
data:源文件数据指针
size:源文件数据长度
if(Type == A_FRAME){
m_Packet.stream_index = pAStream->index;
m_Packet.pts = m_AudioIndex++ * 1000;
m_Packet.dts = m_Packet.pts;
m_Packet.duration = 1000;
}else{
m_Packet.stream_index = pVStream->index;
m_Packet.pts = m_VideoIndex++*(1000/DEFAULT_VIDEO_FRAMERATE) * 1000;
m_Packet.dts = m_Packet.pts;
m_Packet.duration = 1000/DEFAULT_VIDEO_FRAMERATE * 1000;
//I Frame
if (Type == I_FRAME) {
m_Packet.flags = AV_PKT_FLAG_KEY;
}
}
m_Packet.data = (uint8_t*)Date;
m_Packet.size = Size;
return av_interleaved_write_frame(pFormatCtx, &m_Packet);
结束文件写入
/* 关闭写MP4 */
av_write_trailer(pFormatCtx);
/* 是否资源 */
avformat_free_context(pFormatCtx);
av_free_packet(&m_Packet);
结果
参考
FFmpeg源代码简单分析:avformat_write_header()
FFmpeg源代码简单分析:常见结构体的初始化和销毁(AVFormatContext,AVFrame等)