Skip to content

feature: the hardware half of USB video capture - #153

Merged
ewowi merged 7 commits into
MoonModules:mainfrom
Gohnnyman:feat/uvc-transport
Oct 24, 2026
Merged

ewowi merged 7 commits into
MoonModules:mainfrom
Gohnnyman:feat/uvc-transport

Conversation

@Gohnnyman

Copy link
Copy Markdown
Contributor

What's new

Fills the capture source the previous PR left stubbed. MJPEG from a USB

grabber, decoded by the P4's JPEG engine, area-averaged into the picture
at whatever size the service asks for.

  • The UVC host runs behind CONFIG_MM_USB_VIDEO, so only the P4 images
    carry it (+126 KB flash).
  • Modes are read when the device enumerates, so a board with no grabber
    fails fast instead of waiting on an open.
  • noiseFloor cuts a grabber's near-black to black; staleMs drops the
    picture when a source stops, rather than leaving the room lit from a
    frozen frame.
  • The status line names the live rate and only what is going wrong.

Measured on an ESP32-P4 with a 640x480 HDMI grabber: 60 fps into a
60x34 picture, nothing skipped, nothing failing.

Note

What's left to implement is HDR decoding and channel balances, it'll be done in next PRs

My Setup

image

Active Parts

Basically my whole setup was created for PS5. I have these active parts:

Connection logic

The logic of the whole setup is:

  • PS5 -> HDMI Splitter
  • HDMI Splitter OUTPUT 1 -> TV
  • HDMI Splitter OUTPUT 2 -> HDMI USB Grabber
  • HDMI USB Grabber -> ESP32 P4 NANO
  • ESP32 P4 NANO -> LED Strip

Note

During my testing I also tried this HDMI USB Grabber. As I remember, I swapped for the model above because sometimes it flickered with green. But it was long time ago and now I'm not sure whether the problem was in grabber or in code itself. Also Hyperion mentioned in their docs that this grabber not so good.

What's working and what's not

Working:

  • HDCP in HDMI - Netflix & Youtube on PS5 working great
  • 4k 120Hz - I specifically tried to find parts that allow to play 4k 120Hz
  • No changes in PS5 output on TV - at least I don't see any differences, the picture on TV is the same as without setup

Not working:

  • Orange and yellow colors are somehow green. It's fixable with channel balances and HDR decoding that will be added in next PRs
  • One-Touch Play & Power Off Link - PS5 now completely ignores power off / on requests from tv and vice versa

Gohnnyman and others added 2 commits October 22, 2026 13:27
MJPEG from a USB grabber, decoded by the P4's JPEG engine and area-averaged
into the picture at the size the service asks for. The host runs behind
CONFIG_MM_USB_VIDEO, so only the P4 images carry it.

Modes are read when the device enumerates, so a board with no grabber fails
before the open's timeout, and a continuous-range device is offered a rung per
rate a picture asks for rather than only the ends of its range. The driver
takes the frame size from the negotiation instead of a guess here.

noiseFloor cuts a grabber's near-black to black, as one compare on a byte.
staleMs drops the picture when a source stops, rather than leaving the room lit
from a frozen frame. The status line names the live rate per second and only
what is going wrong.

Measured on an ESP32-P4 with a 640x480 grabber: 60 fps into a 60x34 picture,
nothing skipped, nothing failing.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The capture's own controls already appear behind hasUsbVideo, while the source
offering it did not, so a board without the transport listed a source whose
open could only answer that no grabber is attached.

A config carrying the capture by label ports across either way: the Select
apply matches on the name, and one that is not listed matches nothing.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Oct 22, 2026 •

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

Run configuration

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@Gohnnyman

Copy link
Copy Markdown
Contributor Author

@ewowi Hi!
Sorry, this PR ~600 lines without tests, it's more than 400, but I don't have an idea how to split it

Gohnnyman and others added 2 commits October 22, 2026 14:12
The capture task read noiseFloor straight off the control while the render
thread could be writing it, which threadsanitizer reports as a race on the
byte. The floor is now taken in prepareBuffers beside bufW_ and bufH_, before
the task exists, so the task owns every value it decodes through.

An edit re-prepares, so a new floor still reaches the next frame.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…e allocation

A grabber unplugged and plugged back now streams again without touching a control, and one plugged in after a failed open is picked up. The USB host and the UVC driver live only while USB capture is the source, so a P4 that never selects it holds none of their RAM.

**Core**
- PictureService watches videoCaptureFormatGeneration() once a second and rebuilds when it moves: an unplug closes the stream, which frees the port, and a replug opens again. The generation only grows and reads 0 while no device is listed.
- Scheduler::instance() and requestPrepareTree() are MM_NONBLOCKING, so a tick may ask for a rebuild.

