T-CREATOR

WebRTC 入門:ブラウザだけで始めるリアルタイム通信

WebRTC 入門:ブラウザだけで始めるリアルタイム通信

WebRTC 入門:ブラウザだけで始めるリアルタイム通信

現代の Web 開発では、ユーザー体験の向上が最重要課題となっています。特に、リアルタイムでの情報共有や双方向コミュニケーションは、多くの Web アプリケーションで求められる機能です。

従来のリアルタイム通信では、複雑なサーバー構築やプラグインの導入が必要でしたが、WebRTC(Web Real-Time Communication)の登場により、これらの課題が大幅に改善されました。WebRTC を使用することで、開発者はブラウザの標準機能だけを使って、音声・動画通話やデータ共有を実現できるようになりました。

この記事では、WebRTC の基礎知識から実践的な実装方法まで、初心者の方にもわかりやすく解説いたします。

背景

リアルタイム通信の進化

インターネットが普及し始めた 1990 年代から 2000 年代初頭にかけて、リアルタイム通信は限られた技術でのみ実現可能でした。当時の主な手法をまとめると以下のようになります。

#技術特徴制約
1Flash豊富な機能プラグイン必須、セキュリティ問題
2Java AppletクロスプラットフォームJRE 必須、重い動作
3ActiveXWindows 統合Windows 限定、セキュリティリスク
4Ajax/Comet軽量疑似リアルタイム、サーバー負荷大

これらの技術は、それぞれ優れた特徴を持っていましたが、プラットフォーム依存やセキュリティの問題、そして何よりユーザーが追加のソフトウェアをインストールする必要があるという大きな課題がありました。

mermaidflowchart LR
  browser[ブラウザ] -->|プラグイン必要| plugin[Flash/Java]
  plugin -->|HTTP通信| server[Webサーバー]
  server -->|中継| other[他のクライアント]

  style plugin fill:#ffcccc
  style server fill:#ffffcc

上図は従来の通信方式を示しており、すべての通信がサーバーを経由する必要がありました。これにより、サーバー負荷が増大し、レイテンシーの問題も発生していました。

ブラウザ標準技術としての WebRTC

WebRTC は 2011 年に Google がオープンソースプロジェクトとして開始し、その後 W3C(World Wide Web Consortium)と IETF(Internet Engineering Task Force)によって標準化が進められました。

WebRTC の発展段階は以下のように整理できます:

mermaidstateDiagram-v2
  [*] --> Google開発: 2011年
  Google開発 --> 標準化開始: 2012年
  標準化開始 --> 主要ブラウザ対応: 2013-2017年
  主要ブラウザ対応 --> 標準仕様策定: 2021年
  標準仕様策定 --> 広域普及: 現在

この標準化プロセスにより、WebRTC は追加のプラグインを必要とせず、主要なモダンブラウザ(Chrome、Firefox、Safari、Edge)で直接動作するようになりました。

現在のブラウザ対応状況は次の表の通りです:

#ブラウザ対応開始バージョン対応状況
1Chrome23 (2012 年)完全対応
2Firefox22 (2013 年)完全対応
3Safari11 (2017 年)完全対応
4Edge12 (2015 年)完全対応

従来の通信方法との違い

WebRTC と従来の通信方法の最も重要な違いは、P2P(Peer-to-Peer)通信の実現です。従来の方式では、すべての通信がサーバーを経由していましたが、WebRTC では直接的なクライアント間通信が可能になりました。

mermaidflowchart TB
  subgraph "従来の方式"
    client1[クライアント A] -->|データ送信| server1[Webサーバー]
    server1 -->|データ転送| client2[クライアント B]
    client2 -->|応答| server1
    server1 -->|応答転送| client1
  end

  subgraph "WebRTC方式"
    clientA[クライアント A] <-->|直接通信| clientB[クライアント B]
    signal[シグナリングサーバー] -.->|接続確立のみ| clientA
    signal -.->|接続確立のみ| clientB
  end

この図からわかるように、WebRTC では接続確立時のみサーバーを使用し、実際のデータ通信は直接行われます。これにより、以下の利点が生まれます:

従来方式の課題点

  • サーバー経由による遅延の発生
  • 大量のデータ処理によるサーバー負荷
  • 同時接続数の制限
  • 帯域幅コストの増大

