Files
mpv.net/mpv.net/MainForm.cs
Frank Skare c18c70c2af -
2019-04-16 18:14:22 +02:00

631 lines
25 KiB
C#

using System;
using System.Drawing;
using System.IO;
using System.Runtime.InteropServices;
using System.Threading;
using System.Windows.Forms;
using System.Linq;
using System.Collections.Generic;
using VBNET;
using System.Threading.Tasks;
using System.ComponentModel;
namespace mpvnet
{
public partial class MainForm : Form
{
public static MainForm Instance { get; set; }
public static IntPtr Hwnd;
private Point LastCursorPosChanged;
private int LastCursorChangedTickCount;
private bool IgnoreDpiChanged = true;
private MenuItemEx TracksMenu;
private List<MediaTrack> MediaTracks = new List<MediaTrack>();
public new ContextMenuStripEx ContextMenu;
private float MpvAutofit = 0.50f;
private bool MpvFullscreen;
private int MpvScreen = -1;
private string MpvNetDarkMode = "system";
private string MpvSid = "";
private string MpvAid = "";
private string MpvVid = "";
public MainForm()
{
InitializeComponent();
try
{
AppDomain.CurrentDomain.UnhandledException += CurrentDomain_UnhandledException;
Application.ThreadException += Application_ThreadException;
Msg.SupportURL = "https://github.com/stax76/mpv.net#support";
Instance = this;
Hwnd = Handle;
MinimumSize = new Size(FontHeight * 16, FontHeight * 9);
Text += " " + Application.ProductVersion;
foreach (var i in mp.mpvConf)
ProcessMpvProperty(i.Key, i.Value);
foreach (var i in mp.mpvNetConf)
ProcessMpvNetProperty(i.Key, i.Value);
ProcessCommandLineEarly();
if (MpvScreen == -1) MpvScreen = Array.IndexOf(Screen.AllScreens, Screen.PrimaryScreen);
SetScreen(MpvScreen);
ChangeFullscreen(MpvFullscreen);
}
catch (Exception ex)
{
Msg.ShowException(ex);
}
}
private void ContextMenu_Opening(object sender, CancelEventArgs e)
{
lock (MediaTracks)
{
TracksMenu.DropDownItems.Clear();
MediaTrack[] audTracks = MediaTracks.Where(track => track.Type == "a").ToArray();
MediaTrack[] subTracks = MediaTracks.Where(track => track.Type == "s").ToArray();
MediaTrack[] vidTracks = MediaTracks.Where(track => track.Type == "v").ToArray();
foreach (MediaTrack track in vidTracks)
{
var mi = ContextMenu.Add("Track > " + track.Text);
mi.Action = () => { mp.commandv("set", "vid", track.ID.ToString()); };
mi.Checked = MpvVid == track.ID.ToString();
}
if (vidTracks.Length > 0)
ContextMenu.Add("Track > -");
foreach (MediaTrack track in audTracks)
{
var mi = ContextMenu.Add("Track > " + track.Text);
mi.Action = () => { mp.commandv("set", "aid", track.ID.ToString()); };
mi.Checked = MpvAid == track.ID.ToString();
}
if (subTracks.Length > 0)
ContextMenu.Add("Track > -");
foreach (MediaTrack track in subTracks)
{
var mi = ContextMenu.Add("Track > " + track.Text);
mi.Action = () => { mp.commandv("set", "sid", track.ID.ToString()); };
mi.Checked = MpvSid == track.ID.ToString();
}
if (subTracks.Length > 0)
{
var mi = ContextMenu.Add("Track > S: No subtitles");
mi.Action = () => { mp.commandv("set", "sid", "no"); };
mi.Checked = MpvSid == "no";
}
}
}
protected void SetScreen(int targetIndex)
{
Screen[] screens = Screen.AllScreens;
if (targetIndex < 0) targetIndex = 0;
if (targetIndex > screens.Length - 1) targetIndex = screens.Length - 1;
SetScreen(screens[Array.IndexOf(screens, screens[targetIndex])]);
}
protected void SetScreen(Screen screen)
{
Rectangle target = screen.Bounds;
Left = target.X + Convert.ToInt32((target.Width - Width) / 2.0);
Top = target.Y + Convert.ToInt32((target.Height - Height) / 2.0);
SetStartFormPositionAndSize();
}
void SetStartFormPositionAndSize()
{
if (IsFullscreen || mp.VideoSize.Width == 0) return;
Screen screen = Screen.FromControl(this);
int height = Convert.ToInt32(screen.Bounds.Height * MpvAutofit);
int width = Convert.ToInt32(height * mp.VideoSize.Width / (double)mp.VideoSize.Height);
Point middlePos = new Point(Left + Width / 2, Top + Height / 2);
var rect = new Native.RECT(new Rectangle(screen.Bounds.X, screen.Bounds.Y, width, height));
NativeHelp.AddWindowBorders(Handle, ref rect);
int left = middlePos.X - rect.Width / 2;
int top = middlePos.Y - rect.Height / 2;
Native.SetWindowPos(Handle, IntPtr.Zero /* HWND_TOP */, left, top, rect.Width, rect.Height, 4 /* SWP_NOZORDER */);
}
void SetFormPositionAndSizeKeepHeight()
{
if (IsFullscreen || mp.VideoSize.Width == 0) return;
Screen screen = Screen.FromControl(this);
int height = ClientSize.Height;
if (height > screen.Bounds.Height * 0.9) height = Convert.ToInt32(screen.Bounds.Height * MpvAutofit);
int width = Convert.ToInt32(height * mp.VideoSize.Width / (double)mp.VideoSize.Height);
Point middlePos = new Point(Left + Width / 2, Top + Height / 2);
var rect = new Native.RECT(new Rectangle(screen.Bounds.X, screen.Bounds.Y, width, height));
NativeHelp.AddWindowBorders(Handle, ref rect);
int left = middlePos.X - rect.Width / 2;
int top = middlePos.Y - rect.Height / 2;
Screen[] screens = Screen.AllScreens;
if (left < screens[0].Bounds.Left) left = screens[0].Bounds.Left;
int maxLeft = screens[0].Bounds.Left + screens.Select((sc) => sc.Bounds.Width).Sum() - rect.Width - SystemInformation.CaptionHeight;
if (left > maxLeft) left = maxLeft;
Native.SetWindowPos(Handle, IntPtr.Zero /* HWND_TOP */, left, top, rect.Width, rect.Height, 4 /* SWP_NOZORDER */);
}
protected void ProcessCommandLineEarly()
{
var args = Environment.GetCommandLineArgs().Skip(1);
foreach (string i in args)
{
if (i.StartsWith("--"))
{
if (i.Contains("="))
{
string left = i.Substring(2, i.IndexOf("=") - 2);
string right = i.Substring(left.Length + 3);
ProcessMpvProperty(left, right);
ProcessMpvNetProperty(left, right);
}
else
{
string switchName = i.Substring(2);
switch (switchName)
{
case "fs":
case "fullscreen":
MpvFullscreen = true;
break;
}
}
}
}
}
void ProcessMpvProperty(string name, string value)
{
switch (name)
{
case "autofit":
if (value.Length == 3 && value.EndsWith("%"))
if (int.TryParse(value.Substring(0, 2), out int result))
MpvAutofit = result / 100f;
break;
case "fs":
case "fullscreen":
MpvFullscreen = value == "yes";
break;
case "screen":
MpvScreen = Convert.ToInt32(value);
break;
}
}
void ProcessMpvNetProperty(string name, string value)
{
switch (name)
{
case "dark-mode":
MpvNetDarkMode = value;
break;
}
}
public void BuildMenu()
{
string content = File.ReadAllText(mp.InputConfPath);
List<string> lines = null;
Dictionary<string, string> commandInputDic = new Dictionary<string, string>();
if (content.Contains("#menu:"))
lines = content.Split("\r\n".ToCharArray()).ToList();
else
{
lines = Properties.Resources.input_conf.Split("\r\n".ToCharArray()).ToList();
foreach (string i in content.Split("\r\n".ToCharArray()))
{
string line = i.Trim();
if (line.StartsWith("#") || !line.Contains(" ")) continue;
string input = line.Substring(0, line.IndexOf(" ")).Trim();
string command = line.Substring(line.IndexOf(" ") + 1).Trim();
commandInputDic[command] = input;
}
}
foreach (string i in lines)
{
if (!i.Contains("#menu:")) continue;
string left = i.Substring(0, i.IndexOf("#menu:")).Trim();
if (left.StartsWith("#")) continue;
string command = left.Substring(left.IndexOf(" ") + 1).Trim();
string menu = i.Substring(i.IndexOf("#menu:") + "#menu:".Length).Trim();
string input = left.Substring(0, left.IndexOf(" "));
if (input == "_") input = "";
if (menu.Contains(";")) input = menu.Substring(0, menu.IndexOf(";")).Trim();
string path = menu.Substring(menu.IndexOf(";") + 1).Trim().Replace("&", "&&");
if (path == "" || command == "") continue;
if (commandInputDic.Count > 0)
if (commandInputDic.ContainsKey(command))
input = commandInputDic[command];
else
input = "";
MenuItemEx menuItem = ContextMenu.Add(path, () => {
try {
mp.command_string(command);
}
catch (Exception ex) {
Msg.ShowException(ex);
}
});
if (menuItem != null)
{
menuItem.ShortcutKeyDisplayString = input.Replace("_", "") + " ";
if (TracksMenu == null && menuItem.Text.StartsWith("Track ") &&
menuItem.Text.Trim() == "Track")
TracksMenu = menuItem;
}
}
}
private void ContextMenu_Opened(object sender, EventArgs e) => CursorHelp.Show();
private string LastHistory;
private void mp_PlaybackRestart()
{
string filePath = mp.get_property_string("path");
BeginInvoke(new Action(() => { Text = Path.GetFileName(filePath) + " - mpv.net " + Application.ProductVersion; }));
Task.Run(new Action(() => {
string historyFilepath = mp.mpvConfFolderPath + "history.txt";
if (LastHistory != filePath && File.Exists(historyFilepath))
{
File.AppendAllText(historyFilepath, DateTime.Now.ToString() + " " +
Path.GetFileNameWithoutExtension(filePath) + "\r\n");
LastHistory = filePath;
}
lock (MediaTracks)
{
MediaTracks.Clear();
using (MediaInfo mi = new MediaInfo(filePath))
{
int count = mi.GetCount(MediaInfoStreamKind.Video);
for (int i = 0; i < count; i++)
{
MediaTrack track = new MediaTrack();
Add(track, mi.GetVideo(i, "Format"));
Add(track, mi.GetVideo(i, "Format_Profile"));
Add(track, mi.GetVideo(i, "Width") + "x" + mi.GetVideo(i, "Height"));
Add(track, mi.GetVideo(i, "FrameRate") + " FPS");
Add(track, mi.GetVideo(i, "Language/String"));
Add(track, mi.GetVideo(i, "Forced") == "Yes" ? "Forced" : "");
Add(track, mi.GetVideo(i, "Default") == "Yes" ? "Default" : "");
Add(track, mi.GetVideo(i, "Title"));
track.Text = "V: " + track.Text.Trim(" ,".ToCharArray());
track.Type = "v";
track.ID = i + 1;
MediaTracks.Add(track);
}
count = mi.GetCount(MediaInfoStreamKind.Audio);
for (int i = 0; i < count; i++)
{
MediaTrack track = new MediaTrack();
Add(track, mi.GetAudio(i, "Language/String"));
Add(track, mi.GetAudio(i, "Format"));
Add(track, mi.GetAudio(i, "Format_Profile"));
Add(track, mi.GetAudio(i, "BitRate/String"));
Add(track, mi.GetAudio(i, "Channel(s)/String"));
Add(track, mi.GetAudio(i, "SamplingRate/String"));
Add(track, mi.GetAudio(i, "Forced") == "Yes" ? "Forced" : "");
Add(track, mi.GetAudio(i, "Default") == "Yes" ? "Default" : "");
Add(track, mi.GetAudio(i, "Title"));
track.Text = "A: " + track.Text.Trim(" ,".ToCharArray());
track.Type = "a";
track.ID = i + 1;
MediaTracks.Add(track);
}
count = mi.GetCount(MediaInfoStreamKind.Text);
for (int i = 0; i < count; i++)
{
MediaTrack track = new MediaTrack();
Add(track, mi.GetText(i, "Language/String"));
Add(track, mi.GetText(i, "Format"));
Add(track, mi.GetText(i, "Format_Profile"));
Add(track, mi.GetText(i, "Forced") == "Yes" ? "Forced" : "");
Add(track, mi.GetText(i, "Default") == "Yes" ? "Default" : "");
Add(track, mi.GetText(i, "Title"));
track.Text = "S: " + track.Text.Trim(" ,".ToCharArray());
track.Type = "s";
track.ID = i + 1;
MediaTracks.Add(track);
}
void Add(MediaTrack track, string val)
{
if (!string.IsNullOrEmpty(val) && !(track.Text != null && track.Text.Contains(val)))
track.Text += " " + val + ",";
}
}
}
}));
}
class MediaTrack
{
public string Text { get; set; }
public string Type { get; set; }
public int ID { get; set; }
}
private void Mp_Idle()
{
BeginInvoke(new Action(() => { Text = "mpv.net " + Application.ProductVersion; }));
}
private void CM_Popup(object sender, EventArgs e) => CursorHelp.Show();
private void Application_ThreadException(object sender, ThreadExceptionEventArgs e)
{
Msg.ShowException(e.Exception);
}
private void CurrentDomain_UnhandledException(object sender, UnhandledExceptionEventArgs e)
{
Msg.ShowError(e.ExceptionObject.ToString());
}
private void mp_VideoSizeChanged()
{
BeginInvoke(new Action(() => SetFormPositionAndSizeKeepHeight()));
}
private void mp_Shutdown()
{
BeginInvoke(new Action(() => Close()));
}
public bool IsFullscreen => WindowState == FormWindowState.Maximized;
void mpPropChangeFullscreen(bool value)
{
BeginInvoke(new Action(() => ChangeFullscreen(value)));
}
void ChangeFullscreen(bool value)
{
if (value)
{
if (FormBorderStyle != FormBorderStyle.None)
{
FormBorderStyle = FormBorderStyle.None;
WindowState = FormWindowState.Maximized;
}
}
else
{
WindowState = FormWindowState.Normal;
FormBorderStyle = FormBorderStyle.Sizable;
SetFormPositionAndSizeKeepHeight();
}
}
protected override void WndProc(ref Message m)
{
switch (m.Msg)
{
case 0x0201: // WM_LBUTTONDOWN
case 0x0202: // WM_LBUTTONUP
case 0x0100: // WM_KEYDOWN
case 0x0101: // WM_KEYUP
case 0x020A: // WM_MOUSEWHEEL
if (mp.MpvWindowHandle != IntPtr.Zero)
Native.SendMessage(mp.MpvWindowHandle, m.Msg, m.WParam, m.LParam);
break;
case 0x319: // WM_APPCOMMAND
if (mp.MpvWindowHandle != IntPtr.Zero)
Native.PostMessage(mp.MpvWindowHandle, m.Msg, m.WParam, m.LParam);
break;
case 0x0104: // WM_SYSKEYDOWN:
if (mp.MpvWindowHandle != IntPtr.Zero)
Native.SendMessage(mp.MpvWindowHandle, m.Msg, m.WParam, m.LParam);
break;
case 0x0105: // WM_SYSKEYUP:
if (mp.MpvWindowHandle != IntPtr.Zero)
Native.SendMessage(mp.MpvWindowHandle, m.Msg, m.WParam, m.LParam);
break;
case 0x203: // Native.WM.LBUTTONDBLCLK
if (!IsMouseInOSC())
mp.command_string("cycle fullscreen");
break;
case 0x02E0: // WM_DPICHANGED
if (IgnoreDpiChanged) break;
var r2 = Marshal.PtrToStructure<Native.RECT>(m.LParam);
Native.SetWindowPos(Handle, IntPtr.Zero, r2.Left, r2.Top, r2.Width, r2.Height, 0);
break;
case 0x0214: // WM_SIZING
var rc = Marshal.PtrToStructure<Native.RECT>(m.LParam);
var r = rc;
NativeHelp.SubtractWindowBorders(Handle, ref r);
int c_w = r.Right - r.Left, c_h = r.Bottom - r.Top;
float aspect = mp.VideoSize.Width / (float)mp.VideoSize.Height;
int d_w = Convert.ToInt32(c_h * aspect - c_w);
int d_h = Convert.ToInt32(c_w / aspect - c_h);
int[] d_corners = { d_w, d_h, -d_w, -d_h };
int[] corners = { rc.Left, rc.Top, rc.Right, rc.Bottom };
int corner = NativeHelp.GetResizeBorder(m.WParam.ToInt32());
if (corner >= 0)
corners[corner] -= d_corners[corner];
Marshal.StructureToPtr<Native.RECT>(new Native.RECT(corners[0], corners[1], corners[2], corners[3]), m.LParam, false);
m.Result = new IntPtr(1);
return;
}
base.WndProc(ref m);
}
protected override void OnDragEnter(DragEventArgs e)
{
base.OnDragEnter(e);
if (e.Data.GetDataPresent(DataFormats.FileDrop) || e.Data.GetDataPresent(DataFormats.Text))
e.Effect = DragDropEffects.Copy;
}
protected override void OnDragDrop(DragEventArgs e)
{
base.OnDragDrop(e);
if (e.Data.GetDataPresent(DataFormats.FileDrop))
mp.LoadFiles(e.Data.GetData(DataFormats.FileDrop) as String[]);
if (e.Data.GetDataPresent(DataFormats.Text))
mp.LoadURL(e.Data.GetData(DataFormats.Text).ToString());
}
protected override void OnMouseDown(MouseEventArgs e)
{
base.OnMouseDown(e);
if (WindowState == FormWindowState.Normal &&
e.Button == MouseButtons.Left &&
e.Y < ClientSize.Height * 0.9)
{
var HTCAPTION = new IntPtr(2);
Native.ReleaseCapture();
Native.PostMessage(Handle, 0xA1 /* WM_NCLBUTTONDOWN */, HTCAPTION, IntPtr.Zero);
}
if (Width - e.Location.X < 10 && e.Location.Y < 10)
mp.commandv("quit");
}
protected override void OnMouseMove(MouseEventArgs e)
{
base.OnMouseMove(e);
mp.command_string($"mouse {e.X} {e.Y}");
if (CursorHelp.IsPosDifferent(LastCursorPosChanged))
CursorHelp.Show();
}
bool IsMouseInOSC()
{
return PointToClient(Control.MousePosition).Y > ClientSize.Height * 0.9;
}
private void Timer_Tick(object sender, EventArgs e)
{
if (CursorHelp.IsPosDifferent(LastCursorPosChanged))
{
LastCursorPosChanged = Control.MousePosition;
LastCursorChangedTickCount = Environment.TickCount;
}
else if (Environment.TickCount - LastCursorChangedTickCount > 1500 &&
!IsMouseInOSC() && ClientRectangle.Contains(PointToClient(MousePosition)) &&
Form.ActiveForm == this && !ContextMenu.Visible)
{
CursorHelp.Hide();
}
}
public DialogResult ShowMsgBox(string message, MessageBoxIcon icon)
{
var buttons = MessageBoxButtons.OK;
if (icon == MessageBoxIcon.Question) buttons = MessageBoxButtons.OKCancel;
var fn = new Func<DialogResult>(() => MessageBox.Show(message, Application.ProductName, buttons, icon));
return (DialogResult)Invoke(fn);
}
protected override void OnLoad(EventArgs e)
{
base.OnLoad(e);
mp.Init();
mp.observe_property_bool("fullscreen", mpPropChangeFullscreen);
mp.observe_property_bool("ontop", mpPropChangeOnTop);
mp.observe_property_string("sid", mpPropChangeSid);
mp.observe_property_string("aid", mpPropChangeAid);
mp.observe_property_string("vid", mpPropChangeVid);
mp.Shutdown += mp_Shutdown;
mp.VideoSizeChanged += mp_VideoSizeChanged;
mp.PlaybackRestart += mp_PlaybackRestart;
mp.Idle += Mp_Idle;
}
void mpPropChangeOnTop(bool value) => BeginInvoke(new Action(() => TopMost = value));
void mpPropChangeAid(string value) => MpvAid = value;
void mpPropChangeSid(string value) => MpvSid = value;
void mpPropChangeVid(string value) => MpvVid = value;
protected override void OnShown(EventArgs e)
{
base.OnShown(e);
if ((MpvNetDarkMode == "system" && Misc.IsDarkTheme) || MpvNetDarkMode == "always")
ToolStripRendererEx.ColorTheme = Color.Black;
ContextMenu = new ContextMenuStripEx(components);
ContextMenu.Opened += ContextMenu_Opened;
ContextMenu.Opening += ContextMenu_Opening;
BuildMenu();
ContextMenuStrip = ContextMenu;
IgnoreDpiChanged = false;
CheckYouTube();
}
protected override void OnFormClosed(FormClosedEventArgs e)
{
base.OnFormClosed(e);
mp.commandv("quit");
mp.AutoResetEvent.WaitOne(3000);
}
protected override void OnLostFocus(EventArgs e)
{
base.OnLostFocus(e);
CursorHelp.Show();
}
protected override void OnActivated(EventArgs e)
{
base.OnActivated(e);
CheckYouTube();
}
private string LastURL;
void CheckYouTube()
{
string clipboard = Clipboard.GetText();
if (clipboard.StartsWith("https://www.youtube.com/watch?") && LastURL != clipboard && Visible)
{
LastURL = clipboard;
if (Msg.ShowQuestion("Play YouTube URL?") == MsgResult.OK)
mp.LoadURL(clipboard);
}
}
}
}