Amazon Kinesis Video Streams の Producerをブラウザで実現したかったお話

# Abstract
* Amazon Kinesis Video Streams にブラウザから配信したかった。
* 問題いは多いものの、実現はできた。
* 実用には向かなそう。

# Amazon Kinesis Video Streams の概略
>  再生、分析、機械学習のためにメディアストリームをキャプチャ、処理、保存します。
> （https://aws.amazon.com/jp/kinesis/video-streams/）

リアルタイムな動画ストリーミングサービスで、配信、アーカイブのみならずMLフレームワークによる分析にも有用らしいです。

ユーザーが実装分はストリームを流す側と再生する側のふたつに大別できそうです。前者は**Producer** Library、後者は**Stream Parser** LibraryとしてSDKが提供されています。また、再生側にはHLS、MPEG-DASHなどの既存のストリーム規格にしてくれるAPIもあります。

# なぜブラウザか
Webアプリがベースのシステムに組み込むために、ブラウザで動作してくれれば最も完結な実装になるのではという期待がありました（実際そうはなりませんでしたが）。SDKには含まれていませんが、REST APIがあるので実装は可能そうに見えました。

# Producer Library SDK
まずはJavaScriptの Producer Library SDKをダウンロードしましょう。公式サイトのリソースに

> ダウンロード
> プロデューサー SDK - Java
> プロデューサー SDK - C++
> プロデューサー SDK - Android
> （https://aws.amazon.com/jp/kinesis/video-streams/resources）

**ありません！** Webアプリへの組み込みは想定していないということでしょうか？中身はREST APIなAWSなのに、です。とはいえ、**ないものは作るしかないでしょう**。

# API
まずはProcuer LibraryがAWSとやり取りするためのAPIを探します。このあたりはドキュメント（https://docs.aws.amazon.com/kinesisvideostreams/latest/dg/API_Reference.html）に書かれていますので、ここではざっくりと。

Amazon Kinesis Video StreamsのAPIは3種類あります。まずは Amazon Kinesis Video Streams。そのままの名前で、ストリームの管理など、殆どの操作はここにあります。（事前にストリームは作成済みであれば）今回使うのは GetDataEndpoint だけです。次に、Amazon Kinesis Video Streams Archived Mediaというとても長いサービス。HLSやMPEG-DASHなどのストリーミングURLを取得できます。今回は使いません。最後に、Amazon Kinesis Video Streams Media。ストリーミング内のビデオを直接取得する GetMedia と、ストリーミングにデータを追加する PutMedia があります。Producerが使うのはこの PutMedia です。

# 配信
前述のAPIを使ったProducerによるストリーミング配信の流れは下図のようになります。

![REST.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/575392/9966e1e5-0c07-a384-f1ad-68eb10ca1279.png)

1. GetDataEndpoint を呼び出し、PutMediaに利用するDataEndpointのURLを取得
2. URLを返却
3. 2のURLでクライアントを作成
4. ソース（カメラ、マイクなど）からのストリーミングを開始
5. PutMedia を呼び出し ※コネクションは開いたまま
6. ソースからデータが送られる。
7. 5のコネクションに送信
8. 以下略

# ブラウザでの実装
最終的なコードは https://github.com/esnya/amazon-kinesis-video-streams-producer-js-demo にあります。

ブラウザでAWSを利用する場合、JavaScript SDK を使うのが簡潔でしょう。
まずはインストールします。

```bash
npm install --save aws-sdk
```

Amazon Kinesis Video Streamsのクライアントを作成します。

※以下、TypeScriptで書かれています。もちろんJavaScriptでも実装できますので、適宜読み替えてください。

```ts
import AWS from 'aws-sdk';

AWS.config.update({
  region: 'ap-northeast-1',
  accessKeyId: 'xxxx',
  secretAccessKey: 'xxx',
});

const kinesisVideo = new AWS.KinesisVideo();
```

PutMediaのためのEndpointのURLを取得します。（ここからのコードは、async-awaitを使っているので何らかの`async function`の中に書きます。）

```ts
const { DataEndpoint } = await kinesisVideo.getDataEndpoint({
  APIName: 'PUT_MEDIA',
  StreamARN: 'xxxx', // or StreamName: 'xxxx',
}).promise();

const dataEndpoint = DataEndpoint;
if (!dataEndpoint) throw new Error('Failed to get DataEndpoint');
```

取得した`dataEndpoint` を使って Amazon Kinesis Video Streams Media のクライアントを作成します。

```ts
const kinesisVideoMedia = AWS.KinesisVideoMedia({
  endpoint: dataEndpoint,
});
```

PutMedia を呼び出し……

```ts
const putMediaRes = await kinesisVideoMedia.putMedia({
  FragmentTimecodeType: 'ABSOLUTE',
  Body: media, // 適当な空データ
  StreamARN: 'xxxx', // or StreamName: 'xxxx',
}).promise();
```

```
TSError: ⨯ Unable to compile TypeScript:
src/index.ts:47:21 - error TS2339: Property 'putMedia' does not exist on type 'KinesisVideoMedia'.

19   const putMediaRes = await kinesisVideoMedia.putMedia({
```

**putMediaというプロパティはありません**。ドキュメント（https://docs.aws.amazon.com/AWSJavaScriptSDK/latest/AWS/KinesisVideoMedia.html ）にも載っていないので、未実装なようです。

ないので作ります。 `aws-sdk`パッケージの実装を参考に、

`PutMedia.json`

