System designTroubleshooting

USB camera black image on Linux: fix UVC format negotiation

A USB camera that shows a black image in Linux apps usually has a UVC format negotiation fault, not a broken link. Find the format that streams and pin it.

LumaSpec Vision11 min read Share on LinkedIn

On this page 10 sections
  1. 01What the symptom already proves
  2. 02Enumerate every format the device advertises
  3. 03Why Linux apps show black: three causes, in order
    1. The app streams a format the device never fills
    2. The payload size is understated and the alternate setting is too small
    3. Your app opened the wrong node, or a second client holds it
  4. 04Worked example: 640 × 480 YUYV at 30 fps on one high-speed endpoint
  5. 05Fix it from the Linux host side
  6. 06Fix it on the device when you own the firmware
  7. 07Two bench parts that shorten the hunt
  8. 08When the format-negotiation answer is wrong
  9. 09Checklist: black image on Linux, one tool still streaming
  10. 10Questions

Your USB camera enumerates everywhere and streams on Windows, but on Linux every application shows a black image, while one V4L2 bench tool displays the frame without complaint. That single working tool settles the first question: the cable, the power and the isochronous endpoints are fine, and the fault sits in format negotiation between the app and the device's descriptors. This guide walks the diagnosis for anyone bringing up a UVC camera, or a USB virtual-camera gadget, on a Linux host: enumerate the advertised formats, find the one combination that carries data, pin it in the application, and, when you own the firmware, correct the reported numbers so every app works. It also gives the bandwidth arithmetic that decides which isochronous alternate setting the host arms.

What the symptom already proves

Start from the two facts you already have. First, the device enumerates: lsusb lists it and at least one /dev/video node exists. Second, one bench tool that lets you pick formats displays real frames. Together they commit you to a diagnosis: the physical link, the enumeration descriptors and the streaming endpoints all work, so hunting cables and ports wastes the morning. The failure is one level up. Either the application requested a combination of pixel format, frame size and frame interval that the device does not deliver, or the host armed too little isochronous bandwidth for the stream it negotiated.

The Windows result narrows it further. A device that streams on Windows generates frames, so only the choice Linux makes, format or alternate setting, is wrong.

If lsusb lists the camera but no video node appears at all, stop: that is a different failure mode, and checking the USB class is the right starting point.

Enumerate every format the device advertises

Run v4l2-ctl --list-formats-ext against every /dev/videoN node the camera created; only one of them is the capture node. The listing gives you the three values that define a stream:

  • the pixel format, as a four-character code such as YUYV, the packed 4:2:2 format at 2 bytes per pixel, or MJPG for MJPEG;
  • the frame sizes, printed as Size: 640x480 lines;
  • the frame intervals, printed as periods, so 0.030s (33.000 fps) means one frame every 30 ms.

Whatever the bench tool used, those three values exist in this listing. Note them exactly, because the host-side fix is to request precisely them. Multi-node devices trip people here: multiple USB cameras on Linux covers stable node naming, and USB camera at /dev/video0 but no video in your app UI covers the application-side open.

Why Linux apps show black: three causes, in order

Cause What you see Decisive test Fix
App streams a format the device never fills Black in apps that prefer MJPEG; frames with the uncompressed format Capture ten seconds per format and compare byte counts Pin the working format, or fill the advertised one in firmware
Reported payload size is understated Empty or torn frames on Linux while Windows plays Compute the per-microframe payload and compare with the armed setting Report the true worst case, or step down size or rate
Wrong node, or a second client holds the stream Black in one app while another still streams Close every client, then open the capture node alone One reader, right node

The app streams a format the device never fills

Most Linux applications negotiate MJPEG first, because a compressed stream fits any hub and any cable. A device whose descriptors list MJPEG but whose firmware only fills uncompressed buffers has committed to something it cannot produce: the interface arms, payloads arrive empty or undecodable, and the window stays black. The mirror case exists too, a gadget that fills only its uncompressed format while the application insists on compressed.

The test takes ten minutes. Step through every advertised format in the bench tool, capture ten seconds of each to a file, and compare sizes: the format the device fills produces a file growing at the full data rate, and the format it does not produces almost nothing. Pin the honest format in your application, and if you own the firmware, either fill what you advertise or stop advertising it. For the compressed path in general, UVC camera MJPEG streaming on Linux covers what to expect from the payload.

