Raspberry Piで実家の見守りカメラを自作する|WebRTC双方向通話とHTTPS

ノートPCが雲を経由して家の中のカメラにつながるイラスト

Raspberry PiにCamera Module、USBマイク、USBスピーカーを接続すると、離れた実家の映像を見ながら話せる見守りインターホンを作れます。遠隔接続をHomeGrid VPN、映像・音声をWebRTC、ブラウザ向けHTTPSをCaddyに担当させれば、実家のルーターでポート開放やDDNSを設定する必要はありません。

この記事は動作検証済みの完成アプリの配布ではなく、組み立てるための設計・実装ガイドです。コードを大量に載せるより、どのファイルからどの関数を呼び、カメラ・マイク・スピーカーをどこでWebRTCに紐付けるかを説明します。

VPN接続と、接続準備・メディア通信の経路 閲覧端末とRaspberry Piは両方ともCloud Hubへ外向きにVPN接続します。VPN内ではHTTPSの接続準備をCaddyからaiohttpへ転送し、映像・音声は別のUDP経路でaiortcと送受信します。 VPNの接続方向 PC・スマホVPN Cloud HubVPN中継 Raspberry PiLinux Spoke 外向きに接続外向きに接続 実家ルーターのポート開放は不要 VPN内部のアプリケーション通信 HTTPSで接続準備 ブラウザ Caddy aiohttp 画面・POST /offerlocalhostのHTTP WebRTCの映像・音声 ブラウザaiortc 映像・音声のUDP通信 どちらもCloud Hubを経由するVPN内部の通信。映像・音声のUDP通信はCaddyを通りません。
上:VPNの接続方向。下:VPN内で分かれる接続準備と映像・音声の経路。

今回は自宅に既存のHomeGrid VPNのGridがあるものとし、実家にはGridを追加せず、Raspberry Pi自体をLinux Spokeとして登録します。自宅PCもVPNに接続しており、Cloud Hubを経由してVPN内部IPv4でPiへアクセスする構成です。Cloud HubはInternetへのExitとは限りません。

第1部:Raspberry PiとWebRTCで見守りインターホンを作る

1. 用意する機材と送受信するもの

実家側:Raspberry Pi 4/5(メモリ1 GB以上を推奨)、対応するCamera Module、USBマイク、USBスピーカーまたはエコーキャンセル対応USBスピーカーフォン、systemdを備えたRaspberry Pi OS、ネットワーク接続。PC側:マイクとスピーカーを備えたWindows/Macまたはスマートフォン。PC側のカメラは不要です。

方向 入力 出力
実家 → PC Pi Camera Module ブラウザの <video>
実家 → PC PiのUSBマイク ブラウザの <audio> → PCスピーカー
PC → 実家 ブラウザのマイク PiのUSBスピーカー

USBスピーカーの音を同じPiのマイクが拾うとハウリングするため、実家側はエコーキャンセル対応機器を推奨します。

2. Raspberry Piのカメラと音声を確認する

OSを更新し、認識と静止画撮影を確認します。

sudo apt update
sudo apt full-upgrade -y
rpicam-hello --list-cameras
rpicam-still -n -o test.jpg
sudo apt install -y alsa-utils
arecord -l
aplay -l

録音・再生デバイスの番号を確認します。これらは後ほど audio.py で選択します。複数機器がある場合、サンプルレートやデバイス番号は環境に合わせて指定します。

3. DashboardからLinux Spokeを追加する

HomeGrid VPNのDashboardで、「Spoke を追加する」の枠にある「+ Spoke を追加 (QR コード)」ボタンをクリックして端末名を入力し、「Linux(コマンド)」を選びます。「Linux に入れるコマンド」ダイアログが表示されます。

ここでは下段の「分割(--split)」の「Copy」をクリックし、Raspberry PiのTerminalへ貼り付けて実行します。コマンドを手入力・改変する必要はありません。分割モードはVPN対象の通信だけをトンネルへ流し、通常のインターネット通信は実家の回線から行います。トークンを含むコマンドは公開しないでください。

操作の詳細:Linux端末を追加する公式ガイド。

設定後、Pi上でVPN内部IPv4を確認します。

ip -4 addr show dev wg0

以降の 100.64.0.5 は例示用です。実際の割当IPに置き換えてください。実家のルーターにInboundルールを追加する必要はありません。同じテナントのSpoke同士はCloud Hubを経由して通信できます。実機では、WebRTCで選択されたICE候補がVPN内部IPを利用することと、端末のFirewallが必要なUDP通信を許可していることを確認します。

