メディアデバイス

概要

sora_sdk でマイク・スピーカー・カメラを扱うための API と使い方を解説します。ここでは Sora が持つメディアデバイス関連 API のみを説明します。

audio / video 設定とデバイスの関係

Sora の場合、シグナリング時の audio / video の設定はあくまで「音声や映像を利用するかどうか」の設定であって、特定のデバイスを指定するといった仕組みとは異なります。

Sora Flutter SDK では MediaDevices.getUserMedia() でマイクやカメラを指定して MediaStream を作り、その stream を conn.connect() に渡すことで、任意のデバイスを使った音声・映像の送信ができるようになります。

API リファレンス

MediaDevices.enumerateAudioInputDevices()

音声入力デバイス (マイク) の一覧を取得します。

static Future<List<AudioInputDevice>> enumerateAudioInputDevices();
引数:

なし

戻り値:

AudioInputDevice のリスト。各要素は deviceId と label を持ちます。 type は現状 Android でのみ取得できます

使い方

final devices = await MediaDevices.enumerateAudioInputDevices();

for (final device in devices) {
  print('${device.deviceId}: ${device.label}');
}

iOS / iPadOS では音声入力デバイス一覧の取得に AVAudioSession.availableInputs を利用します。 列挙結果は現在の AVAudioSession の設定や音声ルートの状態に依存し、接続状態や OS の音声ルートによって変わることがあります。

deviceId の意味はプラットフォームごとに異なります。

  • Android: AudioDeviceInfo.id を文字列化したものです。接続し直すと変わることがあるため、長期保存して再利用する前提にはしないでください
  • iOS / iPadOS: AVAudioSessionPortDescription.uid です。 availableInputs に含まれる候補だけが列挙されます
  • macOS: CoreAudio の kAudioDevicePropertyDeviceUID です。入力ストリームを持つデバイスだけが列挙されます

label も OS が返す値に依存します。特に Android では AudioDeviceInfo.productName を使うため、機種や接続先によっては分かりやすい製品名にならないことがあります。

MediaDevices.enumerateAudioOutputDevices()

音声出力デバイス (スピーカー) の一覧を取得します。現時点の SDK では、この API で取得した deviceId を使って再生先を切り替えることはできません。

static Future<List<AudioOutputDevice>> enumerateAudioOutputDevices();
引数:

なし

戻り値:

AudioOutputDevice のリスト。各要素は deviceId と label を持ちます。 type は現状 Android でのみ取得できます

使い方

final devices = await MediaDevices.enumerateAudioOutputDevices();

for (final device in devices) {
  print('${device.deviceId}: ${device.label}');
}

列挙結果や deviceId / label の意味はプラットフォームごとに異なります。

  • Android: deviceId は AudioDeviceInfo.id を文字列化したものです。 label は AudioDeviceInfo.productName 、 type は AudioDeviceInfo.type です
  • iOS / iPadOS: AVAudioSession.currentRoute.outputs に含まれる現在の出力ポートだけが列挙されます。 deviceId は AVAudioSessionPortDescription.uid です
  • macOS: CoreAudio の kAudioDevicePropertyDeviceUID を deviceId として返します。出力ストリームを持つデバイスだけが列挙されます

再生先の扱いについては、この後の 音声出力デバイスの選択について を参照してください。

MediaDevices.enumerateVideoInputDevices()

映像入力デバイス (カメラ) の一覧を取得します。

static Future<List<VideoInputDevice>> enumerateVideoInputDevices();
引数:

なし

戻り値:

VideoInputDevice のリスト。各要素は deviceId と label を持ちます

使い方

final devices = await MediaDevices.enumerateVideoInputDevices();

for (final device in devices) {
  print('${device.deviceId}: ${device.label}');
}

列挙結果や deviceId / label の意味はプラットフォームごとに異なります。

  • Android: deviceId は Camera2 の camera ID です。 label は前面/背面で Front Camera / Back Camera 、それ以外は Camera {id} です。 LENS_FACING を取得できたカメラだけが列挙されます
  • iOS / iPadOS: deviceId は AVCaptureDevice.uniqueID 、 label は AVCaptureDevice.localizedName です。 builtInWideAngleCamera が列挙対象です
  • macOS: deviceId は AVCaptureDevice.uniqueID 、 label は AVCaptureDevice.localizedName です。内蔵カメラと外付けカメラが列挙されます