The payload size is understated and the alternate setting is too small

After the application picks a format, size and interval, the host asks the device what payload the stream needs, then arms the video-streaming interface at the lowest alternate setting that can carry it. On USB 2.0 high speed, an isochronous endpoint carries 1,024, 2,048 or 3,072 bytes per 125 microsecond microframe. If the figure the device reports, the payload size in its probe response, is smaller than the real stream, the host arms a pipe that cannot hold it and the frames arrive torn or empty.

Two field signs separate this from the first cause. lsusb -v prints each alternate setting's wMaxPacketSize, so you can compare the armed setting against the arithmetic in the next section. And when the negotiated payload needs more than any setting can carry, the Linux driver refuses to start the stream at all, so the open fails instead of showing black.

Your app opened the wrong node, or a second client holds it

A UVC device can expose several /dev/videoN nodes, and only one delivers image data; an application that opens a metadata node gets a black buffer or an error. The other variant is exclusivity: the bench tool still holding the stream when you launch the real application. Close every camera client, confirm the node with v4l2-ctl --list-formats-ext, since only the capture node lists frame sizes, and open that one node alone. The one-client rule is the same behavior as a camera that works in the vendor tool but not in ROS.

Worked example: 640 × 480 YUYV at 30 fps on one high-speed endpoint

The inputs, each with its source:

  • Frame size 640 × 480, the classic row of an uncompressed UVC listing such as the one above.
  • Pixel format YUYV, 2 bytes per pixel.
  • Frame rate 30 fps.
  • USB 2.0 high speed: 8,000 microframes of 125 microseconds per second, and at most 3,072 bytes per microframe on one high-bandwidth isochronous endpoint, built from three 1,024-byte transactions.
bytes per frame = width × height × bytes per pixel payload per second = bytes per frame × frame rate bytes per microframe = payload per second / 8000

The arithmetic:

  • 640 × 480 = 307,200 pixels per frame
  • 307,200 × 2 = 614,400 bytes per frame
  • 614,400 × 30 = 18,432,000 bytes per second, about 18.4 MB/s or 147 Mbps
  • 18,432,000 ÷ 8,000 = 2,304 bytes per microframe

The decision, with its threshold: 2,304 bytes sits under the 3,072-byte cap, so this stream rides a single high-bandwidth endpoint, provided the device reports at least 2,304 bytes. Report 1,024, a figure copied from a minimal example, and the host arms the 1,024-byte alternate setting: 1,024 ÷ 2,304 = 44 percent of the payload delivered, and over half of every frame never arrives.

The cap also gives the ceiling for this format. 3,072 × 8,000 = 24,576,000 bytes per second, and 24,576,000 ÷ 614,400 = 40 fps. Uncompressed 640 × 480 above 40 fps cannot ride one USB 2.0 high-speed endpoint, so drop the rate or switch to MJPEG, where a 640 × 480 frame lands around 30 to 60 kB with content setting the size, some 0.9 to 1.8 MB/s at 30 fps.

Fix it from the Linux host side

  1. Enumerate with v4l2-ctl --list-formats-ext on every /dev/videoN node and keep the output; only the capture node lists frame sizes.
  2. Find the honest combination: step through each format in the bench tool, capture ten seconds per format, and keep the one whose file grows at the computed rate, 18.4 MB/s in the example above.
  3. Pin all three values in your application's open call: the pixel-format code, the width and height, and one interval from the listing. Leaving any of the three on a default hands the choice back to the failure.
  4. Confirm the first frame decodes. One saved frame that opens as an image closes the case; a correctly sized black frame means empty payloads, and torn stripes mean truncation, so go back to the payload arithmetic.
  5. When the application accepts only MJPEG and the device offers none, change one side deliberately: fill MJPEG in the firmware, or move to a camera that offers it, from USB3 area scan cameras in the camera finder.
  6. After a long soak, confirm the camera still enumerates as USB 3 and not USB 2.0; a camera enumerating as USB 2.0 on a USB 3.0 port is the classic silent downgrade.

Fix it on the device when you own the firmware

The virtual-camera case, a microcontroller board streaming a static test frame or a gadget configured through configfs with the UVC function, fails Linux hosts through exactly the same numbers. Three rules make it work everywhere:

  1. Advertise only formats the generator actually fills; an MJPEG entry with uncompressed data behind it passes Windows and starves Linux.
  2. Report the worst-case payload for every format and interval you advertise, computed with the formula above: 2,304 bytes for the 640 × 480 example.
  3. Match the advertised frame interval to the generator's real rate, and list only intervals you can hold.

