using System.IO; using System.Reflection; using System.Windows; using System.Windows.Controls; using System.Windows.Interop; using Microsoft.Web.WebView2.Core; using VideoDownloader.App.Browser.Features; using VideoDownloader.Core.Config; using Wv2 = Microsoft.Web.WebView2.Wpf; namespace VideoDownloader.App.Browser; /// /// T2-10 미디어 재생목록 플레이어: Direct(MP4)/HLS(hls.js)/DASH(dash.js) 를 /// 생성된 HTML 플레이어로 재생. 곡 종료 시 다음 트랙 자동 재생(연속 재생). /// public sealed class PlayerWindow : Window { private readonly CoreWebView2Environment _env; private readonly AppConfig _config; private readonly Wv2.WebView2 _web = new() { DefaultBackgroundColor = System.Drawing.Color.Black }; private MediaPlaylist _playlist; private readonly TextBlock _title; public PlayerWindow(CoreWebView2Environment env, AppConfig config, MediaPlaylist playlist) { _env = env; _config = config; _playlist = playlist; Title = "재생목록 - MediaBrowser"; Width = 900; Height = 620; WindowStartupLocation = WindowStartupLocation.CenterOwner; SetResourceReference(FontFamilyProperty, "UiFont"); // 암시 스타일 폰트 강제 제거 후 창 루트 상속으로 통일 var bar = new Border { Background = System.Windows.Media.Brushes.Black, Padding = new Thickness(8, 6, 8, 6) }; var dock = new DockPanel(); var prev = MkBtn("⏮ 이전", () => { _playlist.Previous(); PlayCurrent(); }); var next = MkBtn("⏭ 다음", () => { _playlist.Next(); PlayCurrent(); }); var close = MkBtn("✕ 닫기", () => Close()); _title = new TextBlock { Foreground = System.Windows.Media.Brushes.LightGray, FontSize = 12, VerticalAlignment = VerticalAlignment.Center, TextTrimming = TextTrimming.CharacterEllipsis, Margin = new Thickness(8, 0, 8, 0) }; DockPanel.SetDock(close, Dock.Right); DockPanel.SetDock(next, Dock.Right); DockPanel.SetDock(prev, Dock.Left); dock.Children.Add(close); dock.Children.Add(next); dock.Children.Add(prev); dock.Children.Add(_title); bar.Child = dock; var grid = new Grid { RowDefinitions = { new RowDefinition { Height = GridLength.Auto }, new RowDefinition { Height = new GridLength(1, GridUnitType.Star) } } }; Grid.SetRow(bar, 0); Grid.SetRow(_web, 1); grid.Children.Add(bar); grid.Children.Add(_web); Content = grid; Loaded += async (_, _) => { try { var options = _env.CreateCoreWebView2ControllerOptions(); if (_config.PrivateMode) options.IsInPrivateModeEnabled = true; await _web.EnsureCoreWebView2Async(_env, options); var cv = _web.CoreWebView2!; cv.WebMessageReceived += (_, we) => { try { var msg = we.WebMessageAsJson; if (string.IsNullOrEmpty(msg)) return; using var doc = System.Text.Json.JsonDocument.Parse(msg); if (doc.RootElement.TryGetProperty("vdEnded", out var e) && e.GetBoolean()) { Dispatcher.BeginInvoke(new Action(() => { var cur = _playlist.Current; if (cur != null) _playlist.MarkPlayed(cur); // UC-M12 반복 모드: one=현 트랙 재생, all=끝에서 처음으로 순환, none=끝에서 정지 var mode = _config.PlaylistLoopMode; if (mode == "one") { PlayCurrent(); return; } var atEnd = _playlist.Tracks.Count > 0 && _playlist.CurrentIndex >= _playlist.Tracks.Count - 1; if (atEnd && mode != "all") return; _playlist.Next(); PlayCurrent(); })); } } catch { } }; PlayCurrent(); } catch { } }; } protected override void OnSourceInitialized(EventArgs e) { base.OnSourceInitialized(e); try { var hwnd = new WindowInteropHelper(this).Handle; int dark = 1; DwmSetWindowAttribute(hwnd, 20, ref dark, sizeof(int)); } catch { } } [System.Runtime.InteropServices.DllImport("dwmapi.dll")] private static extern int DwmSetWindowAttribute(IntPtr hwnd, int attribute, ref int pvAttribute, int cbAttribute); private Button MkBtn(string text, Action onClick) { var b = new Button { Content = text, Padding = new Thickness(10, 3, 10, 3), Margin = new Thickness(4, 0, 4, 0), Background = System.Windows.Media.Brushes.Transparent, Foreground = System.Windows.Media.Brushes.White, BorderBrush = System.Windows.Media.Brushes.Gray }; b.Click += (_, _) => onClick(); return b; } public void PlayCurrent() { var track = _playlist.Current; if (track == null) { _title.Text = "재생목록이 비었습니다"; return; } _title.Text = $"{_playlist.CurrentIndex + 1}/{_playlist.Tracks.Count} · {track.Title} [{track.Kind}]"; try { _web.CoreWebView2?.NavigateToString(BuildPlayerHtml(track)); } catch { } } /// 트랙 재생 HTML — HLS 는 hls.js, DASH 는 dash.js 번들 인라인. public static string BuildPlayerHtml(MediaTrack track) { var url = track.Url.Replace("&", "&").Replace("\"", """).Replace("<", "<"); var isHls = track.Kind == "Hls" || track.Url.Contains(".m3u8", StringComparison.OrdinalIgnoreCase); var isDash = track.Kind == "Dash" || track.Url.Contains(".mpd", StringComparison.OrdinalIgnoreCase); var hls = isHls ? ReadResource("VideoDownloader.App.Assets.hls.min.js") : ""; var dash = isDash ? ReadResource("VideoDownloader.App.Assets.dash.all.min.js") : ""; var loader = ""; if (isHls) loader = "if (Hls.isSupported()) { var h = new Hls(); h.loadSource(SRC); h.attachMedia(v); } else v.src = SRC;"; else if (isDash) loader = "var p = dashjs.MediaPlayer().create(); p.initialize(v, SRC, true);"; else loader = "v.src = SRC;"; var html = "" + "" : "") + (dash.Length > 0 ? "" : "") + ""; return html; } private static string ReadResource(string name) { var stream = Assembly.GetExecutingAssembly().GetManifestResourceStream(name); return stream == null ? "" : new StreamReader(stream).ReadToEnd(); } }