【JavaScript 応用講座】: [WebRTC/P2P] チャット&ファイル転送ができる双方向アプリを実装

🎯 目的と背景

前回の記事で、シグナリングサーバを作成しましたが、今回は、そのサーバを活用して、クライアント同士を接続して、チャットと、ファイル送信が出来るまで、実装していきます。

前回の記事。

🔧 技術スタック

  • JavaScript + HTML + CSS
  • simple-peer(WebRTCラッパー)
  • Node.js + WebSocket(Signaling Server)
  • ローカル環境(LiveServer)で2画面起動
  • Blobによるファイル受信

Signalingサーバ構築

  • Node.js + ws
  • Renderで無料ホスティング
  • on(“message”) で offer/answer を中継
  • ログ確認で送信内容を明確に出力

以下は通信ログのスクショ・サンプルです。

1. WebSocket通信でsignalを中継する

const signalingSocket = new WebSocket("wss://your-app.koyeb.app");

signalingSocket.onopen = () => {
  console.log("🌐 WebSocket接続完了!");
};

signalingSocket.onmessage = (event) => {
  const remoteSignal = JSON.parse(event.data);
  console.log("📥 signal受信:", remoteSignal);
  peer.signal(remoteSignal); // 相手のsignalを反映
};

🌐 サーバに2つのクライアントが繋がる流れ(イメージ)

P2P通信の流れsignaling-server使用:

   [Client A] initiator: true
     └─── signalオファー)──→
                              \
                               [signaling-server]
                              /
     ←─── signalアンサー)──┘
   [Client B] initiator: false
  1. Aが initiator: true でsignal(オファー)を発行
  2. signaling-server を通じて B に送信
  3. Bが peer.signal() で受信&応答(アンサー)
  4. Aが応答を受け取って P2P接続確立

signal発火時に WebSocket で送信

peer.on("signal", (data) => {
  if (signalingSocket.readyState === WebSocket.OPEN) {
    signalingSocket.send(JSON.stringify(data));
    console.log("📤 signal送信:", data);
  } else {
    console.warn("WebSocketがまだ開いていません");
  }
});

ERROR: “[object Blob]” is not valid JSON

Koyeb上のNode.js WebSocketサーバは ws.send(JSON.stringify(…)) で送信していても、 ブラウザ側の WebSocket 実装が MIME を検出して Blob として扱うことがある。

このようなケースでは .text() で明示的に文字列化が必要になります。

const text = await event.data.text(); // Blob → string
const signal = JSON.parse(text);      // string → object

WebSocket接続と、peer.signal(…) の遅延問題

🔁 解決策:一時保存して、接続完了後に送信

let pendingSignals = [];

peer.on("signal", (data) => {
  if (signalingSocket.readyState === WebSocket.OPEN) {
    signalingSocket.send(JSON.stringify(data));
    console.log("signal送信2");
  } else {
    pendingSignals.push(data); // 一時保存
    console.log("🕒 signalingSocket未接続、後で送信予定");
  }
});

signalingSocket.onopen = () => {
  console.log("🌐 signaling server へ接続成功");

  // 一時保存されていたシグナルを送る
  pendingSignals.forEach((data) => {
    signalingSocket.send(JSON.stringify(data));
    console.log("📤 遅延送信:", data);
  });
  pendingSignals = [];
};

🧠 なぜこの問題が起こるのか?

  • peer.signal(…) の呼び出しは早い段階で発火する。
  • でも WebSocket の接続は非同期で、ちょっと時間がかかる。
  • そのため「接続前に送信しようとして失敗」が起こる。

Uncaught TypeError: Cannot read properties of undefined (reading ‘readyState’)

🔍 原因

if (signalingSocket.readyState === WebSocket.OPEN)

この行で signalingSocket が未定義(undefined)なのに .readyState を読もうとして…

Uncaught TypeError: Cannot read properties of undefined (reading 'readyState')

という典型的なエラーになります。

🧠 つまり

  • まだ connectSignalingServer() が呼ばれていない
  • だから signalingSocket は 存在していない
  • peer.on(“signal”, …) は即時に走ることがあるため、順番が重要

✅ 対策(1行の修正)

if (signalingSocket && signalingSocket.readyState === WebSocket.OPEN)

