This is the CeraLive fork of libuvc, maintained for the
gstlibuvch264src GStreamer
capture element (DJI action cameras and UVC H.264/H.265 devices).
Forked from libuvc/libuvc at SHA 68d07a00e11d1944e27b7295ee69673239c00b4b
(v0.0.7). Hard divergence — not tracking upstream. This fork is CeraLive-owned
and is NOT tracked against, pulled from, or rebased onto upstream
libuvc/libuvc main. The base SHA above is recorded for provenance only.
See CHANGELOG.ceralive.md for the list of CeraLive changes.
CeraLive changes on top of the base:
-
UVC 1.5 header acceptance —
uvc_parse_vc_header()acceptsbcdUVC == 0x0150(unconditional, additive). -
H.265/HEVC format support —
UVC_FRAME_FORMAT_H265enum, GUID registration, and frame handling (unconditional, additive). -
Configurable kernel-driver auto-detach —
uvc_wrap()callslibusb_set_auto_detach_kernel_driver(), gated by the CMake optionLIBUVC_AUTO_DETACH_KERNEL_DRIVER(defaultON). The default preserves DJI/UVC capture behavior. To opt out (manage detach yourself):cmake .. -DLIBUVC_AUTO_DETACH_KERNEL_DRIVER=OFF -
Kernel-driver reattach that survives an uncatchable exit — detaching a driver is undone by a userspace call in the release path, so a process killed with
SIGKILL/SIGSEGV/SIGABRTnever undoes it and leaves both UVC interfaces withdriver = NONEfor good. libuvc now forks a small helper when it first claims an interface; the helper's wakeup is the pipe EOF the kernel delivers on the arming process's death, whatever killed it, and it re-probes the interfaces that are still armed. That helper is created with a rawclone(2), notfork(): glibc'sfork()runs everypthread_atfork()handler registered anywhere in the process, so in a host that also links libsrt the child ran SRT's handler and blocked forever on an SRT mutex a now-vanished thread had held — never reaching the helper, and keeping every descriptor the fork had copied until somethingSIGKILLed it. Gated by the CMake optionLIBUVC_REATTACH_GUARD(defaultON, Linux only):cmake .. -DLIBUVC_REATTACH_GUARD=OFFSee
include/libuvc/reattach_guard.hfor the mechanism and its fail-safe properties, and "Device teardown contract" below for how it relates touvc_close().
The library remains BSD-3-Clause; see LICENSE.txt. CeraLive additions are
also BSD-3-Clause. No license change.
libuvc is a cross-platform library for USB video devices, built atop libusb.
It enables fine-grained control over USB video devices exporting the standard USB Video Class
(UVC) interface, enabling developers to write drivers for previously unsupported devices,
or just access UVC devices in a generic fashion.
Prerequisites: You will need libusb and CMake installed.
To build, you can just run these shell commands:
git clone https://github.com/CERALIVE/libuvc
cd libuvc
mkdir build
cd build
cmake ..
make && sudo make install
and you're set! If you want to change the build configuration, you can edit CMakeCache.txt
in the build directory, or use a CMake GUI to make the desired changes.
BUILD_EXAMPLE enables the example program. BUILD_TEST enables uvc_test, an
interactive OpenCV demo that needs a real UVC camera and a display; it is not the
automated test suite. BUILD_TESTING enables the default-off, hardware-independent
Linux CTest suite. Configure, build, inspect, and run its static build with:
cmake -S . -B build/regression \
-DCMAKE_POLICY_VERSION_MINIMUM=3.5 \
-DCMAKE_BUILD_TYPE=Debug \
-DCMAKE_BUILD_TARGET=Static \
-DBUILD_SHARED_LIBS=OFF \
-DBUILD_EXAMPLE=OFF \
-DBUILD_TEST=OFF \
-DBUILD_TESTING=ON
cmake --build build/regression --parallel
ctest --test-dir build/regression --show-only=json-v1 \
| jq -e '.tests | length == 37'
ctest --test-dir build/regression --output-on-failure
The 37 cases are grouped as descriptor (11: h264, h265,
truncated_format, truncated_frame, degenerate_h26x,
scanner_vc_header_short, scanner_vc_oversized, scanner_vc_zero,
scanner_vs_header_short, scanner_vs_oversized, scanner_vs_zero),
negotiation (5: h264, h265, near_match, probe_set_error,
probe_get_error), transfer (3: terminal_statuses, retry_success,
retry_failure), teardown (8: status_xfer_stops_before_control_release,
every_claimed_interface_released_control_last,
no_status_endpoint_unchanged, sparse_interfaces_control_released_last,
high_index_interfaces_released, cancel_not_found_still_drains,
undeliverable_status_xfer_quarantines, quarantined_handle_stays_armed),
reattach (9: rebinds_after_sigkill, disarmed_interfaces_are_not_rebound,
busy_interface_is_retried, rebinds_despite_a_foreign_atfork_handler,
connect_ioctl_is_what_libusb_would_issue,
claiming_an_interface_arms_the_guard, claim_failure_after_detach_reattaches,
failed_reattach_after_failed_claim_stays_armed,
detach_failure_leaves_nothing_armed), and race
(1: close_races_status_callback).
The ten guard cases exist only when LIBUVC_REATTACH_GUARD is ON; with
-DLIBUVC_REATTACH_GUARD=OFF the suite is the 27 cases that predate it, and CI
runs that configuration too so a rollback stays a real rollback. Four of the
reattach cases really do SIGKILL a forked victim process — that is the point,
since the defect is defined by cleanup code never running. One of those four
also registers a pthread_atfork() handler in the victim and holds its mutex
hostage on a second thread, which is what a host linking libsrt does to the
guard's own fork. The three claim-path cases need no kill at all: they cover a
claim that fails in a process that stays alive, which is a different defect with
the same symptom.
CI runs this suite without camera hardware on Ubuntu 22.04
and Ubuntu 24.04. See
docs/evidence/uvc-camera-compat-stability.md for its exact scope.
LIBUVC_SANITIZE builds the library and the tests with a sanitizer —
instrumenting only the test executable would miss the interesting code, since
the teardown races live in src/device.c and run on the libusb event thread:
cmake -S . -B build/tsan \
-DCMAKE_POLICY_VERSION_MINIMUM=3.5 \
-DCMAKE_BUILD_TYPE=Debug \
-DCMAKE_BUILD_TARGET=Static \
-DBUILD_SHARED_LIBS=OFF \
-DBUILD_EXAMPLE=OFF \
-DBUILD_TEST=OFF \
-DBUILD_TESTING=ON \
-DLIBUVC_SANITIZE=thread
cmake --build build/tsan --parallel
ctest --test-dir build/tsan --output-on-failure \
-R 'libuvc\.(teardown|race|reattach)\.'
libuvc.race.close_races_status_callback is the case this exists for: it drives
uvc_close() against a real libusb event thread, and only an instrumented run
can see a missing happens-before edge between them. Keep the -R filter — the
descriptor, negotiation and transfer suites --wrap free(), which a sanitized
build replaces, so including them reports a mismatched allocator rather than
anything about this code. A dedicated CI job runs exactly this.
uvc_close() owns the whole USB teardown and must keep five invariants that are
not visible from the call site — all regression-locked by the
libuvc.teardown.*, libuvc.reattach.* and libuvc.race.* cases:
-
The VideoControl status interrupt transfer is stopped before any interface is released. It re-arms itself from
_uvc_status_callback(), so a resubmission that lands after the release makes usbfs re-claim the interface from the kernel driver it was just handed back to; the finallibusb_close()then leaves it bound to nothing and the camera's/dev/videoNnever returns.status_mutexkeeps the callback's stopping-check and its resubmission atomic against that stop. -
Every interface in
devh->claimedis released, VideoControl LAST. A failed negotiation can leave the streaming interface claimed, and reattaching the driver to VideoControl is what triggersuvcvideo's probe — a probe that claims the streaming interfaces itself, so it must run after they are free. The order is derived fromdevh->claimedandinfo->ctrl_if.bInterfaceNumber, never assumed: a UVC function may sit at any interface index and expose several VideoStreaming interfaces. -
status_xfer_submittedandstatus_xfer_stoppingare read and written ONLY understatus_mutex, on both threads. They are shared between the closing thread and the libusb event thread.volatile(what they used to be) gives neither atomicity nor a happens-before edge, so the close could observe the transfer "done" and free it — and the handle — while the callback was still inside both. The mutex's unlock/lock pair is what orders the callback's last write before the free. -
A cancel reporting
LIBUSB_ERROR_NOT_FOUNDis still waited out. libusb documents that code as "not in progress, already complete, or already cancelled", and in the last of those the completion callback has not run yet; freeing the transfer there is undefined behaviour by libusb's own contract._uvc_status_callback()is therefore the only thing that ever clearsstatus_xfer_submitted, and the bounded drain is the only way out of the stop. -
Every interface libuvc claims is handed back, on every exit — including the ones where
uvc_close()never runs. Invariants 1–4 all assume the process lives long enough to execute them;SIGKILL,SIGSEGVand a watchdog'sSIGABRTexecute nothing at all, and the kernel does not re-probe an interface when usbfs's claim is dropped at fd-close.uvc_claim_if()therefore arms the interface with the reattach guard before it detaches the driver, and it is disarmed only where the driver is genuinely back — byuvc_release_if()on a normal teardown, or byuvc_claim_if()itself on a claim that never completed. The guard's forked helper does the rest from outside the process.uvc_reattach_guard_destroy()runs fromuvc_free_devh()— the one place the handle is truly released, and reached from neither quarantine path.A claim that fails after its detach succeeded undoes the detach on the spot. The detach and the claim are two kernel calls and only the first can land: a busy device or a kernel racing the same interface fails the second, and the interface is then bound to nothing in a process that is alive and well. Nothing revisits it —
uvc_release_if()skips any interface absent fromdevh->claimed— souvc_claim_if()issues thelibusb_attach_kernel_driver()itself and disarms only if that call reports the kernel has the interface back. This is the one reattach that is not the teardown's, and it is why arming happens at the detach rather than at the claim: between those two calls there is a real interval in which this process is the reason the interface has no driver.The quarantine paths deliberately reattach nothing, and must stay that way. With a quarantined status transfer libusb still owns a URB on the VideoControl status endpoint, so releasing that interface re-creates invariant 1's defect exactly; with a quarantined stream the in-flight URB rides a VideoStreaming interface, so releasing VideoControl alone would make
uvcvideoprobe while usbfs still holds the streaming interfaces and register no video node at all (invariant 2, in reverse). Both leave the handle armed and let the helper repair the binding once this process — and the transfer it was racing — is gone. A quarantine is a leak bounded by the process lifetime, not a wedged camera.libuvc.teardown.quarantined_handle_stays_armedlocks that down.
The documentation for libuvc can currently be found at https://libuvc.github.io/.
The GitHub Actions build check uses a bounded compiler cache for both CMake build
variants. It caches only the workspace-local .ccache directory, keyed by the
runner, compiler identity, and source/build workflow inputs; the build/ and
build-off/ directories are never cached. Local builds do not require ccache, but
you can enable it with CMake's compiler launcher options:
cmake -B build \
-DCMAKE_C_COMPILER_LAUNCHER=ccache \
-DCMAKE_CXX_COMPILER_LAUNCHER=ccache
Happy hacking!