# イベント

## 概要

Sora Flutter SDK のイベントは、主に `Stream<SoraConnectionEvent>` と `Stream<SoraDebugEvent>` で提供されます。

`Stream<SoraConnectionEvent>` では、接続状態、リモートトラックの追加・削除、シグナリング、DataChannel (通知・メッセージング・RPC 等)、タイムアウトを購読できます。

`Stream<SoraDebugEvent>` では、構造化されたログとタイムラインを購読できます。

> **重要**
>
> SoraConnection の生成方法については [実装ガイド](getting_started.html) を、接続オプションについては [シグナリング設定](signaling.html) をご確認ください。このページでは `conn = await Sora.createConnection(config)` で生成した `SoraConnection` インスタンス `conn` を用いて説明します。

> **注釈**
>
> DataChannel の種類の違い (シグナリング用とアプリケーション向けメッセージング) は [DataChannel 経由のシグナリング](datachannel_signaling.html) と [リアルタイムメッセージング機能](messaging.html) をご確認ください。

## 統合イベント Stream

`Stream<SoraConnectionEvent>` は統合イベント Stream です。イベント型ごとに `switch` で分岐して処理します。

```dart
conn.events.listen((event) {
  switch (event) {
    case SoraConnectionStateChangedEvent(:final state):
      print('state: $state');
    case SoraConnectionErrorEvent(:final code, :final message):
      print('error: ${code ?? 'unknown'} ${message ?? ''}');
    case SoraTrackEvent(:final track):
      print('track added: ${track.trackId}');
    case SoraRemoveTrackEvent(:final track):
      print('track removed: ${track.trackId}');
    case SoraNotifyEvent(:final message):
      print('notify: $message');
    case SoraPushEvent(:final message):
      print('push: $message');
    case SoraSwitchedEvent(:final message):
      print('switched: $message');
    case SoraSignalingMessageEvent(:final event):
      print('signaling: ${event.transportType} ${event.direction}');
    case SoraDataChannelOpenEvent(:final event):
      print('datachannel open: ${event.label}');
    case SoraDataChannelMessageEvent(:final message):
      print('message: ${message.label}');
    case SoraTimeoutEvent():
      print('timeout');
  }
});
```

### 接続状態の変更

`SoraConnectionStateChangedEvent` は接続状態が変わったときに通知されます。

```dart
conn.events.listen((event) {
  if (event case SoraConnectionStateChangedEvent(:final state)) {
    switch (state) {
      case SoraConnectingState():
        print('connecting');
      case SoraConnectedState():
        print('connected');
      case SoraDisconnectedState(:final closeInfo):
        print('disconnected');
        if (closeInfo != null) {
          print('close code: ${closeInfo.code}');
          print('close reason: ${closeInfo.reason}');
        }
    }
  }
});
```

通知される状態は次の 3 種類です。

- `SoraConnectingState`
- `SoraConnectedState`
- `SoraDisconnectedState`

#### SoraConnectingState

接続処理の開始後、シグナリング接続の確立に入ったタイミングで通知されます。

```dart
conn.events.listen((event) {
  if (event case SoraConnectionStateChangedEvent(state: SoraConnectingState())) {
    print('connecting');
  }
});
```

#### SoraConnectedState

Sora との PeerConnection が接続済になったタイミングで通知されます。

```dart
conn.events.listen((event) {
  if (event case SoraConnectionStateChangedEvent(state: SoraConnectedState())) {
    print('connected');
  }
});
```

#### SoraDisconnectedState

Sora から切断したタイミングで通知されます。

`closeInfo` を持つ場合は、切断時の code と reason を参照できます。

```dart
conn.events.listen((event) {
  if (event case SoraConnectionStateChangedEvent(
    state: SoraDisconnectedState(:final closeInfo),
  )) {
    print('disconnected');
    if (closeInfo != null) {
      print('close code: ${closeInfo.code}');
      print('close reason: ${closeInfo.reason}');
    }
  }
});
```

> **注釈**
>
> - `closeInfo` は常に入るとは限りません
> - 接続エラーが先に通知されたあとで、 `SoraDisconnectedState` が続く場合があります

### 接続エラー

接続エラーは `SoraConnectionErrorEvent` で通知されます。

```dart
conn.events.listen((event) {
  if (event case SoraConnectionErrorEvent(:final code, :final message)) {
    print('connection error: ${code ?? 'unknown'} ${message ?? ''}');
  }
});
```

