3D visionExplainer

Depth camera color stream on UVC or V4L2: why some show up

Why a depth camera's color stream only appears as a V4L2 device when RGB ships over UVC, plus the one v4l2-ctl command that shows formats before you code.

LumaSpec Vision11 min read Share on LinkedIn

On this page 9 sections
  1. 01Why doesn't a depth camera's color stream appear at /dev/video0?
  2. 02Which depth cameras deliver their color stream over UVC?
  3. 03How can one command tell whether the color stream is a V4L2 device?
  4. 04How do you grab the UVC color stream with GStreamer?
  5. 05Why does the format list offer MJPEG at 1080p but raw YUYV only at 640×480?
  6. 06What are the limits of pulling RGB over UVC?
  7. 07What works when the color stream only lives inside the SDK?
  8. 08How do you choose between the SDK path and the V4L2 path?
  9. 09Questions

Your depth camera's SDK viewer shows a clean color image, yet there is no /dev/video0 to open, and the GStreamer pipeline has nothing to grab. That is not a broken driver or a permission problem: on most depth cameras the color stream is not a webcam at all. Some models ship the RGB sensor as a standard UVC device that V4L2 lists like any USB camera. The rest carry color frames inside the proprietary depth transport, reachable only through the vendor SDK. This guide shows how to tell the two cases apart with one command, what the UVC path looks like in GStreamer, the bandwidth arithmetic behind the format list, and how to choose the right transport per camera.

Why doesn't a depth camera's color stream appear at /dev/video0?

A depth camera's color stream appears at /dev/video0 only when that model delivers RGB as a standard UVC device. Models that carry RGB inside their proprietary depth transport never create a V4L2 capture node, even though the SDK's own viewer shows color.

Behind one USB cable a depth camera is usually several logical devices: a depth sensor, an IR projector control path and, on some models, a separate RGB camera. The vendor picks a transport for each stream at design time. RGB delivered as UVC is claimed by the kernel's uvcvideo class driver and shows up as a normal capture node. RGB carried inside the depth protocol shares the channel the vendor SDK reads; no USB class driver claims it and no node appears.

The SDK's bundled viewer proves nothing either way, because it speaks the proprietary protocol directly. The split is fixed in firmware: it is not a setting, and re-plugging, re-enumerating or changing group permissions will not create a node.

If the camera is UVC-class but your application runs in a container, the node exists on the host and the container simply cannot see it. Pass the right device nodes through, as Camera in Docker: pass V4L2 media nodes, not just /dev/video0 shows.

Which depth cameras deliver their color stream over UVC?

There is no family-wide rule: one vendor ships a Pro-class variant of the same housing purely because its RGB sensor sits on its own UVC interface, while the base model of that same line tunnels color through the depth SDK.

The split runs straight through single product families: base model with color inside the depth middleware transport, Pro-class variant of the same housing with RGB on its own UVC interface. Same look, same depth performance, opposite answer to the /dev/video question. The model number and its interface description decide, not the product family, and the 3D sensors category mixes both kinds.

Two follow-on facts save time. On UVC-color variants the color stream often disappears from the SDK at the same moment, because the RGB sensor left the middleware channel. And some cameras carry the depth pipe itself over UVC in vendor-specific formats, so seeing depth formats in a V4L2 list does not mean color is there too. Ordinary area scan cameras follow the same logic: a USB3 Vision model speaks its own transport and never appears as a webcam.

How can one command tell whether the color stream is a V4L2 device?

Run v4l2-ctl --list-formats-ext on every /dev/video* node. A color stream that V4L2 can reach lists Motion-JPEG or YUYV frames at photographic resolutions, while SDK-tunneled hardware lists no photographic format anywhere.

List what the kernel registered first:

v4l2-ctl --list-devices

A UVC depth camera often registers two or three nodes per physical unit, so interrogate each one. Which of them holds color is the same pick any multi-node USB setup faces: Linux camera device selection: find the RGB stream walks it. The formats come from:

$ v4l2-ctl -d /dev/video0 --list-formats-ext
ioctl: VIDIOC_ENUM_FMT
	Type: Video Capture

	[0]: 'MJPG' (Motion-JPEG, compressed)
			Size: Discrete 1920x1080
				Interval: Discrete 0.033s (30.000 fps)
			Size: Discrete 1280x720
				Interval: Discrete 0.033s (30.000 fps)
	[1]: 'YUYV' (YUYV 4:2:2)
			Size: Discrete 640x480
				Interval: Discrete 0.033s (30.000 fps)

(Output from a typical UVC color node; your camera prints its own list.)

