GStreamer source element for UVC H.264 (and H.265) capture devices — DJI action cameras and compatible USB UVC hardware. Developed by UnlimitedIRL; forked and maintained under CeraLive.
Feeds raw H.264/H.265 bitstream into the cerastream pipeline. HDMI capture paths bypass this element entirely.
Security: CVE-2026-1991 (null-deref in scan-streaming path) is fixed in the CeraLive fork at commit
eae7f49(first shipped in tagceralive-v0.0.7.2, carried forward inceralive-v0.0.7.9, SHAada082b5009e38a89eb7cd6176683b508cd99ff5) and also carried aspatches/cve-2026-1991-scan-streaming-nullguard.patchfor the upstream fallback path. Upstream libuvc is effectively dead (last commit 2024); the CeraLive fork athttps://github.com/CeraLive/libuvc.gitis the canonical dependency.
Display on HDMI output (Rockchip, kernel 6.6):
gst-launch-1.0 libuvch264src index=0 \
! video/x-h264,width=1920,height=1080,framerate=30/1 \
! queue ! h264parse ! queue ! v4l2slh264dec ! queue ! videoconvert ! kmssink
Display on HDMI output (Rockchip, kernel 5.10):
gst-launch-1.0 libuvch264src index=0 \
! video/x-h264,width=1920,height=1080,framerate=30/1 \
! queue ! h264parse ! queue ! mppvideodec ! queue ! videoconvert ! kmssink
Select device by USB serial number:
gst-launch-1.0 libuvch264src index="serial:CAM-001" \
! video/x-h264,width=1920,height=1080,framerate=30/1 \
! queue ! h264parse ! fakesink
Select device by vendor:product ID (hex):
gst-launch-1.0 libuvch264src index="1234:5678" \
! video/x-h264 ! fakesink
Pan/tilt/zoom (capability-gated — silently ignored if device doesn't support it):
gst-launch-1.0 libuvch264src index=0 pan=18000 tilt=0 zoom=100 \
! video/x-h264 ! fakesink
Use the libuvch26xsrc alias when working with H.265. It registers the same element under a dual-codec name that makes the codec intent explicit.
H.265 decode to display (Rockchip, kernel 6.6):
gst-launch-1.0 libuvch26xsrc index=0 \
! video/x-h265,width=1920,height=1080,framerate=30/1 \
! queue ! h265parse ! queue ! v4l2slh265dec ! queue ! videoconvert ! kmssink
H.265 decode to display (Rockchip, kernel 5.10):
gst-launch-1.0 libuvch26xsrc index=0 \
! video/x-h265,width=1920,height=1080,framerate=30/1 \
! queue ! h265parse ! queue ! mppvideodec ! queue ! videoconvert ! kmssink
H.265 capture by serial number — pipe to fakesink for testing:
gst-launch-1.0 libuvch26xsrc index="serial:CAM-002" \
! video/x-h265,width=3840,height=2160,framerate=30/1 \
! queue ! h265parse ! fakesink
These examples wire a separate alsasrc for audio. The element itself carries video only.
ALSA device numbering varies by platform. Check aplay -l to confirm the correct card index.
Common values: hw:2 on generic Linux desktops, hw:5 on RK3588-based boards.
H.265 video + AAC audio muxed to MPEG-TS file:
gst-launch-1.0 \
libuvch26xsrc index=0 \
! video/x-h265,width=1920,height=1080,framerate=30/1 \
! h265parse ! queue ! mux. \
alsasrc device=hw:2 \
! audioconvert ! audioresample \
! audio/x-raw,rate=48000,channels=2 \
! voaacenc bitrate=128000 ! queue ! mux. \
mpegtsmux name=mux ! filesink location=output.ts
RK3588 variant (ALSA card 5):
gst-launch-1.0 \
libuvch26xsrc index="serial:CAM-001" \
! video/x-h265,width=1920,height=1080,framerate=30/1 \
! h265parse ! queue ! mux. \
alsasrc device=hw:5 \
! audioconvert ! audioresample \
! audio/x-raw,rate=48000,channels=2 \
! voaacenc bitrate=128000 ! queue ! mux. \
mpegtsmux name=mux ! filesink location=output.ts
H.264 video + AAC audio muxed to MPEG-TS file:
gst-launch-1.0 \
libuvch264src index=0 \
! video/x-h264,width=1920,height=1080,framerate=30/1 \
! h264parse ! queue ! mux. \
alsasrc device=hw:2 \
! audioconvert ! audioresample \
! audio/x-raw,rate=48000,channels=2 \
! voaacenc bitrate=128000 ! queue ! mux. \
mpegtsmux name=mux ! filesink location=output.ts
Note: Opus audio is not compatible with MPEG-TS. Use
voaacencfor anympegtsmuxpipeline. If you need Opus, mux into Matroska instead (see the MKV recording example below).
H.265 + AAC streamed over SRT (listener mode):
gst-launch-1.0 \
libuvch26xsrc index=0 \
! video/x-h265,width=1920,height=1080,framerate=30/1 \
! h265parse ! queue ! mux. \
alsasrc device=hw:2 \
! audioconvert ! audioresample \
! audio/x-raw,rate=48000,channels=2 \
! voaacenc bitrate=128000 ! queue ! mux. \
mpegtsmux name=mux \
! srtserversink uri="srt://0.0.0.0:9000?mode=listener" latency=200
H.264 streamed over SRT (caller mode, connecting to a remote server):
gst-launch-1.0 \
libuvch264src index=0 \
! video/x-h264,width=1920,height=1080,framerate=30/1 \
! h264parse ! queue ! mux. \
alsasrc device=hw:2 \
! audioconvert ! audioresample \
! audio/x-raw,rate=48000,channels=2 \
! voaacenc bitrate=128000 ! queue ! mux. \
mpegtsmux name=mux \
! srtsink uri="srt://192.168.1.100:9000?mode=caller" latency=200
Record H.265 + AAC to MP4:
gst-launch-1.0 \
libuvch26xsrc index=0 \
! video/x-h265,width=1920,height=1080,framerate=30/1 \
! h265parse ! queue ! mux. \
alsasrc device=hw:2 \
! audioconvert ! audioresample \
! audio/x-raw,rate=48000,channels=2 \
! voaacenc bitrate=128000 ! queue ! mux. \
mp4mux name=mux ! filesink location=recording.mp4
Record H.265 + Opus to Matroska (MKV) — Opus is valid here:
gst-launch-1.0 \
libuvch26xsrc index=0 \
! video/x-h265,width=1920,height=1080,framerate=30/1 \
! h265parse ! queue ! mux. \
alsasrc device=hw:2 \
! audioconvert ! audioresample \
! audio/x-raw,rate=48000,channels=2 \
! opusenc ! queue ! mux. \
matroskamux name=mux ! filesink location=recording.mkv
Record H.264 video only to MKV:
gst-launch-1.0 libuvch264src index="1234:5678" \
! video/x-h264,width=1920,height=1080,framerate=30/1 \
! h264parse ! matroskamux ! filesink location=recording.mkv
A UVC device can go silent while still fully present on the bus — enumerated and
answering every control transfer, but delivering nothing. A close/reopen does not
clear that state; only a USB port reset does. Before reporting a disconnect the
element therefore issues one libusb_reset_device(), then polls for the device
to re-enumerate, reopens, and waits for a real frame — because a successful
uvc_start_streaming() on a still-wedged device returns OK and delivers nothing,
so only a delivered frame proves recovery. This needs no configuration and no
extra privilege, and a genuinely unplugged device still surfaces the usual
RESOURCE/READ error (the reset simply fails).
Nothing here is timed against a particular camera: recovery finishes as soon as
frames actually flow. reset-settle-max-ms (default 8000) budgets the element's
own readiness loop and reset-rearm-frames (default 30) is the delivered-frame
proof required before the one-shot re-arms, so it can never loop.
reset-settle-max-ms is not a worst-case guarantee: uvc_stop_streaming() and
uvc_close() are synchronous with no interruption seam, so a device that is
still re-enumerating can push the total past the budget (measured ~22 s against
an 8 s budget). Size it for the fast path.
Recover a wedged device with a wider budget (slow-enumerating camera):
gst-launch-1.0 libuvch264src index=0 reset-settle-max-ms=15000 \
! video/x-h264,width=1920,height=1080,framerate=30/1 \
! queue ! h264parse ! fakesink
A port reset can leave a device that never comes back: the kernel retries
enumeration, each attempt fails with error -71, and it gives up with
unable to enumerate USB device. There is no device left to reset a second
time — only the port.
deep-port-recovery=true adds one escalation for exactly that case, after the
reset and all of its reopens have already failed. If the device object survived
it does a device-level authorized re-probe; if it did not, it cycles the
port's disable attribute so the hub sees a fresh connect-change. Either way it
then requires a real delivered frame before calling the device recovered, and
otherwise falls through to the usual RESOURCE/READ error.
It writes USB sysfs, so it needs to run as root — embedded in a root service it works, and as a normal user it logs that it was denied instead of pretending. It refuses to act on a port whose hub carries any other device, and on a device whose vid:pid no longer matches the one it recorded before the reset.
This is a logical port-state cycle, not a proven VBUS power removal. It is
off by default because on the reference board it demonstrably fires — the port
really does drop to Powered-off and re-enumerate from scratch — and the
error -71 device still did not come back, 3 out of 3 attempts. Turn it on only
where it has been shown to help.
gst-launch-1.0 libuvch264src index=0 deep-port-recovery=true \
! video/x-h264,width=1920,height=1080,framerate=30/1 \
! queue ! h264parse ! fakesink
Set reconnect=true to enable in-element auto-reconnect when the device is unplugged
mid-stream. The element retries with exponential backoff (1, 2, 4, 8, 16 s; up to 5
attempts) before posting an error. Default is false — a disconnect immediately ends
the stream.
A vid:pid or serial: selector survives a replug (bus address can change). An ordinal
or bus: selector may resolve to a different physical device after replug.
H.264 with reconnect enabled (serial selector survives replug):
gst-launch-1.0 libuvch264src reconnect=true index="serial:CAM-001" \
! video/x-h264,width=1920,height=1080,framerate=30/1 \
! queue ! h264parse ! fakesink
H.265 with reconnect enabled (vid:pid selector):
gst-launch-1.0 libuvch26xsrc reconnect=true index="1234:5678" \
! video/x-h265,width=1920,height=1080,framerate=30/1 \
! queue ! h265parse ! fakesink
The Unix-domain PTZ control socket is off by default. Set control-socket=true to
enable it. The element auto-selects a per-instance path under $XDG_RUNTIME_DIR:
$XDG_RUNTIME_DIR/libuvch264src-<pid>-<seq>.sock
Read the resolved path back after the element reaches PAUSED. Two instances in the same process get distinct paths automatically.
Enable the opt-in PTZ control socket:
gst-launch-1.0 libuvch264src index=0 control-socket=true \
! video/x-h264 ! fakesink
# After PAUSED: g_object_get(element, "control-socket-path", &path, NULL)
Explicit socket path (useful in containers where XDG_RUNTIME_DIR is unset):
gst-launch-1.0 libuvch264src index=0 \
control-socket=true \
control-socket-path=/run/ceralive/ptz.sock \
! video/x-h264 ! fakesink
The socket accepts JSON commands for PAN_TILT, ZOOM, GET_POSITION, and
GET_CAPABILITIES. It routes through the same helpers as the native pan/tilt/zoom
properties — same clamping, same capability gate, same locking.
In C, read the resolved path after PAUSED:
gchar *path = NULL;
g_object_get(src, "control-socket-path", &path, NULL);
/* use path, then: */
g_free(path);| Kernel | H.264 decoder | H.265 decoder | Encoder (both codecs) |
|---|---|---|---|
| 5.10 | mppvideodec |
mppvideodec |
mpph264enc / mpph265enc |
| 6.6 | v4l2slh264dec |
v4l2slh265dec |
mpph264enc / mpph265enc |
On kernel 5.10, mppvideodec handles both H.264 and H.265 via the Rockchip MPP layer. On kernel 6.6, the V4L2 stateless decoders are codec-specific.
The element's caps advertise alignment=au, and it delivers on that: every buffer it
pushes is exactly one access unit — one displayed picture — no matter how many NAL
units the camera split that picture into. Multi-slice encoding (common at 2160p) is
therefore handled transparently; h264parse, h265parse, and the hardware decoders
in the pipelines above see whole frames, not fragments.
A picture's slices are grouped by its Access Unit Delimiter when the camera emits one, and by first-slice-of-picture detection when it does not. Nothing to configure.
This element stamps PTS as pipeline running-time. Residual A/V drift with a Bluetooth microphone is a downstream concern — the BT clock runs independently of the pipeline clock. Add audioresample in the audio branch to absorb BT clock drift, or clock-slave the audio source to the pipeline master clock.
| Property | Type | Default | Description |
|---|---|---|---|
index |
string | "0" |
Device selector: ordinal "0", "vid:pid" (hex), "serial:<sn>", or "bus:<bus>:<addr>" |
pan |
int | 0 |
Absolute pan in UVC arcseconds (±648000); capability-gated |
tilt |
int | 0 |
Absolute tilt in UVC arcseconds (±648000); capability-gated |
zoom |
int | 0 |
Absolute zoom as UVC focal length (0..65535); capability-gated |
control-socket |
bool | false |
Enable opt-in Unix-domain PTZ control socket (default off) |
control-socket-path |
string | null |
Explicit socket path; auto-selects $XDG_RUNTIME_DIR/libuvch264src-<pid>-<seq>.sock when null |
reconnect |
bool | false |
Auto-reconnect on mid-stream disconnect with exponential backoff (default off) |
max-payload |
uint | 0 |
USB payload transfer size hint in bytes (dwMaxPayloadTransferSize); 0 = device default; nonzero clamped to [512, 4194304] with read-back |
transfer-buffers |
uint | 0 |
USB transfer buffer count hint; 0 = library default (no device write); nonzero clamped to [2, 100], applied right before streaming starts (CeraLive fork only; no-op with a warning on upstream libuvc) |
reset-settle-max-ms |
uint | 8000 |
Budget (ms) for the element's own readiness loop after a port reset: re-enumeration polling + reopen retries + wait for the first real frame. A budget, not a delay. Does not bound the synchronous libuvc teardown, which can push the total past it |
reset-rearm-frames |
uint | 30 |
Frames the device must deliver after a recovery before the one-shot port reset re-arms for a later wedge |
auto-port-reset |
bool | true |
Issue the silence-triggered USB port reset; set false to skip USBDEVFS_RESET and use normal disconnect handling |
deep-port-recovery |
bool | false |
Escalate once more when the port reset AND its reopens have both failed: a device-level authorized re-probe, or a port-level disable cycle if the device no longer enumerates. Needs root; never runs on a hub carrying another device. Off by default — proven to fire on hardware, not proven to recover |
Action signal: set-ptz(pan, tilt, zoom) — drives all three axes in one call; returns TRUE if at least one supported axis succeeded.
sudo apt install build-essential cmake git meson pkg-config
sudo apt install libgstreamer1.0-dev libgstreamer-plugins-base1.0-dev
sudo apt install libusb-1.0-0 libusb-1.0-0-dev
# 1. Build libuvc (CeraLive fork, default) — no patch step needed
scripts/build-libuvc.sh
# To use upstream v0.0.7 + patches fallback instead:
# LIBUVC_USE_FORK=OFF scripts/build-libuvc.sh
# 2. Build the plugin
meson setup build libuvch264src/
cd build && meson compile && meson install
# 3. Move .so to the system GStreamer path (multiarch-aware)
MULTIARCH=$(gcc -print-multiarch)
sudo mv /usr/local/lib/${MULTIARCH}/gstreamer-1.0/libgstlibuvch264src.so \
/lib/${MULTIARCH}/gstreamer-1.0/
sudo cp /usr/local/lib/libuvc.* /usr/lib/${MULTIARCH}/$(gcc -print-multiarch) resolves to aarch64-linux-gnu on arm64 and x86_64-linux-gnu on amd64. Do not hardcode the arch string.
The test suite is hardware-independent — it uses a libuvc mock and does not require a UVC device.
# With sanitizers (recommended)
cmake -B build -DENABLE_SANITIZERS=ON && cmake --build build && ctest --test-dir build --output-on-failure
# Without sanitizers (faster)
cmake -B build && cmake --build build && ctest --test-dir build --output-on-failureThese are manual checks for a real device on real hardware — they are not part of the (hardware-independent) ctest suite. Work top-down: confirm the library, then the plugin, then the device.
The plugin links the pinned CeraLive libuvc fork. Confirm the shared object and its version are visible to the loader:
# Is a libuvc resolvable, and which one?
pkg-config --modversion libuvc 2>/dev/null || echo "libuvc not on pkg-config path"
ldconfig -p | grep -i libuvc
# What does the installed plugin actually link against?
MULTIARCH=$(gcc -print-multiarch)
ldd /lib/${MULTIARCH}/gstreamer-1.0/libgstlibuvch264src.so | grep -i 'libuvc\|not found'A libuvc.so* => not found line means the loader cannot find libuvc — copy it
to the system lib dir (see Build Steps step 3) or add its directory to
LD_LIBRARY_PATH.
gst-inspect-1.0 libuvch264srcThis must print the element's factory details (pads, the index/pan/tilt/
zoom/control-socket/reconnect/max-payload properties, and the
set-ptz action signal). If instead you see "No such element or plugin":
- Confirm the
.sois on the plugin path:gst-inspect-1.0 --versionthenGST_PLUGIN_PATH=/lib/${MULTIARCH}/gstreamer-1.0 gst-inspect-1.0 libuvch264src. - Force a clean scan after copying the
.so:rm -f ~/.cache/gstreamer-1.0/registry.*.bin. - Re-run with
GST_DEBUG=GST_PLUGIN_LOADING:5 gst-inspect-1.0 libuvch264srcto see why the load was rejected (an unresolved libuvc symbol points back to step 1). Thelibuvch26xsrcalias should also resolve.
# Is the camera on the USB bus at all?
lsusb
# Does libuvc see it as a UVC device? (uvc_find_devices / any libuvc example tool)
GST_DEBUG=libuvch264src:5 \
gst-launch-1.0 libuvch264src index=0 ! fakesink 2>&1 | head -40Useful selectors when index=0 resolves to the wrong device (see Properties):
index="vid:pid", index="serial:<sn>", or index="bus:<bus>:<addr>". A
malformed selector fails start() loudly with a RESOURCE/SETTINGS error rather
than silently selecting device 0. If enumeration works but start() cannot claim
the interface, check that no other process (a desktop webcam app, v4l2 capture)
already holds the device.