T-CREATOR

WebRTC が「connecting」のまま進まない:ICE 失敗を 5 分で切り分ける手順

WebRTC が「connecting」のまま進まない:ICE 失敗を 5 分で切り分ける手順

WebRTC でビデオ通話やリアルタイム通信を実装した際、接続が「connecting」のまま止まってしまうことはありませんか?

この記事では、WebRTC の ICE(Interactive Connectivity Establishment)プロセスの失敗を素早く特定し、解決するための実践的な手順を解説します。開発現場で遭遇しやすいトラブルに焦点を当て、5 分で原因を切り分けられる方法をお伝えしましょう。

背景

WebRTC 接続の仕組み

WebRTC は、ブラウザ間でリアルタイムに音声・映像・データを送受信するための技術です。しかし、直接接続するためには、NAT やファイアウォールを越える必要があります。

以下の図は、WebRTC 接続における主要なコンポーネントの関係性を示しています。

mermaidflowchart TB
  clientA["クライアント A<br/>(ブラウザ)"]
  clientB["クライアント B<br/>(ブラウザ)"]
  signaling["シグナリングサーバー<br/>(WebSocket/HTTP)"]
  stun["STUN サーバー<br/>(NAT 越え支援)"]
  turn["TURN サーバー<br/>(リレー中継)"]

  clientA -->|"1. SDP Offer/Answer"| signaling
  signaling -->|"1. SDP Offer/Answer"| clientB
  clientA -.->|"2. STUN リクエスト"| stun
  clientB -.->|"2. STUN リクエスト"| stun
  stun -.->|"Public IP 返却"| clientA
  stun -.->|"Public IP 返却"| clientB
  clientA -->|"3a. P2P 接続試行"| clientB
  clientA -.->|"3b. TURN 経由<br/>(P2P 失敗時)"| turn
  turn -.->|"3b. TURN 経由<br/>(P2P 失敗時)"| clientB

図の要点:WebRTC 接続は、シグナリング → STUN による NAT 越え → P2P または TURN 経由という段階を経ます。

ICE の役割

ICE(Interactive Connectivity Establishment)は、ピア間の最適な通信経路を見つけるためのプロトコルです。複数の候補(Candidate)を収集し、優先順位をつけて接続を試みます。

mermaidstateDiagram-v2
  [*] --> new: RTCPeerConnection 作成
  new --> checking: ICE Candidate 収集開始
  checking --> connected: 接続成功
  checking --> failed: 全候補失敗
  connected --> completed: 最適経路確定
  connected --> disconnected: 一時的切断
  disconnected --> connected: 再接続成功
  disconnected --> failed: 再接続失敗
  failed --> [*]
  completed --> [*]

図の要点:ICE は複数の状態を遷移し、「checking」から「connected」に進めない場合、ICE 失敗と判断されます。

ICE が失敗すると、接続状態が「connecting」や「checking」のまま止まってしまい、通信が確立されません。

課題

「connecting」で止まる主な原因

WebRTC の接続が進まない場合、以下のような課題が考えられます。

#原因カテゴリ具体的な問題頻度
1ICE Candidate の不足STUN/TURN サーバーの設定ミス★★★
2ファイアウォールUDP ポートがブロックされている★★★
3シグナリングの不備ICE Candidate が相手に届いていない★★☆
4ネットワーク環境Symmetric NAT で P2P 不可★★☆
5ブラウザ制限セキュアコンテキスト (HTTPS) 未対応★☆☆

これらの課題を素早く切り分けることが、トラブルシューティングの鍵となります。

以下の図は、ICE 接続失敗の主な原因と、それぞれが発生するポイントを示しています。

mermaidflowchart TD
  start["ICE 接続失敗"]
  check1{"ICE Candidate<br/>収集できた?"}
  check2{"STUN/TURN<br/>設定は正しい?"}
  check3{"Candidate<br/>交換できた?"}
  check4{"ファイアウォール<br/>は開いている?"}

  result1["原因 1:<br/>STUN/TURN 設定ミス"]
  result2["原因 2:<br/>シグナリング不備"]
  result3["原因 3:<br/>ファイアウォールブロック"]
  result4["原因 4:<br/>Symmetric NAT"]

  start --> check1
  check1 -->|NO| check2
  check2 -->|NO| result1
  check2 -->|YES| result1
  check1 -->|YES| check3
  check3 -->|NO| result2
  check3 -->|YES| check4
  check4 -->|NO| result3
  check4 -->|YES| result4

