# タイムアウト機能

## 概要

Sora Flutter SDK は WebRTC 接続のライフサイクル各段階でタイムアウト機能を提供します。これにより、無限待機を防ぎ、より堅牢なアプリケーションを構築できます。

タイムアウトを設定するには `SoraConnectionConfig` の `timeoutOptions` に `SoraTimeoutOptions` を指定します。
`timeoutOptions` は省略できますが、指定する値は `SoraTimeoutOptions` です。

## 設定可能なタイムアウト


### connectionTimeout - 接続タイムアウト

`WebRTC` 接続確立までの制限時間です。
`WebSocket` 接続の開始からピア接続の確立完了までにこの時間を超えた場合は、候補 URL 単位ではなく接続処理全体が中止されます。

**デフォルト値**: 30 秒


### disconnectWaitTimeout - 切断待機タイムアウト

切断処理待機中の制限時間です。 `disconnect()` メソッド実行時にリソース解放が完了するまでの制限時間で、この時間を超過した場合は強制的にリソースが解放されます。

**デフォルト値**: 10 秒


### signalingCandidateTimeout - シグナリング候補タイムアウト

シグナリング候補 URL ごとの `WebSocket` 接続確立までの制限時間です。
候補 URL への接続がこの時間内に完了しない場合は、その候補を終了して次の URL を試します。
最後の候補 URL がタイムアウトした場合は、接続処理全体がエラーになります。
シグナリング URL のリダイレクト先への再接続にもこの設定が適用されますが、リダイレクト先がタイムアウトした場合は別の候補 URL を試しません。

複数の候補 URL を指定した場合、候補 URL の切り替えは SDK が自動で行います。すべての候補 URL がタイムアウトした場合だけ、利用者側で接続全体を再試行してください。

**デフォルト値**: 5 秒

## 対象外のタイムアウト

次のタイムアウトは `SoraTimeoutOptions` では変更できません。

- `rpc()`: `SoraRpcOptions.timeout` (ミリ秒) で呼び出しごとに指定します。未指定の場合はタイムアウトしません。詳細は [RPC 機能](rpc.html) をご確認ください
- `replaceVideoTrack()`: 映像トラックの差し替え処理に 10 秒の固定タイムアウトが適用されます
- `getStats()`: 統計情報の取得に 5 秒の固定タイムアウトが適用されます。詳細は [統計情報](stats.html) をご確認ください

## 使用方法

### デフォルトのタイムアウト値を使用する

このとき SoraTimeoutOptions を設定する必要はありません。

```dart
final config = SoraConnectionConfig(
  signalingUrls: ['wss://example.com/signaling'],
  channelId: 'channel-id',
  role: SoraRole.sendrecv,
);
```

### カスタムタイムアウト値を設定する

```dart
final config = SoraConnectionConfig(
  signalingUrls: ['wss://example.com/signaling'],
  channelId: 'channel-id',
  role: SoraRole.sendrecv,
  timeoutOptions: SoraTimeoutOptions(
    connectionTimeout: Duration(seconds: 60),
    disconnectWaitTimeout: Duration(seconds: 15),
    signalingCandidateTimeout: Duration(seconds: 10),
  ),
);
```

## エラーハンドリング

接続処理または切断処理を失敗させるタイムアウトが発生した場合は、 `connect()` / `disconnect()` が例外を投げたうえで、 `conn.events` の `SoraConnectionErrorEvent` でも通知されます。

- `connectionTimeout` 超過: `connect()` が `TimeoutException` を投げます
- `disconnectWaitTimeout` 超過: `disconnect()` が `TimeoutException` を投げます
- 最後の候補 URL またはリダイレクト先での `signalingCandidateTimeout` 超過: `connect()` が `StateError` を投げます

例外と `SoraConnectionErrorEvent` は同じタイムアウトに対して両方通知されます。接続処理の失敗の分岐は `connect()` / `disconnect()` の例外で行い、 `SoraConnectionErrorEvent` は接続状態の表示やログ出力に利用するのが基本です。

最後以外の候補 URL が `signalingCandidateTimeout` を超えても、後続の候補 URL への接続に成功した場合は例外を投げず、 `SoraConnectionErrorEvent` も通知しません。

