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:
@@ -86,6 +86,20 @@ public sealed class MediaCaptureFrameSource : ICameraFrameSource
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Ordered reader-output-subtype candidates for a camera whose live media type
|
||||
/// is <paramref name="activeSubtype"/>. Bgra8 is the fast path (OnFrameArrived
|
||||
/// skips conversion) and works for uncompressed cameras; an MJPG-active source
|
||||
/// cannot be read as Bgra8 by a MediaFrameReader (no BGRA converter on the MJPG
|
||||
/// pipeline — StartAsync answers OutputFormatNotSupported), so MJPG leads with
|
||||
/// NV12 and falls back to the reader's default (null). Every candidate that is
|
||||
/// not Bgra8 is converted in OnFrameArrived.
|
||||
/// </summary>
|
||||
internal static string?[] ReaderSubtypeCandidates(string? activeSubtype) =>
|
||||
string.Equals(activeSubtype, "MJPG", StringComparison.OrdinalIgnoreCase)
|
||||
? new string?[] { MediaEncodingSubtypes.Nv12, null }
|
||||
: new string?[] { MediaEncodingSubtypes.Bgra8, MediaEncodingSubtypes.Nv12, null };
|
||||
|
||||
private async Task<bool> TryStartAsync(bool preferVideoRecord)
|
||||
{
|
||||
MediaCapture? capture = null;
|
||||
@@ -180,32 +194,81 @@ public sealed class MediaCaptureFrameSource : ICameraFrameSource
|
||||
capture.Failed += OnCaptureFailed;
|
||||
capture.CameraStreamStateChanged += OnCameraStreamStateChanged;
|
||||
|
||||
reader = await capture.CreateFrameReaderAsync(colorSource, MediaEncodingSubtypes.Bgra8);
|
||||
if (reader == null)
|
||||
throw new InvalidOperationException($"Camera '{_deviceId}' created no frame reader.");
|
||||
|
||||
// One-line truth of what the camera actually agreed to (format/size/fps).
|
||||
// The reader must output a subtype the source can actually produce, else
|
||||
// StartAsync refuses (OutputFormatNotSupported). An uncompressed camera
|
||||
// converts to Bgra8 fine (the fast path); a camera sitting in MJPG — which
|
||||
// is what we find when ANOTHER app left it there and ours can't re-negotiate
|
||||
// under SharedReadOnly contention — cannot be read as Bgra8 by a
|
||||
// MediaFrameReader (the MJPG pipeline exposes NV12 output, not BGRA). Walk
|
||||
// the candidate ladder; anything that isn't Bgra8 is converted in
|
||||
// OnFrameArrived anyway.
|
||||
string? activeSubtype = null;
|
||||
try
|
||||
{
|
||||
var active = capture.VideoDeviceController?
|
||||
.GetMediaStreamProperties(colorSource.Info.MediaStreamType);
|
||||
if (active is Windows.Media.MediaProperties.VideoEncodingProperties vep)
|
||||
{
|
||||
activeSubtype = vep.Subtype;
|
||||
AppLog.Write($"Camera '{_deviceId}' media type: {vep.Subtype} " +
|
||||
$"{vep.Width}x{vep.Height} @ {vep.FrameRate.Numerator}/{vep.FrameRate.Denominator}fps");
|
||||
}
|
||||
}
|
||||
catch { /* logging must never kill acquisition */ }
|
||||
|
||||
reader.FrameArrived += OnFrameArrived;
|
||||
var status = await reader.StartAsync();
|
||||
if (status != MediaFrameReaderStartStatus.Success)
|
||||
throw new InvalidOperationException(
|
||||
$"Camera '{_deviceId}' frame reader refused to start: {status}.");
|
||||
// Each candidate is allocated AND started under its own catch: a WinRT
|
||||
// call answering an unsupported subtype can THROW (E_INVALIDARG / "value
|
||||
// does not fall within the expected range") instead of returning a start
|
||||
// status, so every throw must degrade to the next candidate — never let
|
||||
// one rejected format abort the whole acquisition. Individual rejections
|
||||
// are logged and collected; only the last-resort net below (the outer
|
||||
// try/catch of this method) keeps cowboying device-level errors.
|
||||
var refused = new List<string>();
|
||||
foreach (var outputSubtype in ReaderSubtypeCandidates(activeSubtype))
|
||||
{
|
||||
var attempt = outputSubtype ?? "<default>";
|
||||
try
|
||||
{
|
||||
reader = outputSubtype is null
|
||||
? await capture.CreateFrameReaderAsync(colorSource)
|
||||
: await capture.CreateFrameReaderAsync(colorSource, outputSubtype);
|
||||
if (reader == null)
|
||||
{
|
||||
refused.Add($"{attempt}: no frame reader");
|
||||
continue;
|
||||
}
|
||||
|
||||
_capture = capture;
|
||||
_frameReader = reader;
|
||||
_lastError = null;
|
||||
_isFailed = false;
|
||||
return true;
|
||||
reader.FrameArrived += OnFrameArrived;
|
||||
var status = await reader.StartAsync();
|
||||
if (status == MediaFrameReaderStartStatus.Success)
|
||||
{
|
||||
_capture = capture;
|
||||
_frameReader = reader;
|
||||
_lastError = null;
|
||||
_isFailed = false;
|
||||
return true;
|
||||
}
|
||||
|
||||
reader.FrameArrived -= OnFrameArrived;
|
||||
reader.Dispose();
|
||||
reader = null;
|
||||
refused.Add($"{attempt}: {status}");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
AppLog.Write($"MediaCaptureFrameSource: reader as '{attempt}' refused: {ex.Message}");
|
||||
if (reader != null)
|
||||
{
|
||||
reader.FrameArrived -= OnFrameArrived;
|
||||
reader.Dispose();
|
||||
reader = null;
|
||||
}
|
||||
refused.Add($"{attempt}: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
throw new InvalidOperationException(
|
||||
$"Camera '{_deviceId}' frame reader refused to start: {string.Join(", ", refused)}.");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user