§1The problem
The capture card only opens in one program at a time, so several apps fought over it, and there was no simple way to watch the captured screen on the iPad.
§2What I built
- Two pagesOne page on the computer that opens the capture card and streams; one on the iPad that only watches.
- Shared capture cardFive virtual copies of the capture card, one per app, kept alive by a relay.
- TuningFine adjustment of latency and image quality.
§3The hard parts
5 entries- 01
One reader per virtual device
The virtual driver only accepts one reader per device: solved with one copy per app and a single encode shared by all of them.
- 02
Black screen with no error
The capture card changes the JPEG size mid-stream and Chrome drops it; and the wrong colour format arrives but doesn't decode. Measured by counting real frames.
- 03
Blurry text when the HDMI signal changes
Detected by reading the ffmpeg log and reopening the input.
- 04
Latency from ~290 ms to ~90 ms
By limiting the encoder's threads and sending each packet immediately.
- 05
Orphan processes holding the capture card
Solved with signal handling and shutting down child processes.
§4Result
- relay latency (was ~290 ms)
- ~90 ms
- virtual copies of one capture card
- 5
Running on my computer; the virtual copies serve several apps at the same time. Relay latency is ~90 ms (my own measurement, compared with reading the capture card directly). The stream goes up to 1080p at 30 fps, without sound.
§5Stack
- Plain Node + WebSocket
- WebRTC (H.264)
- ffmpeg
- v4l2loopback (Linux)
- Shell script
- pm2
- Tailscale