【WebRTCをPythonで利用するには】リアルタイム通信を実装してみた!サンプルも公開

WebRTC Python

Pythonでリアルタイム通信を実現したいと考えたことはありませんか?「WebRTC」と「Python」を組み合わせることで、ビデオ通話や音声ストリーミング、データのやり取りが可能なアプリケーションを手軽に作成できます。

この記事では、WebRTCプラットフォームを運営するSkyWayが、Python向けのWebRTC ライブラリ「aiortc」を活用し、リアルタイム通信の実装方法やサンプルを交えてわかりやすく解説します。

WebRTCの代表的なSDKとして、NTTコミュニケーションズが開発、運営する「SkyWay」があります。 「SkyWay」とは、ビデオ・音声通話をアプリケーションに簡単に実装できる国産SDKです。⇒無料で始めてみる

WebRTCとは

WebRTC

WebRTC(Web Real-Time Communication)は、ブラウザ間やアプリケーション間でリアルタイムに音声、ビデオ、データ通信を行うためのオープンな技術です。従来のブラウザが必要とした外部プラグインなしで、直接ピアツーピア(P2P)通信を可能にします。WebRTCは、主にビデオ通話や音声通話、ファイル共有、オンライン会議などのリアルタイムアプリケーションで利用されいます。

以下記事にてWebRTCの詳細を解説しているので、ご参考ください。

skyway.ntt.com

WebRTCをPythonで利用するには

WebRTC Python

WebRTCは本来、JavaScript向けに設計され、ブラウザ上で直接動作するように作られた技術です。しかし、Python向けのライブラリも存在しており、これを使うことでサーバーサイドやバックエンドでもWebRTCを利用することができます。

Python向けのWebRTC ライブラリ「aiortc」を利用する

PythonでWebRTCを利用するには、主に「aiortc」というライブラリを使用します。aiortcは、WebRTCの機能をPythonで扱うための非同期ライブラリで、音声やビデオのメディアストリーム管理、ピア接続、シグナリングなどをサポートします。

Python向けのWebRTC ライブラリ「aiortc」のGitHub

「aiortc」は、PythonでWebRTCのリアルタイム通信を実現するための主要なライブラリで、GitHubで公開されています。GitHubリポジトリには、WebRTCアプリケーションを構築するためのサンプルコードやドキュメントが揃っており、初心者から上級者まで学びながら開発を進めることができます。

※GitHubは以下をご参照ください

https://github.com/aiortc/aiortc/issues

WebRTC Pythonでリアルタイム通信を実装する方法

ここからは実際にPythonとaiortcを活用し、ブラウザ上でリアルタイム通信を実装する方法をご紹介します。

動作環境

今回開発に利用した環境は次のとおりです。バージョンによっては正常にインストールできない場合や動作しない可能性があります。

  • Mac OS 14.5

    • MacBook Pro(M1チップ, Rosetta2利用)
  • Python v3.12.7

    • homebrewで導入したpyenvを利用してバージョン指定
    • pip 24.3.1
  • Chrome 130.0.6723.117

環境構築

先に示した動作環境にてpythonおよびpipが利用できる状態から開始します。

  • aiortcと関連パッケージ(open source)のインストール

    • aiortc:

      • WebRTCを扱うためのpythonパッケージ
    • aiohttp:

      • httpサーバを利用するためのpythonパッケージ
pip install aiohttp aiortc

システム構成

今回開発するシステム構成は以下のようになります。

WebRTC Python システム構成

各コンポーネントの役割は以下の通りです。

  • Python

    • HTTPサーバー:

      • html/JavaScriptファイルのサーブとWebRTCクライアントが作成したSDPの送受信を担います。今回はブラウザからOfferを受け取り、レスポンスとしてAnswerを返します。一般的にはWebRTCSDPを送受信するためにSignalingサーバーという専用のサーバーを構築しますが、今回は簡易的な交換を行います。
    • WebRTCクライアント:

      • WebカメラにアクセスしてVideoStreamを作成します。その後、VideoStreamの情報を載せたSDPを作成します。
  • ブラウザ

    • WebRTCクライアント:

      • SDPを作成し,python側のWebRTCクライアントへ接続を行います。接続後はhtmlのvideoタグを利用して画面に映像を描画します。

