T-CREATOR

WebRTC Simulcast 設計ベストプラクティス:レイヤ数・ターゲットビットレート・切替条件

WebRTC Simulcast 設計ベストプラクティス:レイヤ数・ターゲットビットレート・切替条件

ビデオ会議やライブ配信のクオリティを左右する重要な技術、それが Simulcast です。 複数の解像度とビットレートを同時に配信することで、ユーザーのネットワーク状況に応じて最適な映像品質を提供できます。しかし、その設計には多くの判断が必要で、レイヤ数をいくつにするか、各レイヤのビットレートをどう設定するか、いつ切り替えるかといった疑問に直面するでしょう。

本記事では、Simulcast の設計における 3 つの重要な要素について、実践的なベストプラクティスをご紹介します。 実際のコード例とともに、あなたのプロジェクトですぐに使える知識をお届けしますね。

背景

WebRTC Simulcast とは

WebRTC における Simulcast は、送信側が同じ映像ソースから異なる解像度・ビットレートのストリームを複数生成し、同時に配信する技術です。 受信側のネットワーク帯域やデバイス性能に応じて、サーバー(SFU)が最適なレイヤを選択して転送することができます。

従来の単一ストリーム配信では、ネットワークが不安定な受信者がいると、その人のために映像品質を下げざるを得ませんでした。 しかし Simulcast を使えば、高速回線のユーザーには高品質な映像を、低速回線のユーザーには低品質な映像を、それぞれ同時に配信できるのです。

Simulcast の基本構造

以下の図は、Simulcast の基本的なデータフローを示しています。

mermaidflowchart TD
    camera["カメラ映像"] --> encoder["エンコーダー"]
    encoder --> high["高解像度レイヤ<br/>1280x720 / 2.5Mbps"]
    encoder --> mid["中解像度レイヤ<br/>640x360 / 1.0Mbps"]
    encoder --> low["低解像度レイヤ<br/>320x180 / 0.3Mbps"]

    high --> sfu["SFU サーバー"]
    mid --> sfu
    low --> sfu

    sfu --> |"高速回線"| user1["ユーザーA<br/>高品質受信"]
    sfu --> |"中速回線"| user2["ユーザーB<br/>中品質受信"]
    sfu --> |"低速回線"| user3["ユーザーC<br/>低品質受信"]

送信者のデバイスで 1 つの映像ソースから 3 つの異なるレイヤが生成され、SFU が各受信者の状況に応じて最適なレイヤを選択して転送します。

図で理解できる要点:

  • 1 つの映像ソースから複数のレイヤが同時生成される
  • SFU が受信者ごとに異なるレイヤを配信できる
  • 帯域に応じた最適な品質が自動選択される

Simulcast が必要とされる理由

ビデオ通信において、参加者のネットワーク環境は多様です。 オフィスの有線 LAN 接続から、移動中のモバイル回線まで、帯域幅は 10 倍以上の差があることも珍しくありません。

また、デバイスの画面サイズも重要な要素でしょう。 スマートフォンの小さな画面では低解像度で十分ですが、4K ディスプレイでは高解像度が求められますね。

Simulcast を適切に設計することで、これらすべてのユーザーに最適な体験を提供できるのです。

課題

レイヤ数の決定が難しい

Simulcast を実装する際、最初に直面するのが「何層のレイヤを用意すべきか」という問題です。 レイヤ数が少なすぎると、多様なネットワーク環境に対応できません。一方で、多すぎると送信側の CPU 負荷が高くなり、帯域も圧迫されてしまいます。

一般的には 2 層から 4 層の間で設計されることが多いのですが、具体的にどう決めればよいのでしょうか。 プロジェクトの要件やターゲットユーザーを考慮した最適な選択が求められます。

ビットレート設定の複雑さ

各レイヤのターゲットビットレートを決めることも、重要かつ難しい課題です。 ビットレートが低すぎると映像がブロックノイズだらけになり、高すぎるとパケットロスが発生して結果的に品質が劣化します。

また、解像度とビットレートの関係も考慮する必要がありますね。 同じビットレートでも、解像度が高いと 1 ピクセルあたりの情報量が減り、品質が低下してしまうのです。

