FFmpeg源碼世界:命令篇
前言
最近在做一些音視頻領(lǐng)域的工作,這個(gè)領(lǐng)域基本繞不開 FFmpeg ,因此想對其源碼進(jìn)行一些研究,站著這個(gè)巨人的肩膀上,學(xué)習(xí)一下其設(shè)計(jì)思想以及實(shí)現(xiàn)思路,F(xiàn)Fmpeg 很多人最開始接觸的應(yīng)該都是它的命令和 ffplay ,這篇我們就先分析下 ffmpeg 命令 的實(shí)現(xiàn)。
從問題出發(fā)
FFmpeg的命令結(jié)構(gòu)是什么樣的 怎么實(shí)現(xiàn)任意功能、配置的隨意組裝的 倒放這種非線性的情況是怎么處理的 *當(dāng)不需要轉(zhuǎn)碼類似只需要轉(zhuǎn)封裝的話是如何處理的
命令結(jié)構(gòu)
ffmpeg [global_options] {[input_file_options] -i input_url} ... {[output_file_options] output_url} ...
流程推測
應(yīng)該要有一個(gè)數(shù)據(jù)層,按照輸入?yún)?shù)進(jìn)行數(shù)據(jù)解析 將解碼、轉(zhuǎn)碼、功能處理、編碼四個(gè)模塊抽象分開 按第一個(gè)環(huán)節(jié)數(shù)據(jù)解析的結(jié)構(gòu)組裝上述四個(gè)模塊,形成一條或多條責(zé)任鏈的效果 開始處理
過程中應(yīng)該有很多分支判斷、參數(shù)設(shè)置之類的情況,在這里都不考慮,我們先只分析主鏈路。
主流程
首先 ffmpeg 命令的入口在于 fftools/ffmpeg.c 的 main 函數(shù)上,先從這個(gè)函數(shù)來看下整體的流程,代碼用的是 ffmpeg 4.4 的版本,同時(shí)會(huì)省略一些無關(guān)緊要的內(nèi)容:
int main(int argc, char **argv)
{
/* 一些前置的初始化動(dòng)作 */
...
/* 數(shù)據(jù)解析函數(shù),不過在這個(gè)函數(shù)里面還會(huì)去開啟輸入/輸出文件流等處理 */
ret = ffmpeg_parse_options(argc, argv);
/* 一些數(shù)據(jù)判斷、開啟基準(zhǔn)測試之類的處理 */
...
/* 開始做實(shí)際的文件轉(zhuǎn)換,即按命令開始處理了 */
if (transcode() < 0)
...
/* 結(jié)束了,統(tǒng)計(jì)耗時(shí)等等 */
...
}
從上述代碼可以看到核心的環(huán)節(jié)就是兩部分:數(shù)據(jù)解析 + 文件轉(zhuǎn)換,接下來我們將針對這兩部分再深入進(jìn)去看看。
接下去我們分析過程中使用的命令如下:
ffmpeg -i douyin_700x1240.mp4 -vf reverse -af areverse reversed.mp4
上述命令是將一個(gè) douyin_700x1240.mp4 這個(gè)視頻中的視頻流于音頻流同時(shí)倒放,生成 reversed.mp4。
數(shù)據(jù)解析
在分析 ffmpeg_parse_options 函數(shù)之前,我們先介紹下 FFmpeg 命令中參數(shù)配置相關(guān)的幾個(gè)主要的結(jié)構(gòu)體:
OptionParseContext
typedef struct OptionParseContext {
OptionGroup global_opts; // 全局配置參數(shù)集
OptionGroupList *groups; // 輸出/輸入配置參數(shù)集
int nb_groups; // groups的長度
/* parsing state */
OptionGroup cur_group; // 下面講解到 add_opt 的時(shí)候會(huì)提到
} OptionParseContext;
該對象用來存儲(chǔ)被解析后的數(shù)據(jù),全局參數(shù),輸入輸出參數(shù)等。
OptionGroupList
/**
* A list of option groups that all have the same group type
* (e.g. input files or output files)
*/
typedef struct OptionGroupList {
const OptionGroupDef *group_def;
OptionGroup *groups;
int nb_groups;
} OptionGroupList;
上面的注釋已經(jīng)解釋得比較清楚了。
OptionGroup
typedef struct OptionGroup {
const OptionGroupDef *group_def;
const char *arg;
Option *opts;
int nb_opts;
AVDictionary *codec_opts;
AVDictionary *format_opts;
AVDictionary *resample_opts;
AVDictionary *sws_dict;
AVDictionary *swr_opts;
} OptionGroup;
這里就是各類參數(shù)的存儲(chǔ)地了,再往下就是單個(gè)配置參數(shù)的存儲(chǔ)地 Option 了。
Option
/**
* An option extracted from the commandline.
* Cannot use AVDictionary because of options like -map which can be
* used multiple times.
*/
typedef struct Option {
const OptionDef *opt;
const char *key;
const char *val;
} Option;
就是配置的鍵值對和定義規(guī)范了。
OptionDef / OptionGroupDef
這里就是各種參數(shù)的規(guī)范定義了,比如 OptionGroupDef :
typedef struct OptionGroupDef {
/**< group name */
const char *name;
/**
* Option to be used as group separator. Can be NULL for groups which
* are terminated by a non-option argument (e.g. ffmpeg output files)
*/
const char *sep;
/**
* Option flags that must be set on each option that is
* applied to this group
*/
int flags;
} OptionGroupDef;
另外 ffmpeg 支持的配置列表在 ffmpeg_opt.c中:
#define OFFSET(x) offsetof(OptionsContext, x)
const OptionDef options[] = {
/* main options */
CMDUTILS_COMMON_OPTIONS
{ "f", HAS_ARG | OPT_STRING | OPT_OFFSET |
OPT_INPUT | OPT_OUTPUT, { .off = OFFSET(format) },
"force format", "fmt" },
{ "y", OPT_BOOL, { &file_overwrite },
.......ffmpeg_parse_options
ffmpeg_parse_options
現(xiàn)在開始讓我們看下解析函數(shù):
作者:gezilinll
鏈接:https://zhuanlan.zhihu.com/p/380359900
來源:知乎
著作權(quán)歸作者所有。商業(yè)轉(zhuǎn)載請聯(lián)系作者獲得授權(quán),非商業(yè)轉(zhuǎn)載請注明出處。
int ffmpeg_parse_options(int argc, char **argv)
{
......
/* 將命令行的參數(shù)進(jìn)行拆分,將拆分結(jié)果分類并設(shè)置到不同的數(shù)據(jù)結(jié)構(gòu)中 */
ret = split_commandline(&octx, argc, argv, options, groups,
FF_ARRAY_ELEMS(groups));
......
/* 這里解析全局的配置參數(shù),我們這個(gè)范例里面沒有直接跳過,跟上面的解析主要是細(xì)節(jié)處理上的不同,不贅述 */
ret = parse_optgroup(NULL, &octx.global_opts);
if (ret < 0) {
av_log(NULL, AV_LOG_FATAL, "Error splitting the argument list: ");
goto fail;
}
/* 開啟配置中的所有輸入文件流 */
/* open_input_file 接口內(nèi)如果有配置文件的起始處理時(shí)間的話就會(huì)去做 Seek 操作 */
ret = open_files(&octx.groups[GROUP_INFILE], "input", open_input_file);
if (ret < 0) {
av_log(NULL, AV_LOG_FATAL, "Error opening input files: ");
goto fail;
}
/* 如果參數(shù)中有設(shè)置濾鏡相關(guān)的話則進(jìn)行初始化,我們這里不涉及,就先略過 */
ret = init_complex_filters();
if (ret < 0) {
av_log(NULL, AV_LOG_FATAL, "Error initializing complex filters.\n");
goto fail;
}
/* 開啟輸出文件流 */
ret = open_files(&octx.groups[GROUP_OUTFILE], "output", open_output_file);
if (ret < 0) {
av_log(NULL, AV_LOG_FATAL, "Error opening output files: ");
goto fail;
}
......
}
在上面的代碼中,我們重點(diǎn)關(guān)注下 split_commandline ,其他函數(shù)按注釋知道其作用即可,大多是一些常規(guī)邏輯上的內(nèi)容,不同的業(yè)務(wù)場景可能有不同的設(shè)計(jì)思路,不影響我們當(dāng)前的分析:
作者:gezilinll
鏈接:https://zhuanlan.zhihu.com/p/380359900
來源:知乎
著作權(quán)歸作者所有。商業(yè)轉(zhuǎn)載請聯(lián)系作者獲得授權(quán),非商業(yè)轉(zhuǎn)載請注明出處。
int split_commandline(OptionParseContext *octx, int argc, char *argv[],
const OptionDef *options,
const OptionGroupDef *groups, int nb_groups)
{
...
/* 初始化 OptionParseContext 內(nèi)全局、輸入、輸出參數(shù)的數(shù)據(jù)對象,包括分配內(nèi)存等 */
init_parse_context(octx, groups, nb_groups);
av_log(NULL, AV_LOG_DEBUG, "Splitting the commandline.\n");
/* 這里開始遍歷參數(shù)了 */
while (optindex < argc) {
const char *opt = argv[optindex++], *arg;
const OptionDef *po;
int ret;
av_log(NULL, AV_LOG_DEBUG, "Reading option '%s' ...", opt);
/* --xxx的跳過 */
if (opt[0] == '-' && opt[1] == '-' && !opt[2]) {
dashdash = optindex;
continue;
}
/* 參數(shù)類型為不帶 - 或者 第二個(gè)字符是空的 比如 reversed.mp4,匹配到了就跳到下一個(gè)參數(shù) */
if (opt[0] != '-' || !opt[1] || dashdash+1 == optindex) {
/* 這里就是找到對應(yīng)的 OptionGroup 對其中的參數(shù)進(jìn)行賦值 */
finish_group(octx, 0, opt);
av_log(NULL, AV_LOG_DEBUG, " matched as %s.\n", groups[0].name);
continue;
}
opt++;
......
/* 匹配當(dāng)前參數(shù)是否-i的配置,并獲取其后的參數(shù)如douyin_700x1240.mp4,匹配到了就跳到下一個(gè)參數(shù) */
if ((ret = match_group_separator(groups, nb_groups, opt)) >= 0) {
GET_ARG(arg);
finish_group(octx, ret, arg);
av_log(NULL, AV_LOG_DEBUG, " matched as %s with argument '%s'.\n",
groups[ret].name, arg);
continue;
}
/* 匹配當(dāng)前參數(shù)是否是之前提到的 options 中定義的參數(shù),如我們范例的 -vf */
po = find_option(options, opt);
/* po->name 就是 vf */
if (po->name) {
if (po->flags & OPT_EXIT) {
/* optional argument, e.g. -h */
arg = argv[optindex++];
} else if (po->flags & HAS_ARG) {
GET_ARG(arg);
} else {
arg = "1";
}
/* 將解析到的參數(shù)配置設(shè)置到輸入或輸出Group的數(shù)據(jù)結(jié)構(gòu)里面 */
/* 根據(jù)options中預(yù)存的flag判斷之后,負(fù)責(zé)把參數(shù)放入octx->cur_group或者global_opts中,目前從代碼上了解到的,
這個(gè) cur_group 就存放不歸屬上面的幾種條件的數(shù)據(jù),最后做下提示,因?yàn)閷﹂_篇提到的問題沒啥影響,先略過吧。 */
add_opt(octx, po, opt, arg);
av_log(NULL, AV_LOG_DEBUG, " matched as option '%s' (%s) with "
"argument '%s'.\n", po->name, po->help, arg);
continue;
}
/* 接下去就是其他一些類型數(shù)據(jù)的匹配、解析、設(shè)置了,比如-nofoo等,不再贅述 */
...
/* 匹配失敗的參數(shù)就會(huì)報(bào)錯(cuò),就我們常見的錯(cuò)誤提示 */
av_log(NULL, AV_LOG_ERROR, "Unrecognized option '%s'.\n", opt);
return AVERROR_OPTION_NOT_FOUND;
}
...
}
解析結(jié)果
經(jīng)過上面這樣一輪解析后, ffmpeg -i douyin_700x1240.mp4 -vf reverse -af areverse reversed.mp4 這樣一行命令的數(shù)據(jù)將按如下結(jié)構(gòu)存放:

流程圖

文件轉(zhuǎn)換
分析完參數(shù)解析就該看接下來文件轉(zhuǎn)換是如何處理的了,這里我們主要分析 transcode 這個(gè)函數(shù)的邏輯,并通過這個(gè)函數(shù)擴(kuò)展一下其他相關(guān)的部分:
作者:gezilinll
鏈接:https://zhuanlan.zhihu.com/p/380359900
來源:知乎
著作權(quán)歸作者所有。商業(yè)轉(zhuǎn)載請聯(lián)系作者獲得授權(quán),非商業(yè)轉(zhuǎn)載請注明出處。
/*
* The following code is the main loop of the file converter
*/
static int transcode(void)
{
int ret, i;
AVFormatContext *os;
OutputStream *ost;
InputStream *ist;
int64_t timer_start;
int64_t total_packets_written = 0;
/* 在這個(gè)接口里面會(huì)去做一堆的比如幀率計(jì)算、緩沖計(jì)算、編解碼器初始化等操作,最后對所有的輸出文件進(jìn)行寫文件頭的操作,完成初始化 */
ret = transcode_init();
......
/* 未接收到停止信號的話就持續(xù)進(jìn)行轉(zhuǎn)換處理,直到完成或接收到停止信號 */
while (!received_sigterm) {
......
/* 這里就是單步轉(zhuǎn)換操作了,后面會(huì)進(jìn)入代碼內(nèi)部近一步介紹 */
ret = transcode_step();
...
}
......
/* 把解碼緩沖區(qū)的剩余數(shù)據(jù)再處理一下然后 flush 準(zhǔn)備收工 */
for (i = 0; i < nb_input_streams; i++) {
ist = input_streams[i];
if (!input_files[ist->file_index]->eof_reached) {
process_input_packet(ist, NULL, 0);
}
}
flush_encoders();
term_exit();
/* 寫一下文件尾 */
for (i = 0; i < nb_output_files; i++) {
os = output_files[i]->ctx;
if (!output_files[i]->header_written) {
av_log(NULL, AV_LOG_ERROR,
"Nothing was written into output file %d (%s), because "
"at least one of its streams received no packets.\n",
i, os->url);
continue;
}
if ((ret = av_write_trailer(os)) < 0) {
av_log(NULL, AV_LOG_ERROR, "Error writing trailer of %s: %s\n", os->url, av_err2str(ret));
if (exit_on_error)
exit_program(1);
}
}
/* 接下去就是一些關(guān)閉 codec 呀關(guān)閉 file 呀之類的操作了 */
......
return ret;
}
上述代碼可以看到,核心的部分就在于 transcode_step 里面:
作者:gezilinll
鏈接:https://zhuanlan.zhihu.com/p/380359900
來源:知乎
著作權(quán)歸作者所有。商業(yè)轉(zhuǎn)載請聯(lián)系作者獲得授權(quán),非商業(yè)轉(zhuǎn)載請注明出處。
/**
* Run a single step of transcoding.
*
* @return 0 for success, <0 for error
*/
static int transcode_step(void)
{
OutputStream *ost;
InputStream *ist = NULL;
int ret;
/* 通過對比當(dāng)前編碼的pts,挑選出最小值的輸出流 */
ost = choose_output();
......
if (ost->filter && ost->filter->graph->graph) {
......
/* 這里就是按照輸出配置的 filter 鏈路對下一步的 process_input 輸出幀做處理了,當(dāng)進(jìn)來這里時(shí)則可能通過其內(nèi)部的 reap_filters 進(jìn)行編碼而不是后面的 reap_filters */
if ((ret = transcode_from_filter(ost->filter->graph, &ist)) < 0)
return ret;
if (!ist)
return 0;
} else if (ost->filter) {
int i;
for (i = 0; i < ost->filter->graph->nb_inputs; i++) {
InputFilter *ifilter = ost->filter->graph->inputs[i];
if (!ifilter->ist->got_output && !input_files[ifilter->ist->file_index]->eof_reached) {
ist = ifilter->ist;
break;
}
}
if (!ist) {
ost->inputs_done = 1;
return 0;
}
} else {
av_assert0(ost->source_index >= 0);
ist = input_streams[ost->source_index];
}
/* 讀取一個(gè) AVPacket 當(dāng)然也包括了一些pts校驗(yàn)計(jì)算等等事情,內(nèi)部先在 get_input_packet 接口中通過 av_read_frame 獲取到Packet */
/* 接著調(diào)用 process_input_packet --> decode_audio/decode_video 從而實(shí)現(xiàn)解碼并將解碼幀放入內(nèi)部數(shù)據(jù)隊(duì)列中 */
/* 解碼后的數(shù)據(jù)會(huì)通過 send_frame_to_filters 做一下 FilterGraph 的初始化和配置,以及通過 av_buffersrc_add_frame 將解碼后的 AVFrame 送入 AVFilterContext */
ret = process_input(ist->file_index);
if (ret == AVERROR(EAGAIN)) {
if (input_files[ist->file_index]->eagain)
ost->unavailable = 1;
return 0;
}
if (ret < 0)
return ret == AVERROR_EOF ? 0 : ret;
/* 根據(jù)濾波器做濾波處理,并把處理完的音視頻編碼到輸出文件中 */
/* 內(nèi)部通過調(diào)用 do_video_out/do_audio_out 最后在 write_packet 中執(zhí)行編碼操作 */
return reap_filters(0);
}
transcode_step 整體的流程就是上面代碼中介紹的,不過我們范例中的例子是一個(gè)倒放的功能,它在\最后編碼的時(shí)候并不會(huì)走最下面的 reap_filters 而是走 transcode_from_filter 中的 reap_filters ,因?yàn)槲覀兪怯?-vf reverse 這個(gè) filter 來處理輸出的。ffmpeg 這個(gè) filter 處理的邏輯會(huì)等待解碼數(shù)據(jù)完全準(zhǔn)備好了,再倒序進(jìn)行編碼,因此會(huì)造成較大的內(nèi)存壓力,特別是移動(dòng)端上。
如果我們命令是抽音頻流,比如 ffmpeg -i douyin_700x1240.mp4 audio.aac,那么走的就是最下面的 reap_filters 了。這里可以多提一下就是我們剛開始說的抽流的情況,當(dāng)不需要又一次編碼的話那么 ffmpeg 命令在 process_intput_packet 中將直接直接調(diào)用 do_streamcopy :
static int process_input_packet(InputStream *ist, const AVPacket *pkt, int no_eof)
{
......
for (i = 0; i < nb_output_streams; i++) {
OutputStream *ost = output_streams[i];
if (!ost->pkt && !(ost->pkt = av_packet_alloc()))
exit_program(1);
if (!check_output_constraints(ist, ost) || ost->encoding_needed)
continue;
do_streamcopy(ist, ost, pkt);
}
return !eof_reached;
}
最后再思考倒放這個(gè)范例的最后一個(gè)問題,從代碼邏輯來看真正能開始執(zhí)行編碼的條件簡單來說有兩個(gè),一個(gè)是 ost->filter->graph->graph 非空,另一個(gè)就是 transcode_from_filter 內(nèi)部的 avfilter_graph_request_oldest 返回值 >=0 ,那么什么情況下這兩個(gè)狀態(tài)能滿足呢?不過部分目前我還沒去深入研究,留著對 AVFilter 模塊的學(xué)習(xí)時(shí)再一起收尾,這里先知道是這樣一個(gè)邏輯就行了。
流程圖

總結(jié)
至此我們對 ffmpeg 命令實(shí)現(xiàn)的主流程做了一次分析,整體實(shí)現(xiàn)的思路跟我們一開始預(yù)測的還是有些接近的,不過 ffmpeg 的組裝主要還是通過其 AVFilter 及相關(guān)的模塊來處理的,其 FilterGraph 應(yīng)該就相當(dāng)于一個(gè)更復(fù)雜的責(zé)任鏈,不過說歸說,實(shí)際看其落地代碼當(dāng)中還是有數(shù)之不盡的細(xì)節(jié)和需要解決的問題的,后面的 FFmpeg 源碼世界的其他章節(jié)將再做更進(jìn)一步的分析學(xué)習(xí)。
作者:gezilinll
鏈接:https://zhuanlan.zhihu.com/p/380359900

技術(shù)交流,歡迎加我微信:ezglumes ,拉你入技術(shù)交流群。
推薦閱讀:
開通專輯 | 細(xì)數(shù)那些年寫過的技術(shù)文章專輯
NDK 學(xué)習(xí)進(jìn)階免費(fèi)視頻來了
推薦幾個(gè)堪稱教科書級別的 Android 音視頻入門項(xiàng)目
覺得不錯(cuò),點(diǎn)個(gè)在看唄~

