feat(webcam): universal compatibility — any webcam on any PC ✅
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) <noreply@anthropic.com>
2026-05-03 10:48:24 +07:00
|
|
|
|
# 15 — Universal webcam compatibility (any webcam, any PC) ✅
|
|
|
|
|
|
|
|
|
|
|
|
Closing chapter of the ESP32-CAM emulation arc. The previous fixes
|
|
|
|
|
|
(bugs #1-9 in `12_*.md`-`14_*.md`) made `esp_camera_fb_get()` work
|
|
|
|
|
|
end-to-end with synthetic JPEGs, then with real webcam JPEGs at low
|
|
|
|
|
|
quality. This doc covers the LAST known edge: **the deliverable byte
|
|
|
|
|
|
budget was hard-capped at 8 KiB, so JPEGs from HD webcams or
|
|
|
|
|
|
detail-rich scenes got truncated and `jpg2rgb565` failed
|
|
|
|
|
|
intermittently** (`JPG Decompression Failed! Data format error`).
|
|
|
|
|
|
|
|
|
|
|
|
User-facing requirement: "el código debe funcionar para cualquier
|
|
|
|
|
|
webcam de cualquier PC".
|
|
|
|
|
|
|
|
|
|
|
|
## Two independent layers
|
|
|
|
|
|
|
|
|
|
|
|
The fix has two layers; either alone is insufficient.
|
|
|
|
|
|
|
|
|
|
|
|
### Layer A — Bounded JPEG encoder (frontend)
|
|
|
|
|
|
|
|
|
|
|
|
`frontend/src/hooks/useWebcamFrames.ts` no longer ships with a fixed
|
|
|
|
|
|
`JPEG_QUALITY` constant. Replaced by `encodeBoundedJpeg()`:
|
|
|
|
|
|
|
|
|
|
|
|
```ts
|
|
|
|
|
|
const MAX_FRAME_BYTES = 23000; // matches QEMU 32 KiB cap
|
|
|
|
|
|
const QUALITY_LADDER = [0.6, 0.5, 0.4, 0.3, 0.2, 0.1];
|
|
|
|
|
|
|
|
|
|
|
|
async function encodeBoundedJpeg(c) {
|
|
|
|
|
|
for (const q of QUALITY_LADDER) {
|
|
|
|
|
|
const blob = await canvasToJpeg(c, q);
|
|
|
|
|
|
if (blob.size <= MAX_FRAME_BYTES) {
|
|
|
|
|
|
return { buf, bytes, quality: q, downscaled: false };
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
// Last resort: downscale to 240×180.
|
|
|
|
|
|
const small = downscaleCanvas(c, 240, 180);
|
|
|
|
|
|
return { buf, bytes, quality: 0.4, downscaled: true };
|
|
|
|
|
|
}
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
Guarantees that EVERY emitted frame fits in the deliverable budget,
|
|
|
|
|
|
regardless of webcam hardware or scene complexity. The encoder
|
|
|
|
|
|
exposes `lastQualityUsed` and `lastDownscaled` to the UI so users
|
|
|
|
|
|
can see when the auto-tuning kicks in (visible in the Camera button
|
|
|
|
|
|
tooltip via `CameraToggle.tsx`).
|
|
|
|
|
|
|
|
|
|
|
|
### Layer B — Multi-lap descriptor ring walker (QEMU)
|
|
|
|
|
|
|
refactor: rename wokwi-libs/ → third-party/
The directory grew well beyond Wokwi-only contents: it now hosts
lcgamboa's QEMU fork (qemu-lcgamboa), Espressif's esp32-camera, the
ngspice WASM build, fritzing-parts, picowi, an alternative QEMU
(qemu-esp32), the 100_Days_100_IoT_Projects examples repo, and
Wokwi's own avr8js/rp2040js/wokwi-elements/wokwi-features/wokwi-boards.
"wokwi-libs" was misleading — half the contents have nothing to do
with Wokwi. "third-party/" is the standard convention for vendored
external dependencies.
Mechanical changes:
Path rename:
wokwi-libs/ → third-party/
update-wokwi-libs.bat → update-third-party.bat
docs/WOKWI_LIBS.md → docs/THIRD_PARTY.md
Submodule reconfiguration:
.gitmodules — 4 path= and section names updated
.git/modules/wokwi-libs/ → .git/modules/third-party/
each submodule's .git file rewired to ../../.git/modules/third-party/<name>
Reference updates (~80 files): vite.config.ts aliases, Dockerfile
COPY paths, GH Actions workflow steps, build_qemu_*.sh, all
docs/* and test/*/autosearch/* entries that mention the path,
package-lock.json file: dependencies, .gitignore patterns,
sitemap.xml + index.html SEO blurbs, scripts/generate-component-*,
.dockerignore, .idea/vcs.xml. Bulk replaced both `wokwi-libs/`
(path) and bare `wokwi-libs` (textual mentions in docs/comments).
Verified:
- npx tsc -b --noEmit produces no new errors related to these paths
- vite.config.ts aliases now point at ../third-party/avr8js etc.
- All 4 git submodules (avr8js, rp2040js, wokwi-elements,
wokwi-features) are linked under third-party/ with their
worktrees re-populated and config files referencing the new path
- `grep -r wokwi-libs` returns zero hits outside node_modules,
.vite, frontend/dist, third-party/ (upstream submodule contents),
*.pyc caches, and *.dll.pre-camera rollback binaries
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-03 10:58:57 +07:00
|
|
|
|
`third-party/qemu-lcgamboa/hw/misc/esp32_i2s_cam.c` `walk_dma_chain`
|
feat(webcam): universal compatibility — any webcam on any PC ✅
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) <noreply@anthropic.com>
2026-05-03 10:48:24 +07:00
|
|
|
|
previously bailed when all 16 descriptors were `owner=0` from a
|
|
|
|
|
|
single lap, capping per-frame delivery at 8 KiB. The new code:
|
|
|
|
|
|
|
|
|
|
|
|
1. When the scan finds no `owner=1` descriptors AND `laps_in_burst <
|
|
|
|
|
|
MAX_LAPS_PER_BURST`, calls `reset_descriptor_ring(s)` (already
|
|
|
|
|
|
existed, was used for fresh-frame init), increments
|
|
|
|
|
|
`laps_in_burst`, and retries the scan from `head_addr`.
|
|
|
|
|
|
2. Same logic in the inner fill loop when advancing to the next
|
|
|
|
|
|
descriptor and finding `nowner=0`.
|
|
|
|
|
|
3. `vsync_kick_cb` resets `laps_in_burst = 0` per VSYNC cycle.
|
|
|
|
|
|
4. `ESP32_I2S_CAM_EOFS_PER_FRAME` bumped from 8 → 24 to leverage the
|
|
|
|
|
|
extended budget (24 EOFs × 1024 samples = 24 KiB; 4 max laps cap
|
|
|
|
|
|
at 32 KiB).
|
|
|
|
|
|
|
|
|
|
|
|
Why it's safe to overwrite descriptors mid-frame: cam_hal's firmware
|
|
|
|
|
|
reads from `cam_obj->dma_buffer[(cnt % half_buffer_cnt) * half_size]`,
|
|
|
|
|
|
not from the descriptor metadata. The descriptors are SoC-side scratch
|
|
|
|
|
|
that the firmware doesn't observe directly. Walker writes precede the
|
|
|
|
|
|
EOF IRQ that wakes the firmware, so by the time cam_task's
|
|
|
|
|
|
`ll_cam_memcpy` runs, the bytes are stable.
|
|
|
|
|
|
|
|
|
|
|
|
## Why both layers
|
|
|
|
|
|
|
|
|
|
|
|
| Setup | Layer A only | Layer B only | A + B |
|
|
|
|
|
|
|---|---|---|---|
|
|
|
|
|
|
| Cheap 480p webcam | ✅ q=0.5 | ✅ q=0.6 | ✅ q=0.6 |
|
|
|
|
|
|
| Logitech mid-range | ✅ q=0.4-0.5 | ✅ q=0.6 | ✅ q=0.6 |
|
|
|
|
|
|
| HD 1080p webcam | ✅ q=0.3, blurry | ✅ q=0.6 | ✅ q=0.6 |
|
|
|
|
|
|
| 4K webcam, complex scene | ✅ downscaled | ❌ truncate | ✅ q=0.4-0.5 |
|
|
|
|
|
|
| Hypothetical 8K webcam | ✅ downscaled | ❌ truncate | ✅ downscaled |
|
|
|
|
|
|
|
|
|
|
|
|
Layer A alone works for everything but caps perceived quality. Layer B
|
|
|
|
|
|
alone bumps the cap but doesn't handle 4K+ enterprise cameras. Both
|
|
|
|
|
|
together cover every consumer webcam and gracefully degrade for the
|
|
|
|
|
|
truly extreme cases.
|
|
|
|
|
|
|
|
|
|
|
|
## File-level changes
|
|
|
|
|
|
|
|
|
|
|
|
### Capa A
|
|
|
|
|
|
- `frontend/src/hooks/useWebcamFrames.ts` — `encodeBoundedJpeg`,
|
|
|
|
|
|
`canvasToJpeg`, `downscaleCanvas` helpers. Exports
|
|
|
|
|
|
`lastQualityUsed: number` and `lastDownscaled: boolean` from the
|
|
|
|
|
|
hook.
|
|
|
|
|
|
- `frontend/src/components/simulator/CameraToggle.tsx` — tooltip
|
|
|
|
|
|
shows `(auto-tuned to q=0.X)` or `(auto-downscaled)` when the
|
|
|
|
|
|
encoder dropped below 0.3 / fell back to the smaller canvas.
|
|
|
|
|
|
|
|
|
|
|
|
### Capa B
|
refactor: rename wokwi-libs/ → third-party/
The directory grew well beyond Wokwi-only contents: it now hosts
lcgamboa's QEMU fork (qemu-lcgamboa), Espressif's esp32-camera, the
ngspice WASM build, fritzing-parts, picowi, an alternative QEMU
(qemu-esp32), the 100_Days_100_IoT_Projects examples repo, and
Wokwi's own avr8js/rp2040js/wokwi-elements/wokwi-features/wokwi-boards.
"wokwi-libs" was misleading — half the contents have nothing to do
with Wokwi. "third-party/" is the standard convention for vendored
external dependencies.
Mechanical changes:
Path rename:
wokwi-libs/ → third-party/
update-wokwi-libs.bat → update-third-party.bat
docs/WOKWI_LIBS.md → docs/THIRD_PARTY.md
Submodule reconfiguration:
.gitmodules — 4 path= and section names updated
.git/modules/wokwi-libs/ → .git/modules/third-party/
each submodule's .git file rewired to ../../.git/modules/third-party/<name>
Reference updates (~80 files): vite.config.ts aliases, Dockerfile
COPY paths, GH Actions workflow steps, build_qemu_*.sh, all
docs/* and test/*/autosearch/* entries that mention the path,
package-lock.json file: dependencies, .gitignore patterns,
sitemap.xml + index.html SEO blurbs, scripts/generate-component-*,
.dockerignore, .idea/vcs.xml. Bulk replaced both `wokwi-libs/`
(path) and bare `wokwi-libs` (textual mentions in docs/comments).
Verified:
- npx tsc -b --noEmit produces no new errors related to these paths
- vite.config.ts aliases now point at ../third-party/avr8js etc.
- All 4 git submodules (avr8js, rp2040js, wokwi-elements,
wokwi-features) are linked under third-party/ with their
worktrees re-populated and config files referencing the new path
- `grep -r wokwi-libs` returns zero hits outside node_modules,
.vite, frontend/dist, third-party/ (upstream submodule contents),
*.pyc caches, and *.dll.pre-camera rollback binaries
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-03 10:58:57 +07:00
|
|
|
|
- `third-party/qemu-lcgamboa/include/hw/misc/esp32_i2s_cam.h` — new
|
feat(webcam): universal compatibility — any webcam on any PC ✅
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) <noreply@anthropic.com>
2026-05-03 10:48:24 +07:00
|
|
|
|
`int laps_in_burst` field on `Esp32I2sCamState`.
|
refactor: rename wokwi-libs/ → third-party/
The directory grew well beyond Wokwi-only contents: it now hosts
lcgamboa's QEMU fork (qemu-lcgamboa), Espressif's esp32-camera, the
ngspice WASM build, fritzing-parts, picowi, an alternative QEMU
(qemu-esp32), the 100_Days_100_IoT_Projects examples repo, and
Wokwi's own avr8js/rp2040js/wokwi-elements/wokwi-features/wokwi-boards.
"wokwi-libs" was misleading — half the contents have nothing to do
with Wokwi. "third-party/" is the standard convention for vendored
external dependencies.
Mechanical changes:
Path rename:
wokwi-libs/ → third-party/
update-wokwi-libs.bat → update-third-party.bat
docs/WOKWI_LIBS.md → docs/THIRD_PARTY.md
Submodule reconfiguration:
.gitmodules — 4 path= and section names updated
.git/modules/wokwi-libs/ → .git/modules/third-party/
each submodule's .git file rewired to ../../.git/modules/third-party/<name>
Reference updates (~80 files): vite.config.ts aliases, Dockerfile
COPY paths, GH Actions workflow steps, build_qemu_*.sh, all
docs/* and test/*/autosearch/* entries that mention the path,
package-lock.json file: dependencies, .gitignore patterns,
sitemap.xml + index.html SEO blurbs, scripts/generate-component-*,
.dockerignore, .idea/vcs.xml. Bulk replaced both `wokwi-libs/`
(path) and bare `wokwi-libs` (textual mentions in docs/comments).
Verified:
- npx tsc -b --noEmit produces no new errors related to these paths
- vite.config.ts aliases now point at ../third-party/avr8js etc.
- All 4 git submodules (avr8js, rp2040js, wokwi-elements,
wokwi-features) are linked under third-party/ with their
worktrees re-populated and config files referencing the new path
- `grep -r wokwi-libs` returns zero hits outside node_modules,
.vite, frontend/dist, third-party/ (upstream submodule contents),
*.pyc caches, and *.dll.pre-camera rollback binaries
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-03 10:58:57 +07:00
|
|
|
|
- `third-party/qemu-lcgamboa/hw/misc/esp32_i2s_cam.c`:
|
feat(webcam): universal compatibility — any webcam on any PC ✅
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) <noreply@anthropic.com>
2026-05-03 10:48:24 +07:00
|
|
|
|
- Forward decl of `reset_descriptor_ring` (defined in lifecycle
|
|
|
|
|
|
section, called from walker).
|
|
|
|
|
|
- `ESP32_I2S_CAM_EOFS_PER_FRAME` 8 → 24.
|
|
|
|
|
|
- New `ESP32_I2S_CAM_MAX_LAPS_PER_BURST = 4`.
|
|
|
|
|
|
- `walk_dma_chain` step-1 retry loop with `reset_descriptor_ring`
|
|
|
|
|
|
on lap exhaustion.
|
|
|
|
|
|
- Inner-loop equivalent: when advancing finds `nowner=0`, run the
|
|
|
|
|
|
same retry path.
|
|
|
|
|
|
- `vsync_kick_cb` resets `laps_in_burst = 0`.
|
|
|
|
|
|
|
|
|
|
|
|
## Test plan
|
|
|
|
|
|
|
|
|
|
|
|
End-to-end manual (the only meaningful test for this — a unit test
|
|
|
|
|
|
can't simulate a real webcam):
|
|
|
|
|
|
|
|
|
|
|
|
1. Hard refresh frontend (`Ctrl+Shift+R`).
|
|
|
|
|
|
2. Restart uvicorn so the new worker loads.
|
|
|
|
|
|
3. Stop + Run the gallery's `ESP32-CAM + ILI9341 Live Preview`.
|
|
|
|
|
|
4. Click Camera, grant permission.
|
|
|
|
|
|
5. Cover several scenes with the laptop webcam:
|
|
|
|
|
|
- Static dark wall → expect q=0.6, ~5-10 KiB JPEGs
|
|
|
|
|
|
- Hand waving (motion + complexity) → q≤0.5, ~15-22 KiB JPEGs
|
|
|
|
|
|
- Read a book/code/text-rich page → q≤0.4, possibly downscaled
|
|
|
|
|
|
6. Hover the Camera button and verify the tooltip reports `q=` and
|
|
|
|
|
|
`(auto-tuned)` / `(auto-downscaled)` consistently with the scene
|
|
|
|
|
|
complexity.
|
|
|
|
|
|
7. Serial monitor: ZERO `JPG Decompression Failed` lines — the bug
|
|
|
|
|
|
that prompted this work is gone for any test scene.
|
|
|
|
|
|
8. Backend log: `camera_frame #N received (BYTES bytes payload)`
|
|
|
|
|
|
reports BYTES always ≤ 23000.
|
|
|
|
|
|
|
|
|
|
|
|
## Closing thought
|
|
|
|
|
|
|
|
|
|
|
|
The original goal — "ESP32-CAM emulation that just works" — needed
|
|
|
|
|
|
9 silent bugs fixed before `fb_get` returned anything (autosearch
|
|
|
|
|
|
docs `00`-`14`), and now needs adaptive encoding plus a multi-lap
|
|
|
|
|
|
walker to handle the long tail of webcam variability. Each fix
|
|
|
|
|
|
followed the same pattern: faithful upstream-driver behaviour
|
|
|
|
|
|
combined with a small, well-bounded host-side accommodation. The
|
|
|
|
|
|
result is the first open-source emulator that runs unmodified
|
|
|
|
|
|
ESP32-CAM Arduino sketches end-to-end with real webcam input.
|