LoginSignup
11
4

More than 3 years have passed since last update.

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

Posted at

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

  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 を使うのが簡潔でしょう。
まずはインストールします。

npm install --save aws-sdk

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

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

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の中に書きます。)

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 のクライアントを作成します。

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

PutMedia を呼び出し……

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

{
  "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

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

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>;
}

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

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を使った中継サーバーを作るのが現実的かと思われます。

11
4
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
11
4