FFmpegにコマンドラインオプションを追加する

本記事は東京大学工学部電子情報・電気電子工学科の「[大規模ソフトウェアを手探る](https://doss.eidos.ic.i.u-tokyo.ac.jp/)」という実習の報告書を兼ねています。

# はじめに
この記事では、FFmpegを改造して自分の好きなコマンドラインオプションを追加する方法について書きます。今回は、ストリーミング形式の一つであるHLSで用いられるAES-128暗号化キーを指定してダウンロードするオプションを追加することにしました。

# FFmpegの開発環境を整える
[この記事](https://qiita.com/grayarea/items/68b87f1ac8d715fb5b74)の通りに開発環境を整えました。

# HLSに関係するところを見つける
VSCodeのプロジェクト全体検索機能を用いて`hls`と検索すると、まず`libavformat/hls.c`がいかにも怪しいと気づきました。
ここで、HLSストリーミング動画をダウンロードするときに用いるオプションの一つに`allowed_extensions`というものがあることを利用して、この文字列をこのファイル内で検索すると、以下の部分のコードがヒットしました。

```{c:hls.c}
#define OFFSET(x) offsetof(HLSContext, x)
#define FLAGS AV_OPT_FLAG_DECODING_PARAM
static const AVOption hls_options[] = {
    {"live_start_index", "segment index to start live streams at (negative values are from the end)",
        OFFSET(live_start_index), AV_OPT_TYPE_INT, {.i64 = -3}, INT_MIN, INT_MAX, FLAGS},
    {"allowed_extensions", "List of file extensions that hls is allowed to access",
        OFFSET(allowed_extensions), AV_OPT_TYPE_STRING,
        {.str = "3gp,aac,avi,flac,mkv,m3u8,m4a,m4s,m4v,mpg,mov,mp2,mp3,mp4,mpeg,mpegts,ogg,ogv,oga,ts,vob,wav"},
        INT_MIN, INT_MAX, FLAGS},
    {"max_reload", "Maximum number of times a insufficient list is attempted to be reloaded",
        OFFSET(max_reload), AV_OPT_TYPE_INT, {.i64 = 1000}, 0, INT_MAX, FLAGS},
    {"http_persistent", "Use persistent HTTP connections",
        OFFSET(http_persistent), AV_OPT_TYPE_BOOL, {.i64 = 1}, 0, 1, FLAGS },
    {"http_multiple", "Use multiple HTTP connections for fetching segments",
        OFFSET(http_multiple), AV_OPT_TYPE_BOOL, {.i64 = -1}, -1, 1, FLAGS},
    {NULL}
};
```
ここのコードを読んでみると、

```
{"オプション名", "オプションの説明", OFFSET(オプション名), オプションの種類, {.種類 = 初期値}, 最小値, 最大値, FLAGS}
```
を追加すればオプションを追加できるということがわかります。
`OFFSET(オプション名)`の部分で、オプション名にダブルクオーテーションがないことから、オプション名と同じ変数がどこかに定義されていることが推測されます。この時点で多分オプション指定をするとその変数に指定の値が代入されると予想しました。`OFFSET`の定義を調べると、以下のコードが見つかりました。

```{c:hls.c}
#define OFFSET(x) offsetof(HLSContext, x)
```

上記コードから、構造体`HLSContext`の番号を指定する関数が定義されていることがわかります。
この構造体が定義されているところを調べると、以下のコードが見つかります。

```{c:hls.c}
typedef struct HLSContext {
    AVClass *class;
    AVFormatContext *ctx;
    int n_variants;
    struct variant **variants;
    int n_playlists;
    struct playlist **playlists;
    int n_renditions;
    struct rendition **renditions;

    int cur_seq_no;
    int live_start_index;
    int first_packet;
    int64_t first_timestamp;
    int64_t cur_timestamp;
    AVIOInterruptCB *interrupt_callback;
    AVDictionary *avio_opts;
    int strict_std_compliance;
    char *allowed_extensions;
    int max_reload;
    int http_persistent;
    int http_multiple;
    AVIOContext *playlist_pb;
} HLSContext;
```

ここに`char *hls_key_file;`を追加すればよいとわかりました。実際の処理がどこに書かれているか調べるために、`key`などの文字列で検索したところ、以下のコードがヒットしました。

```{c}
if (seg->key_type == KEY_NONE) {
    ret = open_url(pls->parent, in, seg->url, c->avio_opts, opts, &is_http);
} else if (seg->key_type == KEY_AES_128) {
    char iv[33], key[33], url[MAX_URL_SIZE];
    if (strcmp(seg->key, pls->key_url)) {
        AVIOContext *pb = NULL;
        if (open_url(pls->parent, &pb, seg->key, c->avio_opts, opts, NULL) == 0) {
            ret = avio_read(pb, pls->key, sizeof(pls->key));
            if (ret != sizeof(pls->key)) {
                av_log(NULL, AV_LOG_ERROR, "Unable to read key file %s\n",
                        seg->key);
            }
            ff_format_io_close(pls->parent, &pb);
        } else {
            av_log(NULL, AV_LOG_ERROR, "Unable to open key file %s\n",
                    seg->key);
        }
        av_strlcpy(pls->key_url, seg->key, sizeof(pls->key_url));
    }
    ff_data_to_hex(iv, seg->iv, sizeof(seg->iv), 0);
    ff_data_to_hex(key, pls->key, sizeof(pls->key), 0);
    iv[32] = key[32] = '\0';
    if (strstr(seg->url, "://"))
        snprintf(url, sizeof(url), "crypto+%s", seg->url);
    else
        snprintf(url, sizeof(url), "crypto:%s", seg->url);

    av_dict_set(&opts, "key", key, 0);
    av_dict_set(&opts, "iv", iv, 0);

    ret = open_url(pls->parent, in, url, c->avio_opts, opts, &is_http);
    if (ret < 0) {
        goto cleanup;
    }
    ret = 0;
} else if (seg->key_type == KEY_SAMPLE_AES) {
    av_log(pls->parent, AV_LOG_ERROR,
            "SAMPLE-AES encryption is not supported yet\n");
    ret = AVERROR_PATCHWELCOME;
}
else
    ret = AVERROR(ENOSYS);
```

この付近のコードで`seg->key`がHLSキーファイルへのパスを保有していそうだったので、いろいろ試行錯誤した結果、以下のコードを追加するだけで思い通りの動作になることがわかった。

```{c}
if (c->hls_key_file != NULL)
    strcpy(seg->key, c->hls_key_file);
```

[ここ](https://doss-gitlab.eidos.ic.i.u-tokyo.ac.jp/grayarea/ffmpeg/commit/ad92baeeef341857b09c7a5033458d8e1787a500)に今までの変更をまとめたコミットがありますので、細かく知りたい場合はそちらをご覧ください。

# まとめ
FFmpegは10万行をゆうに超える大規模ソフトウェアなので、これにオプションを追加するのは難しいだろうと予想していましたが、たった数時間で目標を達成してしまいました。これでは簡単すぎるので、VLCの字幕機能を拡張することにしました。