2
3

WebRTC (Web Real-Time Communication)。ブラウザ標準機能でビデオ通話を実現。

Last updated at Posted at 2024-09-28

image.png

WebRTC (Web Real-Time Communication) は、リアルタイムで音声やビデオ、データ通信をブラウザ間で直接行うための技術であり、ブラウザに組み込まれた基本的な機能です。

WebRTCを使ったブラウザアプリケーションは、JavaScriptを利用して実装されます。
WebRTCのAPIは、音声やビデオのストリーミング、そしてデータ通信を扱うために設計されており、RTCPeerConnectionなどの主要なオブジェクトを使います。
シグナリングサーバーは別途必要(使いまわしが効きます)ですが、ブラウザ間でのリアルタイム通信はブラウザ自体の機能として提供されています。
つまり、JavaScriptを使って、非常にパワフルでリアルタイムな音声・ビデオ通信やデータ転送を実現できるのがWebRTCの大きな特徴です。

WebRTCの特長
ブラウザに組み込まれた標準機能: WebRTCはブラウザ自体が提供するAPIの一部であり、ブラウザが対応していれば、特別な設定やインストールなしで利用できます。

クロスプラットフォーム互換性: WebRTCは、異なるブラウザやデバイス間でも動作するため、例えばChromeからFirefox、PCからスマホなど、異なる環境間でリアルタイム通信が可能です。

プラグイン不要: かつては音声・ビデオ通話を実現するにはプラグインや専用アプリが必要でしたが、WebRTCではそれが不要です。

対応ブラウザ
現在のところ、主要なブラウザはすべてWebRTCに対応しています。

LINEやSkypeなどのビデオチャットツールやアプリの多くは、WebRTC(Web Real-Time Communication)技術を使って実装されているともかんがえられます。WebRTCは、ブラウザやモバイルアプリ間で音声・ビデオ通信をリアルタイムで行うための標準技術です。(多分)。

WebRTCを使用する際のシグナリングサーバーは、Node.jsを使うのが一般的です。Node.jsはJavaScriptベースのサーバーサイド環境なので、JavaScriptと親和性が高く、WebRTCのサーバーとしてよく使われます。

しかし、PythonでもHTTPサーバーを簡単に起動できるため、Pythonを使ってシグナリングサーバーの役割を果たすことも可能です。WebRTC APIの操作は基本的にクライアント側(ブラウザ側)で行うため、シグナリングの通信部分さえ実装できれば、Pythonサーバーを使っても問題ありません。
(今回のビデオ通話のコードはPythonでHTTPサーバを起動しています。)

ビデオ通話のコード

ビデオ要素を2つ追加しました (localVideo と remoteVideo)。
ユーザーのカメラとマイクからのストリームを取得し、localVideoに表示するようにしました。
通話相手のビデオストリームはremoteVideoに表示されます。

実行方法
このコードを実行すると、2つのブラウザウィンドウが開かれ、それぞれのウィンドウにビデオ通話アプリのインターフェースが表示されます。通話を開始するために、どちらか一方のウィンドウで「通話を開始」ボタンをクリックしてください。これで、リアルタイムでのビデオ通話が可能になります。

実行結果。
Google クローム推奨。ブラウザが開かない場合は手動で http://localhost:8000 
このアドレスのページを2つ開いてください。

スクリーンショット 2024-09-29 055439.png

import http.server
import socketserver
import socketio
import threading
import eventlet
import webbrowser
import time

# Socket.IOのサーバーを作成
sio = socketio.Server(cors_allowed_origins='*')

# ソケット接続時の処理
@sio.event
def connect(sid, environ):
    print(f"User {sid} connected")  # 接続されたユーザーのIDを表示

# シグナリング処理
@sio.event
def signal(sid, data):
    print(f"Received signal from {sid}: {data}")  # 受信したシグナリングデータを表示
    sio.emit('signal', data, skip_sid=sid)  # 他のクライアントにデータを送信

# 切断時の処理
@sio.event
def disconnect(sid):
    print(f"User {sid} disconnected")  # 切断されたユーザーのIDを表示

# HTTPリクエストを処理するハンドラー
class CustomHandler(http.server.SimpleHTTPRequestHandler):
    def do_GET(self):
        if self.path == '/':
            self.send_response(200)  # HTTPステータス200を返す
            self.send_header('Content-type', 'text/html')  # レスポンスの内容タイプをHTMLに設定
            self.end_headers()
            with open('index.html', 'rb') as f:
                self.wfile.write(f.read())  # index.htmlの内容をレスポンスとして返す
        else:
            super().do_GET()  # その他のリクエストは親クラスの処理を行う

