kensaurus

Field note · A 24/7 live stream

Streaming a mountain 24/7 from one small PC: what broke, and what fixed it

One NUC on Wi-Fi streams our deck camera to YouTube 24/7. The PC fell behind, night ran slow, silence stalled it, the text jumped. How each got fixed.

Kenji Sakuramoto · kensaurus合同会社 ·

On 8 November I put the deck camera at Tsumagoi Work&Camp, our mountain workplace 1,444 m up in Tsumagoi, Gunma, on YouTube, live around the clock. It looks out at Mt Asama, the valley and the field. The first evening, it buffered.

Watch it → youtube.com/@tsumagoicamp/live

The live stream at 09:04: autumn hills and a valley under a grey sky, with the weather plate at the top left, a booking QR code at the top right, and a pixel-art hermit at the bottom left asking a riddle
The stream at 09:04 on 9 November: our weather station top left, a booking QR top right, Sennin bottom left in the middle of a riddle. A real frame from the stream.

The setup

A Tapo C530WS outdoor camera on the deck, and one Intel NUC that reads it and sends it to YouTube. The NUC is on Wi-Fi only. One ffmpeg process does the video. A small Python supervisor around it restarts it, measures its speed and writes a health file that the Ranch Monitor reads, so a stalled stream shows up on the screen I already watch.

What broke, in the order it broke

The broadcast kept ending

I started with YouTube's "Stream now". Each time the encoder restarted, YouTube ended the broadcast, and its link and watch time went with it: four times in the first hour. A scheduled broadcast on its own key, with auto-stop off, fixed it. A reconnect now stays inside the same video.

The PC fell behind

My first version downloaded every 1080p frame from the graphics chip to draw the text on it, then uploaded it back. With nothing else running, that managed 1.25x real time. When the machine got busy, it fell behind, frames piled up into swap, and YouTube dropped the stream twice in four minutes.

Next, the frame stayed on the GPU the whole way: decode, crop, scale, colour and encode. Only the strips that carry text came down to the CPU to be drawn on, and went back on top. That held 25 frames a second, but on the morning of 9 November, with three strips coming down, ffmpeg still used about 70% of one CPU core.

Now nothing comes down. Each text plate is drawn on its own small transparent canvas and blended onto the video on the GPU (overlay_vaapi keeps the transparency). ffmpeg uses about 31% of one core and holds 25 frames a second in real time.

A frame of the stream with three areas outlined in gold: the weather plate at the top left, the QR code at the top right and Sennin's message box at the bottom left
The text areas that used to come down to the CPU, outlined in gold on a real frame from 9 November (the outlines are added for this page). Their plates are now drawn on transparent canvases and blended on the GPU.

Night ran slow

After dark the camera switches to infrared and sends about 12 to 16 frames a second, still stamped as if there were 25. On the camera's own clock the stream played at about 0.7x, the bitrate sagged, and viewers watched a spinner.

Now every frame is stamped with the wall-clock time it arrives, and a steady 25 frames a second is the very last step before the encoder. The evening line was also losing about 5% of packets beyond our router, so the NUC got BBR congestion control and the bitrate came down to 4,500 kbit/s. YouTube Studio read "Excellent" through that evening.

The stream at night in black-and-white infrared: a bare tree lit up on the left, dark hills and a few lights in the valley
22:54 on 8 November, in infrared, with the earlier plate layout. A real frame from the stream.

Silence stalled it

YouTube wants an audio track, and the camera's microphone was never going on air: at best wind noise, at worst a guest's voice. Silence turned out to be the hard part. Silence as a separate input drifted away from the video: the audio stood still while the video ran on, then burst, and Studio said it was not receiving enough video. Silence made inside the filter died every 94 seconds or so. Forcing strict interleaving locked the muxer at 0 kbps.

What worked was borrowing the camera's own audio track only for its clock. Its samples are zeroed, then replaced by a generated soft wind (brown noise, slow gusts, 40 to 900 Hz) from an expression that never reads the input. No microphone sound can leave.

Every restart is every viewer's problem

A restart is a few seconds of nothing, and everyone watching buffers. So I stopped tweaking it live. Changes go out in batches, and the one automatic restart (a memory guard, about every two and a half hours) stays inside the same broadcast.

Worth leaving open

A mountain on a loop starts to feel fake within the hour. So the picture carries things that are true right now.

Some of his lines are about the mountains in view, from sourced data, and about the cows that graze there sometimes.

The live stream at 10:41: Mt Asama and the autumn valley under cloud, tents and a fence in the foreground, the weather plate top left with 3.2°C and a daylight bar, the booking QR top right, and Sennin bottom left saying 迷ったら、まず寝る。仙人の基本。 in white
10:41 on 9 November: Sennin's line in Japanese, in white. A real frame from the stream.
The same view a minute later, at 10:42: Sennin bottom left now says Unsure? Sleep first. Hermit basics. in sky blue
10:42, the same line in English, in sky blue. A real frame from the stream.

His typing used to jump: the line could be swapped while he was still typing it. Now each line starts at the next minute mark on the stream's own clock, types out over about four seconds, and stays put until the next.

Type !omikuji in the live chat and, within a minute, Sennin reads your fortune out on the stream, by name. One fortune a person a day. Names are cleaned to 16 safe characters, the YouTube ID is kept only as a hash, and the rows are deleted after a week.

Since 9 November the chat has a few more games. !guess (!予想) is a guess at tonight's low; at 7:00 Sennin names the closest, from our station's own overnight minimum. !view (!見る) 1, 2 or 3 votes for one of three views, and the camera glides there and holds for three minutes. !gm (!おはよう) marks the first viewer after sunrise. !answer (!答え) answers his riddle, and the first right answer is named when the answer comes up.

Sennin's message box, enlarged: a pixel-art face with glasses and a white beard, and the line いまの運勢:中吉。南西がいい。
A scheduled fortune at 08:42 on 9 November ("Your luck now: fair. The southwest is good."). A real frame, enlarged.

Privacy

The frame is cropped, not painted over. The microphone never streams. We don't record the stream and rewind is off. On request, for a shoot, an event or a guest who asks, the stream switches to a pause slate. There are pixel masks for the tents in view, but they are switched off now. I turn them on by hand when guests are in view; switching them from the bookings is not built yet.

The stream is free to watch.

See it in the exhibit hall