AWSで動画コンテンツをDRMする&パッケージング&配信する仕組みを作る

# 概要

この記事はDMMグループ Advent Calendar 2020 12日目の記事です。

過去に、AWSを用いて動画配信基盤のディザスタリカバリスタックを構築した記事を書いたことがありますが↓
[もしもに備えた動画配信基盤のDRシステム - DMM inside](https://inside.dmm.com/entry/2020/04/07/evolving_content_delivery_platform_10)

この記事ではその際の知見を使って、0からAWSで動画コンテンツのエンコード・DRM・パッケージング・配信をする仕組みを解説します。

今日は社会情勢も相まって、世界中で急激にライブ配信・リアルタイム通話・VOD・音声配信・ライブECなどのストリーミング関連サービスが勢いをつけてきていると思われます。0から自前でストリーミング機能を実装するようなケースも増えてきているのではないでしょうか。

AWSな配信システムにDRMな仕組みを導入したい、開発工数に余裕がない、プロトタイプ的な配信システムをセキュアかつコスパよく構築したい、というような場面で、この記事の内容が役に立てばと思います。

## 前提

- AWS Media Servicesの概要がわかる。
- その他AWSのメジャーなサービスをある程度認知している。

## 技術スタック

AWS MediaLive、AWS MediaPackage(エンコード・パッケージング)、DRM情報生成API(API Gateway&Lambda)という構成で、HLS AES 128な動画を配信する仕組みを作ります。

詳しく後述しますが、AWSでDRMをかける部分で、[AWS SPEKE](https://docs.aws.amazon.com/speke/latest/documentation/what-is-speke.html) というインターフェースを踏襲したAPIが必要になってきます。

## AWS SPEKEとは

公式ドキュメント https://docs.aws.amazon.com/speke/latest/documentation/what-is-speke.html

SPEKE = Secure Packager and Encoder Key Exchange

簡単にまとめると、動画のパッケージャ(AWS Elemental)とDRMエンクリプタ間で、DRMキーなどの情報をセキュアにやりとりできる仕様のことです。
SPEKEが定めるI/OインターフェースにのっとってDRM情報(キーやHLS AESのURL)を返すサーバを実装すると、AWS Elementalと連携して動画をDRMすることができます。Multi DRMに対応していたりと、AWSでDRMするハードルを下げてくれる便利ツールです。

もうちょっとわかりやすくまとめると、パッケージングは今まで通りAWS Media Serviesに任せてDRMに必要な暗号化キーなどの情報の生成を、こっち側で作ったサーバに移譲できるということです。

また、DRMは各種ベンダー(Widevine, PlayReady, FairPlay)によって暗号化情報生成のインターフェースが違っていますが、AWS SPEKEを使うことで、１つのインターフェース経由でマルチDRMすることができます(合ってるはず..)。

これによって動画ごとにユニークなキーを生成できたり、キーのプロバイダをクラウド・オンプレ好きな場所に構築することができるようになり、色々ハッピーなことが起こります。

## MediaPackageとSPEKEを使ってDRMをかけよう

https://docs.aws.amazon.com/speke/latest/documentation/standard-payload-components.html

SPEKEはDASH IFのCPIX(Content Protection Information eXchange)という仕様をラップしています。
DASH IF CPIXについての説明はこちら https://docs.unified-streaming.com/documentation/drm/cpix_intro.html
(公式のドキュメントではありませんが、分かりやすかったのでUnified Streaming社のドキュメントを貼ってます)

CPIXはDRMに必要な情報をXMLベースでやりとりする仕様らしいです。正直あんまりよく分かってないんですがドキュメントに書いてある通りにデータを送ればだいたい動くので神です。
SPEKEにおいても、基本的にCPIXにのっとってXMLでデータをやりとりします。

公式にVOD動画をパッケージングするときのリクエスト・レスポンス例が載っているので今回はこれを参考に
HLS AES 128で暗号化してみたいと思います。
VOD Workflow Method Call Examples - https://docs.aws.amazon.com/speke/latest/documentation/vod-workflow-methods.html

### 1. SPEKE準拠なキーサーバを実装

いきなりですが今回実装したコードを置いちゃいます。
https://github.com/OdaDaisuke/aws-speke

下記のリファレンス実装を参考にやって行きました。
awslabs/speke-reference-server - https://github.com/awslabs/speke-reference-server

基本的には、リクエストで受け取ったCPIXのXMLを埋めていく処理を書くだけです。
詳しい処理については後述のステップで解説していきます。

さて、キーサーバのセットアップですが今回はLambda + API Gatewayで行なっていきます。
上記コードをLambdaにアップしたら、以下のスクショにならって環境変数を設定しましょう。
![スクリーンショット 2019-12-21 22.31.05.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/73555/5ae415cb-bea7-e40b-1a60-cb9822dd034d.png)

KEY_STORE_BASE_URLは、HLSのマニフェストファイルのext-x-keyのベースURLになります。事前に空のバケットを作って外部からアクセスできるようにACLなど設定しましょう。
２番目のKEY_STORE_BUCKETはClearKeyを保存するバケットの名前です。

そしたら、Lambda用のロールを作成して設定してあげます。この時S3とMediaPackageとAPI Gatewayの操作ポリシーを指定してあげます。
また、同時にAPI Gatewayもセットアップして適当な名前のエンドポイントでキーサーバを叩けるようにしていきますが、今回はリファレンス実装にならってcopy_protectionというエンドポイントを作りました。ちなみにこのキーサーバはAWS内部から叩けられればOKなので外部には公開しなくて大丈夫です。
![スクリーンショット 2019-12-21 22.37.01.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/73555/6290d17b-4969-484e-ede2-9e022e76089e.png)

### 2. Role作成

最初にMediaPackageのRoleを作成します。
現在ロール作成時にデフォルトでMediaPackageを選択することができないので、最初にMediaConvertのロールとして作成した後
以下のように「信頼されたエンティティ」をmediapackage.aws.comに変更、さらにMediaPackageのFullAccessポリシーをアタッチしてください。
![スクリーンショット 2019-12-21 22.04.23.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/73555/21ecdd58-66d7-cac9-2e64-72107bfc88cb.png)

### 3. MediaPackageでパッケージング

まずは下記スクショのPackaging groupsというところから適当な名前でパッケージグループを作成してください。
![スクリーンショット 2019-12-21 22.27.18.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/73555/dd4330f8-576f-44d0-067c-dfd0e9f2e842.png)

次にHLS AES128のパッケージング設定を作成します。
General Settingsはこのように、
![スクリーンショット 2019-12-21 22.41.21.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/73555/d5db1a16-e791-6c76-a526-7a65b4e6cc17.png)

Enable Encryptionにはチェックを入れ、追加で出てくる入力フォームはこのような設定に。
![スクリーンショット 2019-12-21 22.41.58.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/73555/368fb149-059b-3814-3421-14cca18c8e68.png)
Constant Initialization Vectorは、先述したIVのことで、適当な値をUUID形式で入れておけばOKです。
Key server URLには先ほど作成したキーサーバのAPI GatewayのURLを、
Role Arnには先ほど作成したMedia PackageのARNを入れ、
最後にSystem IDを設定。このSystem IDですが、HLS AESは正確にはDRMではないためSystem IDは定義されていないので、適当な値をUUID形式で入力しておきます。今回はコード側 https://github.com/OdaDaisuke/aws-speke/blob/master/src/server_response_builder.py#L13 で定義した`81376844-f976-481e-a84e-cc25d39b0b33`という値を設定してみました。

ここまできたらパッケージングの設定は完璧です。`Save Setting`を押して次に進みます。

### Ingest Asset

再びMediaPackageのホーム画面に戻り、`Asset` -> `Ingest Asset`に進みます。
パッケージングしたい動画があるバケットを選択し、IAM Roleには先ほど作成したMedia Packageのロールを選択します。

ページ中程にある `Asset Details` は、m3u8ファイルを選択しましょう。この時親子構造のマニフェストでなければパッケージングできないので注意です。詳しくは、https://qiita.com/daisukeoda/items/1eecfd639aeae6f1026c を参照。
![スクリーンショット 2019-12-21 22.50.31.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/73555/ffabde05-4bfb-1f69-c90c-bdd1abb1a5c5.png)

最後のPackaging settingsは、先ほど作成したパッケージング設定を選択。
![スクリーンショット 2019-12-21 22.52.37.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/73555/c2eb4235-4fad-ec28-f9fc-9231fbdb4fe4.png)

そして最後に「Ingest assets」を押せば、あとは勝手にDRMしつつパッケージングしてくれます。
ボタンを押したあとは下記のようにAssetsの項目が増えているはず。
![スクリーンショット 2019-12-21 22.54.34.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/73555/f4127864-93ee-6377-da38-1be653d6ff99.png)

最後に、項目のタイトルをクリックしてみるとこのようにPlayback details欄に再生URLが表示されているのでテスト再生してみましょう。
![スクリーンショット 2019-12-21 22.56.08.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/73555/a148c05b-09dd-c014-b123-ad39897e3989.png)

適当な動画プレーヤに再生URLを入力して再生します。
そして暗号化時に設定したライセンスサーバのAPIに向かって復号キーがリクエストされ、動画が再生できていれば完璧です(再生urlは一部隠しています)。

<img width="728" alt="スクリーンショット 2019-12-21 23.02.40.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/73555/f27f7fd5-4a62-b570-2984-a6d34e5931d7.png">

### 4. キーサーバの処理について

SPEKEキーサーバのログをみてみると、以下のようなリクエストを受け取っています。

```xml
<?xml version="1.0" encoding="UTF-8"?>
<cpix:CPIX xmlns:cpix="urn:dashif:org:cpix" xmlns:pskc="urn:ietf:params:xml:ns:keyprov:pskc" xmlns:speke="urn:aws:amazon:com:speke" id="コンテンツID">
  <cpix:ContentKeyList>
    <cpix:ContentKey kid="00112233-4455-6677-8899-aabbccddeeff"></cpix:ContentKey>
  </cpix:ContentKeyList>
  <cpix:DRMSystemList>
    <cpix:DRMSystem kid="00112233-4455-6677-8899-aabbccddeeff" systemId="81376844-f976-481e-a84e-cc25d39b0b33">
      <cpix:PSSH />
      <cpix:ContentProtectionData />
      <cpix:URIExtXKey />
      <speke:KeyFormat />
      <speke:KeyFormatVersions />
      <speke:ProtectionHeader />
    </cpix:DRMSystem>
  </cpix:DRMSystemList>
</cpix:CPIX>
```

そしてレスポンスは以下のようなXMLです。

```xml
<cpix:CPIX xmlns:cpix="urn:dashif:org:cpix" xmlns:pskc="urn:ietf:params:xml:ns:keyprov:pskc" xmlns:speke="urn:aws:amazon:com:speke" id="コンテンツID">
  <cpix:ContentKeyList>
    <cpix:ContentKey explicitIV="IV(UUID形式)のHEXバイナリをbase64した文字列" kid="00112233-4455-6677-8899-aabbccddeeff">
      <cpix:Data>
        <pskc:Secret>
          <pskc:PlainValue>暗号化キー(UUID形式)のHEXバイナリをbase64した文字列</pskc:PlainValue>
        </pskc:Secret>
      </cpix:Data>
    </cpix:ContentKey>
  </cpix:ContentKeyList>
  <cpix:DRMSystemList>
    <cpix:DRMSystem kid="00112233-4455-6677-8899-aabbccddeeff" systemId="81376844-f976-481e-a84e-cc25d39b0b33">
      <cpix:URIExtXKey>キーurlをbase64エンコードした文字列</cpix:URIExtXKey>
      <speke:KeyFormat>aWRlbnRpdHk=</speke:KeyFormat>
      <speke:KeyFormatVersions>MQ==</speke:KeyFormatVersions>
    </cpix:DRMSystem>
  </cpix:DRMSystemList>
</cpix:CPIX>
```

記事の最初の方に書いた、CPIX準拠のXMLです。
意味を軽く説明してみますと、
<cpix:ContentKeyList>のexplicitIVは、AES128またはSAMPLE AESでの暗号化の時に使うもので、CBCブロック暗号化の際に使う初期化ベクトルです。例えばMediaPackageの設定欄にIVを設定する項目がありますが、そこに設定した値が `explicitIV`属性に設定されリクエストが飛んできます。ここで受け取ったexplicitIVは、リクエストで受け取った値とは別な値でレスポンスすれば、値を上書きすることも可能です。この仕様をうまく利用して、キーローテートを実現することも可能。

<pskc:PlainValue>はコンテンツの暗号化キーが入ります。
<cpix:DRMSystemList>の配下の要素には、DRMに使う情報を格納します。今回の場合(AES)だとURIExtXkey、KeyFormat、KeyFormatVersionsだけ必要なのでそれらの情報を埋めてあげます。
<cpix:PSSH>と<cpix:ContentProtectionData>と<speke:ProtectionHeader>はHLS AESの際には必要ないので、要素ごと削除してレスポンスしている感じです。これらはWidevineやPlayReadyでDRMする際に共通して必要になってきますが、今回は解説しません。

以上、SPEKEインターフェースを用いた動画配信システムの最低限の説明でした。

## まとめ

HLS AES 128における処理を紹介してみましたが、SPEKEは他にもWidevineやPlayReady、FairplayなどのメジャーDRMに対応しています。
またDASH(Widevine, PlayReadyが対応)などでの配信も可能です。

AWSを使った動画配信システムの解説記事は多々ありますが、DRMまで対応したものはなかなか少ないと思うので、少しでも役に立てばと思います。

明日の投稿は、 @taniko さんです！