アプリケーションの実装

Python側の実装

まずは必要なモジュールをimportします。あわせて必要な変数を定義しておきます。

import asyncio
import json
import logging
import os
import platform

from aiohttp import web
from aiortc import RTCPeerConnection, RTCSessionDescription
from aiortc.contrib.media import MediaPlayer

DIR_ROOT = os.path.dirname(__file__)

peer_connection = None

※RTCPeerConnectionについては以下をご参照ください。 https://aiortc.readthedocs.io/en/latest/api.html#aiortc.RTCPeerConnection

メインの処理にてhttpサーバを実装し、エンドポイントを追加します。

  • //client.js :html/jsファイルをサーブします。
  • /offer OfferSDPを受け取り、AnswerSDPを返します。

hostおよびポートはお手元の動作環境に合わせて変更してください。

if __name__ == "__main__":
  logging.basicConfig(level=logging.INFO)

  app = web.Application()
  app.on_shutdown.append(on_shutdown)

  # python側: offerを待ち受ける
  app.router.add_post("/offer", receive_offer_and_send_answer)

  # クライアント側: 別サーバでserveしても良いがCORS対応が必要になるので今回は一緒にserveする
  app.router.add_get("/", serve_index_file)
  app.router.add_get("/client.js", serve_javascript_file)

  web.run_app(app, host="0.0.0.0", port=8080)

なお、loggingのログレベルを変更することで、WebRTCのログの出力を増減することができます。今回は logging.INFO にしているため、エンドポイントへのアクセスログが出力されます。 logging.DEBUG にすると、詳細の通信状況のログが出力されます。

続いて、プログラム停止時にRTCPeerConnectionを正常に開放する処理を追加します。

async def on_shutdown(app):
  # RTCPeerConnectionを正常に切断する
  if peer_connection:
    coroutine = peer_connection.close()
    await asyncio.gather(*coroutine)

htmlとjavascriptのファイルをサーブする関数を実装します。

# クライアント側のファイル:htmlを渡す処理
async def serve_index_file(request):
  content = open(os.path.join(DIR_ROOT, "index.html"), "r").read()
  return web.Response(content_type="text/html", text=content)

# クライアント側のファイル:javascriptを渡す処理
async def serve_javascript_file(request):
  content = open(os.path.join(DIR_ROOT, "client.js"), "r").read()
  return web.Response(content_type="application/javascript", text=content)

ブラウザからOfferを受け取った際に呼び出される関数を実装します。

# クライアントからOfferを受け取りAnswerを返す処理
async def receive_offer_and_send_answer(request):
  params = await request.json()
  offer_sdp = RTCSessionDescription(sdp=params["sdp"], type=params["type"])

  # RTCPeerConnectionの作成
  peer_connection = RTCPeerConnection()

  # VideoTrackを作成し、RTCPeerConnectionに追加
  video_track = create_local_video_track()
  peer_connection.addTrack(video_track)

  # 受信したOfferをリモートSDPとしてRTCPeerConnectionに追加
  await peer_connection.setRemoteDescription(offer_sdp)

  # Answerを作成してローカルSDPとしてRTCPeerConnectionに追加
  answer_sdp = await peer_connection.createAnswer()
  await peer_connection.setLocalDescription(answer_sdp)

  # Answerをクライアントに返却
  return web.Response(
    content_type = "application/json",
    text = json.dumps({"sdp": peer_connection.localDescription.sdp, "type": peer_connection.localDescription.type})
  )

関数内では、RTCPeerConnectionを初期化します。

その後、VideoTrackを作成して送信するメディアとしてRTCPeerConnectionに追加します。

そして、RTCPeerConnection受信したOffer SDPの追加とAnswer SDPの作成を行います。