4. Raspberry Pi上のファイル構成

以下のファイルはすべてRaspberry Piに配置します。ただし static/index.html と static/client.js はWebサーバーから送られ、実行されるのはPC・スマートフォンのブラウザ側です。

/opt/webrtc-intercom/
├── app.py           # aiohttp Webサーバー、HTTPルーティング
├── webrtc.py        # aiortc セッション、Offer/Answer
├── camera.py        # Picamera2 → VideoStreamTrack
├── audio.py         # USBマイク・スピーカー、AudioStreamTrack
├── config.py        # VPN IPv4・音声デバイスなど
├── requirements.txt # Python依存モジュール
└── static/
    ├── index.html   # ブラウザの見守り画面
    ├── client.js    # ブラウザWebRTCとボタン操作
    └── style.css

使用モジュールはPi側で picamera2(カメラ)、aiortc(WebRTC)、aiohttp(Webサーバー)、av / PyAV(映像・音声フレーム)、sounddevice / ALSA(USB音声入出力)、numpy(データ処理)です。Picamera2はRaspberry Pi OS側のPythonパッケージとlibcameraの互換性を考慮して導入します。

WebサービスとWebRTCバックエンドは別々のPythonプロセスではありません。app.py が1つのWebサーバーを起動し、/offer のPOSTを受けると同じプロセス内の webrtc.py を呼び出します。

5. ブラウザが見守り画面を開くまで

開発時のHTTP URLの例は http://100.64.0.5:8080/ です(このURLでは通常ブラウザのマイクを使えません。第2部でHTTPS化します)。

Pi側 app.py は aiohttp のルーティングを定義します。

app.router.add_get("/", index)
app.router.add_post("/offer", handle_offer)
app.router.add_static("/static/", BASE_DIR / "static")

GET / で static/index.html、GET /static/client.js でJavaScriptを返します。ブラウザのHTMLでは、映像・音声の出力要素を用意します。

<video id="remoteVideo" autoplay playsinline></video>
<audio id="remoteAudio" autoplay></audio>
<button id="startCall">通話開始</button>
<button id="stopCall">通話終了</button>
<script src="/static/client.js"></script>

この remoteVideo / remoteAudio は client.js 側で document.getElementById() により取得します。

6. client.jsの処理順序:どこでマイクと映像・音声を紐付けるか

「通話開始」ボタンは addEventListener("click", startCall) で startCall() に接続します。処理順は以下です。

  1. getUserMedia({audio: true, video: false}) でPCのマイクだけを取得する
  2. createConnection() を呼び、new RTCPeerConnection({iceServers: []}) を作る
  3. pc.addTrack(micTrack, stream) でPCマイクを送信対象にする(音声は双方向の sendrecv)
  4. pc.addTransceiver("video", {direction: "recvonly"}) でPiカメラ映像を受け取るよう指定
  5. pc.ontrack に受信した映像・音声の表示・再生処理を登録する
  6. pc.createOffer() と setLocalDescription() で接続条件を作成
  7. 同じWebサーバーへ POST /offer する
  8. HTTP応答のAnswerを pc.setRemoteDescription(answer) で設定する
  9. ICE経路確立後にWebRTCメディア通信が始まる

主要な関連付けは次のとおりです。

const remoteVideo = document.getElementById("remoteVideo");
const remoteAudio = document.getElementById("remoteAudio");

const stream = await navigator.mediaDevices.getUserMedia({
  audio: true, video: false
});
const pc = new RTCPeerConnection({ iceServers: [] });
stream.getAudioTracks().forEach(t => pc.addTrack(t, stream));
pc.addTransceiver("video", { direction: "recvonly" });

pc.ontrack = event => {
  if (event.track.kind === "video") {
    remoteVideo.srcObject = new MediaStream([event.track]);
  } else if (event.track.kind === "audio") {
    remoteAudio.srcObject = new MediaStream([event.track]);
  }
};

pc.addTrack() は自分の機器の送信登録、pc.ontrack は相手から届いた映像・音声を受け取るイベントです。ブラウザの音声再生は通常OSが選んだスピーカー・イヤホンから行われます。自動再生制限に応じて通話開始操作に連動した play() が必要です。

7. /offerの入口からRaspberry Pi側のWebRTCへ

ブラウザの client.js はOfferを生成し、fetch("/offer", {method: "POST", ...}) にSDPとtypeをJSONで渡します。実装ではICE候補を収集し終わるのを待つか、追加候補の交換を実装します。

