using System; using System.Collections.Generic; using System.Threading.Tasks; using System.Windows; using System.Windows.Media; using System.Windows.Media.Imaging; using System.Windows.Threading; using ytLive.Helpers; namespace ytLive.Services; /// /// Owns webcam capture sessions app-wide, refcounted by DeviceId. One device = /// at most one capture, one shared WriteableBitmap; the last release stops and /// disposes the source. Frames arrive on a worker thread and are coalesced onto /// the UI dispatcher (at most one pending copy per session, using the latest /// frame) so a 60fps device doesn't drown the render thread. /// public sealed class CameraManager : IDisposable { private sealed class CameraSession { public string DeviceId { get; } public ICameraFrameSource Source { get; } public Action? FrameHandler; public int RefCount; public bool Started; public WriteableBitmap? PreviewBitmap; public VideoFrame? LatestFrame; public bool FramePending; public CameraSession(string deviceId, ICameraFrameSource source) { DeviceId = deviceId; Source = source; RefCount = 1; } } private readonly ICameraEnumerator _enumerator; private readonly Func _frameSourceFactory; private readonly Dispatcher? _uiDispatcher; private readonly Dictionary _sessions = new(); private readonly object _gate = new(); /// Raised on the UI thread when a camera's shared preview bitmap is first created. public event Action? PreviewBitmapChanged; /// Raised when a capture fails to start (device in use, access denied, no preview source). public event Action? CameraFailed; public CameraManager(ICameraEnumerator enumerator, Func frameSourceFactory, Dispatcher? uiDispatcher = null) { _enumerator = enumerator; _frameSourceFactory = frameSourceFactory; _uiDispatcher = uiDispatcher; } public ICameraEnumerator Enumerator => _enumerator; /// Increments the refcount for a device, starting capture the first time. public async Task AcquireAsync(string deviceId) { if (string.IsNullOrWhiteSpace(deviceId)) return false; CameraSession session; bool shouldStart; lock (_gate) { if (_sessions.TryGetValue(deviceId, out var existing)) { existing.RefCount++; shouldStart = false; session = existing; } else { session = new CameraSession(deviceId, _frameSourceFactory(deviceId)); session.FrameHandler = frame => OnFrameAvailable(session, frame); session.Source.FrameAvailable += session.FrameHandler; _sessions[deviceId] = session; shouldStart = true; } } if (!shouldStart) return session.Started; try { await session.Source.StartAsync(); session.Started = true; return true; } catch (Exception ex) { lock (_gate) _sessions.Remove(deviceId); session.Source.FrameAvailable -= session.FrameHandler; AppLog.Write($"CameraManager: failed to start camera '{deviceId}': {ex.Message}"); CameraFailed?.Invoke(deviceId, ex.Message); await SafeStopAsync(session.Source); return false; } } /// Decrements the refcount; stops and disposes the source at zero. public async Task ReleaseAsync(string deviceId) { CameraSession? toStop = null; lock (_gate) { if (!_sessions.TryGetValue(deviceId, out var session)) return; if (--session.RefCount > 0) return; _sessions.Remove(deviceId); toStop = session; } if (toStop == null) return; toStop.Source.FrameAvailable -= toStop.FrameHandler; await SafeStopAsync(toStop.Source); toStop.PreviewBitmap = null; } /// /// Releases a device unconditionally (every ref), regardless of how many scenes /// hold it. Used on identity changes (webcam swap, layout reload) where the old /// device's refcount isn't known after the scenes are replaced. /// public async Task ReleaseAllAsync(string deviceId) { CameraSession? toStop = null; lock (_gate) { if (!_sessions.TryGetValue(deviceId, out var session)) return; session.RefCount = 0; _sessions.Remove(deviceId); toStop = session; } if (toStop == null) return; toStop.Source.FrameAvailable -= toStop.FrameHandler; await SafeStopAsync(toStop.Source); toStop.PreviewBitmap = null; } public VideoFrame? GetLatestFrame(string deviceId) { lock (_gate) return _sessions.TryGetValue(deviceId, out var session) ? session.LatestFrame : null; } public void Dispose() { List sessions; lock (_gate) { sessions = new List(_sessions.Values); _sessions.Clear(); } foreach (var session in sessions) { session.Source.FrameAvailable -= session.FrameHandler; _ = SafeStopAsync(session.Source); } } private static async Task SafeStopAsync(ICameraFrameSource source) { try { await source.StopAsync(); } catch (Exception ex) { AppLog.Write($"CameraManager: stopping camera failed: {ex.Message}"); } } private bool TryGetActiveSession(CameraSession session) { lock (_gate) return _sessions.TryGetValue(session.DeviceId, out var current) && ReferenceEquals(current, session); } private void OnFrameAvailable(CameraSession session, VideoFrame frame) { if (!TryGetActiveSession(session)) return; session.LatestFrame = frame; if (session.PreviewBitmap == null) { if (_uiDispatcher == null) return; if (_uiDispatcher.CheckAccess()) EnsurePreviewBitmap(session); else _uiDispatcher.BeginInvoke(() => { if (TryGetActiveSession(session)) EnsurePreviewBitmap(session); }, DispatcherPriority.Render); return; } if (_uiDispatcher == null || _uiDispatcher.CheckAccess()) { CopyFrame(session); return; } if (session.FramePending) return; session.FramePending = true; _uiDispatcher.BeginInvoke(() => { session.FramePending = false; if (TryGetActiveSession(session) && session.PreviewBitmap != null) CopyFrame(session); }, DispatcherPriority.Render); } private void EnsurePreviewBitmap(CameraSession session) { if (session.PreviewBitmap != null || session.LatestFrame == null) return; var frame = session.LatestFrame; var bitmap = new WriteableBitmap(frame.Width, frame.Height, 96, 96, PixelFormats.Bgra32, null); bitmap.WritePixels(new Int32Rect(0, 0, frame.Width, frame.Height), frame.BgraPixels, frame.Stride, 0); session.PreviewBitmap = bitmap; PreviewBitmapChanged?.Invoke(session.DeviceId, bitmap); } private void CopyFrame(CameraSession session) { var bitmap = session.PreviewBitmap; var frame = session.LatestFrame; if (bitmap == null || frame == null) return; if (frame.Width != bitmap.PixelWidth || frame.Height != bitmap.PixelHeight) return; bitmap.WritePixels(new Int32Rect(0, 0, frame.Width, frame.Height), frame.BgraPixels, frame.Stride, 0); } }