Cloudflare で HTTP Live Streaming (HLS) をキャッシュする

# 目的

Cloudflare で HTTP Live Streaming (HLS) をキャッシュしてみます。

# 動画準備

動画は以下のサイトからダウンロードします。

https://peach.blender.org/download/

# HTTP Live Streaming (HLS) 配信
以下のサイトを参考にしました。

- [【HLS】ウェブサイトの動画をストリーミング再生する【hls.js】 | webico blog](https://blog.webico.work/webmovie-hls)

# インデックスファイル (`.m3u8`) とセグメントファイル (`.ts`) 準備

`ffmpeg` をインストールして、以下のコマンド実行により `.m3u8` と `.ts` ファイルを作成します。

- [コラム FFmpegとは | 株式会社イノコス](https://www.innoqos.com/column/202307-08.html)

```shell:
brew install ffmpeg
```

```shell:
ffmpeg -i big_buck_bunny_720p_30mb.mp4 -c:v copy -c:a copy \
-f hls -hls_time 9 -hls_playlist_type vod \
-hls_segment_filename "big_buck_bunny_720p_30mb%3d.ts" \
big_buck_bunny_720p_30mb.m3u8
```

>[ffmpeg で Apple HTTP Live Streaming（HLS）を扱う | ニコラボ](https://nico-lab.net/hls_muxer_with_ffmpeg/)
`-hls_time` [float]
各セグメントの動画時間。指定した時間が来たら次のキーフレームで分割される。任意の時間で分割する場合にはGOPのフレーム間隔（group of picture size）を決めるエンコードオプションの-g（keyint）、-keyint_minを併用する。-sc_threshold 0をつけるとキーフレームがシーンチェンジにで入らなくなり意図したセグメント分割になる
範囲：0 から FLT_MAX まで
既定値：2
`-hls_playlist_type` [int]
0：何もつけない。既定値
1, event
#EXT-X-PLAYLIST-TYPE:EVENTタグを追加し、-hls_list_size 0（マニフェストにすべてのセグメント名を書き込む）も強制される
2, vod
#EXT-X-PLAYLIST-TYPE:VODタグを追加し、-hls_list_size 0も強制される

>[FFmpeg Formats Documentation](https://ffmpeg.org/ffmpeg-formats.html#hls-2)
`hls_time` duration
Set the target segment length. Default value is 2.
duration must be a time duration specification, see (ffmpeg-utils)the Time duration section in the ffmpeg-utils(1) manual. Segment will be cut on the next key frame after this time has passed.
`hls_playlist_type` vod
Emit #EXT-X-PLAYLIST-TYPE:VOD in the m3u8 header. Forces hls_list_size to 0; the playlist must not change.

以下のようなファイルが作成されます。

```conf:big_buck_bunny_720p_30mb.m3u8
#EXTM3U
#EXT-X-VERSION:3
#EXT-X-TARGETDURATION:16
#EXT-X-MEDIA-SEQUENCE:0
#EXT-X-PLAYLIST-TYPE:VOD
#EXTINF:12.960000,
big_buck_bunny_720p_30mb000.ts
#EXTINF:8.760000,
big_buck_bunny_720p_30mb001.ts
#EXTINF:6.120000,
big_buck_bunny_720p_30mb002.ts
#EXTINF:14.200000,
big_buck_bunny_720p_30mb003.ts
#EXTINF:10.000000,
big_buck_bunny_720p_30mb004.ts
#EXTINF:4.160000,
big_buck_bunny_720p_30mb005.ts
#EXTINF:8.760000,
big_buck_bunny_720p_30mb006.ts
#EXTINF:8.200000,
big_buck_bunny_720p_30mb007.ts
#EXTINF:16.000000,
big_buck_bunny_720p_30mb008.ts
#EXTINF:9.520000,
big_buck_bunny_720p_30mb009.ts
#EXTINF:2.720000,
big_buck_bunny_720p_30mb010.ts
#EXTINF:6.920000,
big_buck_bunny_720p_30mb011.ts
#EXTINF:10.200000,
big_buck_bunny_720p_30mb012.ts
#EXTINF:9.920000,
big_buck_bunny_720p_30mb013.ts
#EXTINF:9.800000,
big_buck_bunny_720p_30mb014.ts
#EXTINF:6.240000,
big_buck_bunny_720p_30mb015.ts
#EXTINF:12.000000,
big_buck_bunny_720p_30mb016.ts
#EXTINF:7.440000,
big_buck_bunny_720p_30mb017.ts
#EXTINF:6.920000,
big_buck_bunny_720p_30mb018.ts
#EXT-X-ENDLIST
```

# プレイヤーHTML (`hls.js`) 準備
以下の HTML を準備します。

```html:hls.html
<!DOCTYPE html>
<html lang="en">

<head>
    <title>HLS Demo</title>
    <script src="https://cdn.jsdelivr.net/npm/hls.js@latest"></script>
</head>

<body>
    <video id="video_sample" controls></video>
    
    <script>
        if (Hls.isSupported()) {
            var video = document.getElementById('video_sample');
            var hls = new Hls();
            hls.loadSource('./big_buck_bunny_720p_30mb.m3u8');
            hls.attachMedia(video);
        }
    </script>
</body>

</html>
```

ディレクトリ構成は以下のようにし、ウェブサーバーに配置します。

```shell:
% tree video          
video
├── big_buck_bunny_720p_30mb.m3u8
├── big_buck_bunny_720p_30mb.mp4
├── big_buck_bunny_720p_30mb000.ts
├── big_buck_bunny_720p_30mb001.ts
├── big_buck_bunny_720p_30mb002.ts
├── big_buck_bunny_720p_30mb003.ts
├── big_buck_bunny_720p_30mb004.ts
├── big_buck_bunny_720p_30mb005.ts
├── big_buck_bunny_720p_30mb006.ts
├── big_buck_bunny_720p_30mb007.ts
├── big_buck_bunny_720p_30mb008.ts
├── big_buck_bunny_720p_30mb009.ts
├── big_buck_bunny_720p_30mb010.ts
├── big_buck_bunny_720p_30mb011.ts
├── big_buck_bunny_720p_30mb012.ts
├── big_buck_bunny_720p_30mb013.ts
├── big_buck_bunny_720p_30mb014.ts
├── big_buck_bunny_720p_30mb015.ts
├── big_buck_bunny_720p_30mb016.ts
├── big_buck_bunny_720p_30mb017.ts
├── big_buck_bunny_720p_30mb018.ts
└── hls.html

1 directory, 22 files
```

# Apache 設定追加

以下の設定をウェブサーバーに追加し、サービスを再起動します。

```conf:/etc/httpd/conf/httpd.conf
AddType application/x-mpegURL .m3u8
AddType video/MP2T .ts
```

```shell:
sudo systemctl restart httpd.service
```

# デフォルトキャッシュ挙動

実際にアクセスしてみると、Cloudflare のデフォルトキャッシュ挙動だと、`.m3u8` も `.ts` もキャッシュされません。

- [Default Cache Behavior · Cloudflare Cache (CDN) docs](https://developers.cloudflare.com/cache/concepts/default-cache-behavior/#default-cached-file-extensions)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/101361/6cb3cbc0-7165-0310-d24c-abc360951f7a.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/101361/a4eaa48a-27fe-a914-79fa-5ee0e66080e1.png)

# キャッシュ戦略

## `.m3u8`

ライブストリーミング配信では、マニフェストファイル `.m3u8` は、特に HLS の場合、新しいセグメントが利用可能になると定期的に更新されます。
新しいビデオセグメントは数秒ごとに追加されるため、`.m3u8` ファイルはまったくキャッシュしないか、非常に低いTTL（理想的には `.ts` セグメント持続時間の半分未満）でキャッシュすることが推奨です。

オンデマンド再生の場合、`.m3u8` ファイルはもっと長くキャッシュしても問題ないです。

> [RFC 8216 - HTTP Live Streaming 日本語訳 6.2.1. General Server Responsibilities](https://tex2e.github.io/rfc-translater/html/rfc8216.html#6-2-1--General-Server-Responsibilities)
If a Media Playlist does not contain the EXT-X-ENDLIST tag, the server MUST make a new version of the Playlist file available that contains at least one new Media Segment. It MUST be made available relative to the time that the previous version of the Playlist file was made available: no earlier than one-half the target duration after that time, and no later than 1.5 times the target duration after that time. This allows clients to utilize the network efficiently.
メディアプレイリストにEXT-X-ENDLISTタグが含まれていない場合、サーバーは、少なくとも1つの新しいメディアセグメントを含むプレイリストファイルの新しいバージョンを利用可能にする必要があります。プレイリストファイルの以前のバージョンが利用可能になった時間に関連して利用可能にする必要があります。その時間後のターゲット継続時間の半分以上、その時間後のターゲット継続時間の1.5倍以内。これにより、クライアントはネットワークを効率的に利用できます。

> [RFC 8216 - HTTP Live Streaming 日本語訳 6.3.4. Reloading the Media Playlist File](https://tex2e.github.io/rfc-translater/html/rfc8216.html#6-3-4--Reloading-the-Media-Playlist-File)
When a client loads a Playlist file for the first time or reloads a Playlist file and finds that it has changed since the last time it was loaded, the client MUST wait for at least the target duration before attempting to reload the Playlist file again, measured from the last time the client began loading the Playlist file.
クライアントが初めてプレイリストファイルをロードするか、プレイリストファイルをリロードして、最後にロードされてから変更されていることを発見した場合、クライアントは、プレイリストファイルのリロードを再試行する前に、少なくともターゲット期間待機する必要があります。クライアントが最後にプレイリストファイルのロードを開始したときから。
If the client reloads a Playlist file and finds that it has not changed, then it MUST wait for a period of one-half the target duration before retrying.
クライアントがプレイリストファイルをリロードして、変更されていないことがわかった場合、再試行する前に、ターゲット期間の半分の期間待機する必要があります。

`hls.js` の場合、以下のように実装されています。

- [hls.js/src/utils/level-helper.ts at f888cb51645695b95cefc165ebd0301370fdf5f0 · video-dev/hls.js](https://github.com/video-dev/hls.js/blob/f888cb51645695b95cefc165ebd0301370fdf5f0/src/utils/level-helper.ts#L404)

## `.ts`

`.ts` のTTLは1時間から1年など、様々に設定可能です。
ライブストリーミング配信では、各 `.ts` ファイルは新しく、いずれにせよオリジンから要求されます。

>[HTTPライブストリーミングとは？| HLSストリーミング | Cloudflare](https://www.cloudflare.com/ja-jp/learning/video/what-is-http-live-streaming/)
セグメント化：動画は長さが数秒のセグメントに分割されます。セグメントの長さは異なりますが、デフォルト値は6秒です（2016年までは10秒でした）。
>[HLS （HTTP Live Streaming）とは何か？ [ Wowza Blog 翻訳 ] | DPSJ](https://www.dpsj.co.jp/tech-articles/wowza-blog-hls)
**セグメントサイズと遅延**
2016 年までは、Apple は HLS に 10 秒のセグメントを使用することを推奨していました。この仕様では、再生を開始する前に 3 つのセグメントをロードする必要がありました。10 秒の推奨事項に固執することで、放送局はセグメントサイズだけで 30 秒の遅延で再生開始することになります。Apple は最終的にデフォルトのセグメントサイズを 6 秒に減らしましたが、それでもライブストリームでは 20 秒近く遅れてしまうことになります。
遅延を減らすための一般的な方法として、セグメントのサイズを小さくすることがありますが、これは低遅延のための「チューニング」と呼ばれています。チャンクを短くすることで、ダウンロード時間を短縮し、高速化することができます。

# キャッシュ設定（オンデマンド再生）
今回はオンデマンド再生のため、`.m3u8` を長めにキャッシュする設定を入れます。

## `.m3u8`

- When incoming requests match…
    - `(http.request.uri.path.extension eq "m3u8")`

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/101361/d74c0124-ca6f-8c00-7ee5-a91c6c258125.png)

## `.ts`

- When incoming requests match…
    - `(http.request.uri.path.extension eq "ts")`

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/101361/eaed0c17-f5d1-2ca4-c93f-88d78dd34646.png)


# キャッシュ確認（オンデマンド再生）
以下のように Cloudflare で HTTP Live Streaming (HLS) をキャッシュすることができました。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/101361/d50c2427-5b85-0a24-108a-eaccf11ad6b0.png)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/101361/b86d2342-2eb8-c11a-65fc-0b9cbda4508c.png)


# その他参考リンク

- [動画配信と戯れる](https://zenn.dev/dai65527/scraps/7b157fe90638e8)
- [hls.jsの推奨設定 - ImageFlux](https://console.imageflux.jp/docs/live/hlsjs)
- [ライブ動画配信プロトコル（HTTP Live Streaming, HLS）の概要図解メモ（AbemaTV／FRESH!）](https://did2memo.net/2017/02/20/http-live-streaming/)