**Platform**
- videoCaptureShutdown(): the UVC driver uninstalls when the source leaves or the service is released, and the pump then frees the device and uninstalls the host library, as the MIDI host does.
- The frame list, format bank and snapshots (about 3 KB) are allocated with the install instead of static internal RAM.
- The frame size and MCU come from the JPEG's SOF header (usbvideo::jpegShape), replacing jpeg_decoder_get_info, which allocated per frame.
- uvc_host_desc_print removed from the open, about 100 ms on the render thread; the modes are still logged.
- The P4 sdkconfig comment wrapped.

**Tests**
- A grabber unplugged and plugged back is opened again (fails without the generation watch).
- A JPEG's size and MCU from its frame header; a truncated or foreign frame has none.
- A byte at the noise floor reads as black, one above it passes through.
- Desktop 2682/2682; esp32p4rev1-eth builds. Hardware test of replug, source switching and 60 fps: Gohnnyman's rig.

**Reviews**
- Fable review of MoonModules#153: replug, install lifetime, static internal RAM, the descriptor print, the per-frame allocation, the generation reset and the floor test: done here. Transfer-error restart, capture stack and CONFIG_UVC_INTERVAL_ARRAY_SIZE: left for the bench.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@ewowi

ewowi commented Oct 22, 2026

Copy link
Copy Markdown
Collaborator

Thanks @Gohnnyman, great work; 60 fps into the picture with nothing skipped is exactly what this needed.

I pushed a maintainer commit on top of yours (29671e6) with the fixes from the review, so you don't have to chase them:

  • Replug: an unplug now closes the stream, so the port recycles, and a replug opens the capture again. A grabber plugged in after an open failed is picked up too. PictureService watches the device list once a second.
  • Lifecycle: the USB host, the pump task and the UVC driver are installed while the source is USB capture and removed when it leaves (videoCaptureShutdown(), as the MIDI host does). The format list and snapshots (~3 KB) moved to the heap with them.
  • Render path: uvc_host_desc_print is gone (the modes are still logged), and the frame size comes from the JPEG's SOF header instead of jpeg_decoder_get_info, which allocated every frame.

Could you test on your rig?

  1. Unplug the grabber mid-stream, then replug: the lights should come back without touching a control.
  2. Switch the source to the test pattern and back.
  3. Your 60 fps measurement still holds.

If you can, also check whether a transfer error (e.g. pulling the HDMI side) restarts the stream, and the capture task's stack high-water mark once.

The docs how-to from your setup write-up is next; we'll draft it from your PR text. Processes behind the review: MoonWalk.

Switching away from the capture and back failed the re-open and stayed failed:
the UVC driver cannot install while the previous teardown is still unwinding,
and nothing asked again. videoCaptureSettling() answers whether a teardown is
in flight, the open is not attempted while it is, and tick1s asks again until
it has finished. The ESP32 teardown no longer blocks the render thread for a
second waiting on the pump; it hands the rest to the pump and returns.

The status names the step a failed open stopped at rather than guessing at the
mode list, which is what a bus still shutting down had been reported as.

A device that goes away now puts the lights out on the tick that notices it:
the stale drop runs before the generation branch can return, and prepareBuffers
no longer publishes a writer's buffer, which was re-lighting the frame the drop
had just put out.

Also from the review: the frame list is null-checked in the driver callbacks,
since a shutdown may free it between events; two USB host failures that had
been reported as no grabber attached name themselves; and the status line reads
the size a writer fills, so a dropped picture no longer reads as 0 x 0.

Desktop 2684/2684; esp32p4rev3-eth-wifi builds. On the rig: switching away and
back after 2 s, 1 s and immediately all recover inside two seconds and hold
60 fps into a 60 x 34 picture.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@Gohnnyman

Copy link
Copy Markdown
Contributor Author

@ewowi Hi! I fixed several things:

  • After grabber is pulled out the lights stuck in the same last seen screen
  • Quickly switching test pattern <-> usb capture breaks the capture because it wasn't properly disconnected in the code
  • some latency

Everything is fixed and tested

  • Unplug the grabber mid-stream, then replug - the lights came back
  • Switch the source to the test pattern and back - now fixed
  • Your 60 fps measurement still holds - yep still holds

One thing I noticed is that test pattern is whole white. It doesn't looks like a test pattern for me, for example user want be able to figure out whether he set up Layout properly or not. I'll update it in the next PR, probably I'll just add static colors on four borders

ewowi and others added 2 commits October 24, 2026 09:45
…e Picture service

Brings the branch up to main, which also fixes CI's check_docgen crash in libclang's bindings. A grabber arriving, leaving or a teardown settling now rebuilds the Picture service alone rather than every layer and driver.

**Core**
- Scheduler.h: the one conflict resolved to main's version (requestPrepare, the live queue), keeping this branch's MM_NONBLOCKING on requestPrepareTree and instance(), and adding it to requestPrepare, which tick1s now calls.
- PictureService: tick1s asks requestPrepare(this) instead of requestPrepareTree(): the picture keeps its size, so no reader re-prepares; a comment's "no longer" reads "does not carry".

