ビデオ通話、バーコードの読み取り、証明写真の撮影など、カメラの映像を画面に出すなら WebView 標準の getUserMedia() で取得して <video> に映します。Tauri のプラグインや Rust のコードは要らず、capability の追加も不要です。その代わり OS ごとのカメラの許可に合わせた準備が要り、特に macOS は Info.plist に使う理由を書かないとカメラを使えません。マイクの録音は マイクから音声を録音する で扱います。
前提条件
getUserMedia() は Tauri のコマンドを通らないので、権限は core:default のままで構いません。
macOS では、カメラを使う理由を src-tauri/Info.plist に書きます。許可ダイアログにこの文が表示されます。このファイルはビルド時に既定の Info.plist にマージされ、tauri dev の実行ファイルにも埋め込まれます(tauri dev で読まれるのはこの場所のファイルだけです)。音声も取るなら NSMicrophoneUsageDescription も並べます。
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>NSCameraUsageDescription</key>
<string>書類に添付する写真を撮るためにカメラを使います</string>
</dict>
</plist>
署名して配布する場合は bundle.macOS.hardenedRuntime が既定で true なので、エンタイトルメントのファイル(例では src-tauri/Entitlements.plist)に com.apple.security.device.camera を入れ、bundle.macOS.entitlements で指定します。
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>com.apple.security.device.camera</key>
<true/>
</dict>
</plist>
{
"bundle": {
"macOS": {
"entitlements": "./Entitlements.plist"
}
}
}
OS ごとに必要な準備は次のとおりです。
| OS | 準備 | 初回の確認 |
|---|---|---|
| Windows | 設定の「プライバシーとセキュリティ」→「カメラ」で、デスクトップアプリからのアクセスをオン | WebView の確認が出ることがある |
| macOS | Info.plist の説明文、署名するならエンタイトルメント | OS のダイアログ(説明文が表示される) |
| Linux | 既定のままでは使えない場合がある | ― |
1. フロントエンドから実装する (TypeScript)
映像を video 要素に映す
muted を付けた <video> は、ユーザーの操作なしで再生を始められます。
<video id="preview" autoplay playsinline muted></video>
<select id="camera"></select>
getUserMedia() の width / height に ideal を付けると「できればこの解像度」という指定になり、対応していないカメラでも近い値で開けます。exact にすると、合わないときに失敗します。使い終わったらトラックを stop() します。止めないとカメラを使ったままになり、カメラのランプも消えません。
const video = document.querySelector<HTMLVideoElement>('#preview')!;
let stream: MediaStream | null = null;
// カメラを止めて解放する
export function stopCamera(): void {
stream?.getTracks().forEach((t) => t.stop());
stream = null;
video.srcObject = null;
}
// deviceId を省略すると既定のカメラ。戻り値で実際の解像度が分かる
export async function startCamera(deviceId?: string): Promise<MediaTrackSettings> {
if (!navigator.mediaDevices?.getUserMedia) throw new Error('getUserMedia が使えません');
stopCamera(); // 切り替えるときは先に止める(同じカメラを二重に開けない機種がある)
stream = await navigator.mediaDevices.getUserMedia({
video: {
deviceId: deviceId ? { exact: deviceId } : undefined,
width: { ideal: 1280 },
height: { ideal: 720 },
},
audio: false,
});
video.srcObject = stream;
await video.play();
return stream.getVideoTracks()[0].getSettings();
}
// 失敗の理由(DOMException の name)を画面向けの文にする
export function describeCameraError(e: unknown): string {
const name = typeof e === 'object' && e !== null && 'name' in e ? String(e.name) : '';
switch (name) {
case 'NotAllowedError': return 'カメラの使用が許可されていません';
case 'NotFoundError': return 'カメラが見つかりません';
case 'NotReadableError': return 'カメラを開けません。ほかのアプリが使っている可能性があります';
case 'OverconstrainedError': return '指定した条件に合うカメラがありません';
default: return String(e);
}
}
カメラを選ぶ・切り替える
enumerateDevices() は、カメラの使用を許可される前に呼ぶと名前(label)が空になり、台数も正しく分からないことがあります。最初に既定のカメラで startCamera() してから一覧を作ります。選んだカメラは deviceId で保存し、次回はそれで開きます。外されていると exact の指定で失敗するので、既定のカメラに戻します。
// (続き)
const select = document.querySelector<HTMLSelectElement>('#camera')!;
const KEY = 'camera-id';
export async function fillCameraList(): Promise<void> {
const cams = (await navigator.mediaDevices.enumerateDevices()).filter((d) => d.kind === 'videoinput');
select.replaceChildren(...cams.map((c, i) => new Option(c.label || `カメラ ${i + 1}`, c.deviceId)));
const current = stream?.getVideoTracks()[0]?.getSettings().deviceId;
if (current) select.value = current;
}
select.addEventListener('change', async () => {
localStorage.setItem(KEY, select.value);
await startCamera(select.value);
});
// USB カメラの抜き差しで一覧を作り直す
navigator.mediaDevices.addEventListener('devicechange', () => void fillCameraList());
// 起動時: 前回のカメラ、だめなら既定のカメラで開く
export async function initCamera(): Promise<void> {
const saved = localStorage.getItem(KEY) ?? undefined;
try {
await startCamera(saved);
} catch (e) {
if (!saved) throw e;
await startCamera(); // 前回のカメラが外されている
}
await fillCameraList();
}
静止画を取り出す
今の 1 コマを canvas に描いて PNG にします。canvas の大きさは表示サイズではなく videoWidth / videoHeight(実際の解像度)に合わせます。自分の顔を映すときは CSS の transform: scaleX(-1) で鏡像にすることが多いですが、CSS は保存する画像には効かないので、そろえたいなら描くときにも反転します。
// (続き)
export async function captureFrame(mirror = false): Promise<Uint8Array> {
const canvas = document.createElement('canvas');
canvas.width = video.videoWidth;
canvas.height = video.videoHeight;
const ctx = canvas.getContext('2d');
if (!ctx || canvas.width === 0) throw new Error('映像がまだ届いていません');
if (mirror) {
ctx.translate(canvas.width, 0);
ctx.scale(-1, 1);
}
ctx.drawImage(video, 0, 0);
const blob = await new Promise<Blob>((resolve, reject) =>
canvas.toBlob((b) => (b ? resolve(b) : reject(new Error('toBlob に失敗しました'))), 'image/png'),
);
return new Uint8Array(await blob.arrayBuffer());
}
できたバイト列は バイナリデータをファイルに保存する の writeFile() で保存し、保存先をユーザーに選ばせるなら ファイルを保存する場所を選ばせる と組み合わせます。撮った写真を紙に出すなら、<img> に表示して window.print() するのが手軽です(プリンターの一覧を取得して印刷する)。
動作確認
npm run tauri dev で起動して initCamera() を呼ぶと、プレビューに映像が出てカメラのランプが点きます。macOS では初回に OS の許可ダイアログが出ます。コンソールで await startCamera() の戻り値を見ると、実際に開いた解像度(width / height)が分かります。stopCamera() を呼ぶとランプが消えます。
よくあるエラーと対処法
NotAllowedError: OS か WebView で拒否されています。Windows はプライバシーの設定を確かめます。macOS は一度拒否すると二度とダイアログが出ないので、システム設定の「プライバシーとセキュリティ」→「カメラ」で許可し直します。Info.plist の説明文が無いときも使えません。navigator.mediaDevicesがundefined:getUserMedia()は安全な場所(セキュアコンテキスト)から読み込んだページでしか使えません。開発時にdevUrlをhttp://192.168.x.x:1420のような IP アドレスにしていると起きます。localhostなら使えます。NotReadableError: ほかのアプリがカメラを使っているか、切り替え時に前のストリームを止めていません。OverconstrainedError:exactで指定したカメラや解像度がありません。idealにするか、既定のカメラで開き直します。- カメラの名前が空・1 台しか出ない: 許可を得る前に
enumerateDevices()を呼んでいます。
OS ごとの違いと注意点
- Windows: OS のプライバシー設定でデスクトップアプリのカメラがオフだと使えません。初回などに WebView の確認が出ることがあります。
- macOS: Info.plist の説明文が必須で、署名するならエンタイトルメントも要ります(前提条件を参照)。
- Linux: WebView の設定によっては、既定のままでは
getUserMedia()が使えない場合があります。配布先で動くかを早めに確かめます。 - ウィンドウを隠す: ウィンドウを隠す・トレイに格納するだけではカメラは解放されません。見えないあいだは
stopCamera()で止めます。 - Rust で映像を処理したい: 画像認識などを Rust で行うなら、WebView で取った静止画をバイト列のまま送るのが簡単です(バイナリデータをファイルに保存する の Rust の例)。
- マイク:
audio: trueで音声も取れます。録音して保存する方法と、Rust の cpal との使い分けは マイクから音声を録音する を参照してください。出力先のスピーカーを選ぶなら 音声の出力スピーカーを切り替える です。