# HTTPサーバーの開始
def start_server():
    PORT = 8000  # ポート番号の設定
    handler = CustomHandler  # ハンドラーの指定
    httpd = socketserver.TCPServer(("", PORT), handler)  # TCPサーバーを作成
    print(f"Serving HTTP on port {PORT}...")  # サーバーが起動したことを表示
    httpd.serve_forever()  # 永久にリクエストを待ち続ける

# Socket.IOサーバーの開始
def start_socket_server():
    app = socketio.WSGIApp(sio)  # Socket.IOアプリを作成
    eventlet.wsgi.server(eventlet.listen(('', 5000)), app)  # Socket.IOサーバーを起動

# メインスレッドでHTTPサーバーを、別スレッドでSocket.IOサーバーを実行
if __name__ == "__main__":
    # HTMLファイルの作成
    with open('index.html', 'w', encoding='utf-8') as f:
        f.write("""
<!DOCTYPE html>
<html lang="ja">
<head>
  <meta charset="UTF-8">
  <title>簡単なビデオ通話アプリ</title>
  <style>
    video {
      width: 400px;  /* ビデオの幅 */
      height: 300px; /* ビデオの高さ */
      border: 1px solid black; /* ビデオの境界線 */
    }
  </style>
</head>
<body>
  <h1>通話相手を選択してください</h1>
  <button id="callButton">通話を開始</button> <!-- 通話を開始するボタン -->
  <video id="localVideo" autoplay muted></video> <!-- 自分のビデオ -->
  <video id="remoteVideo" autoplay></video> <!-- 相手のビデオ -->
  <script src="https://cdn.socket.io/4.0.0/socket.io.min.js"></script> <!-- Socket.IOライブラリの読み込み -->
  <script>
    const socket = io('http://localhost:5000');  // Socket.IOサーバーのポートを指定
    let localConnection; // RTCPeerConnectionオブジェクト
    let localVideo = document.getElementById('localVideo'); // 自分のビデオ要素
    let remoteVideo = document.getElementById('remoteVideo'); // 相手のビデオ要素

    // ICEサーバーの設定
    const servers = {
      iceServers: [{ urls: 'stun:stun.l.google.com:19302' }] // STUNサーバーのURL
    };

    // ボタンを押したときの動作
    document.getElementById('callButton').addEventListener('click', async () => {
      localConnection = new RTCPeerConnection(servers); // 新しいRTCPeerConnectionオブジェクトを作成
      localConnection.onicecandidate = event => { // ICE候補を受信したときの処理
        if (event.candidate) {
          socket.emit('signal', { candidate: event.candidate }); // ICE候補をサーバーに送信
        }
      };

      localConnection.ontrack = event => { // リモートトラックを受信したときの処理
        remoteVideo.srcObject = event.streams[0]; // リモートビデオにストリームを設定
      };

      // メディアデバイスから音声とビデオのストリームを取得
      let stream = await navigator.mediaDevices.getUserMedia({ video: true, audio: true });
      stream.getTracks().forEach(track => localConnection.addTrack(track, stream)); // ストリームのトラックをローカル接続に追加
      localVideo.srcObject = stream; // 自分のビデオにストリームを設定

      let offer = await localConnection.createOffer(); // オファーを作成
      await localConnection.setLocalDescription(offer); // ローカル接続にオファーを設定
      socket.emit('signal', { sdp: offer }); // オファーをサーバーに送信
    });

    // シグナリング情報を受信したとき
    socket.on('signal', async data => {
      if (data.sdp) { // SDP情報がある場合
        await localConnection.setRemoteDescription(new RTCSessionDescription(data.sdp)); // リモートSDPを設定
        if (data.sdp.type === 'offer') { // 受信したSDPがオファーの場合
          let answer = await localConnection.createAnswer(); // アンサーを作成
          await localConnection.setLocalDescription(answer); // ローカル接続にアンサーを設定
          socket.emit('signal', { sdp: answer }); // アンサーをサーバーに送信
        }
      } else if (data.candidate) { // ICE候補がある場合
        await localConnection.addIceCandidate(new RTCIceCandidate(data.candidate)); // ICE候補をローカル接続に追加
      }
    });
  </script>
</body>
</html>
        """)

    # サーバーを別スレッドで起動
    http_thread = threading.Thread(target=start_server)
    http_thread.daemon = True
    http_thread.start()  # HTTPサーバーを非同期で実行

    # Socket.IOサーバーをメインスレッドで起動
    start_socket_server()

    # サーバーが起動するまで待機
    time.sleep(1)  # サーバーが起動するのを少し待つ

    # 2つのブラウザウィンドウを自動で起動
    webbrowser.open("http://localhost:8000")  # 1つ目のブラウザを開く
    webbrowser.open("http://localhost:8000")  # 2つ目のブラウザを開く


参考。

