From cb6a4f4d2c0375786e7270a5d925123ed7a64ca1 Mon Sep 17 00:00:00 2001 From: Solin Date: Tue, 20 Jan 2026 11:23:06 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E9=87=8D=E6=9E=84=E5=AA=92=E4=BD=93?= =?UTF-8?q?=E6=92=AD=E6=94=BE=E6=A3=80=E6=B5=8B=E9=80=BB=E8=BE=91=EF=BC=8C?= =?UTF-8?q?=E4=BD=BF=E7=94=A8=E5=90=8E=E5=8F=B0STA=E7=BA=BF=E7=A8=8B?= =?UTF-8?q?=E6=8C=81=E7=BB=AD=E7=9B=91=E6=8E=A7=E5=B9=B6=E9=9B=86=E6=88=90?= =?UTF-8?q?WinRT=20API=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ActivityMonitor.cs | 52 +++++--- NativeMethods.cs | 323 +++++++++++++++++++++++---------------------- 2 files changed, 205 insertions(+), 170 deletions(-) diff --git a/ActivityMonitor.cs b/ActivityMonitor.cs index ead9e7c..887a6cc 100644 --- a/ActivityMonitor.cs +++ b/ActivityMonitor.cs @@ -124,29 +124,54 @@ namespace TimerApp private void OnTick() { + // 在锁外执行耗时的系统检测 + long idleMs = 0; + bool? newMediaPlaying = null; + lock (_lock) { // 如果处于暂停状态,不处理计时逻辑 - if (_isPaused) - { - return; - } + if (_isPaused) return; - long idleMs; - TimeSpan idleTime; - - // 优化:降低系统API调用频率 - // 每 CheckIntervalTicks (3) 秒更新一次状态 + // 检查是否需要更新状态 _checkTickCounter++; if (_checkTickCounter >= CheckIntervalTicks) { _checkTickCounter = 0; - _cachedIdleMs = NativeMethods.GetIdleTime(); - _cachedMediaPlaying = NativeMethods.IsMediaPlaying(); + // 需要更新,但在锁外进行 + newMediaPlaying = true; } + + // 如果不需要更新,直接使用缓存值 + if (newMediaPlaying == null) + { + idleMs = _cachedIdleMs; + } + } + // 在锁外执行实际的检测 + if (newMediaPlaying == true) + { + idleMs = NativeMethods.GetIdleTime(); + bool playing = NativeMethods.IsMediaPlaying(); + + // 更新缓存 + lock (_lock) + { + _cachedIdleMs = idleMs; + _cachedMediaPlaying = playing; + } + } + + lock (_lock) + { + // 再次检查暂停状态 + if (_isPaused) return; + + // 使用(可能是新更新的)缓存值 idleMs = _cachedIdleMs; - idleTime = TimeSpan.FromMilliseconds(idleMs); + bool isMediaPlaying = _cachedMediaPlaying; + TimeSpan idleTime = TimeSpan.FromMilliseconds(idleMs); if (CurrentState == MonitorState.Resting) { @@ -170,9 +195,6 @@ namespace TimerApp } else { - // 使用缓存的媒体播放状态 - bool isMediaPlaying = _cachedMediaPlaying; - // 工作/空闲模式逻辑 if (idleTime > IdleThreshold || isMediaPlaying) { diff --git a/NativeMethods.cs b/NativeMethods.cs index 0421e3f..d6d46f3 100644 --- a/NativeMethods.cs +++ b/NativeMethods.cs @@ -22,6 +22,9 @@ namespace TimerApp [DllImport("user32.dll")] static extern bool GetLastInputInfo(ref LASTINPUTINFO plii); + [DllImport("kernel32.dll")] + static extern uint GetTickCount(); + /// /// 获取系统空闲时间(毫秒) /// @@ -44,87 +47,164 @@ namespace TimerApp return 0; } - [DllImport("kernel32.dll")] - static extern uint GetTickCount(); + // ========================================== + // Media Detection Logic (Refactored) + // ========================================== + + private static volatile bool _isMediaPlayingCaught; + private static Thread? _mediaMonitorThread; + private static readonly CancellationTokenSource _cts = new CancellationTokenSource(); + private static readonly AutoResetEvent _checkSignal = new AutoResetEvent(false); + + static NativeMethods() + { + // 在静态构造函数中启动后台监控线程 + StartMediaMonitor(); + } /// /// 检测是否有媒体正在播放(视频或音频) + /// 直接返回缓存的状态,无阻塞 /// - /// 如果有媒体正在播放返回 true,否则返回 false public static bool IsMediaPlaying() { - EnsureMediaPlaybackStatusFresh(); - return Volatile.Read(ref _mediaPlaying); + return _isMediaPlayingCaught; } /// - /// 强制失效媒体播放状态缓存 + /// 强制立即进行一次检测 /// public static void InvalidateMediaCache() { - Interlocked.Exchange(ref _mediaLastUpdateMs, -1); - // 不直接重置 _mediaPlaying,以免在检测过程中出现闪烁, - // 只是强制下一次 IsMediaPlaying 触发新的检测。 - // 但如果处于 Resume 状态,假设不播放是安全的。 - Volatile.Write(ref _mediaPlaying, false); + // 唤醒后台线程立即检测 + _checkSignal.Set(); } - private static bool _mediaPlaying; - private static long _mediaLastUpdateMs = -1; - private static int _mediaUpdateInProgress; - private const int MediaCacheWhenPlayingMs = 500; - private const int MediaCacheWhenNotPlayingMs = 1200; - - private static void EnsureMediaPlaybackStatusFresh() + /// + /// 应用退出时清理资源 (可选调用) + /// + public static void Shutdown() { - long now = Environment.TickCount64; - long last = Interlocked.Read(ref _mediaLastUpdateMs); - int cacheMs = Volatile.Read(ref _mediaPlaying) ? MediaCacheWhenPlayingMs : MediaCacheWhenNotPlayingMs; - if (last >= 0 && now - last < cacheMs) - { - return; - } + _cts.Cancel(); + _checkSignal.Set(); // Wake up to exit + } - if (Interlocked.Exchange(ref _mediaUpdateInProgress, 1) == 1) - { - return; - } + private static void StartMediaMonitor() + { + if (_mediaMonitorThread != null) return; - _ = Task.Run(() => + _mediaMonitorThread = new Thread(MediaMonitorLoop) + { + IsBackground = true, + Name = "TimerApp_MediaMonitor", + Priority = ThreadPriority.BelowNormal + }; + _mediaMonitorThread.SetApartmentState(ApartmentState.STA); // COM requirement + _mediaMonitorThread.Start(); + } + + private static void MediaMonitorLoop() + { + while (!_cts.IsCancellationRequested) { - bool playing = false; try { - playing = RunOnStaThread(() => + bool isPlaying = false; + + // 1. Check System Media Transport Controls (WinRT) + // 需要在 STA 线程中小心处理 Task + try + { + // 使用 Task.Run 确保在一个干净的上下文执行异步任务,然后同步等待结果 + // 注意:因为我们本身就在 STA 线程,直接 .Result 可能会有风险, + // 但对于 GSMTC API,只要没有 SynchronizationContext 绑定到此线程通常是安全的。 + // 这里为了稳妥,我们捕获任何异常。 + isPlaying = CheckSystemMediaTransportControls(); + } + catch + { + // Ignored + } + + // 2. Check Legacy Audio Session API (COM) + if (!isPlaying) { try { - return TryIsSystemMediaSessionPlayingAsync().GetAwaiter().GetResult(); + isPlaying = TryIsAudioPlaying(); } catch { - return false; + // Ignored } - }); + } - playing = playing || TryIsAudioPlaying(); + // Update volatile cache + _isMediaPlayingCaught = isPlaying; + + // Wait for next check (default 1s, or wake up immediately on Invalidate) + _checkSignal.WaitOne(1000); } catch { - playing = false; + // Prevent thread crash + Thread.Sleep(1000); } - finally - { - Volatile.Write(ref _mediaPlaying, playing); - Interlocked.Exchange(ref _mediaLastUpdateMs, Environment.TickCount64); - Interlocked.Exchange(ref _mediaUpdateInProgress, 0); - } - }); + } } + private static bool CheckSystemMediaTransportControls() + { + // 同步等待异步方法,因为我们在专用线程上,且没有 UI SyncContext,这是安全的 + return CheckSystemMediaTransportControlsAsync().GetAwaiter().GetResult(); + } + + private static async Task CheckSystemMediaTransportControlsAsync() + { + try + { + // RequestAsync 可能在某些系统上较慢 + var manager = await GlobalSystemMediaTransportControlsSessionManager.RequestAsync(); + + var current = manager.GetCurrentSession(); + if (IsPlaying(current)) return true; + + foreach (var session in manager.GetSessions()) + { + if (IsPlaying(session)) return true; + } + + return false; + } + catch + { + return false; + } + } + + private static bool IsPlaying(GlobalSystemMediaTransportControlsSession? session) + { + if (session == null) return false; + try + { + var info = session.GetPlaybackInfo(); + return info != null && info.PlaybackStatus == GlobalSystemMediaTransportControlsSessionPlaybackStatus.Playing; + } + catch + { + return false; + } + } + + // ========================================== + // COM Audio Detection + // ========================================== + private static bool TryIsAudioPlaying() { - return TryIsAudioPlaying(ERole.eMultimedia) || TryIsAudioPlaying(ERole.eConsole) || TryIsAudioPlaying(ERole.eCommunications); + return TryIsAudioPlaying(ERole.eMultimedia) || + TryIsAudioPlaying(ERole.eConsole) || + TryIsAudioPlaying(ERole.eCommunications); } private static bool TryIsAudioPlaying(ERole role) @@ -139,51 +219,57 @@ namespace TimerApp try { Type? enumeratorType = Type.GetTypeFromCLSID(CLSID_MMDeviceEnumerator); - if (enumeratorType is null) - return false; + if (enumeratorType is null) return false; deviceEnumeratorObj = Activator.CreateInstance(enumeratorType); - if (deviceEnumeratorObj is null) - return false; + if (deviceEnumeratorObj is null) return false; deviceEnumerator = (IMMDeviceEnumerator)deviceEnumeratorObj; - Marshal.ThrowExceptionForHR(deviceEnumerator.GetDefaultAudioEndpoint(EDataFlow.eRender, role, out device)); + // GetDefaultAudioEndpoint can fail if no audio device is present + int hr = deviceEnumerator.GetDefaultAudioEndpoint(EDataFlow.eRender, role, out device); + if (hr != 0) return false; // S_OK = 0 Guid iid = typeof(IAudioSessionManager2).GUID; - Marshal.ThrowExceptionForHR(device.Activate(ref iid, CLSCTX.CLSCTX_ALL, IntPtr.Zero, out sessionManagerObj)); - if (sessionManagerObj is null) - return false; + hr = device.Activate(ref iid, CLSCTX.CLSCTX_ALL, IntPtr.Zero, out sessionManagerObj); + if (hr != 0 || sessionManagerObj is null) return false; + sessionManager = (IAudioSessionManager2)sessionManagerObj; - Marshal.ThrowExceptionForHR(sessionManager.GetSessionEnumerator(out sessionEnumerator)); - Marshal.ThrowExceptionForHR(sessionEnumerator.GetCount(out int count)); + hr = sessionManager.GetSessionEnumerator(out sessionEnumerator); + if (hr != 0) return false; + + hr = sessionEnumerator.GetCount(out int count); + if (hr != 0) return false; for (int i = 0; i < count; i++) { - Marshal.ThrowExceptionForHR(sessionEnumerator.GetSession(i, out IAudioSessionControl? sessionControl)); - if (sessionControl is null) - continue; - + IAudioSessionControl? sessionControl = null; try { - Marshal.ThrowExceptionForHR(sessionControl.GetState(out AudioSessionState state)); - if (state != AudioSessionState.Active) - continue; + hr = sessionEnumerator.GetSession(i, out sessionControl); + if (hr != 0 || sessionControl is null) continue; - if (sessionControl is IAudioMeterInformation meter) + hr = sessionControl.GetState(out AudioSessionState state); + if (hr != 0) continue; + + if (state == AudioSessionState.Active) { - Marshal.ThrowExceptionForHR(meter.GetPeakValue(out float peak)); - if (peak > 0.0001f) + if (sessionControl is IAudioMeterInformation meter) + { + hr = meter.GetPeakValue(out float peak); + if (hr == 0 && peak > 0.000001f) // Slightly relaxed threshold + return true; + } + else + { + // Cannot check peak, assume active means playing return true; - } - else - { - return true; + } } } finally { - Marshal.FinalReleaseComObject(sessionControl); + if (sessionControl != null) Marshal.FinalReleaseComObject(sessionControl); } } @@ -195,94 +281,14 @@ namespace TimerApp } finally { - if (sessionEnumerator is not null) Marshal.FinalReleaseComObject(sessionEnumerator); - if (sessionManagerObj is not null) Marshal.FinalReleaseComObject(sessionManagerObj); - if (device is not null) Marshal.FinalReleaseComObject(device); - if (deviceEnumeratorObj is not null) Marshal.FinalReleaseComObject(deviceEnumeratorObj); + if (sessionEnumerator != null) Marshal.FinalReleaseComObject(sessionEnumerator); + if (sessionManager != null) Marshal.FinalReleaseComObject(sessionManager); // Corrected variable usage + if (sessionManagerObj != null) Marshal.FinalReleaseComObject(sessionManagerObj); + if (device != null) Marshal.FinalReleaseComObject(device); + if (deviceEnumeratorObj != null) Marshal.FinalReleaseComObject(deviceEnumeratorObj); } } - private static async Task TryIsSystemMediaSessionPlayingAsync() - { - try - { - GlobalSystemMediaTransportControlsSessionManager manager = await GlobalSystemMediaTransportControlsSessionManager.RequestAsync(); - - GlobalSystemMediaTransportControlsSession? current = manager.GetCurrentSession(); - if (IsPlaying(current)) - { - return true; - } - - foreach (GlobalSystemMediaTransportControlsSession session in manager.GetSessions()) - { - if (IsPlaying(session)) - { - return true; - } - } - - return false; - } - catch - { - return false; - } - } - - private static bool IsPlaying(GlobalSystemMediaTransportControlsSession? session) - { - if (session is null) - { - return false; - } - - try - { - GlobalSystemMediaTransportControlsSessionPlaybackInfo? info = session.GetPlaybackInfo(); - return info is not null && info.PlaybackStatus == GlobalSystemMediaTransportControlsSessionPlaybackStatus.Playing; - } - catch - { - return false; - } - } - - private static bool RunOnStaThread(Func action) - { - bool result = false; - Exception? error = null; - - Thread thread = new Thread(() => - { - try - { - result = action(); - } - catch (Exception ex) - { - error = ex; - } - }); - - thread.IsBackground = true; - thread.SetApartmentState(ApartmentState.STA); - thread.Start(); - - // Wait with timeout (e.g. 2 seconds) to prevent hanging - if (!thread.Join(2000)) - { - return false; - } - - if (error is not null) - { - return false; - } - - return result; - } - private static readonly Guid CLSID_MMDeviceEnumerator = new Guid("BCDE0395-E52F-467C-8E3D-C4579291692E"); private enum EDataFlow @@ -322,6 +328,7 @@ namespace TimerApp private interface IMMDeviceEnumerator { int NotImpl1(); + [PreserveSig] int GetDefaultAudioEndpoint(EDataFlow dataFlow, ERole role, out IMMDevice ppDevice); } @@ -330,6 +337,7 @@ namespace TimerApp [InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] private interface IMMDevice { + [PreserveSig] int Activate(ref Guid iid, CLSCTX dwClsCtx, IntPtr pActivationParams, [MarshalAs(UnmanagedType.IUnknown)] out object ppInterface); } @@ -340,6 +348,7 @@ namespace TimerApp { int NotImpl1(); int NotImpl2(); + [PreserveSig] int GetSessionEnumerator(out IAudioSessionEnumerator sessionEnum); } @@ -348,7 +357,9 @@ namespace TimerApp [InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] private interface IAudioSessionEnumerator { + [PreserveSig] int GetCount(out int sessionCount); + [PreserveSig] int GetSession(int sessionCount, out IAudioSessionControl session); } @@ -357,6 +368,7 @@ namespace TimerApp [InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] private interface IAudioSessionControl { + [PreserveSig] int GetState(out AudioSessionState state); int NotImpl1(); int NotImpl2(); @@ -373,6 +385,7 @@ namespace TimerApp [InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] private interface IAudioMeterInformation { + [PreserveSig] int GetPeakValue(out float peak); } }