using MediaBrowser.Model.Dto; using MediaBrowser.Model.Entities; using MediaBrowser.Model.Logging; using MediaBrowser.UI.Configuration; using MediaBrowser.UI.Controller; using MediaBrowser.UI.Playback; using MediaBrowser.UI.Playback.ExternalPlayer; using System; using System.Collections.Generic; using System.ComponentModel.Composition; using System.IO; using System.Linq; using System.Net.Http; using System.Threading; using System.Threading.Tasks; namespace MediaBrowser.Plugins.MpcHc { /// /// Class GenericExternalPlayer /// [Export(typeof(BaseMediaPlayer))] public class MpcHcMediaPlayer : BaseExternalPlayer { /// /// The state sync lock /// private object stateSyncLock = new object(); /// /// The MPC HTTP interface resource pool /// private SemaphoreSlim MpcHttpInterfaceResourcePool = new SemaphoreSlim(1, 1); [ImportingConstructor] public MpcHcMediaPlayer([Import("logger")] ILogger logger) : base(logger) { } /// /// Gets or sets the HTTP interface cancellation token. /// /// The HTTP interface cancellation token. private CancellationTokenSource HttpInterfaceCancellationTokenSource { get; set; } /// /// Gets or sets a value indicating whether this instance has started playing. /// /// true if this instance has started playing; otherwise, false. private bool HasStartedPlaying { get; set; } /// /// Gets or sets the status update timer. /// /// The status update timer. private Timer StatusUpdateTimer { get; set; } /// /// Gets a value indicating whether this instance can monitor progress. /// /// true if this instance can monitor progress; otherwise, false. protected override bool CanMonitorProgress { get { return true; } } /// /// The _current position ticks /// private long? _currentPositionTicks; /// /// Gets the current position ticks. /// /// The current position ticks. public override long? CurrentPositionTicks { get { return _currentPositionTicks; } } /// /// The _current playlist index /// private int _currentPlaylistIndex; /// /// Gets the index of the current playlist. /// /// The index of the current playlist. public override int CurrentPlaylistIndex { get { return _currentPlaylistIndex; } } /// /// Gets the name. /// /// The name. public override string Name { get { return "MpcHc"; } } /// /// Gets a value indicating whether this instance can close automatically. /// /// true if this instance can close automatically; otherwise, false. protected override bool CanCloseAutomatically { get { return true; } } /// /// Determines whether this instance can play the specified item. /// /// The item. /// true if this instance can play the specified item; otherwise, false. public override bool CanPlay(BaseItemDto item) { return item.IsVideo || item.IsAudio; } /// /// Gets the command arguments. /// /// The items. /// The options. /// The player configuration. /// System.String. protected override string GetCommandArguments(List items, PlayOptions options, PlayerConfiguration playerConfiguration) { var formatString = "{0} /play /fullscreen /close"; var firstItem = items[0]; var startTicks = Math.Max(options.StartPositionTicks, 0); if (startTicks > 0 && firstItem.IsVideo && firstItem.VideoType.HasValue && firstItem.VideoType.Value == VideoType.Dvd) { formatString += " /dvdpos 1#" + TimeSpan.FromTicks(startTicks).ToString("hh\\:mm\\:ss"); } else { formatString += " /start " + TimeSpan.FromTicks(startTicks).TotalMilliseconds; } return GetCommandArguments(items, formatString); } /// /// Gets the path for command line. /// /// The item. /// System.String. protected override string GetPathForCommandLine(BaseItemDto item) { var path = base.GetPathForCommandLine(item); if (item.IsVideo && item.VideoType.HasValue) { if (item.VideoType.Value == VideoType.Dvd) { // Point directly to the video_ts path // Otherwise mpc will play any other media files that might exist in the dvd top folder (e.g. video backdrops). var videoTsPath = Path.Combine(path, "video_ts"); if (Directory.Exists(videoTsPath)) { path = videoTsPath; } } if (item.VideoType.Value == VideoType.BluRay) { // Point directly to the bdmv path var bdmvPath = Path.Combine(path, "bdmv"); if (Directory.Exists(bdmvPath)) { path = bdmvPath; } } } return FormatPath(path); } /// /// Formats the path. /// /// The path. /// System.String. private string FormatPath(string path) { if (path.EndsWith(":\\", StringComparison.OrdinalIgnoreCase)) { path = path.TrimEnd('\\'); } return path; } /// /// Called when [external player launched]. /// protected override void OnExternalPlayerLaunched() { base.OnExternalPlayerLaunched(); ReloadStatusUpdateTimer(); } /// /// Reloads the status update timer. /// private void ReloadStatusUpdateTimer() { DisposeStatusTimer(); HttpInterfaceCancellationTokenSource = new CancellationTokenSource(); StatusUpdateTimer = new Timer(OnStatusUpdateTimerStopped, null, TimeSpan.FromSeconds(1), TimeSpan.FromSeconds(1)); } /// /// Called when [status update timer stopped]. /// /// The state. private async void OnStatusUpdateTimerStopped(object state) { try { var token = HttpInterfaceCancellationTokenSource.Token; using (var stream = await UIKernel.Instance.HttpManager.Get(StatusUrl, MpcHttpInterfaceResourcePool, token).ConfigureAwait(false)) { token.ThrowIfCancellationRequested(); using (var reader = new StreamReader(stream)) { token.ThrowIfCancellationRequested(); var result = await reader.ReadToEndAsync().ConfigureAwait(false); token.ThrowIfCancellationRequested(); ProcessStatusResult(result); } } } catch (HttpRequestException ex) { Logger.ErrorException("Error connecting to MpcHc status interface", ex); } catch (OperationCanceledException) { // Manually cancelled by us Logger.Info("Status request cancelled"); } } /// /// Processes the status result. /// /// The result. private async void ProcessStatusResult(string result) { // Sample result // OnStatus('test.avi', 'Playing', 5292, '00:00:05', 1203090, '00:20:03', 0, 100, 'C:\test.avi') // 5292 = position in ms // 00:00:05 = position // 1203090 = duration in ms // 00:20:03 = duration var quoteChar = result.IndexOf(", \"", StringComparison.OrdinalIgnoreCase) == -1 ? '\'' : '\"'; // Strip off the leading "OnStatus(" and the trailing ")" result = result.Substring(result.IndexOf(quoteChar)); result = result.Substring(0, result.LastIndexOf(quoteChar)); // Strip off the filename at the beginning result = result.Substring(result.IndexOf(string.Format("{0}, {0}", quoteChar), StringComparison.OrdinalIgnoreCase) + 3); // Find the last index of ", '" so that we can extract and then strip off the file path at the end. var lastIndexOfSeparator = result.LastIndexOf(", " + quoteChar, StringComparison.OrdinalIgnoreCase); // Get the current playing file path var currentPlayingFile = result.Substring(lastIndexOfSeparator + 2).Trim(quoteChar); // Strip off the current playing file path result = result.Substring(0, lastIndexOfSeparator); var values = result.Split(',').Select(v => v.Trim().Trim(quoteChar)).ToList(); var currentPositionTicks = TimeSpan.FromMilliseconds(double.Parse(values[1])).Ticks; //var currentDurationTicks = TimeSpan.FromMilliseconds(double.Parse(values[3])).Ticks; var playstate = values[0]; var playlistIndex = GetPlaylistIndex(currentPlayingFile); if (playstate.Equals("stopped", StringComparison.OrdinalIgnoreCase)) { if (HasStartedPlaying) { await ClosePlayer().ConfigureAwait(false); } } else { lock (stateSyncLock) { if (_currentPlaylistIndex != playlistIndex) { OnMediaChanged(_currentPlaylistIndex, _currentPositionTicks, playlistIndex); } _currentPositionTicks = currentPositionTicks; _currentPlaylistIndex = playlistIndex; } if (playstate.Equals("playing", StringComparison.OrdinalIgnoreCase)) { HasStartedPlaying = true; PlayState = PlayState.Playing; } else if (playstate.Equals("paused", StringComparison.OrdinalIgnoreCase)) { HasStartedPlaying = true; PlayState = PlayState.Paused; } } } /// /// Gets the index of the playlist. /// /// The now playing path. /// System.Int32. private int GetPlaylistIndex(string nowPlayingPath) { for (var i = 0; i < Playlist.Count; i++) { var item = Playlist[i]; var pathArg = GetPathForCommandLine(item); if (pathArg.Equals(nowPlayingPath, StringComparison.OrdinalIgnoreCase)) { return i; } if (item.VideoType.HasValue) { if (item.VideoType.Value == VideoType.BluRay || item.VideoType.Value == VideoType.Dvd || item.VideoType.Value == VideoType.HdDvd) { if (nowPlayingPath.StartsWith(pathArg, StringComparison.OrdinalIgnoreCase)) { return i; } } } } return -1; } /// /// Called when [player stopped internal]. /// protected override void OnPlayerStoppedInternal() { HttpInterfaceCancellationTokenSource.Cancel(); DisposeStatusTimer(); _currentPositionTicks = null; _currentPlaylistIndex = 0; HasStartedPlaying = false; HttpInterfaceCancellationTokenSource = null; base.OnPlayerStoppedInternal(); } /// /// Disposes the status timer. /// private void DisposeStatusTimer() { if (StatusUpdateTimer != null) { StatusUpdateTimer.Dispose(); } } /// /// Releases unmanaged and - optionally - managed resources. /// /// true to release both managed and unmanaged resources; false to release only unmanaged resources. protected override void Dispose(bool dispose) { if (dispose) { DisposeStatusTimer(); MpcHttpInterfaceResourcePool.Dispose(); } base.Dispose(dispose); } /// /// Seeks the internal. /// /// The position ticks. /// Task. protected override Task SeekInternal(long positionTicks) { var additionalParams = new Dictionary(); var time = TimeSpan.FromTicks(positionTicks); var timeString = time.Hours + ":" + time.Minutes + ":" + time.Seconds; additionalParams.Add("position", timeString); return SendCommandToPlayer("-1", additionalParams); } /// /// Pauses the internal. /// /// Task. protected override Task PauseInternal() { return SendCommandToPlayer("888", new Dictionary()); } /// /// Uns the pause internal. /// /// Task. protected override Task UnPauseInternal() { return SendCommandToPlayer("887", new Dictionary()); } /// /// Stops the internal. /// /// Task. protected override Task StopInternal() { return SendCommandToPlayer("890", new Dictionary()); } /// /// Closes the player. /// /// Task. protected Task ClosePlayer() { return SendCommandToPlayer("816", new Dictionary()); } /// /// Sends a command to MPC using the HTTP interface /// http://www.autohotkey.net/~specter333/MPC/HTTP%20Commands.txt /// /// The command number. /// The additional params. /// Task. /// commandNumber private async Task SendCommandToPlayer(string commandNumber, Dictionary additionalParams) { if (string.IsNullOrEmpty(commandNumber)) { throw new ArgumentNullException("commandNumber"); } if (additionalParams == null) { throw new ArgumentNullException("additionalParams"); } var url = CommandUrl + "?wm_command=" + commandNumber; url = additionalParams.Keys.Aggregate(url, (current, name) => current + ("&" + name + "=" + additionalParams[name])); Logger.Info("Sending command to MPC: " + url); try { using (var stream = await UIKernel.Instance.HttpManager.Get(url, MpcHttpInterfaceResourcePool, HttpInterfaceCancellationTokenSource.Token).ConfigureAwait(false)) { } } catch (HttpRequestException ex) { Logger.ErrorException("Error connecting to MpcHc command interface", ex); } catch (OperationCanceledException) { // Manually cancelled by us Logger.Info("Command request cancelled"); } } /// /// Gets a value indicating whether this instance can pause. /// /// true if this instance can pause; otherwise, false. public override bool CanPause { get { return true; } } /// /// Gets the server name that the http interface will be running on /// /// The HTTP server. private string HttpServer { get { return "localhost"; } } /// /// Gets the port that the web interface will be running on /// /// The HTTP port. private string HttpPort { get { return "13579"; } } /// /// Gets the url of that will be called to for status /// /// The status URL. private string StatusUrl { get { return "http://" + HttpServer + ":" + HttpPort + "/status.html"; } } /// /// Gets the url of that will be called to send commands /// /// The command URL. private string CommandUrl { get { return "http://" + HttpServer + ":" + HttpPort + "/command.html"; } } } }