组合视频流和音频流
通过之前视频流与音频流编解码的学习,我们可以做到将视频流与音频流数据抽离出来,并将这些数据编码为对应的视频或音频。但往往一个多媒体文件中既包含音频也包含视频,所以本次我们学习如何通过 FFmpeg 将 图片+PCM -> mp4 文件
图片+PCM -> mp4 文件,就是将之前 图片->MP4 和 PCM->MP3 的流程组合起来,只不过要注意视频与音频的pts值设置
因为代码量较多,这里选择划分为各个模块进行讲解:基础模块、视频模块、音频模块
基础模块
在基础模块中主要实现编码流程中通用的部分,其余与音频、视频有关的函数均由视频模块、音频模块进行调用
extern"C" {
#include "libavcodec/avcodec.h"
#include "libavformat/avformat.h"
#include"libswresample/swresample.h"
#include "libswscale/swscale.h"
#include "libavutil/imgutils.h"
}
#include <iostream>
#include <opencv2/opencv.hpp>
using namespace std;
using namespace cv;
//通过位运算实现选择测试功能模块 1-测试视频 2-测试音频 3-音视频
#define mode 3
#define VideoMode 0x01
#define AudioMode 0x02
void rgbPcmtoMp4() {
int ret;
//声明所需的变量名
AVFormatContext* dstFmtCtx = NULL;
AVCodec* videoCodec = NULL, * audioCodec = NULL;
AVCodecContext* vCodecCtx = NULL, * aCodecCtx = NULL;
AVStream* vStream = NULL, * aStream = NULL;
//PCM文件
const char* audioFile = "result.pcm";
//最终输出的文件名
const char* dstFile = "result.mp4";
FILE* file=NULL;
do {
//创建输出结构上下文 AVFormatContext,会根据文件后缀创建相应的初始化参数
ret = avformat_alloc_output_context2(&dstFmtCtx, NULL, NULL, dstFile);
if (ret < 0) {
cout << "Could not create output context" << endl;
break;
}
//打开文件
ret = avio_open(&dstFmtCtx->pb, dstFile, AVIO_FLAG_READ_WRITE);
if (ret < 0) {
cout << "Could not open output file" << endl;
break;
}
#if mode & VideoMode
//添加一个视频流
vCodecCtx = addVideoStream(dstFmtCtx, &vStream);
if (vCodecCtx == NULL) {
cout << "Could not add video stream" << endl;
break;
}
#endif
#if mode & AudioMode
//添加一个音频流
aCodecCtx = addAudioStream(dstFmtCtx, &aStream);
if (aCodecCtx == NULL) {
cout << "Could not add audio stream" << endl;
break;
}
#endif
//写入文件头信息
ret = avformat_write_header(dstFmtCtx, NULL);
if (ret != AVSTREAM_INIT_IN_WRITE_HEADER) {
cout << "Write file header fail" << endl;
break;
}
//打印流信息
av_dump_format(dstFmtCtx, 0, dstFile, 1);
#if mode & VideoMode
//编码视频流
ret=encodeVideo(dstFmtCtx,vCodecCtx,vStream);
if (ret<0) {
cout << "encodeVideo fail" << endl;
break;
}
#endif
#if mode & AudioMode
fopen_s(&file,audioFile,"rb");
if (file==NULL) {
cout << "open audio file fail" << endl;
break;
}
//编码音频流
ret=encodeAudio(dstFmtCtx,aCodecCtx,aStream, file);
if (ret < 0) {
cout << "encodeAudio fail" << endl;
break;
}
#endif
//向文件中写入文件尾部标识,并释放该文件
av_write_trailer(dstFmtCtx);
} while (0);
//释放资源
if (dstFmtCtx) {
avformat_free_context(dstFmtCtx);
avio_closep(&dstFmtCtx->pb);
}
if (vCodecCtx) avcodec_free_context(&vCodecCtx);
if (aCodecCtx) avcodec_free_context(&aCodecCtx);
if (file) fclose(file);
}
代码中的方法在之前都有介绍过,这里不再阐述
视频模块
视频模块中又分为 初始化 和 编码 两个部分,视频模块中与之前的视频流编码没有区别,可以当作复习再看一遍
初始化
初始化中主要工作为为文件添加一个视频流,并设置相关的参数,为之后的编码作准备
//宽、高、每幅图像显示的帧数
int w = 600, h = 900, perFrameCnt = 25;
/**
* 添加一个视频流,并初始化AVCodecContext和AVStream
* @param fmtCtx AVFormatContext结构体指针
* @param stream Straem指针的指针,用于在函数中进行赋值
* @return 成功返回创建的AVCodecContext指针,若失败则返回NULL
*/
AVCodecContext* addVideoStream(AVFormatContext* fmtCtx, AVStream** stream) {
int ret = 0;
AVCodecContext* codecCtx = NULL;
do {
//查找编码器
AVCodec* codec = avcodec_find_encoder(fmtCtx->oformat->video_codec);
if (codec == NULL) {
ret = -1;
cout << "Cannot find any endcoder" << endl;
break;
}
//申请编码器上下文结构体
codecCtx = avcodec_alloc_context3(codec);
if (codecCtx == NULL) {
ret = -1;
cout << "Cannot alloc AVCodecContext" << endl;
break;
}
//创建视频流
*stream = avformat_new_stream(fmtCtx, codec);
if (*stream == NULL) {
ret = -1;
cout << "failed create new video stream" << endl;
break;
}
//为视频流设置参数
AVCodecParameters* param = (*stream)->codecpar;
param->width = w;
param->height = h;
param->codec_type = AVMEDIA_TYPE_VIDEO;
param->format = AV_PIX_FMT_YUV420P; //视频帧格式:YUV420P
param->bit_rate = 400000; //码率
//将参数传给解码器上下文
ret = avcodec_parameters_to_context(codecCtx, param);
if (ret < 0) {
cout << "Cannot copy para to context" << endl;
break;
}
// 某些封装格式必须要设置该标志,否则会造成封装后文件中信息的缺失,如:mp4
if (fmtCtx->oformat->flags & AVFMT_GLOBALHEADER) {
codecCtx->flags |= AV_CODEC_FLAG_GLOBAL_HEADER;
}
//gop表示多少个帧中存在一个关键帧
codecCtx->gop_size = 12;
codecCtx->qmin = 10;
codecCtx->qmax = 51;
codecCtx->qcompress = (float)0.6;
//设置时间基
codecCtx->time_base = AVRational{ 1,25 };
//打开解码器
ret = avcodec_open2(codecCtx, codec, NULL);
if (ret < 0) {
cout << "Open encoder failed" << endl;
break;
}
//再将codecCtx设置的参数传给param,用于写入头文件信息
ret = avcodec_parameters_from_context(param, codecCtx);
if (ret < 0) {
cout << "Cannot copy para from context" << endl;
break;
}
} while (0);
//出错则释放资源
if (ret < 0) {
if (codecCtx) avcodec_free_context(&codecCtx);
return NULL;
}
return codecCtx;
}
编码
//执行具体的编码操作
int encodeVideoFrame(AVFormatContext* fmtCtx, AVCodecContext* codecCtx, AVFrame* frame, AVPacket* pkt, AVStream* vStream) {
int ret = 0;
//将frame发送至编码器进行编码,codecCtx中保存了codec
//当frame为NULL时,表示将缓冲区中的数据读取出来
if (avcodec_send_frame(codecCtx, frame) >= 0) {
//接收编码后形成的packet
while (avcodec_receive_packet(codecCtx, pkt) >= 0) {
//设置对应的流索引
pkt->stream_index = vStream->index;
pkt->pos = -1;
//转换pts至基于时间基的pts,可以理解为视频帧显示的时间戳
av_packet_rescale_ts(pkt, codecCtx->time_base, vStream->time_base);
cout << "encoder success pts:" << pkt->pts<< endl;
//将包数据写入文件中,该方法不用使用 av_packet_unref
ret = av_interleaved_write_frame(fmtCtx, pkt);
if (ret < 0) {
char errStr[256];
av_strerror(ret, errStr, 256);
cout << "error is:" << errStr << endl;
break;
}
}
}
return ret;
}
int encodeVideo(AVFormatContext* fmtCtx, AVCodecContext* codecCtx, AVStream* stream) {
int ret = 0;
AVFrame* rgbFrame = NULL, * yuvFrame = NULL;
AVPacket* pkt = av_packet_alloc();
do {
//申请Frame
rgbFrame = av_frame_alloc();
yuvFrame = av_frame_alloc();
//获取相关参数,为之后调用av_frame_get_buffer,需要给Frame赋值
//视频帧需要设置格式、宽、高
int w = codecCtx->width, h = codecCtx->height;
AVPixelFormat format = codecCtx->pix_fmt;
yuvFrame->width = w;
yuvFrame->height = h;
yuvFrame->format = format;
rgbFrame->width = w;
rgbFrame->height = h;
//这里使用BGR类型是因为OpenCV读取图片其格式为BGR
rgbFrame->format = AV_PIX_FMT_BGR24;
//为视频数据分配新的缓冲区,但有几个注意点
ret = av_frame_get_buffer(rgbFrame, 0);
//bgr格式数据是连续的,所以linesize[0]应等于格式所占字节数*图像宽
//但av_frame_get_buffer方法所得到的linesize[0]会大于字节数*图像宽
//需要我们手动设置linesize[0]
rgbFrame->linesize[0] = w*3;
if (ret < 0) {
cout << "rgbFrame get buffer fail" << endl;;
break;
}
//为视频数据分配新的缓冲区
ret = av_frame_get_buffer(yuvFrame, 0);
if (ret < 0) {
cout << "yuvFrame get buffer fail" << endl;;
break;
}
//设置BGR数据转换为YUV的SwsContext
SwsContext* imgCtx = sws_getContext(w, h, (AVPixelFormat)rgbFrame->format,
w, h, format, 0, NULL, NULL, NULL);
//FFmpeg读取图片流程过于复杂,所以使用OpenCV读取图像
//想了解FFmpeg如何读取图像的,可以看看 视频流编码流程(三)
Mat img;
//用于编码为视频帧的图像
char imgPath[] = "img/p0.jpg";
//获取对应一帧BGR图像所需的字节数
int size = av_image_get_buffer_size((AVPixelFormat)rgbFrame->format,w,h,1);
for (int i = 0; i < 7; i++) {
imgPath[5] = '0' + i;
img = imread(imgPath);
//BGR数据填充至图像帧
memcpy(rgbFrame->data[0], img.data, size);
//进行图像格式转换
sws_scale(imgCtx,
rgbFrame->data,
rgbFrame->linesize,
0,
h,
yuvFrame->data,
yuvFrame->linesize);
for (int j = 0; j < perFrameCnt; j++) {
//设置 pts 值,用于度量解码后视频帧位置
yuvFrame->pts = i * perFrameCnt + j;
//将编码过程抽离为一个函数
ret = encodeVideoFrame(fmtCtx, codecCtx, yuvFrame, pkt, stream);
if (ret < 0) {
cout << "Do encodeVideoFrame function Fail" << endl;
break;
}
}
if (ret < 0) break;
}
if (ret < 0) break;
//刷新解码缓存区
ret = encodeVideoFrame(fmtCtx, codecCtx, NULL, pkt, stream);
if (ret < 0) {
cout << "Do encodeVideoFrame function Fail" << endl;
break;
}
} while (0);
//释放使用的资源
if (rgbFrame) av_frame_free(&rgbFrame);
if (yuvFrame) av_frame_free(&yuvFrame);
av_packet_free(&pkt);
return ret;
}
音频模块
音频模块中也分为 初始化 和 编码 两个部分,音频模块大致流程与之前的音频流编码没有区别,只是在音频帧pts设置上需要做一些改变,因为之前编码音频流只需要保证按照顺序播放即可(即pts呈递增趋势),但是加入了画面,就要保证音频与视频播放同步
初始化
初始化中主要工作为为文件添加一个音频流,并设置相关的参数,为之后的编码作准备
static int select_bit_rate(AVCodec* codec) {
// 对于不同的编码器最优码率不一样,单位bit/s;对于mp3来说,192kbps可以获得较好的音质效果。
int bit_rate = 64000;
AVCodecID id = codec->id;
if (id == AV_CODEC_ID_MP3) {
bit_rate = 192000;
}
else if (id == AV_CODEC_ID_AC3) {
bit_rate = 192000;
}
return bit_rate;
}
/**
* 添加一个音频流,并初始化AVCodecContext和AVStream
* @param fmtCtx AVFormatContext结构体指针
* @param stream Straem指针的指针,用于在函数中进行赋值
* @return 成功返回创建的AVCodecContext指针,若失败则返回NULL
*/
AVCodecContext* addAudioStream(AVFormatContext* fmtCtx, AVStream** stream) {
int ret = 0;
AVCodecContext* codecCtx = NULL;
do {
//查找编码器
AVCodec* codec = avcodec_find_encoder(fmtCtx->oformat->audio_codec);
if (codec == NULL) {
ret = -1;
cout << "Cannot find any audio endcoder" << endl;
break;
}
//申请编码器上下文结构体
codecCtx = avcodec_alloc_context3(codec);
if (codecCtx == NULL) {
ret = -1;
cout << "Cannot alloc context" << endl;
break;
}
//设置相关参数
codecCtx->bit_rate = select_bit_rate(codec); // 码率
codecCtx->sample_rate = 48000; // 采样率
codecCtx->sample_fmt = AV_SAMPLE_FMT_FLTP; // 采样格式
codecCtx->channel_layout = AV_CH_LAYOUT_STEREO; // 声道格式
codecCtx->channels = 2; // 声道数
//创建音频流
*stream = avformat_new_stream(fmtCtx, codec);
if (*stream == NULL) {
ret = -1;
cout << "failed create new audio stream" << endl;
break;
}
//打开解码器
ret = avcodec_open2(codecCtx, codec, NULL);
if (ret < 0) {
cout << "avcodec_open2 fail" << endl;
break;
}
AVCodecParameters* param = (*stream)->codecpar;
param->codec_type = AVMEDIA_TYPE_AUDIO;
// 将codecCtx设置的参数传给param,用于写入头文件信息
ret = avcodec_parameters_from_context(param, codecCtx);
if (ret < 0) {
cout << "Cannot copy para from context" << endl;
break;
}
} while (0);
//出错则释放资源
if (ret < 0) {
if (codecCtx) avcodec_free_context(&codecCtx);
return NULL;
}
return codecCtx;
}
编码
//执行具体的编码操作
int encodeAudioFrame(AVFormatContext* fmtCtx, AVCodecContext* codecCtx, AVFrame* frame, AVPacket* pkt, AVStream* aStream) {
int ret = 0;
//将frame发送至编码器进行编码
if (avcodec_send_frame(codecCtx, frame) >= 0) {
//接收编码后形成的packet
while (avcodec_receive_packet(codecCtx, pkt) >= 0) {
//设置对应的流索引
pkt->stream_index = aStream->index;
pkt->pos = -1;
//转换pts至基于时间基的pts
av_packet_rescale_ts(pkt, codecCtx->time_base, aStream->time_base);
cout << "encoder audio success pts:" << pkt->pts << endl;
//将包数据写入文件中,该方法不用使用 av_packet_unref
ret = av_interleaved_write_frame(fmtCtx, pkt);
if (ret < 0) {
char errStr[256];
av_strerror(ret, errStr, 256);
cout << "error is:" << errStr << endl;
break;
}
}
}
return ret;
}
int encodeAudio(AVFormatContext* fmtCtx, AVCodecContext* codecCtx, AVStream* stream, FILE* file) {
int ret = 0;
AVFrame* srcFrame = NULL, * dstFrame = NULL;
AVPacket* pkt = av_packet_alloc();
do {
/*设置音频帧参数
* PCM文件中的参数,PCM文件不携带音频格式信息,需要我们自己记录相关的参数
* 并为srcFrame的参数赋值
* 音频帧调用av_frame_get_buffer分配缓冲区,需要format、nb_samples、channel_layout
*/
srcFrame = av_frame_alloc();
srcFrame->format = AV_SAMPLE_FMT_FLT;
srcFrame->nb_samples = 1152;
srcFrame->channel_layout = AV_CH_LAYOUT_STEREO;
srcFrame->channels = 2;
srcFrame->sample_rate = 44100;
dstFrame = av_frame_alloc();
dstFrame->format = codecCtx->sample_fmt;
dstFrame->nb_samples = codecCtx->frame_size;
dstFrame->channel_layout = codecCtx->channel_layout;
dstFrame->channels = codecCtx->channels;
dstFrame->sample_rate = codecCtx->sample_rate;
//为音频数据分配新的缓冲区
ret = av_frame_get_buffer(srcFrame, 0);
if (ret < 0) {
cout << "frame get buffer fail" << endl;;
break;
}
//使得srcFrame可写
av_frame_make_writable(srcFrame);
ret = av_frame_get_buffer(dstFrame, 0);
if (ret < 0) {
cout << "frame get buffer fail" << endl;;
break;
}
av_frame_make_writable(dstFrame);
// 申请进行对应转换所需的SwrContext
SwrContext* swrCtx = swr_alloc_set_opts(NULL,
codecCtx->channel_layout, codecCtx->sample_fmt, codecCtx->sample_rate,
srcFrame->channel_layout, (enum AVSampleFormat)srcFrame->format, srcFrame->sample_rate,
0, NULL);
int audioPts = 0;
// 获取一帧源数据的字节大小
int require_size = av_samples_get_buffer_size(NULL, srcFrame->channels, srcFrame->nb_samples, (AVSampleFormat)srcFrame->format, 0);
while (1) {
ret = fread(srcFrame->data[0], 1, require_size, file);
if (ret < 0) {
cout << "fread error" << endl;
break;
}
// 进行格式转换
ret = swr_convert_frame(swrCtx, dstFrame, srcFrame);
if (ret < 0) {
cout << "swr_convert_frame fail " << ret << endl;
continue;
}
/*
* 音频:pts = (timebase.den/sample_rate)*[nb_samples*index]
* index为当前第几个音频AVFrame(索引从0开始),nb_samples为每个AVFrame中的采样数
* 但经过swr_convert_frame后,nb_samples会修改并且大小会改变
* 需要一个数记录,而(timebase.den/sample_rate)中的部分会在
* av_packet_rescale_ts 函数中进行转换
*/
dstFrame->pts = audioPts;
audioPts += dstFrame->nb_samples;
ret = encodeAudioFrame(fmtCtx, codecCtx, dstFrame, pkt, stream);
if (ret < 0) {
cout << "Do encodeVideoFrame function Fail 1" << endl;
break;
}
if (feof(file)) {
break;
}
}
if (ret < 0) break;
//刷新转换缓存区
while (swr_convert_frame(swrCtx, dstFrame, NULL) == 0) {
if (dstFrame->nb_samples != 0) {
dstFrame->pts = audioPts;
audioPts += dstFrame->nb_samples;
ret = encodeAudioFrame(fmtCtx, codecCtx, dstFrame, pkt, stream);
if (ret < 0) {
cout << "Do encodeVideoFrame function Fail 2" << endl;
break;
}
}
else {
break;
}
}
//刷新编码缓冲区
ret = encodeAudioFrame(fmtCtx, codecCtx, NULL, pkt, stream);
if (ret < 0) {
cout << "Do encodeVideoFrame function Fail 3" << endl;
break;
}
} while (0);
//释放资源
av_packet_free(&pkt);
av_frame_free(&srcFrame);
av_frame_free(&dstFrame);
return ret;
}
实现效果
通过代码可以知道,案例中编写视频长度应该只有7s,而音频长度取决于PCM文件大小,这里使用的PCM是由1分31秒的mp3文件转换成的,所以音频长度 > 视频长度,最终呈现效果为视频与音频同步播放,7s之后画面呈现视频最后一帧图像,mp4文件长度为音频长度——1分31秒
现在我们已经可以将原始数据转换为mp4文件
加上视频文件的解码,我们就可以做到将一个多媒体文件中的视频流和另一个多媒体文件中的音频流组合在一起形成一个新的多媒体文件了