以下の図は、不適切なビットレート設定による問題を示しています。

mermaidflowchart LR
    subgraph problem1["問題1: ビットレート過小"]
        low_br["低ビットレート<br/>720p @ 0.5Mbps"] --> blocky["ブロックノイズ<br/>視聴困難"]
    end

    subgraph problem2["問題2: ビットレート過大"]
        high_br["高ビットレート<br/>360p @ 3Mbps"] --> loss["パケットロス<br/>フリーズ頻発"]
    end

    subgraph solution["適切な設定"]
        optimal["バランス型<br/>720p @ 2.5Mbps<br/>360p @ 1.0Mbps"] --> smooth["スムーズな映像"]
    end

図で理解できる要点:

  • ビットレートと解像度のバランスが重要
  • 過小設定は画質劣化、過大設定はネットワーク負荷を招く
  • 適切な組み合わせが快適な視聴体験を実現する

切替条件の最適化

レイヤの切り替えタイミングの判断も複雑な課題です。 ネットワーク状況の一時的な変動で頻繁に切り替わると、ユーザー体験が損なわれてしまいます。

一方で、切り替えが遅すぎると、パケットロスや遅延が発生してしまうでしょう。 帯域推定、パケットロス率、RTT(Round Trip Time)など、複数の指標を総合的に判断する必要があります。

さらに、切り替え時のヒステリシス(履歴効果)をどう設計するかも重要です。 上位レイヤへの切り替えと下位レイヤへの切り替えで、異なる閾値を設定することで、安定性を高められます。

#課題項目影響範囲複雑度
1レイヤ数の決定CPU 負荷、帯域使用量
2ビットレート設定映像品質、ネットワーク負荷
3切替条件の最適化ユーザー体験、安定性

解決策

レイヤ数の設計指針

実践的なレイヤ数の選択には、ターゲットユーザーの環境分析が不可欠です。 以下のガイドラインを参考に、プロジェクトに最適な構成を選びましょう。

2 層構成(シンプル型)

モバイルアプリや限定的な用途に適しています。 送信側の負荷を最小限に抑えつつ、基本的な帯域適応が可能です。

  • 高レイヤ:720p @ 2.5Mbps(WiFi・有線環境向け)
  • 低レイヤ:360p @ 0.8Mbps(モバイル回線向け)

3 層構成(標準型)

最もバランスが取れた構成で、多くのビデオ会議システムで採用されています。 幅広いネットワーク環境に対応でき、送信側の負荷も許容範囲内です。

  • 高レイヤ:1280x720 @ 2.5Mbps
  • 中レイヤ:640x360 @ 1.0Mbps
  • 低レイヤ:320x180 @ 0.3Mbps

4 層構成(高品質型)

4K 配信や大画面表示が必要な場合に検討します。 送信側の CPU 負荷が高くなるため、デバイス性能の確認が必要でしょう。

mermaidflowchart TD
    input["映像入力"] --> decision{"ユースケース<br/>判定"}

    decision --> |"モバイル中心"| layer2["2層構成<br/>720p + 360p"]
    decision --> |"汎用的"| layer3["3層構成<br/>720p + 360p + 180p"]
    decision --> |"高品質配信"| layer4["4層構成<br/>1080p + 720p + 360p + 180p"]

    layer2 --> check2["CPU負荷: ★☆☆"]
    layer3 --> check3["CPU負荷: ★★☆"]
    layer4 --> check4["CPU負荷: ★★★"]

この図は、ユースケースに応じたレイヤ数の選択フローを示しています。 CPU 負荷とのバランスを考慮して、適切な構成を選ぶことが重要ですね。

図で理解できる要点:

  • ユースケースによって最適なレイヤ数が異なる
  • レイヤ数が多いほど CPU 負荷が高くなる
  • 汎用的には 3 層構成が推奨される

ターゲットビットレートの設定方法

ビットレートの設定には、解像度に応じた適切な値を選ぶ必要があります。 以下の表は、実践的な推奨値をまとめたものです。