WebRTC 方式の利点

  • 低遅延の実現(P2P 通信)
  • サーバー負荷の大幅軽減
  • スケーラビリティの向上
  • 帯域幅コストの削減

課題

既存のリアルタイム通信の制約

従来のリアルタイム通信システムには、技術的・運用的な多くの制約が存在していました。これらの制約は、開発者にとって大きな負担となり、ユーザー体験の向上を阻む要因でもありました。

技術的制約

まず、最も大きな問題として、プラットフォーム依存の課題がありました。Flash ベースのアプリケーションでは、ユーザーが Flash Player をインストールし、定期的に更新する必要がありました。

javascript// Flash時代の典型的な実装例(参考)
var flashMovie = document.getElementById('flashApp');
if (flashMovie && flashMovie.sendMessage) {
  flashMovie.sendMessage('Hello World');
} else {
  alert('Flash Playerが必要です');
}

このようなコードでは、Flash Player の存在確認が常に必要でした。また、ブラウザや OS のアップデートにより、互換性の問題が頻繁に発生していました。

パフォーマンスの制約

従来の通信方式では、すべてのデータがサーバーを経由する必要があったため、以下のような性能問題が発生していました:

mermaidsequenceDiagram
    participant A as クライアント A
    participant S as サーバー
    participant B as クライアント B

    A->>S: メッセージ送信
    Note over S: サーバー処理<br/>(遅延発生)
    S->>B: メッセージ転送
    B->>S: 応答
    Note over S: サーバー処理<br/>(遅延発生)
    S->>A: 応答転送

この図が示すように、各通信でサーバー処理による遅延が発生し、リアルタイム性が損なわれていました。特に動画や音声のストリーミングでは、この遅延が致命的な問題となっていました。

プラグイン依存からの脱却

Adobe Flash Player の終了(2020 年 12 月)は、プラグイン依存の問題を象徴する出来事でした。多くの企業が、Flash 依存のシステムからの移行を余儀なくされ、大規模な開発リソースが必要となりました。

プラグイン依存の具体的問題

#問題カテゴリ具体的な影響対策コスト
1セキュリティ脆弱性の頻繁な発見
2メンテナンスプラグインの更新管理
3互換性OS アップデート時の動作不良
4ユーザビリティインストール手順の複雑さ
5モバイル対応iOS/Android での動作制限

これらの問題により、開発チームは継続的にプラグイン関連のトラブルシューティングに時間を費やす必要がありました。

javascript// プラグイン検出の典型的なコード例
function detectPlugin() {
  // Flash検出
  if (navigator.plugins && navigator.plugins.length > 0) {
    for (let i = 0; i < navigator.plugins.length; i++) {
      if (
        navigator.plugins[i].name.indexOf('Flash') !== -1
      ) {
        return 'flash';
      }
    }
  }

  // Java Applet検出
  if (navigator.javaEnabled && navigator.javaEnabled()) {
    return 'java';
  }

  return 'none';
}

// 使用例
var plugin = detectPlugin();
if (plugin === 'none') {
  showPluginInstallMessage();
}

このようなプラグイン検出コードは、各アプリケーションで実装する必要があり、コード品質の低下と保守性の悪化を招いていました。

サーバー負荷の問題

従来のリアルタイム通信では、サーバーがすべての通信を中継する必要があったため、サーバー負荷が大きな課題となっていました。

負荷増大の要因

mermaidflowchart TD
    users["同時接続ユーザー数"] -->|n人| connections["接続数: n×(n-1)/2"]
    connections --> server_load["サーバー負荷"]
    media["メディアデータ"] -->|帯域幅| server_load
    processing["リアルタイム処理"] -->|CPU使用率| server_load

    server_load --> cost["運用コスト増加"]
    server_load --> latency["遅延発生"]
    server_load --> scalability["スケーラビリティ限界"]

特に音声・動画通信では、データサイズが大きく、サーバーの帯域幅と CPU リソースを大量に消費していました。

例えば、10 人が参加するビデオ会議の場合:

javascript// 従来方式でのサーバー負荷計算例
const participantCount = 10;
const videoQuality = '720p'; // 約2Mbps
const totalBandwidth =
  participantCount * participantCount * 2; // Mbps