図で理解できる要点:

  • ICE 失敗は段階的に切り分けられる
  • Candidate 収集とシグナリング、ネットワーク設定の 3 つが主要チェックポイント

解決策

5 分で実践できる切り分け手順

ここでは、実際の開発現場で使える診断手順を順番に紹介します。各ステップは 1 分程度で確認できますので、焦らず進めていきましょう。

ステップ 1:ICE 状態のログを取得

まずは、現在の ICE 接続状態を確認するためのログを仕込みます。

以下のコードは、RTCPeerConnection のイベントリスナーを設定し、接続状態を監視するものです。

typescript// RTCPeerConnection のインスタンスを作成
const peerConnection = new RTCPeerConnection({
  iceServers: [{ urls: 'stun:stun.l.google.com:19302' }],
});

次に、ICE 接続状態の変化を監視するリスナーを追加しましょう。

typescript// ICE 接続状態の変化を監視
peerConnection.addEventListener(
  'iceconnectionstatechange',
  () => {
    console.log(
      'ICE Connection State:',
      peerConnection.iceConnectionState
    );

    // 各状態の意味
    // new: まだ候補を収集していない
    // checking: 候補をチェック中
    // connected: 少なくとも 1 つの候補で接続成功
    // completed: すべての候補をチェック完了
    // failed: すべての候補で接続失敗
    // disconnected: 接続が切れた
    // closed: 接続が閉じられた
  }
);

さらに、ICE 収集状態も確認します。

typescript// ICE 収集状態の変化を監視
peerConnection.addEventListener(
  'icegatheringstatechange',
  () => {
    console.log(
      'ICE Gathering State:',
      peerConnection.iceGatheringState
    );

    // gathering: 候補を収集中
    // complete: 収集完了
  }
);

確認ポイント: iceConnectionState が「checking」のまま「connected」に進まない場合、ICE 失敗の可能性が高いです。

ステップ 2:ICE Candidate の収集確認

ICE Candidate が正しく収集されているかを確認します。これにより、STUN/TURN サーバーの設定ミスを検出できるでしょう。

typescript// ICE Candidate の収集を監視
peerConnection.addEventListener('icecandidate', (event) => {
  if (event.candidate) {
    console.log('ICE Candidate:', event.candidate);

    // Candidate のタイプを確認
    // host: ローカルネットワークアドレス
    // srflx: STUN で取得した外部アドレス
    // relay: TURN サーバー経由のアドレス
    console.log('Candidate Type:', event.candidate.type);
    console.log(
      'Candidate Address:',
      event.candidate.address
    );
  } else {
    // null の場合、すべての候補収集が完了
    console.log('ICE Candidate 収集完了');
  }
});

収集された Candidate の種類を分類して表示する関数を作成しましょう。

typescript// Candidate の種類別に集計する関数
function analyzeCandidates(candidates: RTCIceCandidate[]) {
  const types = {
    host: 0,
    srflx: 0,
    relay: 0,
  };

  candidates.forEach((candidate) => {
    if (candidate.type === 'host') types.host++;
    if (candidate.type === 'srflx') types.srflx++;
    if (candidate.type === 'relay') types.relay++;
  });

  console.log('Candidate 集計:', types);
  return types;
}

診断ポイント:

  • host のみ:STUN/TURN サーバーが機能していない
  • srflx がある:STUN は動作している
  • relay がある:TURN も動作している

ステップ 3:STUN/TURN サーバーの設定確認

STUN/TURN サーバーの設定が正しいか確認します。設定ミスは最も頻繁に発生する問題です。

