WebRTC と WebSocket を使ったローカル画面共有アプリ開発

WebRTC と WebSocket を使ったローカル画面共有アプリ開発

同一ネットワーク内でリアルタイム画面共有を行うWebアプリケーションの実装方法を説明していきます

WebRTC #画面共有#Socket#WebRTC

WebRTC と WebSocket を使ったローカル画面共有アプリ開発

サムネイル

同一ネットワーク内でリアルタイム画面共有を行うWebアプリケーションの実装方法を説明していきます

更新日: 8/9/2025

技術構成

  • WebRTC
  • WebSocket

WebRTCがメディアストリームの直接転送、WebSocketがシグナリング(接続の仲介)を処理。
このリポジトリを基に、実装のポイントや画面共有に関する技術を解説していきます。

アーキテクチャ

基本構造は、シグナリングサーバーとクライアントの2つに分かれます。シグナリングサーバーとは、WebRTCでピア同士が直接通信を開始するために必要な情報を中継するサーバーのことです。実際の画面データはクライアント間で直接やり取りされます。

サーバー構成

TypeScriptで実装されたサーバーは、Node.js + Express.js + Socket.IOで構築されています。HTTPとHTTPSの両プロトコルをサポートしますが、画面共有APIはセキュアコンテキストでのみ動作するため、HTTPS対応が必須です。

const httpServer = createServer(app);
let httpsServer: any = null;

// SSL証明書の確認
const sslCredentials = ensureSSLCertificates();
if (sslCredentials) {
  httpsServer = createHttpsServer(sslCredentials, app);
}

サーバーはユーザー管理と共有セッションの管理を行います。各ユーザーはSocket.IOの接続IDで識別され、共有セッションは視聴者のセットを持つ形です。

クライアント構成

クライアントサイドはScreenShareAppクラスとしてしておきます。
WebRTC接続の管理、Socket.IOイベントの処理、UIの更新を処理するように。

class ScreenShareApp {
  constructor() {
    this.socket = io();
    this.currentStream = null;
    this.peerConnections = new Map();
    this.viewerConnection = null;
    this.currentViewingHost = null;
  }
}

画面共有機能の実装

画面共有機能の中核は、ブラウザのScreen Capture APIを使用した画面キャプチャです。ユーザーの許可を得て画面をキャプチャし、それをWebRTC経由で他のユーザーに送信します。

画面キャプチャの開始

async startSharing() {
  try {
    this.currentStream = await navigator.mediaDevices.getDisplayMedia({
      video: true,
      audio: false
    });

    // 共有終了の検知
    this.currentStream.getVideoTracks()[0].addEventListener('ended', () => {
      this.stopSharing();
    });

    this.socket.emit("start-sharing", { shareType: "screen" });
    this.displayLocalStream();
    
  } catch (error) {
    this.handleScreenShareError(error);
  }
}
ISSUE - 課題

画面共有APIは、HTTPS環境でのみ動作します。ローカルIPアドレスでアクセスする場合は、HTTPS接続が必須です。

WebRTC接続

WebRTC接続は、視聴者と共有者の間でオファーとアンサーをやり取りすることで作成されます。また、NAT越えのために必要な接続情報(ICE情報)も交換します。
ちなみにNAT越えとは、異なるプライベートネットワーク間で直接通信を行うための技術で、STUNサーバーを使って自分のパブリックIPアドレスを取得し、相手に伝えることで実現します。

this.viewerConnection = new RTCPeerConnection({
  iceServers: [
    { urls: "stun:stun.l.google.com:19302" },
    { urls: "stun:stun1.l.google.com:19302" },
    { urls: "stun:stun.cloudflare.com:3478" }
  ],
  iceCandidatePoolSize: 10
});

※念の為複数のSTUNサーバーを設定。

セキュリティとSSL証明書

HTTPSでアクセスするためには、SSL証明書が必要です。開発環境では自己署名証明書を使用しますが、そのための生成スクリプトも用意されています。

SSL証明書の生成

証明書生成スクリプトは、OpenSSLを使用してローカル開発用の証明書を作成します。ローカルIPアドレスを自動検出し、適切な設定で証明書を生成します。

// ローカルIPアドレスの取得
const localIP = getLocalIPAddress();

// OpenSSL設定の作成
const sanConfig = `[req]
distinguished_name = req_distinguished_name
req_extensions = v3_req
prompt = no

[v3_req]
subjectAltName = @alt_names

[alt_names]
DNS.1 = localhost
IP.1 = 127.0.0.1
IP.2 = ${localIP}`;