console.log(`必要サーバー帯域幅: ${totalBandwidth}Mbps`);
// 出力: 必要サーバー帯域幅: 200Mbps

このような負荷により、サーバーインフラのコストが急激に増加し、多くの企業にとって大きな運用負担となっていました。

図で理解できる要点

  • 従来の通信方式では全データがサーバーを経由し、遅延とコストが発生
  • プラグイン依存により互換性とセキュリティの問題が常在
  • 参加者数の増加に伴いサーバー負荷が指数的に増大

解決策

WebRTC の仕組み

WebRTC は、これらの課題を解決するために設計された革新的な技術です。その核となるのは、ブラウザ間で直接通信を行う P2P(Peer-to-Peer)アーキテクチャです。

WebRTC の 3 つの主要 API

WebRTC は、以下の 3 つの主要 API によって構成されています:

javascript// 1. MediaStream API - メディア取得
navigator.mediaDevices
  .getUserMedia({
    video: true,
    audio: true,
  })
  .then((stream) => {
    console.log('カメラ・マイクアクセス成功');
    localVideo.srcObject = stream;
  });
javascript// 2. RTCPeerConnection API - P2P接続
const peerConnection = new RTCPeerConnection({
  iceServers: [{ urls: 'stun:stun.l.google.com:19302' }],
});

// 接続状態の監視
peerConnection.onconnectionstatechange = () => {
  console.log('接続状態:', peerConnection.connectionState);
};
javascript// 3. RTCDataChannel API - データ通信
const dataChannel =
  peerConnection.createDataChannel('messages');

dataChannel.onopen = () => {
  console.log('データチャンネル開通');
  dataChannel.send('Hello WebRTC!');
};

WebRTC 通信フロー

WebRTC の通信確立プロセスは、以下のような流れで行われます:

mermaidsequenceDiagram
    participant A as ピア A
    participant S as シグナリングサーバー
    participant B as ピア B

    A->>A: getUserMedia()<br/>ローカルメディア取得
    B->>B: getUserMedia()<br/>ローカルメディア取得

    A->>A: createOffer()<br/>オファー生成
    A->>S: オファー送信
    S->>B: オファー転送

    B->>B: setRemoteDescription()<br/>オファー設定
    B->>B: createAnswer()<br/>アンサー生成
    B->>S: アンサー送信
    S->>A: アンサー転送

    A->>A: setRemoteDescription()<br/>アンサー設定

    Note over A,B: ICE Candidate交換
    A-->>S: ICE Candidate
    S-->>B: ICE Candidate
    B-->>S: ICE Candidate
    S-->>A: ICE Candidate

    Note over A,B: P2P接続確立
    A-->>B: 直接メディア通信開始
    B-->>A: 直接メディア通信開始

このフローにより、初期の接続確立時のみサーバーを使用し、実際の音声・動画データは直接やり取りされます。

P2P 通信の利点

P2P 通信の採用により、従来の課題が劇的に改善されました。

レイテンシーの改善

mermaidflowchart LR
    subgraph "従来方式"
        A1[ピア A] -->|50ms| S1[サーバー]
        S1 -->|50ms| B1[ピア B]
        note1["総遅延: 100ms"]
    end

    subgraph "WebRTC方式"
        A2[ピア A] <-->|30ms| B2[ピア B]
        note2["総遅延: 30ms"]
    end

実際の測定値に基づく比較データ:

#通信方式平均遅延改善率
1従来方式(サーバー経由)100-200ms-
2WebRTC(P2P 直接)30-50ms70%改善

サーバー負荷の削減

javascript// 従来方式でのサーバー負荷計算
function calculateServerLoad(users, quality) {
  const bitratePerUser = {
    '480p': 1, // 1Mbps
    '720p': 2, // 2Mbps
    '1080p': 4, // 4Mbps
  };

  // 各ユーザーが他全員にデータを送信
  const totalLoad =
    users * (users - 1) * bitratePerUser[quality];
  return totalLoad;
}

console.log(
  '従来方式 10人720p:',
  calculateServerLoad(10, '720p'),
  'Mbps'
);
// 出力: 従来方式 10人720p: 180 Mbps
javascript// WebRTC方式でのサーバー負荷(シグナリングのみ)
function calculateWebRTCServerLoad(users) {
  // シグナリングデータのみ(通常1KB以下のJSON)
  const signalingPerConnection = 0.001; // 1KB = 0.001Mbps相当
  const connections = (users * (users - 1)) / 2;
  return connections * signalingPerConnection;
}

