VLCで副字幕を表示可能にする

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

# はじめに
実験の2日目に「[FFmpegにコマンドラインオプションを追加する](https://qiita.com/grayarea/items/f7bc7b98ca2f4ba286ca)」という目標ではじめましたが、ほんの数時間で目標達成してしまい、物足りなかったのでVLCをいじって副字幕を表示可能にすることを目標にすることにしました。VLCが副字幕に対応していないことによって二つの字幕を同時に表示して言語を勉強したい人たちにとっては使いづらい、と数年前から問題視されており、これを実現すれば世界中の多くの人が喜ぶだろうと思い、このテーマに決めました。この記事ではそこでの試行錯誤とその過程をまとめます。

# 開発環境を整える
「[macOS上でVLCをソースからビルドする](https://qiita.com/grayarea/items/d6b8667be60e1fb774ee)」の通りにVLCのソースコードをビルドし、それ以降はLLDBデバッガなどを用いて解析、ソースコード変更ののちに再ビルドという作業を繰り返して開発します。

# VLCのファイル構造
VLCのファイル構造の中で、今回の目的に必要だったものをピックアップしてみました。

```
modules/
    codec/
        subsdec.c ... 字幕をデコードして表示する処理
        mkv/
            *.hpp
            *.cpp ... mkvファイルの字幕を取り出す処理
    demux/
        subtitle.c .. 外部字幕を取得する処理
    gui/
        macosx/
            *.h
            *.m ... macOSにおけるGUI部分に関する処理
            UI/
                *.xib ... macOSにおけるGUIそのもの
src/
    input/
        es_out.c ... 字幕の追加・転送・変更に関する処理
        es_out_timeshift.c ... 表示する字幕に関する処理をes_out.cに送る処理
        decoder.c ... 字幕をデコードするために必要なデコーダーを指定する処理
        input.c ... var.cによって変数が変化したときの処理
        var.c ... GUIからの入力を変数に入れる処理
```

# 副字幕メニューの追加
基本的には`modules/gui/macosx`フォルダ内を変更して再ビルドすればメニューが追加できます。iOSアプリ開発とmacOSアプリ開発で培ったObjective-Cの知識を使えば、GUIとしてメニューを追加するだけなら簡単にできました。あとはそのメニューとVLC本体を連携するための処理を書く必要があります。字幕トラックメニューに関係する処理は以下の4箇所です。

```{objc:VLCMainMenu.h}
@property (readwrite, weak) IBOutlet NSMenuItem *subtitle_track;
@property (readwrite, weak) IBOutlet NSMenu *subtitle_tracksMenu;
```

```{objc:VLCMainMenu.m}
[_subtitle_track setTitle: _NS("Subtitles Track")];
[_subtitle_tracksMenu setTitle: _NS("Subtitles Track")];
...
[self setupVarMenuItem:_subtitle_track target: (vlc_object_t *)p_input
                   var:"spu-es" selector: @selector(toggleVar:)];
...
[_subtitle_track setEnabled: b_enabled];
```

すべての箇所に対して「字幕トラック2」の処理を加えてあげて再ビルドすると、実行時に`Subtitle Track 2`のメニューが追加されていることが確認できました。ただ、`Subtitle Track 1`と全く同じ動作をしてしまいます。つまり、`Subtitle Track 1`を変更すると字幕の言語は変更されるものの、`Subtitle Track 2`でも同じ言語の字幕が選択されている状態になってしまうのです。これを解決するために、`spu-es`という文字列に注目しました。`VLCMainMenu.m`のこの部分のコードがVLC本体に変数を渡している部分だと予想できたので、`Subtitle Track 2`に関するコードは`spu-es2`と置き換えることにしました。この状態で再ビルドすると、`Subtitle Track 2`は`Subtitle Track 1`と違い、常にグレイアウトしている状態になります。これはVLC本体の変数が`spu-es2`を全く参照していないため、字幕言語を追加する処理すら働いていないので当然なのです。

# VLC本体を弄る
上記の問題に対応するためには、「VLCのファイル構造」でも取り上げた、`src/input/var.c`を変更する必要があります。このファイルはGUIからの入力を変数に代入し、変更があればその都度代入する処理をします。

```{c:var.c}
static int EsSpuCallback ( vlc_object_t *p_this, char const *psz_cmd,
                           vlc_value_t oldval, vlc_value_t newval, void * );

...
CALLBACK( "spu-es", EsSpuCallback ),
...
case SPU_ES:
        return "spu-es";
...
static int EsSpuCallback( vlc_object_t *p_this, char const *psz_cmd,
                           vlc_value_t oldval, vlc_value_t newval, void *p_data )
{
    input_thread_t *p_input = (input_thread_t*)p_this;
    VLC_UNUSED( psz_cmd); VLC_UNUSED( oldval ); VLC_UNUSED( p_data );

    if( newval.i_int < 0 )
        newval.i_int = -SPU_ES; /* disable spu es */

    input_ControlPushHelper( p_input, INPUT_CONTROL_SET_ES_BY_ID, &newval );

    return VLC_SUCCESS;
}
```

これらの各部分に対して`spu-es2`用のコードを追加すればいいのですが、ここで`spu-es`と`SPU_ES`の関連付けを`spu-es2`の場合はどうすればよいかで困りました。とりあえず`spu-es2`は`SPU_ES2`と対応付けることにして他の部分も変えていく方針に決めました。
そうすると、VLCの奥深くの部分まで書き換える必要が出てきました。mkvファイルを開くと呼ばれる`modules/codec/mkv`フォルダ内のファイルにはmkvの字幕を処理するものがあり、mp4ファイルを開くと呼ばれる`modules/codec/mp4`フォルダ内のファイルにはmp4の字幕を処理するものがある、と動画ファイルのフォーマットごとに字幕の処理について定義されています。これらはすべて字幕を表示するときに`SPU_ES`という定数しかないことを前提に書かれているため、これらすべてを書き換える必要が出てきますが、さすがにそれは10日という制限のもとでは不可能ですし、不具合が出てもテストしようがないので危険です。結局、以下のように、もっと上のレイヤーである`src/input/es_out_timeshift.c`を弄って、「`SPU_ES`の処理が来たときには`SPU_ES2`の処理もする」という処理を追加することにしました。

```{c:es_out_timeshift.c}
if (p_fmt->i_cat == SPU_ES) {
    p_fmt->i_cat = SPU_ES2;

    vlc_mutex_lock( &p_sys->lock );

    TsAutoStop( p_out );

    if( CmdInitAdd( &cmd, p_es, p_fmt, p_sys->b_delayed ) )
    {
        vlc_mutex_unlock( &p_sys->lock );
        free( p_es );
        return NULL;
    }

    TAB_APPEND( p_sys->i_es, p_sys->pp_es, p_es );

    if( p_sys->b_delayed )
        TsPushCmd( p_sys->p_ts, &cmd );
    else
        CmdExecuteAdd( p_sys->p_out, &cmd );

    vlc_mutex_unlock( &p_sys->lock );
    p_fmt->i_cat = SPU_ES;
}
```

この状態で再ビルドすると、字幕トラック2を開くと字幕トラック1と同じ言語一覧が出てくるようになりました。
このあと、いろいろ迷走してしまったのですが、結局`modules/demux/subtitle.c`と`modules/codec/mkv/mkv.cpp`を書き換えることによって内部字幕＋外部字幕の組み合わせなら複数字幕を表示することに成功しました。残念ながら、外部字幕＋外部字幕の組み合わせもしくは内部字幕＋内部字幕の組み合わせでは片方の字幕しか表示されないという不具合は最後まで修正することができませんでした。

# multisubsについて
実験の発表日の前日に、チームメンバーの一人が以下のプロジェクトを見つけました。
https://repo.or.cz/vlc/multisubs.git/
multisubsという名前で、VLC2.0（3年前のバージョン）に複数字幕表示機能を追加するというものでした。これを参考にすると、`spu-es2`に対応させる定数は、`SPU_ES2`ではなく`SPU_ES`でよいということがわかりました。たしかに`SPU_ES`だけにすれば、`modules/codec/`以下にある各コーデックごとの処理を記述したコードに変更を加える必要がなく、単純な構造になります。このコードを参考に、1日徹夜して実装してみました。しかし、`SPU_ES2`を追加する方法による実装未満の結果しか得られませんでした。字幕トラック1で選択した言語の字幕しか表示されないのです。
VLC2.0の時代から大きくコードが変更されているため、単純にそのまま移植しただけでは動かないのかもしれません。それ以降いろいろ頑張ってコードを変更してみたりデバッグしてみたりしましたが、結局複数字幕の表示は実現できませんでした。（一応コードはプロジェクトのmultisubsブランチにpushしてあります）

# まとめ
長時間VLCに変更を加え続けて副字幕の表示を実現しようと試行錯誤しましたが、結局内部字幕＋外部字幕のみの対応となってしまい、満足のいく結果は得られませんでした。しかし、VSCodeを用いたデバッグの方法を学んだり、どんなにプロジェクト全体が大きく、すべてのコードの内容が理解できなくとも、一部に対する変更だけならうまくデバッガを用いれば実現できるということを学ぶことができました。この経験を生かして今後のプログラミング人生を歩んでいきたいと思います。