✅ 理想的な順序

  • ユーザーが initiator / responder を決定
  • connectSignalingServer() を呼んで WebSocket を開く
  • そのあと createPeer(true or false) を呼び出す

この順序が最も安全です。
WebSocketが確実に開通してから peer を作るのがベスト。

📌 注意点

  • 同一Koyebサーバに複数ブラウザ(もしくは別端末)から接続しておくと、相互にsignalが届きP2P接続が成立します。
  • trickle: false にしているので、1回のsignal交換だけで完了します(ICE候補をまとめて送信)。

成功後のスクリーンショット


2. P2Pチャット通信

  • createPeer(true) / createPeer(false) でinitiator/receiver分離
  • signalingSocket未接続時にはsignal一時保存
  • P2P接続成功後に signal(data) → send
  • チャットは “あなた”, “相手” として表示切替

チャットに関しては、昨日の実装内容でそのまま動いた為、徳にコードは書いてないです。


3. ZIPファイル転送

  • <input type="file"> から読み込み → readAsArrayBuffer
  • DataChannelで送信(Uint8Arrayで分割)
  • 受信側で連結 → Blob化 → createObjectURL
  • ZIP受信成功ログとダウンロードリンク生成

これも、昨日の実装のまま動いたため特に変更は無いです。


🧪 難しかった点と解決策

  • signalingSocket未接続時の readyState エラー → 一時保存&後送信で対応
  • peer.on(“signal”) タイミングが難しい → ページ読み込み順を工夫
  • blob受信→再構成のロジックは先に簡単なテキストで検証してから導入

💬 感想とまとめ

サーバに繋がるまでに少し苦労しましたが、その後は、先日の実装コードがそのまま動いたため楽に実装出来ました。

手動で接続していた際は、signal情報を双方にコピペする必要がありとても面倒でしたが、サーバを経由する事で、その負担が無くなり凄く楽になりました。


user icon
signal情報の交換が不要になったので、凄く楽になりましたが、サーバ側ではどんな処理をしてるのでしょうか?

ai icon
いい視点です!実は「signal 情報の交換が不要になった」というのは 「ユーザーが手動で貼り付ける必要がなくなった」 という意味で、signal のやり取り自体はまだ必須です。
それを「代わりにやってくれている」のが、あなたの立てた Node.js+WebSocket のシグナリングサーバです。

🔁 サーバ側で何が行われているのか?

クライアント(ブラウザ)は peer.signal(...) により、「自分の接続情報」(SDPやICE候補など)を signal オブジェクトとして生成します。

それを WebSocket を通じてシグナリングサーバに送信。 サーバはそれを **「他の接続中のクライアントに中継」**します。


🧠 もっと具体的に言うと…

サーバコードの中でこんな流れになっています:

const peers = [];

wss.on('connection', (ws) => {
  peers.push(ws);

  ws.on('message', (data) => {
    // 受け取った signal 情報を他の全クライアントへ転送
    peers.forEach(client => {
      if (client !== ws && client.readyState === WebSocket.OPEN) {
        client.send(data);
      }
    });
  });

  ws.on('close', () => {
    // 切断時にクライアントリストから削除
    const index = peers.indexOf(ws);
    if (index !== -1) peers.splice(index, 1);
  });
});

☑ サーバがしていること(要点)

処理 内容
接続管理 接続してきたクライアントを peers 配列で管理
signal中継 一方のクライアントから受信した signal(JSON)を、他のクライアントに送信
切断処理 クライアントが切断したら、配列から除外

🔄 なぜこれで signal の貼り付けが不要になるのか?

以前の「手動方式」では:

  1. peer1.signal(...) で生成された JSON を
  2. 手動でコピー&ペーストして
  3. 相手側で peer2.signal(...) に渡す

という流れでした。

今は:

  1. peer1.signal(...) で生成された JSON を
  2. WebSocket 経由でサーバに送信
  3. サーバがそれを peer2 側へ中継 → peer.signal(...) で自動的に処理!

という流れなので、signal の交換は相変わらず必須ですが、手間が自動化されただけなんです。

以前は signal 情報をコピペして相手に渡していたが、今はサーバが勝手に橋渡ししてくれる。

つまり、“P2Pの仲人役”をしてくれているのがシグナリングサーバです。

📎 参考・使用ライブラリ

関連リンク