最後にAnswer SDPをjson文字列として返します。

続いて、カメラ映像を取得してVideoTrackを作成する関数 create_local_video_track を実装します。

# WebカメラからVideoTrackを取得する処理
def create_local_video_track():
  options = {"framerate": "30", "video_size": "640x480"}

  # 引き渡すパラメータはOSによって変えてやる必要がある
  if platform.system() == "Darwin":
    webcam = MediaPlayer("default:none", format="avfoundation", options=options)
  elif platform.system() == "Windows":
    webcam = MediaPlayer("video=Integrated Camera", format="dshow", options=options)
  else:
    # 他のOSは今回はサポートしない
    raise Exception("This OS is not supported.")
  return webcam.video

MediaPlayer はファイルやデバイスから映像・音声にMediaStreamとしてアクセスするクラスです。今回はデバイスの映像にアクセスしています。

このクラスの詳細な使い方はaiortcの公式ドキュメントを参照ください。

Python側の実装はこれで完了です。

最終的なコードの全体像は次のとおりです。

import asyncio
import json
import logging
import os
import platform

from aiohttp import web
from aiortc import RTCPeerConnection, RTCSessionDescription
from aiortc.contrib.media import MediaPlayer

DIR_ROOT = os.path.dirname(__file__)

peer_connection = None

# クライアントからOfferを受け取りAnswerを返す処理
async def receive_offer_and_send_answer(request):
  params = await request.json()
  offer_sdp = RTCSessionDescription(sdp=params["sdp"], type=params["type"])

  # RTCPeerConnectionの作成
  peer_connection = RTCPeerConnection()

  # VideoTrackを作成し、RTCPeerConnectionに追加
  video_track = create_local_video_track()
  peer_connection.addTrack(video_track)

  # 受信したOfferをリモートSDPとしてRTCPeerConnectionに追加
  await peer_connection.setRemoteDescription(offer_sdp)

  # Answerを作成してローカルSDPとしてRTCPeerConnectionに追加
  answer_sdp = await peer_connection.createAnswer()
  await peer_connection.setLocalDescription(answer_sdp)

  # Answerをクライアントに返却
  return web.Response(
    content_type = "application/json",
    text = json.dumps({"sdp": peer_connection.localDescription.sdp, "type": peer_connection.localDescription.type})
  )

# WebカメラからVideoTrackを取得する処理
def create_local_video_track():
  options = {"framerate": "30", "video_size": "640x480"}

  # 引き渡すパラメータはOSによって変えてやる必要がある
  if platform.system() == "Darwin":
    webcam = MediaPlayer("default:none", format="avfoundation", options=options)
  elif platform.system() == "Windows":
    webcam = MediaPlayer("video=Integrated Camera", format="dshow", options=options)
  else:
    # 他のOSは今回はサポートしない
    raise Exception("This OS is not supported.")
  return webcam.video

# クライアント側のファイル:htmlを渡す処理
async def serve_index_file(request):
  content = open(os.path.join(DIR_ROOT, "index.html"), "r").read()
  return web.Response(content_type="text/html", text=content)

# クライアント側のファイル:javascriptを渡す処理
async def serve_javascript_file(request):
  content = open(os.path.join(DIR_ROOT, "client.js"), "r").read()
  return web.Response(content_type="application/javascript", text=content)

async def on_shutdown(app):
  # RTCPeerConnectionを正常に切断する
  if peer_connection:
    coroutine = peer_connection.close()
    await asyncio.gather(*coroutine)

# app.pyを直接実行した場合のみ処理される
if __name__ == "__main__":
  logging.basicConfig(level=logging.INFO)

  app = web.Application()
  app.on_shutdown.append(on_shutdown)

  # python側: offerを待ち受ける
  app.router.add_post("/offer", receive_offer_and_send_answer)

  # クライアント側: 別サーバでserveしても良いがCORS対応が必要になるので今回は一緒にserveする
  app.router.add_get("/", serve_index_file)
  app.router.add_get("/client.js", serve_javascript_file)

  web.run_app(app, host="0.0.0.0", port=8080)

