Linode VM で SRT を活用したライブ動画配信用のウェブサーバーを構築する

# 目次
- はじめに
- 本記事のゴールと対象読者
- SRT とは
- 構築する動画配信環境
- 環境構築
	- Linux 環境の準備
	- Nginx をインストールしライブ動画コンテンツの置き場を作成
	- Let's Encrypt で HTTPS 通信の有効化
	- 動画編集用 FFmpeg 準備
	- Linode VM で SRT 通信を受け取るための準備
	- Linux 再起動時の SRT Listener 自動動作開始
	- HLS & DASH コンテンツを生成する FFmpeg スクリプトの準備
		- HLS 用スクリプト作成
		- DASH 用スクリプト作成
		- 両方同時出力用スクリプト作成
		- スクリプトを実行可能にする
	- SRT Caller 用 OBS の準備
- 動作の確認
	- SRT 送信確認
	- FFmpeg スクリプト実行
	- プレイヤー再生確認
- 古いコンテンツの自動削除
- まとめ

# はじめに
IP 及び HTTP の利用を前提とした動画配信サービスの人気上昇とクラウドサービスの普及により、大規模な OTT サービス事業者や放送局から中小規模の企業に至るまで、インターネット及びクラウドサービスを活用した動画配信のユースケースが増えています。一方、動画配信サービスは高性能なコンピューティングリソース及び高帯域かつ安定的なネットワーク環境を求める傾向があります。各企業は動画配信サービスにおける費用削減を試みると同時に高い品質や低遅延など、優れたユーザーエクスペリエンスを維持しようとしています。

そんな中、最近注目を集めている技術として <font color="Magenta">SRT (Secure Reliable Transport)</font> があります。従来放送局などで利用していた伝送方式 (RTMP) が持つ課題を乗り越え、現代のインターネット環境を踏まえて、動画データ伝送における厳しい要求事項を満たすことができる技術として注目されています。似たものとして Zixi などがあります。その中で、SRT は開発元の Haivision 社がオープンソースで公開し、基本無償で利用可能といった背景により、世界中の専門ベンダーが SRT 関連製品やソリューションを次々と出しています。

その SRT を含め、無償で利用可能なソフトウェアのみを利用して、少し本格的なライブ動画配信環境を構築する方法を紹介します。

