カメラを利用して映像を送信する

概要

Sora Flutter SDK のデフォルトの映像入力は、 libwebrtc のカメラキャプチャを利用します。 MediaDevices.getUserMedia() で LocalMediaStream と LocalVideoTrack を生成し、 SoraConnection.connect() に LocalMediaStream を渡すと、SDK が接続時にカメラキャプチャを開始します。

接続前に LocalVideoTrack.textureId を取得してプレビューを表示する場合も、SDK がプラットフォーム側のカメラキャプチャを開始します。 disconnect() や SoraConnection.dispose() は接続を終了しますが、アプリケーション側で作成したトラックやストリームは破棄しません。 利用終了後は接続を切断してから LocalVideoTrack.dispose() を呼び出し、最後に LocalMediaStream.dispose() を呼び出してください。

本ページでは、 getUserMedia() を使った標準的なカメラ映像送信の設定と操作について説明します。 映像の描画については 映像を描画する をご確認ください。 また、外部映像入力 ( pub.dev の camera パッケージや createExternalVideoTrack ) については 外部カメラパッケージを利用する および 外部映像入力を利用する をご確認ください。

基本の流れ

  1. MediaDevices.getUserMedia() で LocalMediaStream を生成する
  2. 必要に応じて接続前に LocalVideoTrack.textureId を取得してプレビューを表示する
  3. SoraConnection.connect(stream) で接続する
  4. 利用終了時に disconnect() を呼び出し、track を破棄してから LocalMediaStream を破棄する

次の例は、カメラ映像の送信を開始するための最小構成の完全な処理フローです。 MediaStream の生成から接続開始までをまとめて示しています。

import 'package:sora_sdk/sora_sdk.dart';

Future<void> startVideo() async {
  // getUserMedia でカメラとマイクの MediaStream を生成する
  // ここでは音声は関係ないため false としている
  final stream = await MediaDevices.getUserMedia(
    const GetUserMediaOptions(audio: false, video: true),
  );

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

  // 接続すると SDK が自動でカメラキャプチャを開始する
  await conn.connect(stream);
}

カメラデバイスを選択する

GetUserMediaOptions.videoDeviceId に MediaDevices.enumerateVideoInputDevices() で取得したデバイス ID を指定することで、使用するカメラデバイスを選択できます。 省略時は SDK が既定のカメラ(多くの端末では前面カメラ)を使用します。

// 利用可能なカメラの一覧を取得する
final cameras = await MediaDevices.enumerateVideoInputDevices();

// Android で背面カメラを選択する例
final backCamera = cameras.firstWhere(
  (c) => c.label.contains('Back'),
);

final stream = await MediaDevices.getUserMedia(
  GetUserMediaOptions(
    audio: true,
    video: true,
    videoDeviceId: backCamera.deviceId,
  ),
);

解像度とフレームレートを指定する

GetUserMediaOptions の videoWidth 、videoHeight 、videoFrameRate で希望する解像度とフレームレートを指定できます。 省略時のデフォルトは 640x480 @30fps です。

指定した値はあくまで希望であり、デバイスが対応している範囲で最も近い値が選択されます。事前に VideoInputDevice.supportedFormats() でカメラの対応フォーマットを確認することを推奨します。

// カメラの対応フォーマットを確認する
final formats = await backCamera.supportedFormats();

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

カメラを切り替える

接続中に replaceVideoTrack() により、前面カメラと背面カメラを切り替えることができます。

import 'package:sora_sdk/sora_sdk.dart';

Future<void> switchCamera(
  SoraConnection conn,
  LocalMediaStream stream,
  String videoDeviceId,
) async {
  final currentTracks = stream.getVideoTracks();
  if (currentTracks.isEmpty) {
    throw StateError('差し替え元の映像トラックがありません');
  }
  final oldTrack = currentTracks.first;

  // 切り替え先のカメラで一時的な MediaStream を生成する。
  final newStream = await MediaDevices.getUserMedia(
    GetUserMediaOptions(
      audio: false,
      video: true,
      videoDeviceId: videoDeviceId,
    ),
  );

  try {
    final newTracks = newStream.getVideoTracks();
    if (newTracks.isEmpty) {
      throw StateError('差し替え先の映像トラックがありません');
    }
    final newTrack = newTracks.first;

    try {
      // 成功すると newTrack は接続中の stream に追加されます。
      await conn.replaceVideoTrack(stream, newTrack);
    } catch (error, stackTrace) {
      // 失敗時は newTrack が接続中の stream の所有になっていないため、
      // 呼び出し側で明示的に破棄します。
      try {
        await newTrack.dispose();
      } catch (_) {
        // 元の差し替えエラーを保持します。
      }
      Error.throwWithStackTrace(error, stackTrace);
    }

    // 成功時は newTrack が接続中の stream に移っているため破棄しません。
    // replaceVideoTrack() は oldTrack を自動で破棄しないため、ここで破棄します。
    await oldTrack.dispose();
  } finally {
    // LocalMediaStream.dispose() は含まれる track を破棄しません。
    // 成功時も失敗時も一時 stream 自身だけを解放します。
    await newStream.dispose();
  }
}

送信映像のソフトミュート

接続中に setVideoEnabled(false) を呼ぶことにより送信映像トラックは無効となり、 入力デバイスからの映像は配信されなくなります(ソフトミュート)。 このとき実際にはパケットを送っており、黒塗りフレームを送り続ける状態となります。

解除するには setVideoEnabled(true) を呼びます。

// 送信映像を無効化する
conn.setVideoEnabled(false);

// 送信映像を再開する
conn.setVideoEnabled(true);

// 現在の状態を確認する
print(conn.isVideoEnabled);

ローカルプレビューを表示する

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

接続前に textureId を取得すると、Flutter の Texture を表示するためのプラットフォーム側のテクスチャが確保され、カメラキャプチャも開始されます。 この処理は Sora への接続とは独立しているため、プレビューだけを表示する場合も、利用終了時に映像トラックを明示的に破棄してください。

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

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

カメラキャプチャを停止してリソースを解放する

disconnect() は Sora への送信と接続を終了しますが、アプリケーション側で作成した LocalVideoTrack の dispose() は呼び出しません。 LocalVideoTrack.dispose() は接続中には呼び出せないため、先に接続を切断してください。 LocalMediaStream.dispose() も含まれるトラックを破棄しないため、トラックを先に破棄してから、ストリームを破棄します。

await conn.disconnect();
await videoTrack.dispose();
await stream.dispose();
await conn.dispose();

プラットフォーム別の注意

Android

  • カメラを利用する場合は AndroidManifest.xml に CAMERA 権限の宣言と、ランタイム権限の取得が必要です
  • エミュレーターではカメラデバイスが利用できないため、映像送信の動作確認には実機が必要です
  • 詳細は Android での利用方法 をご確認ください

iOS / iPadOS

  • カメラを利用する場合は Info.plist に NSCameraUsageDescription の設定と、ランタイム権限の取得が必要です
  • シミュレーターではカメラデバイスが利用できません
  • 詳細は iOS / iPadOS での利用方法 をご確認ください

macOS

  • カメラを利用する場合は Info.plist に NSCameraUsageDescription を設定し、Entitlements に com.apple.security.device.camera を追加する必要があります
  • 詳細は macOS での利用方法 をご確認ください

関連ページ