Successfully reported this slideshow.
We use your LinkedIn profile and activity data to personalize ads and to show you more relevant ads. You can change your ad preferences anytime.

MediaRecorder と WebM で、オレオレ Live Streaming

9,516 views

Published on

WebRTC Meetup #8 の発表資料です。

Published in: Technology
  • Be the first to comment

MediaRecorder と WebM で、オレオレ Live Streaming

  1. 1. MediaRecorder と WebM で オレオレ Live Streaming 2015.06.05 WebRTC Meetup Tokyo #8 インフォコム株式会社 がねこまさし @massie_g 1
  2. 2. 今日のお話 • getUserMedia • RTCPeerConnection • MediaRecorder • CODEC • 「通信」ではなく、「配信」のお話 – スケールするリアルタイム配信の仕組みを作りたい 2
  3. 3. もくじ • ブラウザ側で無茶をする • WebMの内側 • サーバ側で頑張る • Media Source Extensions の夢 3
  4. 4. 1. ブラウザ側で無茶をする 4
  5. 5. Media Recorder API • getUserMedia()で取得したストリームを記録 するAPI – http://www.w3.org/TR/mediastream-recording/ – http://html5experts.jp/mganeko/12475/ • Firefoxで実装済 5
  6. 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(); } 6
  7. 7. 使い方 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
  8. 8. デモ • https://lab.infocom.co.jp/demo/webrtc-recorder.html • 録画を止める → 再生できる 8
  9. 9. リアルタイム配信の準備 • 録画しながら、再生もしたい… • なら、無理やりの出番じゃない? 9
  10. 10. 作戦1 • 短時間で、録画開始、録画停止を繰り返す – 5秒~10秒くらい • 順次、再生する 10
  11. 11. 作戦1 MediaRecorder.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
  12. 12. 作戦1:デモ • https://lab.infocom.co.jp/demo/webrtcfirefox-rec-split.html 12
  13. 13. 作戦2 • 作戦1の結果...blob切り替わり時にちらつく • ちらつき防止には、昔から「ダブルバッファー」 • → 今回は、2つのvideoタグを、交互に表示 13
  14. 14. 作戦2 MediaRecorderstart() … MediaRecorder.stop() MediaRecorderstart() … MediaRecorder.stop() MediaRecorderstart() … MediaRecorder.stop() MediaRecorderstart() … MediaRecorder.stop() Blob 1 Blob 2 Blob 3 Blob 4 video1 (preload) video2 (preload) 14
  15. 15. 作戦2:デモ • https://lab.infocom.co.jp/demo/webrtcfirefox-rec-dual.html 15
  16. 16. 作戦2の結果 • ちらつき、ほとんど分からない – 良く見ると気が付くけど… • これで、配信もできるはず 16
  17. 17. 作戦3:無理やり、映像配信 Firefox XHR POST .webm XHR POST .webm XHR POST .webm XHR POST .webm Firefox Chrome HTTP GET .webm HTTP GET .webm HTTP GET .webm HTTP GET .webm HTTP GET .webm HTTP GET .webm HTTP GET .webm HTTP GET .webm 17 video1 video2 video1 video2
  18. 18. 作戦3の結果 • 配信できた! – デモは省略 • ちらつきも、ほとんど分からない – 良く見ると気が付くけど… • 天の声 – 「ダブルバッファーもどき、無理やりすぎ」 – 「MediaRecorder、もっと良く見ろ」 • MediaRecorder.start(optional long timeslice); – iOSで何とか見れないの? 18
  19. 19. 作戦3の結果 • 配信できた! – デモは省略 • ちらつきも、ほとんど分からない – 良く見ると気が付くけど… • 天の声 – 「ダブルバッファーもどき、無理やりすぎ」 – 「MediaRecorder、もっと良く見ろ」 • MediaRecorder.start(optional long timeslice); – iOSで何とか見れないの? 19
  20. 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(); } 停止しなくて呼ばれる インターバルを 指定できた! (ミリ秒) 20
  21. 21. 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
  22. 22. Blobの中身は? • MediaRecorderで取得したBlobの中身 – evt.data.type = "video/webm“ – どうやらWebMらしい • でも、最初のBlobと、2番目以降のBlobで違 う?? • WebMの中身って、どうなっている?? 22
  23. 23. 2. Inside of WebM 23
  24. 24. WebMとは • Wikipediaより http://ja.wikipedia.org/wiki/WebM – 米Googleが開発している – オープンでロイヤリティフリーな – 動画コンテナフォーマット • コーデック – 映像: VP8 / VP9 – 音声: Vorbis • コンテナ – Matroskaのサブセット
  25. 25. 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と言った方が近いかも – 対応していない機能(タグ)は無視する – テキストではなくバイナリで表現
  26. 26. EMBLのイメージ <EBML> <EBMLVersion>1</EBMLVersion> <DocType>webm</DocType> </EBML> <Segment> … <Info> … </Info> <Tracks> … </Tracks> <Cluster> … </Cluster> <Cluster> … </Cluster> … </Segment> これがバイナリで格納されている
  27. 27. WebMのおおまかな構造 ヘッダー 部分 映像/音声 部分 おまけ 部分 Header Meta Seek Info Segment Info Tracks (Chapters) Cluster Cluster Cluster Cluster (Cue Data) (Attachment) (Tagging) Matroska的 EBML Segment WebRTC的
  28. 28. WebM/Matroska/EBML バイナリのレイアウト
  29. 29. EBML EBMLVersion “webm” Segment 1 Tracks CodecName “VP8” DOCType
  30. 30. タグ/Elementのバイナリ表現 • 3つのパートでタグ/Elementは構成される ID DataSize Data 1~4バイト 1~8バイト 0~0x00FFFFFFFFFFFFFEバイト ペタバイト級まで 可変長 可変長 可変長 実際のデータだけでなく、IDもDataSizeも可変長 厄介なヤツ。マジ勘弁…
  31. 31. 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の値に含める
  32. 32. 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の値は予約済(多分)
  33. 33. 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
  34. 34. 44 http://www.matroska.org/technical/specs/index.html より ID 型
  35. 35. 解析例(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
  36. 36. 解析例(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"
  37. 37. 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 – メモリ上に一括して読み込む、しょぼい実装ですが…
  38. 38. あれ、何の話だっけ? • MediaRecorder.ondataavailable() で • インターバル指定で取得したBLOBの話
  39. 39. MediaRecorderのBlobと WebMの対応 ヘッダー部 Cluster (映像/音声データ) Cluster (映像/音声データ) Cluster (映像/音声データ) Cluster (映像/音声データ) Cluster (映像/音声データ) Blob 1 Blob 2 Blob 3 49 Header Meta Seek Info Segment Info Tracks
  40. 40. ブラウザ側で何とかなる? • 次々と出現するBlob – 最初にはそろっていない • 動的に連結しながら window.URL.createObjectURL() で取り出す → できない (多分) 50
  41. 41. ブラウザ側で何とかなる? • 次々と出現するBlob – 最初にはそろっていない • 動的に連結しながら window.URL.createObjectURL() で取り出す → できない (多分) なら、サーバーで無理やりの出番じゃない? 51
  42. 42. 3. サーバー側で頑張る 52
  43. 43. 作戦4:やりたいこと 53 Firefox XHR POST Blob1 XHR POST Blob2 XHR POST Blob3 XHR POST Blob4 Firefox Chrome HTTP GET Blob 1 Blob 2 Blob 4Blob 3 Blob 1 Blob 2 Blob 4Blob 3 HTTP GET サーバ側で順次連結し、 1つのレスポンスで返す サーバー側で順次連結し、 1つのレスポンスで返す
  44. 44. 作戦4:やりたいこと 54 Firefox XHR POST Blob1 XHR POST Blob2 XHR POST Blob3 XHR 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
  45. 45. サーバー側の実装 • 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
  46. 46. デモ 56
  47. 47. 作戦4の結果 • できた! – 疑似的なリアルタイム中継が可能 • HTTP 通信だけで実現 – WebRTCの通信部分は全く使わず • もしかして、「あれ」ができるのでは?
  48. 48. 作戦5:「中継を中継する」Part3 Firefox XHR POST BLOB1 Firefox Chrome HTTP GET .webm Part1: WebRTC Meetup Tokyo #2 http://www.slideshare.net/mganeko/meetup2-lt-audio Part2: WebRTC Meetup Tokyo #4 http://www.slideshare.net/mganeko/webrtc-meetup4-lt HTTP POST HTTP POST Firefox Chrome Firefox Chrome 58 XHR POST BLOB2 XHR POST BLOB3 HTTP POST Node.js Node.js Node.js Node.js
  49. 49. 作戦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
  50. 50. 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
  51. 51. #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
  52. 52. HLS vs. WMLS • HLS – ○:仕組みがシンプル • ただのファイルの集合体なので、CDNなどに置ける • キャッシュ(ブラウザ、Proxy)、アンチウィルスとも相性が良い – ○:クライアントが賢い。サーバー負荷低い – ○:通信切断時に、再開しやすい • クライアント側がどこから再開すれば良いか分かっている – △:再生環境が限られる(iOS, Mac OSX) – ×:サーバー側で、再エンコードが必要なケースが多い • WMLS – ×:仕組みが面倒 • サーバーのロジックが必要 • キャッシュ(ブラウザ、Proxy)、アンチウィルスと相性が良くない – ×:クライアントは単純。サーバーが頑張る – ×:通信切断時に、どこから再開すれば良いか分からない(?) – △:再生環境が限られる(Chrome, Firefox) – ○:サーバ側で、再エンコードが不要 62
  53. 53. キャッシュ問題 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
  54. 54. アンチウィルス問題 64 アンチウィルス サービス ブラウザ .ts .ts .ts .ts .ts .ts アンチウィルス サービス ブラウザ .webm … … … ウイルススキャンが終わらない… (HTTPSなら除外されてOK) HLS WMLS
  55. 55. サーバー側を、もっとシンプルに • “HTML5”なんだから、もっとブラウザ側ででき るはず • MSE: Media Source Extensions – …使えそう 65
  56. 56. 4. Media Source Extensionsの夢 66
  57. 57. Media Source Extensions • http://w3c.github.io/media-source/ – JavaScript でメディア再生をコントロールする – 結構、低レベルな印象 • 参考:Media Source Extensionsを使ってみた (WebM編) – http://qiita.com/tomoyukilabs/items/57ba8a982ab372611669 • これを使えば、1つのvideoタグで、連続して再生 できるのでは? – なんちゃってダブルバッファー不要 – サーバーも頑張らなくて良い 67
  58. 58. 作戦6:やりたいこと 68 [Headers] Header Info Tracks Blob 1 Firefox XHR POST Blob1 XHR POST Blob2 XHR POST Blob3 [Blob1’] Cluster Cluster Blob 2 [Blob2] Cluster Cluster Blob 3 [Blob3] Cluster Cluster
  59. 59. 作戦6:やりたいこと 69 [Headers] Header Info Tracks Blob 1 Firefox XHR POST Blob1 XHR POST Blob2 XHR POST Blob3 [Blob1’] Cluster Cluster Blob 2 [Blob2] Cluster Cluster Blob 3 [Blob3] Cluster Cluster Chrome XHR GET Headers → 初期化セグメント として利用 XHR GET Blob2 → メディアセグメントとして利用 XHR GET Blob1’ → メディアセグメントとして利用
  60. 60. 作戦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 青:自分で
  61. 61. 作戦6の結果 • 失敗。何も表示されず • 原因追及 chrome://media-internals 71
  62. 62. 作戦6の結果 • Got a block with a timecode before the previous block • どうやら「Audio Track のブロックのtimecodeが変」と言 われている • Clusterの内部構造を調べる必要あり 72
  63. 63. Clusterの内部 73 Cluster SimpleBlock • Track Number  1:Video • Timecode  some value Timecode SimpleBlock • Track Number  2:Audio • Timecode  0 Media Source Extensions は、ここに厳密 ※video で普通に再生する分にはOK
  64. 64. 作戦6:再生側実装イメージ 74 var video = document.getElementById('video'); var ms = new MediaSource(); var sb = ms.addSourceBuffer('video/webm; codecs="vp8" '); // NO Audio 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 で取得したころに再実行 } } ※getUserMedia()側も Audio無しで
  65. 65. 作戦6’の結果 • 成功 – 映像だけなら再生可能 – 音声なし ・・・残念! – 時々映像止まる? • 原因不明 • 自分のJavaScriptの不備かも 75
  66. 66. まとめ • MediaRecorder に期待 – CODEC指定や、ビットレート指定ができると嬉しい • Media Source Extensions はまだ夢 – Youtubeでは使われているらしい – 厳密すぎるのでは? • 自己流で真似して、HLSのシンプルさ/優秀さを実感 – 対応プラットフォーム/ブラウザが増えると良い • DRM/著作権保護が入ってくると、がらりと変わりそう – ライブ配信の場合は、認証の仕組みで弾けば良さそう • MPEG-DASH というのもあるらしい… 76
  67. 67. Thank you! 無理やり、最高! 77

×