Repurposing a Xiaomi Smart Camera C302 (indoor pan-tilt, China-cloud only) into a low-latency FaceTime / Zoom / Meet camera over the Tailscale tailnet via go2rtc + WebRTC, without adding it to Frigate / the NVR, and while keeping the existing Tapo NVR cams fully air-gapped. Built on telep-mainframe over 2026-08-08/09. This note captures the full pipeline plus the long gotcha gauntlet that cost the most time.
SUPERSEDED (2026-08-15) — the C302 has been REMOVED;
192.168.30.168is now a DEAD IPThe Xiaomi C302 is no longer deployed.
192.168.30.168no longer hosts any camera — treat any reference to it as historical. The C302 was the only camera that ever used the separatego2rtc-campath; the Tapo NVR cams (.119DÉL /.139ÉSZAK) go direct RTSP into Frigate, not throughgo2rtc-cam. Any downstream config that still pings.168is stale — e.g. the OliveTin “Kamerák” action was corrected on 2026-08-15 to ping.119/.139instead. See 2026-08-15-camera-alerts-dead-tailscale-magicdns-docker-dns. This note is kept for the go2rtc/WebRTC pipeline and gotchas, which remain reusable.
For Agents
- Container:
go2rtc-camat/home/levander/go2rtc-cam/on telep-mainframe (separate from Frigate’s own go2rtc).- Ports: api
:1984, rtsp:8555, webrtc:8556(Frigate’s go2rtc already holds:8554).- Tailnet URL:
https://telep-mainframe.taild4189d.ts.net:8450→ WebUI. Winning stream page:…:8450/webrtc.html?src=c302.- Cam: C302 at
192.168.30.168on thecamsVLAN; H265; Xiaomi P2P source, cloud-brokered key = NOT air-gappable.- Client: OBS on the Mac → macOS Window Capture of a chromeless Chrome app-window rendering the WebRTC page → OBS Virtual Camera → FaceTime. Sub-second latency.
- Privacy: C302 is on a hardware relay (physically powered off when unused). A red “BELSŐ KAMERA AKTÍV” banner lights the camwall TV whenever the cam pings.
- Secrets live OUTSIDE this vault — see Where the secrets live. Nothing sensitive is recorded here.
Goal & privacy stance
Use the C302 as an on-demand meeting camera with the lowest possible latency, but:
- Do NOT add it to Frigate / the NVR (it’s a personal meeting cam, not surveillance).
- Keep the Tapo NVR cams (
.119DÉL /.139ÉSZAK) air-gapped — the C302’s needs must not reopen the whole camera VLAN to the internet. - The C302 cannot be network-air-gapped and still stream (its go2rtc source requires cloud login for a per-connection key — see gotcha #3). Since we can’t isolate it at the network layer, isolation is physical: the C302 is powered through a hardware relay and is only energized during a call. A persistent red banner on the camwall TV makes “the indoor cam is live” impossible to miss.
Architecture
End-to-end pipeline
graph LR C302["Xiaomi C302<br/>192.168.30.168<br/><i>H265, P2P</i>"] -->|Xiaomi P2P<br/>cloud-brokered key| G2["go2rtc-cam<br/>on telep-mainframe<br/>:1984 / :8555 / :8556"] MiCloud["Mi Home cloud<br/>region <b>de</b>"] -.->|per-conn key| G2 G2 -->|tailscale serve<br/>--https=8450| TS["tailnet<br/>telep-mainframe.taild4189d.ts.net:8450"] TS -->|WebRTC<br/>H265 native| CH["Chrome app-window<br/>webrtc.html?src=c302"] CH -->|macOS Window Capture| OBS["OBS + Virtual Camera"] OBS -->|Camera Extension| FT["FaceTime / Zoom / Meet"] style C302 fill:#3d2020,stroke:#d00000,color:#fff style G2 fill:#264653,stroke:#2a9d8f,color:#fff style MiCloud fill:#2d2d2d,stroke:#888,color:#fff style OBS fill:#264653,stroke:#2a9d8f,color:#fff
go2rtc-cam container
At /home/levander/go2rtc-cam/ on telep-mainframe:
- Image
alexxit/go2rtc,network_mode: host,restart: unless-stopped. - Config at
config/go2rtc.yaml:api: { listen: ":1984" }rtsp: { listen: ":8555" }— not:8554, which Frigate’s own go2rtc restream (127.0.0.1:8554) already holds. This is a second, independent go2rtc instance living alongside Frigate’s.webrtc: { listen: ":8556", candidates: [ "192.168.1.123:8556", "100.115.209.87:8556" ] }
- Streams:
c302: xiaomi://<account>:de@192.168.30.168?did=1183965641&model=xiaomi.camera.c302n— the native Xiaomi P2P source (H265).deis the account region (see gotcha #1).c302_h264: ffmpeg:c302#video=h264#audio=aac— a transcode fallback for players that can’t decode H265 (see gotcha #5). go2rtc streams are LAZY — zero CPU until something actually consumes them, so keeping this defined costs nothing when unused.
Explicit WebRTC candidates are mandatory here
Because the container is
network_mode: host, go2rtc binds all interfaces — including the docker0 and Incus/veth bridges (172.17.x,172.18.x,172.19.x). Left to auto-discovery it advertises junk bridge IPs as ICE candidates and WebRTC fails to connect. The explicitcandidates:list pins the two that actually work: the LAN IP192.168.1.123:8556and the tailnet IP100.115.209.87:8556. (Same class of “host-network binds everything” bug that produced bogus mDNS records in 2026-08-04-telep-mainframe-airplay-receiver-uxplay.)
Tailnet exposure (interim)
Exposed with a shared-node tailscale serve on the telep-mainframe node:
sudo tailscale serve --bg --https=8450 http://127.0.0.1:1984
→ https://telep-mainframe.taild4189d.ts.net:8450.
Follow-up — should get its own tailnet node
Per tailnet-service-exposure-convention, new services should get their own dedicated
tag:telepnode + hostname (e.g.c302.taild4189d.ts.net) rather than a sharedserveport. This deployment used the interim shared-serve pattern to get running; migrating it to a dedicated node is a noted follow-up.
Client side (Mac): OBS → Virtual Camera → FaceTime
The winning capture pipeline is WebRTC-in-a-real-browser + OBS macOS Window Capture (the why is gotchas #6–#8):
- Open the WebRTC page in a chromeless Chrome app-window:
open -na "Google Chrome" --args --app="https://telep-mainframe.taild4189d.ts.net:8450/webrtc.html?src=c302" - In OBS, add a macOS Window Capture source targeting that Chrome window (NOT OBS’s built-in browser source — see gotcha #7).
- Start OBS Virtual Camera (OBS 28+ registers as a system Camera Extension), then pick “OBS Virtual Camera” inside FaceTime / Zoom / Meet.
Trade-off: keep the Chrome window visible during calls
macOS Window Capture of a minimized window goes blank. The Chrome app-window must stay open and un-minimized (it can be off to the side / on another Space) for the duration of the call.
Router firewall scoping — the C302 is NOT air-gappable, so scope it per-IP
The C302’s Xiaomi P2P needs both directions on the cams VLAN (telep-router):
cams → wan— to reach the Mi Home cloud for the per-connection key (gotcha #3).cams → lan— so the cam’s P2P return packets can reach go2rtc on the LAN (gotcha #4).
Because the cam genuinely needs internet + LAN return traffic, go2rtc cannot air-gap it. The fix is to scope the exposure to that one device rather than the whole VLAN:
- The temporary zone-wide
cams→wan/cams→lanforwardings (which had opened the whole camera VLAN, including the Tapo cams) were replaced with per-IP rules:c302-wanandc302-lan, both withsrc_ip=192.168.30.168.
- This restores full air-gap for the Tapo NVR cams (
.119/.139) — only192.168.30.168can talk to WAN/LAN; the surveillance cams stay isolated with no internet. - Firewall backups are on the router.
Relationship to prior camera-VLAN posture
This is a per-IP refinement of the VLAN-wide
cams→wanopened in Changes Made (2026-08-03). That change gave all cams internet; this note narrows it back down to just the C302, re-air-gapping the Tapo cams.
Presence indicator — internal-cam-alert.service
A persistent red banner on the camwall TV whenever the indoor cam is powered/online, so it’s obvious the C302 is live:
- Unit:
internal-cam-alert.service→/usr/local/bin/internal-cam-alert.py(root,DISPLAY=:0,After=camwall-x). - Pings
192.168.30.168every 3s. State machine: ACTIVE after 1 successful ping, INACTIVE after 3 consecutive failures. - When ACTIVE, draws a full-width override-redirect banner
BELSŐ KAMERA AKTÍV— 120px white text on#d00000, geometry3840x200+0+0, no window manager,-topmost. Clears when the hardware relay cuts power to the cam. - Enabled, survives reboot. Rides the same
:0X display as the camwall / uxplay overlays (2026-08-04-telep-mainframe-airplay-receiver-uxplay, 2026-07-28-camwall-4-substream-composite).
The gotcha gauntlet
The parts that cost the most time — read before touching any of this.
1. Xiaomi login
70016"登录验证失败" = wrong region, not wrong passwordThe C302’s Mi account lives on server
de. go2rtc’s WebUI login flow does handle captcha + email 2FA — but with the wrong region the request fails with70016before the challenge is ever presented. Once the region was set tode, go2rtc passed both the captcha and the 2FA and logged in. If you see70016, fix the region first; don’t chase the credentials.
2. Getting did / model / account / region — the tokens extractor
Use PiotrMachowski’s Xiaomi Cloud Tokens Extractor (it handles captcha + 2FA). Installed at
~/xiaomi-tokens/on the Mac.
- Its console output is NOT written to a file by default — pipe it:
... | tee out.txtto keep the values.- Values obtained: account
6426258469, regionde, did1183965641, modelxiaomi.camera.c302n, cam IP192.168.30.168, home id600001097108.
3. The xiaomi source needs a cloud LOGIN — you can't feed it pre-obtained tokens
go2rtc’s
xiaomi://source authenticates to the Mi cloud to obtain per-connection keys (“local video, cloud-brokered key”). You cannot hand it the extractor’s token and skip login — it must log in itself (credential stored by go2rtc as<account>: V1:***). Consequence: this route is inherently not air-gappable — hence the physical relay + per-IP firewall scoping instead of network isolation.
4. P2P across the VLAN needs
cams → lanWithout the LAN return path, go2rtc logs
read udp i/o timeoutand delivers 0-byte frames — the cam’s P2P return packets get silently dropped at the VLAN boundary. Addingcams→lan(scoped to.168) fixed it. This is why the cam can’t be purecams→wanonly.
5. The C302 streams H265 — RTSP into OBS shows black
OBS’s RTSP Media Source cannot display H.265 (renders black). That’s what the
c302_h264ffmpeg transcode was for. But the better answer is WebRTC: a real browser decodes H265 natively, so the WebRTC path needs no transcode at all.
6. OBS Media Source black even when VLC plays the same stream → disable hardware decoding
On macOS, uncheck “Use hardware decoding” on the OBS Media Source. That single toggle was the fix for the RTSP path being black while VLC showed it fine.
7. OBS's built-in browser (CEF) renders WebRTC gray — use a real browser + Window Capture
The exact same
webrtc.html?src=c302is snappy and sub-second in Safari/Chrome, but OBS’s built-in browser source (CEF) renders it gray — a known OBS limitation. The workaround that won: open the page in a chromeless Chrome app-window and use OBS macOS Window Capture of it. Sub-second into FaceTime. (See the client-side section for the exactopen -nacommand and the keep-it-visible trade-off.)
8. The latency floor is the TRANSPORT, not the camera
WebRTC in a real browser is sub-second, which proves the C302’s P2P path is fine. The laggy variants were the transports: MSE was ~1s, and RTSP + H264 transcode was the worst. Reach for WebRTC first; only fall back to the transcode for players that can’t do WebRTC/H265.
9. No local PTZ — the C302 has no miio endpoint
The C302 does not expose the local miio endpoint (UDP
54321unreachable; TCP554/6467closed). Motor / pan-tilt control is Mi Home cloud only — there is no LAN PTZ. For “Center Stage”-style auto-framing, the path is an OBS face-tracking plugin (digital PTZ / crop-follow; not installed).
10. Setup-time router gotchas
- The cams SSID
telep-cc(WPA2/psk2, client isolation on) is normally hidden; it was temporarily unhidden for Mi Home onboarding, then re-hidden.- telep-router’s SSH host key changed again (lingering effect of the 2026-08-03 factory reset) — clear + re-accept
known_hosts; this is expected, not an intrusion.
Where the secrets live
No secrets are stored in this vault
- Mi Home account password — not recorded anywhere in the vault; entered interactively into the tokens extractor / go2rtc login.
- go2rtc-stored credential (
V1:***auth blob) — lives only insidego2rtc-cam’s config/state on telep-mainframe at/home/levander/go2rtc-cam/.- Camera token / per-connection keys — brokered live by the Mi cloud per connection; never persisted to the vault. The non-sensitive identifiers (did, model, region, home id, LAN IP) are safe and are recorded above because they’re needed to reconstruct the config.
Related
- telep-mainframe — the host; runs Frigate’s own go2rtc on
:8554, hence this second instance on:8555/:8556. - telep-mainframe-handover — host handover / operational context.
- telep-router — the site gateway where the per-IP
camsfirewall scoping lives. See Camera VLAN (telep-cc) and Changes Made (2026-08-03). - tailnet-service-exposure-convention — the dedicated-node-per-service rule this deployment defers on (interim shared
tailscale serve). - 2026-08-08-bambuddy-deploy-plan — same go2rtc / host-network / tailnet-service family; also faces the
network_mode: hostexposure trade-off. - 2026-08-04-telep-mainframe-airplay-receiver-uxplay — the red banner rides the same
:0X display as uxplay/camwall overlays; also a “host-network binds everything” bug. - 2026-07-28-camwall-4-substream-composite — the camwall TV the presence banner draws over.
- 2026-08-03-telep-router-factory-reset-recovery — origin of the recurring router SSH host-key change.