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

## 概要

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

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

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

## 基本の流れ

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

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

```dart
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);
}
```

> **注釈**
>
> 送信側のロール (`SoraRole.sendonly` / `SoraRole.sendrecv`) で映像を送信する場合は、 `video: true` を指定するか、 `video` を未指定にします。
> `video` を未指定 (`null`) にすると `connect` メッセージに `video` キーを含めず、 Sora のデフォルト (`true`) が使われます。
> `video: false` を指定すると映像は送信されません。音声も同様に `audio` で指定します。
> `recvonly` ではカメラは使用されません。

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

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

```dart
// 利用可能なカメラの一覧を取得する
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,
  ),
);
```

> **注釈**
>
> この例は `Android` の `label` が `Back Camera` であることを前提にしています。
> `iOS` / `macOS` では `AVCaptureDevice.localizedName` が返るため、 `label` に `Back` が含まれるとは限りません。
> `iOS` / `macOS` で背面カメラを選ぶ場合は、プラットフォームのカメラ情報に応じて選択してください。

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

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

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

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

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

> **注釈**
>
> サイマルキャストで複数レイヤーの映像を送信する場合は、送信元の解像度が不足していると期待したレイヤー構成で送れないことがあります。
> 詳細は Sora ドキュメントの [サイマルキャスト機能の注意](https://sora-doc.shiguredo.jp/SIMULCAST#注意) をご確認ください。

## カメラを切り替える

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

```dart
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)` を呼びます。

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

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

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

> **注釈**
>
> これは送信映像トラックの `Enabled` 制御です。受信映像の表示停止や、ローカルプレビュー用 `Texture` の表示制御ではありません。
> また、ソフトミュートでは映像入力デバイスは無効化されないため、端末のプライバシーインジケーターは消灯しません。

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

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

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

```dart
final stream = await MediaDevices.getUserMedia(
  const GetUserMediaOptions(audio: false, video: true),
);
final videoTrack = stream.getVideoTracks().first;
final localTextureId = await videoTrack.textureId;
```

```dart
Widget buildLocalVideo(int textureId) {
  return Texture(textureId: textureId);
}
```

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

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

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

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

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

### Android

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

### iOS / iPadOS

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

### macOS

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

## 関連ページ

- [メディアデバイス](media_device.html)
- [映像を描画する](video_rendering.html)
- [実装ガイド](getting_started.html)
- [外部カメラパッケージを利用する](external_camera_package.html)
- [外部映像入力を利用する](external_video_source.html)