**Reviews**
- Maintainer review of 61c17f6: the CI prose finding and the docgen crash fixed here; the suggestion to scope the rebuild applied.
- Checks: desktop build and 2764 tests pass; check_docgen 0 errors; specs up to date; esp32p4rev1-eth builds and, on an ESP32-P4 rev 1.3 with an MS2109-type grabber, shows a Windows PC's HDMI on a 128x128 grid sent as Art-Net.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… out

Prose rose by one Weasel finding in CI: "just" here meant "a moment ago", which the rule cannot tell from emphasis.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@ewowi
ewowi merged commit f030e3f into MoonModules:main Oct 24, 2026
9 checks passed
@ewowi

ewowi commented Oct 24, 2026

Copy link
Copy Markdown
Collaborator

Merged, thanks @Gohnnyman, and thanks for testing all three on your rig. Recovering inside two seconds at 60 fps is exactly right.

I read 61c17f6 and it looks good. The teardown no longer holds the render thread, a failed open says where it stopped, and the unplug drop has its test. The null checks on bus are safe too: uvc_host_uninstall waits for its task before it returns OK, and videoCaptureShutdown returns early when it doesn't.

Before merging I added two maintainer commits to get CI green:

  • e4895af, main merged in. That fixes the check_docgen crash in libclang ("Unknown template argument kind 437"), which wasn't your code: main fixed it in Free internal RAM on the ESP32; live JSON in MoonLive services; a change rebuilds only what it reshapes #154, after your branch was cut. The one conflict, in Scheduler.h, keeps your MM_NONBLOCKING markings. Two more changes ride along:
    • requestPrepare(this) instead of requestPrepareTree() in tick1s, so a device change rebuilds only the Picture service, not every layer and driver;
    • the prose line "which a dropped picture no longer carries" now reads "does not carry".
  • 9d3b541: "a frame the drop just put out" now reads "the frame the drop has put out". The prose check reads "just" as emphasis.

I tested it on an ESP32-P4 (rev 1.3, Ethernet) with an MS2109-type HDMI grabber, a Windows mini PC as the source, a 128×128 grid, and Art-Net out. Video shows end to end. One thing I measured, for your next round: the capture rate falls with the picture's size, whatever the rendering does.

Picture Capture
60×34 33–34 fps
128×72 19–21 fps, the same with the layer and Art-Net off

The grabber opened 640×480 @ 60. Is the step from the decoded frame down to the picture done per output pixel in software? On your rig, which mode do you get at 60 fps? I also see uvc-isoc: invalid MJPEG SOI about once a second, shown as "frames failing".

A test pattern that shows the layout's edges is a good idea for the next PR. Next we draft the docs how-to from your setup write-up.

@MoonModules

Copy link
Copy Markdown
Owner
Untitled.mov
IMG_3884

@ewowi

ewowi commented Oct 24, 2026

Copy link
Copy Markdown
Collaborator

@Gohnnyman , I will see if I can also get this supported on Linux boxes. Or do you also plan to do that ?

@Gohnnyman

Gohnnyman commented Oct 24, 2026 •

Copy link
Copy Markdown
Contributor Author

@Gohnnyman , I will see if I can also get this supported on Linux boxes. Or do you also plan to do that ?

What is Linux box? 🫪

Edit: I didn't plan to do that because I'm not sure what that is

@ewowi

ewowi commented Oct 24, 2026

Copy link
Copy Markdown
Collaborator

A raspberry pi or nano pi (but also windows/macos), docker container

Moonlight runs on these devices as well

@Gohnnyman

Copy link
Copy Markdown
Contributor Author

Ohh, right... There is the main point:
My girlfriend asked my to buy this backlight setup for tv. And I thought "why I should buy it when I could create it myself?"

Then I started investigating how I could do this. The main problem for me was that everyone said that I need raspberry pi or simillar for this. I thought "why do I need the whole pc (mini pc) just to compute some bytes of video?". It looked to me like a real overhead for a backlight setup. Also expensive (for a backlight). Then I found esp32 p4 that could decode mjpeg and process video frames in proper time.

I'm not sure, but I didn't find any similar setup with microcontroller instead of mini-pc (raspberry pi-like). I think it's the firs one of the kind. I'm not sure but I'd be happy if you prove me wrong 😄

So I aimed at this specific setup. ESP32 P4 and that's it. I didn't plan to make it work on any other platform, but I'd be happy to see it work anywhere else

@ewowi

ewowi commented Oct 24, 2026

Copy link
Copy Markdown
Collaborator

I am not trying to prove you wrong as I like the idea we created something unique 🙂.
I need to set it up with a led strip yet. But I think I will use it as well as tv backlight 👍
So there is no noticeable latency ?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants