releases/VideoDownloader.App/Browser/PlayerWindow.cs

167 lines
7.5 KiB
C#

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;
/// <summary>
/// T2-10 미디어 재생목록 플레이어: Direct(MP4)/HLS(hls.js)/DASH(dash.js) 를
/// 생성된 HTML 플레이어로 재생. 곡 종료 시 다음 트랙 자동 재생(연속 재생).
/// </summary>
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 = "재생목록 — Video Downloader";
Width = 900; Height = 620;
WindowStartupLocation = WindowStartupLocation.CenterOwner;
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 { }
}
/// <summary>트랙 재생 HTML — HLS 는 hls.js, DASH 는 dash.js 번들 인라인.</summary>
public static string BuildPlayerHtml(MediaTrack track)
{
var url = track.Url.Replace("&", "&amp;").Replace("\"", "&quot;").Replace("<", "&lt;");
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 = "<!DOCTYPE html><html><head><meta charset='utf-8'><style>" +
"body{background:#000;margin:0;overflow:hidden}video{width:100vw;height:100vh}" +
"</style></head><body><video id='v' controls autoplay playsinline></video>" +
"<script>var SRC=" + System.Text.Json.JsonSerializer.Serialize(track.Url) + ";" +
(hls.Length > 0 ? "<script>" + hls + "</script>" : "") +
(dash.Length > 0 ? "<script>" + dash + "</script>" : "") +
"<script>(function(){var v=document.getElementById('v');" + loader +
"v.addEventListener('ended',function(){try{window.chrome.webview.postMessage({vdEnded:true});}catch(e){}});" +
"v.addEventListener('error',function(){try{window.chrome.webview.postMessage({vdEnded:true});}catch(e){}});})();" +
"</script></body></html>";
return html;
}
private static string ReadResource(string name)
{
var stream = Assembly.GetExecutingAssembly().GetManifestResourceStream(name);
return stream == null ? "" : new StreamReader(stream).ReadToEnd();
}
}