ブラウザ側の実装

まずはhtmlの要素を作成します。

  • WebRTCの接続操作を行うボタン: buttonElement
  • 映像を描画する領域: videoElement
<!DOCTYPE html>
<html lang="ja">
<head>
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta charset="UTF-8" />
    <title>WebRTC with Python</title>
    <script src="client.js"></script>
</head>
<body>
    <div id="buttons">
        <button id="start">Start</button>
        <button id="stop" disabled="true">Stop</button>
    </div>
    <div id="media">
        <video id="video" width="640" autoplay="true"></video>
    </div>
</body>
</html>

次にJavaScriptにて各処理を実装します。

まず、ページの読み込み完了時に、ボタンのクリックイベントと処理を紐付けます。

あわせて必要な変数を定義しておきます。

let peerConnection = null;

window.onload = () => {
  const startBtn = document.getElementById('start');
  const stopBtn = document.getElementById('stop');

  startBtn.addEventListener("click", async () => {
    initPeerConnection();
    startBtn.disabled = true;
    await createOffer();
    stopBtn.disabled = false;
  })

  stopBtn.addEventListener("click", async () => {
    stopBtn.disabled = true;
    peerConnection.close();
  })
};

ボタンは画面に二つあり、それぞれstart/stopの機能を持たせています。

startボタンは一度クリックすると無効になり、代わりにstopボタンが有効になります。

stopボタンがクリックされた場合はRTCPeerConnctionを正常に切断します。

今回は一度きり受信を行うことを想定しているため、stopボタンを一度クリックするとすべてのボタンが無効になります。

PeerConnectionの初期化を行う initPeerConnection を追加します。

// RTCPeerConnectionの初期化
const initPeerConnection = () => {
  peerConnection = new RTCPeerConnection();
  
  // ICEの状態がcompleteになったらOfferを送信する
  peerConnection.addEventListener('icegatheringstatechange', async () => {
    if (peerConnection.iceGatheringState !== 'complete') {
      return
    }
    const res = await sendOffer(peerConnection.localDescription);
    const answer = await res.json();
    // Python側から受信したAnswerをリモートSDPとしてRTCPeerConnectionに追加
    peerConnection.setRemoteDescription(answer);
  })

  // VideoTrackを受信したらvideoタグに表示する
  peerConnection.addEventListener('track', (event) => {
    if (event.track.kind == 'video') {
      document.getElementById('video').srcObject = event.streams[0];
    }
  });
};

PeerConnectionの以下のイベントにハンドラを設定します。

icegatheringstatechangeイベント:

Offerの作成後、ICE収集の状態が変わった際に発火するイベント。今回は収集完了を待ってからOfferを送信する(Vanilla ICE)ため、 変化後の状態がcompete 以外の場合は早期returnします。

complete なった場合はOfferをPython側に送信し、レスポンスとして受け取ったAnswerをRTCPeerConnectionに追加します。これにより、疎通が開始され、後述のtrackイベントが発火するようになります。

trackイベント:

メディアの受信が開始された際に発火するイベント。引数でMediaStreamTrackを受け取ることができるので、videoタグにセットして再生します。

OfferのSDPを作成する関数 createOffer と作成したSDPを送信する関する sendOffer を追加します。

// Offer SDPの作成
const createOffer = async () => {
  // 映像受信のOfferを作成し、ローカルSDPとしてRTCPeerConnectionに追加
  peerConnection.addTransceiver('video', { direction: 'recvonly' });
  const offer = await peerConnection.createOffer();
  await peerConnection.setLocalDescription(offer);
  // この後ICE Candidateが収集され、icegatheringstatechangeイベントが発火する
};

// Python側にOfferを送信し、answerを受け取る
const sendOffer = async (offer) => {
  return await fetch('http://0.0.0.0:8080/offer', {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json'
    },
    body: JSON.stringify({
      sdp: offer.sdp,
      type: offer.type,
    })
  });
};

