FFmpeg可能是當今視/音頻領(lǐng)域應(yīng)用最為廣泛的開源項目了愉舔,國內(nèi)許多著名的影音程序或多或少地都用了它的代碼。作為視/音頻領(lǐng)域研究或開發(fā)的人光羞,無論如何都不應(yīng)該錯過這個項目逆航。本文就拿FFmpeg示例程序中的muxing.c文件美旧,來對FFmpeg的使用作一篇簡要介紹渤滞。胖兔的老習(xí)慣,仍然是直接從代碼開擼榴嗅。先看文件中定義的OutputSteam數(shù)據(jù)結(jié)構(gòu):
typedef struct OutputStream {
AVStream *st; //視頻或音頻流
AVCodecContext *enc; //編碼配置
int64_t next_pts; //下一幀的PTS妄呕,用于視/音頻同步
int samples_count; //聲音采樣計數(shù)
AVFrame *frame; //視頻/音頻幀
AVFrame *tmp_frame; //臨時幀
float t, tincr, tincr2; //用于聲音生成
struct SwsContext *sws_ctx; //視頻轉(zhuǎn)換配置
struct SwrContext *swr_ctx; //聲音重采樣配置
} OutputStream;
對視/音頻文件的操作,實際上都是針對視頻/音頻流來進行的嗽测。這個OutputStream類就是用于操作視頻/音頻流的包裝類绪励。
接下來從主函數(shù)開始,按順序梳理整個代碼流程:
if (argc < 2) {
printf("usage: %s output_file\n"
"API example program to output a media file with libavformat.\n"
"This program generates a synthetic audio and video stream, encodes and\n"
"muxes them into a file named output_file.\n"
"The output format is automatically guessed according to the file extension.\n"
"Raw images can also be output by using '%%d' in the filename.\n"
"\n", argv[0]);
return 1;
}
這里介紹了示例程序的功能和使用方法唠粥。運行本程序的時候要帶一個輸出文件名參數(shù)疏魏,然后程序?qū)⑸梢粋€同步的視頻和音頻流,編碼復(fù)用到指定的文件中去晤愧。輸出的格式是根據(jù)給定的文件擴展名自動猜取的大莫。
filename = argv[1];
for (i = 2; i+1 < argc; i+=2) {
if (!strcmp(argv[i], "-flags") || !strcmp(argv[i], "-fflags"))
av_dict_set(&opt, argv[i]+1, argv[i+1], 0);
}
這里檢查程序啟動有沒有帶其他參數(shù),有的話納入到參數(shù)字典养涮。對于不甚精通音/視頻技術(shù)的初學(xué)者來說葵硕,這里直接忽略就好了眉抬。
avformat_alloc_output_context2(&oc, NULL, NULL, filename);
if (!oc) {
printf("Could not deduce output format from file extension: using MPEG.\n");
avformat_alloc_output_context2(&oc, NULL, "mpeg", filename);
}
fmt = oc->oformat;
這里初始化了AVFormatContext(格式配置)贯吓,它在FFmpeg程序里是貫穿始終的一個類,非常重要蜀变。注意avformat_alloc_output_context2這個函數(shù)悄谐,它的第二個參數(shù)可以是一個AVFormat實例,用來決定視頻/音頻格式库北,如果被設(shè)為NULL就繼續(xù)看第三個參數(shù)爬舰,這是一個描述格式的字符串们陆,比如可以是“h264"、 "mpeg"等情屹;如果它也是NULL坪仇,就看最后第四個filename,從它的擴展名來推斷應(yīng)該使用的格式垃你。比如用戶指定的文件名是”test.avi"椅文,就會使用普通的AVI格式。有人說那我用h264格式但文件名就想用.avi行不行惜颇,當然可以皆刺,把第三個參數(shù)設(shè)為"h264"就行了,這時就不會從文件名來推斷格式了凌摄。
if (fmt->video_codec != AV_CODEC_ID_NONE) {
add_stream(&video_st, oc, &video_codec, fmt->video_codec);
have_video = 1;
encode_video = 1;
}
if (fmt->audio_codec != AV_CODEC_ID_NONE) {
add_stream(&audio_st, oc, &audio_codec, fmt->audio_codec);
have_audio = 1;
encode_audio = 1;
}
接下來根據(jù)推斷出的格式添加視頻/音頻流羡蛾。如果給定的是"mp4"這樣的格式,默認是既有視頻也有音頻锨亏;如果給定的是"mp3"痴怨,那就只有音頻沒有視頻了。我們暫停一下main函數(shù)屯伞,去看看add_stream函數(shù)是如何定義的腿箩,注意筆者添加的中文注釋(代碼有刪節(jié),便于突出主要流程劣摇。本文后續(xù)其他代碼同樣處理):
void add_stream(OutputStream *ost, AVFormatContext *oc,
AVCodec **codec, enum AVCodecID codec_id)
{
//根據(jù)推斷出的格式珠移,尋找相應(yīng)的AVCodec編碼
*codec = avcodec_find_encoder(codec_id);
//分配一個視頻/音頻流,這里的ost就是本文一開頭分析的OutputStream結(jié)構(gòu)數(shù)據(jù)
ost->st = avformat_new_stream(oc, NULL);
//設(shè)定流ID號末融,與流在文件中的序號對應(yīng)(一個文件中可以有多個視頻/音頻流)
ost->st->id = oc->nb_streams-1;
//分配CodecContext編碼上下文钧惧,存入OutputStream結(jié)構(gòu)
AVCodecContext *c = avcodec_alloc_context3(*codec);
ost->enc = c;
//根據(jù)視頻、音頻不同類型勾习,初始化CodecContext編碼配置
switch ((*codec)->type) {
case AVMEDIA_TYPE_AUDIO: //這部分是音頻數(shù)據(jù)
c->sample_fmt = (*codec)->sample_fmts ? (*codec)->sample_fmts[0] : AV_SAMPLE_FMT_FLTP; //采樣格式
c->bit_rate = 64000; //碼率
c->sample_rate = 44100; //采樣速率
c->channels = av_get_channel_layout_nb_channels(c->channel_layout); //聲道數(shù)
c->channel_layout = AV_CH_LAYOUT_STEREO; //聲道布局
c->channels = av_get_channel_layout_nb_channels(c->channel_layout);
ost->st->time_base = (AVRational){ 1, c->sample_rate }; //計時基準
break;
case AVMEDIA_TYPE_VIDEO: //這部分是視頻數(shù)據(jù)
c->codec_id = codec_id; //視頻編碼
c->bit_rate = 400000; //碼率
c->width = 352; //視頻寬高浓瞪,注意必須是雙數(shù),YUV420P格式要求
c->height = 288;
ost->st->time_base = (AVRational){ 1, STREAM_FRAME_RATE }; //計時基準
c->time_base = ost->st->time_base;
c->gop_size = 12;
c->pix_fmt = STREAM_PIX_FMT;
break;
}
//是否需要分離的Stream Header
if (oc->oformat->flags & AVFMT_GLOBALHEADER)
c->flags |= AV_CODEC_FLAG_GLOBAL_HEADER;
}
敲黑板巧婶!這里有重點乾颁!我們知道做視/音頻程序,必須要考慮視頻和音頻數(shù)據(jù)的同步問題艺栈,技術(shù)上怎么實現(xiàn)英岭?請看上面代碼中的time_base,用來設(shè)定計時基準湿右,它來源于圖像/聲音采集原理诅妹。對于視頻,我們知道人眼視覺殘留的時間是1/24秒,視頻只要達到每秒24幀以上人就不會覺得有閃爍或卡頓吭狡,一般會設(shè)成25尖殃,也就是代碼中的STREAM_FRAME_RATE常數(shù),視頻time_base設(shè)為1/25划煮,也就是每一個視頻幀停留1/25秒送丰。再看音頻,聲音的采樣是指一秒內(nèi)采集多少次聲音數(shù)據(jù)弛秋,采樣頻率越高聲音質(zhì)量越好蚪战,44.1kHz就可以達到CD音響質(zhì)量,也是MPEG標準聲音質(zhì)量铐懊。那么它的基準就是1/44100邀桑。
繼續(xù)接著看main函數(shù):
if (have_video)
open_video(oc, video_codec, &video_st, opt);
if (have_audio)
open_audio(oc, audio_codec, &audio_st, opt);
所有參數(shù)都設(shè)好了,可以打開視頻/音頻編碼科乎,分配必要的緩沖區(qū)了壁畸。先看open_video函數(shù):
void open_video(AVFormatContext *oc, AVCodec *codec, OutputStream *ost, AVDictionary *opt_arg)
{
AVCodecContext *c = ost->enc;
AVDictionary *opt = NULL;
//拷貝用戶設(shè)定的參數(shù)字典
av_dict_copy(&opt, opt_arg, 0);
//打開編碼器,隨后釋放參數(shù)字典
avcodec_open2(c, codec, &opt);
av_dict_free(&opt);
//分配并初始化一個可重復(fù)使用的視頻幀茅茂,指定好像素點格式和寬高
ost->frame = alloc_picture(c->pix_fmt, c->width, c->height);
//如果輸出格式不是YUV420P捏萍,那么需要一個臨時的YUV420P幀便于進行轉(zhuǎn)換
ost->tmp_frame = NULL;
if (c->pix_fmt != AV_PIX_FMT_YUV420P) {
ost->tmp_frame = alloc_picture(AV_PIX_FMT_YUV420P, c->width, c->height);
}
//從CodecContext中拷貝參數(shù)到流/復(fù)用器
avcodec_parameters_from_context(ost->st->codecpar, c);
}
上面的代碼使用了alloc_picture函數(shù)來分配視頻幀。這個函數(shù)是這樣定義的:
AVFrame *alloc_picture(enum AVPixelFormat pix_fmt, int width, int height)
{
AVFrame *picture = av_frame_alloc();
picture->format = pix_fmt;
picture->width = width;
picture->height = height;
//分配幀數(shù)據(jù)緩沖區(qū)
av_frame_get_buffer(picture, 32);
return picture;
}
注意av_frame_get_buffer函數(shù)空闲,它為幀數(shù)據(jù)分配緩沖區(qū)令杈,第二個參數(shù)32用于對齊,如果搞不清楚怎么設(shè)的話碴倾,直接設(shè)為0就行逗噩,F(xiàn)Fmpeg會自動處理。
視頻打開了跌榔。接著看打開音頻的open_audio函數(shù):
void open_audio(AVFormatContext *oc, AVCodec *codec, OutputStream *ost, AVDictionary *opt_arg)
{
int nb_samples;
AVDictionary *opt = NULL;
AVCodecContext *c = ost->enc;
//拷貝參數(shù)字典
av_dict_copy(&opt, opt_arg, 0);
//打開編碼器异雁,釋放參數(shù)字典
avcodec_open2(c, codec, &opt);
av_dict_free(&opt);
//初始化信號生成器,用于聲音自動生成
ost->t = 0;
ost->tincr = 2 * M_PI * 110.0 / c->sample_rate;
ost->tincr2 = 2 * M_PI * 110.0 / c->sample_rate / c->sample_rate;
//采樣大小僧须。如果幀大小固定纲刀,則為frame_size
if (c->codec->capabilities & AV_CODEC_CAP_VARIABLE_FRAME_SIZE)
nb_samples = 10000;
else
nb_samples = c->frame_size;
//分配音頻幀和臨時音頻幀
ost->frame = alloc_audio_frame(c->sample_fmt, c->channel_layout, c->sample_rate, nb_samples);
ost->tmp_frame = alloc_audio_frame(AV_SAMPLE_FMT_S16, c->channel_layout, c->sample_rate, nb_samples);
//從CodecContext中拷貝參數(shù)到流/復(fù)用器
avcodec_parameters_from_context(ost->st->codecpar, c);
//創(chuàng)建重采樣配置,設(shè)定聲道數(shù)担平、輸入輸出采樣率示绊、采樣格式等選項
ost->swr_ctx = swr_alloc();
av_opt_set_int(ost->swr_ctx, "in_channel_count", c->channels, 0);
av_opt_set_int(ost->swr_ctx, "in_sample_rate", c->sample_rate, 0);
av_opt_set_sample_fmt(ost->swr_ctx, "in_sample_fmt", AV_SAMPLE_FMT_S16, 0);
av_opt_set_int(ost->swr_ctx, "out_channel_count", c->channels, 0);
av_opt_set_int(ost->swr_ctx, "out_sample_rate", c->sample_rate, 0);
av_opt_set_sample_fmt(ost->swr_ctx, "out_sample_fmt", c->sample_fmt, 0);
swr_init(ost->swr_ctx);
}
分配音頻幀使用了alloc_audio_frame函數(shù):
AVFrame *alloc_audio_frame(enum AVSampleFormat sample_fmt,
uint64_t channel_layout, int sample_rate, int nb_samples)
{
AVFrame *frame = av_frame_alloc();
frame->format = sample_fmt; //采樣格式
frame->channel_layout = channel_layout; //聲道布局
frame->sample_rate = sample_rate; //采樣率
frame->nb_samples = nb_samples; //采樣大小
if (nb_samples) {
av_frame_get_buffer(frame, 0);
}
return frame;
}
現(xiàn)在視頻/音頻都設(shè)定好了,回到main函數(shù)暂论,接下來看看格式設(shè)定是否正確:
av_dump_format(oc, 0, filename, 1);
這一行在命令行下導(dǎo)出當前格式設(shè)定面褐,執(zhí)行以后輸出示例是這樣的:
可以看到,我們設(shè)定的輸出文件名是test2.mp4空另,文件中包含兩個流盆耽,一個視頻流蹋砚,H264格式扼菠,幀格式Y(jié)UV420P摄杂,幀大小352*288;另一個音頻流循榆,AAC格式析恢,采樣率44.1kHz,立體聲秧饮。
繼續(xù)往下看:
//打開輸出文件
avio_open(&oc->pb, filename, AVIO_FLAG_WRITE);
//輸出流的頭部
avformat_write_header(oc, &opt);
OK映挂,現(xiàn)在萬事俱備,只欠寫入了盗尸。接著看視頻和音頻數(shù)據(jù)是如何寫入的:
while (encode_video || encode_audio) {
if (encode_video && (!encode_audio ||
av_compare_ts(video_st.next_pts, video_st.enc->time_base,
audio_st.next_pts, audio_st.enc->time_base) <= 0)) {
encode_video = !write_video_frame(oc, &video_st);
} else {
encode_audio = !write_audio_frame(oc, &audio_st);
}
}
這里值得注意的還是視/音頻同步問題柑船。寫入文件的時候,什么時候?qū)懸曨l幀泼各,什么時候?qū)懸纛l幀鞍时?代碼給出了一個辦法,在有視頻無音頻扣蜻,或者視頻時間戳落后于音頻的時候就寫視頻幀逆巍,否則就寫入音頻幀。av_compare_ts函數(shù)用來進行時間戳(Timestamp)比較莽使。
接著看視頻幀是怎么寫入的:
int write_video_frame(AVFormatContext *oc, OutputStream *ost)
{
AVCodecContext *c = ost->enc;
//生成視頻幀
AVFrame *frame = get_video_frame(ost);
int got_packet = 0;
AVPacket pkt = { 0 };
//初始化數(shù)據(jù)包
av_init_packet(&pkt);
//將視頻幀編碼壓入數(shù)據(jù)包
avcodec_encode_video2(c, &pkt, frame, &got_packet);
if (got_packet) {
//如果有數(shù)據(jù)包生成锐极,則寫入流
ret = write_frame(oc, &c->time_base, ost->st, &pkt);
} else {
ret = 0;
}
return (frame || got_packet) ? 0 : 1;
}
流程比較一目了然。先看get_video_frame函數(shù)是如何生成視頻幀的:
AVFrame *get_video_frame(OutputStream *ost)
{
AVCodecContext *c = ost->enc;
//檢查是否繼續(xù)生成視頻幀芳肌。如果超過預(yù)定時長就停止生成
if (av_compare_ts(ost->next_pts, c->time_base,
STREAM_DURATION, (AVRational){ 1, 1 }) >= 0)
return NULL;
//使幀數(shù)據(jù)可寫灵再,此處視頻數(shù)據(jù)是代碼生成的,注意frame指針本身不可以修改
//因為FFmpeg內(nèi)部會引用這個指針亿笤,一旦改了可能會破壞視頻
av_frame_make_writable(ost->frame)檬嘀;
//如果目標格式不是YUV420P,那么必須要進行格式轉(zhuǎn)換
if (c->pix_fmt != AV_PIX_FMT_YUV420P) {
if (!ost->sws_ctx) { //先獲取轉(zhuǎn)換環(huán)境
ost->sws_ctx = sws_getContext(c->width, c->height, AV_PIX_FMT_YUV420P,
c->width, c->height, c->pix_fmt, SCALE_FLAGS, NULL, NULL, NULL);
}
//向臨時幀填充數(shù)據(jù)责嚷,之后轉(zhuǎn)換填入當前幀
fill_yuv_image(ost->tmp_frame, ost->next_pts, c->width, c->height);
sws_scale(ost->sws_ctx, (const uint8_t * const *) ost->tmp_frame->data,
ost->tmp_frame->linesize, 0, c->height, ost->frame->data, ost->frame->linesize);
} else {
//目標格式就是YUV420P鸳兽,直接轉(zhuǎn)換就可以
fill_yuv_image(ost->frame, ost->next_pts, c->width, c->height);
}
//新幀生成,PTS遞增
ost->frame->pts = ost->next_pts++;
return ost->frame;
}
測試程序的視頻幀是由代碼生成的罕拂,具體在fill_yuv_image函數(shù)里:
void fill_yuv_image(AVFrame *pict, int frame_index, int width, int height)
{
int x, y, i;
i = frame_index;
//生成Y
for (y = 0; y < height; y++)
for (x = 0; x < width; x++)
pict->data[0][y * pict->linesize[0] + x] = x + y + i * 3;
//生成Cb和Cr
for (y = 0; y < height / 2; y++) {
for (x = 0; x < width / 2; x++) {
pict->data[1][y * pict->linesize[1] + x] = 128 + y + i * 2;
pict->data[2][y * pict->linesize[2] + x] = 64 + x + i * 5;
}
}
}
這里用代碼揍异,按照一定規(guī)律填寫YUV420P格式的數(shù)據(jù),注意下面的循環(huán)爆班,可以明白為什么視頻的寬和高必須是2的倍數(shù)了吧衷掷。
回到視頻幀寫入函數(shù)write_video_frame,它接下來調(diào)用了avcodec_encode_video2函數(shù)柿菩,將幀數(shù)據(jù)編碼壓入數(shù)據(jù)包戚嗅。然而,這個函數(shù)在最新版FFmpeg里已經(jīng)被廢棄了,新版本采用了更加靈活的編碼方式懦胞。胖兔采用的是以下修改后的代碼:
ret = avcodec_send_frame(c, frame); //將幀送入編碼配置上下文
while (ret >= 0) {
ret = avcodec_receive_packet(c, &pkt); //循環(huán)接收數(shù)據(jù)包替久,直到所有包接收完成
if (ret == AVERROR(EAGAIN) || ret == AVERROR_EOF)
break;
write_frame(oc, &c->time_base, ost->st, &pkt);
}
對比一下新老代碼,可以看到老代碼是比較死的躏尉,送一幀進去蚯根,只能接收一個包出來;新代碼則允許送一幀進去胀糜,接收N個包出來颅拦。這樣能夠有效避免因為幀編碼壓縮延遲導(dǎo)致數(shù)據(jù)包滯留的問題。
收到數(shù)據(jù)包之后教藻,要把它寫入視頻流距帅,使用的write_frame函數(shù):
int write_frame(AVFormatContext *fmt_ctx, const AVRational *time_base, AVStream *st, AVPacket *pkt)
{
//轉(zhuǎn)換時間戳,由數(shù)據(jù)包向視頻流
av_packet_rescale_ts(pkt, *time_base, st->time_base);
pkt->stream_index = st->index; //指明包屬于哪個流
return av_interleaved_write_frame(fmt_ctx, pkt); //將包寫入流
}
OK括堤,到這里視頻寫入就結(jié)束了锥债。音頻的采集與編碼過程與之類似。這里不再詳細解析了痊臭,具體可以參見示例程序代碼哮肚。
回到main函數(shù),完成視頻/音頻寫入以后广匙,最后還需要做的就是收尾工作:
av_write_trailer(oc); //寫尾部
if (have_video)
close_stream(oc, &video_st); //關(guān)閉視頻流
if (have_audio)
close_stream(oc, &audio_st); //關(guān)閉音頻流
avio_closep(&oc->pb); //關(guān)閉輸出文件
avformat_free_context(oc); //釋放格式配置上下文
解析結(jié)束允趟。希望對需要的人有所幫助。
最后貼一張程序生成的視頻動圖(壓縮后效果一般鸦致,勉強鎮(zhèn)樓潮剪,湊合看看效果吧)。
碼農(nóng)也精彩分唾!