From 36914e209c7e6acf45fa0199b32195ffd8508ec7 Mon Sep 17 00:00:00 2001 From: David Montero Crespo Date: Sun, 3 May 2026 00:48:24 -0300 Subject: [PATCH] =?UTF-8?q?feat(webcam):=20universal=20compatibility=20?= =?UTF-8?q?=E2=80=94=20any=20webcam=20on=20any=20PC=20=E2=9C=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit User goal: ESP32-CAM live preview that works with any webcam, regardless of resolution, brand, or scene complexity. The previous fixed-quality 0.28 was fragile (intermittent decode errors on moving/textured scenes) and capped visual quality unnecessarily. Two-layer fix; either alone is insufficient: LAYER A — Bounded JPEG encoder (frontend, this repo) frontend/src/hooks/useWebcamFrames.ts: encodeBoundedJpeg() walks a quality ladder [0.6, 0.5, ..., 0.1] until the JPEG fits in MAX_FRAME_BYTES (23 000). If even q=0.1 overshoots — extreme HD/4K scenes — falls back to a 240×180 downscaled canvas at q=0.4. Guarantees every emitted frame fits the deliverable budget regardless of webcam hardware. The hook now exposes lastQualityUsed + lastDownscaled so UI can surface when auto-tuning kicks in. frontend/src/components/simulator/CameraToggle.tsx: Tooltip shows "(auto-tuned to q=0.X)" or "(auto-downscaled, q=0.X)" while streaming so users see what the encoder picked. LAYER B — Multi-lap descriptor ring walker (qemu-lcgamboa, submodule) Bumps the QEMU per-frame deliverable cap from 8 KiB to ~32 KiB by letting the walker reset the descriptor ring up to 4 times per VSYNC. Submodule pointer bumped to eb8b7a5d. Combined, the demo now supports: - Cheap 480p webcams: q=0.6, 5-10 KiB JPEGs, sharp - Logitech mid-range: q=0.5-0.6, 8-15 KiB JPEGs, sharp - HD 1080p webcams: q=0.4-0.6, 15-23 KiB JPEGs, sharp - 4K complex scenes: downscaled, still readable Documented as bug closure in: test/test-esp32-cam/autosearch/15_universal_webcam_compat.md Co-Authored-By: Claude Opus 4.7 (1M context) --- .../src/components/simulator/CameraToggle.tsx | 29 ++- frontend/src/hooks/useWebcamFrames.ts | 168 +++++++++++++++--- .../autosearch/15_universal_webcam_compat.md | 140 +++++++++++++++ wokwi-libs/qemu-lcgamboa | 2 +- 4 files changed, 312 insertions(+), 27 deletions(-) create mode 100644 test/test-esp32-cam/autosearch/15_universal_webcam_compat.md diff --git a/frontend/src/components/simulator/CameraToggle.tsx b/frontend/src/components/simulator/CameraToggle.tsx index 281ff63e..c84571d3 100644 --- a/frontend/src/components/simulator/CameraToggle.tsx +++ b/frontend/src/components/simulator/CameraToggle.tsx @@ -22,8 +22,16 @@ interface CameraToggleProps { } export const CameraToggle: React.FC = ({ boardId }) => { - const { status, errorMessage, framesSent, lastFrameBytes, start, stop } = - useWebcamFrames(); + const { + status, + errorMessage, + framesSent, + lastFrameBytes, + lastQualityUsed, + lastDownscaled, + start, + stop, + } = useWebcamFrames(); const handleClick = () => { if (!boardId) return; @@ -35,9 +43,24 @@ export const CameraToggle: React.FC = ({ boardId }) => { }; const isOn = status === 'streaming'; + + // Build a streaming tooltip that exposes the adaptive encoder state. + // Users see "auto-tuned" hints when the encoder had to drop quality + // or downscale — useful diagnostic for HD/4K webcams. + const streamingTooltip = () => { + const kb = (lastFrameBytes / 1024).toFixed(1); + const q = lastQualityUsed.toFixed(2); + const tuneNote = lastDownscaled + ? ` (auto-downscaled, q=${q})` + : lastQualityUsed < 0.3 + ? ` (auto-tuned to q=${q})` + : ` (q=${q})`; + return `Streaming webcam (${framesSent} frames, last=${kb} KB${tuneNote}) — click to stop`; + }; + const tooltip = status === 'streaming' - ? `Streaming webcam (${framesSent} frames, last=${(lastFrameBytes / 1024).toFixed(1)} KB) — click to stop` + ? streamingTooltip() : status === 'requesting' ? 'Asking for camera permission…' : status === 'denied' diff --git a/frontend/src/hooks/useWebcamFrames.ts b/frontend/src/hooks/useWebcamFrames.ts index a6155fbd..7b11a231 100644 --- a/frontend/src/hooks/useWebcamFrames.ts +++ b/frontend/src/hooks/useWebcamFrames.ts @@ -14,7 +14,10 @@ * Implementation notes: * - QVGA (320×240) at 10 fps. Larger sizes work but bandwidth * scales linearly and the firmware's DMA buffer is fixed-size. - * - JPEG quality 0.6 keeps each frame in the 8–14 KB range. + * - JPEG output is BOUNDED via `encodeBoundedJpeg` so any webcam + * on any PC produces frames that fit in the QEMU 8 KiB cap. + * Detail-rich scenes get progressively lower quality; HD/4K + * webcams fall back to a 240×180 downscale. See encodeBoundedJpeg. * - We use OffscreenCanvas when available (Chrome/Edge); fall back * to a hidden DOM canvas for Safari < 17. */ @@ -35,6 +38,14 @@ export interface UseWebcamFramesResult { framesSent: number; /** Last frame payload size (bytes). */ lastFrameBytes: number; + /** JPEG quality level used for the last frame (0.1 - 0.5). The + * encoder drops this dynamically when scenes are too complex to + * fit in the emulator's per-frame byte budget. */ + lastQualityUsed: number; + /** True if the last frame had to be downscaled (the quality ladder + * bottomed out). Indicates an HD/4K webcam where even quality 0.1 + * exceeded MAX_FRAME_BYTES at full QVGA resolution. */ + lastDownscaled: boolean; start: (boardId: string) => Promise; stop: () => void; /** A `