Repository: datasone/MPVMediaControl Branch: master Commit: 23636da7f5e6 Files: 20 Total size: 59.9 KB Directory structure: gitextract_f3ieohtx/ ├── .gitattributes ├── .gitignore ├── LICENSE ├── MPVMediaControl/ │ ├── App.config │ ├── MPVMediaControl.csproj │ ├── MediaController.cs │ ├── PipeClient.cs │ ├── PipeServer.cs │ ├── Program.cs │ ├── Properties/ │ │ ├── AssemblyInfo.cs │ │ ├── Resources.Designer.cs │ │ ├── Resources.resx │ │ ├── Settings.Designer.cs │ │ └── Settings.settings │ ├── StreamString.cs │ └── SystemMeidaTransportControlsInterop.cs ├── MPVMediaControl.sln ├── README.md ├── notify_media.conf └── notify_media.lua ================================================ FILE CONTENTS ================================================ ================================================ FILE: .gitattributes ================================================ ############################################################################### # Set default behavior to automatically normalize line endings. ############################################################################### * text=auto ############################################################################### # Set default behavior for command prompt diff. # # This is need for earlier builds of msysgit that does not have it on by # default for csharp files. # Note: This is only used by command line ############################################################################### #*.cs diff=csharp ############################################################################### # Set the merge driver for project and solution files # # Merging from the command prompt will add diff markers to the files if there # are conflicts (Merging from VS is not affected by the settings below, in VS # the diff markers are never inserted). Diff markers may cause the following # file extensions to fail to load in VS. An alternative would be to treat # these files as binary and thus will always conflict and require user # intervention with every merge. To do so, just uncomment the entries below ############################################################################### #*.sln merge=binary #*.csproj merge=binary #*.vbproj merge=binary #*.vcxproj merge=binary #*.vcproj merge=binary #*.dbproj merge=binary #*.fsproj merge=binary #*.lsproj merge=binary #*.wixproj merge=binary #*.modelproj merge=binary #*.sqlproj merge=binary #*.wwaproj merge=binary ############################################################################### # behavior for image files # # image files are treated as binary by default. ############################################################################### #*.jpg binary #*.png binary #*.gif binary ############################################################################### # diff behavior for common document formats # # Convert binary document formats to text before diffing them. This feature # is only available from the command line. Turn it on by uncommenting the # entries below. ############################################################################### #*.doc diff=astextplain #*.DOC diff=astextplain #*.docx diff=astextplain #*.DOCX diff=astextplain #*.dot diff=astextplain #*.DOT diff=astextplain #*.pdf diff=astextplain #*.PDF diff=astextplain #*.rtf diff=astextplain #*.RTF diff=astextplain ================================================ FILE: .gitignore ================================================ ## Ignore Visual Studio temporary files, build results, and ## files generated by popular Visual Studio add-ons. ## ## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore # User-specific files *.rsuser *.suo *.user *.userosscache *.sln.docstates # User-specific files (MonoDevelop/Xamarin Studio) *.userprefs # Mono auto generated files mono_crash.* # Build results [Dd]ebug/ [Dd]ebugPublic/ [Rr]elease/ [Rr]eleases/ x64/ x86/ [Ww][Ii][Nn]32/ [Aa][Rr][Mm]/ [Aa][Rr][Mm]64/ bld/ [Bb]in/ [Oo]bj/ [Oo]ut/ [Ll]og/ [Ll]ogs/ # Visual Studio 2015/2017 cache/options directory .vs/ # Uncomment if you have tasks that create the project's static files in wwwroot #wwwroot/ # Visual Studio 2017 auto generated files Generated\ Files/ # MSTest test Results [Tt]est[Rr]esult*/ [Bb]uild[Ll]og.* # NUnit *.VisualState.xml TestResult.xml nunit-*.xml # Build Results of an ATL Project [Dd]ebugPS/ [Rr]eleasePS/ dlldata.c # Benchmark Results BenchmarkDotNet.Artifacts/ # .NET Core project.lock.json project.fragment.lock.json artifacts/ # ASP.NET Scaffolding ScaffoldingReadMe.txt # StyleCop StyleCopReport.xml # Files built by Visual Studio *_i.c *_p.c *_h.h *.ilk *.meta *.obj *.iobj *.pch *.pdb *.ipdb *.pgc *.pgd *.rsp *.sbr *.tlb *.tli *.tlh *.tmp *.tmp_proj *_wpftmp.csproj *.log *.vspscc *.vssscc .builds *.pidb *.svclog *.scc # Chutzpah Test files _Chutzpah* # Visual C++ cache files ipch/ *.aps *.ncb *.opendb *.opensdf *.sdf *.cachefile *.VC.db *.VC.VC.opendb # Visual Studio profiler *.psess *.vsp *.vspx *.sap # Visual Studio Trace Files *.e2e # TFS 2012 Local Workspace $tf/ # Guidance Automation Toolkit *.gpState # ReSharper is a .NET coding add-in _ReSharper*/ *.[Rr]e[Ss]harper *.DotSettings.user # TeamCity is a build add-in _TeamCity* # DotCover is a Code Coverage Tool *.dotCover # AxoCover is a Code Coverage Tool .axoCover/* !.axoCover/settings.json # Coverlet is a free, cross platform Code Coverage Tool coverage*.json coverage*.xml coverage*.info # Visual Studio code coverage results *.coverage *.coveragexml # NCrunch _NCrunch_* .*crunch*.local.xml nCrunchTemp_* # MightyMoose *.mm.* AutoTest.Net/ # Web workbench (sass) .sass-cache/ # Installshield output folder [Ee]xpress/ # DocProject is a documentation generator add-in DocProject/buildhelp/ DocProject/Help/*.HxT DocProject/Help/*.HxC DocProject/Help/*.hhc DocProject/Help/*.hhk DocProject/Help/*.hhp DocProject/Help/Html2 DocProject/Help/html # Click-Once directory publish/ # Publish Web Output *.[Pp]ublish.xml *.azurePubxml # Note: Comment the next line if you want to checkin your web deploy settings, # but database connection strings (with potential passwords) will be unencrypted *.pubxml *.publishproj # Microsoft Azure Web App publish settings. Comment the next line if you want to # checkin your Azure Web App publish settings, but sensitive information contained # in these scripts will be unencrypted PublishScripts/ # NuGet Packages *.nupkg # NuGet Symbol Packages *.snupkg # The packages folder can be ignored because of Package Restore **/[Pp]ackages/* # except build/, which is used as an MSBuild target. !**/[Pp]ackages/build/ # Uncomment if necessary however generally it will be regenerated when needed #!**/[Pp]ackages/repositories.config # NuGet v3's project.json files produces more ignorable files *.nuget.props *.nuget.targets # Microsoft Azure Build Output csx/ *.build.csdef # Microsoft Azure Emulator ecf/ rcf/ # Windows Store app package directories and files AppPackages/ BundleArtifacts/ Package.StoreAssociation.xml _pkginfo.txt *.appx *.appxbundle *.appxupload # Visual Studio cache files # files ending in .cache can be ignored *.[Cc]ache # but keep track of directories ending in .cache !?*.[Cc]ache/ # Others ClientBin/ ~$* *~ *.dbmdl *.dbproj.schemaview *.jfm *.pfx *.publishsettings orleans.codegen.cs # Including strong name files can present a security risk # (https://github.com/github/gitignore/pull/2483#issue-259490424) #*.snk # Since there are multiple workflows, uncomment next line to ignore bower_components # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) #bower_components/ # RIA/Silverlight projects Generated_Code/ # Backup & report files from converting an old project file # to a newer Visual Studio version. Backup files are not needed, # because we have git ;-) _UpgradeReport_Files/ Backup*/ UpgradeLog*.XML UpgradeLog*.htm ServiceFabricBackup/ *.rptproj.bak # SQL Server files *.mdf *.ldf *.ndf # Business Intelligence projects *.rdl.data *.bim.layout *.bim_*.settings *.rptproj.rsuser *- [Bb]ackup.rdl *- [Bb]ackup ([0-9]).rdl *- [Bb]ackup ([0-9][0-9]).rdl # Microsoft Fakes FakesAssemblies/ # GhostDoc plugin setting file *.GhostDoc.xml # Node.js Tools for Visual Studio .ntvs_analysis.dat node_modules/ # Visual Studio 6 build log *.plg # Visual Studio 6 workspace options file *.opt # Visual Studio 6 auto-generated workspace file (contains which files were open etc.) *.vbw # Visual Studio LightSwitch build output **/*.HTMLClient/GeneratedArtifacts **/*.DesktopClient/GeneratedArtifacts **/*.DesktopClient/ModelManifest.xml **/*.Server/GeneratedArtifacts **/*.Server/ModelManifest.xml _Pvt_Extensions # Paket dependency manager .paket/paket.exe paket-files/ # FAKE - F# Make .fake/ # CodeRush personal settings .cr/personal # Python Tools for Visual Studio (PTVS) __pycache__/ *.pyc # Cake - Uncomment if you are using it # tools/** # !tools/packages.config # Tabs Studio *.tss # Telerik's JustMock configuration file *.jmconfig # BizTalk build output *.btp.cs *.btm.cs *.odx.cs *.xsd.cs # OpenCover UI analysis results OpenCover/ # Azure Stream Analytics local run output ASALocalRun/ # MSBuild Binary and Structured Log *.binlog # NVidia Nsight GPU debugger configuration file *.nvuser # MFractors (Xamarin productivity tool) working folder .mfractor/ # Local History for Visual Studio .localhistory/ # BeatPulse healthcheck temp database healthchecksdb # Backup folder for Package Reference Convert tool in Visual Studio 2017 MigrationBackup/ # Ionide (cross platform F# VS Code tools) working folder .ionide/ # Fody - auto-generated XML schema FodyWeavers.xsd ================================================ FILE: LICENSE ================================================ MIT License Copyright (c) 2021 datasone Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: MPVMediaControl/App.config ================================================  ================================================ FILE: MPVMediaControl/MPVMediaControl.csproj ================================================  Debug AnyCPU {F7771502-9FE9-4BD0-B073-FDC2ABF5622A} WinExe MPVMediaControl MPVMediaControl v4.7.2 512 true true AnyCPU true full false bin\Debug\ DEBUG;TRACE prompt 4 AnyCPU pdbonly true bin\Release\ TRACE prompt 4 ResXFileCodeGenerator Resources.Designer.cs Designer True Resources.resx SettingsSingleFileGenerator Settings.Designer.cs True Settings.settings True 10.0.17763.1000 4.3.0 ================================================ FILE: MPVMediaControl/MediaController.cs ================================================ using System; using System.IO; using System.Threading; using Windows.Media; using Windows.Media.Playback; using Windows.Storage; using Windows.Storage.Streams; namespace MPVMediaControl { public class MediaController { private SystemMediaTransportControls _controls; private SystemMediaTransportControlsDisplayUpdater _updater; private MediaPlayer _mediaPlayer; public readonly int Pid; private readonly string SocketName; public class MCMediaFile { public string Title; public string Artist; public string Path; public string ShotPath; public MediaPlaybackType Type; public bool ThumbnailObtained = false; private IStorageFile _thumbnailFile; public IStorageFile ThumbnailFile() { if (!ThumbnailObtained && ShotPath != "") { Thread.Sleep(100); var count = 0; while (!System.IO.File.Exists(ShotPath) && count++ < 50) { Thread.Sleep(100); } ThumbnailObtained = true; _thumbnailFile = StorageFile.GetFileFromPathAsync(ShotPath).GetAwaiter().GetResult(); } return _thumbnailFile; } public void Cleanup() { if (System.IO.File.Exists(ShotPath)) { System.IO.File.Delete(ShotPath); } } public MCMediaFile Clone() { return this.MemberwiseClone() as MCMediaFile; } } private MCMediaFile _file = new MCMediaFile(); public MCMediaFile File { get => _file; set { _file.Title = value.Title; _file.Artist = value.Artist; if (_file.Path != value.Path) _file.ThumbnailObtained = false; _file.Path = value.Path; _file.ShotPath = value.ShotPath.Replace('/', '\\'); _file.Type = value.Type; _updater.ClearAll(); _updater.Type = _file.Type; switch (_file.Type) { case MediaPlaybackType.Image: _updater.ImageProperties.Title = _file.Title; break; case MediaPlaybackType.Music: _updater.MusicProperties.Title = _file.Title; _updater.MusicProperties.Artist = _file.Artist; break; case MediaPlaybackType.Video: _updater.VideoProperties.Title = _file.Title; break; } try { var file = _file.ThumbnailFile(); if (file != null) { _updater.Thumbnail = RandomAccessStreamReference.CreateFromFile(file); } } catch (Exception e) when (e is FileNotFoundException || e is UnauthorizedAccessException || e is ArgumentException) { } _updater.Update(); } } public enum PlayState { Play, Pause, Stop } private PlayState _state; public PlayState State { get => _state; set { _state = value; switch (value) { case PlayState.Play: _controls.IsEnabled = true; _controls.PlaybackStatus = MediaPlaybackStatus.Playing; break; case PlayState.Pause: _controls.IsEnabled = true; _controls.PlaybackStatus = MediaPlaybackStatus.Paused; break; case PlayState.Stop: _updater.ClearAll(); _controls.IsEnabled = false; _controls.PlaybackStatus = MediaPlaybackStatus.Closed; break; } } } public void InitSMTC() { _mediaPlayer = new MediaPlayer(); _controls = _mediaPlayer.SystemMediaTransportControls; _mediaPlayer.CommandManager.IsEnabled = false; _updater = _controls.DisplayUpdater; _controls.ButtonPressed += ButtonPressed; _controls.IsPlayEnabled = true; _controls.IsPauseEnabled = true; _controls.IsNextEnabled = true; _controls.IsPreviousEnabled = true; State = _state; if (_file.Path != null) { File = _file; } } public MediaController(int pid, string socketName, bool initSMTC) { Pid = pid; SocketName = socketName; _state = PlayState.Stop; if (initSMTC) { InitSMTC(); } } public MediaController DuplicateSelf() { var newObj = new MediaController(Pid, SocketName, false); newObj._file = _file.Clone(); newObj._state = _state; return newObj; } public void Cleanup(bool cleanFile) { if (cleanFile) { _file?.Cleanup(); } _updater.ClearAll(); // Ensure objects collected while "resetting SMTC" _updater = null; _controls = null; _mediaPlayer = null; } public void UpdateShotPath(string path) { var shotPath = path.Replace('/', '\\'); shotPath = shotPath.Replace("\\\\", "\\"); File.ShotPath = shotPath; File.ThumbnailObtained = false; try { _updater.Thumbnail = RandomAccessStreamReference.CreateFromFile(_file.ThumbnailFile()); } catch (Exception e) when (e is FileNotFoundException || e is UnauthorizedAccessException || e is ArgumentException) { } _updater.Update(); } private void ButtonPressed(SystemMediaTransportControls controls, SystemMediaTransportControlsButtonPressedEventArgs args) { switch (args.Button) { case SystemMediaTransportControlsButton.Play: Play(); break; case SystemMediaTransportControlsButton.Pause: Pause(); break; case SystemMediaTransportControlsButton.Next: Next(); break; case SystemMediaTransportControlsButton.Previous: Previous(); break; } } private void Play() { PipeClient.SendCommand(SocketName, "{ \"command\": [\"set_property\", \"pause\", false] }\r\n"); } private void Pause() { PipeClient.SendCommand(SocketName, "{ \"command\": [\"set_property\", \"pause\", true] }\r\n"); } private void Next() { PipeClient.SendCommand(SocketName, "{ \"command\": [\"playlist-next\", \"weak\"] }\r\n"); } private void Previous() { PipeClient.SendCommand(SocketName, "{ \"command\": [\"playlist-prev\", \"weak\"] }\r\n"); } } } ================================================ FILE: MPVMediaControl/PipeClient.cs ================================================ using System; using System.Collections.Generic; using System.IO.Pipes; using System.Linq; using System.Text; using System.Threading; using System.Threading.Tasks; namespace MPVMediaControl { class PipeClient { public static void SendCommand(string socketName, string command) { new Thread(_ => { var pipeClient = new NamedPipeClientStream(socketName); pipeClient.Connect(); var ss = new StreamString(pipeClient); ss.WriteString(command); pipeClient.Close(); }).Start(); } } } ================================================ FILE: MPVMediaControl/PipeServer.cs ================================================ using System; using System.Collections.Concurrent; using System.Collections.Generic; using System.IO; using System.IO.Pipes; using System.Linq; using System.Text; using System.Threading; using Windows.Media; namespace MPVMediaControl { class PipeServer { private const int NumThreads = 16; private static readonly Thread[] Servers = new Thread[NumThreads]; private static readonly object ParseLock = new object(); private static ConcurrentQueue CommandQueue = new ConcurrentQueue(); public static void StartServer() { for (var i = 0; i < NumThreads; i++) { Servers[i] = new Thread(ServerThread); Servers[i].Start(); } } public static void Cleanup() { for (var i = 0; i < NumThreads; i++) { if (Servers[i] != null) Servers[i].Abort(); } } private static void ServerThread(object data) { while (true) { using (NamedPipeServerStream pipeServer = new NamedPipeServerStream("mpvmcsocket", PipeDirection.InOut, NumThreads)) { try { pipeServer.WaitForConnection(); #if DEBUG System.Diagnostics.Debug.WriteLine($"{Thread.CurrentThread.ManagedThreadId} Received message"); #endif CommandQueue.Enqueue(Thread.CurrentThread.ManagedThreadId); var ss = new StreamString(pipeServer); var command = ss.ReadString(); #if DEBUG System.Diagnostics.Debug.WriteLine(command); #endif while (true) { lock (ParseLock) { #if DEBUG CommandQueue.TryPeek(out var nextId); System.Diagnostics.Debug.WriteLine( $"Next ID is {nextId}, current thread is {Thread.CurrentThread.ManagedThreadId}"); #endif if (!CommandQueue.TryPeek(out var id) || id != Thread.CurrentThread.ManagedThreadId) continue; CommandQueue.TryDequeue(out _); ParseCommand(command); break; } } } catch (IOException) { } catch (ThreadAbortException) { pipeServer.Close(); break; } } } } private static string FromHexString(string hexString) { var bytes = new byte[hexString.Length / 2]; for (var i = 0; i < bytes.Length; i++) { bytes[i] = Convert.ToByte(hexString.Substring(i * 2, 2), 16); } return Encoding.UTF8.GetString(bytes); // returns: "Hello world" for "48656C6C6F20776F726C64" } private static void ParseFile(MediaController controller, Dictionary parameters) { var title = FromHexString(parameters["title"]); var artist = FromHexString(parameters["artist"]); var path = FromHexString(parameters["path"]); var shotPath = parameters.ContainsKey("shot_path") ? FromHexString(parameters["shot_path"]) : String.Empty; // Using MediaPlaybackType.Unknown will cause exception, so another default value has to be set var type = MediaPlaybackType.Music; if (parameters.ContainsKey("type")) { switch (parameters["type"]) { case "video": type = MediaPlaybackType.Video; break; case "music": type = MediaPlaybackType.Music; break; case "image": type = MediaPlaybackType.Image; break; } } // Processing metadata may take some time, so only checking path isn't enough. if (title == controller.File.Title && artist == controller.File.Artist && path == controller.File.Path) return; var file = new MediaController.MCMediaFile { Title = title, Artist = artist, Path = path, ShotPath = shotPath.Replace('/', '\\'), Type = type, }; controller.File = file; } private static void ParseCommand(string command) { if (command.EndsWith(" \r\n")) command = command.Substring(0, command.Length - 3); if (command.StartsWith("^") && command.EndsWith("$")) { command = command.TrimStart('^').TrimEnd('$'); var commandName = command.Split('[')[1].Split(']')[0]; var parameters = new Dictionary(); var parameterStrs = command.Split('('); foreach (var parameterStr in parameterStrs) { if (parameterStr.Last() != ')') continue; var parameter = parameterStr.Substring(0, parameterStr.Length - 1); var parameterName = parameter.Split('=')[0]; var parameterValue = parameter.Split('=')[1]; parameters.Add(parameterName, parameterValue); } #if DEBUG System.Diagnostics.Debug.WriteLine(commandName); #endif var pid = int.Parse(parameters["pid"]); var socketName = parameters["socket_name"]; var controller = Program.AppContext.GetController(pid, socketName); switch (commandName) { case "setFile": ParseFile(controller, parameters); break; case "setState": var isPlaying = parameters["playing"] == "true"; if (Program.AppContext != null && controller != null && controller.File.Path != null) { var expectedState = isPlaying ? MediaController.PlayState.Play : MediaController.PlayState.Pause; if (controller.State != expectedState) controller.State = expectedState; } break; case "setQuit": var quit = parameters["quit"] == "true"; if (quit) { if ((Program.AppContext != null && controller != null) && controller.File.Path != null) { if (controller.State != MediaController.PlayState.Stop) controller.State = MediaController.PlayState.Stop; Program.AppContext.RemoveController(pid); Program.AppContext.ExitIfNoControllers(); } } break; case "setShot": var shotPath = parameters["shot_path"]; controller.UpdateShotPath(FromHexString(shotPath)); break; } } } } } ================================================ FILE: MPVMediaControl/Program.cs ================================================ using System; using System.Collections.Generic; using System.Drawing; using System.Linq; using System.Threading; using System.Threading.Tasks; using System.Windows.Forms; namespace MPVMediaControl { internal static class Program { public static MyAppContext AppContext; private const string Guid = "851aefa6-d429-4f3b-9047-7e08d35810ad"; /// /// The main entry point for the application. /// [STAThread] private static void Main() { using (Mutex mutex = new Mutex(false, "Global\\CrystalLyrics_" + Guid)) { if (!mutex.WaitOne(0, false)) { System.Diagnostics.Debug.WriteLine("Another instance of this application is already running."); return; } Application.EnableVisualStyles(); Application.SetCompatibleTextRenderingDefault(false); AppContext = new MyAppContext(); Application.Run(AppContext); } } } public class MyAppContext : ApplicationContext { private readonly NotifyIcon _trayIcon; private static List _controllers; public MyAppContext() { _trayIcon = new NotifyIcon { Text = "MPV Media Control", Icon = new Icon(SystemIcons.Application, 32, 32), ContextMenu = new ContextMenu(new [] { new MenuItem("Reset SMTC", ResetControllers), new MenuItem("Exit", Exit) }), Visible = true }; PipeServer.StartServer(); _controllers = new List(); } public MediaController GetController(int pid, string socketName) { if (_controllers.FindIndex(c => c.Pid == pid) == -1) { _controllers.Add(new MediaController(pid, socketName, true)); return _controllers.Last(); } return _controllers.Find(c => c.Pid == pid); } public void RemoveController(int pid) { var controller = _controllers.Find(c => c.Pid == pid); controller.Cleanup(true); _controllers.Remove(controller); } private async void ResetControllers(object sender, EventArgs e) { var newControllers = _controllers.Select(c => c.DuplicateSelf()).ToList(); _controllers.ForEach(c => c.Cleanup(false)); _controllers.Clear(); // We need to manually trigger GC to remove MusicPlayer instances and prevent duplicate SMTC controls GC.Collect(); _controllers.AddRange(newControllers); // There is a bug in Windows where the control is not visible (but exists and is able to interact) if the info is updated too fast // Sleep for a bit to prevent this foreach (var controller in _controllers) { await Task.Delay(400); controller.InitSMTC(); } } private void Exit(object sender, EventArgs e) { _trayIcon.Visible = false; PipeServer.Cleanup(); _controllers.ForEach(i => i.Cleanup(true)); Application.Exit(); Environment.Exit(0); } public async void ExitIfNoControllers() { if (_controllers.Count == 0) { await Task.Run(() => Exit(null, null)); } } } } ================================================ FILE: MPVMediaControl/Properties/AssemblyInfo.cs ================================================ using System.Reflection; using System.Runtime.InteropServices; // General Information about an assembly is controlled through the following // set of attributes. Change these attribute values to modify the information // associated with an assembly. [assembly: AssemblyTitle("MPVMediaControl")] [assembly: AssemblyDescription("")] [assembly: AssemblyConfiguration("")] [assembly: AssemblyCompany("")] [assembly: AssemblyProduct("MPVMediaControl")] [assembly: AssemblyCopyright("Copyright © 2021")] [assembly: AssemblyTrademark("")] [assembly: AssemblyCulture("")] // Setting ComVisible to false makes the types in this assembly not visible // to COM components. If you need to access a type in this assembly from // COM, set the ComVisible attribute to true on that type. [assembly: ComVisible(false)] // The following GUID is for the ID of the typelib if this project is exposed to COM [assembly: Guid("f7771502-9fe9-4bd0-b073-fdc2abf5622a")] // Version information for an assembly consists of the following four values: // // Major Version // Minor Version // Build Number // Revision // // You can specify all the values or you can default the Build and Revision Numbers // by using the '*' as shown below: // [assembly: AssemblyVersion("1.0.*")] [assembly: AssemblyVersion("1.0.0.0")] [assembly: AssemblyFileVersion("1.0.0.0")] ================================================ FILE: MPVMediaControl/Properties/Resources.Designer.cs ================================================ //------------------------------------------------------------------------------ // // This code was generated by a tool. // Runtime Version:4.0.30319.42000 // // Changes to this file may cause incorrect behavior and will be lost if // the code is regenerated. // //------------------------------------------------------------------------------ namespace MPVMediaControl.Properties { /// /// A strongly-typed resource class, for looking up localized strings, etc. /// // This class was auto-generated by the StronglyTypedResourceBuilder // class via a tool like ResGen or Visual Studio. // To add or remove a member, edit your .ResX file then rerun ResGen // with the /str option, or rebuild your VS project. [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "4.0.0.0")] [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] internal class Resources { private static global::System.Resources.ResourceManager resourceMan; private static global::System.Globalization.CultureInfo resourceCulture; [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] internal Resources() { } /// /// Returns the cached ResourceManager instance used by this class. /// [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] internal static global::System.Resources.ResourceManager ResourceManager { get { if ((resourceMan == null)) { global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("MPVMediaControl.Properties.Resources", typeof(Resources).Assembly); resourceMan = temp; } return resourceMan; } } /// /// Overrides the current thread's CurrentUICulture property for all /// resource lookups using this strongly typed resource class. /// [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] internal static global::System.Globalization.CultureInfo Culture { get { return resourceCulture; } set { resourceCulture = value; } } } } ================================================ FILE: MPVMediaControl/Properties/Resources.resx ================================================  text/microsoft-resx 2.0 System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 ================================================ FILE: MPVMediaControl/Properties/Settings.Designer.cs ================================================ //------------------------------------------------------------------------------ // // This code was generated by a tool. // Runtime Version:4.0.30319.42000 // // Changes to this file may cause incorrect behavior and will be lost if // the code is regenerated. // //------------------------------------------------------------------------------ namespace MPVMediaControl.Properties { [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.VisualStudio.Editors.SettingsDesigner.SettingsSingleFileGenerator", "11.0.0.0")] internal sealed partial class Settings : global::System.Configuration.ApplicationSettingsBase { private static Settings defaultInstance = ((Settings)(global::System.Configuration.ApplicationSettingsBase.Synchronized(new Settings()))); public static Settings Default { get { return defaultInstance; } } } } ================================================ FILE: MPVMediaControl/Properties/Settings.settings ================================================  ================================================ FILE: MPVMediaControl/StreamString.cs ================================================ using System; using System.IO; using System.Text; namespace MPVMediaControl { // Defines the data protocol for reading and writing strings on our stream public class StreamString { private Stream ioStream; private Encoding streamEncoding; public StreamString(Stream ioStream) { this.ioStream = ioStream; streamEncoding = new UTF8Encoding(); } public string ReadString() { int len = 1024; byte[] inBuffer = new byte[len]; ioStream.Read(inBuffer, 0, len); return streamEncoding.GetString(inBuffer).TrimEnd('\0'); } public int WriteString(string outString) { byte[] outBuffer = streamEncoding.GetBytes(outString); int len = outBuffer.Length; if (len > UInt16.MaxValue) { len = (int)UInt16.MaxValue; } ioStream.Write(outBuffer, 0, len); ioStream.Flush(); return outBuffer.Length; } } } ================================================ FILE: MPVMediaControl/SystemMeidaTransportControlsInterop.cs ================================================ // From https://github.com/AdamBraden/WindowsInteropWrappers/blob/master/CoreWindowInterop/SystemMediaTransportControlsInterop.cs using System; using System.Runtime.InteropServices.WindowsRuntime; using Windows.Media; namespace UWPInterop { //MIDL_INTERFACE("ddb0472d-c911-4a1f-86d9-dc3d71a95f5a") //ISystemMediaTransportControlsInterop : public IInspectable //{ //public: // virtual HRESULT STDMETHODCALLTYPE GetForWindow( // /* [in] */ __RPC__in HWND appWindow, // /* [in] */ __RPC__in REFIID riid, // /* [iid_is][retval][out] */ __RPC__deref_out_opt void** mediaTransportControl) = 0; //}; [System.Runtime.InteropServices.Guid("ddb0472d-c911-4a1f-86d9-dc3d71a95f5a")] [System.Runtime.InteropServices.InterfaceType(System.Runtime.InteropServices.ComInterfaceType .InterfaceIsIInspectable)] interface ISystemMediaTransportControlsInterop { SystemMediaTransportControls GetForWindow(IntPtr appWindow, [System.Runtime.InteropServices.In] ref Guid riid); } //Helper to initialize SystemMediaTransportControls public static class SystemMediaTransportControlsInterop { public static SystemMediaTransportControls GetForWindow(IntPtr hWnd) { ISystemMediaTransportControlsInterop systemMediaTransportControlsInterop = (ISystemMediaTransportControlsInterop)WindowsRuntimeMarshal.GetActivationFactory( typeof(SystemMediaTransportControls)); Guid guid = new Guid("99FA3FF4-1742-42A6-902E-087D41F965EC"); return systemMediaTransportControlsInterop.GetForWindow(hWnd, ref guid); } } } ================================================ FILE: MPVMediaControl.sln ================================================  Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio Version 16 VisualStudioVersion = 16.0.30907.101 MinimumVisualStudioVersion = 10.0.40219.1 Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MPVMediaControl", "MPVMediaControl\MPVMediaControl.csproj", "{F7771502-9FE9-4BD0-B073-FDC2ABF5622A}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU Release|Any CPU = Release|Any CPU EndGlobalSection GlobalSection(ProjectConfigurationPlatforms) = postSolution {F7771502-9FE9-4BD0-B073-FDC2ABF5622A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {F7771502-9FE9-4BD0-B073-FDC2ABF5622A}.Debug|Any CPU.Build.0 = Debug|Any CPU {F7771502-9FE9-4BD0-B073-FDC2ABF5622A}.Release|Any CPU.ActiveCfg = Release|Any CPU {F7771502-9FE9-4BD0-B073-FDC2ABF5622A}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {3FE63DFA-48B1-403D-809A-BEE2A06C3D7E} EndGlobalSection EndGlobal ================================================ FILE: README.md ================================================ # MPVMediaControl This tool adds SMTC feature to mpv player, it communicates with mpv by named pipe, and can be controlled by any other programs. *Screenshot on Windows 11 22000 (It's much more useable than the one in 10)* ## Features - Media metadata display including title and artist information - Thumbnail generated from mpv screenshot or youtube cover image - Media controls (play/pause, prev, next) ## Usage Put `notify_media.lua` in mpv's `scripts` directory and place `MPVMediaControl.exe` to `~~/bin`. You can change the default settings via the `notify_media.conf` file. A `Reset SMTC` item in menu will reset the state of SMTC, useful when Windows is glitched and controls are not working properly (e.g. not displaying or disappearing). For retrieving youtube cover image, `curl.exe` is used. It has been bundled in Windows 10 since [1803, or actually build 17063](https://devblogs.microsoft.com/commandline/tar-and-curl-come-to-windows/). So if you are using older versions of Windows, you may need to manually download and put `curl.exe` into $PATH. ## Protocol This tool listens on a named pipe called `mpvmcsocket`, and receives commands through this protocol: ``` [commandName](param1=value1)(param2=value2)... ``` e.g. ``` [setState](pid=1000)(playing=true) ``` ### Commands - `setFile`: set currently playing file - `setShot`: set screenshot file path - `setState`: set player state (playing or not) - `setQuit`: notify the tool that player has quit, and the SMTC should be cleared ### Parameters - Common parameters - `pid={num}`: the player's pid. This is a mandatory parameter for every command, it is used to identify different instances. - `socket_name={String}`: the name of MPV's input-ipc-server socket. This is mandatory for every command, and the value is **directly** the name string. - Though Windows allows Unicode named pipes, it will make things more complex and I didn't see any benefits to use non-ASCII characters. - Parameters for `setFile` - `title={hexString}`: the title of the media file - `artist={hexString}`: the artist of the media file, music files only. The value can be empty. - `path={hexString}`: the path of the media file - `type={music,video,image}`: the type of the media file - Parameters for `setShot` - `shot_path={hexString}`: the path of the thumbnail image file. The value can be empty. The `hexString` mentioned above means the original string should be encoded to hex, e.g. `Hello <=> 48656C6C6F` - Parameters for `setState` - `playing={bool}`: if the media is being played, `true` for playing, `false` for pausing - Parameters for `setQuit` - `quit=true`: this is always true if you want to quit The media control part works by sending commands to mpv's ipc socket, which `notify_media.lua` will set mpv to listen on, with the configured `socket_name` in `notify_media.conf`. ================================================ FILE: notify_media.conf ================================================ ### Default config for notify_media.lua debug=no # Path to executable (MPVMediaControl.exe) binary_path=~~/bin/MPVMediaControl.exe # Path for storing temporary screenshots shot_path=~~/ # If you want to delay taking screenshot for videos, set this to the number of delayed seconds delayed_sec=3 # Name of mpv's input-ipc-server (defaults to mpvsocket_{pid}), it shall be useful when multiple scripts set up the IPC socket for controls. # String "{pid}" in this config value will be automatically convert to the ID of mpv process to prevent conflicts between multiple instances. socket_name=mpvsocket_{pid} # Should we kill MPVMediaControl when mpv exits, as sometimes the program won't exit properly with mpv kill_mc=no ================================================ FILE: notify_media.lua ================================================ local mp = require 'mp' local utils = require 'mp.utils' local opt = require 'mp.options' local o = { debug = false, -- Path to executable (MPVMediaControl.exe) binary_path = "~~/bin/MPVMediaControl.exe", -- Path for storing temporary screenshots shot_path = "~~/", -- If you want to delay taking screenshot for videos, set this to the number of delayed seconds delayed_sec = 3, -- Name of mpv's input-ipc-server (defaults to mpvsocket_{pid}), string "{pid}" in the value will be automatically replaced with the ID of mpv process socket_name = "mpvsocket_{pid}", -- Should we kill MPVMediaControl when mpv exits, as sometimes the program won't exit properly with mpv kill_mc = false, } opt.read_options(o, "notify_media") o.binary_path = mp.command_native({ "expand-path", o.binary_path }) local pid = utils.getpid() local start_of_file = true local new_file = false local yt_thumbnail = false local yt_failed = false local mpv_socket_name = o.socket_name:gsub("{pid}", tostring(pid)) -- Print contents of `tbl`, with indentation. -- `indent` sets the initial level of indentation. function tprint (tbl, indent) if not indent then indent = 0 end for k, v in pairs(tbl) do formatting = string.rep(" ", indent) .. k .. ": " if type(v) == "table" then print(formatting) tprint(v, indent+1) elseif type(v) == 'boolean' then print(formatting .. tostring(v)) else print(formatting .. v) end end end function debug_log(message) if o.debug then if not message then print("DEBUG: nil") return end if "table" == type(message) then print("DEBUG: ") tprint(message) else print("DEBUG: " .. message) end end end ipc_socket_file = "\\\\.\\pipe\\mpvmcsocket" function write_to_socket(message) _, pipe = pcall(io.open, ipc_socket_file, "w") if pipe then pcall(pipe.write, pipe, message) pcall(pipe.flush, pipe) pcall(pipe.close, pipe) debug_log(message) end end function get_metadata(data, keys) for _, v in pairs(keys) do if data[v] and string.len(data[v]) > 0 then return data[v] end end return "" end function encode_element(str) -- return str:gsub("%(", "\\\\["):gsub("%)", "\\\\]") return tohex(str) end function tohex(str) return (str:gsub('.', function (c) return string.format('%02X', string.byte(c)) end)) end function save_shot(path) if youtube_thumbail(path) then local shot_path_encoded = encode_element(shot_path) message_content = "^[setShot](pid=" .. pid .. ")(shot_path=" .. shot_path_encoded .. ")(socket_name=" .. mpv_socket_name .. ")$" write_to_socket(message_content) return end if start_of_file and media_type() == "video" and o.delayed_sec ~= 0 then mp.add_timeout(o.delayed_sec, function() save_shot(path) end) start_of_file = false return end result = mp.commandv("screenshot-to-file", path) if not result then mp.add_timeout(0.5, function() save_shot(path) end) else local shot_path_encoded = encode_element(shot_path) message_content = "^[setShot](pid=" .. pid .. ")(shot_path=" .. shot_path_encoded .. ")(socket_name=" .. mpv_socket_name .. ")$" write_to_socket(message_content) end end function youtube_thumbail(path) if not yt_thumbnail then debug_log(mp.get_property("path")) if not yt_failed and string.find(mp.get_property("path"), "www.youtube.com") then -- generate a url to the thumbnail file vid_id = mp.get_property("filename") vid_id = string.gsub(vid_id, "watch%?v=", "") -- Strip possible prefix. vid_id = string.sub(vid_id, 1, 11) -- Strip possible suffix. thumb_url = "https://i.ytimg.com/vi/" .. vid_id .. "/maxresdefault.jpg" local dl_process = mp.command_native({ name = "subprocess", playback_only = true, args = {"curl", "-L", "-s", "-o", shot_path, thumb_url}, }) if dl_process.status == 0 then yt_thumbnail = true return true end end yt_failed = true return false end return true end function media_type() fps = mp.get_property_native("estimated-vf-fps") if fps and fps > 1 then return "video" else return "music" end end function notify_metadata_updated() metadata = mp.get_property_native("metadata") debug_log(metadata) if not metadata then return end artist = get_metadata(metadata, { "artist", "ARTIST", "Artist" }) title = get_metadata(metadata, { "title", "TITLE", "Title", "icy-title" }) if media_type() == "music" and (not artist or artist == "" or not title or title == "") then chapter_metadata = mp.get_property_native("chapter-metadata") if chapter_metadata then chapter_artist = chapter_metadata["performer"] if not artist or artist == "" then artist = chapter_artist end chapter_title = chapter_metadata["title"] if not title or title == "" then title = chapter_title end end end if not title or title == "" then title = mp.get_property_native("media-title") end path = mp.get_property_native("path") if path:sub(2, 3) ~= ":\\" and path:sub(2, 3) ~= ":/" then dir = mp.get_property_native("working-directory") path = dir .. "\\" .. path end if not artist then artist = "" end if title then title = encode_element(title) end if artist then artist = encode_element(artist) end path = encode_element(path) local shot_dir = mp.command_native({ "expand-path", o.shot_path }) shot_path = shot_dir .. "\\" .. pid .. ".jpg" if mp.get_property("video-codec") then save_shot(shot_path) end message_content = "^[setFile](pid=" .. pid .. ")(title=" .. title .. ")(artist=" .. artist .. ")(path=" .. path .. ")(type=" .. media_type() .. ")(socket_name=" .. mpv_socket_name .. ")$" write_to_socket(message_content) end function play_state_changed() idle = mp.get_property_native("core-idle") is_playing = not idle message_content = "^[setState](pid=" .. pid .. ")(playing=" .. tostring(is_playing) .. ")(socket_name=" .. mpv_socket_name .. ")$" write_to_socket(message_content) if not idle then mp.add_timeout(10, play_state_changed) end end function notify_current_file() -- Even all things are right in MPVMediaControl, there may be native crash caused by Windows itself (SHCORE.dll). -- This line let mpv run the program again when a new file is loaded, help mitigating the problem. -- It won't cost much as MPVMC will not allow multiple instances to be started. But if you don't want this, comment out the line below. run_mpvmc_program() notify_metadata_updated() end function run_mpvmc_program() mp.command_native({ name = "subprocess", playback_only = false, capture_stdout = false, capture_stderr = false, detach = true, args = { o.binary_path }, }) end mp.set_property("options/input-ipc-server", "\\\\.\\pipe\\" .. mpv_socket_name) function start_register_event() if new_file then notify_current_file() start_of_file = true new_file = false yt_thumbnail = false yt_failed = false mp.observe_property("media-title", nil, notify_metadata_updated) mp.observe_property("metadata", nil, notify_metadata_updated) mp.observe_property("chapter", nil, notify_metadata_updated) mp.register_event("end-file", play_state_changed) mp.observe_property("core-idle", nil, play_state_changed) end end mp.register_event("file-loaded", function() new_file = true end) mp.register_event("playback-restart", start_register_event) function on_quit() if shot_path then os.remove(shot_path) end write_to_socket("^[setQuit](pid=" .. pid .. ")(quit=true)(socket_name=" .. mpv_socket_name .. ")$") if o.kill_mc then -- Force kill MPVMediaControl.exe mp.command_native({ name = "subprocess", playback_only = false, args = {"taskkill", "/F", "/IM", "MPVMediaControl.exe"}, }) end end mp.register_event("shutdown", on_quit)