#解像度フレームレート推奨ビットレート最小ビットレート用途
11920x108030fps3.5-4.0 Mbps2.5 Mbpsフル HD 会議
21280x72030fps2.0-2.5 Mbps1.5 MbpsHD 会議(標準)
3640x36030fps0.8-1.2 Mbps0.5 MbpsSD 会議
4320x18030fps0.25-0.4 Mbps0.15 Mbps低帯域環境

ビットレート計算の基本式

ビットレートは、解像度のピクセル数に比例して増やすのが基本です。 以下の計算式を参考にできます。

scssビットレート (bps) = ピクセル数 × フレームレート × ビット効率係数

ビット効率係数は、H.264 エンコーダの場合、0.07〜0.10 程度が目安となります。 H.265(HEVC)では、同じ品質を約半分のビットレートで実現できますね。

VP8/VP9 での Simulcast 設定例

WebRTC で一般的に使用される VP8/VP9 コーデックでの設定例を見てみましょう。

typescript// RTCRtpEncodingParametersの型定義
interface SimulcastEncoding {
  rid: string; // レイヤ識別子
  maxBitrate: number; // 最大ビットレート(bps)
  scaleResolutionDownBy: number; // 解像度のスケールダウン係数
  maxFramerate?: number; // 最大フレームレート
}

この型定義は、各 Simulcast レイヤの設定パラメータを表しています。 ridはレイヤの識別子、maxBitrateは最大ビットレート、scaleResolutionDownByは元の解像度からのスケールダウン係数です。

typescript// 3層Simulcastの設定例
const encodings: SimulcastEncoding[] = [
  {
    rid: 'h', // 高品質レイヤ
    maxBitrate: 2_500_000, // 2.5 Mbps
    scaleResolutionDownBy: 1.0,
    maxFramerate: 30,
  },
  {
    rid: 'm', // 中品質レイヤ
    maxBitrate: 1_000_000, // 1.0 Mbps
    scaleResolutionDownBy: 2.0, // 1/2解像度
    maxFramerate: 30,
  },
  {
    rid: 'l', // 低品質レイヤ
    maxBitrate: 300_000, // 0.3 Mbps
    scaleResolutionDownBy: 4.0, // 1/4解像度
    maxFramerate: 15,
  },
];

この設定により、元の映像が 1280x720 の場合、高レイヤは 1280x720、中レイヤは 640x360、低レイヤは 320x180 で配信されます。 低レイヤはフレームレートも 15fps に抑えることで、さらに帯域を節約しています。

切替条件の実装戦略

レイヤ切り替えの判断には、複数の指標を組み合わせた総合的なアプローチが効果的です。 以下の図は、切り替え判断のロジックフローを示しています。

mermaidstateDiagram-v2
    [*] --> Monitoring: 初期状態

    Monitoring --> CheckBandwidth: 定期評価<br/>(1秒ごと)

    CheckBandwidth --> HighQuality: 帯域 > 2.0Mbps<br/>かつ<br/>パケットロス < 2%
    CheckBandwidth --> MediumQuality: 帯域 0.8-2.0Mbps<br/>かつ<br/>パケットロス < 5%
    CheckBandwidth --> LowQuality: 帯域 < 0.8Mbps<br/>または<br/>パケットロス > 5%

    HighQuality --> Monitoring: 継続監視
    MediumQuality --> Monitoring: 継続監視
    LowQuality --> Monitoring: 継続監視

この状態遷移図は、帯域とパケットロス率に基づく品質レイヤの選択ロジックを表しています。 定期的な評価により、ネットワーク状況の変化に動的に対応できますね。

図で理解できる要点:

  • 1 秒ごとの定期的な評価で状態を判定
  • 帯域とパケットロス率の両方を考慮
  • 3 つの品質レイヤ間をスムーズに遷移

切り替え判断の主要指標

#指標重み上位レイヤ閾値下位レイヤ閾値備考
1利用可能帯域> 1.5 倍のビットレート< 1.2 倍のビットレート最重要指標
2パケットロス率< 2%> 5%品質に直結
3RTT(往復遅延)< 150ms> 300msリアルタイム性
4ジッター< 30ms> 80ms補助的指標

ヒステリシス設計

頻繁な切り替えを防ぐため、上位レイヤへの切り替えと下位レイヤへの切り替えで異なる閾値を設定します。

