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);
}
}