From 70db34455fbe5dbb4658f8ea88f62695fab6c756 Mon Sep 17 00:00:00 2001 From: gramps Date: Sat, 5 Sep 2026 17:07:24 -0700 Subject: [PATCH] =?UTF-8?q?perf(camera):=20MJPEG=20negotiation=20before=20?= =?UTF-8?q?frame=20reader=20=E2=80=94=20camera=20drives=20at=20its=20best?= =?UTF-8?q?=20rate?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Slice 10 from today-slices (d1126dd): UVC cameras default MediaFrameReader to their FIRST media type (YUY2 at crippled fps — C920 720p = 5fps). A 60fps container then shows a 5-20fps face cam as "every few frames cut out". Fix: VideoDeviceController.SetMediaStreamPropertiesAsync negotiated to best MJPEG >=640x360 @>=30fps (<=1280 wide) BEFORE creating the frame reader, with device-default fallback. Plus a startup.log truth line of the actual agreed media type so the next diagnosis starts from facts (no more re-deriving the fps). Build 0 warnings, 289 tests. --- Services/MediaCaptureFrameSource.cs | 48 +++++++++++++++++++++++++++++ 1 file changed, 48 insertions(+) diff --git a/Services/MediaCaptureFrameSource.cs b/Services/MediaCaptureFrameSource.cs index 67a057c..8b7d646 100644 --- a/Services/MediaCaptureFrameSource.cs +++ b/Services/MediaCaptureFrameSource.cs @@ -102,6 +102,43 @@ public sealed class MediaCaptureFrameSource : ICameraFrameSource }; await capture.InitializeAsync(settings); + // MediaFrameReader defaults to the camera's FIRST media type — on UVC webcams + // (Logitech C920 et al.) that is often YUY2 at a crippled rate (720p5!), and a + // real-time 60fps container then shows a 5-20fps face cam as "every few frames + // cut out". OBS's mechanism, available via VideoDeviceController (VideoProfile + // is NOT projected in the 19041 SDK): negotiate the stream property. Pick the + // best MJPEG >=640x360 @>=30fps (largest, fastest), set it BEFORE the frame + // reader is created, fall back to device default on refusal, and log the live + // media type so the next diagnosis starts from facts. + try + { + var controller = capture.VideoDeviceController; + var candidates = controller == null + ? new System.Collections.Generic.List() + : controller + .GetAvailableMediaStreamProperties(MediaStreamType.VideoPreview) + .OfType() + .Where(v => string.Equals(v.Subtype, "MJPG", StringComparison.OrdinalIgnoreCase) + && v.Width >= 640 && v.Height >= 360 + && v.FrameRate.Denominator > 0 + && v.FrameRate.Numerator / v.FrameRate.Denominator >= 30.0) + .OrderByDescending(v => v.FrameRate.Numerator / (double)Math.Max(1, v.FrameRate.Denominator)) + .ThenByDescending(v => (long)v.Width * v.Height) + .ToList(); + if (candidates.Count > 0) + { + var pick = candidates + .OrderBy(v => (long)v.Width * v.Height) + .LastOrDefault(v => v.Width <= 1280) ?? candidates[0]; + await controller!.SetMediaStreamPropertiesAsync(MediaStreamType.VideoPreview, pick); + } + } + catch (Exception fmtEx) + { + AppLog.Write($"MediaCaptureFrameSource: MJPG negotiation refused ({fmtEx.Message}); " + + "keeping device-default media type"); + } + // Post-init resource validation: the capture must actually be bound to // the device we asked for and must expose a live video controller. if (!string.Equals(capture.MediaCaptureSettings?.VideoDeviceId, _deviceId, @@ -147,6 +184,17 @@ public sealed class MediaCaptureFrameSource : ICameraFrameSource 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). + try + { + var active = capture.VideoDeviceController? + .GetMediaStreamProperties(colorSource.Info.MediaStreamType); + if (active is Windows.Media.MediaProperties.VideoEncodingProperties vep) + 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)