From 13d870bdd3aff9c0ad6446bc3b206bd2beee666a Mon Sep 17 00:00:00 2001 From: var1ap Date: Fri, 11 Nov 2016 14:45:37 +0400 Subject: [PATCH] MPV support for linux first imlementation. --- .gitignore | 1 + libse/Settings.cs | 2 +- src/Forms/Settings.cs | 5 +- src/Forms/SettingsMpv.Designer.cs | 45 ++- src/Forms/SettingsMpv.cs | 20 +- src/Logic/UiUtil.cs | 2 +- src/Logic/VideoPlayers/LibMpvMono.cs | 431 ++++++++++++++++++++++++ src/Logic/VideoPlayers/libMpvDynamic.cs | 2 +- src/SubtitleEdit.csproj | 1 + 9 files changed, 485 insertions(+), 24 deletions(-) create mode 100644 src/Logic/VideoPlayers/LibMpvMono.cs diff --git a/.gitignore b/.gitignore index 2e8cee5b1..94c120a71 100644 --- a/.gitignore +++ b/.gitignore @@ -32,3 +32,4 @@ SubtitleEdit-*-setup.exe /src/SubtitleEdit.VC.opendb /src/SubtitleEdit.VC.VC.opendb /src/SubtitleEdit.VC.db +/src/SubtitleEdit.userprefs \ No newline at end of file diff --git a/libse/Settings.cs b/libse/Settings.cs index a66eff7c0..fb4fefc77 100644 --- a/libse/Settings.cs +++ b/libse/Settings.cs @@ -668,7 +668,7 @@ namespace Nikse.SubtitleEdit.Core OpenSubtitleExtraExtensions = "*.mp4;*.m4v;*.mkv;*.ts"; // matroska/mp4/m4v files (can contain subtitles) ListViewColumnsRememberSize = true; VlcWaveTranscodeSettings = "acodec=s16l"; // "acodec=s16l,channels=1,ab=64,samplerate=8000"; - MpvVideoOutput = "direct3d_shaders"; // mpv "vo" option + MpvVideoOutput = "vaapi"; // mpv "vo" option UseTimeFormatHHMMSSFF = false; ClearStatusBarAfterSeconds = 10; MoveVideo100Or500MsPlaySmallSample = false; diff --git a/src/Forms/Settings.cs b/src/Forms/Settings.cs index 66b2cc2d8..6cc93010a 100644 --- a/src/Forms/Settings.cs +++ b/src/Forms/Settings.cs @@ -2327,7 +2327,10 @@ namespace Nikse.SubtitleEdit.Forms private void RefreshMpvSettings() { radioButtonVideoPlayerMPV.Enabled = LibMpvDynamic.IsInstalled; - labelMpvSettings.Text = "--vo=" + Configuration.Settings.General.MpvVideoOutput; + if (!Configuration.IsRunningOnLinux()) + labelMpvSettings.Text = "--vo=" + Configuration.Settings.General.MpvVideoOutput; + else + labelMpvSettings.Text = "--vo=vaapi"; } private void linkLabelBingSubscribe_LinkClicked(object sender, LinkLabelLinkClickedEventArgs e) diff --git a/src/Forms/SettingsMpv.Designer.cs b/src/Forms/SettingsMpv.Designer.cs index 77d94fa48..28b1bc6ce 100644 --- a/src/Forms/SettingsMpv.Designer.cs +++ b/src/Forms/SettingsMpv.Designer.cs @@ -1,5 +1,7 @@ namespace Nikse.SubtitleEdit.Forms { + using Nikse.SubtitleEdit.Core; + sealed partial class SettingsMpv { /// @@ -38,23 +40,37 @@ // // buttonDownload // - this.buttonDownload.Location = new System.Drawing.Point(12, 24); - this.buttonDownload.Name = "buttonDownload"; - this.buttonDownload.Size = new System.Drawing.Size(186, 23); - this.buttonDownload.TabIndex = 0; - this.buttonDownload.Text = "Download mpv dll"; - this.buttonDownload.UseVisualStyleBackColor = true; - this.buttonDownload.Click += new System.EventHandler(this.buttonDownload_Click_1); + if (!Configuration.IsRunningOnLinux()) + { + this.buttonDownload.Location = new System.Drawing.Point(12, 24); + this.buttonDownload.Name = "buttonDownload"; + this.buttonDownload.Size = new System.Drawing.Size(186, 23); + this.buttonDownload.TabIndex = 0; + this.buttonDownload.Text = "Download mpv dll"; + this.buttonDownload.UseVisualStyleBackColor = true; + this.buttonDownload.Click += new System.EventHandler(this.buttonDownload_Click_1); + } // // comboBoxVideoOutput // this.comboBoxVideoOutput.FormattingEnabled = true; - this.comboBoxVideoOutput.Items.AddRange(new object[] { - "direct3d_shaders", - "direct3d ", - "sdl", - "vaapi", - "vdpau"}); + if (Configuration.IsRunningOnLinux()) + { + this.comboBoxVideoOutput.Items.AddRange(new object[] { + "sdl", + "vaapi", + "vdpau"}); + } + else + { + this.comboBoxVideoOutput.Items.AddRange(new object[] { + "direct3d_shaders", + "direct3d ", + "sdl", + "vaapi", + "vdpau"}); + } + this.comboBoxVideoOutput.Location = new System.Drawing.Point(12, 109); this.comboBoxVideoOutput.Name = "comboBoxVideoOutput"; this.comboBoxVideoOutput.Size = new System.Drawing.Size(186, 21); @@ -113,7 +129,8 @@ this.Controls.Add(this.buttonCancel); this.Controls.Add(this.label1); this.Controls.Add(this.comboBoxVideoOutput); - this.Controls.Add(this.buttonDownload); + if (!Configuration.IsRunningOnLinux()) + this.Controls.Add(this.buttonDownload); this.FormBorderStyle = System.Windows.Forms.FormBorderStyle.FixedDialog; this.KeyPreview = true; this.MaximizeBox = false; diff --git a/src/Forms/SettingsMpv.cs b/src/Forms/SettingsMpv.cs index 8f8397364..ee72e6595 100644 --- a/src/Forms/SettingsMpv.cs +++ b/src/Forms/SettingsMpv.cs @@ -13,11 +13,15 @@ namespace Nikse.SubtitleEdit.Forms { InitializeComponent(); labelPleaseWait.Text = string.Empty; - comboBoxVideoOutput.Text = Configuration.Settings.General.MpvVideoOutput; + if (!Configuration.IsRunningOnLinux()) + comboBoxVideoOutput.Text = Configuration.Settings.General.MpvVideoOutput; + else + comboBoxVideoOutput.Text = "vaapi"; buttonCancel.Text = Configuration.Settings.Language.General.Cancel; buttonOK.Text = Configuration.Settings.Language.General.Ok; Text = Configuration.Settings.Language.SettingsMpv.Title; - buttonDownload.Text = Configuration.Settings.Language.SettingsMpv.DownloadMpv; + if (!Configuration.IsRunningOnLinux()) + buttonDownload.Text = Configuration.Settings.Language.SettingsMpv.DownloadMpv; } private void wc_DownloadDataCompleted(object sender, DownloadDataCompletedEventArgs e) @@ -27,7 +31,8 @@ namespace Nikse.SubtitleEdit.Forms MessageBox.Show(Configuration.Settings.Language.SettingsMpv.DownloadMpvFailed); labelPleaseWait.Text = string.Empty; buttonOK.Enabled = true; - buttonDownload.Enabled = true; + if (!Configuration.IsRunningOnLinux()) + buttonDownload.Enabled = true; Cursor = Cursors.Default; return; } @@ -53,7 +58,8 @@ namespace Nikse.SubtitleEdit.Forms Cursor = Cursors.Default; labelPleaseWait.Text = string.Empty; buttonOK.Enabled = true; - buttonDownload.Enabled = true; + if (!Configuration.IsRunningOnLinux()) + buttonDownload.Enabled = true; MessageBox.Show(Configuration.Settings.Language.SettingsMpv.DownloadMpvOk); } @@ -63,7 +69,8 @@ namespace Nikse.SubtitleEdit.Forms { labelPleaseWait.Text = Configuration.Settings.Language.General.PleaseWait; buttonOK.Enabled = false; - buttonDownload.Enabled = false; + if (!Configuration.IsRunningOnLinux()) + buttonDownload.Enabled = false; Refresh(); Cursor = Cursors.WaitCursor; @@ -80,7 +87,8 @@ namespace Nikse.SubtitleEdit.Forms { labelPleaseWait.Text = string.Empty; buttonOK.Enabled = true; - buttonDownload.Enabled = true; + if (!Configuration.IsRunningOnLinux()) + buttonDownload.Enabled = true; Cursor = Cursors.Default; MessageBox.Show(exception.Message + Environment.NewLine + Environment.NewLine + exception.StackTrace); } diff --git a/src/Logic/UiUtil.cs b/src/Logic/UiUtil.cs index af0e6e566..70a202c40 100644 --- a/src/Logic/UiUtil.cs +++ b/src/Logic/UiUtil.cs @@ -174,7 +174,7 @@ namespace Nikse.SubtitleEdit.Logic GeneralSettings gs = Configuration.Settings.General; if (Configuration.IsRunningOnLinux()) - return new MPlayer(); + return new LibMpvMono(); // Mono on OS X is 32 bit and thus requires 32 bit VLC. Place VLC in the same // folder as Subtitle Edit and add this to the app.config inside the diff --git a/src/Logic/VideoPlayers/LibMpvMono.cs b/src/Logic/VideoPlayers/LibMpvMono.cs new file mode 100644 index 000000000..dae46c316 --- /dev/null +++ b/src/Logic/VideoPlayers/LibMpvMono.cs @@ -0,0 +1,431 @@ +using Nikse.SubtitleEdit.Core; +using System; +using System.Collections.Generic; +using System.Globalization; +using System.IO; +using System.Runtime.InteropServices; +using System.Text; +using System.Windows.Forms; +using Nikse.SubtitleEdit.Core.SubtitleFormats; + + +namespace Nikse.SubtitleEdit.Logic.VideoPlayers +{ + public class LibMpvMono : VideoPlayer, IDisposable + { + + + #region mpv dll methods + [DllImport("mpv", CallingConvention = CallingConvention.Cdecl)] + private static extern IntPtr mpv_create(); + + + [DllImport("mpv", CallingConvention = CallingConvention.Cdecl)] + private static extern int mpv_initialize(IntPtr mpvHandle); + + + [DllImport("mpv", CallingConvention = CallingConvention.Cdecl)] + private static extern int mpv_command(IntPtr mpvHandle, IntPtr utf8Strings); + + + [DllImport("mpv", CallingConvention = CallingConvention.Cdecl)] + private static extern int mpv_terminate_destroy(IntPtr mpvHandle); + + + [DllImport("mpv", CallingConvention = CallingConvention.Cdecl)] + private static extern IntPtr mpv_wait_event(IntPtr mpvHandle, double wait); + + + [DllImport("mpv", CallingConvention = CallingConvention.Cdecl)] + private static extern int mpv_set_option(IntPtr mpvHandle, byte[] name, int format, ref long data); + + + [DllImport("mpv", CallingConvention = CallingConvention.Cdecl)] + private static extern int mpv_set_option_string(IntPtr mpvHandle, byte[] name, byte[] value); + + + [DllImport("mpv", CallingConvention = CallingConvention.Cdecl)] + private static extern IntPtr mpv_get_property_string(IntPtr mpvHandle, byte[] name); + + + [DllImport("mpv", CallingConvention = CallingConvention.Cdecl)] + private static extern int mpv_get_property(IntPtr mpvHandle, byte[] name, int format, ref double data); + + + [DllImport("mpv", CallingConvention = CallingConvention.Cdecl)] + private static extern int mpv_set_property(IntPtr mpvHandle, byte[] name, int format, ref byte[] data); + + + [DllImport("mpv", CallingConvention = CallingConvention.Cdecl)] + private static extern int mpv_free(IntPtr data); + + #endregion + + private IntPtr _libMpvDll; + private IntPtr _mpvHandle; + private Timer _videoLoadedTimer; + // private Timer _videoEndedTimer; + + public override event EventHandler OnVideoLoaded; + public override event EventHandler OnVideoEnded; + + private const int MpvFormatString = 1; + + private object GetDllType(Type type, string name) + { + IntPtr address = NativeMethods.GetProcAddress(_libMpvDll, name); + if (address != IntPtr.Zero) + { + return Marshal.GetDelegateForFunctionPointer(address, type); + } + return null; + } + + + private static byte[] GetUtf8Bytes(string s) + { + return Encoding.UTF8.GetBytes(s + "\0"); + } + + public static IntPtr AllocateUtf8IntPtrArrayWithSentinel(string[] arr, out IntPtr[] byteArrayPointers) + { + int numberOfStrings = arr.Length + 1; // add extra element for extra null pointer last (sentinel) + byteArrayPointers = new IntPtr[numberOfStrings]; + IntPtr rootPointer = Marshal.AllocCoTaskMem(IntPtr.Size * numberOfStrings); + for (int index = 0; index < arr.Length; index++) + { + var bytes = GetUtf8Bytes(arr[index]); + IntPtr unmanagedPointer = Marshal.AllocHGlobal(bytes.Length); + Marshal.Copy(bytes, 0, unmanagedPointer, bytes.Length); + byteArrayPointers[index] = unmanagedPointer; + } + Marshal.Copy(byteArrayPointers, 0, rootPointer, numberOfStrings); + return rootPointer; + } + + private void DoMpvCommand(params string[] args) + { + if (_mpvHandle == IntPtr.Zero) + return; + + IntPtr[] byteArrayPointers; + var mainPtr = AllocateUtf8IntPtrArrayWithSentinel(args, out byteArrayPointers); + mpv_command(_mpvHandle, mainPtr); + foreach (var ptr in byteArrayPointers) + { + Marshal.FreeHGlobal(ptr); + } + Marshal.FreeHGlobal(mainPtr); + } + + public override string PlayerName + { + get { return "MPV Player"; } + } + + private int _volume = 75; + public override int Volume + { + get + { + return _volume; + } + set + { + DoMpvCommand("set", "volume", value.ToString()); + _volume = value; + } + } + + public override double Duration + { + get + { + if (_mpvHandle == IntPtr.Zero) + return 0; + + int mpvFormatDouble = 5; + double d = 0; + mpv_get_property(_mpvHandle, GetUtf8Bytes("duration"), mpvFormatDouble, ref d); + return d; + } + } + + public override double CurrentPosition + { + get + { + if (_mpvHandle == IntPtr.Zero) + return 0; + + int mpvFormatDouble = 5; + double d = 0; + mpv_get_property(_mpvHandle, GetUtf8Bytes("time-pos"), mpvFormatDouble, ref d); + return d; + } + set + { + if (_mpvHandle == IntPtr.Zero) + return; + + DoMpvCommand("seek", value.ToString(CultureInfo.InvariantCulture), "absolute"); + } + } + + private double _playRate = 2.0; + public override double PlayRate + { + get + { + return _playRate; + } + set + { + DoMpvCommand("set", "speed", value.ToString(CultureInfo.InvariantCulture)); + _playRate = value; + } + } + + public void GetNextFrame() + { + if (_mpvHandle == IntPtr.Zero) + return; + + DoMpvCommand("frame-step"); + } + + public void GetPreviousFrame() + { + if (_mpvHandle == IntPtr.Zero) + return; + + DoMpvCommand("frame-back-step"); + } + + public override void Play() + { + if (_mpvHandle == IntPtr.Zero) + return; + + var bytes = GetUtf8Bytes("no"); + mpv_set_property(_mpvHandle, GetUtf8Bytes("pause"), MpvFormatString, ref bytes); + } + + public override void Pause() + { + if (_mpvHandle == IntPtr.Zero) + return; + + var bytes = GetUtf8Bytes("yes"); + mpv_set_property(_mpvHandle, GetUtf8Bytes("pause"), MpvFormatString, ref bytes); + } + + public override void Stop() + { + Pause(); + CurrentPosition = 0; + } + + public override bool IsPaused + { + get + { + if (_mpvHandle == IntPtr.Zero) + return true; + + var lpBuffer = mpv_get_property_string(_mpvHandle, GetUtf8Bytes("pause")); + string s = Marshal.PtrToStringAnsi(lpBuffer); + bool isPaused = s == "yes"; + mpv_free(lpBuffer); + return isPaused; + } + } + + public override bool IsPlaying + { + get { return !IsPaused; } + } + + private List _audioTrackIds; + public int AudioTrackCount + { + get + { + if (_audioTrackIds == null) + { + _audioTrackIds = new List(); + var lpBuffer = mpv_get_property_string(_mpvHandle, GetUtf8Bytes("track-list")); + string trackListJson = Marshal.PtrToStringAnsi(lpBuffer); + foreach (var json in Json.ReadObjectArray(trackListJson)) + { + string trackType = Json.ReadTag(json, "type"); + string id = Json.ReadTag(json, "id"); + if (trackType == "audio") + { + _audioTrackIds.Add(id); + } + } + mpv_free(lpBuffer); + } + return _audioTrackIds.Count; + } + } + + public int AudioTrackNumber + { + get + { + var lpBuffer = mpv_get_property_string(_mpvHandle, GetUtf8Bytes("aid")); + string str = Marshal.PtrToStringAnsi(lpBuffer); + int number = 0; + if (AudioTrackCount > 1 && _audioTrackIds.Contains(str)) + { + number = _audioTrackIds.IndexOf(str); + } + mpv_free(lpBuffer); + return number; + } + set + { + string id = "1"; + if (AudioTrackCount > 1 && value >= 0 && value < _audioTrackIds.Count) + { + id = _audioTrackIds[value]; + } + DoMpvCommand("set", "aid", id); + } + } + + public static bool IsInstalled + { + get + { + return true; + } + } + + public static string GetMpvPath(string fileName) + { + var path = Path.Combine(Configuration.DataDirectory, fileName); + if (File.Exists(path)) + return path; + + return null; + } + + public override void Initialize(Control ownerControl, string videoFileName, EventHandler onVideoLoaded, EventHandler onVideoEnded) + { + _mpvHandle = mpv_create(); + OnVideoLoaded = onVideoLoaded; + OnVideoEnded = onVideoEnded; + + if (!string.IsNullOrEmpty(videoFileName)) + { + //Mono.Unix. + mpv_initialize(_mpvHandle); + + string videoOutput = "vaapi"; + if (!string.IsNullOrWhiteSpace(Configuration.Settings.General.MpvVideoOutput)) + videoOutput = Configuration.Settings.General.MpvVideoOutput; + mpv_set_option_string(_mpvHandle, GetUtf8Bytes("vo"), GetUtf8Bytes(videoOutput)); // "direct3d_shaders" is default, "direct3d" could be used for compabality with old systems + + mpv_set_option_string(_mpvHandle, GetUtf8Bytes("keep-open"), GetUtf8Bytes("always")); // don't auto close video + mpv_set_option_string(_mpvHandle, GetUtf8Bytes("no-sub"), GetUtf8Bytes("")); // don't load subtitles + if (ownerControl != null) + { + int mpvFormatInt64 = 4; + var windowId = ownerControl.Handle.ToInt64(); + mpv_set_option(_mpvHandle, GetUtf8Bytes("wid"), mpvFormatInt64, ref windowId); + } + DoMpvCommand("loadfile", videoFileName); + + _videoLoadedTimer = new Timer { Interval = 50 }; + _videoLoadedTimer.Tick += VideoLoadedTimer_Tick; + _videoLoadedTimer.Start(); + } + } + + private void VideoLoadedTimer_Tick(object sender, EventArgs e) + { + _videoLoadedTimer.Stop(); + const int mpvEventFileLoaded = 8; + int l = 0; + while (l < 10000) + { + Application.DoEvents(); + try + { + if (_mpvHandle != IntPtr.Zero) + { + var eventIdHandle = mpv_wait_event(_mpvHandle, 0); + var eventId = Convert.ToInt64(Marshal.PtrToStructure(eventIdHandle, typeof(int))); + if (eventId == mpvEventFileLoaded) + { + break; + } + } + l++; + } + catch + { + return; + } + } + Application.DoEvents(); + if (OnVideoLoaded != null) + OnVideoLoaded.Invoke(this, null); + Application.DoEvents(); + Pause(); + } + + + public override void DisposeVideoPlayer() + { + Dispose(); + } + + private void ReleaseUnmangedResources() + { + try + { + lock (this) + { + if (_mpvHandle != IntPtr.Zero) + { + mpv_terminate_destroy(_mpvHandle); + _mpvHandle = IntPtr.Zero; + } + + if (_libMpvDll != IntPtr.Zero) + { + NativeMethods.FreeLibrary(_libMpvDll); + _libMpvDll = IntPtr.Zero; + } + } + } + catch + { + // ignored + } + } + + ~LibMpvMono() + { + Dispose(false); + } + + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + protected virtual void Dispose(bool disposing) + { + if (_mpvHandle != IntPtr.Zero) + DoMpvCommand("quit"); + ReleaseUnmangedResources(); + } + + } +} diff --git a/src/Logic/VideoPlayers/libMpvDynamic.cs b/src/Logic/VideoPlayers/libMpvDynamic.cs index cb24c918d..7b9e0ec26 100644 --- a/src/Logic/VideoPlayers/libMpvDynamic.cs +++ b/src/Logic/VideoPlayers/libMpvDynamic.cs @@ -378,7 +378,7 @@ namespace Nikse.SubtitleEdit.Logic.VideoPlayers { _mpvInitialize.Invoke(_mpvHandle); - string videoOutput = "direct3d_shaders"; + string videoOutput = "vaapi"; if (!string.IsNullOrWhiteSpace(Configuration.Settings.General.MpvVideoOutput)) videoOutput = Configuration.Settings.General.MpvVideoOutput; _mpvSetOptionString(_mpvHandle, GetUtf8Bytes("vo"), GetUtf8Bytes(videoOutput)); // "direct3d_shaders" is default, "direct3d" could be used for compabality with old systems diff --git a/src/SubtitleEdit.csproj b/src/SubtitleEdit.csproj index 1d229bbd1..cfa2ae974 100644 --- a/src/SubtitleEdit.csproj +++ b/src/SubtitleEdit.csproj @@ -866,6 +866,7 @@ +