Slide 1

Slide 1 text

Media Capture and Streams: W3C仕様と現場での知見

Slide 2

Slide 2 text

未来の「当たり前」の教育をつくる ほんの少し先の未来、 ネットで学ぶときに「当たり前」とされるようなサービスを創るのは、 私たちにしかできない。 日々そう考えて企画開発しています。 それは教育の「当たり前」を変えること。 この前人未到の挑戦に、加わってみませんか。

Slide 3

Slide 3 text

中村遼大 / @nowaki28 ● 教育事業本部サービス開発部Webフロントセクション ● 2021~ 中途入社 ● 11/30にフロントエンドカンファレンス関西で登壇予定

Slide 4

Slide 4 text

背景 ● ZEN大学関連でオンライン試験システムを開発している ● 不正対策の一環で、カメラを用いた本人確認や受験者映像の記録を実装 ○ MediaStream APIの存在は知っていたが、初めてしっかり触った ○ 今日はそのときの話も踏まえて、 MediaStream API周りの話をします

Slide 5

Slide 5 text

今日のゴール ● W3CのMedia Capture and Streams仕様(https://www.w3.org/TR/mediacapture-streams)がなんとなく分かる ● これからMediaStream APIに触れる人が@nowaki28と同じ失敗をしなくて済む 扱わない話題 ● (P2Pなど)通信に関する話題 ● ウェブオーディオ API, 画面収録 APIなど、カメラ以外の個別のメディアに関する話題 ● その他

Slide 6

Slide 6 text

Media Capture and Streams: 概要

Slide 7

Slide 7 text

Media Capture and Streams https://www.w3.org/TR/mediacapture-streams > This document defines APIs for requesting access to local multimedia devices, such as microphones or video cameras. ● マイクやカメラのようなデバイスへのアクセスを提供する API群の仕様 ● WebRTC WGが担当 ※W3C仕様書は基本的にブラウザベンダ向けの文書なので先に MDNを読んだ方がよい

Slide 8

Slide 8 text

MediaStream API https://www.w3.org/TR/mediacapture-streams/#stream-api ● Webアプリケーションにメディアストリームの入出力制御手段を提供する API群 ○ ストリームがどこで消費されるか ○ メディアを生成するデバイスの制御 ○ 利用可能なデバイスの情報 ● 主にMediaStreamTrackとMediaStreamインターフェースから構成される ○ MediaStreamTrack ■ 音声や動画のトラック ○ MediaStream ■ 複数(0個以上)のMediaStreamTrackを保持するオブジェクト ■ カメラやマイクの場合、 navigator.mediaDevices.getUserMedia() で取得

Slide 9

Slide 9 text

メディアパイプライン ざっくりいうと: MediaStream API = 多様なsource(入力元)から多様なsink(出力先)へのデータパイ プラインを扱うためのAPI ● source: カメラ, マイク, 画面収録など ● sink: , などのメディア要素(HTMLMediaElement)など

Slide 10

Slide 10 text

引用: TPAC-2020-Joint-Meetings slides https://docs.google.com/presentation/d/1gxTvUTlJT9xUrb2uWIQaTuiA_rINwho80hXJ9_UWkNk/edit?slide=id.ga1a6d12563_21_253#slide=id.ga1a6d12563_21_253

Slide 11

Slide 11 text

メディアトラックの状態管理

Slide 12

Slide 12 text

失敗談: Safariでだけ起きる不具合 ● カメラ使用中に別タブでカメラを使用、停止 →戻ってきた後にトラックが黒いフレームを表示し続ける不具合 当初は何も分からなすぎて手探りだった →

Slide 13

Slide 13 text

ライフサイクルとメディアフロー MediaStreamTrackにはLife-cycleとMedia Flowという2つのレイヤでの状態が存在する ● Life-cycle: https://www.w3.org/TR/mediacapture-streams/#life-cycle ○ トラックの実体が存在しているかどうか ○ “live”or“ended” ● Media Flow: https://www.w3.org/TR/mediacapture-streams/#media-flow ○ トラックが伝送可能な状態かどうか ○ muted, enabledの2種のフラグが存在

Slide 14

Slide 14 text

ライフサイクル Life-cycle:トラックの実体が存在しているかどうか ● MediaStreamTrack.readyState: トラックの生存状態 ○ “live” | “ended” ○ 明示的なMediaStreamTrack.stop() の呼び出し,デバイスの喪失, ネットワークセッションの終了な どによりendedに遷移 ■ 明示的にstop()した場合を除き、endedイベントが発火 ○ 一度endedになったトラックは生き返らない

Slide 15

Slide 15 text

メディアフロー Media Flow: トラックが伝送可能な状態かどうか 更に2軸で状態が存在する ● MediaStreamTrack.muted: アプリケーション外の要因で停止中 ○ Boolean ○ 他のアプリケーションがデバイスを占有するなど ■ mute/unmuteイベントが発火 ● MediaStreamTrack.enabled: アプリケーション内の要因で停止中 ○ Boolean ○ アプリケーションがトラックを一時的に停止させたい時に制御するためのフラグ ■ JavaScriptから設定可能 ■ enabled = falseに設定してもデバイス自体は稼働し続ける

Slide 16

Slide 16 text

振り返り: Safariでだけ起きる不具合 ● 別タブでカメラ使用→元のページのトラックがmuted = trueになっていた ○ 別タブでカメラ解放後もunmuteされないのは謎 ● ユーザがページに戻ってきた後でトラックの状態を確認し、 mutedになっていたらMediaStream を作り直すことで対処 🧠💭複数のブラウザ・複数のデバイスでテストすることは大事

Slide 17

Slide 17 text

ライフサイクルとメディアフロー: まとめ MediaStreamTrackの状態を表す3つのフラグ ● readyState (read-only): トラックの生存状態…"live" | "ended" ● muted (read-only): 一時的に使用不可能な状態(外的要因) ● enabled: アプリケーション上での出力制御(内的要因) i.e. トラックが正常に再生されている状態 = { readyState: “live”, muted: false, enabled: true }

Slide 18

Slide 18 text

メディアリソースの解放

Slide 19

Slide 19 text

失敗談: 「カメラ使用中」が消えない ● useEffectでgetUserMedia()してにアタッチするReactコンポーネントを実装していた ○ コンポーネントがアンマウントされてもカメラ使用中のインジケータが消えない ○ 要素は実際にDOMから消えている

Slide 20

Slide 20 text

ガベージコレクション https://www.w3.org/TR/mediacapture-streams/#garbage-collection > A MediaStreamTrack object MUST NOT be garbage collected if it is not ended and there are any event listeners registered for mute, unmute or ended events. Each source type can further refine the garbage collection rules as sources may never fire a particular event. ● UAは“live”でイベントリスナが登録されている MediaStreamTrackをGCしない ○ そもそも、普通のオブジェクトも参照が無くなって即座にGCされる訳ではないが... ● MediaStreamTrackが生きている間はデバイスも稼働し続ける ○ ユーザへのフィードバック(プライバシーインジケータ)も消えない ○ 明示的にMediaStreamTrack.stop()を呼び出し停止することを推奨

Slide 21

Slide 21 text

振り返り: 「カメラ使用中」が消えない ● コンポーネント内で完結しているため、あまり考えずに (GCされるので)放っておいてもいいと 思っていた ○ → 一度カメラを使うとインジケータが消えないバグ ○ そもそもGCとデバイスリソースの解放は別物なので依存するべきではない ■ GCされたらデバイスドライバをクローズするのはUA実装依存の挙動 ● cleanup関数で track.stop() することで解決

Slide 22

Slide 22 text

メディアリソースの解放: まとめ ● MediaStreamを破棄するときは必ず明示的に終了 : MediaStreamTrack.stop() する ○ Reactコンポーネントの副作用などでストリームを開始している場合は特に注意

Slide 23

Slide 23 text

デバイスの利用

Slide 24

Slide 24 text

Device Enumeration: 利用可能なデバイスの列挙 ● MediaDevices.enumerateDevices()で利用可能なデバイスの一覧を取得可能 ○ ユーザがどのデバイスを使うか選択するような UIではこのAPIを使う

Slide 25

Slide 25 text

失敗談: 空のデバイス選択プルダウン ● デバイス選択UIでプルダウンの内容が空 ● MediaDevices.enumerateDevicesを返していた ○ 当然、カメラは物理的に存在する ■ 🧠💭カメラが認識されていない...?

Slide 26

Slide 26 text

Device Enumeration: 利用可能なデバイスの列挙 enumerateDevices()はユーザが利用に同意したデバイスの情報しか表示しない Web APIはData Minimization原則に従ってユーザの同意無く finger printとなる情報を公開しない ● 初回は getUserMedia() → enumerateDevices() の順に実行する必要がある ○ ユーザが同意しているかどうかは Permissions APIから判定可能

Slide 27

Slide 27 text

Permissions API https://www.w3.org/TR/permissions ● メディアデバイスに限らず、アプリケーションが UAに強力な機能を要求するための API ○ 位置情報, 通知, クリップボードの読み取りなど ● Permissions.query(permissionDiscriptor)で現在のユーザの同意状況を確認できる ● PermissionStatus: “prompt”| “granted” | “denied” の3値 ○ 初期値は “prompt”

Slide 28

Slide 28 text

振り返り: 空のデバイス選択プルダウン ● デバイス選択UIで enumerateDevices() だけ先に呼んでいた ○ ユーザが同意前なのでラベルや IDが取得できなかった ■ エラーにならず匿名のオブジェクトが返るだけなので仕様をちゃんと読んでいないとハマる ● getUserMedia() → enumerateDevices() に順序を変えて解決 { "deviceId":"", "kind":"videoinput", "label":"", "groupId":"" } { "deviceId":"...", "kind":"videoinput", "label":"FaceTime HDカメラ (B6DF:451A)", "groupId":"..." } ユーザの同意

Slide 29

Slide 29 text

メディアの選択と制御

Slide 30

Slide 30 text

Constraints: 制約 https://www.w3.org/TR/mediacapture-streams/#dfn-constraint どのようなトラックをUAに要求するかはConstraintsとして指定する ● 実際に適用できる値が選ばれる ○ デバイスやその動作条件を決定するのに用いられる ■ 指定値に合わせてUAが映像や音声を加工するものではない ○ 可能な範囲でConstraintsに最も近い設定が適用される ※ resizeMode: “crop-and-scale”の場合はデバイス側の機能によりある程度柔軟にリサイズ可能 navigator.mediaDevices.getUserMedia({ "video": { "width": 1280, "facingMode": "user", }, });

Slide 31

Slide 31 text

Capabilities: 能力 https://www.w3.org/TR/mediacapture-streams/#dfn-capabilities Capabilities = デバイス側の実際に持っている性能 ● MediaDevices.getSupportedConstraints() ○ UAがサポートするConstraintキーを取得 ● MediaStreamTrack.getCapabilities() ○ デバイスが実際にサポートする Constraintキーを取得 ■ e.g. 物理カメラ依存の高度な制約(露出, ISO, ホワイトバランスなど) ○ 高度な制御を行う際の feature detectionなどに ※MediaStreamTrack.getCapabilities()がNewely AvailableなAPIであることに留意

Slide 32

Slide 32 text

Settings(Source Settings): 設定 https://www.w3.org/TR/mediacapture-streams/#dfn-settings アプリケーションからの要求(Constraints)に対してデバイス側の実際に持っている性能 (Capabilities)を突き合わせた結果、実際に適用された設定 ● MediaStreamTrack.getSettings() で確認可能

Slide 33

Slide 33 text

失敗談: デバイスを選択しても切り替わらないバグ ● プルダウンでどのデバイスを使用するか選択する UI() ○ ユーザがプルダウンで切り替えても使われるカメラ・マイクが切り替わらないバグ ○ Chrome(, Edge)でだけ発生する ←プルダウンは切り替わるが映像は変わらない

Slide 34

Slide 34 text

二種類のConstraint: ideal / exact ● ideal (default): できればこの値にしてほしい ● exact: この値でなければならない ○ getUserMedia(constraints) 時にconstraintを満たすデバイスが無い場合は例外 : OverconstrainedError をthrow →指定したcapabilityを備えるデバイスしか使いたくないのか、一番近いものを使いたいのかアプリケーションの 仕様と相談して選択 { "video": { "width": 1280, "height": 720, "frameRate": { "min": 24, "ideal": 30 }, "facingMode": { "exact": "user" } } }

Slide 35

Slide 35 text

振り返り: デバイスを選択しても切り替わらないバグ ● deviceIdをexact指定しないで渡していた ○ Chromeの実装がexactではないdeviceIdの指定を無視するように (デフォルトにフォールバック) ○ プルダウンでデバイスを選択しても切り替わらないバグへ ○ →exactで指定することで解消

Slide 36

Slide 36 text

メディアの選択と制御: まとめ ● Constraints = どんなsourceが欲しいか →UAがデバイス能力(Capabilities)と付き合わせる →最終的に適用される設定 (Settings)が決定する ● ideal (デフォルト値) = できるだけ近いもの, exact = 満たせなければエラー ○ デバイス選択などでは deviceIdをexactで指定する User Agent Application Media Device constraints: width: 1920 height: 1080 frameRate: 30 capabilities: width: 640-1280 height: 480-720 frameRate: 30-60 settings: width: 1280 height: 720 frameRate: 30 getUserMedia(constraints) MediaStream

Slide 37

Slide 37 text

写真を撮る

Slide 38

Slide 38 text

引用(赤線部は加工): TPAC-2020-Joint-Meetings slides https://docs.google.com/presentation/d/1gxTvUTlJT9xUrb2uWIQaTuiA_rINwho80hXJ9_UWkNk/edit?slide=id.ga1a6d12563_21_253#slide=id.ga1a6d12563_21_253

Slide 39

Slide 39 text

ImageCapture https://www.w3.org/TR/image-capture ● ビデオトラックから静止画を取得するための API ● new ImageCapture(MediaStreamTrack) 引用: https://developer.mozilla.org/en-US/docs/Web/API/ImageCapture

Slide 40

Slide 40 text

ImageCapture 引用: https://developer.mozilla.org/en-US/docs/Web/API/ImageCapture ● GoogleChromeLabs/imagecapture-polyfill: https://github.com/GoogleChromeLabs/imagecapture-polyfill

Slide 41

Slide 41 text

ImageCapture.grabFrame() ● MediaStreamTrackから現在のフレームを bitmap形式で取り出す ○ 加工(clip, resize, …etc)などの用途に向く ImageCapture.takePhoto(photoSettings) ● blobで返してくれるので使いやすい ● MediaStreamTrackに適用されているsettingsが適用されるとは限らない ○ https://www.w3.org/TR/image-capture/#dom-imagecapture-takephoto >Devices MAY temporarily stop streaming data, reconfigure themselves with the appropriate photo settings, take the photo, and then resume streaming. In this case, the stopping and restarting of streaming SHOULD cause onmute and onunmute events to fire on the track in question. ■ カメラデバイスが撮影モードに切り替わる場合がある ● i.e. シャッターを切るのと同レベルの制御 ○ デバイスによってはトラックが一時停止する可能性 ■ 引数(PhotoSettings)でサイズなどを指定可能

Slide 42

Slide 42 text

● ImageCapture.takePhoto()で写真撮影するデモ

Slide 43

Slide 43 text

余談: takePhoto() が思ってたより失敗する ● Sentryを見ている感じだとtakePhoto() が失敗すること自体は割とよくあるっぽい ○ muted中や起動直後、スマホの縦横切り替え時などは失敗するらしい ■ orientationの変更、他のアプリによる占有、メモリの状況などの影響を受けるため ● 特にAndroid端末は種類が多くデバイス依存の挙動でエッジケースを踏みやすい ■ 再実行すると問題なく動いたりする ○ ビデオから1フレーム抜くだけのgrabFrame()の方が安定性は高い(ように感じる) ■ takePhoto()は長時間連続でのキャプチャには不向き?

Slide 44

Slide 44 text

今回得た教訓まとめ ● トラックの状態 → readyState, muted, enabledプロパティ ○ Safariはmuteを解除してくれないことがあるっぽい ● 使い終わったら明示的に MediaStreamTrack.stop() でデバイスを解放する ● enumerateDevices() の前に getUserMedia() でユーザの同意を得る ● デバイス選択はdeviceIdをexactで指定する ● 大体の最初にハマるポイントは MDNに書いてあるし、仕様を読めば理解できる リンク ● Media Capture and Streams (W3C Candidate Recommendation Draft) ● Media Capture and Streams API (Media Stream) - Web APIs | MDN

Slide 45

Slide 45 text

ご清聴ありがとうございました

Slide 46

Slide 46 text

未来の「当たり前」の教育をつくる ほんの少し先の未来、 ネットで学ぶときに「当たり前」とされるようなサービスを創るのは、 私たちにしかできない。 日々そう考えて企画開発しています。 それは教育の「当たり前」を変えること。 この前人未到の挑戦に、加わってみませんか。

Slide 47

Slide 47 text

時間が余ったときの話題 ● 要素(PEPC): https://github.com/WICG/PEPC/blob/main/usermedia_element.md ○ 元々として提案されていたもの の一部 ○ TPAC 2025で議題に上がっていた模様 : https://docs.google.com/presentation/d/1sd5zEnvlXO5Sk3ENQorUUIQiRz65sv0KZKxDMMYHM3I/edit?slide=id.g37005560f20_2_0 #slide=id.g37005560f20_2_0 ● Background Blur Effect: https://w3c.github.io/mediacapture-extensions/#background-blur-effect-status ○ Constraintsの backgroundBlur フラグで、JSからカメラの背景ぼかしを操作 ■ Media Capture and Streams Extensionsの中で提案されている仕様 ○ Chromeで実験的な機能のフラグを有効にすれば試すことができる ■ chrome://flags/#enable-experimental-web-platform-features