WARNING: this is entirely vibe-coded. It works for me, but it may set your computer and camera on fire. Everything in here (especially comments) might be totally wrong. Use with some skepticism.
Capture analog video (composite CVBS or S-Video) on a Raspberry Pi Zero 2W using the ADV7280 uHat Shield. Configured for PAL by default; NTSC is supported with minor changes (see below).
- Hardware (was) available from Ethermata on Tindie.
- Hardware creator's repo: lerabot/dvx-nebula.
- Based on sdtv2csi by rniwase.
This does not capture sound. I don't remember if the hardware would support that, I think it would have to go through the rPi directly. I capture sound separately, so there's nothing in here that does sound.
- Raspberry Pi Zero 2W
- ADV7280 uHat Shield (Hackaday.io)
- SD card (Class 10 / A1 recommended -- captures run ~1.1 MB/s)
- Analog video source (composite CVBS or S-Video)
- (optional) push button, LED + resistor
The hat connects via the 40-pin GPIO header and routes video through the CSI ribbon connector.
Analog video → ADV7280 → CSI → V4L2 (/dev/video0) → GStreamer → MJPEG AVI
The ADV7280 converts the analog signal to a UYVY stream over MIPI CSI-2. A patched adv7180
kernel module sets the correct frame dimensions.
GStreamer encodes using the Pi's hardware JPEG encoder (v4l2jpegenc), which accepts UYVY
directly with no CPU-side colour conversion. The Pi Zero 2W cannot do real-time H.264 from this
source: the required UYVY→YUV420p conversion maxes out at ~23 fps, below the 50 fps input rate.
With I2P deinterlacing (the default), the ADV7280 outputs one progressive frame per field,
doubling the rate: 50 fps for PAL, ~60 fps for NTSC. Captures are saved as MJPEG AVI
(~1.1 MB/s). make recordings syncs to the Mac and transcodes to H.265.
setup-adv.py has configurable options near the top: input source (CVBS/S-Video), signal
standard, I2P deinterlacing, AGC, contrast, and adaptive contrast. After changing settings:
make settings-updateIn setup-adv.py:
STANDARD = "NTSC"In recorder.py:
GST_CAPS = "video/x-raw,format=UYVY,width=720,height=507"
GST_FRAMERATE = "60000/1001" # NTSC + I2P (~59.94 fps)(With I2P disabled: GST_FRAMERATE = "30000/1001".) make stream uses the same settings.
- Flash Raspberry Pi OS Bookworm (64-bit Lite) with Raspberry Pi Imager. Set hostname to
camera, enable SSH, configure Wi-Fi. - Boot the Pi, then from your Mac/PC (requires
sshpassandffmpeg--brew install sshpass ffmpeg):
make initial-setupUploads scripts, installs packages, patches config.txt, builds and installs the patched
adv7180 kernel module (takes a few minutes), and reboots. The recorder service starts
automatically on every subsequent boot.
To re-apply after changing recorder.py or other scripts (without rebuilding the kernel module):
make setupTo reduce boot time (optional, reboots):
make optimizeThe Pi runs recorder.service on every boot. The LED does 3 quick blinks when
the recorder is ready, then a double-blink every 2.5 seconds while idle. Press
the button to start or stop recording.
Recordings are saved to /home/pi/recordings/ as video_YYYYMMDD_HHMMSS.avi.
To sync to the Mac, transcode to H.265, and delete from the Pi:
make recordings
# -> recordings/video_YYYYMMDD_HHMMSS.mp4 (H.265, CRF 16, normal speed)
# -> recordings/video_YYYYMMDD_HHMMSS_slow.mp4 (H.265, CRF 16, half speed)| Physical pin | GPIO | Connection |
|---|---|---|
| 38 | GPIO 20 | Button: one leg to pin 38, other leg to GND |
| 40 | GPIO 21 | LED anode (+) → 150Ω → GPIO 21; cathode (−) → GND |
GPIO assignments can be changed via BTN_TOGGLE and LED_GPIO at the top of recorder.py.
stills-from-video (included) extracts JPEG frames from an AVI at a chosen rate.
Creates a directory named after the file; frames saved as frame_000001.jpg, etc.
Maximum JPEG quality (-q:v 1). Runs on Mac/PC; requires ffmpeg.
stills-from-video 1 video_20261015_143022.avi # one frame per second
stills-from-video 10 video_20261015_143022.avi # 10 frames per second
stills-from-video 50 video_20261015_143022.avi # every frame (50fps source)
# -> video_20261015_143022/frame_000001.jpg ...make streamStreams MJPEG from the Pi and plays it locally via ffplay.