iOS / iPadOS では、主に前面カメラと背面の広角カメラが列挙されます。望遠カメラや超広角カメラは個別には列挙されません。

USB カメラなど 外部の映像入力 を選ぶときの経路の整理は 外部カメラパッケージを利用する を参照してください。

VideoInputDevice.supportedFormats()

カメラが対応している解像度・フレームレートの離散的な一覧を取得します。 VideoInputDevice のインスタンスメソッドです。

Future<List<VideoInputFormat>> supportedFormats();
引数:

なし

戻り値:

VideoInputFormat のリスト

各要素は次のフィールドを持ちます。

  • width (int): 幅
  • height (int): 高さ
  • maxFrameRate (int): その解像度で利用できる最大フレームレート

maxFrameRate の求め方はプラットフォームごとに異なります。

  • Android: CONTROL_AE_AVAILABLE_TARGET_FPS_RANGES の上限値の最大を返します。取得できない場合は 30 にフォールバックします
  • iOS / iPadOS: AVCaptureDeviceFormat.videoSupportedFrameRateRanges の最大値を返します
  • macOS: AVCaptureDeviceFormat.videoSupportedFrameRateRanges の最大値を返します

プラットフォーム側が 29.97 のような非整数を返す場合は、SDK が round() で整数に丸めます。

使い方

final devices = await MediaDevices.enumerateVideoInputDevices();
final formats = await devices.first.supportedFormats();

for (final format in formats) {
  print('${format.width}x${format.height} @${format.maxFrameRate}fps');
}

対応フォーマットは端末ごとに異なります。固定値を前提にせず、取得結果から選ぶようにしてください。

MediaDevices.getUserMedia()

送信用の MediaStream を生成します。マイク音声とカメラ映像をまとめて扱う、最も一般的な API です。

static Future<LocalMediaStream> getUserMedia(GetUserMediaOptions options);
引数:

GetUserMediaOptions (音声・映像の有無、使用デバイス、映像フォーマット)。

戻り値:

LocalMediaStream 。指定した audio / video に応じた track が入ります。

MediaDevices.getUserMedia() は接続ロールとは独立しています。 GetUserMediaOptions.audio / GetUserMediaOptions.video の指定に応じてローカルトラックを生成しますが、その時点では Sora へ送信しません。 SoraRole.sendonly または SoraRole.sendrecv で送信する場合にのみ、生成した LocalMediaStream を SoraConnection.connect() に渡します。 SoraRole.recvonly でもローカルトラックを生成できますが、 SoraConnection.connect() には渡しません。

GetUserMediaOptions のフィールド

フィールド

型

デフォルト

説明

audio

bool

true

音声トラックを含めるか

video

bool

true

映像トラックを含めるか

audioDeviceId

String?

null

音声入力に使うデバイス ID。 MediaDevices.enumerateAudioInputDevices() で列挙した deviceId を指定します。省略時は前回の明示選択を解除して、プラットフォーム標準の入力選択に戻ります

videoDeviceId

String?

null

映像入力に使うカメラデバイス ID。 MediaDevices.enumerateVideoInputDevices() で列挙した deviceId を指定します。省略時は SDK / プラットフォームが選んだ既定のカメラを使います

videoWidth

int?

null

映像入力の希望幅

videoHeight

int?

null

映像入力の希望高さ

videoFrameRate

int?

null

映像入力の希望フレームレート

audio と video の少なくとも片方は true にする必要があります。両方 false の場合は StateError が送出されます。 videoWidth / videoHeight / videoFrameRate を省略した場合のデフォルトは 640x480 @30fps です。これはブラウザの getUserMedia({ video: true }) と同じ挙動に合わせています。

使い方

final audioDevices = await MediaDevices.enumerateAudioInputDevices();
final videoDevices = await MediaDevices.enumerateVideoInputDevices();

final stream = await MediaDevices.getUserMedia(
  GetUserMediaOptions(
    audio: true,
    video: true,
    audioDeviceId: audioDevices.first.deviceId,
    videoDeviceId: videoDevices.first.deviceId,
    videoWidth: 1280,
    videoHeight: 720,
    videoFrameRate: 30,
  ),
);

videoWidth / videoHeight / videoFrameRate は希望値です。特定の解像度やフレームレートを使いたい場合は、 VideoInputDevice.supportedFormats() でカメラの対応フォーマットを確認してから指定してください。

