WebVTTの世界：TVerがどのように字幕を配信しているか

## TVerで字幕出るけどあれどうなってるんだろう

TVerで字幕のある動画を見たりするのですが、あれはどうなっているのか気になったので調べてみました。

##  HTTP Live Streaming (HLS) における字幕

RFC8216にはHLS全般についての定義がされており、字幕についても定義されています。
TVerで配信しているデータもこの形式に沿っていました。

### プレイリストの種類

HLSでは、以下のようなプレイリストが使用されます。
それぞれUTF-8エンコードのファイルです。

#### マスタープレイリスト

マスタープレイリストは、複数のビデオ、オーディオ、字幕のプレイリストを関連付けるために使用されます。

#### ビデオプレイリスト

ビデオプレイリストは、ビデオコンテンツのセグメントを含むプレイリストです。

#### オーディオプレイリスト

オーディオプレイリストは、オーディオコンテンツのセグメントを含むプレイリストです。

#### 字幕プレイリスト

字幕プレイリストは、字幕のセグメントを含むプレイリストです。
字幕はWebVTT形式で提供されることが多いようです。

### プレイリストの関連

マスタープレイリストと各種プレイリストの関連は以下のような感じです:

```plaintext
マスタープレイリスト
├── ビデオプレイリスト1
│   ├── video_segment1.ts
│   ├── video_segment2.ts
│   └── video_segment3.ts
├── ビデオプレイリスト2
│   ├── video_segment1.ts
│   ├── video_segment2.ts
│   └── video_segment3.ts
├── オーディオプレイリスト1
│   ├── audio_segment1.ts
│   ├── audio_segment2.ts
│   └── audio_segment3.ts
└── 字幕プレイリスト1
    ├── subtitle_segment1.vtt
    ├── subtitle_segment2.vtt
    └── subtitle_segment3.vtt
```

このように、マスタープレイリストが各種ビデオプレイリスト、オーディオプレイリスト、および字幕プレイリストを関連付けています。

## TVerの字幕を読み解いてみる

TVerで、**さがす**から**字幕あり**をクリックすると、字幕がある動画がリストアップされます。
この中から適当なデータを選んで見ていきます。
動画のページを開いて、開発者ツールでネットワークタブを表示し、`m3u8`でフィルタします。
一番最初に検出されるファイルがおそらくマスタープレイリストだと思われるので、最初に検出されたパスのデータを選んで表示してみるとこのようなデータが入っていました:
```
#EXTM3U
#EXT-X-VERSION:4
#EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID="audio-0",NAME="en (Main)",DEFAULT=YES,AUTOSELECT=YES,LANGUAGE="en",URI="https://example.com/hls/rendition.m3u8"
#EXT-X-MEDIA:TYPE=SUBTITLES,GROUP-ID="subtitles-0",NAME="字幕",DEFAULT=YES,AUTOSELECT=YES,LANGUAGE="ja-JP",URI="https://example.com/hls/rendition.m3u8"
#EXT-X-STREAM-INF:PROGRAM-ID=0,BANDWIDTH=1039500,CODECS="mp4a.40.2,avc1.4d001f",RESOLUTION=960x540,AUDIO="audio-0",CLOSED-CAPTIONS=NONE,SUBTITLES="subtitles-0"
https://example.com/hls/rendition.m3u8
#EXT-X-I-FRAME-STREAM-INF:PROGRAM-ID=0,BANDWIDTH=1039500,CODECS="mp4a.40.2,avc1.4d001f",RESOLUTION=960x540,URI="https://example.com/hls/iframe.m3u8"
#EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID="audio-1",NAME="en (Main)",DEFAULT=YES,AUTOSELECT=YES,LANGUAGE="en",URI="https://example.com/hls/rendition.m3u8"
#EXT-X-STREAM-INF:PROGRAM-ID=0,BANDWIDTH=334400,CODECS="mp4a.40.2,avc1.420015",RESOLUTION=480x270,AUDIO="audio-1",CLOSED-CAPTIONS=NONE,SUBTITLES="subtitles-0"
https://example.com/hls/rendition.m3u8
#EXT-X-I-FRAME-STREAM-INF:PROGRAM-ID=0,BANDWIDTH=334400,CODECS="mp4a.40.2,avc1.420015",RESOLUTION=480x270,URI="https://example.com/hls/iframe.m3u8"
#EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID="audio-2",NAME="en (Main)",DEFAULT=YES,AUTOSELECT=YES,LANGUAGE="en",URI="https://example.com/hls/rendition.m3u8"
#EXT-X-STREAM-INF:PROGRAM-ID=0,BANDWIDTH=570900,CODECS="mp4a.40.2,avc1.4d001e",RESOLUTION=640x360,AUDIO="audio-2",CLOSED-CAPTIONS=NONE,SUBTITLES="subtitles-0"
https://example.com/hls/rendition.m3u8
#EXT-X-I-FRAME-STREAM-INF:PROGRAM-ID=0,BANDWIDTH=570900,CODECS="mp4a.40.2,avc1.4d001e",RESOLUTION=640x360,URI="https://example.com/hls/iframe.m3u8"
#EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID="audio-3",NAME="en (Main)",DEFAULT=YES,AUTOSELECT=YES,LANGUAGE="en",URI="https://example.com/hls/rendition.m3u8"
#EXT-X-STREAM-INF:PROGRAM-ID=0,BANDWIDTH=1669800,CODECS="mp4a.40.2,avc1.4d001f",RESOLUTION=1280x720,AUDIO="audio-3",CLOSED-CAPTIONS=NONE,SUBTITLES="subtitles-0"
https://example.com/hls/rendition.m3u8
#EXT-X-I-FRAME-STREAM-INF:PROGRAM-ID=0,BANDWIDTH=1669800,CODECS="mp4a.40.2,avc1.4d001f",RESOLUTION=1280x720,URI="https://example.com/hls/iframe.m3u8"
#EXT-X-STREAM-INF:PROGRAM-ID=0,BANDWIDTH=3163600,CODECS="mp4a.40.2,avc1.640028",RESOLUTION=1920x1080,AUDIO="audio-3",CLOSED-CAPTIONS=NONE,SUBTITLES="subtitles-0"
https://example.com/hls/rendition.m3u8
#EXT-X-I-FRAME-STREAM-INF:PROGRAM-ID=0,BANDWIDTH=3163600,CODECS="mp4a.40.2,avc1.640028",RESOLUTION=1920x1080,URI="https://example.com/hls/iframe.m3u8"
```

