オンデマンド動画配信を支える技術 - わかる！HLS編

## TL;DR

- HLSは原理がシンプルでわかりやすく情報が豊富。
- HLSはほとんどのブラウザで視聴可能だが実装依存もある。
- HLSでの視聴制限は一工夫必要。

## はじめに

近年NetflixやPrime Videoなど、たくさんの動画配信サービスがありますね。皆さんも使われているものもあるのではないでしょうか？
Netflixなどほど大規模でなくとも、様々な場面で動画配信の技術を活用しているのを目にします。

[私たちはWebサービス受託開発の会社](https://qiita.com/organizations/takeyuweb-inc)ですので、これまでにお客様からのご相談にお応えして、動画配信サービスも開発してきました。

この文書はオンデマンド動画配信 (VOD: Video on Demand) 開発についてまとめた社内資料を一般向けに再編集したものです。
動画配信サービス開発に興味のあるエンジニアにとってのはじめの一歩となれば幸いです。

### 想定する読者

- VOD実装経験がない
- バックエンド寄りのエンジニア、フルスタックエンジニア

### この文書で書くこと

- 配信形態
- 動画配信プロトコル、ファイルフォーマット
- HLSのしくみ
- ブラウザの対応状況
- 視聴制限の実装とHTTPヘッダーの利用

### この文書で書かないこと

- 暗号化
- DRM
- コーデック
- ライブ配信
- 動画プレイヤー JavaScriptライブラリの利用方法
- ブラウザ以外（専用アプリ）

## 配信形態による分類

ここでは動画配信を利用形態と性質から３つに分けて説明します。

### ライブ動画配信

UStream など、いわゆる生放送（生配信）です。

### リアルタイム動画配信

ビデオ会議など双方向の動画配信で特に必要とされます。
遅延が少しでもあると満足に使えないような用途での配信形態です。

### オンデマンド動画配信

Netflix など、ユーザーが見たいときに見たいものを見ることができる動画配信です。
あらかじめ用意しておいた動画ファイルをダウンロードさせることで実現します。
ライブ配信やリアルタイム配信の録画データを蓄積してオンデマンド動画配信に使うこともあります。

この文書ではオンデマンド動画配信について説明します。

## オンデマンド動画配信プロトコル

いろいろありますが、最近よく見かけるものについて説明します。

### Real Time Messaging Protocol (RTMP)

- Flash プレーヤーとサーバーの間で、音声・動画・データをやりとりするストリーミングのプロトコル。
- HTTP ではなく専用のサーバやネットワーク構築の敷居が高い。
- Flash プレイヤーなので配信用としては将来性がない。
  - Flashプレイヤーが元気な頃はよく使われていた。

### HTTP Live Streaming (HLS)

- ざっくりいうと、大きな動画ファイルも細切れにして、少しずつHTTPでダウンロードしながら再生すれば、ストリーミング再生できるよね、という方式。
- HTTPを使うので、一般的な Webサーバ を使えるほか、CDNへのキャッシュなどWebエンジニアにはなじみ深い技術スタックで配信できる。
- インターネット上に情報が多い。
- **2018年現在、再生可能なブラウザが多い。**
    - Safari および Edge でネイティブ対応。
    - そのほかのブラウザでも Media Source Extensions (MSE) という仕組みで HTML5 JavaScript で再生可能。
    - Windows 7 版 IE11 はMSE非対応であるが、Flashを組み合わせて再生可能。

### MPEG-DASH

- HLSと似たようなものだが多機能。
- iOS Safari で再生できない。

### CMAF

-  "Common" Media File Format
-  HLSとDASHのように乱立している様々なメディアコンテナの規格を統一しようとする次世代規格。

**2018年現在、HLS がよく用いられています。**

## HTTP Live Streaming 概観

HTTP Live Streaming (HLS) は、[Appleにによるストリーミングのためのファイルフォーマット](https://developer.apple.com/streaming/)で、VODとライブ配信のどちらにも対応しています。長らくAppleの独自規格でしたが、現在では HLS version 7 が  [RFC 8216](https://tools.ietf.org/html/rfc8216)として標準化されています。

![HLS.jpg](https://qiita-image-store.s3.amazonaws.com/0/7000/53b1fdc5-3895-2110-308e-210a7e05e445.jpeg)


### プレイリストとメディアセグメント

長い動画も細切れにすれば、１つ１つは小さくてすぐにダウンロードできます。
これを次々に再生していくことで、全体のダウンロードを待たず再生する、ストリーミング再生のようなことが行えます。このような方式を プログレッシブダウンロードといいます。

HLSの基本的な仕組みはこれだけのことです。

細切れしたファイル１つ１つを**メディアセグメント**、メディアセグメントのURLなどを記載したファイルを**プレイリスト**といい、HLSではそれぞれを HTTP GET で取得します。

HTTPなので、極端なことをいえば、昔ながらのレンタルサーバーでもプレイリストとメディアセグメントを置くだけでVOD配信できますし、S3などのクラウドストレージやCDNキャッシュなどでも扱いやすいです。

### アダプティブストリーミング

HLSでは、ユーザーが利用中の回線速度にあわせて、再生する動画の品質を変更することができます。たとえば高速な回線を利用している場合は高品質、モバイルデバイスなど低速な環境では低品質という具合です。この技術を**アダプティブストリーミング**といいます。

メディアセグメントのURLのかわりにプレイリストのURLを記述した**マスターインデックスファイル**を用意し、これをプレイヤーで読み込みます。プレイヤーはマスターインデックスファイルの情報から使用するプレイリストを選んで再生に使用します。

![Adaptive.jpg](https://qiita-image-store.s3.amazonaws.com/0/7000/21786f81-bb25-29a8-fc2e-09d727fe1b87.jpeg)


### 字幕

[WebVTT](https://w3c.github.io/webvtt/) を使います。
もし複数の言語の字幕を提供したい場合、言語ごとにWebVTTファイルを作成します。

https://tools.ietf.org/html/rfc8216#section-3.5

## Real World HLS

### ネイティブ対応

ブラウザの標準機能として `video` タグでHLS再生できるのは、残念ながら一部のブラウザのみです。

- Microsoft Edge
- macOS Safari
- iOS Safari
- Android Chrome
- Android Firefox

### Media Source Extensions (MSE)

ネイティブ対応していないブラウザでは、Media Source Extensions (以下MSE)という仕組みを使うことで、再生することができます。
ざっくりいうと、「HLSのプレイリストを解析、記載されているメディアセグメントを取得してvideoタグのソースに突っ込む」処理をJavaScriptで書ける、というHTML5の機能です。

最近のブラウザであれば、iOS Safari の除いて対応しています。IEも 11 以上（Windows 7版を除く）であれば利用できます。
ネイティブ対応しているブラウザでも、そうでないブラウザとの挙動を揃えるなどの目的で MSE を使うこともあります。

### Flash Player

MSEにも対応していない場合は、Flash Player で実装します。
がご存じの通り将来性はないので、「どうしてもIE10やIE11(Windows 7)を切れない」場合以外は考えません。

### 動画プレイヤー実装

いくつかの実装が知られています。筆者は比較できるほどには各プレイヤーについて精通していないので、ここでは簡単な紹介にとどめます。

#### JW Player

https://www.jwplayer.com/

Flash全盛の頃からの老舗動画プレイヤー。
イベント発火のタイミングなど、ブラウザ間の差異が小さくするための努力を感じます。
IE 11 (Windows 7) はFlashを使います。

#### video.js

https://videojs.com/

オープンソース。プラグインで機能を拡張できます。
以前は videojs-contrib-hls を使う紹介記事が多く見られましたが、その後 http-streaming に引き継がれています。
[version 7から http-streaming が標準添付されるようになったので、現在は追加のプラグインなしに HLS 再生が可能です。](https://blog.videojs.com/video-js-7-is-here/)

#### hls.js

https://github.com/video-dev/hls.js/

多くのHTML5 動画プレイヤーが多くな形式に対応する中、「HLSをMSEで再生すること」に絞って開発されているプレイヤーです。
iOS Safariは、MSEに対応していないので使えません。iOS Safari は、HLSにネイティブ対応しているのでなくても再生できます。ただし、hls.js だけでは iOS Safari とそれ以外とを同じようには扱えない、ということになるでしょう。

### HLS形式への変換

用意した動画ファイルをHLSに変換するには、ffmpegを使うケースが多いです。ただし、ffmpegの保守管理も大変なので、予算が許せば [Amazon Elastic Transcoder](https://aws.amazon.com/jp/elastictranscoder/) や [AWS Elemental MediaConvert](https://aws.amazon.com/jp/mediaconvert/) を使うのがよいです。

特に、「利用者がアップロードした動画を変換する」用途に使う場合、「どのようにして変換ジョブを管理するか」という部分をまるごと任せることができるクラウドサービスの利用がとても便利です。

#### ffmpeg

昔から動画変換といえばコレ。

#### Amazon Elastic Transcoder

https://aws.amazon.com/jp/elastictranscoder

ソースのS3バケットに置いたメディアファイルを、出力先のS3バケットに出力する、というサービスです。
変換に成功した動画ファイルの長さと解像度で課金されます。
変換サーバの構築や運用、スケールなどの問題に悩まされることなく、変換の際の細かなパラメータチューニングをしなくても事前定義されているプリセットを使うことで十分実用できるのが強みです。

後継の AWS Elemental MediaConvert も[最近登場した](https://aws.amazon.com/jp/blogs/news/amazon-media-services-tokyo-launch/)ので、これから使うならそちらの方がよさそうです。

#### AWS Media Services

https://aws.amazon.com/jp/mediaconvert/

Elastic Transcoderは「変換するだけ」でしたが、こちらはもっといろいろな機能があり、より大規模で高度な機能をもつ動画配信サービスの構築を意識しているようです。

#### Azure Media Services

https://azure.microsoft.com/ja-jp/services/media-services/

### 視聴制限

ある動画コンテンツを視聴できるユーザーを制限したい、というのはよくある要件です。

HLSは、再生中にサーバからプレイヤーを制御する仕様を持ちません。
このようなケースでは再生中に視聴を制限するのではなく、再生に必要なプレイリストやメディアセグメントのダウンロードを制限します。

たとえば、プレイヤーに渡すプレイリストのURLとしてWebアプリケーションのURLを渡すことで、ユーザーがそのプレイリストに対しての視聴権限があるかどうかを確認することができます。
視聴を許可する場合に、メディアセグメントを取得するためのURL（Amazon CloudFront や S3 であれば有効期限付きの署名付きURL）を記載したプレイリストを生成して返します。

プレイリストに記載するメディアセグメントのURLも同様にすることで、メディアセグメントごとに制限を行うこともできます。

ただし、プレイリストやメディアセグメントのGETの際にCookieを送信するかどうかは実装に依存するため、プレイリストのURLにトークン等セッションを特定するクエリストリングなどを使うことになると思います。このため、厳密な制限は難しいです。

セッションハイジャックや、意図的なトークン付きURLの配布による不正ダウンロードなどのリスクがあるため、セッションIDの期限や破棄についてはよく検討する必要があります。

### ロックアウト

視聴制限の項で述べたとおり、再生中に視聴を制限することはできません。

プレイリストやセグメントのURLにトークンを付与することでセッションを特定している場合、このトークンをサーバ側で管理することで、トークンの失効によりダウンロードを制限することはできます。

ただし、プレイヤーがすでにダウンロードを終えているセグメントについてはキャッシュから再生を続けられてしまうので、それを制御したい場合はプレイヤーの実装で頑張るほかなく、厳密に制限することは難しいです。

### ユーザーによって視聴可能なビットレートを制限

視聴制限の応用で、動的にマスターインデックスファイルを生成することで対応可能です。

### HTTP ヘッダーの送出

最新のvideo.jsではないのですが、videojs6の頃に調査した結果、 video.js + videojs-contrib-hls による HLS セグメント取得時のヘッダー送出の挙動は以下の通りで、JavaScript制御かネイティブかによって挙動が異なってしまいます。

iOSの場合にCookieでのセッション管理をしたい場合、ブラウザのみでは実現できません。

| ブラウザ | Origin送出 | リファラー送出 | Cookie送出 | 備考 |
| ---------- | ------------ | ----------------- | ------------- | ----- |
| IE11 | ○ | ○ | × | MSEによる。CORS制限あり |
| Firefox | ○ | ○ | × | MSEによる。CORS制限あり |
| Chrome | ○ | ○ | × | MSEによる。CORS制限あり |
| iOS Safari | × | ○ | × | ネイティブ。CORS制限の対象外。 |
| Android Chrome | × | × | ○ | ネイティブ。CORS制限の対象外。CORSによるCookie送出制限も機能しない。 |

`videojs.options.html5.hls.overrideNative = true`設定時

| ブラウザ | Origin送出 | リファラー送出 | Cookie送出 | 備考 |
| ---------- | ------------ | ----------------- | ------------- | ----- |
| IE11 | ○ | ○ | × | |
| Firefox | ○ | ○ | × | |
| Chrome | ○ | ○ | × | |
| iOS Safari | × | ○ | × | ネイティブ。iOS SafariはMSE未対応のため。 |
| Android Chrome | ○ | ○ | × | MSEにより他のPCブラウザと同様の挙動に |

`videojs.options.html5.hls.overrideNative = true` `videojs.options.html5.hls.withCredentials = true` 設定時

| ブラウザ | Origin送出 | リファラー送出 | Cookie送出 | 備考 |
| ---------- | ------------ | ----------------- | ------------- | ----- |
| IE11 | ○ | ○ | ○ | |
| Firefox | ○ | ○ | ○ | |
| Chrome | ○ | ○ | ○ | |
| iOS Safari | × | ○ | × | ネイティブ。iOS SafariはMSE未対応のため。 |
| Android Chrome | ○ | ○ | ○ | MSEにより他のPCブラウザと同様の挙動に |

※Origin送出時、CORSによるアクセス制御が発生します。送出しない≒ネイティブではCORSによるアクセス制御は適用されません。
※Origin送出時にCookieを使うには、CORSの`Access-Control-Allow-Credentials`が必要。また、ORIGINの指定にワイルドカードは使用できない。

```js
var hlsOptions = {
  "withCredentials": true,
  "overrideNative": true
};
var options = {
  "controls": true,
  "autoplay": false,
  "preload": "auto",
  "html5": {
    "nativeAudioTracks": false,
    "nativeVideoTracks": false,
    "hls": hlsOptions
  },
  "flash": {
    "hls": hlsOptions
  }
};
var player = window.player = videojs('video-player', options);
```

### 再生時イベントの発火順が実装によって異なる

経験上、ブラウザごとに `video` タグのイベント発火のタイミングが違ったり、そもそもイベントが発火しないなど、嵌まりどころが多いです。（もしかすると最新では状況も変わってきているかもしれません）

JWPlayerではとこの違いも吸収してくれるので、だいぶ信用して使うことができますが、あまりイベント・再生制御をあてにしない設計を心がけた方がよいようです。

### Edgeのプリロードがものすごい

Edgeブラウザの場合に、ページ表示の段階から、すごいスピードで先読みしました。
視聴を制限したい場合に都合が悪いのでなるべくさせたくないのですが、ネイティブ実装では制御できません。
アップデートで改善されたかもしれません。重要なのは実装によって挙動が違い、その制御が難しいかできないこと、です。

## おわりに

HLSは私たちにとって理解しやすく、ネットワーク的にも扱いやすい方法です。
動画プレイヤーライブラリの開発も活発で、ブラウザベースで動画配信をするなら、モバイル含めておそらく最も確実な方法でしょう。
特にiOSのシェアの高い日本において、安定して動くネイティブ実装があるというのは強いです。

一方で、再生制御に制限があること、今回は触れませんでしたがコンテンツ保護（DRM）の端末互換性がないなど難しいことをするのは難しい、という面もあります。

次回以降、暗号化やDRMについてや、ライブ動画配信についてもまとめたいと思います。