typescript// 正しい STUN/TURN 設定の例
const configuration: RTCConfiguration = {
  iceServers: [
    // Google の公開 STUN サーバー
    {
      urls: 'stun:stun.l.google.com:19302',
    },
    // Cloudflare の公開 STUN サーバー
    {
      urls: 'stun:stun.cloudflare.com:3478',
    },
  ],
};

TURN サーバーを使う場合は、認証情報も必要になります。

typescript// TURN サーバーの設定(認証あり)
const configurationWithTurn: RTCConfiguration = {
  iceServers: [
    {
      urls: 'stun:stun.l.google.com:19302',
    },
    {
      urls: 'turn:your-turn-server.com:3478',
      username: 'your-username',
      credential: 'your-password',
    },
  ],
};

const peerConnectionWithTurn = new RTCPeerConnection(
  configurationWithTurn
);

設定が正しく読み込まれているか確認する関数も用意しましょう。

typescript// ICE サーバー設定の確認関数
function verifyIceServers(pc: RTCPeerConnection) {
  const config = pc.getConfiguration();

  console.log('設定されている ICE サーバー:');
  config.iceServers?.forEach((server, index) => {
    console.log(`Server ${index + 1}:`, {
      urls: server.urls,
      username: server.username ? '設定あり' : '設定なし',
      credential: server.credential
        ? '設定あり'
        : '設定なし',
    });
  });
}

// 実行例
verifyIceServers(peerConnection);

よくある設定ミス:

  • URL のスペルミス(stunsturn と誤記)
  • TURN サーバーの認証情報が古い
  • ポート番号の間違い

ステップ 4:シグナリングの確認

ICE Candidate が相手に正しく送信されているか確認します。シグナリングサーバーの実装ミスを検出できるでしょう。

以下は、WebSocket を使ったシグナリングの実装例です。

typescript// WebSocket 接続の確立
const signalingSocket = new WebSocket(
  'wss://your-signaling-server.com'
);

signalingSocket.addEventListener('open', () => {
  console.log('シグナリングサーバーに接続しました');
});

ICE Candidate を相手に送信する処理を実装します。

typescript// ICE Candidate を相手に送信
peerConnection.addEventListener('icecandidate', (event) => {
  if (event.candidate) {
    // Candidate をシグナリングサーバー経由で送信
    const message = {
      type: 'ice-candidate',
      candidate: event.candidate.toJSON(),
    };

    signalingSocket.send(JSON.stringify(message));
    console.log(
      'ICE Candidate を送信:',
      event.candidate.type
    );
  }
});

相手から受信した ICE Candidate を追加する処理も必要です。

typescript// 相手からの ICE Candidate を受信
signalingSocket.addEventListener(
  'message',
  async (event) => {
    const message = JSON.parse(event.data);

    if (
      message.type === 'ice-candidate' &&
      message.candidate
    ) {
      try {
        await peerConnection.addIceCandidate(
          new RTCIceCandidate(message.candidate)
        );
        console.log('相手の ICE Candidate を追加しました');
      } catch (error) {
        console.error('ICE Candidate 追加エラー:', error);
      }
    }
  }
);

送受信の整合性を確認するカウンター機能を追加しましょう。

typescript// シグナリングの統計情報
const signalingStats = {
  candidatesSent: 0,
  candidatesReceived: 0,
};

// 送信時にカウント
peerConnection.addEventListener('icecandidate', (event) => {
  if (event.candidate) {
    signalingStats.candidatesSent++;
    console.log(
      `送信: ${signalingStats.candidatesSent} 個目の Candidate`
    );
  }
});

// 受信時にカウント
signalingSocket.addEventListener('message', (event) => {
  const message = JSON.parse(event.data);
  if (message.type === 'ice-candidate') {
    signalingStats.candidatesReceived++;
    console.log(
      `受信: ${signalingStats.candidatesReceived} 個目の Candidate`
    );
  }
});

診断ポイント:

  • 送信カウントが 0:自分の Candidate が収集されていない
  • 受信カウントが 0:相手の Candidate が届いていない(シグナリングの問題)

ステップ 5:ファイアウォール・ネットワーク確認

最後に、ネットワークレベルの問題を確認します。企業ネットワークや厳しい NAT 環境では、この問題が頻発するでしょう。

