MpcHcMediaPlayer.cs 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580
  1. using MediaBrowser.Model.Dto;
  2. using MediaBrowser.Model.Entities;
  3. using MediaBrowser.Model.Logging;
  4. using MediaBrowser.UI.Configuration;
  5. using MediaBrowser.UI.Controller;
  6. using MediaBrowser.UI.Playback;
  7. using MediaBrowser.UI.Playback.ExternalPlayer;
  8. using System;
  9. using System.Collections.Generic;
  10. using System.ComponentModel.Composition;
  11. using System.IO;
  12. using System.Linq;
  13. using System.Net.Http;
  14. using System.Threading;
  15. using System.Threading.Tasks;
  16. namespace MediaBrowser.Plugins.MpcHc
  17. {
  18. /// <summary>
  19. /// Class GenericExternalPlayer
  20. /// </summary>
  21. [Export(typeof(BaseMediaPlayer))]
  22. public class MpcHcMediaPlayer : BaseExternalPlayer
  23. {
  24. /// <summary>
  25. /// The state sync lock
  26. /// </summary>
  27. private object stateSyncLock = new object();
  28. /// <summary>
  29. /// The MPC HTTP interface resource pool
  30. /// </summary>
  31. private SemaphoreSlim MpcHttpInterfaceResourcePool = new SemaphoreSlim(1, 1);
  32. [ImportingConstructor]
  33. public MpcHcMediaPlayer([Import("logger")] ILogger logger)
  34. : base(logger)
  35. {
  36. }
  37. /// <summary>
  38. /// Gets or sets the HTTP interface cancellation token.
  39. /// </summary>
  40. /// <value>The HTTP interface cancellation token.</value>
  41. private CancellationTokenSource HttpInterfaceCancellationTokenSource { get; set; }
  42. /// <summary>
  43. /// Gets or sets a value indicating whether this instance has started playing.
  44. /// </summary>
  45. /// <value><c>true</c> if this instance has started playing; otherwise, <c>false</c>.</value>
  46. private bool HasStartedPlaying { get; set; }
  47. /// <summary>
  48. /// Gets or sets the status update timer.
  49. /// </summary>
  50. /// <value>The status update timer.</value>
  51. private Timer StatusUpdateTimer { get; set; }
  52. /// <summary>
  53. /// Gets a value indicating whether this instance can monitor progress.
  54. /// </summary>
  55. /// <value><c>true</c> if this instance can monitor progress; otherwise, <c>false</c>.</value>
  56. protected override bool CanMonitorProgress
  57. {
  58. get
  59. {
  60. return true;
  61. }
  62. }
  63. /// <summary>
  64. /// The _current position ticks
  65. /// </summary>
  66. private long? _currentPositionTicks;
  67. /// <summary>
  68. /// Gets the current position ticks.
  69. /// </summary>
  70. /// <value>The current position ticks.</value>
  71. public override long? CurrentPositionTicks
  72. {
  73. get
  74. {
  75. return _currentPositionTicks;
  76. }
  77. }
  78. /// <summary>
  79. /// The _current playlist index
  80. /// </summary>
  81. private int _currentPlaylistIndex;
  82. /// <summary>
  83. /// Gets the index of the current playlist.
  84. /// </summary>
  85. /// <value>The index of the current playlist.</value>
  86. public override int CurrentPlaylistIndex
  87. {
  88. get
  89. {
  90. return _currentPlaylistIndex;
  91. }
  92. }
  93. /// <summary>
  94. /// Gets the name.
  95. /// </summary>
  96. /// <value>The name.</value>
  97. public override string Name
  98. {
  99. get { return "MpcHc"; }
  100. }
  101. /// <summary>
  102. /// Gets a value indicating whether this instance can close automatically.
  103. /// </summary>
  104. /// <value><c>true</c> if this instance can close automatically; otherwise, <c>false</c>.</value>
  105. protected override bool CanCloseAutomatically
  106. {
  107. get
  108. {
  109. return true;
  110. }
  111. }
  112. /// <summary>
  113. /// Determines whether this instance can play the specified item.
  114. /// </summary>
  115. /// <param name="item">The item.</param>
  116. /// <returns><c>true</c> if this instance can play the specified item; otherwise, <c>false</c>.</returns>
  117. public override bool CanPlay(BaseItemDto item)
  118. {
  119. return item.IsVideo || item.IsAudio;
  120. }
  121. /// <summary>
  122. /// Gets the command arguments.
  123. /// </summary>
  124. /// <param name="items">The items.</param>
  125. /// <param name="options">The options.</param>
  126. /// <param name="playerConfiguration">The player configuration.</param>
  127. /// <returns>System.String.</returns>
  128. protected override string GetCommandArguments(List<BaseItemDto> items, PlayOptions options, PlayerConfiguration playerConfiguration)
  129. {
  130. var formatString = "{0} /play /fullscreen /close";
  131. var firstItem = items[0];
  132. var startTicks = Math.Max(options.StartPositionTicks, 0);
  133. if (startTicks > 0 && firstItem.IsVideo && firstItem.VideoType.HasValue && firstItem.VideoType.Value == VideoType.Dvd)
  134. {
  135. formatString += " /dvdpos 1#" + TimeSpan.FromTicks(startTicks).ToString("hh\\:mm\\:ss");
  136. }
  137. else
  138. {
  139. formatString += " /start " + TimeSpan.FromTicks(startTicks).TotalMilliseconds;
  140. }
  141. return GetCommandArguments(items, formatString);
  142. }
  143. /// <summary>
  144. /// Gets the path for command line.
  145. /// </summary>
  146. /// <param name="item">The item.</param>
  147. /// <returns>System.String.</returns>
  148. protected override string GetPathForCommandLine(BaseItemDto item)
  149. {
  150. var path = base.GetPathForCommandLine(item);
  151. if (item.IsVideo && item.VideoType.HasValue)
  152. {
  153. if (item.VideoType.Value == VideoType.Dvd)
  154. {
  155. // Point directly to the video_ts path
  156. // Otherwise mpc will play any other media files that might exist in the dvd top folder (e.g. video backdrops).
  157. var videoTsPath = Path.Combine(path, "video_ts");
  158. if (Directory.Exists(videoTsPath))
  159. {
  160. path = videoTsPath;
  161. }
  162. }
  163. if (item.VideoType.Value == VideoType.BluRay)
  164. {
  165. // Point directly to the bdmv path
  166. var bdmvPath = Path.Combine(path, "bdmv");
  167. if (Directory.Exists(bdmvPath))
  168. {
  169. path = bdmvPath;
  170. }
  171. }
  172. }
  173. return FormatPath(path);
  174. }
  175. /// <summary>
  176. /// Formats the path.
  177. /// </summary>
  178. /// <param name="path">The path.</param>
  179. /// <returns>System.String.</returns>
  180. private string FormatPath(string path)
  181. {
  182. if (path.EndsWith(":\\", StringComparison.OrdinalIgnoreCase))
  183. {
  184. path = path.TrimEnd('\\');
  185. }
  186. return path;
  187. }
  188. /// <summary>
  189. /// Called when [external player launched].
  190. /// </summary>
  191. protected override void OnExternalPlayerLaunched()
  192. {
  193. base.OnExternalPlayerLaunched();
  194. ReloadStatusUpdateTimer();
  195. }
  196. /// <summary>
  197. /// Reloads the status update timer.
  198. /// </summary>
  199. private void ReloadStatusUpdateTimer()
  200. {
  201. DisposeStatusTimer();
  202. HttpInterfaceCancellationTokenSource = new CancellationTokenSource();
  203. StatusUpdateTimer = new Timer(OnStatusUpdateTimerStopped, null, TimeSpan.FromSeconds(1), TimeSpan.FromSeconds(1));
  204. }
  205. /// <summary>
  206. /// Called when [status update timer stopped].
  207. /// </summary>
  208. /// <param name="state">The state.</param>
  209. private async void OnStatusUpdateTimerStopped(object state)
  210. {
  211. try
  212. {
  213. var token = HttpInterfaceCancellationTokenSource.Token;
  214. using (var stream = await UIKernel.Instance.HttpManager.Get(StatusUrl, MpcHttpInterfaceResourcePool, token).ConfigureAwait(false))
  215. {
  216. token.ThrowIfCancellationRequested();
  217. using (var reader = new StreamReader(stream))
  218. {
  219. token.ThrowIfCancellationRequested();
  220. var result = await reader.ReadToEndAsync().ConfigureAwait(false);
  221. token.ThrowIfCancellationRequested();
  222. ProcessStatusResult(result);
  223. }
  224. }
  225. }
  226. catch (HttpRequestException ex)
  227. {
  228. Logger.ErrorException("Error connecting to MpcHc status interface", ex);
  229. }
  230. catch (OperationCanceledException)
  231. {
  232. // Manually cancelled by us
  233. Logger.Info("Status request cancelled");
  234. }
  235. }
  236. /// <summary>
  237. /// Processes the status result.
  238. /// </summary>
  239. /// <param name="result">The result.</param>
  240. private async void ProcessStatusResult(string result)
  241. {
  242. // Sample result
  243. // OnStatus('test.avi', 'Playing', 5292, '00:00:05', 1203090, '00:20:03', 0, 100, 'C:\test.avi')
  244. // 5292 = position in ms
  245. // 00:00:05 = position
  246. // 1203090 = duration in ms
  247. // 00:20:03 = duration
  248. var quoteChar = result.IndexOf(", \"", StringComparison.OrdinalIgnoreCase) == -1 ? '\'' : '\"';
  249. // Strip off the leading "OnStatus(" and the trailing ")"
  250. result = result.Substring(result.IndexOf(quoteChar));
  251. result = result.Substring(0, result.LastIndexOf(quoteChar));
  252. // Strip off the filename at the beginning
  253. result = result.Substring(result.IndexOf(string.Format("{0}, {0}", quoteChar), StringComparison.OrdinalIgnoreCase) + 3);
  254. // Find the last index of ", '" so that we can extract and then strip off the file path at the end.
  255. var lastIndexOfSeparator = result.LastIndexOf(", " + quoteChar, StringComparison.OrdinalIgnoreCase);
  256. // Get the current playing file path
  257. var currentPlayingFile = result.Substring(lastIndexOfSeparator + 2).Trim(quoteChar);
  258. // Strip off the current playing file path
  259. result = result.Substring(0, lastIndexOfSeparator);
  260. var values = result.Split(',').Select(v => v.Trim().Trim(quoteChar)).ToList();
  261. var currentPositionTicks = TimeSpan.FromMilliseconds(double.Parse(values[1])).Ticks;
  262. //var currentDurationTicks = TimeSpan.FromMilliseconds(double.Parse(values[3])).Ticks;
  263. var playstate = values[0];
  264. var playlistIndex = GetPlaylistIndex(currentPlayingFile);
  265. if (playstate.Equals("stopped", StringComparison.OrdinalIgnoreCase))
  266. {
  267. if (HasStartedPlaying)
  268. {
  269. await ClosePlayer().ConfigureAwait(false);
  270. }
  271. }
  272. else
  273. {
  274. lock (stateSyncLock)
  275. {
  276. if (_currentPlaylistIndex != playlistIndex)
  277. {
  278. OnMediaChanged(_currentPlaylistIndex, _currentPositionTicks, playlistIndex);
  279. }
  280. _currentPositionTicks = currentPositionTicks;
  281. _currentPlaylistIndex = playlistIndex;
  282. }
  283. if (playstate.Equals("playing", StringComparison.OrdinalIgnoreCase))
  284. {
  285. HasStartedPlaying = true;
  286. PlayState = PlayState.Playing;
  287. }
  288. else if (playstate.Equals("paused", StringComparison.OrdinalIgnoreCase))
  289. {
  290. HasStartedPlaying = true;
  291. PlayState = PlayState.Paused;
  292. }
  293. }
  294. }
  295. /// <summary>
  296. /// Gets the index of the playlist.
  297. /// </summary>
  298. /// <param name="nowPlayingPath">The now playing path.</param>
  299. /// <returns>System.Int32.</returns>
  300. private int GetPlaylistIndex(string nowPlayingPath)
  301. {
  302. for (var i = 0; i < Playlist.Count; i++)
  303. {
  304. var item = Playlist[i];
  305. var pathArg = GetPathForCommandLine(item);
  306. if (pathArg.Equals(nowPlayingPath, StringComparison.OrdinalIgnoreCase))
  307. {
  308. return i;
  309. }
  310. if (item.VideoType.HasValue)
  311. {
  312. if (item.VideoType.Value == VideoType.BluRay || item.VideoType.Value == VideoType.Dvd || item.VideoType.Value == VideoType.HdDvd)
  313. {
  314. if (nowPlayingPath.StartsWith(pathArg, StringComparison.OrdinalIgnoreCase))
  315. {
  316. return i;
  317. }
  318. }
  319. }
  320. }
  321. return -1;
  322. }
  323. /// <summary>
  324. /// Called when [player stopped internal].
  325. /// </summary>
  326. protected override void OnPlayerStoppedInternal()
  327. {
  328. HttpInterfaceCancellationTokenSource.Cancel();
  329. DisposeStatusTimer();
  330. _currentPositionTicks = null;
  331. _currentPlaylistIndex = 0;
  332. HasStartedPlaying = false;
  333. HttpInterfaceCancellationTokenSource = null;
  334. base.OnPlayerStoppedInternal();
  335. }
  336. /// <summary>
  337. /// Disposes the status timer.
  338. /// </summary>
  339. private void DisposeStatusTimer()
  340. {
  341. if (StatusUpdateTimer != null)
  342. {
  343. StatusUpdateTimer.Dispose();
  344. }
  345. }
  346. /// <summary>
  347. /// Releases unmanaged and - optionally - managed resources.
  348. /// </summary>
  349. /// <param name="dispose"><c>true</c> to release both managed and unmanaged resources; <c>false</c> to release only unmanaged resources.</param>
  350. protected override void Dispose(bool dispose)
  351. {
  352. if (dispose)
  353. {
  354. DisposeStatusTimer();
  355. MpcHttpInterfaceResourcePool.Dispose();
  356. }
  357. base.Dispose(dispose);
  358. }
  359. /// <summary>
  360. /// Seeks the internal.
  361. /// </summary>
  362. /// <param name="positionTicks">The position ticks.</param>
  363. /// <returns>Task.</returns>
  364. protected override Task SeekInternal(long positionTicks)
  365. {
  366. var additionalParams = new Dictionary<string, string>();
  367. var time = TimeSpan.FromTicks(positionTicks);
  368. var timeString = time.Hours + ":" + time.Minutes + ":" + time.Seconds;
  369. additionalParams.Add("position", timeString);
  370. return SendCommandToPlayer("-1", additionalParams);
  371. }
  372. /// <summary>
  373. /// Pauses the internal.
  374. /// </summary>
  375. /// <returns>Task.</returns>
  376. protected override Task PauseInternal()
  377. {
  378. return SendCommandToPlayer("888", new Dictionary<string, string>());
  379. }
  380. /// <summary>
  381. /// Uns the pause internal.
  382. /// </summary>
  383. /// <returns>Task.</returns>
  384. protected override Task UnPauseInternal()
  385. {
  386. return SendCommandToPlayer("887", new Dictionary<string, string>());
  387. }
  388. /// <summary>
  389. /// Stops the internal.
  390. /// </summary>
  391. /// <returns>Task.</returns>
  392. protected override Task StopInternal()
  393. {
  394. return SendCommandToPlayer("890", new Dictionary<string, string>());
  395. }
  396. /// <summary>
  397. /// Closes the player.
  398. /// </summary>
  399. /// <returns>Task.</returns>
  400. protected Task ClosePlayer()
  401. {
  402. return SendCommandToPlayer("816", new Dictionary<string, string>());
  403. }
  404. /// <summary>
  405. /// Sends a command to MPC using the HTTP interface
  406. /// http://www.autohotkey.net/~specter333/MPC/HTTP%20Commands.txt
  407. /// </summary>
  408. /// <param name="commandNumber">The command number.</param>
  409. /// <param name="additionalParams">The additional params.</param>
  410. /// <returns>Task.</returns>
  411. /// <exception cref="System.ArgumentNullException">commandNumber</exception>
  412. private async Task SendCommandToPlayer(string commandNumber, Dictionary<string, string> additionalParams)
  413. {
  414. if (string.IsNullOrEmpty(commandNumber))
  415. {
  416. throw new ArgumentNullException("commandNumber");
  417. }
  418. if (additionalParams == null)
  419. {
  420. throw new ArgumentNullException("additionalParams");
  421. }
  422. var url = CommandUrl + "?wm_command=" + commandNumber;
  423. url = additionalParams.Keys.Aggregate(url, (current, name) => current + ("&" + name + "=" + additionalParams[name]));
  424. Logger.Info("Sending command to MPC: " + url);
  425. try
  426. {
  427. using (var stream = await UIKernel.Instance.HttpManager.Get(url, MpcHttpInterfaceResourcePool, HttpInterfaceCancellationTokenSource.Token).ConfigureAwait(false))
  428. {
  429. }
  430. }
  431. catch (HttpRequestException ex)
  432. {
  433. Logger.ErrorException("Error connecting to MpcHc command interface", ex);
  434. }
  435. catch (OperationCanceledException)
  436. {
  437. // Manually cancelled by us
  438. Logger.Info("Command request cancelled");
  439. }
  440. }
  441. /// <summary>
  442. /// Gets a value indicating whether this instance can pause.
  443. /// </summary>
  444. /// <value><c>true</c> if this instance can pause; otherwise, <c>false</c>.</value>
  445. public override bool CanPause
  446. {
  447. get
  448. {
  449. return true;
  450. }
  451. }
  452. /// <summary>
  453. /// Gets the server name that the http interface will be running on
  454. /// </summary>
  455. /// <value>The HTTP server.</value>
  456. private string HttpServer
  457. {
  458. get
  459. {
  460. return "localhost";
  461. }
  462. }
  463. /// <summary>
  464. /// Gets the port that the web interface will be running on
  465. /// </summary>
  466. /// <value>The HTTP port.</value>
  467. private string HttpPort
  468. {
  469. get
  470. {
  471. return "13579";
  472. }
  473. }
  474. /// <summary>
  475. /// Gets the url of that will be called to for status
  476. /// </summary>
  477. /// <value>The status URL.</value>
  478. private string StatusUrl
  479. {
  480. get
  481. {
  482. return "http://" + HttpServer + ":" + HttpPort + "/status.html";
  483. }
  484. }
  485. /// <summary>
  486. /// Gets the url of that will be called to send commands
  487. /// </summary>
  488. /// <value>The command URL.</value>
  489. private string CommandUrl
  490. {
  491. get
  492. {
  493. return "http://" + HttpServer + ":" + HttpPort + "/command.html";
  494. }
  495. }
  496. }
  497. }