`code` にはエラー種別が、 `message` にはエラーの詳細が含まれます。
`message` は null になる可能性があるため、利用の際は null チェックを行ってください。
`retriable` にはリトライ可能かどうかが `bool?` で入ります。
`details` には `SoraConnectionErrorDetails?` として `attempts` (リトライ試行回数) と `platformError` (プラットフォーム固有のエラー情報) が入る場合があります。

> **注釈**
>
> 接続エラーには `connect()` / `disconnect()` が例外を投げる経路と、 `SoraConnectionErrorEvent` で通知される経路の 2 つがあります。
> たとえば接続タイムアウト ( `connectionTimeout` ) のときは `connect()` が `TimeoutException` を投げたうえで、 `SoraConnectionErrorEvent` も通知されます。
> 最後のシグナリング候補 URL またはリダイレクト先のタイムアウト ( `signalingCandidateTimeout` ) のときは `connect()` が `StateError` を投げます。
> エラーハンドリングは両方の経路を想定してください。
> 詳細は [タイムアウト機能](timeout.html) をご確認ください。

`SoraConnectionErrorEvent.code` に入る既知のエラーコードは、公開 API の `SoraErrorCode` が定義する次の 18 個です。

- `SoraErrorCode.eventChannelError`: イベントチャネルのエラー- エラーコード: `event_channel_error`
- `SoraErrorCode.websocketError`: `WebSocket` のエラー- エラーコード: `websocket_error`
- `SoraErrorCode.connectionTimeout`: WebRTC 接続確立のタイムアウト ( [タイムアウト機能](timeout.html) )- エラーコード: `connection_timeout`
- `SoraErrorCode.disconnectTimeout`: 切断処理のタイムアウト ( [タイムアウト機能](timeout.html) )- エラーコード: `disconnect_timeout`
- `SoraErrorCode.signalingCandidateTimeout`: シグナリング候補 URL の接続タイムアウト ( [タイムアウト機能](timeout.html) )- エラーコード: `signaling_candidate_timeout`
- `SoraErrorCode.observerBridgeCreationFailed`: observer bridge の生成失敗- エラーコード: `observer_bridge_creation_failed`
- `SoraErrorCode.observerBridgeObserverCreationFailed`: `PeerConnectionObserver` の生成失敗- エラーコード: `observer_bridge_observer_creation_failed`
- `SoraErrorCode.audioInputInitializationFailed`: iOS の音声入力初期化失敗- エラーコード: `audio_input_initialization_failed`
- `SoraErrorCode.cameraOpenError`: カメラのオープン失敗- エラーコード: `camera_open_error`
- `SoraErrorCode.offerInvalid`: Sora から受け取った offer の SDP が `null` 、または `PeerConnection` を用意できない- エラーコード: `offer_invalid`
- `SoraErrorCode.reofferInvalid`: Sora から受け取った re-offer の SDP が `null` 、または再利用する `PeerConnection` がない- エラーコード: `reoffer_invalid`
- `SoraErrorCode.setRemoteDescriptionFailed`: リモート SDP の設定に失敗- エラーコード: `set_remote_description_failed`
- `SoraErrorCode.createAnswerFailed`: answer の生成に失敗- エラーコード: `create_answer_failed`
- `SoraErrorCode.setLocalDescriptionFailed`: ローカル SDP の設定に失敗- エラーコード: `set_local_description_failed`
- `SoraErrorCode.createPeerConnectionFailed`: `PeerConnection` の生成に失敗- エラーコード: `create_peer_connection_failed`
- `SoraErrorCode.addAudioTrackFailed`: ローカルの音声トラックの追加に失敗- エラーコード: `add_audio_track_failed`
- `SoraErrorCode.addVideoTrackFailed`: ローカルの映像トラックの追加に失敗- エラーコード: `add_video_track_failed`
- `SoraErrorCode.unexpectedNativeEvent`: ネイティブから想定外のイベントが届いた- エラーコード: `unexpected_native_event`

`SoraConnectionErrorEvent.code` の型は `String?` です。イベントによっては `null` になる可能性があります。
この一覧は SDK が定義する既知の定数一覧であり、ネイティブの接続状態やプラットフォーム側のイベントから渡される実行時の文字列が、この 18 個に限定されることを示すものではありません。

#### 切断理由コード

`SoraDisconnectReason` は、 `disconnect` メッセージの理由コードと、 SDK が切断を分類するためにネイティブから受け取る理由タグです。 `SoraConnectionErrorEvent.code` のエラーコード一覧には含まれません。

