メディアデバイスガイド¶
概要¶
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 の設定や 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のリスト。各要素は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 ではフロントとバックの広角カメラが主な列挙対象で、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 のフィールド¶
フィールド |
型 |
デフォルト |
説明 |
|---|---|---|---|
|
|
|
音声 track を含めるか |
|
|
|
映像 track を含めるか |
|
|
|
音声入力に使うデバイス ID。 |
|
|
|
映像入力に使うカメラデバイス ID。 |
|
|
|
映像入力の希望幅 |
|
|
|
映像入力の希望高さ |
|
|
|
映像入力の希望フレームレート |
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 本の映像を送信したい場合は、送信元の解像度も重要です。送信元の解像度が不足していると、期待したレイヤー構成で送れない場合があります。サイマルキャスト利用時の考え方は simulcast と Sora ドキュメントのサイマルキャスト機能の注意 をご確認ください。
音声入力デバイス指定のプラットフォーム別の挙動¶
- Android では
JavaAudioDeviceModuleの preferred input device を利用します- Android の
BUILTIN_MICやTYPE_USB_DEVICE/TYPE_USB_HEADSETのように追加の communication routing を前提としない入力は、現時点ではsetPreferredInputDevice(...)による通常経路で扱います - Android の type ごとの個別検証は
BLUETOOTH_SCO以外はまだ限定的です。機種依存の routing が必要な type は今後の対応対象です
- Android の
- 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 の 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の 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();
- 引数:
なし
- 戻り値:
LocalVideoTrack。captureTypeはVideoTrackCaptureType.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() で取得した LocalVideoTrack の textureId を 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() してから、最後に MediaStream を dispose() します。 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();