Any RTSP camera and a computer that stays on is enough to put a camera on YouTube permanently, with nothing bought. The channel's Live tab has both of ours, running since 2026-08-29 from the machine described on the Frigate-on-Windows page; the pasteable scripts, the exact ffmpeg arguments and the numbers they produce are on the ffmpeg page.
What the two streams are
The mosaic. Four cameras in one 1920×1080 picture, two by two: Face and Water on top, Treat and Bed underneath. Ten frames a second, 3.5 Mbit/s, a music bed we synthesise ourselves. This is the stream a viewer leaves open on a second monitor. One URL, four jobs; if she is not at the bowl she is probably on the bed. At the time of writing it is watch?v=HaRC7bsF-pM.
The Face cam. One camera, head-on at the food bowl, full 2560×1440 at 15 frames a second, 2.5 Mbit/s. This is the camera that is not public on the website (only a curated still is), so the 24/7 stream is the only place to see it move. At the time of writing it is watch?v=JrPoY5QHLdw.
Why a mosaic instead of four streams? YouTube treats each live stream as a separate video with its own chat, its own thumbnail and its own place in the feed. Four of them dilute the audience four ways and quadruple the things that can go wrong at 3 a.m. One mosaic gives a viewer the whole room, and the one single-camera stream exists because Face is the money shot and deserves the full resolution.
The pane choice is a privacy decision, written in the script itself. The overhead Court camera sees the room's entrance, where people pass. Face does not. So the public mosaic shows Face, not Court, and the Court camera stays on the website as a refreshing still that a person can check but nobody can scrub.
Why there is no OBS
OBS is a screen-and-scene compositor for a person sitting at the machine. We wanted a service: something that starts at boot, runs as SYSTEM with no desktop, and recovers on its own. ffmpeg is that, since it reads RTSP, stacks pictures, talks RTMP and has no window.
The other reason is that the cameras were already in one place. Frigate uses go2rtc to pull each camera once and re-serve it on rtsp://127.0.0.1:8554/<camera>, so Frigate's detector, Frigate's recorder and the live-stream ffmpeg all read the same local copy and the cameras see one client. Nothing on the YouTube side ever touches a camera password. The setup for that half is on the camera-setup page.
| Process | What it does |
|---|---|
| 4 × tile | Reads one camera's 640×360 sub-stream from go2rtc, drops the audio, copies the video untouched into MPEG-TS on a local UDP port. No decode, no encode. About 20 MB of memory each; 22 CPU-seconds in 80 minutes. |
| 1 × mosaic | Reads the four UDP ports, scales each pane to 960×540, stacks them 2×2, loops the music, encodes with h264_qsv on the Intel UHD 630, pushes RTMP. |
| 1 × Face | Reads the Face camera's full 2560×1440 main stream, decodes it with d3d11va, re-times to 15 fps, loops its own music, encodes with h264_qsv, pushes RTMP. |
| Both encoders together | About 2,230 CPU-seconds over 80 minutes on a 12-thread i7-8700, under 4 % of the machine, because the actual H.264 work is on the iGPU. The whole box, with Frigate decoding ten cameras at the same time, sat at 14–15 %. |
The two rules that took a week
Rule one: never stream-copy a camera straight to YouTube. The obvious first attempt is -c:v copy from the camera to RTMP, because it costs nothing. It ran, and YouTube showed it, and then the player buffered and skipped while ffmpeg's log filled with non-monotonic DTS warnings. A Tapo camera emits frames on its own clock with no audio YouTube can use; YouTube's ingest wants constant frame rate, a keyframe every two seconds, and an audio track. The fix is to decode and re-encode for the streams YouTube sees: re-time to a fixed 10 or 15 fps with -vsync cfr, set the GOP to two seconds' worth of frames (-g 20 at 10 fps, -g 30 at 15), and mux in a looping WAV. The tiles still stream-copy, because they only go one hop over loopback to the compositor, which is what re-times them. The camera's own microphone is never used; the comment in the script says Never camera mic and means it.
Rule two: watch the socket, not the process. The first watchdog restarted ffmpeg when it exited, and ffmpeg does not exit when YouTube hangs up. In the 2026-08-30 outage schtasks reported the task as Running and the process was alive; the TCP connection to YouTube's port 1935 had gone to CloseWait and ffmpeg was writing into a dead pipe. The watchdog now gives a fresh ffmpeg 45 seconds to complete the RTMP handshake (observed at 10–50 s), and after that requires Get-NetTCPConnection -OwningProcess <pid> -RemotePort 1935 to show at least one Established row every five seconds. Anything else, it kills and restarts. The wrapper log for the afternoon of 2026-09-02 shows what that looks like in practice: twelve RTMP connection unhealthy (state=CloseWait) restarts one minute apart while YouTube's edge was unhappy, then a session that has run clean since 16:48 with speed=1x and the bitrate pinned at 3,650 kbit/s. Nobody noticed.
There is a third, smaller rule. The four tile processes have no socket to watch, and a plain -c:v copy from an RTSP source that has been open for ten hours can quietly start passing broken frames (non-existing PPS 0 referenced in the compositor's log). So a daily task at 04:00 stops and restarts the four tiles and then the compositor, and that failure has not come back.
-
Get the camera into go2rtc.
If you run Frigate you already have go2rtc; add the camera under
go2rtc: streams:with both its RTSP paths (Tapo:stream1main,stream2sub). From then on everything readsrtsp://127.0.0.1:8554/<camera>. On Windows this works because WSL2 is in mirrored networking mode; see the WSL2 files. - Create the stream in YouTube Studio. Go live → Stream → Create stream. Copy the server URL and the stream key. The default key is persistent: it does not change when ffmpeg reconnects, so a restart resumes the same broadcast. The key is a credential: anyone with it can broadcast as your channel.
-
Put the URL and the key in a .env file next to the script.
Two lines,
YT_RTMP_URL=andYT_STREAM_KEY=. The script reads the file at start and builds the destination from it. The scheduled task's XML never contains the key, so exporting or sharing the task is safe. -
Install ffmpeg and test one camera by hand.
winget install Gyan.FFmpeggives you anffmpeg.exewithh264_qsvandd3d11vabuilt in (we run 8.1.1). In a console, run one command: RTSP in over TCP,-vf fps=15,format=nv12, a looping WAV as the second input,h264_qsvat 2,500 kbit/s with-g 30, AAC 128k,-f flvto<YT_RTMP_URL>/<YT_STREAM_KEY>. Within a minute Studio's Stream health should say Excellent or Good. If it says "no data", check the key; if it complains about frame rate or keyframes, you have skipped rule one. -
Wrap it in a restart loop with the socket check.
A PowerShell
while($true)that starts ffmpeg withStart-Process -PassThru, polls every five seconds, kills it over a memory cap, and after 45 seconds kills it if there is noEstablishedconnection to port 1935. Log the reason each time. The complete script is on the ffmpeg page. -
Register it as a scheduled task at boot.
Run as SYSTEM, trigger At startup, hidden window,
MultipleInstancesPolicy IgnoreNewso a second copy can never start. Check the task's "Stop the task if it runs longer than" setting: Task Scheduler defaults to three days, and ours still showed 72:00:00 when we looked, masked only by the daily refresh. Set it to unlimited. -
Verify with the socket, not the task list.
Get-NetTCPConnection -RemotePort 1935 | Select OwningProcess, State. OneEstablishedrow per stream. Then open the watch page on a phone and count to ten; the stream should be within about fifteen seconds of real time at these settings.
About the music
YouTube wants an audio track and a silent one is treated as a fault, so every stream carries a bed. Ours is synthesised from scratch in Python (numpy and scipy, nothing sampled: one loop-perfect cue of 18 to 20 seconds per station, all in one key, sharing a three-note signature) and rendered to 44.1 kHz stereo WAV, so there is nothing for Content ID to match and nothing to license on a channel meant to run for years. If you use a library track instead, keep the attribution in the stream description and check the licence covers a broadcast that never ends.
What this does not give you
- It is not a security feed. There is 10–20 seconds of latency and YouTube can pause the broadcast whenever it likes. The recording that matters is Frigate's, on the local disk; see retention.
- Windows Update will reboot the box. Both streams are boot tasks and come back on their own; the stream key is persistent so the watch URL survives. YouTube may still create a new video id if the broadcast is recreated, which is why the durable link is the Live tab.
- The iGPU has limits. Two QSV encoders plus detection on the same UHD 630 is comfortable. Four 1080p encoders would not be. If you want more streams, make one mosaic bigger rather than adding encoders.
- No chat moderation, no overlays, no scene switching. That is what OBS is for.