typescript// 切り替え判定の設定インターフェース
interface LayerSwitchConfig {
  // 帯域の閾値(bps)
  bandwidthThresholds: {
    upgradeToHigh: number; // 高レイヤへの昇格閾値
    downgradeFromHigh: number; // 高レイヤからの降格閾値
    upgradeToMedium: number; // 中レイヤへの昇格閾値
    downgradeFromMedium: number; // 中レイヤからの降格閾値
  };

  // パケットロスの閾値(%)
  packetLossThresholds: {
    maxForUpgrade: number; // 昇格可能な最大ロス率
    minForDowngrade: number; // 降格が必要なロス率
  };

  // 安定性のための設定
  stabilityWindow: number; // 判定に使う時間窓(ms)
  minDurationBeforeSwitch: number; // 切り替え前の最小待機時間(ms)
}

この設定インターフェースでは、切り替え判断に必要なすべてのパラメータを定義しています。 帯域とパケットロスの両方に対して、昇格と降格で異なる閾値を設けることがポイントです。

typescript// 実際の設定値の例
const switchConfig: LayerSwitchConfig = {
  bandwidthThresholds: {
    upgradeToHigh: 3_000_000, // 3.0 Mbps以上で高レイヤへ
    downgradeFromHigh: 2_000_000, // 2.0 Mbps未満で中レイヤへ
    upgradeToMedium: 1_200_000, // 1.2 Mbps以上で中レイヤへ
    downgradeFromMedium: 800_000, // 0.8 Mbps未満で低レイヤへ
  },

  packetLossThresholds: {
    maxForUpgrade: 2.0, // 2%未満なら昇格可能
    minForDowngrade: 5.0, // 5%以上なら降格必要
  },

  stabilityWindow: 3000, // 3秒間の統計を使用
  minDurationBeforeSwitch: 2000, // 最低2秒は同じレイヤを維持
};

この設定により、ネットワークの一時的な変動では切り替わらず、持続的な変化にのみ反応するようになります。 stabilityWindowで 3 秒間の平均値を使い、minDurationBeforeSwitchで最低 2 秒間は同じレイヤを維持することで、安定性を確保しています。

具体例

WebRTC API を使った実装例

実際の WebRTC API を使用した Simulcast の実装例をご紹介します。 送信側の設定から、受信側のレイヤ選択まで、一連の流れを見ていきましょう。

送信側:Simulcast 有効化とエンコーディング設定

typescript// メディアストリームの取得
async function setupSimulcastSender(): Promise<RTCPeerConnection> {
  // カメラ映像を取得
  const stream = await navigator.mediaDevices.getUserMedia({
    video: {
      width: { ideal: 1280 },
      height: { ideal: 720 },
      frameRate: { ideal: 30 },
    },
  });

  return stream;
}

まず、カメラから 1280x720、30fps の映像を取得します。 これが、Simulcast の元となる映像ソースとなりますね。

typescript// PeerConnectionの作成とトラック追加
async function addSimulcastTrack(
  pc: RTCPeerConnection,
  stream: MediaStream
): Promise<RTCRtpSender> {
  const videoTrack = stream.getVideoTracks()[0];

  // トラックを追加してSenderを取得
  const sender = pc.addTrack(videoTrack, stream);

  return sender;
}

取得した映像トラックを PeerConnection に追加し、RTCRtpSender オブジェクトを取得します。 この Sender を使って、エンコーディングパラメータを設定していきます。

typescript// Simulcastエンコーディングの設定
async function configureSimulcast(
  sender: RTCRtpSender
): Promise<void> {
  // 現在のパラメータを取得
  const params = sender.getParameters();

  // エンコーディング設定がない場合は初期化
  if (!params.encodings || params.encodings.length === 0) {
    params.encodings = [];
  }

  // 3層のSimulcast設定
  params.encodings = [
    {
      rid: 'h',
      maxBitrate: 2_500_000,
      scaleResolutionDownBy: 1.0,
    },
    {
      rid: 'm',
      maxBitrate: 1_000_000,
      scaleResolutionDownBy: 2.0,
    },
    {
      rid: 'l',
      maxBitrate: 300_000,
      scaleResolutionDownBy: 4.0,
    },
  ];

  // パラメータを適用
  await sender.setParameters(params);
}

