<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          FFmpeg源碼世界:命令篇

          共 26014字,需瀏覽 53分鐘

           ·

          2021-06-21 22:22

          前言

          最近在做一些音視頻領(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} ...

          流程推測

          1. 應(yīng)該要有一個(gè)數(shù)據(jù)層,按照輸入?yún)?shù)進(jìn)行數(shù)據(jù)解析
          2. 將解碼、轉(zhuǎn)碼、功能處理、編碼四個(gè)模塊抽象分開
          3. 按第一個(gè)環(huán)節(jié)數(shù)據(jù)解析的結(jié)構(gòu)組裝上述四個(gè)模塊,形成一條或多條責(zé)任鏈的效果
          4. 開始處理

          過程中應(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ù)交流群。

          推薦閱讀:

          音視頻面試基礎(chǔ)題

          OpenGL ES 學(xué)習(xí)資源分享

          開通專輯 | 細(xì)數(shù)那些年寫過的技術(shù)文章專輯

          NDK 學(xué)習(xí)進(jìn)階免費(fèi)視頻來了

          推薦幾個(gè)堪稱教科書級別的 Android 音視頻入門項(xiàng)目

          覺得不錯(cuò),點(diǎn)個(gè)在看唄~


          瀏覽 39
          點(diǎn)贊
          評論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          評論
          圖片
          表情
          推薦
          點(diǎn)贊
          評論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  日韩天堂在线 | 亚洲成人超碰在线观看 | 国产在线播放福利 | 在线免费观看黄色电影 | 91人妻人人澡人人爽人 |