今回は映像の受信を行いたいため、addTransceiverに video{direction: 'revonly'} を渡しています。送信などを行いたい場合は他のパラメータに書き換えてください。

sendOffer の送り先のホストやポートはPython側で指定したものを利用してください。

ブラウザ側(JavaScript)の実装はこれで完了です。

最終的なコードの全体像は次のとおりです。

let peerConnection = null;

window.onload = () => {
  const startBtn = document.getElementById('start');
  const stopBtn = document.getElementById('stop');

  startBtn.addEventListener("click", async () => {
    initPeerConnection();
    startBtn.disabled = true;
    await createOffer();
    stopBtn.disabled = false;
  })

  stopBtn.addEventListener("click", async () => {
    stopBtn.disabled = true;
    peerConnection.close();
  })
};

// RTCPeerConnectionの初期化
const initPeerConnection = () => {
  peerConnection = new RTCPeerConnection();

  // ICEの状態がcompleteになったらOfferを送信する
  peerConnection.addEventListener('icegatheringstatechange', async () => {
    if (peerConnection.iceGatheringState !== 'complete') {
      return
    }
    const res = await sendOffer(peerConnection.localDescription);
    const answer = await res.json();
    // Python側から受信したAnswerをリモートSDPとしてRTCPeerConnectionに追加
    peerConnection.setRemoteDescription(answer);
  })
  
  // VideoTrackを受信したらvideoタグに表示する
  peerConnection.addEventListener('track', (event) => {
    if (event.track.kind == 'video') {
      document.getElementById('video').srcObject = event.streams[0];
    }
  });
};

// Offer SDPの作成
const createOffer = async () => {
  // 映像受信のOfferを作成し、ローカルSDPとしてRTCPeerConnectionに追加
  peerConnection.addTransceiver('video', { direction: 'recvonly' });
  const offer = await peerConnection.createOffer();
  await peerConnection.setLocalDescription(offer);
  // この後ICE Candidateが収集され、icegatheringstatechangeイベントが発火する
};

// Python側にOfferを送信し、answerを受け取る
const sendOffer = async (offer) => {
  return await fetch('http://0.0.0.0:8080/offer', {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json'
    },
    body: JSON.stringify({
      sdp: offer.sdp,
      type: offer.type,
    })
  });
};

以上で実装は完成です。

動作確認

ブラウザから、Python側で指定したURLにアクセスし、startボタンを押します。

すると、WebRTC経由でカメラの映像を表示することができます。

stopボタンを押すと通信を終了します。再度接続したい場合はブラウザリロードを行なってください。

WebRTC Python 実装

Pythonの実行ログは以下のようになります。(ログレベルをlogging.INFOにした場合)

エンドポイントへのアクセスと、通信経路候補(candidate)を収集しているログが出力されます。

❯ python app.py
======== Running on http://0.0.0.0:8080 ========
(Press CTRL+C to quit)

# 0.0.0.0:8080へアクセス
INFO:aiohttp.access:127.0.0.1 [18/Nov/2024:16:58:58 +0900] "GET / HTTP/1.1" 200 634 "-" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.0.0 Safari/537.36"
INFO:aiohttp.access:127.0.0.1 [18/Nov/2024:16:58:58 +0900] "GET /client.js HTTP/1.1" 200 2168 "http://0.0.0.0:8080/" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.0.0 Safari/537.36"

# startボタンを押す
INFO:aioice.ice:Connection(0) Remote candidate "395abb06-f577-41d8-98d6-c248bec7f019.local" resolved to 100.64.1.16
INFO:aioice.ice:Connection(0) Remote candidate "b293bfd8-d6e2-4bfb-aa74-9ef028ca74bb.local" resolved to 2400:4051:4e4:5400:8c92:2c9c:3a9b:9926
INFO:aiohttp.access:127.0.0.1 [18/Nov/2024:17:00:14 +0900] "POST /offer HTTP/1.1" 200 2408 "http://0.0.0.0:8080/" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.0.0 Safari/537.36"