:::note info
RTMP と SRT の違いについて解説した Haivision 社の記事
[Today’s Alternatives to RTMP for Getting Your Streams to the Internet](https://www.haivision.com/blog/live-video-streaming/alternatives-rtmp-getting-streams-internet/)
:::

# 本記事のゴールと対象読者
本記事では SRT を利用したライブ動画配信サーバーの構築をゴールとします。また、なるべく費用をかけずにライブ動画配信サーバーを素早く構築したい方を対象にしています。

:::note info
本記事の手順をスムーズに進めるにあたり、下記の条件を満たしていることが望ましいです。
- OS (Windows 10, Linux) やソフトウェア (OBS, FFmpeg, Nginx) における基礎的な知識や経験があること
- HTTP を利用した動画配信技術における基礎的な理解があること
- アカマイの Cloud Computing Services アカウントを保有していること
- HTTPS 通信ができるよう Let's Encrypt からの証明書発行してもらうため、自分が利用するドメインに対する DNS レコードの変更権限があること
:::

上記の条件を満たしていれば、約2時間ほどでライブ動画配信ができるウェブサーバーを構築できます。もしアカマイの Cloud Computing Services 自体に馴染みがない、又はアカウントを保有していない方は、下記の記事を参考にしてアカウントを作成してみてください。
<b>[Linodeとは何かをわかりやすく解説](https://qiita.com/isss802/items/5313c7a6da1d9ed50a38)</b>

# SRTについて
![00-0_SRT logo.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3562398/01465b5b-f7ef-bb7d-02a2-6e3aac2870ce.png)

2017年に Haivision 社が発表した SRT は、その名 <font color="Magenta">(Secure Reliable Transport)</font> が示すように、セキュアかつ安定的に動画データを伝送するための技術として誕生しました。不安定でセキュアではないインターネット環境での利用を想定し、その伝送環境の不安定さによるライブ配信へのインパクトやユーザーエクスペリエンスの低下を最低限に抑制することを目的としています。
<b>[SRT Open Source Protocol (SRT とは)](https://www.haivision.com/products/srt-secure-reliable-transport/)</b>

これまでの RTMP のような伝送方式とは異なり TCP ではなく UDP で動作しますが、パケットの伝達保証やエラー補正機能を持たない UDP レイヤーにおいて、 SRT がメディア伝送の特性を踏まえた伝送制御ソフトウェアとして動作します。SRT を利用したデータ送受信を行う場合はその両方のデバイスが SRT に対応している必要があります。また、SRT は最大256ビットの AES 暗号化に対応し、セキュアに動画データを伝送することができます。

また、コンテンツ内容を問わない (Content Agnostic) という特性もあり、X.264 だけでなく VP9, AV1, HEVC を含め様々なコーデックにも対応しています。既存 RTMP が AV1 や HEVC などに対応してないこととは対照的です。

:::note info
本記事のスコープではありませんが、2023年7月 VSO (Veovera Software Organization) が発表した Enhanced RTMP は新しいコーデックにも対応しています。
<b>[Enhanced RTML の概要](https://github.com/veovera/enhanced-rtmp/blob/main/enhanced-rtmp.pdf)</b>
:::

Haivision 社は SRT を利用するためのコードを GitHub に無償公開しているため、誰でも自由に利用できます。
<b>[SRT ソースコードのリポジトリ](https://github.com/Haivision/srt)</b>

また、Haivision 社を中心とした SRT Alliance という団体があり、様々な企業と動画配信技術の研究開発及び普及に力を入れています。
<b>[SRT Alliance ホームページ](https://srtalliance.org/)</b>

その SRT の動作をイメージした図です。
![00-1_SRT high level.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3562398/7cf5a21b-7266-c1e2-e06c-93183fa13a5d.png)

エンコーダー及びデコーダーはソースである大容量の動画データを限られたネットワーク帯域の回線で伝送するための圧縮と伸張を行います。インターネット環境を通じて動画データを伝送する場合、どうしても遅延やパケットロスなどのリスクが存在します。 TCP を伝送方式に用いる場合、その特徴であるパケット再伝送 (Retransmission) などデータをきちんと送るための補正機能が存在します。しかし、動画配信サービスの特徴を考慮すれば TCP が最適な伝送補正処理を行えているとは言えません。

一方、UDP を用いる SRT で動画データを送受信する場合、送受信側両方にバッファーの役割を持つコンポーネントが付きます。SRT 伝送ではこのバッファーを利用し動画配信サービスの特徴を踏まえたデータ伝送補正を行います。その伝送補正に関わるパラメーターは様々であり、各々のユースケースにおける要件に柔軟に対応できるようパラメーター値の調整が利用者に委ねられています。

非常に大まかな説明ですが、このような特性を持つ SRT の利用により、インターネット環境で低遅延かつ安定的な動画配信サービスを実現しやすくなります。

# 構築する動画配信環境
本記事で構築する動画配信環境と重要コンポーネントを表現したワークフロー図です。各手順を進めていく中、自分がどの部分を触っているかを認識しておくと理解しやすいです。
![00-2_TEST env.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3562398/76505867-012b-b292-badf-4f89e27d1dae.png)

:::note info
本記事で利用する主なソフトウェアの Version 一覧
- Ubuntu 22.04 + SRT 1.5.3 (SRT Listener) & FFmpeg 5.1.3 (Transcoder & Packager) & Nginx 1.18.0 (HTTP Web Server)
- Windows 10 (22H2) + OBS 29.1.3 (SRT Caller)
:::

環境を説明する前に SRT の動作モードについて解説します。SRT として動作するコンポーネントはその役割によって3つのモードに分けられます。本記事では Caller と Listener モードのみ登場します。

:::note info
SRT 動作モード
- Caller → 動画データを伝送する宛先アドレス情報 (IP アドレス 及び Port) を知り、SRT 通信を開始する (≒電話で例えると、相手に電話をかける側)
- Listener → Caller から SRT シグナルを受信する側で、Caller のアドレス情報を知る必要はない (≒電話で例えると、相手からの電話信号を待ち受け、かけられてきた電話を取る側)
- Rendezvous → 送受信側におけるロールを分けず、同じ UDP Port を利用して SRT セッションを結ぶ。特殊なネットワーク環境で利用するモード
:::

Caller は宛先アドレスを利用して SRT 通信を開始する役割を持ちます。Listener となるデバイスの IP アドレス 及び Port を事前に知り、通信を開始するという意味で Caller と言います。 Caller としての SRT 機能を搭載した汎用ソフトウェア (FFmpeg, OBS など) が存在します。本記事では、OBS を SRT Caller として使います。

Listener は Caller のアドレスを事前に知る必要がなく、自分の IP アドレス と SRT Listener 用に開いた Port 番号で受信した SRT 通信を処理します。SRT Listener として一度開いた Port は明示的に閉じない限り、開いたままの状態になります。例えば電話機が電話を切った後、いつでも受信可能な状態にあることと同じです。また Caller からの通信が一時的に途切れた場合でも継続して受信処理ができ、同じ Port を利用して別の Caller からの SRT 通信を受信することも可能です。Listener として利用する Port 番号は1024~65535の中で任意で選択できるので、サービス構成の自由度が高く、企業向けファイアウォールの構成とも相性が良いです。本記事では、Haivision 社が公開している SRT リポジトリからソースコードを取得して SRT Listener として利用します。

# 環境構築
では、これよりライブ動画配信サーバー環境の構築を開始します。

### Linux 環境準備
Linode VM を生成します。アカマイの Cloud Computing Services ポータルでログイン後<font color="Magenta">「Create - Linode」</font>を選択します。
![01_creating Linode VM.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3562398/fae3ead5-6812-b704-999a-5ce603fa18ab.png)

生成する VM の各種属性を選択していきます。利用する Linux Distribuition Image としては <font color="Magenta">「Ubuntu 22.04 LTS」</font> を選択し、VM が生成される Region は任意で選択します。エンコーダーとなる OBS と FFmpeg が動くウェブサーバー、そして視聴者となるクライアントの物理的な場所を考慮して Region を選んでください。本記事では<font color="Magenta">「Osaka (jp-osa)」</font>Region を選択しています。
![02_selecting image and region.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3562398/79ff6918-012a-b4eb-35f8-440c3b824acb.png)

Linode Resource プランを選びます。検証目的としてできる限り安価なプランを選びたいので、本記事の例では共有型リソースを提供する<font color="Magenta">「Shared CPU - Linode 8GB」</font>を選択します。
![03_selecting Linode reousrce plan.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3562398/baa52a3e-3e39-f5c8-312c-015195a7fa1a.png)

:::note info
本番環境での運用を検討される場合は占有型リソースを提供する Dedicated CPU プランの利用を推奨します。
:::

:::note warn
本記事の構成では Linode 8GB で十分 (FFmpeg 処理による全体 CPU の利用率が50%を超えない) ですが、ソースとなる動画データの Bitrate や FFmpeg で行う動画処理の内容次第でより高い性能の VM を求められることがあります。必要に応じて適切なプランを選択ください。自分の動画処理がどれほどのリソースを利用しているかは Longview を利用すると分かりやすいです。Longview 利用に関しては下記記事を参照ください。
<b>[Linode Longviewを利用してサーバーの負荷状況を可視化しよう](https://qiita.com/isss802/items/294f1b589e4882075488)</b>

また、動画配信のアクセスが多くなる場合は VM の性能だけを高くしても効率が悪く限界があるため、 CDN の利用を検討してください。
<b>[動画配信 CDN 製品の例 - Akamai AMD](https://www.akamai.com/ja/products/adaptive-media-delivery)</b>
:::

Linode VM の<font color="Magenta">ラベル</font>と <font color="Magenta">Root パスワード</font>を入力 (任意) します。
![04_VM name and password.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3562398/0785ef75-9e18-277c-b48f-bafef4ac1521.png)

必要な情報の入力や選択を完了した後、一番下の<font color="Magenta">「Create Linode」</font>ボタンをクリックすることで Linode VM が生成されます。
![05_commit create Linode.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3562398/0a8e30f9-c1b2-149e-b158-faffc5bf57d4.png)

自動で画面が Linode VM 詳細に遷移します。「PROVISIONING」表示で Linode VM が準備中であることが分かります。
![06_provisionning.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3562398/85b065e9-aed9-bcc3-b566-3198601291c3.png)


Linode VM は数秒で立ち上がり「RUNNING」表示に変わります。その後<font color="Magenta">「Network」</font>タブに移動します。
![07_running.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3562398/1e327e47-f58a-9937-0b5e-9b413dfc515d.png)

「Network」タブでは Linode VM に関連するネットワーク関連情報を確認できます。Reverse DNS アドレスを確認します。<font color="Magenta">IPv4 形式の各オクテットと固定ドメイン<.ip.linodeusercontent.com>の組み合わせ</font>により構成された Reverse DNS アドレスを利用して、該当 Linode VM に SSH アクセス可能です。(IPv4 アドレスを利用して直接アクセスも可能)
![08_check reverse DNS.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3562398/83d569eb-792a-21c8-50bb-1716c71b6684.png)

では、SSH を利用して Linode VM にアクセスします。root アカウントと、事前定義した root パスワードでログイン可能です。ログインに成功したら現在のパッケージをアップグレードします。
```shell-session
apt update -y
apt upgrade -y
```

:::note info
Kernel 及びパッケージのアップグレードに関して
- openssh-server コンフィグレーションファイルの変更可否を問われることがありますが、本記事の内容に影響しないため「keep the local version currently installed」選択します
![openssh-server.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3562398/1d6f6a3f-039a-6bf5-1db4-860dba38c075.png)

- Upgrade による Daemon 再起動するサービスの選択を問われることがありますが、本記事の内容に影響しないためデフォルト選択のままで「OK」選択します
![daemons.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3562398/b8583241-2183-567b-c8da-fed156b6f353.png)
:::

アップグレードが完了したら、一度 Linode VM を再起動し新 Kernel やパッケージをロードします。再起動は数分ほどで完了するので、その後再度 SSH でアクセスします。
```shell-session
reboot
```

### Nginx をインストールしライブ動画コンテンツの置き場を作成
動画配信用ウェブサーバーとして利用する Nginx をインストールします。
```shell-session
apt install nginx -y
```

インストール完了後、Linux 再起動時に自動で起動するようにします。
```shell-session
systemctl enable nginx
```

最後に、FFmpeg で処理した動画コンテンツファイルの置き場を準備します。本ディレクトリーに関連する詳細については後ほど解説します。
```shell-session
mkdir /var/www/html/live
```

ここまで設定が完了したら、Linode VM 詳細画面の「Network」タブで確認した Reverse DNS アドレスを利用して HTTP サーバーの動作を確認します。ウェブブラウザーで下記のようなページが確認できれば成功です。
![09_Nginx work.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3562398/341414ae-db02-9b91-d15f-6d73cd272239.png)

### Let's Encrypt で HTTPS 通信の有効化
<font color="Magenta">(ドメインのレコード変更権限がなく、HTTPS 通信確認が不要であれば本章はスキップ可能)</font>

Let's Encrypt の証明書をセットして HTTPS 通信を有効にする前提条件として、下記条件を満たしてドメイン認証を行う必要があります。条件が満たされないと Let's Encrypt からのドメイン認証が失敗したものと見なされ、証明書が発行されません。

:::note info
Let's Encrypt に証明書を申請するための条件 (いずれか一つで OK)
- Linode VM の Reverse DNS 名を、証明書発行するサービスの公開ホスト名 の CNAME レコードとして登録する
- Linode VM の IPv4 アドレスを、証明書発行するサーバー Hostname の A レコードとして登録する
:::

Let's Encrypt から証明書を発行するツールとして certbot をインストールします。certbot を利用することで自動更新も可能になります。
```shell-session
apt install certbot -y
```

certbot のインストール完了後、証明書発行のリクエストを行います。
```shell-session
certbot certonly --webroot -w /var/www/html -d {証明書申請を行う Hostname}
```


最初の証明書リクエスト時はメールアドレスの入力、及びサービス条件の確認を求められます。
```shell-session
Saving debug log to /var/log/letsencrypt/letsencrypt.log
Enter email address (used for urgent renewal and security notices)
 (Enter 'c' to cancel): {メールアドレスを入力}

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Please read the Terms of Service at
https://letsencrypt.org/documents/LE-SA-v1.3-September-21-2022.pdf. You must
agree in order to register with the ACME server. Do you agree?
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
(Y)es/(N)o: {'Y'を入力}

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Would you be willing, once your first certificate is successfully issued, to
share your email address with the Electronic Frontier Foundation, a founding
partner of the Let's Encrypt project and the non-profit organization that
develops Certbot? We'd like to send you email about our work encrypting the web,
EFF news, campaigns, and ways to support digital freedom.
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
(Y)es/(N)o: {'Y'又は'N'を入力}
Account registered.
Requesting a certificate for {証明書申請を行った Hostname}
```

必要な情報を入力した後 Let's Encrypt によるドメイン認証が正しく行われた場合、下記の結果ログ表示と共に証明書が発行されます。
```shell-session
Successfully received certificate.
Certificate is saved at: /etc/letsencrypt/live/{証明書申請を行った Hostname}/fullchain.pem
Key is saved at:         /etc/letsencrypt/live/{証明書申請を行った Hostname}/privkey.pem
This certificate expires on 2023-12-30.
These files will be updated when the certificate renews.
Certbot has set up a scheduled task to automatically renew this certificate in the background.
```

逆に Let's Encrypt によるドメイン認証が失敗した場合、証明書は発行されず、下記ログが表示されます。
```shell-sesssion
Certbot failed to authenticate some domains (authenticator: webroot). The Certificate Authority reported these problems:
  Domain: {証明書申請を行った Hostname}
  Type:   dns
  Detail: DNS problem: NXDOMAIN looking up A for {証明書申請を行った Hostname} - check that a DNS record exists for this domain; DNS problem: NXDOMAIN looking up AAAA for {証明書申請を行った Hostname} - check that a DNS record exists for this domain

Hint: The Certificate Authority failed to download the temporary challenge files created by Certbot. Ensure that the listed domains serve their content from the provided --webroot-path/-w and that files created there can be downloaded from the internet.

Some challenges have failed.
```

証明書発行に成功した結果ログの中に、下記のように証明書と鍵の保管場所が表示されます。これらを利用し Nginx の HTTPS 通信を有効にします。
> <font color="Magenta">Certificate is saved at: /etc/letsencrypt/live/{証明書申請を行った Hostname}/fullchain.pem
> Key is saved at:         /etc/letsencrypt/live/{証明書申請を行った Hostname}/privkey.pem</font>

Nginx で SSL 通信を有効にするため、Nginx 設定ファイルをアップデートします。HTTPS 通信を有効にするための 443ポートのオープン、証明書と鍵の指定に加え、ここでは CORS ヘッダーの設定を追加しています。
```vim
vim /etc/nginx/sites-available/default

(以下内容を追加、更新し保存)
# SSL configuration
#
listen 443 ssl default_server;
listen [::]:443 ssl default_server;

ssl_certificate "/etc/letsencrypt/live/{証明書申請を行った Hostname}/fullchain.pem";
ssl_certificate_key "/etc/letsencrypt/live/{証明書申請を行った Hostname}/privkey.pem";
ssl_session_cache shared:SSL:1m;
ssl_session_timeout 10m;

add_header Access-Control-Allow-Origin *;
(省略)
```

設定修正後、Nginx を再起動します。
```shell-session
systemctl restart nginx
```

Nginx 再起動後、動画視聴クライアントから、構築したサービスの公開ホスト名 を利用し HTTPS 通信可否を確認します。下記は Chrome での接続結果ですが、URL の部分に鍵マークが現れていれば成功です。
![10_HTTPS-check.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3562398/f2e22df5-0450-e9e1-9e88-f8b319fe9bf9.png)

### 動画編集用 FFmpeg 準備
次は Linode VM にインジェストされた動画データを編集し HLS 及び DASH コンテンツ化するための FFmpeg を準備します。
```shell-session
cd ~
wget https://github.com/BtbN/FFmpeg-Builds/releases/download/autobuild-2023-03-31-12-50/ffmpeg-n5.1.3-linux64-gpl-5.1.tar.xz
tar -xf ffmpeg-n5.1.3-linux64-gpl-5.1.tar.xz
```

FFmpeg が正常動作するか下記コマンドで確認します。問題なく実行できれば Version やライブラリなどを確認できます。
```shell-session
/root/ffmpeg-n5.1.3-linux64-gpl-5.1/bin/ffmpeg -version

(以下内容が表示される)
ffmpeg version n5.1.3-20230331 Copyright (c) 2000-2022 the FFmpeg developers
built with gcc 12.2.0 (crosstool-NG 1.25.0.152_89671bf)
configuration: --prefix=/ffbuild/prefix --pkg-config-flags=--static --pkg-config=pkg-config --cross-prefix=x86_64-ffbuild-linux-gnu- --arch=x86_64 --target-os=linux --enable-gpl --enable-version3 --disable-debug --enable-iconv --enable-libxml2 --enable-zlib --enable-libfreetype --enable-libfribidi --enable-gmp --enable-lzma --enable-fontconfig --enable-libvorbis --enable-opencl --enable-libpulse --enable-libvmaf --enable-libxcb --enable-xlib --enable-amf --enable-libaom --enable-libaribb24 --enable-avisynth --disable-chromaprint --enable-libdav1d --enable-libdavs2 --disable-libfdk-aac --enable-ffnvcodec --enable-cuda-llvm --enable-frei0r --enable-libgme --enable-libkvazaar --enable-libass --enable-libbluray --enable-libjxl --enable-libmp3lame --enable-libopus --enable-mbedtls --enable-librist --enable-libssh --enable-libtheora --enable-libvpx --enable-libwebp --enable-lv2 --enable-libmfx --disable-openal --enable-libopencore-amrnb --enable-libopencore-amrwb --enable-libopenh264 --enable-libopenjpeg --enable-libopenmpt --enable-librav1e --enable-librubberband --disable-schannel --enable-sdl2 --enable-libsoxr --enable-libsrt --enable-libsvtav1 --enable-libtwolame --enable-libuavs3d --enable-libdrm --enable-vaapi --enable-libvidstab --enable-vulkan --enable-libshaderc --enable-libplacebo --enable-libx264 --enable-libx265 --enable-libxavs2 --enable-libxvid --enable-libzimg --enable-libzvbi --extra-cflags=-DLIBTWOLAME_STATIC --extra-cxxflags= --extra-ldflags=-pthread --extra-ldexeflags=-pie --extra-libs='-ldl -lgomp' --extra-version=20230331
libavutil      57. 28.100 / 57. 28.100
libavcodec     59. 37.100 / 59. 37.100
libavformat    59. 27.100 / 59. 27.100
libavdevice    59.  7.100 / 59.  7.100
libavfilter     8. 44.100 /  8. 44.100
libswscale      6.  7.100 /  6.  7.100
libswresample   4.  7.100 /  4.  7.100
libpostproc    56.  6.100 / 56.  6.100
```

### Linode VM で SRT 通信を受け取るための準備
Linode VM 側で SRT インジェストを受信するため、SRT をビルドします。まずは依存性問題を解決するためのパッケージをインストールします。
```shell-session
apt-get install tclsh pkg-config cmake libssl-dev build-essential -y
```

SRT バイナリーをダウンロードし、ビルドします。
```shell-session
wget https://github.com/Haivision/srt/archive/refs/tags/v1.5.3.tar.gz
tar -xf v1.5.3.tar.gz
cd srt-1.5.3 && mkdir _build && cd _build
cmake ..
make
```

SRT ビルドが完了したら、下記コマンドでビルドした SRT の動作を確認します。Listener モードでの動作が確認できれば、Ctrl+C で強制終了します。
```shell-session
/root/srt-1.5.3/_build/srt-live-transmit srt://:2000 udp://127.0.0.1:5000 -v

(以下内容が表示される)
Media path: 'srt://:2000' --> 'udp://127.0.0.1:5000'
Opening SRT source listener on :2000
Binding a server on :2000 ...
 listen...
^C (Ctrl+C 入力)
-------- REQUESTED INTERRUPT!

SrtCommon: DESTROYING CONNECTION, closing sockets (rt%-1 ls%313240613)...
SrtCommon: ... done.
```

### Linux 再起動時の SRT Listener 自動動作開始
rc-local サービスを利用して Linode VM 再起動時にも SRT Listener モードが自動で起動するようにします。まず rc-local.service ファイルを生成します。
```/etc/systemd/system/rc-local.service (作成)
(以下内容を入力し保存)	
[Unit]
 Description=/etc/rc.local Compatibility
 ConditionPathExists=/etc/rc.local

[Service]
 Type=forking
 ExecStart=/etc/rc.local start
 TimeoutSec=0
 StandardOutput=tty
 RemainAfterExit=yes
 SysVStartPriority=99

[Install]
 WantedBy=multi-user.target
```

rc-local サービスで実行するコマンドを記入します。
```/etc/rc.local (作成)
(以下内容を入力し保存)
#!/bin/bash

/root/srt-1.5.3/_build/srt-live-transmit srt://:2000 udp://127.0.0.1:5000 -v
```

rc-local サービス登録準備を完了したら、下記コマンドでサービスを実行可能状態にし Linode VM をリブートします。
```shell-session
chmod +x /etc/rc.local
systemctl enable rc-local
reboot
```

Linode VM リブート完了後、下記コマンドで rc-local サービスの状態を確認します。
```shell-sesion
systemctl status rc-local

(以下内容が表示される)
● rc-local.service - /etc/rc.local Compatibility
     Loaded: loaded (/etc/systemd/system/rc-local.service; enabled; vendor preset: enabled)
    Drop-In: /usr/lib/systemd/system/rc-local.service.d
             └─debian.conf
     Active: activating (start) since Tue 2023-09-26 12:07:01 UTC; 7s ago
Cntrl PID: 614 (rc.local)
      Tasks: 5 (limit: 4558)
     Memory: 4.5M
        CPU: 36ms
     CGroup: /system.slice/rc-local.service
             ├─614 /bin/bash /etc/rc.local start
             └─618 /root/srt-1.5.3/_build/srt-live-transmit srt://:2000 udp://127.0.0.1:5000 -v

Sep 26 12:07:01 localhost systemd[1]: Starting /etc/rc.local Compatibility...
Sep 26 12:07:01 localhost rc.local[618]: Media path: 'srt://:2000' --> 'udp://127.0.0.1:5000'
Sep 26 12:07:01 localhost rc.local[618]: Opening SRT source listener on :2000
Sep 26 12:07:01 localhost rc.local[618]: Binding a server on :2000 ...
Sep 26 12:07:01 localhost rc.local[618]:  listen...
```

この手順により、もし Linode VM がリブートした場合でも SRT Listener がバックグランドで自動開始されることを確認できました。

### HLS & DASH コンテンツを生成する FFmpeg スクリプトの準備
Linode VM で SRT として受信した動画データを FFmpeg を利用し HLS 及び DASH にパッケージング処理するためのスクリプトを作成します。スクリプトは3つ (HLS のみ, DASH のみ, 両方同時出力) 作成します。

:::note info
各スクリプトは下記の属性を持ちます。
- 1080p/720p/480p の Adaptive Bitrate 出力に対応
- 再生中 Bitrate 情報を分かりやすくするため画面の左上にテキストボックスを追加
- HLS のみの場合、セグメントファイル形式は mpegts (.ts)
- DASH のみの場合、セグメントファイル形式は fMP4 (.mp4)
- 両方同時出力 (Both) の場合、HLS 及び DASH のセグメントファイル形式は fMP4 (.mp4)
:::

:::note info
動画コンテンツファイルの保存先情報
(古いコンテンツの自動削除のため、Date 情報をディレクトリー名として利用)
- HLS のみ, DASH のみの場合 : /var/www/html/live/{動画を出力する日の YYYYMMDD}/{都度任意入力する Program 名}/{hls 又は dash}/
- 両方同時出力 (Both) の場合 : /var/www/html/live/{動画を出力する日の YYYYMMDD}/{都度任意入力する Program 名}/both/
- HLS のマスタープレイリストファイル名 : master.m3u8
- DASH のマニフェストファイル名 : manifest.mpd
:::

##### HLS 用スクリプト作成
```hls-publish.sh (作成)
#!/bin/bash
TODAY=$(date '+%Y%m%d')

echo "Input your live program name:"
read PROGRAMNAME

mkdir /var/www/html/live/$TODAY
mkdir /var/www/html/live/$TODAY/$PROGRAMNAME
mkdir /var/www/html/live/$TODAY/$PROGRAMNAME/hls

/root/ffmpeg-n5.1.3-linux64-gpl-5.1/bin/ffmpeg \
        -i udp://:5000 -map 0:v:0 -map 0:a:0 -map 0:v:0 -map 0:a:0 -map 0:v:0 -map 0:a:0 \
        -preset ultrafast -tune zerolatency \
        -c:v libx264 -profile:v baseline -crf 23 -sc_threshold 0 -g 25 -r 25 -c:a aac -ar 48000 \
        -filter:v:0 scale=w=842:h=480:force_original_aspect_ratio=decrease,drawtext=:"text=(480p):fontsize=32:x=32:y=32:box=1:boxcolor=white:fontcolor=black" \
        -b:v:0 1500k -b:a:0 96k \
        -filter:v:1 scale=w=1280:h=720:force_original_aspect_ratio=decrease,drawtext=:"text=(720p):fontsize=32:x=32:y=32:box=1:boxcolor=white:fontcolor=black" \
        -b:v:1 3000k -b:a:1 128k \
        -filter:v:2 scale=w=1920:h=1080:force_original_aspect_ratio=decrease,drawtext=:"text=(1080p):fontsize=32:x=32:y=32:box=1:boxcolor=white:fontcolor=black" \
        -b:v:2 6000k -b:a:2 128k \
        -var_stream_map "v:0,a:0 v:1,a:1 v:2,a:2" \
        -hls_flags independent_segments -hls_flags +program_date_time -hls_segment_type mpegts -hls_time 1 -hls_list_size 5 \
        -format_options "movflags=+cmaf" \
        -master_pl_name master.m3u8 \
        -hls_segment_filename "/var/www/html/media/$TODAY/$PROGRAMNAME/hls/segment-%v_%08d.ts" "/var/www/html/live/$TODAY/$PROGRAMNAME/hls/child-%v.m3u8"
```

##### DASH 用スクリプト作成
```dash-publish.sh (作成)
#!/bin/bash
TODAY=$(date '+%Y%m%d')

echo "Input your live program name:"
read PROGRAMNAME

mkdir /var/www/html/live/$TODAY
mkdir /var/www/html/live/$TODAY/$PROGRAMNAME
mkdir /var/www/html/live/$TODAY/$PROGRAMNAME/dash

/root/ffmpeg-n5.1.3-linux64-gpl-5.1/bin/ffmpeg \
        -i udp://:5000 \
        -preset ultrafast -tune zerolatency -flags +global_header \
        -c:v libx264 -pix_fmt yuv420p -c:a aac -b:a 128k -ac 1 -ar 44100 \
        -use_timeline 0 -use_template 1 -utc_timing_url "https://time.akamai.com/?iso" -index_correction 1 \
        -live 1 -streaming 1 -f dash -seg_duration 1 -dash_segment_type mp4 \
        -ldash 1 -target_latency '0.5' -write_prft 1 -window_size 3 \
        -force_key_frames "expr:gte(t,n_forced*1)" \
        -adaptation_sets "id=0,streams=v id=1,streams=a" \
        -map v:0 -filter:v:0 "scale=-1:1080,drawtext=:text=(6000k):fontsize=32:x=32:y=32:box=1:boxcolor=white:fontcolor=black" \
        -b:v:0 6000k -s:v:0 1920x1080 -profile:v:0 baseline \
        -map v:0 -filter:v:1 "scale=-1:720,drawtext=:text=(3000k):fontsize=32:x=32:y=32:box=1:boxcolor=white:fontcolor=black" \
        -b:v:1 3000k -s:v:1 720x480 -profile:v:1 baseline \
        -map v:0 -filter:v:2 "scale=-1:360,drawtext=:text=(1500k):fontsize=32:x=32:y=32:box=1:boxcolor=white:fontcolor=black" \
        -b:v:2 1500k -s:v:2 640x360 -profile:v:2 baseline \
        -map 0:a \
        -init_seg_name init-stream_r\$RepresentationID\$.mp4 -media_seg_name media-stream_r\$RepresentationID\$-\$Number%08d\$.mp4 \
        -format_options "movflags=+cmaf" \
        /var/www/html/live/$TODAY/$PROGRAMNAME/dash/manifest.mpd
```

##### 両方同時出力用スクリプト作成
```both-publish.sh (作成)
#!/bin/bash
TODAY=$(date '+%Y%m%d')

echo "Input your live program name:"
read PROGRAMNAME

mkdir /var/www/html/live/$TODAY
mkdir /var/www/html/live/$TODAY/$PROGRAMNAME
mkdir /var/www/html/live/$TODAY/$PROGRAMNAME/both

/root/ffmpeg-n5.1.3-linux64-gpl-5.1/bin/ffmpeg \
        -i udp://:5000 \
        -preset ultrafast -tune zerolatency -flags +global_header \
        -c:v libx264 -pix_fmt yuv420p -c:a aac -b:a 128k -ac 1 -ar 44100 \
        -use_timeline 0 -use_template 1 -utc_timing_url "https://time.akamai.com/?iso" -index_correction 1 \
        -live 1 -streaming 1 -f dash -seg_duration 1 -dash_segment_type mp4 \
        -ldash 1 -target_latency '0.5' -write_prft 1 -window_size 3 \
        -force_key_frames "expr:gte(t,n_forced*1)" \
        -adaptation_sets "id=0,streams=v id=1,streams=a" \
        -map v:0 -filter:v:0 "scale=-1:1080,drawtext=:text=(6000k):fontsize=32:x=32:y=32:box=1:boxcolor=white:fontcolor=black" \
        -b:v:0 6000k -s:v:0 1920x1080 -profile:v:0 baseline \
        -map v:0 -filter:v:1 "scale=-1:720,drawtext=:text=(3000k):fontsize=32:x=32:y=32:box=1:boxcolor=white:fontcolor=black" \
        -b:v:1 3000k -s:v:1 720x480 -profile:v:1 baseline \
        -map v:0 -filter:v:2 "scale=-1:360,drawtext=:text=(1500k):fontsize=32:x=32:y=32:box=1:boxcolor=white:fontcolor=black" \
        -b:v:2 1500k -s:v:2 640x360 -profile:v:2 baseline \
        -map 0:a \
        -init_seg_name init-stream_r\$RepresentationID\$.mp4 -media_seg_name media-stream_r\$RepresentationID\$-\$Number%08d\$.mp4 \
        -format_options "movflags=+cmaf" \
        -hls_playlist 1 -hls_master_name master.m3u8 -hls_flags independent_segments -hls_flags program_date_time \
        /var/www/html/live/$TODAY/$PROGRAMNAME/both/manifest.mpd
```

各コンテンツ生成用シェルスクリプトを実行可能にします。
```shell-session
chmod +x hls-publish.sh dash-publish.sh both-publish.sh
```

これで SRT Listener 及び FFmpeg パッケージング処理の準備が完了しました。

### SRT Caller 用 OBS の準備
動画のインジェスト元となるローカル PC に、SRT Caller として利用する OBS をインストールします。OBS は OBS ホームページでダウンロードできます。
<b>[OBS ホームページ](https://obsproject.com/)</b>

OBS をインストール後、実行すると下記の画面が現れますが、いくつか事前にセッティングをしておく必要があります。
![OBS_first-time.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3562398/e0979415-0fa0-7b35-e7bd-d903dddb88be.png)

右下の<font color="Magenta">「Settings」</font>から<font color="Magenta">「Stream」</font>を選択し、SRT Listener になる宛先アドレス情報を入力します。Protocol は SRT、Listener となる Linode VM の IP と SRT としてオープンした Port を入力します。
![OBS_srt.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3562398/bb613e18-bdd6-e5a7-bda6-70b57b6adf4b.png)

<font color="Magenta">「Output」</font>に移動し下記画面と同じように設定します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3562398/4c7931ed-d937-0b3e-80cb-576d775c37aa.png)

:::note info
主な設定変更内容は下記の通りです。
- Output Mode : Advanced 選択
- Rescale Output : チェックして1920x1080を入力
- Bitrate : 6000 Kbps
- Keyframe Interval : 2 s
- CPU Usage Preset : ultrafast
- Tune : zerolatency
:::

<font color="Magenta">「Video」</font>に移動しデフォルト画像度を1920x1080に変更します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3562398/70339c11-c05f-f8bb-6863-9f520f50a4e1.png)

<font color="Magenta">「Advanced」</font>に移動し Stream Delay を無効 (Enable 解除) にします。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3562398/ca0f9bd0-5c6f-8914-b9b2-de1ecede5d83.png)

ここまでの設定が完了したら「Settings」メニューの「Apply → OK」で変更内容を適用します。

:::note info
本記事では音声に関するセッティングを割愛しますが、音声入力機器が既に登録されている場合は「Audio Mixer」が動作している可能性があります。ソースとして利用する音声は必要に応じて設定してください。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3562398/ee82db36-2ca4-6883-d98c-247dce904942.png)
:::

次は、動画データのソースを準備します。本記事ではウェブブラウザーで<font color="Magenta">「https://time.is」</font>を開き、そのウィンドウの表示内容をソースデータとして利用します。まず、ウェブページを開きます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3562398/93e7ed78-9451-ee25-11ff-6fdee028860d.png)

OBS メイン画面の<font color="Magenta">「Sources → +」</font>ボタンをクリックし<font color="Magenta">「Window Capture」</font>を選択します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3562398/f291785b-9bac-c73d-3f8c-9cf4e6800426.png)

「Create new - Window Capture」のまま「OK」を選択します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3562398/e877d479-b3ed-c7a6-250d-7b1060ac423f.png)

ソースとして利用する Window を選択します。ここでは Chrome を利用しているため「Window」の部分で該当しているものを選択します。正しく選択できれば、上のプレビューの部分にブラウザーの画面が映りますので、その状態で「OK」をクリックします。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3562398/034d695a-90ac-5627-3922-70532e2f171a.png)

:::note warn
モニターの画像度と選んでいるデフォルト画像度が異なる場合、このようにブラウザーの画面がはみ出したり、逆に余ることがあります。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3562398/30564bb8-4022-df18-8ed4-1892490a9469.png)

その場合は OBS 画面に表れている赤いボックスの線を操作して、動画データとして変換されるキャンバスとソースデータである Window 画面の大きさを調整します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3562398/26cdce9f-ad99-32ec-bfd8-5c5951132a82.png)
:::