```json
{
  "http": {
    "requestUri": "/putMedia",
    "method": "POST"
  },
  "input": {
    "type": "structure",
    "required": [
      "FragmentTimecodeType"
    ],
    "members": {
      "StreamName": {
        "location": "header",
        "locationName": "x-amzn-stream-name"
      },
      "StreamARN": {
        "location": "header",
        "locationName": "x-amzn-stream-arn"
      },
      "FragmentTimecodeType": {
        "location": "header",
        "locationName": "x-amzn-fragment-timecode-type"
      },
      "ProducerStartTimestamp": {
        "location": "header",
        "locationName": "x-amzn-producer-start-timestamp"
      },
      "ContentType": {
        "location": "header",
        "locationName": "Content-Type"
      },
      "Body": {
        "streaming": true,
        "type": "blob"
      }
    },
    "payload": "Body"
  },
  "output": {
    "type": "structure",
    "members": {
      "Payload": {
      }
    },
    "payload": "Payload"
  }
}
```

`KinesisVideoMedia.js`

```js
const AWS = require('aws-sdk');
const Base = require('aws-sdk/apis/kinesis-video-media-2017-09-30.min.json');
const PutMedia = require('./PutMedia.json');

const Model = {
  ...Base,
  operations: {
    ...Base.operations,
    PutMedia,
  },
};

const service = {};
AWS.apiLoader.services['kinesisvideomedia'] = service;
const KinesisVideoMedia = AWS.Service.defineService('kinesisvideomedia', [
  '2017-09-30',
]);

const attributes = {
  get: function get() {
    const model = Model;
    model.paginators = {};
    return model;
  },
  enumerable: true,
  configurable: true,
};
Object.defineProperty(service, '2017-09-30', attributes);

module.exports = KinesisVideoMedia;
```

`KinesisVideoMedia.d.ts`

```ts
import Base from 'aws-sdk/clients/kinesisvideomedia';
import { Request, AWSError } from 'aws-sdk';

export type PutMediaInput = {
  FragmentTimecodeType: 'ABSOLUTE' | 'RELATIVE';
  ProducerStartTimestamp?: string;
  Body?: Blob | ArrayBuffer | Buffer;
  ContentType?: string;
} & (
  | { StreamName: string }
  | {
      StreamARN: string;
    }
);

export type PutMediaOutput = {
  Payload: string; // パースする機能が見つからなかった。
};

export default class KinesisVideoMedia extends Base {
  putMedia(params: PutMediaInput): Request<PutMediaOutput, AWSError>;
}
```

これを使って再び実行します。

```ts
import KinesisVideoMedia from './KinesisVideoMedia'; // aws-sdkより先に
import AWS from 'aws-sdk';

// 略
const kinesisVideoMedia = new KinesisVideoMedia({
  endpoint: dataEndpoint,
});

const putMediaRes = await kinesisVideoMedia.putMedia({
  FragmentTimecodeType: 'ABSOLUTE',
  Body: media, // WebMファイルのBlobかBuffer、長さ10秒以内
  StreamARN: 'xxxx', // or StreamName: 'xxxx',
}).promise();

console.log(putMediaRes);
```

Node.jsでは問題なく動いているようです。

```
> npx ts-node --files src/index.ts

{ Payload: '...' }
OK
```

しかしブラウザでは……

```
OPTIONS https://xxxx.kinesisvideo.ap-northeast-1.amazonaws.com/putMedia 403 (Forbidden)
Access to XMLHttpRequest at 'https://s-96e4f3e9.kinesisvideo.ap-northeast-1.amazonaws.com/putMedia' from origin 'http://localhost:8080' has been blocked by CORS policy: Response to preflight request doesn't pass access control check: No 'Access-Control-Allow-Origin' header is present on the requested resource.
```

これがブラウザでProducerを実装する際の最もクリティカルな問題です。**KinesisVideoMedia API は Access-Control-Allow-Origin を返してくれない** のです。こればっかりはユーザー側ではどうしようもありません。ひとまずCORSを無効化してみると（参考：https://qiita.com/mottox2/items/498bb31d67caa2d8a71f ）

```
{Payload: "...", $response: Response}
OK
```

無事動いたようです。

## 次回へ続く
あとは、MediaRecorder APIを使って得たデータをputMediaに渡すだけです。これは次回の記事で書くこととします。

# ブラウザ特有の問題
今回、Kinesis Video Streams Producer をブラウザ上で実装することを試みましたが、様々な問題に直面しました。まずは先述の **CORS問題** が挙げられます。この対策はいくつか考えられますが、

1. ブラウザのフラグで無効化 → セキュリティ上のリスク
2. Electronのを使う → Node側でC++ SDKを叩けば良いのでは？
3. 中継サーバーを置く → 同上
4. AWSが対応するのを待つ → いつ？

などであり、いずれも解決が困難であったり、そもそもブラウザで実装する意味を失ったりします。

また、**PutMediaのストリーミングアップロードができない** ことも問題です。これはXHRやfetchにその機能が実装されていないことに起因します。

こちらは次回詳しく触れることとなりますが、**コーデックの問題**があります。Kinesis Video Streams は様々なコーデックを扱えますが、標準はMKVの中にH.264/AACというあまり一般的には見られない形式であり、デコード・エンコードの実装が必要となりそうです。

最後に、昨年末発表された（https://docs.aws.amazon.com/kinesisvideostreams-webrtc-dg/latest/devguide/what-is-kvswebrtc.html
）**WebRTC機能**です。WebRTCに対応してくれればこのようなハックは必要なくなります。しかしながら、このWebRTC ChannelはVideo Streams とは独立した機能のようで、将来的に統合されるのかは未知数です。

# 実用？

上述の問題から、**不可能ではないがハードルは高い**と言えるでしょう。公式のC++やJava等のSDKを使った中継サーバーを作るのが現実的かと思われます。