Anime AR Stock

Vol.2

カメラは「開けば映る」では済まない — Chrome の 300×150、Center Stage、OBS 仮想カメラ

  • getUserMedia
  • iOS Safari
  • Chrome
  • macOS
カメラ起動の状態機械idlestartCamera()requestingplaysInlinelivedenied / error
図2. カメラはマウントでは開かない。ユーザー操作のあと、playsInline 付きで getUserMedia。

前提: カメラはユーザー操作まで開かない

useUserMediaCamera は、フックをマウントしただけではカメラを開きません。呼び出し側が startCamera() を呼んで初めて getUserMedia します。フックの冒頭コメントにも「Required for iPhone Safari permission UX」と書いてあり、iPhone Safari で権限ダイアログをユーザー操作から切り離さないための設計です。video には muted と playsInline を必ず付けます。

フェーズは idle / requesting / live / denied / error の 5 値です。NotAllowedError(と古い PermissionDeniedError)だけを denied に分け、「ブラウザの設定からカメラを許可してください」という、ユーザーが次に取るべき行動を書いた文言を出します。それ以外の失敗は error として原因メッセージをそのまま出します。live になった時点で、体験 ID がある場合だけ camera_granted を計測します。

Chrome: トラックが 300×150 で始まる

Chrome では、映像トラックが <video> 要素のレイアウトサイズに合わせて切り抜き・縮小されることがありました。<video> を DOM に置いた直後はまだサイズが決まっていないので、最初のシンクが既定の 300×150 のプレースホルダになり、MediaPipe に渡るフレームもそのサイズに引きずられます。

対策は 2 つです。制約に resizeMode: 'none' を入れてブラウザ側の縮小を断り、さらに srcObject を付ける前に video の width / height 属性と CSS サイズを 1280×720 に固定します。ストリームが来た後は pinHandsVideoIntrinsicSize() で実際の videoWidth × videoHeight に合わせ直します。

lib/cameraCapture.ts / hooks/useUserMediaCamera.ts
1export const CAMERA_IDEAL_WIDTH = 1280;2export const CAMERA_IDEAL_HEIGHT = 720;3 4export function videoTrackConstraints(facingMode: 'user' | 'environment') {5  return {6    facingMode,7    width: { ideal: CAMERA_IDEAL_WIDTH },8    height: { ideal: CAMERA_IDEAL_HEIGHT },9    frameRate: { ideal: 30 },10    // Chrome otherwise crop-and-scales the track to the <video> CSS box.11    resizeMode: 'none'12  } as MediaTrackConstraints;13}14 15// useUserMediaCamera: attach の前に 1280×720 を固定する16video.width = CAMERA_IDEAL_WIDTH;17video.height = CAMERA_IDEAL_HEIGHT;18video.style.objectFit = 'fill';19const stream = await getPreferredUserMediaStream(facingMode);

なお、以前の記事では「タッチ端末は 1280×720、それ以外は 720×560」と書いていましたが、現在のコードは端末を問わず ideal 1280×720・30fps を要求しています。

Mac: Center Stage が印を画面の外へ追い出す

新しめの Mac の FaceTime カメラは、Center Stage(PTZ の自動フレーミング)が既定で有効です。自動で顔に寄るので、胸の高さで結ぶ印がフレームの外に出るか、CNN に渡す手の切り抜きの中で小さくなりすぎます。人物を撮るカメラとしては正しい挙動ですが、手を撮りたいアプリには逆効果です。

disableCameraAutoFraming() は、トラックの getCapabilities() に zoom / pan / tilt があれば、zoom を最小(最も広角)、pan / tilt を 0 に固定します。PTZ に対応しないカメラやブラウザでは何もしません。

OBS 仮想カメラを避けて実カメラを開き直す

Call Studio は「OBS に撮られる側」の画面です。ところが OBS Virtual Camera を入れている Mac では、Chrome が直前に選んだデバイス(OBS を含む)を facingMode より優先することがあり、カメラ映像の代わりに OBS のプレースホルダ画像が映りました。

デバイスのラベルは権限を取った後でないと読めないので、getPreferredUserMediaStream() は一度普通に getUserMedia してから enumerateDevices() し、ラベルが obs / virtual cam / snap camera / mmhmm / droidcam などに一致するデバイスを除外します。前面カメラなら facetime / built-in / continuity などの内蔵カメラを優先し、今開いているものと違えば deviceId: exact で開き直します。

A. デバイス選択 UI を出す
メリット
確実にユーザーの意図どおりになる。
デメリット
最初の体験に 1 ステップ増える。仮想カメラの存在を知らない人には意味が分からない。
B. ラベルで仮想カメラを除外して開き直す採用
メリット
ユーザー操作なしで実カメラになる。
デメリット
getUserMedia が 2 回走ることがある。ラベルの正規表現は未知の仮想カメラに追従できない。

採用理由: 体験の入口に UI を足したくなかった。外れた場合も最初のストリームにフォールバックするので、最悪でも従来と同じ挙動になる。

実装から得た知見

  • カメラの不具合は「権限が通らない」より「権限は通ったのに映像がおかしい」方が見つけにくい。Chrome の 300×150 のように、エラーが出ないまま推論の入力だけが劣化する。
  • 人物向けに賢くなったカメラ機能(Center Stage)は、手を撮る AR では敵になる。トラックの capabilities を見て明示的に止める。
  • デバイスラベルは権限取得後にしか読めない。「一度開いてから選び直す」の 2 段階を前提に設計する。
  • denied だけは専用の文言にする。ユーザーが取れる行動(設定から許可し直す)が他の失敗と違うから。