mediarecorder と webm で、オレオレ live streaming

67
MediaRecorder と WebM とととと Live Streaming 2015.06.05 WebRTC Meetup Tokyo #8 とととととととととと とととととと @massie_g 1

Upload: mganeko

Post on 26-Jul-2015

3.120 views

Category:

Technology


6 download

TRANSCRIPT

MediaRecorder と WebM でオレオレ Live Streaming

2015.06.05WebRTC Meetup Tokyo #8インフォコム株式会社

がねこまさし@massie_g

1

今日のお話

• getUserMedia• RTCPeerConnection• MediaRecorder

• CODEC

• 「通信」ではなく、「配信」のお話– スケールするリアルタイム配信の仕組みを作り

たい2

もくじ• ブラウザ側で無茶をする• WebM の内側• サーバ側で頑張る• Media Source Extensions の夢

3

1. ブラウザ側で無茶をする

4

Media Recorder API

• getUserMedia() で取得したストリームを記録する API– http://www.w3.org/TR/mediastream-recording/– http://html5experts.jp/mganeko/12475/

• Firefox で実装済

5

使い方var localStream; // getUserMedia() で取得した stream をセットしておくvar recorder = null;var video = document.getElement ById(‘video1’);

function startRecording() { recorder = new MediaRecorder(localStream); recorder.ondataavailable = function(evt) { // 録画が終了したタイミングで呼び出される var videoBlob = new Blob([evt.data], { type: evt.data.type }); video.src = window.URL.createObjectURL(videoBlob); } // 録画開始 recorder.start();} // 録画停止function stopRecording() { recorder.stop();}

6

使い方var localStream; // getUserMedia() で取得した stream をセットしておくvar recorder = null;var video = document.getElement ById(‘video1’);

function startRecording() { recorder = new MediaRecorder(localStream); recorder.ondataavailable = function(evt) { // 録画が終了したタイミングで呼び出される var videoBlob = new Blob([evt.data], { type: evt.data.type }); video.src = window.URL.createObjectURL(videoBlob); } // 録画開始 recorder.start();} // 録画停止function stopRecording() { recorder.stop();}

停止しないと、再生できない

(と、思い込んでた)

7

デモ• https://lab.infocom.co.jp/demo/webrtc-recorder.html

• 録画を止める → 再生できる

8

リアルタイム配信の準備• 録画しながら、再生もしたい…

• なら、無理やりの出番じゃない?

9

作戦 1

• 短時間で、録画開始、録画停止を繰り返す– 5 秒~ 10 秒くらい

• 順次、再生する

10

作戦 1MediaRecorder.start()

…MediaRecorder.stop()

MediaRecorder.start()…

MediaRecorder.stop()

MediaRecorder.start()…

MediaRecorder.stop()

MediaRecorder.start()…

MediaRecorder.stop()

Blob 1

Blob 2

Blob 3

Blob 4

video.src = url1;video.play();

video.src = url2;video.play();

video.src = url3;video.play();

video.src = url4;video.play();

11

作戦 1 :デモ• https://lab.infocom.co.jp/demo/webrtcfirefox-rec-split.html

12

作戦 2

• 作戦 1 の結果 ...blob 切り替わり時にちらつく

• ちらつき防止には、昔から「ダブルバッファー」

• → 今回は、 2 つの video タグを、交互に表示

13

作戦 2MediaRecorderstart()

…MediaRecorder.stop()

MediaRecorderstart()…

MediaRecorder.stop()

MediaRecorderstart()…

MediaRecorder.stop()

MediaRecorderstart()…

MediaRecorder.stop()

Blob 1

Blob 2

Blob 3

Blob 4

video1(preload)

video2(preload)

14

作戦 2 :デモ• https://lab.infocom.co.jp/demo/webrtcfirefox-rec-dual.html

15

作戦 2 の結果• ちらつき、ほとんど分からない– 良く見ると気が付くけど…

• これで、配信もできるはず

16

作戦 3 :無理やり、映像配信

Firefox

XHR POST .webmXHR POST .webmXHR POST .webmXHR POST .webm

Firefox Chrome

HTTP GET .webm

HTTP GET .webmHTTP GET .webm

HTTP GET .webmHTTP GET .webm

HTTP GET .webm

HTTP GET .webm

HTTP GET .webm

17

video1

video2video1

video2

作戦 3 の結果• 配信できた!– デモは省略

• ちらつきも、ほとんど分からない– 良く見ると気が付くけど…

• 天の声– 「ダブルバッファーもどき、無理やりすぎ」– 「 MediaRecorder 、もっと良く見ろ」

• MediaRecorder.start(optional long timeslice);

– iOS で何とか見れないの?18

作戦 3 の結果• 配信できた!– デモは省略

• ちらつきも、ほとんど分からない– 良く見ると気が付くけど…

• 天の声– 「ダブルバッファーもどき、無理やりすぎ」– 「 MediaRecorder 、もっと良く見ろ」

• MediaRecorder.start(optional long timeslice);

– iOS で何とか見れないの?19

MediaRecorder.start(interval)var localStream; // getUserMedia() で取得した stream をセットしておくvar recorder = null;var video = document.getElement ById(‘video1’);

function startRecording() { recorder = new MediaRecorder(localStream); recorder.ondataavailable = function(evt) { // インターバルごとに呼び出される var videoBlob = new Blob([evt.data], { type: evt.data.type }); video.src = window.URL.createObjectURL(videoBlob); } // 録画開始 recorder.start( 5000 );} // 録画停止function stopRecording() { recorder.stop();}

停止しなくて呼ばれる

インターバルを指定できた! (ミリ秒)

20

MediaRecorder.start(interval)… しかしvar localStream; // getUserMedia() で取得した stream をセットしておくvar recorder = null;var video = document.getElement ById(‘video1’);

function startRecording() { recorder = new MediaRecorder(localStream); recorder.ondataavailable = function(evt) { // インターバルごとに呼び出される var videoBlob = new Blob([evt.data], { type: evt.data.type }); video.src = window.URL.createObjectURL(videoBlob); } // 録画開始 recorder.start( 5000 );} // 録画停止function stopRecording() { recorder.stop();}

停止しなくて呼ばれる

インターバルを指定できた! (ミリ秒)

2 個目以降は再生できない。 Blob の中身が違

21

Blob の中身は?• MediaRecorder で取得した Blob の中身– evt.data.type = "video/webm“– どうやら WebM らしい

• でも、最初の Blob と、 2 番目以降の Blobで違う??

• WebM の中身って、どうなっている??

22

2. Inside of WebM

23

WebM とは• Wikipedia より http://ja.wikipedia.org/wiki/WebM– 米 Google が開発している– オープンでロイヤリティフリーな– 動画コンテナフォーマット

• コーデック– 映像: VP8 / VP9– 音声: Vorbis

• コンテナ– Matroska のサブセット

Matroska• Wikipedia より  http://ja.wikipedia.org/wiki/Matroska

– ロシアの入れ子人形マトリョーシカにちなむ– オープンソース( GNU LGPL )で開発中– EBML ( Extensible Binary Meta Language )採用– http://www.matroska.org/technical/specs/index.html– こちらの記事からたどり着きました。ありがとうございます!

• Media Source Extensions を使ってみた (WebM 編 ) @othersight• http://qiita.com/tomoyukilabs/items/57ba8a982ab372611669

• EBML ( Extensible Binary Meta Language )– XML を基に作られた、拡張性に優れたデータ格納方式

• 要素のみ、属性なし、入れ子あり• ※ むしろ YAML と言った方が近いかも

– 対応していない機能 ( タグ ) は無視する– テキストではなくバイナリで表現

EMBL のイメージ<EBML>

<EBMLVersion>1</EBMLVersion><DocType>webm</DocType>

</EBML><Segment> …

<Info> … </Info><Tracks> … </Tracks><Cluster> … </Cluster><Cluster> … </Cluster>…

</Segment>

これがバイナリで格納されている

WebM のおおまかな構造

ヘッダー 部分

映像 / 音声 部分

おまけ 部分

Header

Meta Seek Info

Segment InfoTracks

(Chapters)

ClusterClusterCluster

Cluster

(Cue Data)(Attachment)

(Tagging)

Matroska 的

EBML

Segment

WebRTC 的

WebM/Matroska/EBMLバイナリのレイアウト

EBML

EBMLVersion

“webm”

Segment

1

Tracks

CodecName

“VP8”

DOCType

タグ /Element のバイナリ表現

• 3 つのパートでタグ /Element は構成される

ID DataSize Data

1 ~ 4 バイト 1 ~ 8 バイト 0 ~ 0x00FFFFFFFFFFFFFE バイト      ペタバイト級まで

可変長 可変長

可変長

実際のデータだけでなく、 ID も DataSize も可変長厄介なヤツ。マジ勘弁…

ID 部のバイナリ表現1 バイト目 (2 進 )

1xxx-xxxx

01xx-xxxx xxxx-xxxx

001x-xxxx xxxx-xxxx xxxx-xxxx

0001-xxxx xxxx-xxxx xxxx-xxxx xxxx-xxxx

0x80 ~ 0xFF

0x40 ~ 0x7F

1 バイト目 (16 進 )

0x20 ~ 0x3F

0x10 ~ 0x1F

※ 先頭ビットも、 ID の値に含める

DataSize 部のバイナリ表現

xxxx-xxxxxxxx-xxxx xxxx-xxxxxxxx-xxxx xxxx-xxxx xxxx-xxxxxxxx-xxxx0000-0001

xxxx-xxxxxxxx-xxxx xxxx-xxxxxxxx-xxxx xxxx-xxxx xxxx-xxxx0000-001x

xxxx-xxxxxxxx-xxxx xxxx-xxxxxxxx-xxxx xxxx-xxxx0000-01xx

xxxx-xxxxxxxx-xxxx xxxx-xxxxxxxx-xxxx0000-1xxx

xxxx-xxxxxxxx-xxxx xxxx-xxxx0001-xxxx

xxxx-xxxxxxxx-xxxx001x-xxxx

xxxx-xxxx01xx-xxxx

1xxx-xxxx

1 バイト目 (2 進 )

7bits

14bits

21bits

28bits

35bits

42bits

49bits

56bits

値は Big Endian の Unsigned Integer※ 先頭ビットは、 DataSize の値に含めない※ すべてのビットが 1 の値は予約済(多分)

Data 部分• DataSize で指定されたバイト数• タグ (ID) の種類によって、さまざま型– 整数: unsigned int, signed int (Big Endian)– 文字列 : ASCII string, UTF-8 string– 実数: float (Big Endian)– 日付 : date (Big Endian)– バイナリ : binary– 他の複数のタグ: master

• タグの入れ子の構造• 型を識別するルールは無い …みたい– タグの値と型を対応付ける辞書を持つしかなさそう– http://www.matroska.org/technical/specs/index.html

44

http://www.matroska.org/technical/specs/index.html より

ID 型

解析例 (1) EBMLVersion

• 16 進表記: 42 86 81 01• ID の最初のバイトの 2 進表記: 0100 0010– → ID は 2 バイトの [42][86]– 一覧表から、 EBMLVersion と判明

• DataSize の最初のバイトの 2 進表記: 1000 0001– → DataSize は 1 バイト– サイズは 0000 0001 = 1 バイト

• Data は 1 バイト。先ほどの一覧表から、型は Unsigend Int– →  値は 1

• 結果:  EBMLVersion = 1

解析例 (2) DocType

• 16 進表記: 42 86 84 77 65 62 6D• ID の最初のバイトの 2 進表記: 0100 0010– → ID は 2 バイトの [42][84]– 一覧表から、 DocType と判明

• DataSize の最初のバイトの 2 進表記: 1000 1000– → DataSize は 1 バイト。サイズは 0000 1000 = 4 バイ

ト• Data は 4 バイト。先ほどの一覧表から、型は ASCII string– 77 65 62 6D →  値は "webm"

• 結果:  DocType = "webm"

WebM のパース• 先頭から 1 バイトづつパースしていけば、タグの内容を解析可能

• とくに興味が無いタグや、理解できないタグが出現した場合– データー長はルールに従って算出可能 → スキップすることが可

• ※ もちろんライブラリもあります– libebml http://dl.matroska.org/downloads/libebml/– libmatroska http://dl.matroska.org/downloads/libmatroska/– yamka https://sourceforge.net/projects/yamka/※ 使ってはいません。詳細不明

• Node.js のサンプルコードを書いてみました– https://gist.github.com/mganeko/9ceee931ac5dde298e81– メモリ上に一括して読み込む、しょぼい実装ですが…

あれ、何の話だっけ?• MediaRecorder.ondataavailable() で• インターバル指定で取得した BLOB の話

MediaRecorder の Blob と WebM の対応ヘッダー部

Cluster(映像 / 音声データ)

Cluster(映像 / 音声データ)

Cluster(映像 / 音声データ)

Cluster(映像 / 音声データ)

Cluster(映像 / 音声データ)

Blob 1

Blob 2

Blob 349

HeaderMeta Seek Info

Segment InfoTracks

ブラウザ側で何とかなる?• 次々と出現する Blob– 最初にはそろっていない

• 動的に連結しながらwindow.URL.createObjectURL() で取り出す

→  できない (多分)

50

ブラウザ側で何とかなる?• 次々と出現する Blob– 最初にはそろっていない

• 動的に連結しながらwindow.URL.createObjectURL() で取り出す

→  できない (多分)

なら、サーバーで無理やりの出番じゃない?

51

3. サーバー側で頑張る

52

作戦 4 :やりたいこと

53

Firefox

XHR POST Blob1XHR POST Blob2XHR POST Blob3XHR POST Blob4

Firefox Chrome

HTTP GET

Blob 1 Blob 2 Blob 4Blob 3

Blob 1 Blob 2 Blob 4Blob 3

HTTP GET

サーバ側で順次連結し、1 つのレスポンスで返すサーバー側で順次連結し、1 つのレスポンスで返す

作戦 4 :やりたいこと

54

Firefox

XHR POST Blob1XHR POST Blob2XHR POST Blob3XHR POST Blob4

Firefox Chrome

HTTP GET

Blob 1 Blob 2 Blob 4Blob 3

Blob 1 Blob 2 Blob 4Blob 3

HTTP GET

サーバ側で順次連結し、1 つのレスポンスで返すサーバー側で順次連結し、1 つのレスポンスで返す

“WebM Live Streaming : WMLS”

Node.js

サーバー側の実装• Node.js + express で実装• Stream を理解していない … 苦戦

– 参考: Node.js の Stream API で「データの流れ」を扱う方法 @Jxck_• http://jxck.hatenablog.com/entry/20111204/1322966453

• 複数のファイルを連結して一つの Stream (http.ServerResponse) で返す– 最初 combined-stream を利用

• https://github.com/felixge/node-combined-stream– 後に自前の仕組みに変更

• 最初からすべてのファイルが揃っていない– 後から到着するファイルを待って、順々に連結する– 適切なやり方が分からない

• 仕方なく、 setTimeout() によるポーリングで実現

• GitHub で公開中– https://github.com/mganeko/wmls

• 詳細は別途、 Node.js系の場で?? 55

デモ

56

作戦 4 の結果• できた!–疑似的なリアルタイム中継が可能

• HTTP 通信だけで実現– WebRTC の通信部分は全く使わず

• もしかして、「あれ」ができるのでは?

作戦 5 :「中継を中継する」 Part3

Firefox

XHR POST BLOB1

Firefox

Chrome

HTTP GET .webm

Part1: WebRTC Meetup Tokyo #2 http://www.slideshare.net/mganeko/meetup2-lt-audioPart2: WebRTC Meetup Tokyo #4 http://www.slideshare.net/mganeko/webrtc-meetup4-lt

HTTP POST

HTTP POST

Firefox Chrome

Firefox

Chrome

58

XHR POST BLOB2XHR POST BLOB3

HTTP POST

Node.js

Node.js

Node.js

Node.js

作戦 5 の結果• できた! 音も鳴った! (デモは省略)

– 同一マシン上の疑似環境だけど、原理的には OK– 当然遅延はあるけど、片方向なら許容できそう

• 良いところ(作戦 4, 5含めて)– サーバー側で再エンコード不要

• 悪いところ(作戦 4, 5含めて)– ファイルサイズが大きい

• 1.5MB / 5sec → 2.4Mbit / sec , 18MB / min → 1GB / hour

– Chrome / Firefox でのみ、再生可能– iOS はもう一歩

• Safari では NG• VLC for iOS で、 Video のみ再生可能。 Audio鳴らず…

• 天の声:「それ、 HLS で良くない?」 59

HLS: HTTP Live Streaming• HLS: HTTP Live Streaming

– Apple が開発した、動画配信の方式– iOS, OS X Safari で動画をストリーム再生– PC/Mac の Chrome, Firefox, IE は ×– Android は再生できるはず→試した範囲では、なぜか ×

• プレイリスト . m3u8• 複数の .ts ファイル (MPEG Transport Stream)

– H264 + AAC/MP3– ※ffmpeg で VP8 から変換できる

• H264 を有効にするには、自分でビルドする必要あり– 有償サービス /製品で利用するには、 H264 のライセンス料必要

• 実時間以内で終わらすには、そこそこ高スペックが必要• VP8→H264 変換、 TS 分割の 2 ステップが必要( 1 ステップでは失敗) 60

#EXTM3U#EXT-X-PLAYLIST-TYPE:VOD#EXT-X-TARGETDURATION:10#EXT-X-VERSION:3#EXT-X-MEDIA-SEQUENCE:0#EXTINF:10.0,http://example.com/movie1/sequence1.ts#EXTINF:10.0,http://example.com/movie1/sequence2.ts#EXTINF:10.0,http://example.com/movie1/sequence3.ts#EXTINF:9.0,http://example.com/movie1/sequence4.ts

#EXT-X-ENDLIST

sample.m3u8

sequence1.ts

sequence2.ts

sequence3.ts

sequence4.ts

H264 + AAC/MP3

終了の合図記述が無ければ、クライアントが繰り返し読みに

来る

プレイリストと TS ファイル

61

HLS vs. WMLS• HLS

– ○: 仕組みがシンプル• ただのファイルの集合体なので、 CDN などに置ける• キャッシュ ( ブラウザ、 Proxy) 、アンチウィルスとも相性が良い

– ○: クライアントが賢い。サーバー負荷低い– ○: 通信切断時に、再開しやすい

• クライアント側がどこから再開すれば良いか分かっている– △:再生環境が限られる (iOS, Mac OSX)– × :サーバー側で、再エンコードが必要なケースが多い

• WMLS– × :仕組みが面倒

• サーバーのロジックが必要• キャッシュ ( ブラウザ、 Proxy) 、アンチウィルスと相性が良くない

– × :クライアントは単純。サーバーが頑張る– × :通信切断時に、どこから再開すれば良いか分からない(?)– △:再生環境が限られる (Chrome, Firefox)– ○:サーバ側で、再エンコードが不要 62

キャッシュ問題

63

ブラウザ

.ts

.ts

.ts

ブラウザ

.webm

HLS

WMLS

http://server.com/channelA/0001.ts

http://server.com/channelA/0002.ts

http://server.com/channelA/1001.ts

http://server.com/channelA

.webm

http://server.com/channelA

必ずパーマネントリンクがあるので、

キャッシュが邪魔になることはない

不用意に URL を使いまわすとキャッシュが利いて、過去の映像が再生され

.ts http://server.com/channelA/1002.ts

アンチウィルス問題

64

アンチウィルスサービス ブラウザ

.ts

.ts

.ts

.ts

.ts

.ts

アンチウィルスサービス ブラウザ

.webm

…ウイルススキャンが終わらない…

(HTTPS なら除外されて OK)

HLS

WMLS

サーバー側を、もっとシンプルに• “HTML5” なんだから、もっとブラウザ側

でできるはず

• MSE: Media Source Extensions– … 使えそう

65

4. Media Source Extensions の夢

66

Media Source Extensions

• http://w3c.github.io/media-source/– JavaScript でメディア再生をコントロールする– 結構、低レベルな印象

• 参考: Media Source Extensions を使ってみた (WebM編 )– http://qiita.com/tomoyukilabs/items/57ba8a982ab372611669

• これを使えば、 1 つの video タグで、連続して再生できるのでは?– なんちゃってダブルバッファー不要– サーバーも頑張らなくて良い

67

作戦 6 :やりたいこと

68

[Headers]Header

InfoTracks

Blob 1

Firefox

XHR POST Blob1

XHR POST Blob2

XHR POST Blob3[Blob1’]ClusterCluster

Blob 2

[Blob2]ClusterCluster

Blob 3

[Blob3]ClusterCluster

作戦 6 :やりたいこと

69

[Headers]Header

InfoTracks

Blob 1

Firefox

XHR POST Blob1

XHR POST Blob2

XHR POST Blob3[Blob1’]ClusterCluster

Blob 2

[Blob2]ClusterCluster

Blob 3

[Blob3]ClusterCluster

Chrome

XHR GET Headers→ 初期化セグメントとして利用

XHR GET Blob2→ メディアセグメントとして利用

XHR GET Blob1’→ メディアセグメントとして利用

作戦 6 :再生側実装イメージ

70

var video = document.getElementById('video');var ms = new MediaSource();var sb = ms.addSourceBuffer('video/webm; codecs="vp8,vorbis" '); sb.addEventListener('updateend', appendMediaSegment, false);sb.appendBuffer(header_part); // <-- XHR で取得しておくvideo.src = URL.createObjectURL(ms);

function appendMediaSegment() { var segment = sliceNextCluster(current_media_blob); // <-- XHR で取得しておく if (segment) { sb.appendBuffer(segment); } else { requstNextMediaBlob(); // <-- XHR で取得をリクエストする setTimeout(appendMediaSegment, 1000); // <-- XHR で取得したころに再実行 }}

赤: MSE青:自分で

作戦 6 の結果• 失敗。何も表示されず• 原因追及 chrome://media-internals

71

作戦 6 の結果

• Got a block with a timecode before the previous block• どうやら「 Audio Track のブロックの timecode が

変」と言われている• Cluster の内部構造を調べる必要あり

72

Cluster の内部

73

Cluster

SimpleBlock• Track Number   1:Video• Timecode some value

Timecode

SimpleBlock• Track Number   2:Audio• Timecode 0

Media Source Extensions は、ここに厳密※video で普通に再生する分には OK

作戦 6 :再生側実装イメージ

74

var video = document.getElementById('video');var ms = new MediaSource();var sb = ms.addSourceBuffer('video/webm; codecs="vp8" '); // NO Audiosb.addEventListener('updateend', appendMediaSegment, false);sb.appendBuffer(header_part); // <-- XHR で取得しておくvideo.src = URL.createObjectURL(ms);

function appendMediaSegment() { var segment = sliceNextCluster(current_media_blob); // <-- XHR で取得しておく if (segment) { sb.appendBuffer(segment); } else { requstNextMediaBlob(); // <-- XHR で取得をリクエストする setTimeout(appendMediaSegment, 1000); // <-- XHR で取得したころに再実行 }}

※getUserMedia() 側もAudio 無しで

作戦 6’ の結果• 成功– 映像だけなら再生可能– 音声なし ・・・残念!

– 時々映像止まる? • 原因不明• 自分の JavaScript の不備かも

75

まとめ• MediaRecorder に期待

– CODEC 指定や、ビットレート指定ができると嬉しい

• Media Source Extensions はまだ夢– Youtube では使われているらしい– 厳密すぎるのでは?

• 自己流で真似して、 HLS のシンプルさ / 優秀さを実感– 対応プラットフォーム / ブラウザが増えると良い

• DRM/著作権保護が入ってくると、がらりと変わりそう– ライブ配信の場合は、認証の仕組みで弾けば良さそう

• MPEG-DASH というのもあるらしい… 76

Thank you!

無理やり、最高!

77