typescript// ネットワーク診断用の関数
async function diagnoseNetworkConnectivity() {
  const results = {
    stunReachable: false,
    turnReachable: false,
    p2pPossible: false,
  };

  // 診断用の PeerConnection を作成
  const testPc = new RTCPeerConnection({
    iceServers: [{ urls: 'stun:stun.l.google.com:19302' }],
  });

  return new Promise<typeof results>((resolve) => {
    const candidateTypes = new Set<string>();

    testPc.addEventListener('icecandidate', (event) => {
      if (event.candidate) {
        candidateTypes.add(event.candidate.type);
      } else {
        // 収集完了時に結果を判定
        results.stunReachable = candidateTypes.has('srflx');
        results.p2pPossible =
          candidateTypes.has('srflx') ||
          candidateTypes.has('host');

        testPc.close();
        resolve(results);
      }
    });

    // ダミーのデータチャネルを作成して ICE 収集を開始
    testPc.createDataChannel('test');
    testPc
      .createOffer()
      .then((offer) => testPc.setLocalDescription(offer));
  });
}

診断結果を見やすく表示する関数を作成します。

typescript// 診断実行と結果表示
async function runNetworkDiagnostics() {
  console.log('ネットワーク診断を開始します...');

  const results = await diagnoseNetworkConnectivity();

  console.log('=== 診断結果 ===');
  console.log(
    `STUN 到達性: ${
      results.stunReachable ? '✓ OK' : '✗ NG'
    }`
  );
  console.log(
    `P2P 接続可能性: ${
      results.p2pPossible ? '✓ OK' : '✗ NG'
    }`
  );

  // 推奨アクション
  if (!results.stunReachable) {
    console.warn(
      '推奨: TURN サーバーの導入を検討してください'
    );
  }

  if (!results.p2pPossible) {
    console.error(
      '警告: 現在のネットワークでは P2P 接続ができません'
    );
    console.log('対策: TURN サーバーが必須です');
  }

  return results;
}

// 実行
runNetworkDiagnostics();

ネットワーク問題のチェックリスト:

#確認項目確認方法対処法
1UDP ポートが開いているかブラウザの開発者ツールで確認ファイアウォール設定を変更
2HTTPS で接続しているかURL を確認HTTPS 化する
3Symmetric NAT かsrflx Candidate の有無TURN サーバーを使用
4VPN を使用しているかネットワーク設定を確認VPN を無効化してテスト

具体例

実践:診断ツールの実装

これまでの手順を統合した、包括的な診断ツールを実装してみましょう。このツールは、WebRTC の接続問題を自動的に診断し、レポートを生成します。

typescript// 診断結果の型定義
interface DiagnosticReport {
  timestamp: string;
  iceConnectionState: RTCIceConnectionState;
  iceGatheringState: RTCIceGatheringState;
  candidates: {
    host: number;
    srflx: number;
    relay: number;
  };
  issues: string[];
  recommendations: string[];
}

診断ツールのメインクラスを定義します。

typescript// WebRTC 診断ツールクラス
class WebRTCDiagnostics {
  private peerConnection: RTCPeerConnection;
  private collectedCandidates: RTCIceCandidate[] = [];
  private report: DiagnosticReport;

  constructor(configuration: RTCConfiguration) {
    this.peerConnection = new RTCPeerConnection(
      configuration
    );
    this.report = this.initializeReport();
    this.setupEventListeners();
  }

  private initializeReport(): DiagnosticReport {
    return {
      timestamp: new Date().toISOString(),
      iceConnectionState: 'new',
      iceGatheringState: 'new',
      candidates: { host: 0, srflx: 0, relay: 0 },
      issues: [],
      recommendations: [],
    };
  }

  private setupEventListeners(): void {
    // 次のコードブロックで実装
  }
}

イベントリスナーを設定し、状態変化を記録します。