サイマルキャストで 3 本の映像を送信したい場合は、送信元の解像度も重要です。送信元の解像度が不足していると、期待したレイヤー構成で送れない場合があります。サイマルキャスト利用時の考え方は Sora ドキュメントの サイマルキャスト機能 と サイマルキャスト機能の注意 をご確認ください。

音声入力デバイス指定のプラットフォーム別の挙動

  • Android では JavaAudioDeviceModule の preferred input device を利用します
    • Android の BUILTIN_MIC や TYPE_USB_DEVICE / TYPE_USB_HEADSET のように追加の通話用音声ルーティングを前提としない入力は、現時点では setPreferredInputDevice(...) による通常経路で扱います
    • Android の type ごとの個別検証は BLUETOOTH_SCO 以外はまだ限定的です。機種依存の音声ルーティングが必要な type は今後の対応対象です
  • iOS / iPadOS では AVAudioSession の入力切り替えを利用します
  • macOS では libwebrtc の AudioDeviceModule 経由で録音デバイスを切り替えます

音声デバイスの利用設定

接続前にメディア track を生成する場合は、 SoraConnectionConfig.useAudioDevice ではなく、 MediaDevices.setUseAudioDevice() を使用します。

MediaDevices.setUseAudioDevice(false);
final stream = MediaDevices.createMediaStream();

共有 PeerConnectionFactory の生成後に設定値を変更することはできません。 既に true または false で生成されている場合に異なる値を指定すると StateError が送出されます。 複数の接続やメディア API を同じプロセスで利用する場合は、音声デバイス設定を統一してください。

設定方法の使い分け

useAudioDevice は、共有 PeerConnectionFactory が実音声デバイスを利用するかどうかを指定する設定です。 設定方法によって有効範囲が変わるのではなく、共有 factory が生成される前にどこから設定するかが異なります。

MediaDevices を使わずに接続する場合は、 SoraConnectionConfig.useAudioDevice を指定します。 recvonly や messaging のように、 MediaDevices.createMediaStream() などを呼び出さずに接続する場合にも音声デバイス設定を指定できます。

final conn = await Sora.createConnection(
  SoraConnectionConfig(
    signalingUrls: urls,
    channelId: 'example-channel',
    role: SoraRole.recvonly,
    audio: true,
    useAudioDevice: false,
  ),
);
await conn.connect();

Sora.createConnection() より先に MediaDevices.createMediaStream() などを呼び出す場合は、 MediaDevices.setUseAudioDevice() を先に呼び出します。 この場合も、後から作成する SoraConnectionConfig.useAudioDevice と同じ値を指定してください。

MediaDevices.setUseAudioDevice(false);
final stream = MediaDevices.createMediaStream();

共有 factory が生成された後に異なる値を指定することはできません。 同じプロセス内で複数の接続やメディア API を利用する場合は、音声デバイス設定を統一してください。

音声出力デバイスの選択について

MediaDevices.enumerateAudioOutputDevices() は、現在 OS が認識している音声出力デバイスの一覧を取得する API です。 ただし、SDK の public API としては、列挙した deviceId を指定して再生先を統一的に切り替える API は現時点では提供していません。

音声の再生先は、基本的に各プラットフォームの標準ルーティングに従います。

  • Android では Android / libwebrtc の音声ルーティングに従います。Bluetooth / 受話スピーカー / スピーカーの切り替えは通話用音声ルートの制約を受けるため、出力先だけを常に厳密指定できるとは限りません
  • iOS / iPadOS では AVAudioSession の音声ルートに従います。AirPlay や Bluetooth などの再生先をユーザーに選ばせる場合は、アプリケーション側で AVRoutePickerView を利用してください
  • macOS では OS / CoreAudio の既定音声出力へ再生されます。SDK から再生先を明示選択する public API は未提供です

そのため、 enumerateAudioOutputDevices() は主に「現在利用可能な出力候補を把握する」用途の API として扱ってください。

音声デバイス利用例

Android / iOS / macOS では、音声入力デバイスの選択コード自体は共通です。 MediaDevices.enumerateAudioInputDevices() で候補を列挙し、使いたい入力デバイスの deviceId を GetUserMediaOptions.audioDeviceId に渡します。

final inputDevices = await MediaDevices.enumerateAudioInputDevices();
final selectedInputDevice = inputDevices.first;