これで SRT Listener としての OBS セッティングを完了しました。

# 動作の確認
### SRT データ送受信確認
では OBS メイン画面の右下メニュー<font color="Magenta">「Start Streaming」</font>ボタンをクリックし SRT データ送信を行います。OBS 最下段のテキスト情報で Streaming 状態が表示されます。
![OBS_ingest.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3562398/e0b5b020-32a3-0b5f-75fb-db7cd78de350.png)

:::note info
OBS Streaming を止める場合は「Stop Streaming」をクリックします。
:::

SRT のデータ送信を確認できたら、Linode VM 側で その動画コンテンツを受信できているかを確認します。
```shell-session
systemctl status rc-local

(以下内容が表示される)
● rc-local.service - /etc/rc.local Compatibility
     Loaded: loaded (/etc/systemd/system/rc-local.service; enabled; vendor preset: enabled)
    Drop-In: /usr/lib/systemd/system/rc-local.service.d
             └─debian.conf
     Active: activating (start) since Tue 2023-09-26 12:07:01 UTC; 17min ago
Cntrl PID: 614 (rc.local)
      Tasks: 6 (limit: 4558)
     Memory: 5.7M
        CPU: 5.551s
     CGroup: /system.slice/rc-local.service
             ├─614 /bin/bash /etc/rc.local start
             └─618 /root/srt-1.5.3/_build/srt-live-transmit srt://:2000 udp://127.0.0.1:5000 -v

Sep 26 12:07:01 localhost rc.local[618]: Binding a server on :2000 ...
Sep 26 12:07:01 localhost rc.local[618]:  listen...
Sep 26 12:20:10 localhost rc.local[618]:  accept...
Sep 26 12:20:10 localhost rc.local[618]:  connected.
Sep 26 12:20:10 localhost rc.local[618]: 12:20:10.917589/srt-live-transm*E:SRT.ea: remove_usock: @617192981 not found as either socket or group. Removing only from epoll system.
Sep 26 12:20:10 localhost rc.local[618]: Accepted SRT source connection
Sep 26 12:20:11 localhost rc.local[618]: 12:20:11.917685/SRT:GC*E:SRT.ei: epoll/update: IPE: update struck E1 which is NOT SUBSCRIBED to @617192981
Sep 26 12:20:11 localhost rc.local[618]: 12:20:11.917738/SRT:GC*E:SRT.ei: epoll/update: IPE: update struck E1 which is NOT SUBSCRIBED to @617192981
Sep 26 12:20:21 localhost rc.local[618]: 12:20:21.837574/SRT:TsbPd!W:SRT.br: @617192980: RCV-DROPPED 4 packet(s). Packet seqno %659043764 delayed for 0.429 ms
Sep 26 12:20:23 localhost rc.local[618]: 12:20:23.231547/SRT:TsbPd!W:SRT.br: @617192980: RCV-DROPPED 11 packet(s). Packet seqno %659043949 delayed for 0.331 ms
```