typescript// イベントリスナーの設定
private setupEventListeners(): void {
  // ICE 接続状態の監視
  this.peerConnection.addEventListener('iceconnectionstatechange', () => {
    this.report.iceConnectionState = this.peerConnection.iceConnectionState;
    console.log('ICE Connection State:', this.report.iceConnectionState);
  });

  // ICE 収集状態の監視
  this.peerConnection.addEventListener('icegatheringstatechange', () => {
    this.report.iceGatheringState = this.peerConnection.iceGatheringState;
    console.log('ICE Gathering State:', this.report.iceGatheringState);
  });

  // ICE Candidate の収集
  this.peerConnection.addEventListener('icecandidate', (event) => {
    if (event.candidate) {
      this.collectedCandidates.push(event.candidate);
      this.categorizeCandidate(event.candidate);
    }
  });
}

収集した Candidate を分類する処理を実装します。

typescript// Candidate の分類
private categorizeCandidate(candidate: RTCIceCandidate): void {
  switch (candidate.type) {
    case 'host':
      this.report.candidates.host++;
      break;
    case 'srflx':
      this.report.candidates.srflx++;
      break;
    case 'relay':
      this.report.candidates.relay++;
      break;
  }

  console.log(`Candidate 追加 [${candidate.type}]:`, candidate.address);
}

診断を実行し、問題を検出するメソッドを実装しましょう。

typescript// 診断の実行
async runDiagnostics(): Promise<DiagnosticReport> {
  console.log('診断を開始します...');

  // ダミーのデータチャネルを作成
  this.peerConnection.createDataChannel('diagnostics');

  // Offer を作成して ICE 収集を開始
  const offer = await this.peerConnection.createOffer();
  await this.peerConnection.setLocalDescription(offer);

  // ICE 収集完了を待つ(最大 5 秒)
  await this.waitForIceGathering(5000);

  // 問題の検出
  this.detectIssues();

  // レポートを返す
  return this.report;
}

ICE 収集の完了を待つヘルパーメソッドを作成します。

typescript// ICE 収集完了を待つ
private waitForIceGathering(timeout: number): Promise<void> {
  return new Promise((resolve) => {
    const checkComplete = () => {
      if (this.report.iceGatheringState === 'complete') {
        resolve();
      }
    };

    this.peerConnection.addEventListener('icegatheringstatechange', checkComplete);

    // タイムアウト設定
    setTimeout(() => {
      console.warn('ICE 収集がタイムアウトしました');
      resolve();
    }, timeout);
  });
}

検出された問題を分析し、推奨事項を生成します。

typescript// 問題の検出と推奨事項の生成
private detectIssues(): void {
  const { candidates } = this.report;

  // STUN サーバーの問題
  if (candidates.host > 0 && candidates.srflx === 0) {
    this.report.issues.push(
      'STUN サーバーから srflx Candidate を取得できませんでした'
    );
    this.report.recommendations.push(
      'STUN サーバーの設定を確認してください'
    );
    this.report.recommendations.push(
      'ファイアウォールで UDP ポートがブロックされていないか確認してください'
    );
  }

  // TURN サーバーの問題
  if (candidates.relay === 0) {
    this.report.issues.push(
      'TURN サーバーから relay Candidate を取得できませんでした'
    );
    this.report.recommendations.push(
      'Symmetric NAT 環境では TURN サーバーが必須です'
    );
  }

  // Candidate が全く収集されない
  if (candidates.host === 0) {
    this.report.issues.push(
      'ICE Candidate が全く収集されませんでした'
    );
    this.report.recommendations.push(
      'ブラウザの WebRTC 機能が有効か確認してください'
    );
    this.report.recommendations.push(
      'HTTPS で接続しているか確認してください'
    );
  }
}

診断レポートを見やすく表示するメソッドを追加します。

typescript// レポートの表示
displayReport(): void {
  console.log('\n=== WebRTC 診断レポート ===');
  console.log(`実行時刻: ${this.report.timestamp}`);
  console.log(`ICE 接続状態: ${this.report.iceConnectionState}`);
  console.log(`ICE 収集状態: ${this.report.iceGatheringState}`);

  console.log('\n--- 収集された Candidate ---');
  console.log(`Host: ${this.report.candidates.host} 個`);
  console.log(`Server Reflexive (srflx): ${this.report.candidates.srflx} 個`);
  console.log(`Relay: ${this.report.candidates.relay} 個`);

  if (this.report.issues.length > 0) {
    console.log('\n--- 検出された問題 ---');
    this.report.issues.forEach((issue, index) => {
      console.log(`${index + 1}. ${issue}`);
    });
  }

  if (this.report.recommendations.length > 0) {
    console.log('\n--- 推奨事項 ---');
    this.report.recommendations.forEach((rec, index) => {
      console.log(`${index + 1}. ${rec}`);
    });
  }

  console.log('========================\n');
}