## ICEの情報が出力される
INFO:aioice.ice:Connection(0) Check CandidatePair(('2400:4051:4e4:5400:cd4:8646:d5b4:66ac', 52789) -> ('2400:4051:4e4:5400:8c92:2c9c:3a9b:9926', 54701)) State.FROZEN -> State.WAITING
...
INFO:aioice.ice:Connection(0) ICE completed
...
INFO:aioice.ice:Connection(0) Discovered peer reflexive candidate Candidate(XzOWEpl30N 1 udp 1845504255 2400:4051:4e4:5400:1970:6628:11c3:be27 54701 typ prflx)

以上で実装確認は終わりです。

WebRTCを簡単に実装するには

WebRTCの実装には、専門的な知識かつ膨大な開発工数が必要ですが、SDK(Software Development Kit)を使用する方法もあります。SDKを使用することで、様々なサーバーの構築や管理を簡略化し、効率的に開発を進めることができます

WebRTC SDK「SkyWay」

SkyWay

WebRTCの代表的なSDKとして、NTTコミュニケーションズが開発、運営する「SkyWay」があります。「SkyWay」とは、ビデオ・音声通話をアプリケーションに簡単に実装できる国産SDKです。

大きな特徴としては、以下が挙げられます。

  • スピーディーな開発ができる
    開発資料が豊富かつ日本語でわかりやすい、国内エンジニアがサポートしてくれる
  • 信頼性・安全性が高い
    NTTコミュニケーションズが開発、運営する国産SDK。サービス歴は10年以上で、累計導入サービス数も21,000件以上
  • 無料で開発スタート
    開発検証用として、Freeプランあり。テスト検証期間中は無料で利用可能。商用サービス提供後も基本利用料11万 + 従量課金制で安心。

SkyWayの詳細はこちら

WebRTCのSDKとして提供されているものは、海外製が多いため、開発ドキュメントも英語か和訳のもので開発しにくい傾向にあります。 「SkyWay」であれば、NTTグループが開発、運営する安心の国産SDKかつ、国内エンジニアがサポートしてくれるため、開発運用工数も大幅に削減でき、開発のしやすさからもおすすめです。

テスト検証用は無料のため、ぜひアカウント登録をしてみください。

アカウント登録はこちら

まとめ

WebRTCはもともとJavaScript向けに設計されていますが、Python用のライブラリを活用することで、サーバーサイドやバックエンドでも利用可能です。本記事では、Pythonライブラリ「aiortc」を使った実装方法を解説しました。aiortcは、音声・ビデオのメディアストリーム管理やピア接続をサポートし、PythonでもWebRTCを簡単に扱えるようにします。解説したサンプルコードを参考に、WebRTCを活用したリアルタイム通信をぜひ試してみてください。また、WebRTCのSDK「SkyWay」を使えば、さらに手軽にリアルタイム通信を実装できるのでおすすめです。

ビデオ・音声通話機能を簡単に実装したい方必見!

SkyWay

「SkyWay」なら、あなたのサービスにビデオ通話や音声通話機能をカンタンに組み込む事ができます。

    • NTTコミュニケーションズが運営・開発し、サービス歴は10年以上、累計導入サービス数も21,000件以上
    • 日本語の開発ドキュメントや国内エンジニアがサポートでスムーズな開発が実現!
    • 開発検証用として、Freeプランあり

社内にエンジニアがいない場合でも開発パートナーのご紹介が可能です。ぜひ、ご検討ください。

詳しくはこちら

この記事を書いた人

大場 直史

NTTコミュニケーションズが開発、運営する「SkyWay」のソフトウェアエンジニア。
WebRTCを扱うコアコンポーネントおよびjavascriptSDKを主に担当。

この記事を書いた人

下野 弘朗

NTTコミュニケーションズが開発、運営する「SkyWay」のソフトウェアエンジニア。
SkyWayのモバイル向けSDKの開発に従事。