次は、事前に作成したシェルスクリプトを利用して HLS と DASH 両方のパッケージング処理を行います。

:::note info
ソース動画データのインジェスト (Linode VM への伝送) を FFmpeg パッケージング処理より先に始めた場合、一時的に下記のようなエラーが発生します。本エラーは受信中のソース動画データからキーフレームが見つからないためにパッケージング処理を行えないというメッセージです。しばらく時間が過ぎるとソース動画データのキーフレームを FFmpeg が見つけ、その部分からパッケージング処理が始まります。

[h264 @ 0x564d36c9b9c0] decode_slice_header error
[h264 @ 0x564d36c9b9c0] no frame!
[h264 @ 0x564d36c9b9c0] non-existing PPS 0 referenced
:::

### FFmpeg スクリプト実行
事前に用意したスクリプト (同時出力用) を利用し FFmpeg で HLS 及び DASH としてコンテンツ生成を開始します。HLS のみ及び DASH のみのスクリプトは本記事では解説しませんが、必要に応じて利用してください。
```
./both-publish.sh
Input your live program name: {任意の Program Name を入力}
```

無事にスクリプトが実行されれば、下記のように HLS 及び DASH としてのコンテンツファイルが出力されることを確認できます。
```shell-session
Input #0, mpegts, from 'udp://:5000':
  Duration: N/A, start: 0.000000, bitrate: N/A
  Program 1
    Metadata:
      service_name    : Service01
      service_provider: FFmpeg
  Stream #0:0[0x100]: Video: h264 (Constrained Baseline) ([27][0][0][0] / 0x001B), yuv420p(tv, bt709, progressive), 1920x1080 [SAR 1:1 DAR 16:9], 29.97 fps, 29.97 tbr, 90k tbn
  Stream #0:1[0x101]: Audio: aac (LC) ([15][0][0][0] / 0x000F), 48000 Hz, stereo, fltp, 162 kb/s
Stream mapping:
  Stream #0:0 -> #0:0 (h264 (native) -> h264 (libx264))
  Stream #0:0 -> #0:1 (h264 (native) -> h264 (libx264))
  Stream #0:0 -> #0:2 (h264 (native) -> h264 (libx264))
  Stream #0:1 -> #0:3 (aac (native) -> aac (native))
Press [q] to stop, [?] for help
[Parsed_drawtext_1 @ 0x55a7ee5c4fc0] Using "/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf"
[libx264 @ 0x55a7ee4fa100] using SAR=1/1
[libx264 @ 0x55a7ee4fa100] using cpu capabilities: MMX2 SSE2Fast SSSE3 SSE4.2 AVX FMA3 BMI2 AVX2
[libx264 @ 0x55a7ee4fa100] profile Constrained Baseline, level 4.0, 4:2:0, 8-bit
[libx264 @ 0x55a7ee4fa100] 264 - core 164 - H.264/MPEG-4 AVC codec - Copyleft 2003-2023 - http://www.videolan.org/x264.html - options: cabac=0 ref=1 deblock=0:0:0 analyse=0:0 me=dia subme=0 psy=1 psy_rd=1.00:0.00 mixed_ref=0 me_range=16 chroma_me=1 trellis=0 8x8dct=0 cqm=0 deadzone=21,11 fast_pskip=1 chroma_qp_offset=0 threads=2 lookahead_threads=2 sliced_threads=1 slices=2 nr=0 decimate=1 interlaced=0 bluray_compat=0 constrained_intra=0 bframes=0 weightp=0 keyint=250 keyint_min=25 scenecut=0 intra_refresh=0 rc=abr mbtree=0 bitrate=6000 ratetol=1.0 qcomp=0.60 qpmin=0 qpmax=69 qpstep=4 ip_ratio=1.40 aq=0
[Parsed_drawtext_1 @ 0x55a7ef58a340] Using "/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf"
[libx264 @ 0x55a7ee4f68c0] using SAR=32/27
[libx264 @ 0x55a7ee4f68c0] using cpu capabilities: MMX2 SSE2Fast SSSE3 SSE4.2 AVX FMA3 BMI2 AVX2
[libx264 @ 0x55a7ee4f68c0] profile Constrained Baseline, level 3.0, 4:2:0, 8-bit
[libx264 @ 0x55a7ee4f68c0] 264 - core 164 - H.264/MPEG-4 AVC codec - Copyleft 2003-2023 - http://www.videolan.org/x264.html - options: cabac=0 ref=1 deblock=0:0:0 analyse=0:0 me=dia subme=0 psy=1 psy_rd=1.00:0.00 mixed_ref=0 me_range=16 chroma_me=1 trellis=0 8x8dct=0 cqm=0 deadzone=21,11 fast_pskip=1 chroma_qp_offset=0 threads=2 lookahead_threads=2 sliced_threads=1 slices=2 nr=0 decimate=1 interlaced=0 bluray_compat=0 constrained_intra=0 bframes=0 weightp=0 keyint=250 keyint_min=25 scenecut=0 intra_refresh=0 rc=abr mbtree=0 bitrate=3000 ratetol=1.0 qcomp=0.60 qpmin=0 qpmax=69 qpstep=4 ip_ratio=1.40 aq=0
[Parsed_drawtext_1 @ 0x55a7efec5100] Using "/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf"
[libx264 @ 0x55a7ee4f2680] using SAR=1/1
[libx264 @ 0x55a7ee4f2680] using cpu capabilities: MMX2 SSE2Fast SSSE3 SSE4.2 AVX FMA3 BMI2 AVX2
[libx264 @ 0x55a7ee4f2680] profile Constrained Baseline, level 3.0, 4:2:0, 8-bit
[libx264 @ 0x55a7ee4f2680] 264 - core 164 - H.264/MPEG-4 AVC codec - Copyleft 2003-2023 - http://www.videolan.org/x264.html - options: cabac=0 ref=1 deblock=0:0:0 analyse=0:0 me=dia subme=0 psy=1 psy_rd=1.00:0.00 mixed_ref=0 me_range=16 chroma_me=1 trellis=0 8x8dct=0 cqm=0 deadzone=21,11 fast_pskip=1 chroma_qp_offset=0 threads=2 lookahead_threads=2 sliced_threads=1 slices=2 nr=0 decimate=1 interlaced=0 bluray_compat=0 constrained_intra=0 bframes=0 weightp=0 keyint=250 keyint_min=25 scenecut=0 intra_refresh=0 rc=abr mbtree=0 bitrate=1500 ratetol=1.0 qcomp=0.60 qpmin=0 qpmax=69 qpstep=4 ip_ratio=1.40 aq=0
[dash @ 0x55a7ee4e7b40] Opening '/var/www/html/live/20230930/test001/both/init-stream_r0.mp4' for writing
[dash @ 0x55a7ee4e7b40] Opening '/var/www/html/live/20230930/test001/both/init-stream_r1.mp4' for writing
[dash @ 0x55a7ee4e7b40] Opening '/var/www/html/live/20230930/test001/both/init-stream_r2.mp4' for writing
[dash @ 0x55a7ee4e7b40] Opening '/var/www/html/live/20230930/test001/both/init-stream_r3.mp4' for writing
Output #0, dash, to '/var/www/html/live/20230930/test001/both/manifest.mpd':
  Metadata:
    encoder         : Lavf59.27.100
  Stream #0:0: Video: h264, yuv420p(tv, bt709, progressive), 1920x1080 [SAR 1:1 DAR 16:9], q=2-31, 6000 kb/s, 29.97 fps, 30k tbn (default)
    Metadata:
      encoder         : Lavc59.37.100 libx264
    Side data:
      cpb: bitrate max/min/avg: 0/0/6000000 buffer size: 0 vbv_delay: N/A
  Stream #0:1: Video: h264, yuv420p(tv, bt709, progressive), 720x480 [SAR 32:27 DAR 16:9], q=2-31, 3000 kb/s, 29.97 fps, 30k tbn
    Metadata:
      encoder         : Lavc59.37.100 libx264
    Side data:
      cpb: bitrate max/min/avg: 0/0/3000000 buffer size: 0 vbv_delay: N/A
  Stream #0:2: Video: h264, yuv420p(tv, bt709, progressive), 640x360 [SAR 1:1 DAR 16:9], q=2-31, 1500 kb/s, 29.97 fps, 30k tbn
    Metadata:
      encoder         : Lavc59.37.100 libx264
    Side data:
      cpb: bitrate max/min/avg: 0/0/1500000 buffer size: 0 vbv_delay: N/A
  Stream #0:3: Audio: aac (LC), 44100 Hz, mono, fltp, 128 kb/s
    Metadata:
      encoder         : Lavc59.37.100 aac
[dash @ 0x55a7ee4e7b40] Opening '/var/www/html/live/20230930/test001/both/media-stream_r3-00000001.mp4.tmp' for writing
[dash @ 0x55a7ee4e7b40] Opening '/var/www/html/live/20230930/test001/both/manifest.mpd.tmp' for writing
[dash @ 0x55a7ee4e7b40] Opening '/var/www/html/live/20230930/test001/both/master.m3u8.tmp' for writing
[dash @ 0x55a7ee4e7b40] Opening '/var/www/html/live/20230930/test001/both/media-stream_r0-00000001.mp4.tmp' for writing
[dash @ 0x55a7ee4e7b40] Opening '/var/www/html/live/20230930/test001/both/manifest.mpd.tmp' for writing
[dash @ 0x55a7ee4e7b40] Opening '/var/www/html/live/20230930/test001/both/media-stream_r1-00000001.mp4.tmp' for writing
[dash @ 0x55a7ee4e7b40] Opening '/var/www/html/live/20230930/test001/both/manifest.mpd.tmp' for writing
[dash @ 0x55a7ee4e7b40] Opening '/var/www/html/live/20230930/test001/both/media-stream_r2-00000001.mp4.tmp' for writing
[dash @ 0x55a7ee4e7b40] Opening '/var/www/html/live/20230930/test001/both/manifest.mpd.tmp' for writing
[dash @ 0x55a7ee4e7b40] Opening '/var/www/html/live/20230930/test001/both/media-stream_r3-00000002.mp4.tmp' for writing
[dash @ 0x55a7ee4e7b40] Opening '/var/www/html/live/20230930/test001/both/manifest.mpd.tmp' for writing
[dash @ 0x55a7ee4e7b40] Opening '/var/www/html/live/20230930/test001/both/media_3.m3u8.tmp' for writing
[dash @ 0x55a7ee4e7b40] Opening '/var/www/html/live/20230930/test001/both/media-stream_r0-00000002.mp4.tmp' for writing
(以下省略)
```

