第3弾 ラズパイAIロボット製作(Phase2)

Raspberry Pi カメラ映像を、WebRTC を使って PC ブラウザにリアルタイム送信するようにしました。今までVNCで操作していましたが、WebRTC を使うことで約100ms 前後の実用的な FPV(リアルタイム映像)になったと思います。
以下はMeta Quest3ゴーグルのブラウザで表示させた例です。右下あたりにRaspiカーがあって、そのPiカメラ映像を表示しているのがわかるかと思います。

構成図(Raspi → PC → FPVゴーグル)

Raspberry Pi(Picamera2)
    ↓ WebRTC 映像配信(aiortc)
Wi-Fi
    ↓
PC または、FPVゴーグルへ

必要なライブラリ(Raspberry Pi)

sudo apt update
sudo apt install python3-full python3-venv

仮想環境作成:

python3 -m venv webrtc-env --system-site-packages
source webrtc-env/bin/activate

依存ライブラリ:

pip install aiortc aiohttp av --break-system-packages

WebRTC サーバ(server.py)

import asyncio
import logging
import cv2

from aiohttp import web
from aiortc import RTCPeerConnection, RTCSessionDescription, VideoStreamTrack
from av import VideoFrame
from picamera2 import Picamera2

# ----------------------------------------
# ログを抑制(必要最低限)
# ----------------------------------------
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger("webrtc")
logging.getLogger("aiortc").setLevel(logging.WARNING)
logging.getLogger("picamera2").setLevel(logging.WARNING)


# ----------------------------------------
# Picamera2 初期化(BGR実データをRGB888で取得)
# ----------------------------------------
picam2 = Picamera2()
camera_config = picam2.create_video_configuration(
    main={"size": (640, 480), "format": "RGB888"},
    controls={"FrameRate": 30}
)
picam2.configure(camera_config)
picam2.start()
logger.info("Picamera2 started")


# ----------------------------------------
# WebRTC へ流すための映像トラック
# ----------------------------------------
class PiCameraTrack(VideoStreamTrack):
    def __init__(self, picam2):
        super().__init__()
        self.picam2 = picam2

    async def recv(self):
        pts, time_base = await self.next_timestamp()

        # ★ Raspberry Pi からフレーム取得(実際は B,G,R の順)
        frame = self.picam2.capture_array()

        # ★ カメラが上下左右逆の場合 flip(-1)
        frame = cv2.flip(frame, -1)

        # ★ BGR24 として WebRTC へ送信
        video_frame = VideoFrame.from_ndarray(frame, format="bgr24")
        video_frame.pts = pts
        video_frame.time_base = time_base
        return video_frame


# ----------------------------------------
# WebRTC OFFER/ANSWER 処理
# ----------------------------------------
pcs = set()

async def index(request):
    with open("index.html", "r", encoding="utf-8") as f:
        return web.Response(text=f.read(), content_type="text/html")


async def offer(request):
    params = await request.json()
    offer = RTCSessionDescription(sdp=params["sdp"], type=params["type"])

    pc = RTCPeerConnection()
    pcs.add(pc)

    # 映像トラック追加
    video = PiCameraTrack(picam2)
    pc.addTrack(video)

    @pc.on("connectionstatechange")
    async def on_connectionstatechange():
        logger.info(f"Connection state: {pc.connectionState}")
        if pc.connectionState in ["failed", "closed", "disconnected"]:
            await pc.close()
            pcs.discard(pc)

    await pc.setRemoteDescription(offer)
    answer = await pc.createAnswer()
    await pc.setLocalDescription(answer)

    return web.json_response(
        {"sdp": pc.localDescription.sdp, "type": pc.localDescription.type}
    )


# ----------------------------------------
# サーバ起動
# ----------------------------------------
def main():
    app = web.Application()
    app.router.add_get("/", index)
    app.router.add_post("/offer", offer)

    logger.info("Starting WebRTC server on http://0.0.0.0:8080")
    web.run_app(app, host="0.0.0.0", port=8080)


if __name__ == "__main__":
    main()

ブラウザー(index.html)

WebRTC 接続処理を含む、最小の受信ページです。

<!DOCTYPE html>
<html lang="ja">
<head>
  <meta charset="utf-8">
  <title>Raspberry Pi WebRTC FPV</title>
  <style>
    body { margin:0; background:#000; }
    #video { width:100vw; height:100vh; object-fit:contain; }
    #start { position:fixed; top:20px; left:20px; padding:8px 16px; }
  </style>
</head>
<body>

<button id="start">接続開始</button>
<video id="video" autoplay playsinline></video>

<script>
let pc = null;

async function start() {
  pc = new RTCPeerConnection({
    iceServers: [{ urls: "stun:stun.l.google.com:19302" }]
  });

  pc.ontrack = (event) => {
    document.getElementById("video").srcObject = event.streams[0];
  };

  const offer = await pc.createOffer({ offerToReceiveVideo: true });
  await pc.setLocalDescription(offer);

  // ICE candidate が揃うまで待つ
  await new Promise(resolve => {
    if (pc.iceGatheringState === "complete") resolve();
    else pc.addEventListener("icegatheringstatechange", () => {
      if (pc.iceGatheringState === "complete") resolve();
    });
  });

  const response = await fetch("/offer", {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify(pc.localDescription)
  });

  const answer = await response.json();
  await pc.setRemoteDescription(answer);
}

document.getElementById("start").onclick = start;
</script>

</body>
</html>

サーバ起動 & 接続手順

Raspiでのターミナル操作はVNC接続でPCから行い、カメラ映像はwebRTCで受信します。
まずはRaspi上にて、

hostname -I

例:192.168.x.xx などIPアドレスを確認

python3 server.py

サーバ起動(server.pyindex.html が同じフォルダにあること)

PC or ゴーグルで ブラウザーを開いて:

http://192.168.x.xx:8080
  1. 「接続開始」ボタンを押す
  2. 1〜2秒でカメラ映像が出るはずです
  3. ブラウザを 全画面表示 にして出力するとFPV表示になります。

以上

タイトルとURLをコピーしました