+using Hardcodet.Wpf.TaskbarNotification;
+using Microsoft.Web.WebView2.Core;
+using NAudio.CoreAudioApi;
+using NAudio.CoreAudioApi.Interfaces;
+using System;
+using System.Diagnostics;
+using System.IO;
+using System.Runtime.InteropServices;
+using System.Windows;
+using System.Windows.Controls;
+using System.Windows.Controls.Primitives;
+
+namespace YouTubeMusicPlayer
+{
+ public partial class MainWindow : Window
+ {
+ // Windows 10/11 21H2+ 앱별 오디오 출력 설정
+ private static class AudioPolicyConfigNative
+ {
+ private const string ClassName = "Windows.Media.Internal.AudioPolicyConfig";
+ private static readonly Guid IID = new("ab3d4648-e242-459f-b02f-541c70306324");
+ private const string AudioRenderInterface = "#{e6327cad-dcec-4949-ae8a-991e976a79d2}";
+ private const string MmDevApiPrefix = @"\\?\SWD#MMDEVAPI#";
+
+ [DllImport("combase.dll", ExactSpelling = true)]
+ private static extern int WindowsCreateString([MarshalAs(UnmanagedType.LPWStr)] string sourceString, uint length, out IntPtr hstring);
+
+ [DllImport("combase.dll", ExactSpelling = true)]
+ private static extern int WindowsDeleteString(IntPtr hstring);
+
+ [DllImport("combase.dll", ExactSpelling = true)]
+ private static extern int RoGetActivationFactory(IntPtr activatableClassId, ref Guid iid, out IntPtr factory);
+
+ [UnmanagedFunctionPointer(CallingConvention.StdCall)]
+ private delegate int SetPersistedDefaultAudioEndpointDelegate(IntPtr @this, int processId, int flow, int role, IntPtr deviceId);
+
+ public static uint SetPersistedDefaultAudioEndpoint(int processId, int flow, int role, string deviceId)
+ {
+ IntPtr classId = IntPtr.Zero;
+ IntPtr factory = IntPtr.Zero;
+ IntPtr deviceIdHString = IntPtr.Zero;
+
+ try
+ {
+ string activatePath = ToAudioRenderActivatePath(deviceId);
+
+ Debug.WriteLine($"[AUDIO ROUTING] 원본 Device ID: {deviceId}");
+ Debug.WriteLine($"[AUDIO ROUTING] 변환 Device ID: {activatePath}");
+
+ int hr = WindowsCreateString(
+ ClassName,
+ (uint)ClassName.Length,
+ out classId);
+
+ if (hr < 0)
+ return unchecked((uint)hr);
+
+ Guid iid = IID;
+
+ hr = RoGetActivationFactory(
+ classId,
+ ref iid,
+ out factory);
+
+ if (hr < 0)
+ return unchecked((uint)hr);
+
+ if (factory == IntPtr.Zero)
+ return 0x80004003;
+
+ IntPtr vtable = Marshal.ReadIntPtr(factory);
+
+ // IUnknown 3개 + IInspectable 3개 + 19개 메서드 = SetPersistedDefaultAudioEndpoint slot 25
+ IntPtr methodPtr = Marshal.ReadIntPtr(
+ vtable,
+ 25 * IntPtr.Size);
+
+ var setEndpoint =
+ Marshal.GetDelegateForFunctionPointer<SetPersistedDefaultAudioEndpointDelegate>(
+ methodPtr);
+
+ hr = WindowsCreateString(
+ activatePath,
+ (uint)activatePath.Length,
+ out deviceIdHString);
+
+ if (hr < 0)
+ return unchecked((uint)hr);
+
+ return unchecked((uint)setEndpoint(
+ factory,
+ processId,
+ flow,
+ role,
+ deviceIdHString));
+ }
+ catch (Exception ex)
+ {
+ Debug.WriteLine($"[AudioPolicyConfigNative ERROR] {ex}");
+ return 0x80004005;
+ }
+ finally
+ {
+ if (deviceIdHString != IntPtr.Zero)
+ WindowsDeleteString(deviceIdHString);
+
+ if (factory != IntPtr.Zero)
+ Marshal.Release(factory);
+
+ if (classId != IntPtr.Zero)
+ WindowsDeleteString(classId);
+ }
+ }
+
+ private static string ToAudioRenderActivatePath(string deviceId)
+ {
+ if (string.IsNullOrWhiteSpace(deviceId))
+ return deviceId;
+
+ // 이미 activate-path 형식이면 그대로 사용
+ if (deviceId.StartsWith(MmDevApiPrefix, StringComparison.OrdinalIgnoreCase))
+ return deviceId;
+
+ // MMDevice.ID → Windows audio render activate path
+ return MmDevApiPrefix + deviceId + AudioRenderInterface;
+ }
+ }
+
+ private enum EDataFlow { Render = 0, Capture = 1, All = 2 }
+ private enum ERole { Console = 0, Multimedia = 1, Communications = 2 }
+
+ private const string PlaylistUrl = "https://music.youtube.com/playlist?list=PLDZ8RLmQ_hl6B_n1GBAJi5NFgx7vQDYhI";
+
+ private TaskbarIcon? _trayIcon;
+ private bool _reallyExit = false;
+ private bool _isWebViewReady = false;
+ private readonly string _userDataFolder;
+ private bool _isPruned = false;
+ private bool _isInitialSetup = false;
+
+ private static readonly string[] BlockedPatterns =
+ {
+ "doubleclick.net", "googlesyndication.com", "google-analytics.com",
+ "googletagmanager.com", "googleadservices.com", "/pagead/", "/ptracking",
+ "/api/stats/ads", "/get_midroll_", "ad_break", "youtubei/v1/log_event"
+ };
+
+ private const string PruneScript = @"
+(function () {
+ function findDeep(root, selector) {
+ if (!root) return null;
+ try {
+ const direct = root.querySelector ? root.querySelector(selector) : null;
+ if (direct) return direct;
+ } catch (e) {}
+ let all = [];
+ try { all = root.querySelectorAll ? root.querySelectorAll('*') : []; }
+ catch (e) { return null; }
+ for (const el of all) {
+ try {
+ if (el.shadowRoot) {
+ const found = findDeep(el.shadowRoot, selector);
+ if (found) return found;
+ }
+ } catch (e) {}
+ }
+ return null;
+ }
+
+ function isScriptElement(node) {
+ return node && node.nodeType === Node.ELEMENT_NODE && node.tagName &&
+ node.tagName.toLowerCase() === 'script';
+ }
+
+ function isAncestor(node, target) {
+ if (!node || !target) return false;
+ let current = target.parentNode;
+ while (current) {
+ if (current === node) return true;
+ current = current.parentNode;
+ }
+ return false;
+ }
+
+ function cleanChildren(parent, playerBar) {
+ if (!parent) return;
+ const children = Array.from(parent.childNodes);
+
+ for (const node of children) {
+ if (isScriptElement(node)) continue;
+ if (node === playerBar) continue;
+
+ if (node.nodeType === Node.ELEMENT_NODE && isAncestor(node, playerBar)) {
+ cleanChildren(node, playerBar);
+ continue;
+ }
+
+ try { node.remove(); } catch (e) {}
+ }
+ }
+
+ function prune() {
+ try {
+ const playerBar = findDeep(document, 'ytmusic-player-bar');
+ if (!playerBar) {
+ console.warn('[PLAYER ONLY] ytmusic-player-bar not found');
+ return false;
+ }
+
+ cleanChildren(document.body, playerBar);
+ console.log('[PLAYER ONLY] DOM prune complete');
+ return true;
+ } catch (e) {
+ console.error('[PLAYER ONLY] prune error', e);
+ return false;
+ }
+ }
+
+ return prune();
+})();
+";
+
+ public MainWindow()
+ {
+ InitializeComponent();
+ _userDataFolder = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "WebView2Profile");
+ Closing += MainWindow_Closing;
+ InitializeTrayIcon();
+ _ = InitializeWebViewAsync();
+ }
+
+ private async System.Threading.Tasks.Task AutoResumeAfterPruneAsync()
+ {
+ if (!_isWebViewReady) return;
+
+ const string script = @"
+(function () {
+ const buttons = document.querySelectorAll('ytmusic-player-bar button[aria-label=""재생""]');
+ for (const button of buttons) {
+ if (button.offsetParent !== null) {
+ button.click();
+ return true;
+ }
+ }
+ return false;
+})();
+";
+
+ for (int i = 0; i < 10; i++)
+ {
+ try
+ {
+ string result = await WebView.CoreWebView2.ExecuteScriptAsync(script);
+ if (result.Trim().Equals("true", StringComparison.OrdinalIgnoreCase)) return;
+ }
+ catch { }
+
+ await System.Threading.Tasks.Task.Delay(300);
+ }
+ }
+
+ private async System.Threading.Tasks.Task AutoClickPlayButtonAsync()
+ {
+ if (!_isWebViewReady) return;
+
+ const string script = @"
+(function () {
+ const el = document.querySelector('div.content-wrapper.style-scope.ytmusic-play-button-renderer');
+ if (!el) return false;
+ el.click();
+ return true;
+})();
+";
+
+ for (int i = 0; i < 20; i++)
+ {
+ try
+ {
+ string result = await WebView.CoreWebView2.ExecuteScriptAsync(script);
+ if (result.Trim().Equals("true", StringComparison.OrdinalIgnoreCase)) return;
+ }
+ catch { }
+
+ await System.Threading.Tasks.Task.Delay(500);
+ }
+ }
+
+ private async System.Threading.Tasks.Task InitializeWebViewAsync()
+ {
+ try
+ {
+ Directory.CreateDirectory(_userDataFolder);
+
+ var environment = await CoreWebView2Environment.CreateAsync(
+ userDataFolder: _userDataFolder);
+
+ await WebView.EnsureCoreWebView2Async(environment);
+
+ WebView.CoreWebView2.AddWebResourceRequestedFilter(
+ "*", CoreWebView2WebResourceContext.All);
+
+ WebView.CoreWebView2.WebResourceRequested += CoreWebView2_WebResourceRequested;
+ WebView.CoreWebView2.NavigationCompleted += CoreWebView2_NavigationCompleted;
+
+ _isWebViewReady = true;
+ DumpAudioSessions();
+ StatusTextBlock.Text = "재생 준비 중...";
+
+ WebView.CoreWebView2.Navigate(PlaylistUrl);
+ }
+ catch (Exception ex)
+ {
+ StatusTextBlock.Text = "WebView2 초기화 실패";
+
+ MessageBox.Show(
+ $"WebView2를 초기화하지 못했습니다.\n\n{ex.Message}",
+ "오류",
+ MessageBoxButton.OK,
+ MessageBoxImage.Error);
+ }
+ }
+
+ private void TryRouteWebViewAudioToDevice(string targetDeviceName)
+ {
+ try
+ {
+ int? audioPid = null;
+ var enumerator = new MMDeviceEnumerator();
+
+ try
+ {
+ foreach (var device in enumerator.EnumerateAudioEndPoints(DataFlow.Render, DeviceState.Active))
+ {
+ AudioSessionManager? sessionManager = null;
+
+ try
+ {
+ sessionManager = device.AudioSessionManager;
+ var sessions = sessionManager.Sessions;
+
+ for (int i = 0; i < sessions.Count; i++)
+ {
+ var session = sessions[i];
+
+ try
+ {
+ int pid = (int)session.GetProcessID;
+ if (pid <= 0) continue;
+
+ try
+ {
+ using var process = Process.GetProcessById(pid);
+
+ if (string.Equals(process.ProcessName, "msedgewebview2", StringComparison.OrdinalIgnoreCase))
+ {
+ audioPid = pid;
+ Debug.WriteLine($"★ WebView2 오디오 PID 발견: {audioPid}");
+ break;
+ }
+ }
+ catch { }
+ }
+ catch { }
+ }
+ }
+ finally
+ {
+ try { sessionManager?.Dispose(); } catch { }
+ }
+
+ if (audioPid.HasValue) break;
+ }
+ }
+ finally
+ {
+ enumerator.Dispose();
+ }
+
+ if (!audioPid.HasValue)
+ {
+ Debug.WriteLine("현재 WebView2 오디오 세션을 찾지 못함.");
+ return;
+ }
+
+ MMDevice? targetDevice = null;
+ var deviceEnumerator = new MMDeviceEnumerator();
+
+ try
+ {
+ foreach (var device in deviceEnumerator.EnumerateAudioEndPoints(DataFlow.Render, DeviceState.Active))
+ {
+ Debug.WriteLine($"출력장치 확인: {device.FriendlyName} | ID: {device.ID}");
+
+ if (string.Equals(device.FriendlyName, targetDeviceName, StringComparison.OrdinalIgnoreCase))
+ {
+ targetDevice = device;
+ break;
+ }
+ }
+
+ if (targetDevice == null)
+ {
+ Debug.WriteLine($"대상 출력장치를 찾지 못함: {targetDeviceName}");
+ return;
+ }
+
+ Debug.WriteLine($"★ 대상 출력장치: {targetDevice.FriendlyName}");
+ Debug.WriteLine($"★ 대상 장치 ID: {targetDevice.ID}");
+
+ foreach (ERole role in Enum.GetValues(typeof(ERole)))
+ {
+ uint hr = AudioPolicyConfigNative.SetPersistedDefaultAudioEndpoint(
+ audioPid.Value,
+ (int)EDataFlow.Render,
+ (int)role,
+ targetDevice.ID);
+
+ Debug.WriteLine(
+ $"[AUDIO ROUTING] PID={audioPid.Value} | Device={targetDevice.FriendlyName} | Role={role} | HR=0x{hr:X8}");
+ }
+ }
+ finally
+ {
+ deviceEnumerator.Dispose();
+ }
+
+ Debug.WriteLine(
+ $"★ YouTubeMusicPlayer 오디오 라우팅 완료: PID={audioPid.Value} → {targetDeviceName}");
+ }
+ catch (Exception ex)
+ {
+ Debug.WriteLine($"[AUDIO ROUTING ERROR] {ex}");
+ }
+ }
+
+ private void SetWebViewAudioVolume(float volume)
+ {
+ try
+ {
+ using var enumerator = new MMDeviceEnumerator();
+
+ foreach (var device in enumerator.EnumerateAudioEndPoints(DataFlow.Render, DeviceState.Active))
+ {
+ try
+ {
+ var sessions = device.AudioSessionManager.Sessions;
+
+ for (int i = 0; i < sessions.Count; i++)
+ {
+ var session = sessions[i];
+
+ try
+ {
+ int pid = (int)session.GetProcessID;
+ if (pid <= 0) continue;
+
+ using var process = Process.GetProcessById(pid);
+
+ if (string.Equals(process.ProcessName, "msedgewebview2", StringComparison.OrdinalIgnoreCase))
+ {
+ session.SimpleAudioVolume.Volume = Math.Clamp(volume, 0f, 1f);
+ Debug.WriteLine($"★ WebView2 볼륨 설정: {volume * 100:F0}%");
+ return;
+ }
+ }
+ catch { }
+ }
+ }
+ catch { }
+ }
+ }
+ catch (Exception ex)
+ {
+ Debug.WriteLine($"[VOLUME ERROR] {ex}");
+ }
+ }
+
+ private bool IsDescendantOf(int childPid, int parentPid)
+ {
+ try
+ {
+ int currentPid = childPid;
+
+ for (int i = 0; i < 20; i++)
+ {
+ using var process = Process.GetProcessById(currentPid);
+
+ using var searcher = new System.Management.ManagementObjectSearcher(
+ $"SELECT ParentProcessId FROM Win32_Process WHERE ProcessId = {currentPid}");
+
+ foreach (System.Management.ManagementObject obj in searcher.Get())
+ {
+ int nextPid = Convert.ToInt32(obj["ParentProcessId"]);
+
+ if (nextPid == parentPid) return true;
+ if (nextPid == 0 || nextPid == currentPid) return false;
+
+ currentPid = nextPid;
+ }
+
+ return false;
+ }
+ }
+ catch { }
+
+ return false;
+ }
+
+ private void DumpAudioSessions()
+ {
+ try
+ {
+ int myPid = Process.GetCurrentProcess().Id;
+
+ Debug.WriteLine("");
+ Debug.WriteLine("========== AUDIO SESSION DEBUG ==========");
+ Debug.WriteLine($"YouTubeMusicPlayer PID = {myPid}");
+ Debug.WriteLine("");
+
+ using var enumerator = new MMDeviceEnumerator();
+ var devices = enumerator.EnumerateAudioEndPoints(DataFlow.Render, DeviceState.Active);
+
+ Debug.WriteLine("----- 출력 장치 -----");
+
+ foreach (var device in devices)
+ Debug.WriteLine($"DEVICE: {device.FriendlyName} | ID: {device.ID}");
+
+ Debug.WriteLine("");
+ Debug.WriteLine("----- 오디오 세션 -----");
+
+ foreach (var device in devices)
+ {
+ Debug.WriteLine("");
+ Debug.WriteLine($"[{device.FriendlyName}]");
+
+ try
+ {
+ var sessions = device.AudioSessionManager.Sessions;
+
+ for (int i = 0; i < sessions.Count; i++)
+ {
+ var session = sessions[i];
+ int pid = 0;
+
+ try { pid = (int)session.GetProcessID; }
+ catch { }
+
+ string processName = "";
+
+ if (pid > 0)
+ {
+ try { processName = Process.GetProcessById(pid).ProcessName; }
+ catch { }
+ }
+
+ Debug.WriteLine(
+ $"PID={pid} | Process={processName} | State={session.State} | DisplayName={session.DisplayName}");
+ }
+ }
+ catch (Exception ex)
+ {
+ Debug.WriteLine($"세션 조회 실패: {ex.Message}");
+ }
+ }
+
+ Debug.WriteLine("=========================================");
+ Debug.WriteLine("");
+ }
+ catch (Exception ex)
+ {
+ Debug.WriteLine($"AUDIO DEBUG ERROR: {ex}");
+ }
+ }
+
+ private void CoreWebView2_NavigationCompleted(
+ object? sender,
+ CoreWebView2NavigationCompletedEventArgs e)
+ {
+ if (!e.IsSuccess)
+ {
+ StatusTextBlock.Text = "페이지 로드 실패";
+ return;
+ }
+
+ _isPruned = false;
+ ToggleViewButton.Content = "화면 숨기기";
+ StatusTextBlock.Text = "재생 준비 중...";
+
+ _ = insang_ToggleViewButton_click();
+ }
+
+ private void CoreWebView2_WebResourceRequested(
+ object? sender,
+ CoreWebView2WebResourceRequestedEventArgs e)
+ {
+ string url = e.Request.Uri;
+
+ if (url.Contains("accounts.google.com", StringComparison.OrdinalIgnoreCase))
+ return;
+
+ foreach (var pattern in BlockedPatterns)
+ {
+ if (url.Contains(pattern, StringComparison.OrdinalIgnoreCase))
+ {
+ e.Response = WebView.CoreWebView2.Environment.CreateWebResourceResponse(
+ null, 403, "Blocked", "");
+
+ return;
+ }
+ }
+ }
+
+ private void LoadButton_Click(object sender, RoutedEventArgs e)
+ {
+ if (!_isWebViewReady)
+ {
+ StatusTextBlock.Text = "재생 준비 중...";
+ return;
+ }
+
+ _isPruned = false;
+ ToggleViewButton.Content = "화면 숨기기";
+ StatusTextBlock.Text = "재생 준비 중...";
+ WebView.Visibility = Visibility.Visible;
+
+ _ = AutoResumeAfterPruneAsync();
+
+ Height = 600;
+ WebView.CoreWebView2.Navigate(PlaylistUrl);
+ }
+
+ private async System.Threading.Tasks.Task insang_ToggleViewButton_click()
+ {
+ if (!_isWebViewReady) return;
+
+ _isInitialSetup = true;
+
+ StatusTextBlock.Text = "재생 준비 중...";
+
+ Debug.WriteLine("[디버그] 재생 버튼 클릭 스크립트 실행");
+
+ await WebView.CoreWebView2.ExecuteScriptAsync(@"
+ var buttons = document.querySelectorAll(""ytmusic-play-button-renderer[icon='PLAY_ARROW']"");
+ for (var button of buttons) {
+ if (button.getAttribute('id') == null) button.click();
+ }
+ ");
+
+ if (!_isPruned)
+ {
+ StatusTextBlock.Text = "재생 준비 중...";
+
+ string result = await WebView.CoreWebView2.ExecuteScriptAsync(PruneScript);
+ bool success = result.Trim().Equals("true", StringComparison.OrdinalIgnoreCase);
+
+ if (success)
+ {
+ _isPruned = true;
+ ToggleViewButton.Content = "화면 표시";
+ StatusTextBlock.Text = "재생 준비 중...";
+ WebView.Visibility = Visibility.Visible;
+ WebView.IsHitTestVisible = false;
+ Height = 220;
+
+ await System.Threading.Tasks.Task.Delay(1000);
+
+ ShuffleButton_Click(PauseButton, null);
+ PauseButton_Click(PauseButton, null);
+ }
+ else
+ {
+ _isInitialSetup = false;
+ StatusTextBlock.Text = "플레이어를 찾지 못했습니다.";
+ }
+
+ return;
+ }
+
+ _isInitialSetup = false;
+ StatusTextBlock.Text = "화면 복원 중...";
+ _isPruned = false;
+ ToggleViewButton.Content = "화면 숨기기";
+ WebView.Visibility = Visibility.Visible;
+ WebView.IsHitTestVisible = true;
+ Height = 600;
+
+ WebView.CoreWebView2.Navigate(PlaylistUrl);
+ }
+
+ private async void ToggleViewButton_Click(object sender, RoutedEventArgs e)
+ {
+ if (!_isWebViewReady) return;
+
+ await WebView.CoreWebView2.ExecuteScriptAsync(@"
+ var buttons = document.querySelectorAll(""ytmusic-play-button-renderer[icon='PLAY_ARROW']"");
+ for (var button of buttons) {
+ if (button.getAttribute('id') == null) button.click();
+ }
+");
+
+ await WebView.ExecuteScriptAsync(@"
+ var buttons = document.querySelectorAll('button[aria-label=""일시중지""]');
+ for (const button of buttons) {
+ if (button.offsetParent !== null) {
+ button.click();
+ break;
+ }
+ }
+");
+
+ if (!_isPruned)
+ {
+ StatusTextBlock.Text = "재생 준비 중...";
+
+ string result = await WebView.CoreWebView2.ExecuteScriptAsync(PruneScript);
+ bool success = result.Trim().Equals("true", StringComparison.OrdinalIgnoreCase);
+
+ if (success)
+ {
+ _isPruned = true;
+ ToggleViewButton.Content = "화면 표시";
+ StatusTextBlock.Text = "플레이어만 표시 중";
+ WebView.Visibility = Visibility.Visible;
+ Height = 220;
+ }
+ else
+ {
+ StatusTextBlock.Text = "플레이어를 찾지 못했습니다.";
+ }
+
+ return;
+ }
+
+ StatusTextBlock.Text = "화면 복원 중...";
+ _isPruned = false;
+ ToggleViewButton.Content = "화면 숨기기";
+ WebView.Visibility = Visibility.Visible;
+ Height = 600;
+
+ WebView.CoreWebView2.Navigate(PlaylistUrl);
+ }
+
+ private void DevToolsButton_Click(object sender, RoutedEventArgs e)
+ {
+ if (!_isWebViewReady) return;
+ WebView.CoreWebView2.OpenDevToolsWindow();
+ }
+
+ private void InitializeTrayIcon()
+ {
+ _trayIcon = new TaskbarIcon
+ {
+ ToolTipText = "YouTube Music Player",
+ Icon = new System.Drawing.Icon("YouTubeMusicPlayer.ico")
+ };
+
+ var contextMenu = new ContextMenu();
+
+ var showItem = new MenuItem { Header = "창 표시" };
+
+ showItem.Click += (s, e) =>
+ {
+ Show();
+ WindowState = WindowState.Normal;
+ Activate();
+ Topmost = true;
+ Topmost = false;
+ };
+
+ var separator = new Separator();
+ var exitItem = new MenuItem { Header = "종료" };
+
+ exitItem.Click += (s, e) =>
+ {
+ _reallyExit = true;
+ Close();
+ };
+
+ contextMenu.Items.Add(showItem);
+ contextMenu.Items.Add(separator);
+ contextMenu.Items.Add(exitItem);
+
+ _trayIcon.ContextMenu = contextMenu;
+
+ _trayIcon.TrayLeftMouseDown += (s, e) =>
+ {
+ Show();
+ WindowState = WindowState.Normal;
+ Activate();
+ Topmost = true;
+ Topmost = false;
+ };
+ }
+
+ private async void PlayButton_Click(object sender = null, RoutedEventArgs e = null)
+ {
+ await WebView.ExecuteScriptAsync(@"
+var buttons = document.querySelectorAll('button[aria-label=""재생""]');
+for (const button of buttons) {
+ if (button.offsetParent !== null) {
+ button.click();
+ break;
+ }
+}
+");
+
+ StatusTextBlock.Inlines.Clear();
+ StatusTextBlock.Inlines.Add(new System.Windows.Documents.Run("재생 중 "));
+ StatusTextBlock.Inlines.Add(new System.Windows.Documents.Run("오디오 출력 확인 및 자동설정 중... (완료 후 볼륨조절 가능)")
+ {
+ Foreground = System.Windows.Media.Brushes.Red,
+ FontWeight = FontWeights.Bold
+ });
+
+ _ = System.Threading.Tasks.Task.Run(() =>
+ {
+ DumpAudioSessions();
+ TryRouteWebViewAudioToDevice("헤드폰(BZ-HMR)");
+
+ Dispatcher.Invoke(() =>
+ {
+ SetWebViewAudioVolume((float)(VolumeSlider.Value / 100.0));
+
+ StatusTextBlock.Text = "재생 중";
+ VolumeSlider.IsEnabled = true;
+ VolumeTextBox.IsEnabled = true;
+ VolumeApplyButton.IsEnabled = true;
+ WebView.Margin = new Thickness(0, 0, 0, 0);
+ this.Height = 200;
+ });
+ });
+ }
+
+ private async void PauseButton_Click(object sender = null, RoutedEventArgs e = null)
+ {
+ await WebView.ExecuteScriptAsync(@"
+ var buttons = document.querySelectorAll('button[aria-label=""일시중지""]');
+ for (const button of buttons) {
+ if (button.offsetParent !== null) {
+ button.click();
+ break;
+ }
+ }
+");
+
+ Debug.WriteLine("일시중지");
+
+ if (_isInitialSetup)
+ {
+ _isInitialSetup = false;
+ StatusTextBlock.Text = "재생 준비 완료";
+ PlayButton.IsEnabled = true;
+ PauseButton.IsEnabled = true;
+ }
+ else
+ {
+ StatusTextBlock.Text = "일시중지 중";
+ }
+ WebView.Margin = new Thickness(0, 0, 0, -415);
+ this.Height = 530;
+ }
+
+ private async void ShuffleButton_Click(object sender = null, RoutedEventArgs e = null)
+ {
+ await WebView.ExecuteScriptAsync(@"
+ var buttons = document.querySelectorAll('button[aria-label=""셔플""]');
+ for (const button of buttons) {
+ if (button.offsetParent !== null) {
+ button.click();
+ break;
+ }
+ }
+");
+
+ Debug.WriteLine("셔플");
+ }
+
+ private void VolumeSlider_ValueChanged(object sender, RoutedPropertyChangedEventArgs<double> e)
+ {
+ if (VolumeTextBox != null)
+ VolumeTextBox.Text = ((int)VolumeSlider.Value).ToString();
+ }
+
+ private void VolumeApplyButton_Click(object sender, RoutedEventArgs e)
+ {
+ if (!int.TryParse(VolumeTextBox.Text, out int volume))
+ return;
+
+ volume = Math.Clamp(volume, 0, 100);
+
+ VolumeSlider.Value = volume;
+ VolumeTextBox.Text = volume.ToString();
+
+ SetWebViewAudioVolume(volume / 100f);
+ }
+
+ private void MainWindow_Closing(object? sender, System.ComponentModel.CancelEventArgs e)
+ {
+ if (!_reallyExit)
+ {
+ e.Cancel = true;
+ Hide();
+ return;
+ }
+
+ _trayIcon?.Dispose();
+ }
+ }
+}
\ No newline at end of file