このコードで、3 層の Simulcast エンコーディングを設定しています。 setParametersを呼び出すことで、ブラウザのエンコーダーがこれらの設定に従って複数のレイヤを生成し始めます。

SDP 交換時の注意点

typescript// Offer生成時のSimulcast対応
async function createSimulcastOffer(
  pc: RTCPeerConnection
): Promise<RTCSessionDescriptionInit> {
  // Offerを生成
  const offer = await pc.createOffer();

  // SDPにSimulcast情報が含まれていることを確認
  console.log('SDP with simulcast:', offer.sdp);

  // OfferをローカルDescriptionに設定
  await pc.setLocalDescription(offer);

  return offer;
}

生成された SDP には、a=simulcast行や複数のa=rid行が含まれているはずです。 これにより、受信側は Simulcast が有効であることを認識できます。

受信側:レイヤ選択の実装

受信側では、ネットワーク状況に応じて最適なレイヤを選択します。

typescript// レイヤ選択のための統計情報収集
interface NetworkStats {
  availableBandwidth: number; // 利用可能帯域(bps)
  packetLossRate: number; // パケットロス率(%)
  rtt: number; // RTT(ms)
  timestamp: number; // タイムスタンプ
}

ネットワーク統計を表す型定義です。 これらの値を定期的に取得して、レイヤ切り替えの判断材料とします。

typescript// 統計情報の収集
async function collectNetworkStats(
  pc: RTCPeerConnection
): Promise<NetworkStats> {
  const stats = await pc.getStats();
  let bandwidth = 0;
  let packetLoss = 0;
  let rtt = 0;

  stats.forEach((report) => {
    // インバウンドRTP統計から情報を取得
    if (
      report.type === 'inbound-rtp' &&
      report.kind === 'video'
    ) {
      // パケットロス率の計算
      if (report.packetsReceived && report.packetsLost) {
        const total =
          report.packetsReceived + report.packetsLost;
        packetLoss = (report.packetsLost / total) * 100;
      }

      // 帯域の推定(bytesReceivedから計算)
      if (report.bytesReceived && report.timestamp) {
        // 前回の測定値と比較して帯域を計算
        // (実装は簡略化のため省略)
        bandwidth = estimateBandwidth(report);
      }
    }

    // Candidate統計からRTTを取得
    if (
      report.type === 'candidate-pair' &&
      report.state === 'succeeded'
    ) {
      rtt = report.currentRoundTripTime * 1000; // 秒からミリ秒へ
    }
  });

  return {
    availableBandwidth: bandwidth,
    packetLossRate: packetLoss,
    rtt: rtt,
    timestamp: Date.now(),
  };
}

getStats()メソッドを使用して、WebRTC の詳細な統計情報を取得します。 パケットロス率、帯域、RTT などの値を抽出し、レイヤ選択の判断に使用できますね。

typescript// レイヤ選択ロジック
type QualityLayer = 'high' | 'medium' | 'low';

function selectOptimalLayer(
  stats: NetworkStats,
  config: LayerSwitchConfig,
  currentLayer: QualityLayer
): QualityLayer {
  const { availableBandwidth, packetLossRate } = stats;
  const { bandwidthThresholds, packetLossThresholds } =
    config;

  // パケットロスが高い場合は降格
  if (
    packetLossRate > packetLossThresholds.minForDowngrade
  ) {
    return currentLayer === 'high' ? 'medium' : 'low';
  }

  // 帯域に基づく判定
  if (
    availableBandwidth >=
      bandwidthThresholds.upgradeToHigh &&
    packetLossRate < packetLossThresholds.maxForUpgrade
  ) {
    return 'high';
  }

  if (
    availableBandwidth >=
      bandwidthThresholds.upgradeToMedium &&
    availableBandwidth <
      bandwidthThresholds.downgradeFromHigh
  ) {
    return 'medium';
  }

  if (
    availableBandwidth <
    bandwidthThresholds.downgradeFromMedium
  ) {
    return 'low';
  }

  // 現在のレイヤを維持(ヒステリシス)
  return currentLayer;
}