Pi側で app.router.add_post("/offer", handle_offer) と定義したため、POST /offer を受けると handle_offer() が呼ばれます。

# app.py: HTTPとWebRTCをつなぐ
async def handle_offer(request):
    data = await request.json()
    offer = RTCSessionDescription(sdp=data["sdp"], type=data["type"])
    answer = await create_session(offer)  # webrtc.py を呼ぶ
    return web.json_response({"sdp": answer.sdp, "type": answer.type})

RTCSessionDescription は aiortc が提供します。 create_session は from webrtc import create_session で読み込む普通のPython関数です。WebRTC専用の別のWebサーバーへPOSTし直すわけではありません。

次に webrtc.py は aiortc.RTCPeerConnection を生成し、PiのカメラとUSBマイクを登録、ブラウザからの音声をスピーカーへ渡す受信ハンドラを設定します。

# webrtc.py の要点(完成コードではない)
pc = RTCPeerConnection()
pc.addTrack(PiCameraTrack())     # Piカメラ → PC
pc.addTrack(MicrophoneTrack())   # Piマイク → PC

@pc.on("track")
def on_track(track):
    if track.kind == "audio":
        # audio.py の再生ワーカーへ track を渡す
        ...

await pc.setRemoteDescription(offer)
answer = await pc.createAnswer()
await pc.setLocalDescription(answer)
# pc.localDescription を app.py に返す

ここで PiCameraTrack は camera.py でPicamera2の画像をPyAVの VideoFrame に変換する VideoStreamTrack。 MicrophoneTrack は audio.py でUSBマイクの録音を AudioFrame として返す AudioStreamTrack です。

USBスピーカーは送信トラックではないため、addTrack() しません。Pi側の pc.on("track") で届くPCマイク音声を audio.py の再生ワーカーへ渡します。音声フレームのチャンネル数・サンプルレート・形式は実デバイスに合わせて変換し、同期I/Oがイベントループを止めないようにします。

8. Answerを返した後の映像と音声

app.py はAnswerをHTTPレスポンスとして返し、ブラウザが pc.setRemoteDescription(answer) を実行します。その後ICEで経路が成立すると、通信は次のようになります。

データ Pi側 ブラウザ側
映像 camera.py → pc.addTrack() pc.ontrack → <video>
実家の声 audio.py → pc.addTrack() pc.ontrack → <audio>
自宅の声 pc.on("track") → USBスピーカー getUserMedia() → pc.addTrack()

HTTPの /offer は接続準備の一往復で、映像・音声そのものを継続してPOSTするAPIではありません。WebRTCは別のUDPメディア通信を用います。Pi側の RTCPeerConnection はAnswer返却後も保持し、終了・切断時に閉じ、カメラ・音声デバイスも解放します。

初版は「通話開始」を押したら映像と双方向音声をまとめて接続する形が実装しやすいでしょう。「映像だけ常時表示し、あとから通話開始」へ発展させる場合は、音声トラック追加時の再ネゴシエーションなどが必要です。

第2部:CaddyでHTTPS化し、ブラウザのマイクを使う

9. なぜVPN内部のHTTPだけでは足りないのか

ブラウザの navigator.mediaDevices.getUserMedia() は原則としてHTTPSまたはlocalhost等の安全なコンテキストでのみ動作します。VPN内部の http://100.64.0.5:8080/ を開いただけではPC側マイクを使えません。

そこでCaddyをPiの前段に置きます。

  • ブラウザ → HTTPS :443 → Caddy(VPN内部IPで待ち受け)
  • Caddy → HTTP 127.0.0.1:8080 → app.py
  • Caddyが GET /、静的ファイル、POST /offer を転送
  • WebRTCの映像・音声はCaddyを通らず、aiortcとブラウザ間のUDP通信

10. Pythonをlocalhostへ、CaddyをVPN内部IPへ

app.py のHTTP待ち受けを変更します。

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

CaddyをPiに導入します。

sudo apt update
sudo apt install -y caddy
sudo nano /etc/caddy/Caddyfile

例示用のVPN内部IPv4が 100.64.0.5 の場合:

https://100.64.0.5 {
    bind 100.64.0.5
    tls internal
    reverse_proxy 127.0.0.1:8080
}

tls internal はCaddyの内部CAでサーバー証明書を発行します。設定を検査して反映します。

sudo caddy validate --config /etc/caddy/Caddyfile
sudo systemctl restart caddy
systemctl status caddy

起動時にVPN内部IPがまだない場合はCaddyのBindが失敗することがあるため、起動順序・再起動設定も検証します。

11. Caddyの公開ルートCA証明書を取得する