final stream = await MediaDevices.getUserMedia(
  GetUserMediaOptions(
    audio: true,
    video: false,
    audioDeviceId: selectedInputDevice.deviceId,
  ),
);

プラットフォーム別の注意事項は次のとおりです。

  • Android では AudioDeviceInfo.id は接続状態に依存して変わり得るため、保存した ID の長期利用は前提にしないでください
  • iOS / iPadOS の音声出力切り替えは AVAudioSession の音声ルートに従います。スピーカー / Bluetooth / AirPlay などの再生先をユーザーに選ばせる場合は、アプリケーション側で AVRoutePickerView を利用してください
  • macOS の音声入力は libwebrtc の AudioDeviceModule 経由で切り替わります。現時点では音声出力デバイスの切り替え public API は未提供です。 MediaDevices.enumerateAudioOutputDevices() で一覧取得はできますが、再生先の明示選択はまだサポートしていません

MediaDevices.createMediaStream()

空の LocalMediaStream を生成します。通常のカメラ・マイク構成なら getUserMedia() で十分ですが、 createExternalVideoTrack() で作った外部映像トラックを自分で stream に追加したい場合はこの API を使います。 getUserMedia() は audio: false, video: false で呼べないため、 音声なしで外部映像のみを送る 構成では、まずこの API で空 stream を作る必要があります。

static LocalMediaStream createMediaStream();
引数:

なし

戻り値:

空の LocalMediaStream 。

使い方

// 音声なしの外部映像のみを送る stream
final stream = MediaDevices.createMediaStream();
stream.addTrack(MediaDevices.createExternalVideoTrack());

MediaDevices.createAudioTrack()

音声トラック ( LocalAudioTrack ) を 1 本生成します。マイク音声を送りたい場合に使います。 getUserMedia() は内部でこの API を利用して音声トラックを生成します。

static Future<LocalAudioTrack> createAudioTrack({
  String? audioDeviceId,
});
引数:

audioDeviceId ( String? ): 音声入力に使うデバイス ID。 MediaDevices.enumerateAudioInputDevices() で列挙した deviceId を指定します。省略時は前回の明示選択を解除して、プラットフォーム標準の入力選択に戻ります

戻り値:

LocalAudioTrack 。

使い方

外部映像入力とマイク音声を組み合わせて送る場合は、 createMediaStream() で作った空 stream に追加します。詳細は 外部映像入力を利用する をご確認ください。

final stream = MediaDevices.createMediaStream();
stream.addTrack(await MediaDevices.createAudioTrack());
stream.addTrack(MediaDevices.createExternalVideoTrack());

MediaDevices.createCameraVideoTrack()

SDK 管理のカメラ入力を利用する LocalVideoTrack を 1 本生成します。 getUserMedia() は内部でこの API を利用して映像トラックを生成します。

static LocalVideoTrack createCameraVideoTrack({
  String? videoDeviceId,
  int? videoWidth,
  int? videoHeight,
  int? videoFrameRate,
});
引数:

videoDeviceId ( String? ): 映像入力に使うカメラデバイス ID。 MediaDevices.enumerateVideoInputDevices() で列挙した deviceId を指定します。省略時は SDK / プラットフォームが選んだ既定のカメラを使います。 videoWidth / videoHeight / videoFrameRate ( int? ): 映像入力の希望幅、希望高さ、希望フレームレート

戻り値:

LocalVideoTrack 。

使い方

final device = (await MediaDevices.enumerateVideoInputDevices()).first;
final stream = MediaDevices.createMediaStream();
stream.addTrack(MediaDevices.createCameraVideoTrack(
  videoDeviceId: device.deviceId,
  videoWidth: 1280,
  videoHeight: 720,
  videoFrameRate: 30,
));

videoWidth / videoHeight / videoFrameRate を省略した場合の値はプラットフォームとデバイスに依存します。 getUserMedia() 経由の場合は 640x480 @30fps が使われます。詳細は MediaDevices.getUserMedia() をご確認ください。

MediaDevices.createExternalVideoTrack()

外部映像入力の LocalVideoTrack を 1 本生成します。カメラ以外の映像ソース (画面キャプチャ、動画ファイル、 camera パッケージからの取り込み、合成映像、テストパターンなど) を送りたい場合に使います。生成したトラックに writeFrame(ExternalVideoFrame) を呼び出すと、入力を検証した後、受け付けられたフレームがネイティブ映像ソースへ渡されます。 writeFrame() は同期的な void メソッドですが、Sora への配送、エンコード、ネットワーク送信の完了を示しません。