console.log(
  'WebRTC方式 10人:',
  calculateWebRTCServerLoad(10),
  'Mbps'
);
// 出力: WebRTC方式 10人: 0.045 Mbps

この計算からわかるように、WebRTC では 99%以上のサーバー負荷削減が実現されます。

ブラウザネイティブサポート

WebRTC の最大の利点の一つは、追加のプラグインやソフトウェアを必要とせず、ブラウザの標準機能として動作することです。

標準 API の利用

javascript// プラグイン検出不要、標準APIを直接使用
async function startWebRTC() {
  // 機能サポート確認
  if (
    !navigator.mediaDevices ||
    !window.RTCPeerConnection
  ) {
    console.error('WebRTC未対応ブラウザです');
    return;
  }

  try {
    // カメラ・マイクへのアクセス
    const stream =
      await navigator.mediaDevices.getUserMedia({
        video: { width: 640, height: 480 },
        audio: {
          echoCancellation: true,
          noiseSuppression: true,
        },
      });

    console.log('メディアストリーム取得成功');
    return stream;
  } catch (error) {
    console.error('メディアアクセスエラー:', error.message);
  }
}

クロスプラットフォーム対応

WebRTC の標準化により、以下のプラットフォームで共通のコードが動作します:

javascript// 統一されたWebRTC実装(プラットフォーム差異を吸収)
class WebRTCManager {
  constructor() {
    // ブラウザ差異の吸収
    this.RTCPeerConnection =
      window.RTCPeerConnection ||
      window.mozRTCPeerConnection ||
      window.webkitRTCPeerConnection;

    this.getUserMedia =
      navigator.mediaDevices?.getUserMedia ||
      navigator.getUserMedia ||
      navigator.mozGetUserMedia ||
      navigator.webkitGetUserMedia;
  }

  async createConnection() {
    if (!this.RTCPeerConnection) {
      throw new Error('RTCPeerConnection未対応');
    }

    const connection = new this.RTCPeerConnection({
      iceServers: [
        { urls: 'stun:stun.l.google.com:19302' },
      ],
    });

    return connection;
  }
}

セキュリティの向上

WebRTC は、セキュリティを重視した設計となっており、以下の保護機能が標準で提供されます:

javascript// 暗号化は自動的に適用される
const connection = new RTCPeerConnection({
  iceServers: [{ urls: 'stun:stun.l.google.com:19302' }],
});

// すべてのメディア通信は自動的にDTLS-SRTPで暗号化
connection.oniceconnectionstatechange = () => {
  if (connection.iceConnectionState === 'connected') {
    console.log('暗号化されたP2P接続が確立されました');
  }
};

図で理解できる要点

  • WebRTC は 3 つの主要 API により構成され、段階的な通信確立を行う
  • P2P 通信により遅延を 70%改善し、サーバー負荷を 99%削減
  • ブラウザ標準機能として動作し、追加インストールが不要

具体例

基本的な WebRTC 接続の実装

実際に WebRTC を使用した最小構成の実装を段階的に見ていきましょう。まずは、2 つのピア間での基本的な接続確立から始めます。

HTML 構造の準備

html<!DOCTYPE html>
<html lang="ja">
  <head>
    <meta charset="UTF-8" />
    <meta
      name="viewport"
      content="width=device-width, initial-scale=1.0"
    />
    <title>WebRTC 基本実装</title>
  </head>
  <body>
    <h1>WebRTC ビデオチャット</h1>

    <div class="video-container">
      <video id="localVideo" autoplay muted></video>
      <video id="remoteVideo" autoplay></video>
    </div>

    <div class="controls">
      <button id="startButton">通話開始</button>
      <button id="callButton" disabled>発信</button>
      <button id="hangupButton" disabled>切断</button>
    </div>
  </body>
</html>

RTCPeerConnection の初期化

javascript// WebRTC設定
const configuration = {
  iceServers: [
    { urls: 'stun:stun.l.google.com:19302' },
    { urls: 'stun:stun1.l.google.com:19302' },
  ],
};