Read it like this. Motion-JPEG or YUYV entries at photographic resolutions are the color stream, ready for V4L2. Vendor-specific formats at small frame sizes mean the depth pipe itself is UVC-visible. No photographic format on any node means RGB rides inside the SDK transport, and no V4L2 command will reach it.

One embedded exception: on minimized industrial Linux builds, even a genuine UVC camera registers nothing because the kernel was built without the UVC class driver, and the device sits on the bus unseen. That is a kernel configuration problem with a known fix — see why a UVC camera is not recognized on a minimized Linux kernel.

How do you grab the UVC color stream with GStreamer?

Build the pipeline straight from the format list: if the color node prints image/jpeg at 1920×1080 and 30/1 fps, v4l2src with those exact caps, jpegdec and a sink plays it with no vendor runtime in the process.

gst-launch-1.0 v4l2src device=/dev/video0 ! image/jpeg,width=1920,height=1080,framerate=30/1 ! jpegdec ! videoconvert ! autovideosink

Every cap in that line copies a row the format list printed: image/jpeg, 1920×1080, framerate 30/1. Change one number and negotiation fails on the first try. For a YUYV-only node, drop jpegdec and request video/x-raw,format=YUYV instead. In an application, replace autovideosink with appsink so frames land in your process, or with filesink to capture a test clip.

Failures come in a predictable order. Permission denied is group membership on the device node, not a transport problem — work through the Linux camera device permission checks. A sharp monochrome image means the RGB node was not the one opened; on depth cameras that is usually the IR stream — see why the depth camera's RGB looks black and white. The wrong camera answered after a second unit was added: nodes are handed out in detection order, so select by serial number as in multiple depth cameras on one host: select each by serial number.

Why does the format list offer MJPEG at 1080p but raw YUYV only at 640×480?

Because uncompressed YUYV at 1920×1080 and 30 fps needs 124 MB/s — about 995 Mbit/s — and a USB 2.0 bus carries 480 Mbit/s at most. High resolutions ship compressed as Motion-JPEG, and raw YUYV appears only at sizes that fit.

bytes_per_frame = width × height × bytes_per_pixel MB_per_s = bytes_per_frame × fps ÷ 1,000,000 Mbit_per_s = MB_per_s × 8

YUYV is 4:2:2 with 8 bits per component, so every pixel costs 2 bytes.

  1. 1920 × 1080 = 2,073,600 pixels per frame.
  2. 2,073,600 px × 2 B = 4,147,200 B ≈ 4.15 MB per frame.
  3. 4.15 MB × 30 fps = 124.4 MB/s.
  4. 124.4 MB/s × 8 = 995 Mbit/s.

A USB 2.0 bus carries 480 Mbit/s, so uncompressed 1080p30 cannot exist on it, which is exactly why the list offers Motion-JPEG there. At a typical 10:1 MJPEG compression (an estimate; dark or detailed scenes compress less), 995 Mbit/s drops to roughly 100 Mbit/s, about 12.4 MB/s, and fits with margin. The YUYV entry fits because the frame is small: 640 × 480 = 307,200 px × 2 B = 614,400 B ≈ 0.61 MB, × 30 fps = 18.4 MB/s ≈ 147 Mbit/s. On a USB 3.x cable the usable rate rises to roughly 4 Gbit/s, uncompressed 1080p30 fits, and the format list shows it.

What lands in the list was decided by the sensor-and-bridge pair the camera was designed around; how sensor and bridge selection shapes a UVC Motion-JPEG camera walks through that design side.

When 1080p Motion-JPEG stutters or drops frames on an otherwise healthy link, raise the kernel's USBFS buffer pool: the usbcore module's usbfs_memory_mb parameter, 16 MB by default, is quickly exhausted by long compressed frames.

What are the limits of pulling RGB over UVC?

UVC hands you a webcam-class interface: Motion-JPEG or raw YUV frames plus consumer controls, but no hardware trigger, no binning, and no shared timestamp clock with the depth pipe. Any RGB-to-depth alignment becomes a job for your software.

The control set shrinks to what a webcam exposes — exposure, gain, white balance, focus — through standard UVC controls. There is no trigger input, no strobe output and no frame counter cross-checked against depth, so a grabbed frame carries only the host's receive time.

For stationary parts that costs little: register the two streams once and the offset stays constant. For moving conveyor scenes the offset turns into blur and parallax between the images; correct it with the depth to RGB registration offset fix and match the two stream resolutions first, as depth to color alignment breaks up: match stream resolution first explains.