`#EXT-X-MEDIA:TYPE=SUBTITLES`かつ、`LANGUAGE="ja-JP"`のURLのデータが日本語の字幕のようです。
このURLのデータを取得してみます。

```
#EXTM3U
#EXT-X-VERSION:3
#EXT-X-PLAYLIST-TYPE:VOD
#EXT-X-MEDIA-SEQUENCE:0
#EXT-X-TARGETDURATION:30
#EXTINF:30.000,
https://example.com/hls/segment0.vtt
#EXTINF:30.000,
https://example.com/hls/segment1.vtt
#EXTINF:30.000,
https://example.com/hls/segment2.vtt
#EXTINF:30.000,
https://example.com/hls/segment3.vtt
#EXTINF:30.000,
https://example.com/hls/segment4.vtt
#EXTINF:30.000,
https://example.com/hls/segment5.vtt
#EXTINF:30.000,
https://example.com/hls/segment6.vtt
#EXTINF:30.000,
https://example.com/hls/segment7.vtt
#EXTINF:30.000,
https://example.com/hls/segment8.vtt
#EXTINF:30.000,
https://example.com/hls/segment9.vtt
#EXTINF:30.000,
https://example.com/hls/segment10.vtt
:
:
#EXT-X-ENDLIST
```
WebVTTの字幕のURLがリストアップされていました。
EXTINFを見ると、30秒ごとに区切られているようです。
この中身を見てみます。

```
WEBVTT
X-TIMESTAMP-MAP=LOCAL:00:00:00.000,MPEGTS:0

00:02.033 --> 00:06.561 line:60% position:17.71% align:start
<c.fgc-7.bgc-65.sdc-0>生字幕放送です。一部、字幕で</c>

00:02.033 --> 00:06.561 line:72% position:17.71% align:start
<c.fgc-7.bgc-65.sdc-0>表現しきれない場合があります。</c>
：
：
```

放送局によりますが、`line`や`position`、`align`などの属性があります。
これらはRFCに定義されていないのでTVer独自の属性だと思われます。
`line`はtopからの位置、`position`が`align`からの位置のような感じだと思います。

また、`<c.fgc-7.bgc-65.sdc-0>` のようなタグっぽいものもTVer独自のもののようで、スピーカーを区別するためのタグのような感じでした。

## まとめ

字幕のデータはWebVTT形式で提供されていることが多いようです。
NHK+の字幕もWebVTT形式でしたがかなりクセが強かったです。
NetflixやAmazon Prime等の動画配信サービスは暗号化が施されているので、字幕のデータを取得することはできませんでした。
普段何気なく見ている字幕も、HLSという技術を使って配信されていることがわかりました。

## 参考

https://datatracker.ietf.org/doc/html/rfc8216