let localPeerConnection = null;
let remotePeerConnection = null;
let localStream = null;

ここでは、STUN(Session Traversal Utilities for NAT)サーバーを設定しています。STUN サーバーは、NAT やファイアウォールの背後にあるデバイスの外部 IP アドレスを発見するために使用されます。

メディアストリーム取得

javascript// カメラとマイクへのアクセス
async function startLocalVideo() {
  try {
    const stream =
      await navigator.mediaDevices.getUserMedia({
        video: {
          width: { ideal: 640 },
          height: { ideal: 480 },
          frameRate: { ideal: 30 },
        },
        audio: {
          echoCancellation: true,
          noiseSuppression: true,
          autoGainControl: true,
        },
      });

    localStream = stream;
    const localVideo =
      document.getElementById('localVideo');
    localVideo.srcObject = stream;

    console.log('ローカルストリーム取得成功');
    document.getElementById('callButton').disabled = false;
  } catch (error) {
    console.error('メディアアクセスエラー:', error);
    alert('カメラまたはマイクへのアクセスが拒否されました');
  }
}

このコードでは、ユーザーのカメラとマイクにアクセスし、取得したストリームをローカルビデオ要素に表示しています。

ピア接続の確立

javascript// ピア接続作成(発信側)
async function createPeerConnection() {
  localPeerConnection = new RTCPeerConnection(
    configuration
  );

  // ローカルストリームを接続に追加
  localStream.getTracks().forEach((track) => {
    localPeerConnection.addTrack(track, localStream);
  });

  // リモートストリーム受信時の処理
  localPeerConnection.ontrack = (event) => {
    console.log('リモートストリーム受信');
    const remoteVideo =
      document.getElementById('remoteVideo');
    remoteVideo.srcObject = event.streams[0];
  };

  // ICE Candidate生成時の処理
  localPeerConnection.onicecandidate = (event) => {
    if (event.candidate) {
      console.log('ICE Candidate:', event.candidate);
      // 実際の実装では、シグナリングサーバー経由で送信
      remotePeerConnection.addIceCandidate(event.candidate);
    }
  };

  return localPeerConnection;
}

オファー・アンサー交換

javascript// 発信処理(オファー作成)
async function makeCall() {
  await createPeerConnection();

  try {
    // オファーを作成
    const offer = await localPeerConnection.createOffer({
      offerToReceiveVideo: true,
      offerToReceiveAudio: true,
    });

    // ローカルディスクリプションに設定
    await localPeerConnection.setLocalDescription(offer);
    console.log('オファー作成完了');

    // 相手側での処理をシミュレート
    await handleOffer(offer);
  } catch (error) {
    console.error('オファー作成エラー:', error);
  }
}
javascript// 着信処理(アンサー作成)
async function handleOffer(offer) {
  // リモートピア接続作成
  remotePeerConnection = new RTCPeerConnection(
    configuration
  );

  // ローカルストリームを追加
  localStream.getTracks().forEach((track) => {
    remotePeerConnection.addTrack(track, localStream);
  });

  // ICE Candidate処理
  remotePeerConnection.onicecandidate = (event) => {
    if (event.candidate) {
      localPeerConnection.addIceCandidate(event.candidate);
    }
  };

  try {
    // リモートオファーを設定
    await remotePeerConnection.setRemoteDescription(offer);

    // アンサーを作成
    const answer =
      await remotePeerConnection.createAnswer();
    await remotePeerConnection.setLocalDescription(answer);

    // 発信側にアンサーを送信
    await localPeerConnection.setRemoteDescription(answer);

    console.log('接続確立完了');
  } catch (error) {
    console.error('アンサー処理エラー:', error);
  }
}

音声通話アプリの作成

次に、音声のみに特化したシンプルな通話アプリケーションを作成してみましょう。

音声専用の実装

