Source code for go2.modules.video.streaming.streamer

import asyncio
import threading
import socket

from aiortc import RTCPeerConnection, RTCSessionDescription, RTCRtpSender
from aiohttp import web

from .stream_config import StreamConfig
from .stream_track import OpenCVStreamTrack
from .serve import HTML_CONTENT
from ....logging import get_logger

logger = get_logger(__name__)


# TURN for public conn? https://www.100ms.live/blog/webrtc-python-react#interactive-connectivity-establishment---ice
[docs] class WebRTCStreamer: def __init__(self, stream_config: StreamConfig): self._stream_config = stream_config self._track = OpenCVStreamTrack(stream_config) self._pcs: set[RTCPeerConnection] = set() self._loop: asyncio.AbstractEventLoop = None self._server_thread: threading.Thread = None def _start_in_thread(self): def _run(): self._loop = asyncio.new_event_loop() asyncio.set_event_loop(self._loop) self._loop.run_until_complete(self._run_server()) self._server_thread = threading.Thread(target=_run, daemon=True) self._server_thread.start() def _send(self, frame): if frame is None: return self._track.push_frame(frame) def _get_local_ip_address(self): s = None try: s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) s.connect(("8.8.8.8", 80)) ip = s.getsockname()[0] except Exception: ip = "127.0.0.1" finally: if s: s.close() return ip def _get_port(self): return self._stream_config.port async def _serve_html(self, request: web.Request) -> web.Response: return web.Response(text=HTML_CONTENT, content_type="text/html") async def _offer(self, request): params = await request.json() offer = RTCSessionDescription(sdp=params["sdp"], type=params["type"]) pc = RTCPeerConnection() self._pcs.add(pc) transceiver = pc.addTransceiver(self._track, direction="sendonly") try: caps = RTCRtpSender.getCapabilities("video") vp8_codecs = [c for c in caps.codecs if "VP8" in c.mimeType.upper()] if vp8_codecs: transceiver.setCodecPreferences(vp8_codecs) except Exception: pass # Just best effort @pc.on("connectionstatechange") async def _on_state(): logger.debug(f"[WebRTC] {pc.connectionState}") if pc.connectionState in ("failed", "closed", "disconnected"): await pc.close() self._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, }) async def _run_server(self) -> None: app = web.Application() app.router.add_get("/", self._serve_html) app.router.add_post("/offer", self._offer) runner = web.AppRunner(app) await runner.setup() self._runner = runner site = web.TCPSite(runner, self._stream_config.host, self._stream_config.port) await site.start() while True: await asyncio.sleep(3600) def _shutdown(self) -> None: async def _async_shutdown(): await asyncio.gather(*[pc.close() for pc in self._pcs], return_exceptions=True) self._pcs.clear() if hasattr(self, "_runner"): await self._runner.cleanup() if self._loop and self._loop.is_running(): asyncio.run_coroutine_threadsafe(_async_shutdown(), self._loop)