内部CAは初期状態ではMac・Windowsから信頼されません。PiのCaddy(systemd運用)の公開ルートCA証明書は通常、次にあります。

/var/lib/caddy/.local/share/caddy/pki/authorities/local/root.crt

root.crt はPEM形式のX.509公開証明書です。秘密鍵の root.key は配布しません。

Pi上で自分のユーザーが読める場所へ公開証明書だけをコピーしてから、SCPなどでMac・Windowsへ取得します。証明書のフィンガープリントを別経路でも照合し、自分が管理するCaddyで発行されたものだと確認してから信頼登録してください。

12. macOSのキーチェーンへ登録する

Macの「キーチェーンアクセス」で root.crt をシステムキーチェーンに読み込み、信頼設定を変更できます。Terminalから管理者権限で登録する例:

sudo security add-trusted-cert \
  -d -r trustRoot \
  -k /Library/Keychains/System.keychain \
  ~/Downloads/caddy-root.crt

ファイルパスは実際の場所へ変更してください。SafariやChromeは通常macOSの証明書信頼設定を使用します。Firefoxなどは追加設定が必要な場合があります。

13. Windowsの信頼されたルート証明機関へ登録する

root.crt をWindowsにコピーし、「証明書のインストール」→「証明書をすべて次のストアに配置する」→「信頼されたルート証明機関」を選びます。現在のユーザー用にPowerShellで登録する例:

Import-Certificate -FilePath ".\caddy-root.crt" -CertStoreLocation Cert:\CurrentUser\Root

ルートCAの登録はそのCAが発行する証明書全体を信頼する操作です。出所を検証できないルートCAは登録しないでください。

14. HTTPSでアクセスし、WebRTCの接続を確認する

MacやWindowsから、HomeGrid VPN接続中に以下へアクセスします。

https://100.64.0.5/

Caddyが GET / と GET /static/client.js をPiの app.py へ転送します。「通話開始」を押すと、client.js がPC側のマイク使用許可を求め、Offerを作って同じHTTPSオリジンの POST /offerへ送ります。

Caddyから app.py の handle_offer() へ転送され、webrtc.py がAnswerを返します。JavaScriptの fetch("/offer") は相対URLなのでCaddy導入に合わせてURLを書き換える必要はありません。

その後、選択されたICE候補がVPN内部IPを利用すること、実家の映像・マイクがPCで再生されること、PCの声がPiのUSBスピーカーから聞こえることを確認します。

15. 自動起動・アクセス制限・家族のプライバシー

Pythonの app.py はsystemdで起動し、Caddyも別のsystemdサービスで管理します。一方 webrtc.py、camera.py、audio.py は同じPythonアプリケーション内のモジュールであり、独立したWebサーバーではありません。

同じテナントの端末には、招待した家族の端末も含め、PiのSpokeアドレスへのVPN経路があります。LAN ACLが制御するのはGridや拠点のLAN宛ての通信であり、Pi自身のSpokeアドレスへのアクセスは絞れません。PiのFirewallとアプリの認証で利用者を制限してください。認証のない /offer は、VPN内で到達できる端末から呼べてしまいます。VPN内部IP限定と証明書の信頼登録は、利用者認証の代わりになりません。通話できる利用者を認証・制限し、同時通話の制御、終了時のトラック解放、エラー復旧、音声のハウリング対策を実装してください。また、iceServers: [] だけでWebRTC UDPがVPN内に限定されるとは限らず、選択候補の検証とFirewallによる制限が必要です。

実家の回線を利用する場合は、契約者の同意とISP契約条件の確認が前提です。見守りカメラの設置と音声・映像の取得は、同居家族の同意を得て、撮影範囲・通話中の通知・視聴者の範囲を決めて運用しましょう。

まとめ

  • HomeGrid VPN:PiをLinux Spokeとして追加し、実家ルーターのポート開放なしでVPN内部経路を構成
  • app.py / aiohttp:画面配信と同じWebサーバーへの POST /offer を受け付ける
  • webrtc.py / aiortc:Piカメラ・マイクを addTrack()、PC音声を on("track") で受信
  • client.js:PCマイクを addTrack()、受信映像・音声を ontrack でHTMLへ渡す
  • Caddy:ブラウザ用HTTPSと内部CAを提供し、Mac/Windowsに公開ルートCA証明書を信頼登録

Offer/Answerを交換するHTTPリクエストは一往復。確立したWebRTCの映像と音声は別経路を流れる。この関係が分かれば、自作カメラの機能追加にも応用できます。

参考資料