javascriptclass VoiceCallApp {
    constructor() {
        this.localConnection = null;
        this.localStream = null;
        this.isCallActive = false;

        this.initializeUI();
    }

    // UI要素の初期化
    initializeUI() {
        this.callButton = document.getElementById('callButton');
        this.endCallButton = document.getElementById('endCallButton');
        this.statusDisplay = document.getElementById('status');

        this.callButton.addEventListener('click', () => this.startCall());
        this.endCallButton.addEventListener('click', () => this.endCall());
    }

    // 音声通話開始
    async startCall() {
        try {
            this.updateStatus('マイクへアクセス中...');

            // 音声のみ取得
            this.localStream = await navigator.mediaDevices.getUserMedia({
                audio: {
                    echoCancellation: true,
                    noiseSuppression: true,
                    autoGainControl: true
                },
                video: false
            });

            this.updateStatus('通話準備完了');
            await this.createConnection();

        } catch (error) {
            console.error('音声通話開始エラー:', error);
            this.updateStatus('エラー: ' + error.message);
        }
    }

音声品質の制御

javascript    // 音声品質設定
    async createConnection() {
        this.localConnection = new RTCPeerConnection({
            iceServers: [{ urls: 'stun:stun.l.google.com:19302' }]
        });

        // 音声トラックの追加と設定
        const audioTrack = this.localStream.getAudioTracks()[0];
        const sender = this.localConnection.addTrack(audioTrack, this.localStream);

        // 音声コーデックの設定
        const transceiver = this.localConnection.getTransceivers()
            .find(t => t.sender === sender);

        if (transceiver) {
            const capabilities = RTCRtpSender.getCapabilities('audio');
            const opusCodec = capabilities.codecs.find(codec =>
                codec.mimeType.toLowerCase() === 'audio/opus'
            );

            if (opusCodec) {
                await transceiver.setCodecPreferences([opusCodec]);
                console.log('Opusコーデック設定完了');
            }
        }

        this.setupConnectionHandlers();
    }

接続状態の監視

javascript    // 接続イベントハンドラー設定
    setupConnectionHandlers() {
        // 接続状態変化の監視
        this.localConnection.onconnectionstatechange = () => {
            const state = this.localConnection.connectionState;
            console.log('接続状態:', state);

            switch (state) {
                case 'connecting':
                    this.updateStatus('接続中...');
                    break;
                case 'connected':
                    this.updateStatus('通話中');
                    this.isCallActive = true;
                    this.callButton.disabled = true;
                    this.endCallButton.disabled = false;
                    break;
                case 'disconnected':
                    this.updateStatus('接続切断');
                    this.resetCall();
                    break;
                case 'failed':
                    this.updateStatus('接続失敗');
                    this.resetCall();
                    break;
            }
        };

        // 音声レベルの監視
        this.monitorAudioLevel();
    }

音声レベル監視機能

javascript    // 音声レベル監視
    async monitorAudioLevel() {
        if (!this.localStream) return;

        const audioContext = new AudioContext();
        const analyser = audioContext.createAnalyser();
        const microphone = audioContext.createMediaStreamSource(this.localStream);

        microphone.connect(analyser);
        analyser.fftSize = 256;

        const dataArray = new Uint8Array(analyser.frequencyBinCount);

        const checkAudioLevel = () => {
            if (this.isCallActive) {
                analyser.getByteFrequencyData(dataArray);

                // 音声レベル計算
                const average = dataArray.reduce((a, b) => a + b) / dataArray.length;
                const level = Math.round((average / 255) * 100);

                // UI更新
                const levelDisplay = document.getElementById('audioLevel');
                if (levelDisplay) {
                    levelDisplay.style.width = level + '%';
                    levelDisplay.style.backgroundColor =
                        level > 50 ? '#4CAF50' : level > 20 ? '#FFC107' : '#F44336';
                }

                requestAnimationFrame(checkAudioLevel);
            }
        };

        checkAudioLevel();
    }

    // 状態表示の更新
    updateStatus(message) {
        if (this.statusDisplay) {
            this.statusDisplay.textContent = message;
        }
        console.log('Status:', message);
    }
}

// アプリケーション初期化
document.addEventListener('DOMContentLoaded', () => {
    new VoiceCallApp();
});

画面共有機能の実装

WebRTC では、カメラ映像だけでなく、ユーザーの画面を共有することも可能です。この機能を実装してみましょう。

画面キャプチャ API の使用

