using System; using System.Collections.Generic; using System.Linq; using System.Runtime.InteropServices; using System.Runtime.InteropServices.WindowsRuntime; using System.Threading.Tasks; using Windows.Graphics.Imaging; using Windows.Media.Capture; using Windows.Media.Capture.Frames; using Windows.Media.Devices; using Windows.Media.MediaProperties; using ytLive.Helpers; namespace ytLive.Services; /// /// CPU-first MediaCapture source. Requests BGRA8 frames from the camera's video /// preview source; the capture pipeline does any format conversion, so every /// frame arrives as a normalized . Frames arrive on a /// worker thread — marshal before touching WPF. /// /// Allocation is validated, never taken on faith: after InitializeAsync /// the bound device, its frame sources, and its stream properties are checked, /// the frame reader's start status is read (not swallowed), and the live /// state signals (Failed, CameraStreamStateChanged) are subscribed /// so an async death surfaces as instead of a silent /// empty preview. /// public sealed class MediaCaptureFrameSource : ICameraFrameSource { private readonly string _deviceId; private MediaCapture? _capture; private MediaFrameReader? _frameReader; private string? _lastError; private bool _isFailed; public string DeviceId => _deviceId; public event Action? FrameAvailable; public event Action? SourceFailed; public MediaCaptureFrameSource(string deviceId) { _deviceId = deviceId; } public async Task StartAsync() { // Fallback ladder: prefer the camera's VideoPreview stream, but if the // reader refuses to start there (NoVideoFrameAvailable), retry against // its VideoRecord stream — some devices only deliver through it. // (MediaCaptureSharingMode.Exclusive is not projected by the 19041 SDK.) if (!await TryStartAsync(preferVideoRecord: false)) { if (!await TryStartAsync(preferVideoRecord: true)) throw new InvalidOperationException( _lastError ?? $"Camera '{_deviceId}' could not be started."); } } public async Task StopAsync() { var reader = _frameReader; _frameReader = null; if (reader != null) { reader.FrameArrived -= OnFrameArrived; try { await reader.StopAsync(); } catch (Exception ex) { AppLog.Write($"MediaCaptureFrameSource: stop frame reader failed: {ex.Message}"); } reader.Dispose(); } var capture = _capture; _capture = null; if (capture != null) { UnsubscribeCaptureEvents(capture); capture.Dispose(); } } private async Task TryStartAsync(bool preferVideoRecord) { MediaCapture? capture = null; MediaFrameReader? reader = null; try { capture = new MediaCapture(); var settings = new MediaCaptureInitializationSettings { VideoDeviceId = _deviceId, StreamingCaptureMode = StreamingCaptureMode.Video, MemoryPreference = MediaCaptureMemoryPreference.Cpu, SharingMode = MediaCaptureSharingMode.SharedReadOnly, }; await capture.InitializeAsync(settings); // 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, StringComparison.OrdinalIgnoreCase)) throw new InvalidOperationException( $"Camera '{_deviceId}' initialized but bound a different device."); var colorSource = capture.FrameSources.Values .OrderBy(s => preferVideoRecord ? (s.Info.MediaStreamType == MediaStreamType.VideoRecord ? 0 : 1) : (s.Info.MediaStreamType == MediaStreamType.VideoPreview ? 0 : 1)) .FirstOrDefault(s => s.Info.MediaStreamType is MediaStreamType.VideoPreview or MediaStreamType.VideoRecord); if (colorSource == null) { var kinds = string.Join(", ", capture.FrameSources.Values .Select(s => s.Info.MediaStreamType).Distinct()); throw new InvalidOperationException( $"Camera '{_deviceId}' exposes no video source (found: {kinds}). " + "It may be locked by another app (e.g. NVIDIA Broadcast)."); } // The device must answer for its preview stream — a dead, suspended, or // locked device returns no stream properties even after "successful" init. IReadOnlyList? previewProps; try { previewProps = capture.VideoDeviceController? .GetAvailableMediaStreamProperties(MediaStreamType.VideoPreview); } catch { previewProps = null; } if (previewProps == null || previewProps.Count == 0) throw new InvalidOperationException( $"Camera '{_deviceId}' answered no video stream properties (offline, suspended, or locked)."); // The OS's live state signals: async failures must surface, not vanish. 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."); reader.FrameArrived += OnFrameArrived; var status = await reader.StartAsync(); if (status != MediaFrameReaderStartStatus.Success) throw new InvalidOperationException( $"Camera '{_deviceId}' frame reader refused to start: {status}."); _capture = capture; _frameReader = reader; _lastError = null; _isFailed = false; return true; } catch (Exception ex) { _lastError = ex.Message; if (reader != null) { reader.FrameArrived -= OnFrameArrived; reader.Dispose(); } if (capture != null) { UnsubscribeCaptureEvents(capture); capture.Dispose(); } return false; } } private void UnsubscribeCaptureEvents(MediaCapture capture) { capture.Failed -= OnCaptureFailed; capture.CameraStreamStateChanged -= OnCameraStreamStateChanged; } private void OnCaptureFailed(MediaCapture sender, MediaCaptureFailedEventArgs args) => RaiseFailure($"capture failed ({args.Code}): {args.Message}"); private void OnCameraStreamStateChanged(MediaCapture sender, object args) { // CameraStreamState enum (Windows.Media.Devices): 0=NotStreaming, 1=Streaming, // 2=Failed, 3=Shutdown. The 19041 SDK projection omits member names, so // compare by value — the enum type itself resolves via the property return. if ((int)sender.CameraStreamState == 2) // Failed RaiseFailure("camera stream state is Failed"); } private void RaiseFailure(string message) { if (_isFailed) return; _isFailed = true; AppLog.Write($"MediaCaptureFrameSource: {message}"); try { SourceFailed?.Invoke(message); } catch (Exception ex) { AppLog.Write($"MediaCaptureFrameSource: SourceFailed handler threw: {ex.Message}"); } } private void OnFrameArrived(MediaFrameReader sender, MediaFrameArrivedEventArgs args) { using var frame = sender.TryAcquireLatestFrame(); var videoFrame = frame?.VideoMediaFrame?.SoftwareBitmap; if (videoFrame == null) return; var bitmap = videoFrame.BitmapPixelFormat == BitmapPixelFormat.Bgra8 ? videoFrame : SoftwareBitmap.Convert(videoFrame, BitmapPixelFormat.Bgra8); try { using var buffer = bitmap.LockBuffer(BitmapBufferAccessMode.Read); using var reference = buffer.CreateReference(); if (WindowsRuntimeMarshal.TryGetDataUnsafe(reference, out var pixelsPtr, out var capacity)) { var pixels = new byte[capacity]; Marshal.Copy(pixelsPtr, pixels, 0, (int)capacity); FrameAvailable?.Invoke(new VideoFrame(bitmap.PixelWidth, bitmap.PixelHeight, pixels)); } } catch (Exception ex) { AppLog.Write($"MediaCaptureFrameSource: frame copy failed: {ex.Message}"); } finally { if (!ReferenceEquals(bitmap, videoFrame)) bitmap.Dispose(); } } }