| 定数 | 値 |
| --- | --- |
| `SoraDisconnectReason.noError` | `NO-ERROR` |
| `SoraDisconnectReason.websocketOnClose` | `WEBSOCKET-ONCLOSE` |
| `SoraDisconnectReason.websocketOnError` | `WEBSOCKET-ONERROR` |
| `SoraDisconnectReason.serverDisconnect` | `server_disconnect` |
| `SoraDisconnectReason.peerConnectionFailed` | `peer_connection_failed` |
| `SoraDisconnectReason.peerConnectionClosed` | `peer_connection_closed` |

### track

リモートトラックが追加されたときは `SoraTrackEvent` が通知されます。

トラックは `RemoteMediaStreamTrack` として渡され、 `kind` で音声か映像かを判別できます。

- `kind == 'audio'`: 音声トラック (`textureId` は `null`)
- `kind == 'video'`: 映像トラック (`textureId` は non-null)

```dart
conn.events.listen((event) {
  if (event case SoraTrackEvent(:final track)) {
    if (track.kind == 'audio') {
      print('audio track added: ${track.trackId}');
    } else {
      print('video track added: ${track.trackId}');
      print('textureId: ${track.textureId}');
    }
  }
});
```

`RemoteMediaStreamTrack` のフィールドは以下の通りです。

- `trackId`: トラック ID
- `kind`: `audio` または `video`
- `connectionId`: Sora の `connection_id`
- `textureId`: `kind == 'video'` の場合のみ値を取得できます

映像トラックの追加イベントでは、 `textureId` をそのまま `Texture` に渡せます。

```dart
if (event case SoraTrackEvent(:final track) when track.kind == 'video') {
  return Texture(textureId: track.textureId!);
}
```

> **注釈**
>
> - audio は libwebrtc の track 通知を受けた時点でイベントが流れます
> - video は renderer の作成と sink attach が完了し、 `textureId` が確定してからイベントが流れます
> - video の追加イベントは、libwebrtc が track を検知した瞬間ではなく、レンダリングに使える状態になってから流れます
> - audio と video の厳密な相対順序は保証されません
> - `connectionId` はリモートトラックの `trackId` (`{connectionId}-audio` / `{connectionId}-video`) から復元されます

### removetrack

リモートトラックが削除されたときは `SoraRemoveTrackEvent` が通知されます。

```dart
conn.events.listen((event) {
  if (event case SoraRemoveTrackEvent(:final track)) {
    print('track removed: ${track.trackId}');
  }
});
```

削除イベントでも追加イベント時と同様に `RemoteMediaStreamTrack` 型となります。UI 側は通常 `trackId` で対象を特定して除去します。

接続単位でトラックを扱いたい場合は、 `SoraConnection.remoteMediaStreams` も利用できます。 `connectionId` をキーに `RemoteMediaStream` が入り、 `audioTrack` / `videoTrack` にそれぞれのトラックが格納されます。

> **注釈**
>
> - remote track の追加 / 削除を逐次扱いたい場合は `SoraTrackEvent` / `SoraRemoveTrackEvent` を利用します
> - 接続単位で現在の状態を参照したい場合は `SoraConnection.remoteMediaStreams` を利用します
> - ローカル映像は remote track event とは別で、 `LocalVideoTrack.textureId` や `SoraConnection.localVideo` から取得します

### notify

Sora からシグナリング通知を受け取ったときは `SoraNotifyEvent` が通知されます。