Where the UVC path earns its keep is plumbing: color into an RTSP relay, a web dashboard, a recorder or anything inside a container, with zero vendor runtime and an interface any Linux engineer already knows. The depth SDK keeps running beside it for depth only.

What works when the color stream only lives inside the SDK?

Three honest options exist: open the stream through the SDK's API in your own process, use a GStreamer source plugin for that transport where one ships, or move color to hardware whose RGB sensor enumerates as a separate UVC device.

Opening the SDK inside your process is the baseline: the depth API returns color frames alongside depth. If the downstream stack is GStreamer-based, push those frames into an appsink so the transport stays proprietary while the frames flow anywhere GStreamer reaches. Before writing that bridge, check whether the middleware already ships a GStreamer source plugin; the OpenNI middleware has one in the GStreamer plugin set.

When the SDK hands you pre-aligned color baked into the depth frame, treat that as a data-layout question rather than a transport one — aligned depth streams: data layout and which intrinsics apply covers which intrinsics apply to that arrangement.

The last option is hardware. Specify the next camera so its RGB sensor enumerates as UVC, or mount a separate UVC camera over the same field and register the streams in software. If that choice is open right now, the camera finder filters by interface, and RGB and depth stream interface selection for vision cameras compares the stream-level options.

How do you choose between the SDK path and the V4L2 path?

Take the SDK when you need depth and color registered or timestamped together. Take V4L2 when color must flow into GStreamer, containers or web services without a vendor runtime. Take both side by side on models where RGB is a UVC device.

Requirement SDK path UVC / V4L2 path
Depth and color registered or timestamped together Natural — the SDK aligns them No shared clock; align in software
Color into GStreamer, a container or a web service Works via an appsink bridge Direct from v4l2src, no vendor runtime
Trigger or strobe control on the RGB image Only where the SDK exposes it Not available over UVC
Camera not purchased yet Any model works Filter for models whose RGB enumerates as UVC

The decision in one pass: if v4l2-ctl printed photographic formats, take color over V4L2 for every color-only consumer, keep depth in the SDK, and align the two in software at the frame level. If it printed none, write against the SDK API or its GStreamer plugin — and if the stack must stay SDK-free for color, add a dedicated UVC camera instead of fighting the transport.

Before ordering hardware for a new station, make the 30-second check part of camera selection: one v4l2-ctl call on a sample unit, or one interface question to the vendor. The camera configurator steps through the camera choice, and the remaining depth integration checks live in the 3D topic hub.

Cameras3D sensors

Laser profilers and stereo 3D cameras

170 models · from $699.00

Questions

Can the depth stream also come over V4L2?

On some depth camera families the depth pipe itself travels as a UVC stream in a vendor-specific format, so v4l2-ctl lists it next to the color formats. As a rule of thumb on such devices, 640×480 at 30 fps is the one frame size every stream type accepts, depth or color. If the format list shows only Motion-JPEG and YUYV, that node is color-only and depth stays inside the SDK.

Why does each UVC camera register two video nodes?

Modern kernels create a capture node plus a metadata-only node for each UVC interface; the metadata node carries frame timestamps and sequence numbers and cannot be opened as a video source. Open the capture node and leave the metadata node alone unless your stack needs per-frame kernel timestamps. The type line in --list-formats-ext shows Metadata Capture on the one to leave alone.

Does any of this apply to GigE Vision cameras?

No. V4L2 and UVC are USB mechanisms, while a GigE Vision camera enumerates through the GigE Vision and GenICam stack with its own vendor library, and it never appears as /dev/videoN. The enumeration question only arises on USB-connected cameras; treat a GigE depth or area-scan camera as SDK-reachable by design, whatever its color sensor resolution.

Why did the /dev/video numbering change after a second camera was plugged in?

Node numbers are handed out in detection order, not tied to a camera: the unit that was /dev/video0 today can enumerate as /dev/video2 tomorrow. Select devices by serial number in code, or pin them with a udev rule that creates a stable alias; in containers, pass the specific device nodes rather than relying on the numbering.

Do V4L2 color and SDK depth together overload the USB cable?

Only if you pick uncompressed color. On a USB 2.0 cable, 1080p30 Motion-JPEG costs roughly 12.4 MB/s (about 100 Mbit/s at a typical 10:1 ratio) and a 640×480 16-bit depth stream costs 18.4 MB/s (147 Mbit/s), so both together draw about 31 MB/s — roughly 250 of the bus's 480 Mbit/s, leaving margin. Swap Motion-JPEG for raw YUYV at 1080p and the pair needs 142.8 MB/s, which no USB 2.0 link carries.