using System.IO; using System.Runtime.InteropServices; using NAudio.CoreAudioApi; using NAudio.CoreAudioApi.Interfaces; namespace FoxBrowser.Core; /// /// Labels only audio sessions owned by FoxBrowser's descendant WebView2 processes. /// It never changes sessions belonging to unrelated WebView2 applications. /// public sealed class AudioSessionLabeler : IDisposable { private const uint SnapshotProcess = 0x00000002; private readonly uint _rootProcessId; private readonly string _iconPath; private readonly MMDeviceEnumerator _devices = new(); private readonly AudioSessionManager _sessions; private readonly System.Threading.Timer _refreshTimer; private readonly object _gate = new(); private bool _disposed; public AudioSessionLabeler(uint rootProcessId, string iconPath) { _rootProcessId = rootProcessId; _iconPath = iconPath; var device = _devices.GetDefaultAudioEndpoint(DataFlow.Render, Role.Multimedia); _sessions = device.AudioSessionManager; _sessions.OnSessionCreated += OnSessionCreated; _refreshTimer = new System.Threading.Timer(_ => RefreshAndLabel(), null, TimeSpan.Zero, TimeSpan.FromSeconds(2)); } private void OnSessionCreated(object sender, IAudioSessionControl nativeSession) { LabelSession(new AudioSessionControl(nativeSession), disposeAfter: true); } private void RefreshAndLabel() { if (_disposed) return; try { var ownedPids = GetFoxBrowserWebViewPids(); var sessionCollection = _sessions.Sessions; if (sessionCollection is null) return; for (var index = 0; index < sessionCollection.Count; index++) { var session = sessionCollection[index]; if (session is null) continue; try { if (ownedPids.Contains(session.GetProcessID)) LabelSession(session, ownedPids, disposeAfter: false); } finally { session.Dispose(); } } } catch { /* Audio devices can disappear or be replaced while Windows is running. */ } } private void LabelSession(AudioSessionControl session, HashSet? knownOwnedPids = null, bool disposeAfter = false) { try { var ownedPids = knownOwnedPids ?? GetFoxBrowserWebViewPids(); if (!ownedPids.Contains(session.GetProcessID)) return; session.DisplayName = "FoxBrowser"; if (File.Exists(_iconPath)) session.IconPath = _iconPath; } catch { /* A session may close between PID lookup and property update. */ } finally { if (disposeAfter) session.Dispose(); } } private HashSet GetFoxBrowserWebViewPids() { var processes = ReadProcesses(); var children = new Dictionary>(); foreach (var process in processes) { if (!children.TryGetValue(process.ParentId, out var list)) children[process.ParentId] = list = new List(); list.Add(process); } var result = new HashSet(); var pending = new Queue(new[] { _rootProcessId }); while (pending.Count > 0) { var parent = pending.Dequeue(); if (!children.TryGetValue(parent, out var descendants)) continue; foreach (var child in descendants) { pending.Enqueue(child.ProcessId); if (child.Name.Equals("msedgewebview2.exe", StringComparison.OrdinalIgnoreCase)) result.Add(child.ProcessId); } } return result; } private static List ReadProcesses() { var result = new List(); var snapshot = CreateToolhelp32Snapshot(SnapshotProcess, 0); if (snapshot == IntPtr.Zero || snapshot == new IntPtr(-1)) return result; try { var entry = new ProcessEntry32 { Size = (uint)Marshal.SizeOf() }; if (!Process32First(snapshot, ref entry)) return result; do { result.Add(new ProcessRecord(entry.ProcessId, entry.ParentProcessId, entry.ExeFile)); entry.Size = (uint)Marshal.SizeOf(); } while (Process32Next(snapshot, ref entry)); } finally { CloseHandle(snapshot); } return result; } public void Dispose() { lock (_gate) { if (_disposed) return; _disposed = true; _refreshTimer.Dispose(); _sessions.OnSessionCreated -= OnSessionCreated; _sessions.Dispose(); _devices.Dispose(); } } private readonly record struct ProcessRecord(uint ProcessId, uint ParentId, string Name); [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)] private struct ProcessEntry32 { public uint Size; public uint Usage; public uint ProcessId; public IntPtr DefaultHeapId; public uint ModuleId; public uint Threads; public uint ParentProcessId; public int BasePriority; public uint Flags; [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 260)] public string ExeFile; } [DllImport("kernel32.dll", SetLastError = true)] private static extern IntPtr CreateToolhelp32Snapshot(uint flags, uint processId); [DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)] private static extern bool Process32First(IntPtr snapshot, ref ProcessEntry32 entry); [DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)] private static extern bool Process32Next(IntPtr snapshot, ref ProcessEntry32 entry); [DllImport("kernel32.dll", SetLastError = true)] private static extern bool CloseHandle(IntPtr handle); }