# 映像を描画する

## 概要

Sora Flutter SDK での映像レンダリングの方法について説明します。

描画までの流れは次のとおりです。

1. 映像の `textureId` を取得する- ローカル映像は `LocalVideoTrack.textureId` から `textureId` を取得する
   - リモート映像は `SoraTrackEvent` で受け取った `RemoteMediaStreamTrack.textureId` を使う
2. 取得した `textureId` を Flutter の `Texture` に渡して描画する

SDK は `Texture` を直接使う方法に加えて、 `SoraLocalVideoWidget` / `SoraRemoteVideoWidget` を提供しています。
これらは `textureId` が未確定の間のプレースホルダー表示、親領域へのフィット、左右反転をまとめて扱えます。

イベント全体の一覧や `SoraConnection.events` の使い方は [イベント](event.html) をご確認ください。

## TextureId を取得する

### ローカル映像

ローカル映像を表示するには、まず送信用の `MediaStream` を用意します。 `MediaStream` の作成方法は [メディアデバイス](media_device.html) をご確認ください。

ローカル映像の `textureId` は `LocalVideoTrack.textureId` ( `Future<int>` ) で取得します。

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

`createExternalVideoTrack()` で生成した外部映像入力用の `LocalVideoTrack` で `textureId` を取得すると `StateError` が送出されます。詳細は [外部映像入力を利用する](external_video_source.html) をご確認ください。

`SoraConnection.localVideo` ( `Stream<SoraLocalVideoHandle>` ) からも `textureId` を取得できます。
カメラのトラックが接続に設定され、キャプチャが開始されたタイミングで `SoraLocalVideoHandle` が emit されます。 `textureId` は常に有効な非負値です。

```dart
conn.localVideo.listen((handle) {
  setState(() {
    localTextureId = handle.textureId;
  });
});
```

> **注釈**
>
> - `createExternalVideoTrack()` で生成した外部映像トラックを使う接続では emit されません
> - `replaceVideoTrack()` で外部映像トラックに切り替えた場合や `disconnect()` の場合は、解除の通知が emit されません。 `SoraConnectionStateChangedEvent` などの状態遷移に合わせて、保持している `textureId` を破棄してください

### リモート映像

受信したリモート映像から `textureId` を取得する方法を説明します。
リモート映像を受信するまでの基本的な流れは [実装ガイド](getting_started.html) をご確認ください。また、 `conn.events` によりイベント購読しているものとします。

リモート映像は `conn.events` の `SoraTrackEvent` で受け取ります。トラックは `RemoteMediaStreamTrack` として通知され、 `kind == 'video'` の場合のみ `textureId` が non-null になります。

```dart
final remoteTextureIds = <String, int>{};

conn.events.listen((event) {
  switch (event) {
    case SoraTrackEvent(:final track):
      final textureId = track.textureId;
      if (track.kind == 'video' && textureId != null) {
        setState(() {
          remoteTextureIds[track.trackId] = textureId;
        });
      }
    default:
      break;
  }
});
```

`trackId` は、複数リモートトラックを識別する際に利用できます。

`RemoteMediaStreamTrack` の型や `SoraTrackEvent` の通知タイミングの詳細は [イベント](event.html) を参照してください。

## Texture によるレンダリング

`textureId` を Flutter の `Texture` ウィジェットに渡してレンダリングします。リモートトラックの `textureId` は `int?` なので、 `kind == 'video'` を確認してから利用します。

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

final localVideo = buildVideo(textureId);
final remoteVideo = buildVideo(track.textureId!);
```

複数のリモート映像を並べて表示する場合は、受信したトラックのリストを `GridView` や `Wrap` などに流し込みます。

```dart
Widget buildRemoteVideos(Map<String, int> remoteTextureIds) {
  return GridView.count(
    crossAxisCount: 2,
    children: remoteTextureIds.values
        .map((textureId) => Texture(textureId: textureId))
        .toList(),
  );
}
```


### 削除されたリモートトラックの映像を取り除く

リモート側の切断等により `SoraRemoveTrackEvent` が通知された映像トラックの `textureId` は描画に使えなくなるため、対応する `Texture` をツリーから取り除きます。

```dart
// trackId をキーに textureId を管理するマップです
final remoteTextureIds = <String, int>{};

conn.events.listen((event) {
  switch (event) {
    case SoraTrackEvent(:final track):
      final textureId = track.textureId;
      if (track.kind == 'video' && textureId != null) {
        setState(() {
          remoteTextureIds[track.trackId] = textureId;
        });
      }
    // SoraRemoveTrackEvent で該当する trackId をリストから削除します
    case SoraRemoveTrackEvent(:final track):
      if (track.kind == 'video') {
        setState(() {
          // trackId をキーに管理すると、SoraRemoveTrackEvent を受けたタイミングで該当する Texture を自然に表示から外せます
          remoteTextureIds.remove(track.trackId);
        });
      }
    default:
      break;
  }
});
```

## Widget によるレンダリング

`SoraLocalVideoWidget` と `SoraRemoteVideoWidget` を使うと、 `textureId` の管理と `Texture` の組み立てを Widget に任せられます。

### SoraRemoteVideoWidget

`SoraTrackEvent` で受け取った `RemoteMediaStreamTrack` をそのまま渡します。 `track.textureId` が `null` の間は `placeholder` が表示されます。

```dart
Widget buildRemoteVideo(RemoteMediaStreamTrack track) {
  return SoraRemoteVideoWidget(track: track);
}
```

| フィールド | 説明 |
| --- | --- |
| `track` | 表示するリモート映像トラック ( `kind == 'video'` )。 `kind == 'audio'` を渡した場合は debug 時に assert で検出されます |
| `fit` | 親領域へのフィット方法。デフォルトは `BoxFit.contain` |
| `mirror` | 水平反転するかどうか。デフォルトは `false` |
| `placeholder` | `track.textureId` が `null` の間に表示する Widget。デフォルトは黒色の塗りつぶし |

### SoraLocalVideoWidget

`textureId` を渡します。 `null` または負値のときは `placeholder` が表示されます。 `0` は有効な値として扱われます。

```dart
Widget buildLocalVideo(int? textureId) {
  return SoraLocalVideoWidget(textureId: textureId, mirror: true);
}
```

`fit` / `mirror` / `placeholder` は `SoraRemoteVideoWidget` と同じです。

## 関連ページ

- [イベント](event.html)
- [実装ガイド](getting_started.html)
- [メディアデバイス](media_device.html)
