Amazon IVS を利用して簡易的な動画配信プラットフォームサービスを作る

# 概要
本記事では、2020/07/15にGAとなった [Amazon Interactive Video Service](https://aws.amazon.com/jp/ivs/) (以下 IVS) を利用して、簡易的な動画配信プラットフォームサービスを作成する。
尚、実装は PHP / Lumen を用いて実装している。

# IVS とは
簡単にセットアップが可能なマネージド型のライブストリーミング用のサービスで、低遅延な映像体験も特徴のひとつである。
IVSに最適化されたプレイヤーSDKも提供しており、プラットフォーム間の機能差異を意識することなく、サービスの提供を可能としている。
https://aws.amazon.com/jp/ivs/features/

# プラットフォーム実装のための構成
簡易的な動画配信プラットフォームを作成するためには以下の構成で実現することが可能である。

![Untitled (6).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/61893/19285f17-3091-0a6e-918a-ffafc2a83c24.png)

IVS からストリームの開始・終了したイベントは Amazon EventBridge を利用して受け取り、SNSやLambdaにイベントの内容を渡すことが出来る。なお、構成図上は EventBridge と Lambda の構成としている。
Lambda からイベントの内容に合わせて、プラットフォーム側のAPIを実行することでプラットフォームサービス側での各チャネルの配信状態を把握することが可能となる。

IVSのAPIは以下のリージョンで利用可能である。東京リージョンは現状だと利用できないが、
動画の配信・視聴に関してはCDNを利用して、世界中のどこからでも利用可能である。
- 欧州 (アイルランド) (eu-west-1)
- 米国東部 (バージニア北部) (us-east-1)
- 米国西部 (オレゴン) (us-west-2)

今回はプラットフォーム実装用のインスタンスは東京リージョンを EventBrigde, Lambda, IVS に関しては オレゴンリージョンで作成した。
また、チャネルの作成や配信状態を管理するために1つのテーブル(stream)を定義した。

| name         | type     | description |
|--------------|----------|-------------|
| id           | int      |             |
| uuid         | varchar  | 識別用ID    |
| name         | varchar  | タイトル    |
| ivs_arn      | varchar  | ivsのARN     |
| playback_url | text     | 視聴URL            |
| rtmp_url     | varchar  | 配信用URL            |
| stream_key   | varchar  | 配信用のストリームキー            |
| is_live      | boolean  |配信中の判定フラグ  |
| is_delete    | boolean  | 削除フラグ   |
| created_at   | datetime |             |
| updated_at   | datetime |             |

migrationファイルは以下になる。

```php
        Schema::create('stream', function (Blueprint $table) {
            $table->id();
            $table->string('uuid')->unique();
            $table->string('name');
            $table->string('ivs_arn');
            $table->text('playback_url');
            $table->string('rtmp_url');
            $table->string('stream_key');
            $table->boolean('is_live')->default(false);
            $table->boolean('is_delete')->default(false);
            $table->timestamps();
        });
```

# チャネルの発行・登録
IVSからのチャネル・ストリームキーの発行は CreateChannelのAPIを利用する。
チャネルを作成する際に配信を行うタイプを指定する`type`と低遅延機能を利用するか否かの`latencyMode`を設定する必要がある。
設定しない場合は `type: STANDARD`, `latencyMode: LOW` で登録される。
またチャネルには名前を設定することが可能となっており、名前を設定することでマネコン上でも管理がしやすくなる。

配信タイプに関しては `STANDARD`, `BASIC` の2種類がある。

```
STANDARD: Multiple qualities are generated from the original input, to automatically give viewers the best experience for their devices and network conditions. Vertical resolution can be up to 1080 and bitrate can be up to 8.5 Mbps.

BASIC: Amazon IVS delivers the original input to viewers. The viewer’s video-quality choice is limited to the original input. Vertical resolution can be up to 480 and bitrate can be up to 1.5 Mbps. 
```

STANDARD の場合は配信された動画に対してトランスコードが行われて、ソース画質(1080p)以外にも複数のレンディションが含まれる。(配信可能なビットレートは最大 8.5Mbps)
一方 BASIC の場合はトランスコードを行わず、最大480pのソース画質のみを提供する。(配信可能なビットレートは最大 1.5Mbps)

タイプの違いにより input 側の料金が大きく変わるためサービス提供に合わせてタイプの設定が必要になる。
(inputの料金は STANDARD: 2.00 USD/h, BASIC: 0.20 USD/h なため、トランスコードを行わない分BASICは 1/10の価格になっている。)

APIを実行すると以下の形式でレスポンスが返ってくる。作成したタイミングでストリームキーも発行されるため、チャネルの発行のみで配信を行うのに必要な情報はそろう。
`arn` に関してはチャネルの操作や配信開始の通知の受け取りに必要な情報のためDBなどに登録しておく。
その他、配信に必要な `ingestEndpoint`, `streamKey`, 視聴に必要な `playbackUrl` も合わせて登録しておくと
視聴時に必要な url をAPI経由で取得する手間も省ける。

```
{
   "channel": { 
      "arn": "string",
      "authorized": boolean,
      "ingestEndpoint": "string",
      "latencyMode": "string",
      "name": "string",
      "playbackUrl": "string",
      "tags": { 
         "string" : "string" 
      },
      "type": "string"
   },
   "streamKey": { 
      "arn": "string",
      "channelArn": "string",
      "tags": { 
         "string" : "string" 
      },
      "value": "string"
   }
}
```


チャネルの発行部分をPHPで実装するとこのような形になる。
IVS側の name に関しては管理しやすいように UUID を発行して登録をしている。
配信を行う URL は `rtmps://${ingestEndpoint}/app` の形式になるため、DBに登録するタイミングでフォーマットに合わせる。

```php
  public function createStream(Request $request)
  {
    $request = $request->json()->all();

    if (empty($request['name'])) {
      return response()->json(['message' => 'empty name'], 400);
    }

    $uuid = Uuid::uuid4();
    $ivs = App::make('aws')->createClient('ivs');
    $result = $ivs->createChannel([
      'latencyMode' => 'LOW',
      'name' => $uuid->toString(),
      'type' => 'STANDARD',
    ]);

    $stream = Stream::create([
      'uuid' => $uuid->toString(),
      'name' => $request['name'],
      'ivs_arn' => $result['channel']['arn'],
      'playback_url' => $result['channel']['playbackUrl'],
      'rtmp_url' => 'rtmps://' . $result['channel']['ingestEndpoint'] . '/app',
      'stream_key' => $result['streamKey']['value']
    ]);

    return response()->json($stream->makeVisible(['rtmp_url', 'stream_key']), 201);
  }
```

https://docs.aws.amazon.com/ivs/latest/APIReference/API_CreateChannel.html

# 配信開始・終了の受け取り
IVSからの配信開始・終了の受け取りには Amazon EventBridge を利用して受け取る必要がある。
IVS から EventBridge への通知は`IVS Stream State Change`(ステータスの変更通知), `IVS Stream Health Change`(ヘルスステータスの変更通知), `IVS Limit Breach`(リミット超過通知) の3種類が存在する。

配信開始・終了に関しては`IVS Stream State Change`から通知されるため、こちらのイベントタイプを使用する。
`IVS Stream State Change` のイベントタイプから送られてくるイベントは、以下の3種類存在する。

|イベント名|内容|
|:---:|:---:|
|Stream Start|ストリームが視聴可能になるイベント|
|Stream End|ストリームが視聴不能になるイベント|
|Stream Failure|何らかの原因でストリームに失敗したイベント|

配信開始・終了に関しては `Stream Start`, `Stream End`のイベントを受けて処理を行う。
IVS からイベントを受け取った際に以下の形式で受け取る。

```json
{
   "version": "0",
   "id": "01234567-0123-0123-0123-012345678901",
   "detail-type": "IVS Stream State Change",
   "source": "aws.ivs",
   "account": "aws_account_id",
   "time": "2017-06-12T10:23:43Z",
   "region": "us-east-1",
   "resources": [
     "arn:aws:ivs:us-east-1:aws_account_id:channel/12345678-1a23-4567-a1bc-1a2b34567890"
   ],
   "detail": {
     "event_name": "Stream Start"
   }
}
```

`event_name` に通知されるイベント名が記載されているため、イベントに合わせて通知先で処理する必要が出てくる。
また、`resources` に対象のARNが記載されているため、ARNに対して配信開始の処理を行う。
上記の例は `Stream Start` の例であるが他のイベントでも detail 内に情報が入っている。
なお、`Stream Start`, `Stream End` に関しては event_name 以外の情報が含まれていない。
その他のイベントに関してはドキュメント上に例は記載されているが本記事では割愛する。

通知先のlambda functionに関しては `event_name` の内容で実行するAPIを変えるように実装に行っている。
EventBrigde 登録前に作成しておく。

```js

var request = require('request');

const HOST = '';
const EVENT_STREAM_START = 'Stream Start';
const EVENT_STREAM_END = 'Stream End';
const LIVE_START_ENDPOINT = '/api/v1/live/start';
const LIVE_STOP_ENDPOINT = '/api/v1/live/stop';

exports.handler = function(event, context) {

    var actionEndpoint = null;
    const params = {
      arn: event.resources[0]
    }

    switch (event.detail.event_name) {
      case EVENT_STREAM_START:
        actionEndpoint = LIVE_START_ENDPOINT;
        break;
      case EVENT_STREAM_END:
        actionEndpoint = LIVE_STOP_ENDPOINT;
        break;
      default: 
        return;
    }

    const options = {
      uri: HOST + actionEndpoint,
      headers: {
        "Content-type": "application/json",
      },
      json: params
    };

    request.post(options, function(error, response, body){});
}
```


`IVS Stream State Change` の設定を受け取るために EventBridge の設定を行う。
イベントタイプをすべてのイベントに設定することで3つのイベントタイプすべてを受け取ることが可能となるが、今回は`IVS Stream State Change`のイベントのみのため以下の形で設定を行う。

![コメント 2020-08-16 235518.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/61893/21785823-bebe-b2ff-3b9b-6d1f015199d7.jpeg)

ターゲットに関しては先ほど作成したlambda function を指定する。
![コメント 2020-08-16 235419.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/61893/1bc0f493-45dd-a3be-704e-82020fefa718.jpeg)

最後にlambdaから実行されるAPIを実装する。
配信開始・終了の両方とも処理としてはサービス側に登録しているARNの情報を取得し、配信状態を切り替える。
ただ配信開始時に登録されていないARNで来た場合に IVS側でストリームの停止を行えるAPI(stopStream)があるため、APIを実行して配信を停止させる処理を入れている。

* 配信開始時

```php
  public function postLiveStart(Request $request)
  {
    $request = $request->json()->all();

    $condition = [
      'ivs_arn' => $request['arn'],
      'is_delete' => false,
      'is_live' => false
    ];

    $stream = Stream::where($condition)
              ->orderBy('updated_at', 'desc')
              ->first();

    if (!$stream) {
      $ivs = App::make('aws')->createClient('ivs');
      $ivs->stopStream([
        'channelArn' => $request['arn']
      ]);
      return response()->json(['message' => 'not found streams'], 404);
    }

    $stream->is_live = true;
    $stream->save();

    return response()->json([]);
  }
```

* 配信終了時

```php
  public function postLiveStop(Request $request)
  {
    $request = $request->json()->all();

    $condition = [
      'ivs_arn' => $request['arn'],
      'is_delete' => false,
      'is_live' => true
    ];

    $stream = Stream::where($condition)
              ->orderBy('updated_at', 'desc')
              ->first();

    if (!$stream) {
      return response()->json(['message' => 'not found streams'], 404);
    }

    $stream->is_live = false;
    $stream->save();

    return response()->json([]);
  }
```


https://docs.aws.amazon.com/ivs/latest/userguide/SUE.html
https://docs.aws.amazon.com/ivs/latest/APIReference/API_StopStream.html

# 実際に配信をする
APIを使ってチャネルを作成して、生成したURL,ストリームキー(StreamName)を登録して配信を開始する
![コメント 2020-08-23 175805.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/61893/64962066-4ca9-aed7-97af-865bdfd03be8.jpeg)

配信開始すると、EventBridgeを経由して lambda function が invoke される。その後配信開始処理が行われて、視聴が可能になる。
本記事では紹介をしていないが view の部分にプレイヤーSDKの組み込みや登録したチャネルの取得APIを追加実装してページを作成すると最終的にはこのようにブラウザ上で配信の視聴が可能になる。
![Inkedコメント 2020-08-17 231932_LI.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/61893/6d1566aa-b20a-b399-cd88-733f9863c3a3.jpeg)

# 最後に
本記事では IVS を利用して簡易的な動画配信プラットフォームを作成した。
IVSで用意されているAPIを利用することで自前で配信基盤を準備することなく利用できるため、動画配信プラットフォームを作りたい人にとってはとても敷居が低くなったのではないかと思われる。
IVSは他にも時間指定のmetadataを登録したり、視聴時のURLに認証機能を持たせるPlayback Auth の機能などがあるため、用途に合わせて活用していくことで、様々な特徴を持ったサービスがどんどんできていくのではないかと期待している。

# 参考
https://aws.amazon.com/jp/ivs/
https://docs.aws.amazon.com/ivs/latest/userguide/what-is.html

# 実装コード
https://github.com/clom/yu-streaming