このロジックでは、まずパケットロスをチェックし、次に帯域を評価しています。 ヒステリシスにより、閾値付近での頻繁な切り替えを防いでいますね。

実際のレイヤ切り替え実装

typescript// レイヤ切り替えの実行(SFU経由)
async function switchLayer(
  pc: RTCPeerConnection,
  layer: QualityLayer,
  signaling: SignalingChannel
): Promise<void> {
  // レイヤに対応するridを決定
  const ridMap: Record<QualityLayer, string> = {
    high: 'h',
    medium: 'm',
    low: 'l',
  };

  const targetRid = ridMap[layer];

  // シグナリングサーバー経由でSFUにレイヤ変更を要求
  await signaling.send({
    type: 'switch-layer',
    rid: targetRid,
  });

  console.log(
    `Switched to ${layer} layer (rid: ${targetRid})`
  );
}

実際のレイヤ切り替えは、SFU サーバーへのシグナリングメッセージで行います。 クライアントが直接エンコーディングを切り替えるのではなく、SFU に対して「この rid のレイヤを送ってください」と要求する形ですね。

定期的な監視と自動切り替え

typescript// 自動レイヤ切り替えの実装
class SimulcastLayerManager {
  private currentLayer: QualityLayer = 'medium';
  private lastSwitchTime: number = 0;
  private statsHistory: NetworkStats[] = [];

  constructor(
    private pc: RTCPeerConnection,
    private signaling: SignalingChannel,
    private config: LayerSwitchConfig
  ) {}

  // 監視開始
  startMonitoring(): void {
    setInterval(async () => {
      await this.evaluateAndSwitch();
    }, 1000); // 1秒ごとに評価
  }

  // 評価と切り替え
  private async evaluateAndSwitch(): Promise<void> {
    // 統計情報を収集
    const stats = await collectNetworkStats(this.pc);
    this.statsHistory.push(stats);

    // 履歴を一定期間分のみ保持
    const windowMs = this.config.stabilityWindow;
    const cutoff = Date.now() - windowMs;
    this.statsHistory = this.statsHistory.filter(
      (s) => s.timestamp > cutoff
    );

    // 平均値を計算
    const avgStats = this.calculateAverageStats();

    // 最適なレイヤを選択
    const optimalLayer = selectOptimalLayer(
      avgStats,
      this.config,
      this.currentLayer
    );

    // レイヤが変わる場合は切り替え
    if (optimalLayer !== this.currentLayer) {
      const now = Date.now();
      const timeSinceLastSwitch = now - this.lastSwitchTime;

      // 最小待機時間をチェック
      if (
        timeSinceLastSwitch >=
        this.config.minDurationBeforeSwitch
      ) {
        await switchLayer(
          this.pc,
          optimalLayer,
          this.signaling
        );
        this.currentLayer = optimalLayer;
        this.lastSwitchTime = now;
      }
    }
  }

  // 平均統計の計算
  private calculateAverageStats(): NetworkStats {
    const count = this.statsHistory.length;
    if (count === 0) {
      return {
        availableBandwidth: 0,
        packetLossRate: 0,
        rtt: 0,
        timestamp: Date.now(),
      };
    }

    const sum = this.statsHistory.reduce((acc, stat) => ({
      availableBandwidth:
        acc.availableBandwidth + stat.availableBandwidth,
      packetLossRate:
        acc.packetLossRate + stat.packetLossRate,
      rtt: acc.rtt + stat.rtt,
      timestamp: stat.timestamp,
    }));

    return {
      availableBandwidth: sum.availableBandwidth / count,
      packetLossRate: sum.packetLossRate / count,
      rtt: sum.rtt / count,
      timestamp: Date.now(),
    };
  }
}

このクラスは、Simulcast レイヤの自動管理を実現します。 1 秒ごとにネットワーク統計を収集し、設定された時間窓内の平均値を使って安定した判断を行います。

使用例