javascriptclass ScreenShareApp {
    constructor() {
        this.peerConnection = null;
        this.localStream = null;
        this.screenStream = null;
        this.isScreenSharing = false;

        this.initializeControls();
    }

    // コントロール初期化
    initializeControls() {
        const shareButton = document.getElementById('shareScreenButton');
        const stopShareButton = document.getElementById('stopShareButton');

        shareButton.addEventListener('click', () => this.startScreenShare());
        stopShareButton.addEventListener('click', () => this.stopScreenShare());
    }

    // 画面共有開始
    async startScreenShare() {
        try {
            // 画面キャプチャの取得
            this.screenStream = await navigator.mediaDevices.getDisplayMedia({
                video: {
                    cursor: 'always', // カーソルを含める
                    displaySurface: 'monitor' // モニター全体
                },
                audio: {
                    echoCancellation: true,
                    noiseSuppression: true
                }
            });

            console.log('画面共有ストリーム取得成功');

            // ローカル表示
            const localVideo = document.getElementById('localVideo');
            localVideo.srcObject = this.screenStream;

            // 画面共有停止の検出
            this.screenStream.getVideoTracks()[0].onended = () => {
                console.log('画面共有が停止されました');
                this.stopScreenShare();
            };

            this.isScreenSharing = true;
            this.updateShareButtons();

            // WebRTC接続がある場合はストリームを更新
            await this.replaceVideoTrack();

        } catch (error) {
            console.error('画面共有開始エラー:', error);
            alert('画面共有を開始できませんでした: ' + error.message);
        }
    }

ビデオトラックの動的置換

javascript    // ビデオトラックの置き換え
    async replaceVideoTrack() {
        if (!this.peerConnection || !this.screenStream) return;

        try {
            const videoSender = this.peerConnection.getSenders()
                .find(sender => sender.track && sender.track.kind === 'video');

            if (videoSender) {
                const newVideoTrack = this.screenStream.getVideoTracks()[0];
                await videoSender.replaceTrack(newVideoTrack);
                console.log('ビデオトラック置換完了');
            }

        } catch (error) {
            console.error('ビデオトラック置換エラー:', error);
        }
    }

    // 画面共有停止
    async stopScreenShare() {
        if (this.screenStream) {
            // 全トラックを停止
            this.screenStream.getTracks().forEach(track => {
                track.stop();
            });
            this.screenStream = null;
        }

        this.isScreenSharing = false;
        this.updateShareButtons();

        // カメラ映像に戻す
        await this.returnToCameraVideo();
    }

    // カメラ映像に復帰
    async returnToCameraVideo() {
        try {
            // カメラストリームを再取得
            const cameraStream = await navigator.mediaDevices.getUserMedia({
                video: { width: 640, height: 480 },
                audio: true
            });

            this.localStream = cameraStream;

            // ローカル表示更新
            const localVideo = document.getElementById('localVideo');
            localVideo.srcObject = cameraStream;

            // WebRTC接続のトラックを更新
            if (this.peerConnection) {
                const videoSender = this.peerConnection.getSenders()
                    .find(sender => sender.track && sender.track.kind === 'video');

                if (videoSender) {
                    const newVideoTrack = cameraStream.getVideoTracks()[0];
                    await videoSender.replaceTrack(newVideoTrack);
                }
            }

            console.log('カメラ映像に復帰完了');

        } catch (error) {
            console.error('カメラ復帰エラー:', error);
        }
    }

画面共有の品質制御

javascript    // 画面共有品質の動的調整
    async adjustScreenShareQuality() {
        if (!this.screenStream || !this.peerConnection) return;

        const videoSender = this.peerConnection.getSenders()
            .find(sender => sender.track && sender.track.kind === 'video');

        if (videoSender) {
            const params = videoSender.getParameters();

            // エンコーディングパラメータの調整
            if (params.encodings && params.encodings.length > 0) {
                params.encodings[0].maxBitrate = 2000000; // 2Mbps
                params.encodings[0].maxFramerate = 15;    // 15fps

                try {
                    await videoSender.setParameters(params);
                    console.log('画面共有品質調整完了');
                } catch (error) {
                    console.error('品質調整エラー:', error);
                }
            }
        }
    }

    // UI更新
    updateShareButtons() {
        const shareButton = document.getElementById('shareScreenButton');
        const stopShareButton = document.getElementById('stopShareButton');

        shareButton.disabled = this.isScreenSharing;
        stopShareButton.disabled = !this.isScreenSharing;

        const statusText = this.isScreenSharing ? '画面共有中' : '準備完了';
        document.getElementById('shareStatus').textContent = statusText;
    }
}

