fix(webcam): reader-output-subtype ladder + record WinRT per-call wrap rule

A browser grabbing the C920 kills our reader startup: the camera stays in
MJPG 1920x1080@30 (another app's format) and our OBS-refusal to re-negotiate
under SharedReadOnly contention (`SetMediaStreamPropertiesAsync` throws
'file in use / CaptureMode is SharedReadOnly') leaves it there.
CreateFrameReaderAsync(.., Bgra8) + StartAsync then refuses with
OutputFormatNotSupported — the MJPG-active source only exposes NV12 at the
reader level (clue: startup.log 19:01/19:05 sessions, same hardware that
started YUY2 640x480 fine at 08:52). Fresh launches showed no webcam and
'Add Webcam' failed.

Reader creation is now a per-candidate ladder (ReaderSubtypeCandidates):
Bgra8 for uncompressed cameras (unchanged fast path); NV12 then the
source-default for MJPG cameras — converted in OnFrameArrived like any
non-BGRA frame. Each candidate is allocated AND started under its OWN
catch: WinRT answers an unsupported subtype with a throw (E_INVALIDARG),
not a status, so a single rejected format must degrade to the next
candidate instead of aborting acquisition (creator rule — see MyMistakes
WINRT resource-allocation recipe). Rejections are logged and collected
into the final error.

Good Dog: 4 unit tests lock the candidate ordering (MJPG never Bgra8,
case-insensitive, uncompressed keeps Bgra8 first, unknown/null -> Bgra8).
301/301 green, 0 warnings. [no push]
This commit is contained in:
2026-09-15 19:25:39 -07:00
parent 64a5a6d06f
commit 94a934ffa9
5 changed files with 223 additions and 64 deletions
+21
View File
@@ -15,6 +15,27 @@
## 🔬 Recipes registry
### WINRT RESOURCE-ALLOCATION CALLS: WRAP PER-CALL, DEGRADE TO NEXT OPTION (RECIPE)
Creator callout (2026-09-15): WinRT/COM calls that allocate or start a resource — `InitializeAsync`,
`CreateFrameReaderAsync`, `StartAsync`, `CreateReaderAsync` & friends — are THROW-HEAVY. An unsupported
subtype/format, a device that just vanished, an access mode rejected mid-flight: these surface as
`ArgumentException`/`E_INVALIDARG` ("value does not fall within the expected range") or HRESULTs, NOT as
a returned status you can switch on. Relying on ONE outer catch to "handle failures" is not handling —
one rejected call inside a fallback ladder aborts the whole ladder and every untried option. Rule:
- Each allocation/start call inside a try/catch of its OWN, so a throw on candidate N falls through to
candidate N+1 (log each rejection with its message/status; collect them for the final error string).
- `return`/`break` on success must be reached WITHOUT passing through a `finally` that disposes the
resource you just committed (classic reader/capture dispose-after-commit bug).
- Unsubscribe + dispose the partial resource in the catch block when the subscription happened before
the throwing call.
- The outer catch stays as the LAST-RESORT net for device-level errors, not the primary one.
- Same discipline applies to the frame-consumption side: teardown races reader threads (see HANDOFF
crash follow-up) — a frame callback can't assume the pipeline is alive.
Applied in `MediaCaptureFrameSource`'s reader-subtype ladder (2026-09-15, webcam-take fix).
### SPIN GUARD → RESOLVED — web overlay transparency + bounding box (RECIPE)
**THE ONE ROOT CAUSE THAT EXPLAINS EVERY FAILED TAKE:** WebView2's `CapturePreviewAsync`