生成された証明書は365日間有効で、localhost、127.0.0.1、およびローカルIPアドレスでのアクセス ができるようになります。

自己署名証明書を使用する場合、ブラウザで証明書の警告が表示されます。開発環境では「詳細設定」から「安全でないページに移動」を選択して続行します。

エラーハンドリングと接続状態

WebRTC接続はたびたび不安定になることがあるため、エラーハンドリングと状態監視が必要になります。

接続状態

接続状態は、WebRTCのconnectionStateプロパティで監視できます。開発中はこのプロパティを元に状態を把握できた方がいいのでログで吐き出しておきます。

this.viewerConnection.onconnectionstatechange = () => {
  if (this.viewerConnection.connectionState === "connected") {
    console.debug("WebRTC接続を開始しました");
  } else if (this.viewerConnection.connectionState === "failed") {
    console.error("WebRTC接続に失敗しました");
    this.showError("接続に失敗しました。再試行してください。");
  }
};

エラーの種類と対処

画面共有で発生する主なエラーとその対処法が以下。それぞれのエラーは、ユーザーの操作やブラウザの制限に起因することが多いので、問題切り分けのためにざっくりとでも理解しておきましょう。

エラー名 原因 対処方法
NotAllowedError ユーザーが画面共有を拒否 ユーザーに再度許可を求める
NotSupportedError ブラウザが非対応 対応ブラウザの案内を表示
NotFoundError 共有可能な画面なし エラーメッセージを表示

パフォーマンス

画面共有は大量のデータを転送するため、パフォーマンスについても考えましょう。
解像度やフレームレートを明確に制限しておきます。

解像度とフレームレートの制限

const stream = await navigator.mediaDevices.getDisplayMedia({
  video: {
    width: { ideal: 1920, max: 1920 },
    height: { ideal: 1080, max: 1080 },
    frameRate: { ideal: 30, max: 30 }
  },
  audio: false
});

フレームレートを30fpsに制限することで、帯域幅を節約します。60fpsで送信する場合と比較して、データ量が半分になるためです。例えば、フルHD画面を非圧縮で送信する場合、60fpsでは約3Gbps必要ですが、30fpsなら約1.5Gbpsで済みます。WebRTCは自動的に圧縮を行いますが、元のデータ量が少ないほど、ネットワークへの負荷も軽減されます。

複数視聴者への対応

一人の共有者に対して複数の視聴者が接続する場合、共有者は各視聴者に対して個別のピア接続を作成します。
視聴者同士が影響し合うことなく、安定した視聴にするために必要な設計です。

// 各視聴者に対してトラックを追加
this.currentStream.getTracks().forEach(track => {
  pc.addTrack(track, this.currentStream);
});

// ピア接続を保存
this.peerConnections.set(data.viewerId, pc);

トラブルシューティング

開発や利用時に遭遇する可能性のある問題と、その解決方法をまとめています。多くの問題は、ネットワーク設定やブラウザの制限に関連しています。

接続できない場合

接続の問題は、ネットワーク設定やファイアウォールが原因であることが多いです。以下の点を順番に確認してください。

  • 同じWiFiネットワークに接続しているか
  • HTTPSでアクセスしているか(HTTPでは画面共有APIが動作しません)
  • ファイアウォールがポート3000と3001をブロックしていないか
  • SSL証明書の警告を受け入れたか

映像が表示されない場合

映像が表示されない問題は、WebRTC接続の失敗やブラウザの互換性に起因することがあります。

  • ブラウザの開発者ツールでコンソールエラーを確認
  • WebRTC接続状態をデバッグ情報で確認(window.debugApp()を実行)
  • 別のブラウザで試す(Chrome、Firefox、Edgeで動作確認済み)

まとめ

WebRTCとWebSocketを組み合わせて同一ネットワーク間での画面共有機能を実装しましたが、構造を理解すれば拡張しやすいかと思います。

また、画面共有機能をつける場合、必ずと言っていいほど発生する要望や要件に「全画面モード」の切り替えがあります。ユーザビリティ向上のために、優先度高めで実装しましょう。
個人的には拡張性を考慮してブラウザの全画面APIは使いません。可能であればCSSによる実装にしましょう。

検索

検索条件に一致する記事が見つかりませんでした