WebRTCでは、ブラウザやモバイルアプリケーションでリアルタイム通信を実現するために、いくつかのAPI関数が用意されています。これらのAPIは、音声・ビデオのストリーミング、データの送受信を可能にするために設計されており、ブラウザが直接ピアツーピア(P2P)通信をサポートできるようになっています。

以下、WebRTCでよく使用される主要なAPIとその概要を説明します。

  1. RTCPeerConnection
    WebRTCの中心となるAPIです。このオブジェクトは、ブラウザ間のピアツーピア通信を確立し、音声、ビデオ、データチャネルを通じてメディアやデータをやり取りするために使用されます。

主なメソッドとプロパティ:

createOffer():

ピア接続を開始するためのオファー(SDP: セッション記述プロトコル)を生成します。
例: localConnection.createOffer().then(offer => localConnection.setLocalDescription(offer));
createAnswer():

オファーを受け取った側が、それに応じて返答(アンサー)を生成します。
例: remoteConnection.createAnswer().then(answer => remoteConnection.setLocalDescription(answer));
setLocalDescription():

自分のSDP(オファーまたはアンサー)を設定します。
例: localConnection.setLocalDescription(offer);
setRemoteDescription():

相手から受け取ったSDP(オファーまたはアンサー)を設定します。
例: remoteConnection.setRemoteDescription(offer);
addIceCandidate():

ICE候補(接続候補)を追加します。これにより、通信経路が確立されます。
例: localConnection.addIceCandidate(candidate);
addTrack():

ローカルの音声・ビデオストリームをピア接続に追加します。
例: localConnection.addTrack(track, stream);
onicecandidate:

ICE候補が見つかったときに発生するイベントです。候補を相手に送信します。
例: localConnection.onicecandidate = event => sendCandidateToRemote(event.candidate);
ontrack:

相手側からメディアストリームが届いたときに発生します。リモートストリームを再生します。
例: remoteConnection.ontrack = event => remoteVideo.srcObject = event.streams[0];
2. getUserMedia()
このAPIは、ユーザーのデバイス(カメラやマイクなど)から音声やビデオのメディアストリームを取得するために使用されます。取得したストリームは、RTCPeerConnectionを通じて他のピアに送信できます。

使用例:


navigator.mediaDevices.getUserMedia({ video: true, audio: true })
  .then(stream => {
    localVideo.srcObject = stream;
    localConnection.addStream(stream);
  })
  .catch(error => console.error('Error accessing media devices.', error));

引数:

video: trueはカメラ映像の取得を意味します。
audio: trueはマイク音声の取得を意味します。
返り値: メディアストリームオブジェクトが返され、その中にカメラとマイクのトラック(映像と音声のデータ)が含まれます。

  1. RTCDataChannel
    このAPIは、音声・ビデオ以外の任意のデータ(テキストメッセージやファイルなど)をピアツーピアで送受信するためのものです。

主なメソッドとプロパティ:

createDataChannel():

データチャネルを作成します。
例: let dataChannel = localConnection.createDataChannel('myChannel');
onmessage:

リモートピアからメッセージを受信した際に呼び出されます。
例: dataChannel.onmessage = event => console.log('Message from peer:', event.data);
send():

データチャネルを通じてメッセージを送信します。
例: dataChannel.send('Hello from this peer!');
4. ICE (Interactive Connectivity Establishment)
WebRTCがピアツーピア接続を確立するためのプロトコルです。複数の経路候補を生成し、最適な経路で接続を確立するために使用されます。

主なイベント:

onicecandidate:
新しいICE候補が見つかった際に発生します。通常、このイベントが発生したら、候補情報を相手のピアに送信します。
使用例:


localConnection.onicecandidate = event => {
    if (event.candidate) {
        // シグナリングサーバー経由で相手に送信
        sendCandidateToRemote(event.candidate);
    }
};
  1. MediaStream
    getUserMedia()で取得されたカメラやマイクのメディアストリームを扱うためのオブジェクトです。このストリームは、RTCPeerConnectionに追加して他のピアに送信したり、HTMLの要素に設定して画面上に表示したりします。

使用例:


// カメラ映像を画面に表示
const videoElement = document.querySelector('video');
navigator.mediaDevices.getUserMedia({ video: true })
  .then(stream => {
    videoElement.srcObject = stream;
  });

まとめ
WebRTCにはブラウザ間でリアルタイム通信を確立するための便利なAPIが多数用意されています。主にRTCPeerConnectionがピアツーピア接続を管理し、getUserMedia()でメディアストリームを取得し、RTCDataChannelでデータ通信を行います。ICEは最適な接続経路を見つける役割を果たし、これらのAPIを組み合わせることで、音声・ビデオ・データの通信をブラウザ上で実現できます。

2
3
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
3