The kernel's UVC gadget function documentation, in the Linux kernel source tree under its USB gadget section, covers the configfs side of these settings, and configuring a USB gadget's UVC mode walks the same descriptors from the device end. When the device is a bridge chip rather than your own firmware, the advertised list comes from the bridge: selecting sensors and bridges for a UVC MJPEG camera explains how that choice fixes the format table.

Two bench parts that shorten the hunt

Keep one known-good camera and one known-good cable on the bench, and the host-or-device question closes in minutes. The LS-CMR-0166 is a 12 MP USB3 area scan camera with a 1/1.7" rolling-shutter sensor; when it streams in the same application on the same port, the Linux stack is clean and the fault lives in the other device.

Area scan cameras12 MP USB3 area scan camera, IMX226, 1/1.7" rolling shutter, 30 fps, 4000 × 3000 resolution (LS-CMR-0166)LS-CMR-0166
Sensor format
1/1.7"
Interface
USB3
Resolution
12 MP
Shutter
Rolling
$560.00View partBuild a system with it

For the cable, the LS-CBL-0219 is a 3 m USB3.0 camera cable built for drag-chain use, so it survives being moved between benches and between machines.

Cables3 m USB3.0 camera cable, high-flex (drag chain) (LS-CBL-0219)LS-CBL-0219$62.00View partBuild a system with it

Power leads and every other interconnect are in the accessories finder, and a full station can be specced from the camera step of the configurator.

When the format-negotiation answer is wrong

Read this section only when no tool on any port displays a frame:

Checklist: black image on Linux, one tool still streaming

  • One bench tool displays frames, so stop hunting cables and ports.
  • v4l2-ctl --list-formats-ext output saved for the capture node.
  • Honest combination found: the format whose ten-second capture grows at the computed rate.
  • Application pins the format code, the size and one listed interval, with nothing left on a default.
  • Payload arithmetic done: bytes per microframe at or under 3,072, and the device's reported figure covers it.
  • Every other camera client closed, and the capture node, not a metadata node, is the one open.
  • A sixty-second capture still grows at full rate with the application running.

More bring-up failures of this kind sit on the machine vision systems topic page. If the numbers above do not close your case, bring the format listing and your payload arithmetic to the contact page and we will work the descriptors with you.

Questions

Why does my USB camera work on Windows but show a black image on Linux?

The two stacks arm the streaming interface differently. Windows host stacks tend to select the highest-bandwidth alternate setting, which masks a payload figure the device understates. The Linux UVC driver sizes the isochronous pipe from the payload figure the device reports and arms the smallest setting that fits it, so the same understated figure starves the stream while Windows keeps playing. Correct the reported payload on the device, or pin a working format on the host side.

What does the frame interval in v4l2-ctl --list-formats-ext output mean?

It is the frame period, not a rate: divide one by the printed seconds, so a line reading 0.030s means about 33 frames per second. A USB camera advertises discrete intervals, and an application must request one value from that list; asking for a rate the descriptors never offered is a common cause of an empty stream. Always copy the interval from the listing rather than from memory.

How large should the payload size be that a UVC device reports?

At least the worst-case bytes the stream can send in one 125 microsecond microframe: width times height times bytes per pixel times frame rate, divided by 8,000. For 640 by 480 YUYV at 30 fps that is 2,304 bytes, under the 3,072-byte high-speed cap. The figure travels in the probe response the host reads before arming the interface. Recompute it for every entry whenever the advertised format table changes, because one stale value starves only the formats near it.

Could the cable still be the problem when apps show a black image?

Not if any tool on the same port displays frames: one working stream proves the cable, the power and the endpoints, so leave the connector alone. Suspect the cable only when nothing streams anywhere. Then swap in a known-good cable, confirm the camera enumerates as USB 3 rather than USB 2.0, and reseat both ends before touching drivers or descriptors.

Can I get the black app working without changing its code?

Yes, as a stopgap: run a small capture process that opens the working format on the camera and republishes the frames through a loopback video node, then point the application at that node. This also sidesteps the one-client limit, since the app never opens the camera directly. The durable fix is still pinning the format inside the application or correcting the figures the device reports.