Pythonでリアルタイム通信を実現したいと考えたことはありませんか?「WebRTC」と「Python」を組み合わせることで、ビデオ通話や音声ストリーミング、データのやり取りが可能なアプリケーションを手軽に作成できます。
この記事では、WebRTCプラットフォームを運営するSkyWayが、Python向けのWebRTC ライブラリ「aiortc」を活用し、リアルタイム通信の実装方法やサンプルを交えてわかりやすく解説します。
WebRTCの代表的なSDKとして、NTTコミュニケーションズが開発、運営する「SkyWay」があります。 「SkyWay」とは、ビデオ・音声通話をアプリケーションに簡単に実装できる国産SDKです。⇒無料で始めてみる
WebRTCとは
WebRTC(Web Real-Time Communication)は、ブラウザ間やアプリケーション間でリアルタイムに音声、ビデオ、データ通信を行うためのオープンな技術です。従来のブラウザが必要とした外部プラグインなしで、直接ピアツーピア(P2P)通信を可能にします。WebRTCは、主にビデオ通話や音声通話、ファイル共有、オンライン会議などのリアルタイムアプリケーションで利用されいます。
以下記事にてWebRTCの詳細を解説しているので、ご参考ください。
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
システム構成
今回開発するシステム構成は以下のようになります。
各コンポーネントの役割は以下の通りです。
Python
ブラウザ
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サーバを実装し、エンドポイントを追加します。
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ボタンを押すと通信を終了します。再度接続したい場合はブラウザリロードを行なってください。
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」
WebRTCの代表的なSDKとして、NTTコミュニケーションズが開発、運営する「SkyWay」があります。「SkyWay」とは、ビデオ・音声通話をアプリケーションに簡単に実装できる国産SDKです。
大きな特徴としては、以下が挙げられます。
- スピーディーな開発ができる:
開発資料が豊富かつ日本語でわかりやすい、国内エンジニアがサポートしてくれる - 信頼性・安全性が高い:
NTTコミュニケーションズが開発、運営する国産SDK。サービス歴は10年以上で、累計導入サービス数も21,000件以上 - 無料で開発スタート:
開発検証用として、Freeプランあり。テスト検証期間中は無料で利用可能。商用サービス提供後も基本利用料11万 + 従量課金制で安心。
WebRTCのSDKとして提供されているものは、海外製が多いため、開発ドキュメントも英語か和訳のもので開発しにくい傾向にあります。 「SkyWay」であれば、NTTグループが開発、運営する安心の国産SDKかつ、国内エンジニアがサポートしてくれるため、開発運用工数も大幅に削減でき、開発のしやすさからもおすすめです。
テスト検証用は無料のため、ぜひアカウント登録をしてみください。
まとめ
WebRTCはもともとJavaScript向けに設計されていますが、Python用のライブラリを活用することで、サーバーサイドやバックエンドでも利用可能です。本記事では、Pythonライブラリ「aiortc」を使った実装方法を解説しました。aiortcは、音声・ビデオのメディアストリーム管理やピア接続をサポートし、PythonでもWebRTCを簡単に扱えるようにします。解説したサンプルコードを参考に、WebRTCを活用したリアルタイム通信をぜひ試してみてください。また、WebRTCのSDK「SkyWay」を使えば、さらに手軽にリアルタイム通信を実装できるのでおすすめです。