:::note info
FFmpeg により払い出される動画コンテンツファイルの置き場のネーミングルールとして、Linux のシステム時刻データ (date) から年月日 (YYYYMMDD) 部分を抜き出したものをディレクトリー名として利用する形にしています。また、その時刻データは UTC (+00:00) を基準にしています。
:::

:::note warn
同じ日 (UTC) で複数のライブ配信を行う場合、一度利用した Program Name の再利用はできません。Program Name が重複すると新旧のライブ動画コンテンツデータが混ざってしまうため、正しいライブ動画の配信ができなくなります。
:::

### プレイヤー再生確認
最後、プレイヤーを利用してライブ動画の再生確認を行います。ここではウェブブラウザーで利用可能な標準プレイヤーを利用します。

■ HLS プレイヤーで再生確認
<b>[標準 HLS プレイヤー (hls.js)](https://hlsjs.video-dev.org/demo/)</b>
<font color="Magenta">再生用プレイバック URL - https://{Hostname}/live/{Date}/{Program Name}/both/master.m3u8</font>

■ DASH プレイヤーで再生確認
<b>[標準 DASH プレイヤー (dash.js)](https://reference.dashif.org/dash.js/latest/samples/dash-if-reference-player/index.html)</b>
<font color="Magenta">再生用プレイバック URL - https://{Hostname}/live/{Date}/{Program Name}/both/manifest.mpd</font>

下記はプレイヤー再生画面の例です。上の大きな画面がソースとなる部分で、左下が HLS、右下が DASH プレイヤーでの再生となります。
![both_test_result.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3562398/d44cde2d-4d42-a5dc-087c-3733d7d491cc.png)

プレイヤーでの再生確認が完了したら、FFmpeg の処理や OBS でのデータ送信を止めます。

:::note info
FFmpeg 処理を止める方法
- キーボードボタン「Q」入力でパッケージング処理終了可能
- 「Q」で終了できない場合、Ctrl+C で強制終了
:::

# 古いコンテンツの自動削除
これで SRT でインジェストしたソースデータで HLS 及び DASH としてのライブ動画配信ができることを確認しました。ただ、この構成ではライブ配信用の動画コンテンツを Linode VM に紐づいた Block Storage に保存し続けてしまいます。Block Storage の空き容量がなくなると正常な動作ができなくなります。その問題を防止するため、指定した期限を過ぎた古い動画コンテンツファイルを自動削除するシェルスクリプトを作成します。

下記は出力後30日を経過した動画コンテンツが保存されているディレクトリーを自動削除するスクリプトの例です。前述で説明したディレクトリーの構造を利用したものです。
```cleanup.sh (作成)
#!/bin/bash

# Define the directory where the old directories are located
dir_to_clean="/var/www/html/live"

# Calculate the cutoff date (30 days ago)
cutoff_date=$(date -d "30 days ago" +%Y%m%d)

# Remove directories older than the cutoff date
find "$dir_to_clean" -type d -name '[0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9]' -maxdepth 1 | \
while read -r dir; do
    dir_name=$(basename "$dir")
    if [ "$dir_name" -le "$cutoff_date" ]; then
        rm -r "$dir"
        echo "Removed directory: $dir"
    fi
done
```

:::note info
30日以上前のデータ削除ではなく、7日以上前のデータを削除したい場合は下記の「"30 days ago"」を「"7 days ago"」に変更します。
cutoff_date=$(date -d "<b><font color="Red">7</font></b> days ago" +%Y%m%d)
:::

このスクリプトを Linux に毎日0時0分に自動実行させるようにするため、cron を利用します。
```shell-session
chmod +x cleanup.sh
crontab -e

(以下が表示される)
no crontab for root - using an empty one

Select an editor.  To change later, run 'select-editor'.
  1. /bin/nano        <---- easiest
  2. /usr/bin/vim.basic
  3. /usr/bin/vim.tiny
  4. /bin/ed

Choose 1-4 [1]: {このまま Enter}

(nano 編集機で cron を修正できる。下記コードを入力し保存する)
0 0 * * * /root/cleanup.sh
```

これで古いコンテンツを自動削除できるようになったため、Linode VM に紐づいた Block Storage の容量を管理しやすくなります。

:::note warn
高画質かつ複数の ABR (Adaptive Bitrate) スタックを持つ動画を長時間出力し続ける場合 (≒Linear 配信サービス)、30日より短い間隔でのコンテンツ削除や、Linode VM に紐づく Block Storage 容量の拡張が必要です。
:::

# まとめ
ここまでの手順を実行することにより、下記の結果を得ることができました。

:::note info
- 不安定なインターネット区間でのソース動画データを安定的に伝送 → SRT (OBS + SRT Binary)
- 受信したソースの動画データをウェブ配信に適したフォーマット (HLS 及び DASH) としてリアルタイム変換 → FFmpeg
- ウェブサーバーを構築しライブ動画配信サーバーを構築 → Nginx
- HTTPS 通信用の証明書を発行 → Let's Encrypt
- 古い動画コンテンツを自動削除し、費用がかかるストレージの利用量を削減 → rc-local
- これらすべてをクラウド上の VM 一つの中で安価に実現 → Linode VM
:::

構築に利用したソフトウェアはすべて無償利用が可能なものであり、ほぼ費用をかけず Linode VM 上で動作させることができました。今後、本記事の事例以外にも様々なユースケースが発掘されることを期待します。