リダイレクト先の `signalingCandidateTimeout` 超過では、別の候補 URL への切り替えは行われません。 `connect()` の完了前であれば `connect()` が `StateError` を投げ、接続完了後であれば `SoraConnectionErrorEvent` の通知だけが行われます。

タイムアウトの対象外のエラーもあります。次のエラーは例外と `SoraConnectionErrorEvent` のどちらか一方でのみ通知されます。

- `rpc()` のタイムアウト: `TimeoutException` が `rpc()` の呼び出し元へ返るのみです。 `SoraConnectionErrorEvent` は通知されません
- 接続確立後に発生するエラー (例: `websocket_error` / `camera_open_error`): `connect()` の例外ではなく `SoraConnectionErrorEvent` でのみ通知されます

`SoraTimeoutEvent` は WebSocket の reason が `TIMEOUT` の場合に通知されます。ただし、切断処理中や PeerConnection の接続確立後は通知されません。タイムアウトの検知は `SoraConnectionErrorEvent` と `connect()` / `disconnect()` の例外で行うのが基本です。

### タイムアウトエラーコード

- `connection_timeout` - 接続タイムアウト発生
- `disconnect_timeout` - 切断タイムアウト発生
- `signaling_candidate_timeout` - シグナリング候補タイムアウト発生

### サンプルコード

```dart
final conn = await Sora.createConnection(config);

conn.events.listen((event) {
  if (event case SoraConnectionErrorEvent(:final code, :final message)) {
    switch (code) {
      case SoraErrorCode.connectionTimeout:
        print('接続タイムアウト: ${message ?? ''}');
      case SoraErrorCode.disconnectTimeout:
        print('切断タイムアウト: ${message ?? ''}');
      case SoraErrorCode.signalingCandidateTimeout:
        print('シグナリング接続タイムアウト: ${message ?? ''}');
    }
  }
});
```

## リトライロジック

タイムアウトが発生した場合のリトライ処理例です。複数の候補 URL は SDK が自動で試行するため、この例は全候補 URL の接続に失敗した後の接続全体の再試行だけを扱います。

`conn.events` は `connect()` より前に購読してください。全候補 URL の `signalingCandidateTimeout` では、 `connect()` が `StateError` を投げるため、 `SoraConnectionErrorEvent.code` を確認して再試行対象を判定します。その他の `StateError` は再試行しません。

```dart
import 'dart:async';

import 'package:sora_sdk/sora_sdk.dart';

Future<SoraConnection> connectWithRetry(
  SoraConnectionConfig config,
  int maxRetries,
) async {
  int retries = 0;
  while (true) {
    final conn = await Sora.createConnection(config);
    var candidateTimeout = false;
    var connected = false;
    final subscription = conn.events.listen((event) {
      if (event is SoraConnectionErrorEvent &&
          event.code == SoraErrorCode.signalingCandidateTimeout) {
        candidateTimeout = true;
      }
    });

    try {
      await conn.connect();
      connected = true;
      return conn;
    } on TimeoutException catch (error, stackTrace) {
      if (retries >= maxRetries) {
        Error.throwWithStackTrace(error, stackTrace);
      }
      retries++;
      print('接続タイムアウト (リトライ $retries/$maxRetries)');
    } on StateError catch (error, stackTrace) {
      // StreamController は非同期でイベントを配信するため、
      // connect() の例外を受け取った後にイベントを処理する時間を確保します。
      await Future<void>.delayed(Duration.zero);
      if (!candidateTimeout) {
        Error.throwWithStackTrace(error, stackTrace);
      }
      if (retries >= maxRetries) {
        Error.throwWithStackTrace(error, stackTrace);
      }
      retries++;
      print('シグナリング候補タイムアウト (リトライ $retries/$maxRetries)');
    } finally {
      await subscription.cancel();
      if (!connected) {
        await conn.dispose();
      }
    }
    await Future<void>.delayed(const Duration(seconds: 2));
  }
}
```

## 推奨タイムアウト値

| 設定項目 | 推奨値 | 備考 |
| --- | --- | --- |
| `connectionTimeout` | 30-60 秒 | ネットワーク遅延を考慮 |
| `disconnectWaitTimeout` | 10-15 秒 | リソース解放時間を確保 |
| `signalingCandidateTimeout` | 5-10 秒 | URL 試行時間を確保 |

タイムアウト値は環境やネットワーク状況に応じて調整してください。