typescript// 初期化と使用例
async function initializeSimulcast() {
  // PeerConnectionの作成
  const pc = new RTCPeerConnection(configuration);

  // メディアストリームの取得
  const stream = await setupSimulcastSender();

  // トラックの追加
  const sender = await addSimulcastTrack(pc, stream);

  // Simulcastの設定
  await configureSimulcast(sender);

  // レイヤマネージャーの初期化
  const manager = new SimulcastLayerManager(
    pc,
    signalingChannel,
    switchConfig
  );

  // 自動切り替えの開始
  manager.startMonitoring();

  // Offer/Answerの交換
  const offer = await createSimulcastOffer(pc);
  await signalingChannel.send({
    type: 'offer',
    sdp: offer,
  });
}

これで、完全に動作する Simulcast システムが構築できます。 自動的にネットワーク状況を監視し、最適なレイヤに切り替えてくれるでしょう。

パフォーマンス最適化の Tips

実装した Simulcast をさらに最適化するためのテクニックをご紹介します。

CPU 使用率の監視と調整

typescript// CPU使用率に基づくレイヤ数の動的調整
async function monitorCpuAndAdjustLayers(
  sender: RTCRtpSender
): Promise<void> {
  // CPUの統計情報を取得(実装は環境依存)
  const cpuUsage = await getCpuUsage();

  const params = sender.getParameters();

  // CPU使用率が80%を超えたら低レイヤを無効化
  if (cpuUsage > 80) {
    params.encodings = params.encodings?.map((encoding) => {
      if (encoding.rid === 'l') {
        return { ...encoding, active: false };
      }
      return encoding;
    });

    await sender.setParameters(params);
    console.log('Disabled low layer due to high CPU usage');
  }
}

送信側の CPU 負荷が高い場合、一時的にレイヤ数を減らすことで、安定性を保てます。 特にモバイルデバイスでは重要な最適化ですね。

ネットワーク条件による事前調整

typescript// 初期ネットワーク測定に基づく設定
async function optimizeInitialSettings(
  connection: RTCPeerConnection
): Promise<SimulcastEncoding[]> {
  // 簡易的な帯域測定
  const estimatedBandwidth =
    await measureInitialBandwidth();

  // 帯域に応じて初期レイヤを調整
  if (estimatedBandwidth < 1_000_000) {
    // 1Mbps未満の場合は2層のみ
    return [
      {
        rid: 'm',
        maxBitrate: 800_000,
        scaleResolutionDownBy: 1.0,
      },
      {
        rid: 'l',
        maxBitrate: 300_000,
        scaleResolutionDownBy: 2.0,
      },
    ];
  } else {
    // 通常の3層構成
    return [
      {
        rid: 'h',
        maxBitrate: 2_500_000,
        scaleResolutionDownBy: 1.0,
      },
      {
        rid: 'm',
        maxBitrate: 1_000_000,
        scaleResolutionDownBy: 2.0,
      },
      {
        rid: 'l',
        maxBitrate: 300_000,
        scaleResolutionDownBy: 4.0,
      },
    ];
  }
}

接続開始時に簡易的な帯域測定を行い、初期設定を最適化することで、無駄なエンコーディング負荷を避けられます。

まとめ

WebRTC Simulcast の設計において、レイヤ数・ターゲットビットレート・切替条件の 3 つの要素を適切に設定することが、高品質なビデオ通信を実現する鍵となります。

レイヤ数の選択では、ターゲットユーザーの環境を分析し、2 層から 4 層の間で最適な構成を選びましょう。 多くの汎用的なユースケースでは、3 層構成(720p・360p・180p)がバランスの取れた選択となります。

ターゲットビットレートは、解像度とのバランスが重要です。 720p では 2.0〜2.5Mbps、360p では 0.8〜1.2Mbps、180p では 0.25〜0.4Mbps が実践的な目安となるでしょう。

切替条件には、帯域とパケットロス率を主要指標とし、ヒステリシスを設けることで安定性を確保できます。 3 秒程度の時間窓で統計を平均化し、最低 2 秒間は同じレイヤを維持する設計が効果的ですね。

これらのベストプラクティスを実装することで、多様なネットワーク環境のユーザーに最適な映像品質を提供できるシステムを構築できます。 ぜひ、あなたのプロジェクトに合わせてパラメータを調整し、最高のユーザー体験を実現してください。

関連リンク