static LocalVideoTrack createExternalVideoTrack();
引数:

なし

戻り値:

LocalVideoTrack 。 writeFrame() で I420 フレームを投入できる外部映像入力用のトラックです。

特性

  • フレーム供給はアプリケーション側の責任: writeFrame() を呼ばなければフレームはネイティブ映像ソースへ渡りません。呼び出し間隔はアプリケーション側で制御しますが、実際の送信フレームレートは保証されません。例: Dart の Timer.periodic で 33ms (30 FPS 相当) 間隔に呼ぶ
  • 同期的なフレーム引き渡し: writeFrame() は Future を返しません。 AdaptFrame に受け付けられたフレームの Y / U / V plane は呼び出し中にネイティブ側の I420 バッファー へコピーされるため、戻り後に元の Dart 側のバッファーを再利用できます
  • フレームの適合: AdaptFrame が 0 を返した場合はフレームを破棄して正常に戻ります。受け付けられたフレームは、必要に応じて適合後の解像度へスケーリングされます
  • SDK 側のカメラキャプチャは動かない: LocalVideoTrack.textureId からのローカルプレビューは提供されません。ローカルで表示したい場合はアプリケーション側で Flutter の CustomPaint などにフレームを描画してください
  • I420 (YUV 4:2:0 planar) 固定: 他のフォーマットは事前にアプリケーション側で I420 に変換してください
  • カメラ入力との相互切り替え: SoraConnection.replaceVideoTrack() で差し替え可能です

使い方

単独生成 (音声なしの外部映像のみ送る構成。stream は createMediaStream() で作る):

final stream = MediaDevices.createMediaStream();
final track = MediaDevices.createExternalVideoTrack();
stream.addTrack(track);

await conn.connect(stream);

// 接続後、任意のタイミングで I420 フレームを流し込む
track.writeFrame(ExternalVideoFrame(
  width: 640,
  height: 480,
  yPlane: yBytes,
  uPlane: uBytes,
  vPlane: vBytes,
  yStride: 640,
  uStride: 320,
  vStride: 320,
));

マイク音声と組み合わせたいときは、 getUserMedia() で音声のみの stream を作り、外部映像トラックを追加します:

final stream = await MediaDevices.getUserMedia(
  GetUserMediaOptions(audio: true, video: false),
);
stream.addTrack(MediaDevices.createExternalVideoTrack());

ExternalVideoFrame のフィールド (plane / stride / rotation / timestampUs など)、バリデーション規則、 writeFrame() の振る舞い (adapt によるスキップや scale、エラー条件)、カメラと外部入力の切り替え手順、一定フレームレートで書き込むサンプルなどの詳細は 外部映像入力を利用する をご確認ください。

利用例

音声映像付きの標準構成

final audioDevices = await MediaDevices.enumerateAudioInputDevices();
final videoDevices = await MediaDevices.enumerateVideoInputDevices();
final stream = await MediaDevices.getUserMedia(
  GetUserMediaOptions(
    audio: true,
    video: true,
    audioDeviceId: audioDevices.first.deviceId,
    videoDeviceId: videoDevices.first.deviceId,
  ),
);

final conn = await Sora.createConnection(
  SoraConnectionConfig(
    signalingUrls: <String>['wss://example.com/signaling'],
    channelId: 'example-channel',
    role: SoraRole.sendrecv,
    audio: true,
    video: true,
  ),
);
await conn.connect(stream);

プラットフォームごとのデバイス権限

カメラやマイクを利用する際のプラットフォームごとの権限設定は以下をご確認ください。

リソース解放

接続を終了して、その LocalMediaStream と track をもう使わないタイミングで解放してください。 disconnect() や SoraConnection.dispose() を呼んでも、アプリケーション側で作成した LocalMediaStream / LocalAudioTrack / LocalVideoTrack は自動では解放されません。

解放するときは、先に track を dispose() してから、最後に LocalMediaStream を dispose() します。 LocalMediaStream.dispose() は中の LocalAudioTrack / LocalVideoTrack を自動では解放しません。

await conn.disconnect();

for (final track in stream.getAudioTracks()) {
  await track.dispose();
}
for (final track in stream.getVideoTracks()) {
  await track.dispose();
}
await stream.dispose();
await conn.dispose();