mirror of
https://github.com/jellyfin/jellyfin.git
synced 2024-09-07 11:57:55 +02:00
574 lines
19 KiB
C#
574 lines
19 KiB
C#
using MediaBrowser.Common.Logging;
|
|
using MediaBrowser.Model.DTO;
|
|
using MediaBrowser.Model.Entities;
|
|
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
|
|
{
|
|
/// <summary>
|
|
/// Class GenericExternalPlayer
|
|
/// </summary>
|
|
[Export(typeof(BaseMediaPlayer))]
|
|
public class MpcHcMediaPlayer : BaseExternalPlayer
|
|
{
|
|
/// <summary>
|
|
/// The state sync lock
|
|
/// </summary>
|
|
private object stateSyncLock = new object();
|
|
|
|
/// <summary>
|
|
/// The MPC HTTP interface resource pool
|
|
/// </summary>
|
|
private SemaphoreSlim MpcHttpInterfaceResourcePool = new SemaphoreSlim(1, 1);
|
|
|
|
/// <summary>
|
|
/// Gets or sets the HTTP interface cancellation token.
|
|
/// </summary>
|
|
/// <value>The HTTP interface cancellation token.</value>
|
|
private CancellationTokenSource HttpInterfaceCancellationTokenSource { get; set; }
|
|
|
|
/// <summary>
|
|
/// Gets or sets a value indicating whether this instance has started playing.
|
|
/// </summary>
|
|
/// <value><c>true</c> if this instance has started playing; otherwise, <c>false</c>.</value>
|
|
private bool HasStartedPlaying { get; set; }
|
|
|
|
/// <summary>
|
|
/// Gets or sets the status update timer.
|
|
/// </summary>
|
|
/// <value>The status update timer.</value>
|
|
private Timer StatusUpdateTimer { get; set; }
|
|
|
|
/// <summary>
|
|
/// Gets a value indicating whether this instance can monitor progress.
|
|
/// </summary>
|
|
/// <value><c>true</c> if this instance can monitor progress; otherwise, <c>false</c>.</value>
|
|
protected override bool CanMonitorProgress
|
|
{
|
|
get
|
|
{
|
|
return true;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// The _current position ticks
|
|
/// </summary>
|
|
private long? _currentPositionTicks;
|
|
|
|
/// <summary>
|
|
/// Gets the current position ticks.
|
|
/// </summary>
|
|
/// <value>The current position ticks.</value>
|
|
public override long? CurrentPositionTicks
|
|
{
|
|
get
|
|
{
|
|
return _currentPositionTicks;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// The _current playlist index
|
|
/// </summary>
|
|
private int _currentPlaylistIndex;
|
|
|
|
/// <summary>
|
|
/// Gets the index of the current playlist.
|
|
/// </summary>
|
|
/// <value>The index of the current playlist.</value>
|
|
public override int CurrentPlaylistIndex
|
|
{
|
|
get
|
|
{
|
|
return _currentPlaylistIndex;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets the name.
|
|
/// </summary>
|
|
/// <value>The name.</value>
|
|
public override string Name
|
|
{
|
|
get { return "MpcHc"; }
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets a value indicating whether this instance can close automatically.
|
|
/// </summary>
|
|
/// <value><c>true</c> if this instance can close automatically; otherwise, <c>false</c>.</value>
|
|
protected override bool CanCloseAutomatically
|
|
{
|
|
get
|
|
{
|
|
return true;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Determines whether this instance can play the specified item.
|
|
/// </summary>
|
|
/// <param name="item">The item.</param>
|
|
/// <returns><c>true</c> if this instance can play the specified item; otherwise, <c>false</c>.</returns>
|
|
public override bool CanPlay(DtoBaseItem item)
|
|
{
|
|
return item.IsVideo || item.IsAudio;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets the command arguments.
|
|
/// </summary>
|
|
/// <param name="items">The items.</param>
|
|
/// <param name="options">The options.</param>
|
|
/// <param name="playerConfiguration">The player configuration.</param>
|
|
/// <returns>System.String.</returns>
|
|
protected override string GetCommandArguments(List<DtoBaseItem> 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);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets the path for command line.
|
|
/// </summary>
|
|
/// <param name="item">The item.</param>
|
|
/// <returns>System.String.</returns>
|
|
protected override string GetPathForCommandLine(DtoBaseItem 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);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Formats the path.
|
|
/// </summary>
|
|
/// <param name="path">The path.</param>
|
|
/// <returns>System.String.</returns>
|
|
private string FormatPath(string path)
|
|
{
|
|
if (path.EndsWith(":\\", StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
path = path.TrimEnd('\\');
|
|
}
|
|
|
|
return path;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Called when [external player launched].
|
|
/// </summary>
|
|
protected override void OnExternalPlayerLaunched()
|
|
{
|
|
base.OnExternalPlayerLaunched();
|
|
|
|
ReloadStatusUpdateTimer();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Reloads the status update timer.
|
|
/// </summary>
|
|
private void ReloadStatusUpdateTimer()
|
|
{
|
|
DisposeStatusTimer();
|
|
|
|
HttpInterfaceCancellationTokenSource = new CancellationTokenSource();
|
|
|
|
StatusUpdateTimer = new Timer(OnStatusUpdateTimerStopped, null, TimeSpan.FromSeconds(1), TimeSpan.FromSeconds(1));
|
|
}
|
|
|
|
/// <summary>
|
|
/// Called when [status update timer stopped].
|
|
/// </summary>
|
|
/// <param name="state">The state.</param>
|
|
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");
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Processes the status result.
|
|
/// </summary>
|
|
/// <param name="result">The result.</param>
|
|
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;
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets the index of the playlist.
|
|
/// </summary>
|
|
/// <param name="nowPlayingPath">The now playing path.</param>
|
|
/// <returns>System.Int32.</returns>
|
|
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;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Called when [player stopped internal].
|
|
/// </summary>
|
|
protected override void OnPlayerStoppedInternal()
|
|
{
|
|
HttpInterfaceCancellationTokenSource.Cancel();
|
|
|
|
DisposeStatusTimer();
|
|
_currentPositionTicks = null;
|
|
_currentPlaylistIndex = 0;
|
|
HasStartedPlaying = false;
|
|
HttpInterfaceCancellationTokenSource = null;
|
|
|
|
base.OnPlayerStoppedInternal();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Disposes the status timer.
|
|
/// </summary>
|
|
private void DisposeStatusTimer()
|
|
{
|
|
if (StatusUpdateTimer != null)
|
|
{
|
|
StatusUpdateTimer.Dispose();
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Releases unmanaged and - optionally - managed resources.
|
|
/// </summary>
|
|
/// <param name="dispose"><c>true</c> to release both managed and unmanaged resources; <c>false</c> to release only unmanaged resources.</param>
|
|
protected override void Dispose(bool dispose)
|
|
{
|
|
if (dispose)
|
|
{
|
|
DisposeStatusTimer();
|
|
|
|
MpcHttpInterfaceResourcePool.Dispose();
|
|
}
|
|
|
|
base.Dispose(dispose);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Seeks the internal.
|
|
/// </summary>
|
|
/// <param name="positionTicks">The position ticks.</param>
|
|
/// <returns>Task.</returns>
|
|
protected override Task SeekInternal(long positionTicks)
|
|
{
|
|
var additionalParams = new Dictionary<string, string>();
|
|
|
|
var time = TimeSpan.FromTicks(positionTicks);
|
|
|
|
var timeString = time.Hours + ":" + time.Minutes + ":" + time.Seconds;
|
|
|
|
additionalParams.Add("position", timeString);
|
|
|
|
return SendCommandToPlayer("-1", additionalParams);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Pauses the internal.
|
|
/// </summary>
|
|
/// <returns>Task.</returns>
|
|
protected override Task PauseInternal()
|
|
{
|
|
return SendCommandToPlayer("888", new Dictionary<string, string>());
|
|
}
|
|
|
|
/// <summary>
|
|
/// Uns the pause internal.
|
|
/// </summary>
|
|
/// <returns>Task.</returns>
|
|
protected override Task UnPauseInternal()
|
|
{
|
|
return SendCommandToPlayer("887", new Dictionary<string, string>());
|
|
}
|
|
|
|
/// <summary>
|
|
/// Stops the internal.
|
|
/// </summary>
|
|
/// <returns>Task.</returns>
|
|
protected override Task StopInternal()
|
|
{
|
|
return SendCommandToPlayer("890", new Dictionary<string, string>());
|
|
}
|
|
|
|
/// <summary>
|
|
/// Closes the player.
|
|
/// </summary>
|
|
/// <returns>Task.</returns>
|
|
protected Task ClosePlayer()
|
|
{
|
|
return SendCommandToPlayer("816", new Dictionary<string, string>());
|
|
}
|
|
|
|
/// <summary>
|
|
/// Sends a command to MPC using the HTTP interface
|
|
/// http://www.autohotkey.net/~specter333/MPC/HTTP%20Commands.txt
|
|
/// </summary>
|
|
/// <param name="commandNumber">The command number.</param>
|
|
/// <param name="additionalParams">The additional params.</param>
|
|
/// <returns>Task.</returns>
|
|
/// <exception cref="System.ArgumentNullException">commandNumber</exception>
|
|
private async Task SendCommandToPlayer(string commandNumber, Dictionary<string, string> 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");
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets a value indicating whether this instance can pause.
|
|
/// </summary>
|
|
/// <value><c>true</c> if this instance can pause; otherwise, <c>false</c>.</value>
|
|
public override bool CanPause
|
|
{
|
|
get
|
|
{
|
|
return true;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets the server name that the http interface will be running on
|
|
/// </summary>
|
|
/// <value>The HTTP server.</value>
|
|
private string HttpServer
|
|
{
|
|
get
|
|
{
|
|
return "localhost";
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets the port that the web interface will be running on
|
|
/// </summary>
|
|
/// <value>The HTTP port.</value>
|
|
private string HttpPort
|
|
{
|
|
get
|
|
{
|
|
return "13579";
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets the url of that will be called to for status
|
|
/// </summary>
|
|
/// <value>The status URL.</value>
|
|
private string StatusUrl
|
|
{
|
|
get
|
|
{
|
|
return "http://" + HttpServer + ":" + HttpPort + "/status.html";
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets the url of that will be called to send commands
|
|
/// </summary>
|
|
/// <value>The command URL.</value>
|
|
private string CommandUrl
|
|
{
|
|
get
|
|
{
|
|
return "http://" + HttpServer + ":" + HttpPort + "/command.html";
|
|
}
|
|
}
|
|
}
|
|
}
|