診断ツールの使用例を示します。

typescript// 使用例
async function runWebRTCDiagnostics() {
  // 診断ツールのインスタンス作成
  const diagnostics = new WebRTCDiagnostics({
    iceServers: [
      { urls: 'stun:stun.l.google.com:19302' },
      { urls: 'stun:stun.cloudflare.com:3478' },
    ],
  });

  // 診断実行
  const report = await diagnostics.runDiagnostics();

  // 結果表示
  diagnostics.displayReport();

  return report;
}

// 実行
runWebRTCDiagnostics();

トラブルシューティングフローチャート

以下の図は、実際のトラブルシューティングで使える判断フローを示しています。

mermaidflowchart TD
  start["接続が connecting で止まる"]

  step1{"ICE Candidate<br/>が収集されている?"}
  step2{"srflx Candidate<br/>がある?"}
  step3{"相手から Candidate<br/>を受信している?"}
  step4{"ファイアウォールで<br/>UDP が開いている?"}
  step5{"TURN サーバーを<br/>使用している?"}

  fix1["STUN/TURN 設定を確認<br/>診断ツールを実行"]
  fix2["STUN サーバーを追加<br/>UDP ポート開放を依頼"]
  fix3["シグナリングサーバーを確認<br/>Candidate 送受信をログ出力"]
  fix4["ファイアウォール設定を変更<br/>ネットワーク管理者に相談"]
  fix5["TURN サーバーを導入<br/>Symmetric NAT 対策"]

  success["接続成功"]

  start --> step1
  step1 -->|NO| fix1
  step1 -->|YES| step2
  step2 -->|NO| step4
  step2 -->|YES| step3
  step3 -->|NO| fix3
  step3 -->|YES| success
  step4 -->|NO| fix4
  step4 -->|YES| step5
  step5 -->|NO| fix5
  step5 -->|YES| success

  fix1 --> success
  fix2 --> success
  fix3 --> success
  fix4 --> success
  fix5 --> success

フローチャートの活用ポイント:

  • 上から順に確認することで、5 分以内に原因を特定できる
  • 各判断ノードで診断ツールのログを参照する
  • 修正後は必ず接続テストを実行する

よくあるエラーコードと対処法

WebRTC の接続トラブルで遭遇しやすいエラーと、その解決方法を整理しましょう。

エラー 1:ICE failed, add a STUN server

エラーメッセージ:

textError: ICE failed, add a STUN server and see about:webrtc for more details

発生条件:

  • STUN サーバーが設定されていない
  • ICE Candidate が 1 つも収集されない

解決方法:

  1. RTCConfiguration に STUN サーバーを追加する
typescriptconst configuration: RTCConfiguration = {
  iceServers: [{ urls: 'stun:stun.l.google.com:19302' }],
};
  1. ブラウザの about:webrtc または chrome:​/​​/​webrtc-internals​/​ で詳細を確認する

  2. 複数の STUN サーバーを設定して冗長化する

typescriptconst configuration: RTCConfiguration = {
  iceServers: [
    { urls: 'stun:stun.l.google.com:19302' },
    { urls: 'stun:stun1.l.google.com:19302' },
    { urls: 'stun:stun.cloudflare.com:3478' },
  ],
};

エラー 2:Failed to execute 'addIceCandidate'

エラーメッセージ:

textDOMException: Failed to execute 'addIceCandidate' on 'RTCPeerConnection':
Error processing ICE candidate

発生条件:

  • リモート記述(Remote Description)が設定される前に ICE Candidate を追加しようとした
  • 無効な Candidate フォーマット

