# 統計情報

## 概要

Sora Flutter SDK では、Sora との WebRTC 接続中にクライアントが送受信している音声・映像・データの状況（ビットレート、RTT、パケットロスなど）を統計情報として取得できます。

## 用語

### WebRTC 統計情報

WebRTC 統計情報とは [W3C Identifiers for WebRTC's Statistics API](https://www.w3.org/TR/webrtc-stats/) に定義されている標準仕様です。
`Sora Flutter SDK` が返す統計情報の `type` とメンバー名も、この仕様に基づいています。
ただし、SDK は `RTCStatsReport` を `Dart` のオブジェクトへ変換しているのではなく、`libwebrtc` の `RTCStatsReport::ToJson()` が生成した JSON 文字列を FFI で返しています。
そのため、JSON のルート構造、出力される `type` 、各メンバーの有無は、`libwebrtc` のバージョン、接続状態、メディア構成によって変わります。
ブラウザーの `RTCPeerConnection.getStats()` が返す `RTCStatsReport` の `Map` 構造を、そのまま前提にしないでください。

## 統計情報取得方法

統計情報の取得には `SoraConnection.getStats()` を利用します。

以下のコード例では、次の import を共通で使用します。

```dart
import 'dart:async';
import 'dart:convert';

import 'package:sora_sdk/sora_sdk.dart';
```

```dart
Future<String?> getInitialStats() async {
  final config = SoraConnectionConfig(
    signalingUrls: <String>['wss://sora.example.com/signaling'],
    channelId: 'example-channel',
    role: SoraRole.sendrecv,
    audio: true,
    video: true,
  );

  final stream = await MediaDevices.getUserMedia(
    GetUserMediaOptions(audio: true, video: true),
  );
  SoraConnection? conn;

  try {
    conn = await Sora.createConnection(config);
    await conn.connect(stream);

    // WebRTC 統計情報を JSON 文字列として取得する
    return await conn.getStats();
  } finally {
    await conn?.dispose();
    for (final track in stream.getTracks()) {
      await track.dispose();
    }
    await stream.dispose();
  }
}
```

### getStats()

**戻り値**: `Future<String?>`

接続中の `PeerConnection` から WebRTC 統計情報を取得し、ネイティブの `Stats` レポートを JSON 文字列に変換して返します。
通常、JSON のルートは `Stats` レポートを要素とする配列です。
レポートが存在しない場合は `null` が返り、レポートが存在しても統計情報が空の場合は `[]` が返ることがあります。
互換性のため、解析側では `null` と空文字列の両方を先に処理してください。

> **注釈**
>
> `getStats()` の実行中に再度呼び出しても `StateError` は送出されません。進行中の統計取得処理を共有し、各呼び出しが同じ取得結果を待ちます。複数のネイティブ取得処理が同時に実行されることもありません。

#### 同時呼び出し

同時に取得したい処理が複数ある場合は、各呼び出しをそのまま待つことができます。

```dart
Future<void> getStatsConcurrently(SoraConnection conn) async {
  final first = conn.getStats();
  final second = conn.getStats();

  final results = await Future.wait<String?>(<Future<String?>>[
    first,
    second,
  ]);
}
```

`results` には同じ統計取得処理の結果が 2 件入ります。定期取得では、前回の取得完了後に次の `getStats()` を呼ぶ構成にすると、取得結果を順番に処理できます。

#### 状態ごとの戻り値と例外

- `PeerConnection` がまだ生成されていない場合や、切断後に取得した場合は `null` が返ります
- 取得中に `disconnect()` が実行されると、進行中の `getStats()` は `StateError` で完了します
- `SoraConnection.dispose()` 後に `getStats()` を呼ぶと `StateError` が送出されます
- ネイティブ側のコールバックが到着しないまま孤立した `getStats()` が上限 (10 件) に達すると、 `StateError` が送出されます。コールバックが到着して孤立が解消されるまで新しい `getStats()` は失敗し、解消されない場合は接続を再生成する必要があります
- ネイティブ側の応答が 5 秒以内に届かない場合は `TimeoutException` が送出されます

### 戻り値の JSON 構造

戻り値の JSON 文字列を `dart:convert` の `jsonDecode()` でデコードすると、Stats レポートの `List<dynamic>` が得られます。
各要素は `Map<String, dynamic>` 相当の JSON オブジェクトです。
`libwebrtc` 内部ではレポートを `id` で検索できる Map として管理していますが、`ToJson()` の出力は Map の値を並べた配列です。
配列の順序は契約されていないため、`id` を配列の位置の代わりに使用してください。

各 `Stats` レポートは、レポートを識別する `id` 、レポートの種類を表す `type` 、取得時刻を表す `timestamp` を持ちます。
その他のメンバーはレポートの種類や実装によって異なり、該当しないメンバーは出力されません。

```dart
List<Map<String, dynamic>> decodeStats(String? raw) {
  if (raw == null || raw.isEmpty) {
    return const <Map<String, dynamic>>[];
  }
  final decoded = jsonDecode(raw);
  if (decoded is! List) {
    throw const FormatException('Stats JSON のルートが配列ではありません');
  }
  return <Map<String, dynamic>>[
    for (final value in decoded)
      if (value is Map) Map<String, dynamic>.from(value),
  ];
}

Future<void> printStats(SoraConnection conn) async {
  final reports = decodeStats(await conn.getStats());
  for (final report in reports) {
    print('${report['id']}: ${report['type']}');
  }
}
```

例えば、ネイティブの `ToJson()` は次のような配列を返します。

```json
[
  {
    "type": "peer-connection",
    "id": "P",
    "timestamp": 1787550022589501,
    "dataChannelsOpened": 0,
    "dataChannelsClosed": 0
  }
]
```

`timestamp` はネイティブの JSON シリアライザーが出力する値です。
ブラウザーの `RTCStatsReport` と同じ単位や数値型になるとは限らないため、時間差の計算では SDK と `libwebrtc` のバージョンを確認してください。

標準仕様に定義されている代表的な `type`:

| type | `W3C WebRTC Stats` 仕様上の説明 |
| --- | --- |
| `peer-connection` | `PeerConnection` 全体の統計。`dataChannelsOpened` / `dataChannelsClosed` などを参照できます |
| `transport` | 接続トランスポートの統計。`bytesSent` / `bytesReceived` 、`dtlsState` 、`iceState` などを参照できます |
| `certificate` | 接続に使用する証明書の統計。`fingerprint` や `fingerprintAlgorithm` などを参照できます |
| `codec` | 利用中のコーデック情報。`mimeType` や `payloadType` を参照できます |
| `inbound-rtp` | 受信中の RTP 統計。`bytesReceived` 、`packetsReceived` 、`packetsLost` 、`jitter` などを参照できます |
| `outbound-rtp` | 送信中の RTP 統計。`bytesSent` 、`packetsSent` 、`framesEncoded` 、`frameWidth` / `frameHeight` などを参照できます |
| `remote-inbound-rtp` | リモート側の受信 RTP 統計。`roundTripTime` などを参照できます |
| `remote-outbound-rtp` | リモート側の送信 RTP 統計。`remoteTimestamp` や `roundTripTime` などを参照できます |
| `candidate-pair` | ICE candidate pair の接続状態。`state` (`succeeded` / `failed` など) や `availableOutgoingBitrate` を参照できます |
| `local-candidate` / `remote-candidate` | ICE candidate の統計。`address` 、`port` 、`protocol` などを参照できます |
| `media-source` | 送信元のメディア統計。`kind` (`audio` / `video`) や `trackIdentifier` などを参照できます |
| `media-playout` | 音声の再生統計。`kind` や再生遅延に関するメンバーを参照できます |
| `data-channel` | `DataChannel` ごとの統計。`label` 、`state` 、`bytesSent` / `bytesReceived` などを参照できます |

`track` は現行の `W3C RTCStatsType` に定義されておらず、現行 `libwebrtc` の Stats 型にも対応するレポート型はありません。
メディアトラックに関する情報は、`inbound-rtp` や `media-source` の `trackIdentifier` などのメンバーとして出力されます。
古い `libwebrtc` や別の実装が未知の `type` を返す可能性はあるため、`track` を含め、一覧にない `type` をエラーとして扱わないでください。

各 `type` が持つメンバーの詳細は [W3C WebRTC Stats 仕様](https://www.w3.org/TR/webrtc-stats/) をご確認ください。
標準仕様に定義された `type` でも、メンバーは接続状態やメディアの種類によって省略されます。
また、`libwebrtc` のバージョンによって、標準仕様にないメンバーが含まれることがあります。

### 使用例

映像送信統計を取得する例:

```dart
Future<void> printVideoStats(SoraConnection conn) async {
  final raw = await conn.getStats();
  if (raw == null || raw.isEmpty) {
    return;
  }
  final reports = decodeStats(raw);

  // type が outbound-rtp かつ kind が video のレポートを収集する
  for (final report in reports) {
    if (report['type'] != 'outbound-rtp') continue;
    if (report['kind'] != 'video') continue;

    print('codec: ${report['mimeType']}');
    print('resolution: ${report['frameWidth']}x${report['frameHeight']}');
    print('bytesSent: ${report['bytesSent']}');
    print('packetsSent: ${report['packetsSent']}');
    print('framesEncoded: ${report['framesEncoded']}');
  }
}
```

> **注釈**
>
> Stats レポートは配列の各要素に入ります。
> 配列にない要素、未知の `type`、未知のメンバーを前提にせず、必要な `type` だけを選択してください。

コーデック情報を取得する例:

```dart
Future<void> printCodecStats(SoraConnection conn) async {
  final raw = await conn.getStats();
  if (raw == null || raw.isEmpty) {
    return;
  }
  final reports = decodeStats(raw);

  for (final report in reports) {
    if (report['type'] != 'codec') continue;

    print('mimeType: ${report['mimeType']}');
    print('payloadType: ${report['payloadType']}');
  }
}
```

### タイムアウト

`getStats()` は内部で 5 秒のタイムアウトを持ちます。ネイティブ側のコールバックが応答しない場合に `TimeoutException` が送出されます。

```dart
Future<void> getStatsWithTimeout(SoraConnection conn) async {
  try {
    final stats = await conn.getStats();
    print(stats);
  } on TimeoutException {
    print('Stats retrieval timed out');
  }
}
```
