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 `