統計情報

概要

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

用語

WebRTC 統計情報

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

統計情報取得方法

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

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

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

import 'package:sora_sdk/sora_sdk.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 と空文字列の両方を先に処理してください。

同時呼び出し

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

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 を持ちます。 その他のメンバーはレポートの種類や実装によって異なり、該当しないメンバーは出力されません。

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() は次のような配列を返します。

[
  {
    "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 仕様 をご確認ください。 標準仕様に定義された type でも、メンバーは接続状態やメディアの種類によって省略されます。 また、libwebrtc のバージョンによって、標準仕様にないメンバーが含まれることがあります。

使用例

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

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']}');
  }
}

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

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 が送出されます。

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