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 screen-capture sessions app-wide, refcounted by target key /// ("monitor:<n>", "window:<hwnd>", "picker:<...>"). One target = /// 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). Mirrors CameraManager. /// public sealed class ScreenCaptureManager : IDisposable { private sealed class CaptureSession { public string Key { get; } public IScreenCaptureSource Source { get; } public Action? FrameHandler; public int RefCount; public bool Started; public WriteableBitmap? PreviewBitmap; public VideoFrame? LatestFrame; public bool FramePending; public CaptureSession(string key, IScreenCaptureSource source) { Key = key; Source = source; RefCount = 1; } } private readonly Func _sourceFactory; private readonly Dispatcher? _uiDispatcher; private readonly Dictionary _sessions = new(); private readonly object _gate = new(); /// Raised on the UI thread when a capture's shared preview bitmap is first created. public event Action? PreviewBitmapChanged; /// Raised when a capture cannot be created or started (monitor gone, access denied, no target). public event Action? CaptureFailed; public ScreenCaptureManager(Func sourceFactory, Dispatcher? uiDispatcher = null) { _sourceFactory = sourceFactory; _uiDispatcher = uiDispatcher; } /// Increments the refcount for a target key, starting capture the first time. public async Task AcquireAsync(string key) { if (string.IsNullOrWhiteSpace(key)) return false; CaptureSession session; bool shouldStart; lock (_gate) { if (_sessions.TryGetValue(key, out var existing)) { existing.RefCount++; shouldStart = false; session = existing; } else { IScreenCaptureSource? source; try { source = _sourceFactory(key); } catch (Exception ex) { AppLog.Write($"ScreenCaptureManager: creating capture '{key}' failed: {ex.Message}"); CaptureFailed?.Invoke(key, ex.Message); return false; } if (source == null) { CaptureFailed?.Invoke(key, "No capture target for this key"); return false; } session = new CaptureSession(key, source); session.FrameHandler = frame => OnFrameAvailable(session, frame); session.Source.FrameAvailable += session.FrameHandler; _sessions[key] = 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(key); session.Source.FrameAvailable -= session.FrameHandler; AppLog.Write($"ScreenCaptureManager: failed to start capture '{key}': {ex.Message}"); CaptureFailed?.Invoke(key, ex.Message); await SafeStopAsync(session.Source); return false; } } /// Decrements the refcount; stops and disposes the source at zero. public async Task ReleaseAsync(string key) { CaptureSession? toStop = null; lock (_gate) { if (!_sessions.TryGetValue(key, out var session)) return; if (--session.RefCount > 0) return; _sessions.Remove(key); toStop = session; } if (toStop == null) return; toStop.Source.FrameAvailable -= toStop.FrameHandler; await SafeStopAsync(toStop.Source); toStop.PreviewBitmap = null; } /// /// Releases a target unconditionally (every ref), regardless of how many /// scenes hold it. Used on capture re-designation and layout reloads where /// the old key's refcount isn't known after the scenes are replaced. /// public async Task ReleaseAllAsync(string key) { CaptureSession? toStop = null; lock (_gate) { if (!_sessions.TryGetValue(key, out var session)) return; session.RefCount = 0; _sessions.Remove(key); toStop = session; } if (toStop == null) return; toStop.Source.FrameAvailable -= toStop.FrameHandler; await SafeStopAsync(toStop.Source); toStop.PreviewBitmap = 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(IScreenCaptureSource source) { try { await source.StopAsync(); } catch (Exception ex) { AppLog.Write($"ScreenCaptureManager: stopping capture failed: {ex.Message}"); } } private bool TryGetActiveSession(CaptureSession session) { lock (_gate) return _sessions.TryGetValue(session.Key, out var current) && ReferenceEquals(current, session); } private void OnFrameAvailable(CaptureSession 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(CaptureSession 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.Key, bitmap); } private void CopyFrame(CaptureSession 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); } }