基於FFmpeg的封裝格式MP4(TS)
一、 封裝MP4原理:
每一幀音訊或視訊都有一個持續時間:duration:
取樣頻率是指將模擬聲音波形進行數字化時,每秒鐘抽取聲波幅度樣本的次數。
。正常人聽覺的頻率範圍大約在20Hz~20kHz之間,根據奈奎斯特取樣理論,為了保證聲音不失真,取樣頻率應該在40kHz左右。常用的音訊取樣頻率有8kHz、
11.025kHz、22.05kHz、16kHz、37.8kHz、44.1kHz、48kHz等,如果採用更高的取樣頻率,還可以達到DVD的音質
對取樣率為44.1kHz的AAC音訊進行解碼時,一幀的解碼時間須控制在23.22毫秒內。
背景知識:
(一個AAC原始幀包含一段時間內1024個取樣及相關資料)
分析:
1) AAC
音訊幀的播放時間=一個AAC幀對應的取樣樣本的個數/取樣頻率(單位為s)
一幀 1024個 sample。取樣率 Samplerate 44100KHz,每秒44100個sample, 所以根據公式 音訊幀的播放時間=一個AAC幀對應的取樣樣本的個數/取樣頻率
當前AAC一幀的播放時間是= 1024*1000000/44100= 22.32ms(單位為ms)
2) MP3
mp3 每幀均為1152個位元組, 則:
frame_duration = 1152 * 1000000 / sample_rate
例如:sample_rate = 44100HZ時,計算出的時長為26.122ms,這就是經常聽到的mp3每幀播放時間固定為26ms的由來。
3)H264
視訊的播放時間跟幀率有關 frame_duration = 1000/fps
例如:fps = 25.00 ,計算出來的時常為40ms,這就是同行所說的40ms一幀視訊資料。
理論上的音視訊(播放)同步是這樣的:
由此得到了每一幀資料的持續時間,音視訊交叉儲存在容器中:一個時間軸:
時間軸:0 22.32 40 44.62 66.96 80 89.16 111.48 120 ................
音 頻 :0 22.32 44.62 66.96 89.16 111.48 ................
視 頻 :0 40 80 120 ................
即視訊的持續時間相加 和音訊的持續時間相加作比較,誰小寫入哪個。
但實際情況(播放)是不成立的
1:首先解決一個問題
為什麼不 音訊播音訊的 視訊播視訊的 即上面的 到 第22.32ms播一幀音訊 ,到40ms播一幀視訊。
因為這個22.32ms 或40ms是算不準的或者說和音效卡播的時間是不一樣的。這裡就需要知道音效卡播一幀/或者說播放一個buf音訊需要多長時間。
2:音效卡每次播一個取樣點 而不是一幀。聲音當一個取樣點丟失了都可以聽出來,視訊則不然。
3:音視訊同步方式:1----回撥方式
假設音效卡有兩塊快取都是存放要播放的聲音pcm的 一直在播放"B"buf 首先確定幾點
(1)buf大小是固定的這樣播放一個buf的時間就是固定的,假設30ms;
(2)當buf“B”播放完畢即buf用完,再播放buf“A",保證音訊pcm一直都連續
(3)當一個buf播放完畢,那說明系統(音效卡)過了30ms, 這時候有可能真正的時間過了40ms(這裡不用關心),這裡則通過回撥得到一次時間30ms;
(4)再去用視訊對應音訊的30ms,這時候的時間就是準確的:
時間軸:0 30 60 90 120 ................
音 頻 :0 22.32 44.62 66.96 89.16 111.48 ................
視 頻 :0 40 80 120 ................
(5)這裡有個問題就是 視訊中 30ms 到40ms 這中間的10ms是怎麼算出來的,這個是不用關心的,因為人的眼睛10ms是看不出來的,
即當音訊的30ms一次回撥時,就可以播放第二幀視訊,如上圖
第一次回撥(30ms)---播(40ms)視訊,
第一次回撥(60ms)---播(80ms)視訊,
第一次回撥(90ms)---不播視訊,
第一次回撥(120ms)---播(120ms)視訊。
4:音視訊同步方式:1----阻塞方式
還是看上面的圖
(1)buf"B"一直在播放,傳入buf"A"的外部buf把資料給buf"A"後 不立即返回,等到buf"B"播放完成再返回,
這時從傳入到經過阻塞出來就是一個buf的時間例如上面的30ms。
(2)然後buf"A"一直在播放,傳入buf"B"的外部buf把資料給buf"B"後 不立即返回,等到buf"A"播放完成再返回,
這時從傳入到經過阻塞出來就是一個buf的時間例如上面的30ms。
(3)迴圈上面(1)(2),即得到了如回撥方式同樣的那個30ms時間。下面和回撥方式一樣,見回撥方式(4)(5)。
二 、基於FFmpeg的封裝格式處理:
本文記錄一個基於FFmpeg的視音訊複用器(Simplest FFmpeg muxer)。視音訊複用器(Muxer)即是將視訊壓縮資料(例如H.264)和音訊壓縮資料(例如AAC)合併到一個封裝格式資料(例如MKV)中去。如圖所示。在這個過程中並不涉及到編碼和解碼。
本文記錄的程式將一個H.264編碼的視訊碼流檔案和一個MP3編碼的音訊碼流檔案,合成為一個MP4封裝格式的檔案。
流程
程式的流程如下圖所示。從流程圖中可以看出,一共初始化了3個AVFormatContext,其中2個用於輸入,1個用於輸出。3個AVFormatContext初始化之後,通過avcodec_copy_context()函式可以將輸入視訊/音訊的引數拷貝至輸出視訊/音訊的AVCodecContext結構體。然後分別呼叫視訊輸入流和音訊輸入流的av_read_frame(),從視訊輸入流中取出視訊的AVPacket,音訊輸入流中取出音訊的AVPacket,分別將取出的AVPacket寫入到輸出檔案中即可。其間用到了一個不太常見的函式av_compare_ts(),是比較時間戳用的。通過該函式可以決定該寫入視訊還是音訊。
本文介紹的視音訊複用器,輸入的視訊不一定是H.264裸流檔案,音訊也不一定是純音訊檔案。可以選擇兩個封裝過的視音訊檔案作為輸入。程式會從視訊輸入檔案中“挑”出視訊流,音訊輸入檔案中“挑”出音訊流,再將“挑選”出來的視音訊流複用起來。
PS1:對於某些封裝格式(例如MP4/FLV/MKV等)中的H.264,需要用到名稱為“h264_mp4toannexb”的bitstream filter。
PS2:對於某些封裝格式(例如MP4/FLV/MKV等)中的AAC,需要用到名稱為“aac_adtstoasc”的bitstream filter。
簡單介紹一下流程中各個重要函式的意義:
avformat_open_input():開啟輸入檔案。
avcodec_copy_context():賦值AVCodecContext的引數。
avformat_alloc_output_context2():初始化輸出檔案。
avio_open():開啟輸出檔案。
avformat_write_header():寫入檔案頭。
av_compare_ts():比較時間戳,決定寫入視訊還是寫入音訊。這個函式相對要少見一些。
av_read_frame():從輸入檔案讀取一個AVPacket。
av_interleaved_write_frame():寫入一個AVPacket到輸出檔案。
av_write_trailer():寫入檔案尾。
程式碼
下面貼上程式碼:
/**
* 最簡單的基於FFmpeg的視音訊複用器
* Simplest FFmpeg Muxer
* 本程式可以將視訊碼流和音訊碼流打包到一種封裝格式中。
* 程式中將AAC編碼的音訊碼流和H.264編碼的視訊碼流打包成
* MPEG2TS封裝格式的檔案。
* 需要注意的是本程式並不改變視音訊的編碼格式。
*
* This software mux a video bitstream and a audio bitstream
* together into a file.
* In this example, it mux a H.264 bitstream (in MPEG2TS) and
* a AAC bitstream file together into MP4 format file.
*
*/
#include <stdio.h>
#define __STDC_CONSTANT_MACROS
#ifdef _WIN32
//Windows
extern "C"
{
#include "libavformat/avformat.h"
};
#else
//Linux...
#ifdef __cplusplus
extern "C"
{
#endif
#include <libavformat/avformat.h>
#ifdef __cplusplus
};
#endif
#endif
/*
FIX: H.264 in some container format (FLV, MP4, MKV etc.) need
"h264_mp4toannexb" bitstream filter (BSF)
*Add SPS,PPS in front of IDR frame
*Add start code ("0,0,0,1") in front of NALU
H.264 in some container (MPEG2TS) don't need this BSF.
*/
//'1': Use H.264 Bitstream Filter
#define USE_H264BSF 0
/*
FIX:AAC in some container format (FLV, MP4, MKV etc.) need
"aac_adtstoasc" bitstream filter (BSF)
*/
//'1': Use AAC Bitstream Filter
#define USE_AACBSF 0
int main(int argc, char* argv[])
{
AVOutputFormat *ofmt = NULL;
//Input AVFormatContext and Output AVFormatContext
AVFormatContext *ifmt_ctx_v = NULL, *ifmt_ctx_a = NULL,*ofmt_ctx = NULL;
AVPacket pkt;
int ret, i;
int videoindex_v=-1,videoindex_out=-1;
int audioindex_a=-1,audioindex_out=-1;
int frame_index=0;
int64_t cur_pts_v=0,cur_pts_a=0;
//const char *in_filename_v = "cuc_ieschool.ts";//Input file URL
const char *in_filename_v = "cuc_ieschool.h264";
//const char *in_filename_a = "cuc_ieschool.mp3";
//const char *in_filename_a = "gowest.m4a";
//const char *in_filename_a = "gowest.aac";
const char *in_filename_a = "huoyuanjia.mp3";
const char *out_filename = "cuc_ieschool.mp4";//Output file URL
av_register_all();
//Input
if ((ret = avformat_open_input(&ifmt_ctx_v, in_filename_v, 0, 0)) < 0) {
printf( "Could not open input file.");
goto end;
}
if ((ret = avformat_find_stream_info(ifmt_ctx_v, 0)) < 0) {
printf( "Failed to retrieve input stream information");
goto end;
}
if ((ret = avformat_open_input(&ifmt_ctx_a, in_filename_a, 0, 0)) < 0) {
printf( "Could not open input file.");
goto end;
}
if ((ret = avformat_find_stream_info(ifmt_ctx_a, 0)) < 0) {
printf( "Failed to retrieve input stream information");
goto end;
}
printf("===========Input Information==========\n");
av_dump_format(ifmt_ctx_v, 0, in_filename_v, 0);
av_dump_format(ifmt_ctx_a, 0, in_filename_a, 0);
printf("======================================\n");
//Output
avformat_alloc_output_context2(&ofmt_ctx, NULL, NULL, out_filename);
if (!ofmt_ctx) {
printf( "Could not create output context\n");
ret = AVERROR_UNKNOWN;
goto end;
}
ofmt = ofmt_ctx->oformat;
for (i = 0; i < ifmt_ctx_v->nb_streams; i++) {
//Create output AVStream according to input AVStream
if(ifmt_ctx_v->streams[i]->codec->codec_type==AVMEDIA_TYPE_VIDEO){
AVStream *in_stream = ifmt_ctx_v->streams[i];
AVStream *out_stream = avformat_new_stream(ofmt_ctx, in_stream->codec->codec);
videoindex_v=i;
if (!out_stream) {
printf( "Failed allocating output stream\n");
ret = AVERROR_UNKNOWN;
goto end;
}
videoindex_out=out_stream->index;
//Copy the settings of AVCodecContext
if (avcodec_copy_context(out_stream->codec, in_stream->codec) < 0) {
printf( "Failed to copy context from input to output stream codec context\n");
goto end;
}
out_stream->codec->codec_tag = 0;
if (ofmt_ctx->oformat->flags & AVFMT_GLOBALHEADER)
out_stream->codec->flags |= CODEC_FLAG_GLOBAL_HEADER;
break;
}
}
for (i = 0; i < ifmt_ctx_a->nb_streams; i++) {
//Create output AVStream according to input AVStream
if(ifmt_ctx_a->streams[i]->codec->codec_type==AVMEDIA_TYPE_AUDIO){
AVStream *in_stream = ifmt_ctx_a->streams[i];
AVStream *out_stream = avformat_new_stream(ofmt_ctx, in_stream->codec->codec);
audioindex_a=i;
if (!out_stream) {
printf( "Failed allocating output stream\n");
ret = AVERROR_UNKNOWN;
goto end;
}
audioindex_out=out_stream->index;
//Copy the settings of AVCodecContext
if (avcodec_copy_context(out_stream->codec, in_stream->codec) < 0) {
printf( "Failed to copy context from input to output stream codec context\n");
goto end;
}
out_stream->codec->codec_tag = 0;
if (ofmt_ctx->oformat->flags & AVFMT_GLOBALHEADER)
out_stream->codec->flags |= CODEC_FLAG_GLOBAL_HEADER;
break;
}
}
printf("==========Output Information==========\n");
av_dump_format(ofmt_ctx, 0, out_filename, 1);
printf("======================================\n");
//Open output file
if (!(ofmt->flags & AVFMT_NOFILE)) {
if (avio_open(&ofmt_ctx->pb, out_filename, AVIO_FLAG_WRITE) < 0) {
printf( "Could not open output file '%s'", out_filename);
goto end;
}
}
//Write file header
if (avformat_write_header(ofmt_ctx, NULL) < 0) {
printf( "Error occurred when opening output file\n");
goto end;
}
//FIX
#if USE_H264BSF
AVBitStreamFilterContext* h264bsfc = av_bitstream_filter_init("h264_mp4toannexb");
#endif
#if USE_AACBSF
AVBitStreamFilterContext* aacbsfc = av_bitstream_filter_init("aac_adtstoasc");
#endif
while (1) {
AVFormatContext *ifmt_ctx;
int stream_index=0;
AVStream *in_stream, *out_stream;
//Get an AVPacket
if(av_compare_ts(cur_pts_v,ifmt_ctx_v->streams[videoindex_v]->time_base,cur_pts_a,ifmt_ctx_a->streams[audioindex_a]->time_base) <= 0){
ifmt_ctx=ifmt_ctx_v;
stream_index=videoindex_out;
if(av_read_frame(ifmt_ctx, &pkt) >= 0){
do{
in_stream = ifmt_ctx->streams[pkt.stream_index];
out_stream = ofmt_ctx->streams[stream_index];
if(pkt.stream_index==videoindex_v){
//FIX:No PTS (Example: Raw H.264)
//Simple Write PTS
if(pkt.pts==AV_NOPTS_VALUE){
//Write PTS
AVRational time_base1=in_stream->time_base;
//Duration between 2 frames (us)
int64_t calc_duration=(double)AV_TIME_BASE/av_q2d(in_stream->r_frame_rate);
//Parameters
pkt.pts=(double)(frame_index*calc_duration)/(double)(av_q2d(time_base1)*AV_TIME_BASE);
pkt.dts=pkt.pts;
pkt.duration=(double)calc_duration/(double)(av_q2d(time_base1)*AV_TIME_BASE);
frame_index++;
}
cur_pts_v=pkt.pts;
break;
}
}while(av_read_frame(ifmt_ctx, &pkt) >= 0);
}else{
break;
}
}else{
ifmt_ctx=ifmt_ctx_a;
stream_index=audioindex_out;
if(av_read_frame(ifmt_ctx, &pkt) >= 0){
do{
in_stream = ifmt_ctx->streams[pkt.stream_index];
out_stream = ofmt_ctx->streams[stream_index];
if(pkt.stream_index==audioindex_a){
//FIX:No PTS
//Simple Write PTS
if(pkt.pts==AV_NOPTS_VALUE){
//Write PTS
AVRational time_base1=in_stream->time_base;
//Duration between 2 frames (us)
int64_t calc_duration=(double)AV_TIME_BASE/av_q2d(in_stream->r_frame_rate);
//Parameters
pkt.pts=(double)(frame_index*calc_duration)/(double)(av_q2d(time_base1)*AV_TIME_BASE);
pkt.dts=pkt.pts;
pkt.duration=(double)calc_duration/(double)(av_q2d(time_base1)*AV_TIME_BASE);
frame_index++;
}
cur_pts_a=pkt.pts;
break;
}
}while(av_read_frame(ifmt_ctx, &pkt) >= 0);
}else{
break;
}
}
//FIX:Bitstream Filter
#if USE_H264BSF
av_bitstream_filter_filter(h264bsfc, in_stream->codec, NULL, &pkt.data, &pkt.size, pkt.data, pkt.size, 0);
#endif
#if USE_AACBSF
av_bitstream_filter_filter(aacbsfc, out_stream->codec, NULL, &pkt.data, &pkt.size, pkt.data, pkt.size, 0);
#endif
//Convert PTS/DTS
pkt.pts = av_rescale_q_rnd(pkt.pts, in_stream->time_base, out_stream->time_base, (AVRounding)(AV_ROUND_NEAR_INF|AV_ROUND_PASS_MINMAX));
pkt.dts = av_rescale_q_rnd(pkt.dts, in_stream->time_base, out_stream->time_base, (AVRounding)(AV_ROUND_NEAR_INF|AV_ROUND_PASS_MINMAX));
pkt.duration = av_rescale_q(pkt.duration, in_stream->time_base, out_stream->time_base);
pkt.pos = -1;
pkt.stream_index=stream_index;
printf("Write 1 Packet. size:%5d\tpts:%lld\n",pkt.size,pkt.pts);
//Write
if (av_interleaved_write_frame(ofmt_ctx, &pkt) < 0) {
printf( "Error muxing packet\n");
break;
}
av_free_packet(&pkt);
}
//Write file trailer
av_write_trailer(ofmt_ctx);
#if USE_H264BSF
av_bitstream_filter_close(h264bsfc);
#endif
#if USE_AACBSF
av_bitstream_filter_close(aacbsfc);
#endif
end:
avformat_close_input(&ifmt_ctx_v);
avformat_close_input(&ifmt_ctx_a);
/* close output */
if (ofmt_ctx && !(ofmt->flags & AVFMT_NOFILE))
avio_close(ofmt_ctx->pb);
avformat_free_context(ofmt_ctx);
if (ret < 0 && ret != AVERROR_EOF) {
printf( "Error occurred.\n");
return -1;
}
return 0;
}
結果
輸入檔案為:
視訊:cuc_ieschool.ts
音訊:huoyuanjia.mp3
輸出檔案為:
cuc_ieschool.mp4
輸出的檔案視訊為“cuc_ieschool”,配合“霍元甲”的音訊。