完整な画面共有フロー

mermaidsequenceDiagram
    participant U as ユーザー
    participant B as ブラウザ
    participant A as アプリ
    participant P as ピア接続

    U->>A: 画面共有ボタンクリック
    A->>B: getDisplayMedia() 呼び出し
    B->>U: 共有画面選択ダイアログ
    U->>B: 画面選択・許可
    B->>A: ScreenStream取得
    A->>P: replaceTrack() でビデオ変更
    P->>P: リモートピアに新しいストリーム送信

    Note over A: 画面共有中

    U->>A: 共有停止ボタンクリック
    A->>B: track.stop() 呼び出し
    A->>B: getUserMedia() でカメラ再取得
    A->>P: replaceTrack() でカメラに戻す
    P->>P: リモートピアにカメラストリーム送信

図で理解できる要点

  • WebRTC 接続は段階的な実装により、メディア取得・接続確立・通信を順次行う
  • 音声通話では品質制御と音声レベル監視により安定した通信を実現
  • 画面共有では動的なトラック置換により、カメラ映像と画面の切り替えが可能

まとめ

WebRTC は、ブラウザだけでリアルタイム通信を実現する革新的な技術です。従来のプラグイン依存やサーバー中心のアーキテクチャから脱却し、P2P 通信によって低遅延・高品質な通信を可能にしました。

WebRTC の主な利点

この記事を通じて明らかになった WebRTC の主要な利点を整理すると、以下のようになります:

#利点カテゴリ具体的効果従来比改善率
1遅延削減P2P 直接通信70%改善
2サーバー負荷シグナリングのみ99%削減
3開発効率プラグイン不要-
4セキュリティ標準暗号化-
5コスト削減インフラ費用大幅削減

特に注目すべきは、技術的な改善だけでなく、開発者の生産性向上と運用コストの大幅な削減を同時に実現している点です。

実装における重要なポイント

WebRTC を実際に導入する際の重要なポイントは以下の通りです:

技術的考慮事項

  • ブラウザサポートの確認(現在は主要ブラウザで完全対応)
  • STUN/TURN サーバーの適切な設定
  • エラーハンドリングの実装
  • 音声・映像品質の最適化

運用面での考慮事項

  • シグナリングサーバーの設計
  • NAT 越えの対応策
  • セキュリティポリシーの策定
  • モニタリングとログ収集

今後の発展性

WebRTC の技術は継続的に発展しており、以下の分野での活用が期待されています:

mermaidflowchart LR
    webrtc[WebRTC] --> education[教育・e-learning]
    webrtc --> healthcare[遠隔医療]
    webrtc --> business[ビジネス会議]
    webrtc --> gaming[リアルタイムゲーム]
    webrtc --> iot[IoT連携]
    webrtc --> ai[AI音声認識]

    education --> features1[画面共有・録画]
    healthcare --> features2[高品質映像・データ送信]
    business --> features3[多人数接続・セキュリティ]
    gaming --> features4[低遅延・データチャンネル]
    iot --> features5[軽量通信・デバイス連携]
    ai --> features6[音声解析・リアルタイム処理]

特に、AI 技術との組み合わせによって、リアルタイム翻訳や音声認識、感情解析などの高度な機能を持つアプリケーションの開発が加速すると予想されます。

学習の次のステップ

WebRTC の基礎を理解された方は、以下のステップで学習を進めることをおすすめします:

段階 1:基礎固め

  • MediaStream API の詳細理解
  • RTCPeerConnection の各種プロパティ
  • ICE プロセスの深い理解

段階 2:実践応用

  • 複数人会議システムの実装
  • メディアサーバー(SFU/MCU)の活用
  • モバイルブラウザでの最適化

段階 3:高度な実装

  • WebRTC Statistics API による通信品質監視
  • adaptive bitrate による帯域制御
  • E2E 暗号化の実装

WebRTC は、Web 開発において今後ますます重要になる技術です。この記事で紹介した基礎知識と実装例を参考に、ぜひ実際のプロジェクトでの活用にチャレンジしてみてください。

ブラウザだけで実現できるリアルタイム通信の可能性は無限大です。あなたのアイデアと WebRTC の技術力で、新しいコミュニケーション体験を創造していきましょう。

関連リンク