解決方法:

  1. リモート記述の設定後に Candidate を追加する
typescript// 正しい順序
await peerConnection.setRemoteDescription(
  remoteDescription
);

// その後に Candidate を追加
await peerConnection.addIceCandidate(candidate);
  1. Candidate を一時的にキューに保存する実装
typescript// Candidate キューの実装
class CandidateQueue {
  private queue: RTCIceCandidate[] = [];
  private remoteDescriptionSet = false;

  constructor(private pc: RTCPeerConnection) {}

  async setRemoteDescription(
    desc: RTCSessionDescriptionInit
  ) {
    await this.pc.setRemoteDescription(desc);
    this.remoteDescriptionSet = true;

    // キューに溜まった Candidate を追加
    await this.flushQueue();
  }

  async addIceCandidate(candidate: RTCIceCandidate) {
    if (this.remoteDescriptionSet) {
      // すぐに追加
      await this.pc.addIceCandidate(candidate);
    } else {
      // キューに保存
      this.queue.push(candidate);
    }
  }

  private async flushQueue() {
    for (const candidate of this.queue) {
      await this.pc.addIceCandidate(candidate);
    }
    this.queue = [];
  }
}

使用例を示します。

typescript// 使用例
const queue = new CandidateQueue(peerConnection);

// シグナリングから受信した Candidate を追加
signalingSocket.addEventListener(
  'message',
  async (event) => {
    const message = JSON.parse(event.data);

    if (message.type === 'answer') {
      await queue.setRemoteDescription(message.answer);
    }

    if (
      message.type === 'ice-candidate' &&
      message.candidate
    ) {
      await queue.addIceCandidate(
        new RTCIceCandidate(message.candidate)
      );
    }
  }
);

エラー 3:NetworkError when attempting to fetch resource

エラーメッセージ:

textNetworkError: A network error occurred while attempting to fetch resource

発生条件:

  • HTTPS ではないページで WebRTC を使用している(一部ブラウザ)
  • シグナリングサーバーに接続できない

解決方法:

  1. HTTPS でページを提供する
javascript// 開発環境で HTTPS を有効化(Next.js の例)
// package.json
{
  "scripts": {
    "dev": "next dev --experimental-https"
  }
}
  1. ローカル開発では localhost を使用する(HTTP でも許可される)

  2. シグナリングサーバーの接続確認

typescript// WebSocket 接続のエラーハンドリング
const socket = new WebSocket('wss://your-server.com');

socket.addEventListener('error', (event) => {
  console.error('WebSocket エラー:', event);
  console.log(
    '対策: サーバーの URL とポートを確認してください'
  );
});

socket.addEventListener('close', (event) => {
  console.warn('WebSocket 切断:', event.code, event.reason);
});

エラーコード対応表:

エラーコード意味主な原因対処法
1000正常切断正常な接続終了対応不要
1006異常切断ネットワーク障害再接続処理を実装
1008ポリシー違反認証失敗など認証情報を確認
1011サーバーエラーサーバー側の問題サーバーログを確認

まとめ

WebRTC の「connecting」状態から進まない問題は、ICE プロセスの失敗が主な原因です。本記事では、5 分で原因を切り分けるための実践的な手順をご紹介しました。

重要なポイントを振り返りましょう:

  1. ICE 状態のログ取得iceConnectionStateiceGatheringState を監視する
  2. Candidate の収集確認hostsrflxrelay の 3 種類が収集されているか確認する
  3. STUN/TURN 設定確認:URL のスペルミスや認証情報の誤りをチェックする
  4. シグナリングの確認:Candidate の送受信が正しく行われているか検証する
  5. ネットワーク確認:ファイアウォールや NAT の種類を確認し、必要なら TURN サーバーを導入する

本記事で紹介した診断ツールを活用すれば、問題の原因を素早く特定できるでしょう。特に Symmetric NAT 環境では、TURN サーバーの導入が必須となるケースが多いことを覚えておいてください。

WebRTC のトラブルシューティングは慣れるまで難しく感じるかもしれませんが、段階的に確認していけば必ず原因にたどり着けますよ。

関連リンク