組合視頻流和音頻流
通過之前視頻流與音頻流編解碼的學(xué)習(xí),我們可以做到將視頻流與音頻流數(shù)據(jù)抽離出來,并將這些數(shù)據(jù)編碼為對(duì)應(yīng)的視頻或音頻。但往往一個(gè)多媒體文件中既包含音頻也包含視頻,所以本次我們學(xué)習(xí)如何通過 FFmpeg 將 圖片+PCM -> mp4 文件
圖片+PCM -> mp4 文件,就是將之前 圖片->MP4 和 PCM->MP3 的流程組合起來,只不過要注意視頻與音頻的pts值設(shè)置
因?yàn)榇a量較多,這里選擇劃分為各個(gè)模塊進(jìn)行講解:基礎(chǔ)模塊、視頻模塊、音頻模塊
基礎(chǔ)模塊
在基礎(chǔ)模塊中主要實(shí)現(xiàn)編碼流程中通用的部分,其余與音頻、視頻有關(guān)的函數(shù)均由視頻模塊、音頻模塊進(jìn)行調(diào)用
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;
//通過位運(yùn)算實(shí)現(xiàn)選擇測(cè)試功能模塊 1-測(cè)試視頻 2-測(cè)試音頻 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 {
//創(chuàng)建輸出結(jié)構(gòu)上下文 AVFormatContext,會(huì)根據(jù)文件后綴創(chuàng)建相應(yīng)的初始化參數(shù)
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
//添加一個(gè)視頻流
vCodecCtx = addVideoStream(dstFmtCtx, &vStream);
if (vCodecCtx == NULL) {
cout << "Could not add video stream" << endl;
break;
}
#endif
#if mode & AudioMode
//添加一個(gè)音頻流
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
//向文件中寫入文件尾部標(biāo)識(shí),并釋放該文件
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);
}
代碼中的方法在之前都有介紹過,這里不再闡述
視頻模塊
視頻模塊中又分為 初始化 和 編碼 兩個(gè)部分,視頻模塊中與之前的視頻流編碼沒有區(qū)別,可以當(dāng)作復(fù)習(xí)再看一遍
初始化
初始化中主要工作為為文件添加一個(gè)視頻流,并設(shè)置相關(guān)的參數(shù),為之后的編碼作準(zhǔn)備
//寬、高、每幅圖像顯示的幀數(shù)
int w = 600, h = 900, perFrameCnt = 25;
/**
* 添加一個(gè)視頻流,并初始化AVCodecContext和AVStream
* @param fmtCtx AVFormatContext結(jié)構(gòu)體指針
* @param stream Straem指針的指針,用于在函數(shù)中進(jìn)行賦值
* @return 成功返回創(chuàng)建的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;
}
//申請(qǐng)編碼器上下文結(jié)構(gòu)體
codecCtx = avcodec_alloc_context3(codec);
if (codecCtx == NULL) {
ret = -1;
cout << "Cannot alloc AVCodecContext" << endl;
break;
}
//創(chuàng)建視頻流
*stream = avformat_new_stream(fmtCtx, codec);
if (*stream == NULL) {
ret = -1;
cout << "failed create new video stream" << endl;
break;
}
//為視頻流設(shè)置參數(shù)
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; //碼率
//將參數(shù)傳給解碼器上下文
ret = avcodec_parameters_to_context(codecCtx, param);
if (ret < 0) {
cout << "Cannot copy para to context" << endl;
break;
}
// 某些封裝格式必須要設(shè)置該標(biāo)志,否則會(huì)造成封裝后文件中信息的缺失,如:mp4
if (fmtCtx->oformat->flags & AVFMT_GLOBALHEADER) {
codecCtx->flags |= AV_CODEC_FLAG_GLOBAL_HEADER;
}
//gop表示多少個(gè)幀中存在一個(gè)關(guān)鍵幀
codecCtx->gop_size = 12;
codecCtx->qmin = 10;
codecCtx->qmax = 51;
codecCtx->qcompress = (float)0.6;
//設(shè)置時(shí)間基
codecCtx->time_base = AVRational{ 1,25 };
//打開解碼器
ret = avcodec_open2(codecCtx, codec, NULL);
if (ret < 0) {
cout << "Open encoder failed" << endl;
break;
}
//再將codecCtx設(shè)置的參數(shù)傳給param,用于寫入頭文件信息
ret = avcodec_parameters_from_context(param, codecCtx);
if (ret < 0) {
cout << "Cannot copy para from context" << endl;
break;
}
} while (0);
//出錯(cuò)則釋放資源
if (ret < 0) {
if (codecCtx) avcodec_free_context(&codecCtx);
return NULL;
}
return codecCtx;
}
編碼
//執(zhí)行具體的編碼操作
int encodeVideoFrame(AVFormatContext* fmtCtx, AVCodecContext* codecCtx, AVFrame* frame, AVPacket* pkt, AVStream* vStream) {
int ret = 0;
//將frame發(fā)送至編碼器進(jìn)行編碼,codecCtx中保存了codec
//當(dāng)frame為NULL時(shí),表示將緩沖區(qū)中的數(shù)據(jù)讀取出來
if (avcodec_send_frame(codecCtx, frame) >= 0) {
//接收編碼后形成的packet
while (avcodec_receive_packet(codecCtx, pkt) >= 0) {
//設(shè)置對(duì)應(yīng)的流索引
pkt->stream_index = vStream->index;
pkt->pos = -1;
//轉(zhuǎn)換pts至基于時(shí)間基的pts,可以理解為視頻幀顯示的時(shí)間戳
av_packet_rescale_ts(pkt, codecCtx->time_base, vStream->time_base);
cout << "encoder success pts:" << pkt->pts<< endl;
//將包數(shù)據(jù)寫入文件中,該方法不用使用 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 {
//申請(qǐng)F(tuán)rame
rgbFrame = av_frame_alloc();
yuvFrame = av_frame_alloc();
//獲取相關(guān)參數(shù),為之后調(diào)用av_frame_get_buffer,需要給Frame賦值
//視頻幀需要設(shè)置格式、寬、高
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類型是因?yàn)镺penCV讀取圖片其格式為BGR
rgbFrame->format = AV_PIX_FMT_BGR24;
//為視頻數(shù)據(jù)分配新的緩沖區(qū),但有幾個(gè)注意點(diǎn)
ret = av_frame_get_buffer(rgbFrame, 0);
//bgr格式數(shù)據(jù)是連續(xù)的,所以linesize[0]應(yīng)等于格式所占字節(jié)數(shù)*圖像寬
//但av_frame_get_buffer方法所得到的linesize[0]會(huì)大于字節(jié)數(shù)*圖像寬
//需要我們手動(dòng)設(shè)置linesize[0]
rgbFrame->linesize[0] = w*3;
if (ret < 0) {
cout << "rgbFrame get buffer fail" << endl;;
break;
}
//為視頻數(shù)據(jù)分配新的緩沖區(qū)
ret = av_frame_get_buffer(yuvFrame, 0);
if (ret < 0) {
cout << "yuvFrame get buffer fail" << endl;;
break;
}
//設(shè)置BGR數(shù)據(jù)轉(zhuǎn)換為YUV的SwsContext
SwsContext* imgCtx = sws_getContext(w, h, (AVPixelFormat)rgbFrame->format,
w, h, format, 0, NULL, NULL, NULL);
//FFmpeg讀取圖片流程過于復(fù)雜,所以使用OpenCV讀取圖像
//想了解FFmpeg如何讀取圖像的,可以看看 視頻流編碼流程(三)
Mat img;
//用于編碼為視頻幀的圖像
char imgPath[] = "img/p0.jpg";
//獲取對(duì)應(yīng)一幀BGR圖像所需的字節(jié)數(shù)
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數(shù)據(jù)填充至圖像幀
memcpy(rgbFrame->data[0], img.data, size);
//進(jìn)行圖像格式轉(zhuǎn)換
sws_scale(imgCtx,
rgbFrame->data,
rgbFrame->linesize,
0,
h,
yuvFrame->data,
yuvFrame->linesize);
for (int j = 0; j < perFrameCnt; j++) {
//設(shè)置 pts 值,用于度量解碼后視頻幀位置
yuvFrame->pts = i * perFrameCnt + j;
//將編碼過程抽離為一個(gè)函數(shù)
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;
//刷新解碼緩存區(qū)
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;
}
音頻模塊
音頻模塊中也分為 初始化 和 編碼 兩個(gè)部分,音頻模塊大致流程與之前的音頻流編碼沒有區(qū)別,只是在音頻幀pts設(shè)置上需要做一些改變,因?yàn)橹熬幋a音頻流只需要保證按照順序播放即可(即pts呈遞增趨勢(shì)),但是加入了畫面,就要保證音頻與視頻播放同步
初始化
初始化中主要工作為為文件添加一個(gè)音頻流,并設(shè)置相關(guān)的參數(shù),為之后的編碼作準(zhǔn)備
static int select_bit_rate(AVCodec* codec) {
// 對(duì)于不同的編碼器最優(yōu)碼率不一樣,單位bit/s;對(duì)于mp3來說,192kbps可以獲得較好的音質(zhì)效果。
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;
}
/**
* 添加一個(gè)音頻流,并初始化AVCodecContext和AVStream
* @param fmtCtx AVFormatContext結(jié)構(gòu)體指針
* @param stream Straem指針的指針,用于在函數(shù)中進(jìn)行賦值
* @return 成功返回創(chuàng)建的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;
}
//申請(qǐng)編碼器上下文結(jié)構(gòu)體
codecCtx = avcodec_alloc_context3(codec);
if (codecCtx == NULL) {
ret = -1;
cout << "Cannot alloc context" << endl;
break;
}
//設(shè)置相關(guān)參數(shù)
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; // 聲道數(shù)
//創(chuàng)建音頻流
*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設(shè)置的參數(shù)傳給param,用于寫入頭文件信息
ret = avcodec_parameters_from_context(param, codecCtx);
if (ret < 0) {
cout << "Cannot copy para from context" << endl;
break;
}
} while (0);
//出錯(cuò)則釋放資源
if (ret < 0) {
if (codecCtx) avcodec_free_context(&codecCtx);
return NULL;
}
return codecCtx;
}
編碼
//執(zhí)行具體的編碼操作
int encodeAudioFrame(AVFormatContext* fmtCtx, AVCodecContext* codecCtx, AVFrame* frame, AVPacket* pkt, AVStream* aStream) {
int ret = 0;
//將frame發(fā)送至編碼器進(jìn)行編碼
if (avcodec_send_frame(codecCtx, frame) >= 0) {
//接收編碼后形成的packet
while (avcodec_receive_packet(codecCtx, pkt) >= 0) {
//設(shè)置對(duì)應(yīng)的流索引
pkt->stream_index = aStream->index;
pkt->pos = -1;
//轉(zhuǎn)換pts至基于時(shí)間基的pts
av_packet_rescale_ts(pkt, codecCtx->time_base, aStream->time_base);
cout << "encoder audio success pts:" << pkt->pts << endl;
//將包數(shù)據(jù)寫入文件中,該方法不用使用 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 {
/*設(shè)置音頻幀參數(shù)
* PCM文件中的參數(shù),PCM文件不攜帶音頻格式信息,需要我們自己記錄相關(guān)的參數(shù)
* 并為srcFrame的參數(shù)賦值
* 音頻幀調(diào)用av_frame_get_buffer分配緩沖區(qū),需要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;
//為音頻數(shù)據(jù)分配新的緩沖區(qū)
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);
// 申請(qǐng)進(jìn)行對(duì)應(yīng)轉(zhuǎn)換所需的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;
// 獲取一幀源數(shù)據(jù)的字節(jié)大小
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;
}
// 進(jìn)行格式轉(zhuǎn)換
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為當(dāng)前第幾個(gè)音頻AVFrame(索引從0開始),nb_samples為每個(gè)AVFrame中的采樣數(shù)
* 但經(jīng)過swr_convert_frame后,nb_samples會(huì)修改并且大小會(huì)改變
* 需要一個(gè)數(shù)記錄,而(timebase.den/sample_rate)中的部分會(huì)在
* av_packet_rescale_ts 函數(shù)中進(jìn)行轉(zhuǎn)換
*/
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;
//刷新轉(zhuǎn)換緩存區(qū)
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;
}
}
//刷新編碼緩沖區(qū)
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;
}
實(shí)現(xiàn)效果
通過代碼可以知道,案例中編寫視頻長(zhǎng)度應(yīng)該只有7s,而音頻長(zhǎng)度取決于PCM文件大小,這里使用的PCM是由1分31秒的mp3文件轉(zhuǎn)換成的,所以音頻長(zhǎng)度 > 視頻長(zhǎng)度,最終呈現(xiàn)效果為視頻與音頻同步播放,7s之后畫面呈現(xiàn)視頻最后一幀圖像,mp4文件長(zhǎng)度為音頻長(zhǎng)度——1分31秒
現(xiàn)在我們已經(jīng)可以將原始數(shù)據(jù)轉(zhuǎn)換為mp4文件
加上視頻文件的解碼,我們就可以做到將一個(gè)多媒體文件中的視頻流和另一個(gè)多媒體文件中的音頻流組合在一起形成一個(gè)新的多媒體文件了