Files
TimerApp/NativeMethods.cs

393 lines
13 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

using System;
using System.Runtime.InteropServices;
using System.Threading;
using System.Threading.Tasks;
using Windows.Media.Control;
namespace TimerApp
{
public static class NativeMethods
{
[StructLayout(LayoutKind.Sequential)]
struct LASTINPUTINFO
{
public static readonly int SizeOf = Marshal.SizeOf(typeof(LASTINPUTINFO));
[MarshalAs(UnmanagedType.U4)]
public UInt32 cbSize;
[MarshalAs(UnmanagedType.U4)]
public UInt32 dwTime;
}
[DllImport("user32.dll")]
static extern bool GetLastInputInfo(ref LASTINPUTINFO plii);
[DllImport("kernel32.dll")]
static extern uint GetTickCount();
/// <summary>
/// 获取系统空闲时间(毫秒)
/// </summary>
/// <returns></returns>
public static long GetIdleTime()
{
LASTINPUTINFO lastInputInfo = new LASTINPUTINFO
{
cbSize = (uint)Marshal.SizeOf<LASTINPUTINFO>(),
dwTime = 0
};
if (GetLastInputInfo(ref lastInputInfo))
{
uint tickCount = GetTickCount();
uint idleMs = unchecked(tickCount - lastInputInfo.dwTime);
return idleMs;
}
return 0;
}
// ==========================================
// 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();
}
/// <summary>
/// 检测是否有媒体正在播放(视频或音频)
/// 直接返回缓存的状态,无阻塞
/// </summary>
public static bool IsMediaPlaying()
{
return _isMediaPlayingCaught;
}
/// <summary>
/// 强制立即进行一次检测
/// </summary>
public static void InvalidateMediaCache()
{
// 唤醒后台线程立即检测
_checkSignal.Set();
}
/// <summary>
/// 应用退出时清理资源 (可选调用)
/// </summary>
public static void Shutdown()
{
_cts.Cancel();
_checkSignal.Set(); // Wake up to exit
}
private static void StartMediaMonitor()
{
if (_mediaMonitorThread != null) return;
_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)
{
try
{
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
{
isPlaying = TryIsAudioPlaying();
}
catch
{
// Ignored
}
}
// Update volatile cache
_isMediaPlayingCaught = isPlaying;
// Wait for next check (default 1s, or wake up immediately on Invalidate)
_checkSignal.WaitOne(1000);
}
catch
{
// Prevent thread crash
Thread.Sleep(1000);
}
}
}
private static bool CheckSystemMediaTransportControls()
{
// 同步等待异步方法,因为我们在专用线程上,且没有 UI SyncContext这是安全的
return CheckSystemMediaTransportControlsAsync().GetAwaiter().GetResult();
}
private static async Task<bool> 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);
}
private static bool TryIsAudioPlaying(ERole role)
{
object? deviceEnumeratorObj = null;
IMMDeviceEnumerator? deviceEnumerator = null;
IMMDevice? device = null;
object? sessionManagerObj = null;
IAudioSessionManager2? sessionManager = null;
IAudioSessionEnumerator? sessionEnumerator = null;
try
{
Type? enumeratorType = Type.GetTypeFromCLSID(CLSID_MMDeviceEnumerator);
if (enumeratorType is null) return false;
deviceEnumeratorObj = Activator.CreateInstance(enumeratorType);
if (deviceEnumeratorObj is null) return false;
deviceEnumerator = (IMMDeviceEnumerator)deviceEnumeratorObj;
// 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;
hr = device.Activate(ref iid, CLSCTX.CLSCTX_ALL, IntPtr.Zero, out sessionManagerObj);
if (hr != 0 || sessionManagerObj is null) return false;
sessionManager = (IAudioSessionManager2)sessionManagerObj;
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++)
{
IAudioSessionControl? sessionControl = null;
try
{
hr = sessionEnumerator.GetSession(i, out sessionControl);
if (hr != 0 || sessionControl is null) continue;
hr = sessionControl.GetState(out AudioSessionState state);
if (hr != 0) continue;
if (state == AudioSessionState.Active)
{
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;
}
}
}
finally
{
if (sessionControl != null) Marshal.FinalReleaseComObject(sessionControl);
}
}
return false;
}
catch
{
return false;
}
finally
{
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 readonly Guid CLSID_MMDeviceEnumerator = new Guid("BCDE0395-E52F-467C-8E3D-C4579291692E");
private enum EDataFlow
{
eRender = 0,
eCapture = 1,
eAll = 2
}
private enum ERole
{
eConsole = 0,
eMultimedia = 1,
eCommunications = 2
}
private enum AudioSessionState
{
Inactive = 0,
Active = 1,
Expired = 2
}
[Flags]
private enum CLSCTX : uint
{
CLSCTX_INPROC_SERVER = 0x1,
CLSCTX_INPROC_HANDLER = 0x2,
CLSCTX_LOCAL_SERVER = 0x4,
CLSCTX_REMOTE_SERVER = 0x10,
CLSCTX_ALL = CLSCTX_INPROC_SERVER | CLSCTX_INPROC_HANDLER | CLSCTX_LOCAL_SERVER | CLSCTX_REMOTE_SERVER
}
[ComImport]
[Guid("A95664D2-9614-4F35-A746-DE8DB63617E6")]
[InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
private interface IMMDeviceEnumerator
{
int NotImpl1();
[PreserveSig]
int GetDefaultAudioEndpoint(EDataFlow dataFlow, ERole role, out IMMDevice ppDevice);
}
[ComImport]
[Guid("D666063F-1587-4E43-81F1-B948E807363F")]
[InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
private interface IMMDevice
{
[PreserveSig]
int Activate(ref Guid iid, CLSCTX dwClsCtx, IntPtr pActivationParams, [MarshalAs(UnmanagedType.IUnknown)] out object ppInterface);
}
[ComImport]
[Guid("77AA99A0-1BD6-484F-8BC7-2C654C9A9B6F")]
[InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
private interface IAudioSessionManager2
{
int NotImpl1();
int NotImpl2();
[PreserveSig]
int GetSessionEnumerator(out IAudioSessionEnumerator sessionEnum);
}
[ComImport]
[Guid("E2F5BB11-0570-40CA-ACDD-3AA01277DEE8")]
[InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
private interface IAudioSessionEnumerator
{
[PreserveSig]
int GetCount(out int sessionCount);
[PreserveSig]
int GetSession(int sessionCount, out IAudioSessionControl session);
}
[ComImport]
[Guid("F4B1A599-7266-4319-A8CA-E70ACB11E8CD")]
[InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
private interface IAudioSessionControl
{
[PreserveSig]
int GetState(out AudioSessionState state);
int NotImpl1();
int NotImpl2();
int NotImpl3();
int NotImpl4();
int NotImpl5();
int NotImpl6();
int NotImpl7();
int NotImpl8();
}
[ComImport]
[Guid("C02216F6-8C67-4B5B-9D00-D008E73E0064")]
[InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
private interface IAudioMeterInformation
{
[PreserveSig]
int GetPeakValue(out float peak);
}
}
}