メディアデバイスガイド

概要

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 のリスト。各要素は deviceIdlabel を持ちます。 type は現状 Android でのみ取得できます

使い方

final devices = await MediaDevices.enumerateAudioInputDevices();

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

iOS / iPadOS では音声入力デバイス一覧の取得に AVAudioSession.availableInputs を利用します。 列挙結果は現在の AVAudioSession の設定や route 状態に依存します。SDK は Bluetooth やヘッドセットの入力候補を取得しやすくするため、 列挙前に入力対応の音声 session を有効化しますが、接続状態や OS の route によって列挙結果が変わることがあります。

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 のリスト。各要素は deviceIdlabel を持ちます。 type は現状 Android でのみ取得できます

使い方

final devices = await MediaDevices.enumerateAudioOutputDevices();

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

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

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

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

MediaDevices.enumerateVideoInputDevices()

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

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

なし

戻り値:

VideoInputDevice のリスト。各要素は deviceIdlabel を持ちます

使い方

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: deviceIdAVCaptureDevice.uniqueIDlabelAVCaptureDevice.localizedName です。 builtInWideAngleCamera が列挙対象です
  • macOS: deviceIdAVCaptureDevice.uniqueIDlabelAVCaptureDevice.localizedName です。内蔵カメラと外付けカメラが列挙されます

iOS / iPadOS ではフロントとバックの広角カメラが主な列挙対象で、Pro 系 iPhone の望遠・超広角レンズは個別には出てきません。

USB カメラなど 外部の映像入力 を選ぶときの経路の整理は 外部カメラ利用ガイド を参照してください。

VideoInputDevice.supportedFormats()

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

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

なし

戻り値:

VideoInputFormat のリスト

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

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

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

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

使い方

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 が入ります。

GetUserMediaOptions のフィールド

フィールド

デフォルト

説明

audio

bool

true

音声 track を含めるか

video

bool

true

映像 track を含めるか

audioDeviceId

String?

null

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

videoDeviceId

String?

null

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

videoWidth

int?

null

映像入力の希望幅

videoHeight

int?

null

映像入力の希望高さ

videoFrameRate

int?

null

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

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

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

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

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

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

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

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

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

音声デバイス利用例

Android / iOS / macOS では、音声入力デバイスの選択コード自体は共通です。 MediaDevices.enumerateAudioInputDevices() で候補を列挙し、使いたい input device の deviceIdGetUserMediaOptions.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 の route に従います。スピーカー / Bluetooth / AirPlay などの再生先をユーザーに選ばせる場合は、アプリ側で AVRoutePickerView を利用してください
  • macOS の音声入力は libwebrtc の AudioDeviceModule 経由で切り替わります。現時点では音声出力デバイスの切り替え public API は未提供です。 MediaDevices.enumerateAudioOutputDevices() で一覧取得はできますが、再生先の明示選択はまだサポートしていません

MediaDevices.createMediaStream()

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

static LocalMediaStream createMediaStream();
引数:

なし

戻り値:

空の LocalMediaStream

使い方

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

MediaDevices.createExternalVideoTrack()

外部映像入力の LocalVideoTrack を 1 本生成します。カメラ以外の映像ソース (画面キャプチャ、動画ファイル、 camera パッケージからの取り込み、合成映像、テストパターンなど) を送りたい場合に使います。生成したトラックに writeFrame(ExternalVideoFrame) で I420 フレームを流し込むと、そのフレームが Sora へ送信されます。

static LocalVideoTrack createExternalVideoTrack();
引数:

なし

戻り値:

LocalVideoTrackcaptureTypeVideoTrackCaptureType.external になります。

特性

  • フレーム供給はアプリ側の責任: writeFrame() を呼ばなければ映像は送信されません。フレームレートはアプリ側で制御します。例: Dart の Timer.periodic で 33ms (30 FPS 相当) 間隔に呼ぶ
  • 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);

送信音声の有効 / 無効

接続中に setAudioEnabled() を使うと、自分が送る音声の有効 / 無効を切り替えられます。 また isAudioEnabled() で現在の状態を確認できます。

conn.setAudioEnabled(false);
print(conn.isAudioEnabled);

これは送信音声の制御です。受信音声の再生停止ではありません。受信音声の再生や再生制御は 音声再生ガイド をご確認ください。

送信映像の有効 / 無効

接続中に setVideoEnabled() を使うと、自分が送る映像の有効 / 無効を切り替えられます。 また isVideoEnabled で現在の状態を確認できます。

conn.setVideoEnabled(false);
print(conn.isVideoEnabled);

これは送信映像の制御です。受信映像の表示停止や、ローカルプレビュー用 Texture の表示制御ではありません。

ローカル映像のプレビュー

getUserMedia() で取得した LocalVideoTracktextureId を Flutter の Texture に渡すと、カメラ映像をローカルでレンダリングできます。接続前のカメラプレビューにも使えます。

final stream = await MediaDevices.getUserMedia(
  const GetUserMediaOptions(audio: true, video: true),
);
final videoTrack = stream.getVideoTracks().first;
final localTextureId = await videoTrack.textureId;
Widget buildLocalVideo(int textureId) {
  return Texture(textureId: textureId);
}

recvonly では LocalVideoTrack 自体が生成されないため、ローカルプレビューは使えません。

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

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

リソース解放

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

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

await conn.disconnect();

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