Sora のシグナリング通知の詳細は Sora のドキュメント [シグナリング通知](https://sora-doc.shiguredo.jp/SIGNALING_NOTIFY) をご確認ください。

```dart
conn.events.listen((event) {
  if (event case SoraNotifyEvent(:final message)) {
    print('notify: $message');
  }
});
```

`connection.created` などの通知をここで受け取ることができます。

> **注釈**
>
> シグナリング通知の詳細は [DataChannel 経由のシグナリング](datachannel_signaling.html) をご確認ください

### push

Sora からのプッシュ通知を受け取ったときは `SoraPushEvent` が通知されます。

```dart
conn.events.listen((event) {
  if (event case SoraPushEvent(:final message)) {
    print('push: $message');
  }
});
```

> **注釈**
>
> メッセージの設計はサーバー設定と運用に依存します

### switched

シグナリングが WebSocket から DataChannel に切り替わったときは `SoraSwitchedEvent` が通知されます。

```dart
conn.events.listen((event) {
  if (event case SoraSwitchedEvent(:final message)) {
    print('switched: $message');
  }
});
```

### timeout

PeerConnection の接続確立前に、WebSocket が reason `TIMEOUT` で閉じられた場合に `SoraTimeoutEvent` が通知されます。切断処理中は通知されません。

```dart
conn.events.listen((event) {
  if (event case SoraTimeoutEvent()) {
    print('timeout');
  }
});
```

> **注釈**
>
> - タイムアウト関連のエラーコードは `SoraConnectionErrorEvent` にも通知されます
> - 詳細は [タイムアウト機能](timeout.html) をご確認ください

### message

DataChannel を利用したリアルタイムメッセージング機能でメッセージを受信したときは `SoraDataChannelMessageEvent` が通知されます。

```dart
conn.events.listen((event) {
  if (event case SoraDataChannelMessageEvent(:final message)) {
    print('label: ${message.label}');
    print('data: ${message.data}');
  }
});
```

> **注釈**
>
> 詳細は [リアルタイムメッセージング機能](messaging.html) をご確認ください

### datachannel

DataChannel が利用可能になったときは `SoraDataChannelOpenEvent` が通知されます。

```dart
conn.events.listen((event) {
  if (event case SoraDataChannelOpenEvent(:final event)) {
    print('label: ${event.label}');
    print('direction: ${event.direction}');
  }
});
```

メッセージング機能を使う場合は、このイベントを送信可能の目安として扱えます。

> **注釈**
>
> 詳細は [リアルタイムメッセージング機能](messaging.html) と [DataChannel 経由のシグナリング](datachannel_signaling.html) をご確認ください

### signaling

シグナリングメッセージの送受信を確認したいときは `SoraSignalingMessageEvent` を利用します。

```dart
conn.events.listen((event) {
  if (event case SoraSignalingMessageEvent(:final event)) {
    print('${event.transportType} ${event.direction}');
    print(event.data);
  }
});
```

`transportType` は `websocket` または `datachannel` です。
`direction` は `sent` または `received` です。

`data` には、シグナリングメッセージの内容がそのまま入ります。 offer / answer / candidate などの内容を確認することができます。

## デバッグイベント Stream

`conn.debugEvents` はデバッグイベント専用 Stream です。構造化された `log` と `timeline` を受け取りたい場合に利用します。

```dart
conn.debugEvents.listen((event) {
  switch (event) {
    case SoraLogDebugEvent(:final event):
      print('${event.title}: ${event.message}');
    case SoraTimelineDebugEvent(:final event):
      print('${event.logType.value} ${event.type}');
      print(event.data);
  }
});
```

### log

SDK の構造化ログは `SoraLogDebugEvent` で受け取ることができます。

```dart
conn.debugEvents.listen((event) {
  if (event case SoraLogDebugEvent(:final event)) {
    print('${event.title}: ${event.message}');
  }
});
```

> **注釈**
>
> このイベントはデバッグ時のトレースログ出力用途を想定しています

### timeline

タイムラインの構造化ログは `SoraTimelineDebugEvent` で受け取ることができます。

```dart
conn.debugEvents.listen((event) {
  if (event case SoraTimelineDebugEvent(:final event)) {
    print('${event.logType.value} ${event.type}');
    print(event.data);
  }
});
```

このイベントに含まれる `SoraTimelineEvent` では、主に次の情報を参照します。

- `type`: イベント種別
- `logType`: `websocket` / `datachannel` / `peerconnection` / `sora`
- `data`: イベントの詳細データ
- `dataChannelId`: DataChannel に紐づく場合の ID
- `dataChannelLabel`: DataChannel に紐づく場合の label

> **注釈**
>
> このイベントはデバッグ時の時系列調査用途を想定しています

## 接続確立後の識別子

offer を受信すると、次の getter がサーバーから割り当てられた値で更新されます。

- `connectionId`
- `serverClientId`
- `bundleId`
- `sessionId`

接続前や offer 未到達のときは `null` です。RPC のパラメータ組み立てなどで利用できます。

## イベント購読の終了

`conn.events` や `conn.debugEvents` を購読した場合は、 `conn.dispose()` の前に購読を解除してください。

```dart
late final StreamSubscription<SoraConnectionEvent> eventSubscription;
late final StreamSubscription<SoraDebugEvent> debugEventSubscription;

void setup(SoraConnection conn) {
  eventSubscription = conn.events.listen((event) {
    // イベント処理
  });
  debugEventSubscription = conn.debugEvents.listen((event) {
    // debug event 処理
  });
}

Future<void> dispose(SoraConnection conn) async {
  await eventSubscription.cancel();
  await debugEventSubscription.cancel();
  await conn.dispose();
}
```
