ApiEntryPoint.cs 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691
  1. using System;
  2. using System.Collections.Generic;
  3. using System.Diagnostics;
  4. using System.IO;
  5. using System.Linq;
  6. using System.Threading;
  7. using System.Threading.Tasks;
  8. using MediaBrowser.Api.Playback;
  9. using MediaBrowser.Common.Configuration;
  10. using MediaBrowser.Controller.Configuration;
  11. using MediaBrowser.Controller.Library;
  12. using MediaBrowser.Controller.MediaEncoding;
  13. using MediaBrowser.Controller.Plugins;
  14. using MediaBrowser.Controller.Session;
  15. using MediaBrowser.Model.IO;
  16. using MediaBrowser.Model.Session;
  17. using Microsoft.Extensions.Logging;
  18. namespace MediaBrowser.Api
  19. {
  20. /// <summary>
  21. /// Class ServerEntryPoint.
  22. /// </summary>
  23. public class ApiEntryPoint : IServerEntryPoint
  24. {
  25. /// <summary>
  26. /// The instance.
  27. /// </summary>
  28. public static ApiEntryPoint Instance;
  29. /// <summary>
  30. /// The logger.
  31. /// </summary>
  32. private ILogger _logger;
  33. /// <summary>
  34. /// The configuration manager.
  35. /// </summary>
  36. private IServerConfigurationManager _serverConfigurationManager;
  37. private readonly ISessionManager _sessionManager;
  38. private readonly IFileSystem _fileSystem;
  39. private readonly IMediaSourceManager _mediaSourceManager;
  40. /// <summary>
  41. /// The active transcoding jobs
  42. /// </summary>
  43. private readonly List<TranscodingJob> _activeTranscodingJobs = new List<TranscodingJob>();
  44. private readonly Dictionary<string, SemaphoreSlim> _transcodingLocks =
  45. new Dictionary<string, SemaphoreSlim>();
  46. private bool _disposed = false;
  47. /// <summary>
  48. /// Initializes a new instance of the <see cref="ApiEntryPoint" /> class.
  49. /// </summary>
  50. /// <param name="logger">The logger.</param>
  51. /// <param name="sessionManager">The session manager.</param>
  52. /// <param name="config">The configuration.</param>
  53. /// <param name="fileSystem">The file system.</param>
  54. /// <param name="mediaSourceManager">The media source manager.</param>
  55. public ApiEntryPoint(
  56. ILogger<ApiEntryPoint> logger,
  57. ISessionManager sessionManager,
  58. IServerConfigurationManager config,
  59. IFileSystem fileSystem,
  60. IMediaSourceManager mediaSourceManager)
  61. {
  62. _logger = logger;
  63. _sessionManager = sessionManager;
  64. _serverConfigurationManager = config;
  65. _fileSystem = fileSystem;
  66. _mediaSourceManager = mediaSourceManager;
  67. _sessionManager.PlaybackProgress += OnPlaybackProgress;
  68. _sessionManager.PlaybackStart += OnPlaybackStart;
  69. Instance = this;
  70. }
  71. public static string[] Split(string value, char separator, bool removeEmpty)
  72. {
  73. if (string.IsNullOrWhiteSpace(value))
  74. {
  75. return Array.Empty<string>();
  76. }
  77. if (removeEmpty)
  78. {
  79. return value.Split(new[] { separator }, StringSplitOptions.RemoveEmptyEntries);
  80. }
  81. return value.Split(separator);
  82. }
  83. public SemaphoreSlim GetTranscodingLock(string outputPath)
  84. {
  85. lock (_transcodingLocks)
  86. {
  87. if (!_transcodingLocks.TryGetValue(outputPath, out SemaphoreSlim result))
  88. {
  89. result = new SemaphoreSlim(1, 1);
  90. _transcodingLocks[outputPath] = result;
  91. }
  92. return result;
  93. }
  94. }
  95. private void OnPlaybackStart(object sender, PlaybackProgressEventArgs e)
  96. {
  97. if (!string.IsNullOrWhiteSpace(e.PlaySessionId))
  98. {
  99. PingTranscodingJob(e.PlaySessionId, e.IsPaused);
  100. }
  101. }
  102. private void OnPlaybackProgress(object sender, PlaybackProgressEventArgs e)
  103. {
  104. if (!string.IsNullOrWhiteSpace(e.PlaySessionId))
  105. {
  106. PingTranscodingJob(e.PlaySessionId, e.IsPaused);
  107. }
  108. }
  109. /// <summary>
  110. /// Runs this instance.
  111. /// </summary>
  112. public Task RunAsync()
  113. {
  114. try
  115. {
  116. DeleteEncodedMediaCache();
  117. }
  118. catch (Exception ex)
  119. {
  120. _logger.LogError(ex, "Error deleting encoded media cache");
  121. }
  122. return Task.CompletedTask;
  123. }
  124. /// <summary>
  125. /// Deletes the encoded media cache.
  126. /// </summary>
  127. private void DeleteEncodedMediaCache()
  128. {
  129. var path = _serverConfigurationManager.GetTranscodePath();
  130. if (!Directory.Exists(path))
  131. {
  132. return;
  133. }
  134. foreach (var file in _fileSystem.GetFilePaths(path, true))
  135. {
  136. _fileSystem.DeleteFile(file);
  137. }
  138. }
  139. /// <inheritdoc />
  140. public void Dispose()
  141. {
  142. Dispose(true);
  143. GC.SuppressFinalize(this);
  144. }
  145. /// <summary>
  146. /// Releases unmanaged and - optionally - managed resources.
  147. /// </summary>
  148. /// <param name="dispose"><c>true</c> to release both managed and unmanaged resources; <c>false</c> to release only unmanaged resources.</param>
  149. protected virtual void Dispose(bool dispose)
  150. {
  151. if (_disposed)
  152. {
  153. return;
  154. }
  155. if (dispose)
  156. {
  157. // TODO: dispose
  158. }
  159. var jobs = _activeTranscodingJobs.ToList();
  160. var jobCount = jobs.Count;
  161. IEnumerable<Task> GetKillJobs()
  162. {
  163. foreach (var job in jobs)
  164. {
  165. yield return KillTranscodingJob(job, false, path => true);
  166. }
  167. }
  168. // Wait for all processes to be killed
  169. if (jobCount > 0)
  170. {
  171. Task.WaitAll(GetKillJobs().ToArray());
  172. }
  173. _activeTranscodingJobs.Clear();
  174. _transcodingLocks.Clear();
  175. _sessionManager.PlaybackProgress -= OnPlaybackProgress;
  176. _sessionManager.PlaybackStart -= OnPlaybackStart;
  177. _disposed = true;
  178. }
  179. /// <summary>
  180. /// Called when [transcode beginning].
  181. /// </summary>
  182. /// <param name="path">The path.</param>
  183. /// <param name="playSessionId">The play session identifier.</param>
  184. /// <param name="liveStreamId">The live stream identifier.</param>
  185. /// <param name="transcodingJobId">The transcoding job identifier.</param>
  186. /// <param name="type">The type.</param>
  187. /// <param name="process">The process.</param>
  188. /// <param name="deviceId">The device id.</param>
  189. /// <param name="state">The state.</param>
  190. /// <param name="cancellationTokenSource">The cancellation token source.</param>
  191. /// <returns>TranscodingJob.</returns>
  192. public TranscodingJob OnTranscodeBeginning(
  193. string path,
  194. string playSessionId,
  195. string liveStreamId,
  196. string transcodingJobId,
  197. TranscodingJobType type,
  198. Process process,
  199. string deviceId,
  200. StreamState state,
  201. CancellationTokenSource cancellationTokenSource)
  202. {
  203. lock (_activeTranscodingJobs)
  204. {
  205. var job = new TranscodingJob(_logger)
  206. {
  207. Type = type,
  208. Path = path,
  209. Process = process,
  210. ActiveRequestCount = 1,
  211. DeviceId = deviceId,
  212. CancellationTokenSource = cancellationTokenSource,
  213. Id = transcodingJobId,
  214. PlaySessionId = playSessionId,
  215. LiveStreamId = liveStreamId,
  216. MediaSource = state.MediaSource
  217. };
  218. _activeTranscodingJobs.Add(job);
  219. ReportTranscodingProgress(job, state, null, null, null, null, null);
  220. return job;
  221. }
  222. }
  223. public void ReportTranscodingProgress(TranscodingJob job, StreamState state, TimeSpan? transcodingPosition, float? framerate, double? percentComplete, long? bytesTranscoded, int? bitRate)
  224. {
  225. var ticks = transcodingPosition.HasValue ? transcodingPosition.Value.Ticks : (long?)null;
  226. if (job != null)
  227. {
  228. job.Framerate = framerate;
  229. job.CompletionPercentage = percentComplete;
  230. job.TranscodingPositionTicks = ticks;
  231. job.BytesTranscoded = bytesTranscoded;
  232. job.BitRate = bitRate;
  233. }
  234. var deviceId = state.Request.DeviceId;
  235. if (!string.IsNullOrWhiteSpace(deviceId))
  236. {
  237. var audioCodec = state.ActualOutputAudioCodec;
  238. var videoCodec = state.ActualOutputVideoCodec;
  239. _sessionManager.ReportTranscodingInfo(deviceId, new TranscodingInfo
  240. {
  241. Bitrate = bitRate ?? state.TotalOutputBitrate,
  242. AudioCodec = audioCodec,
  243. VideoCodec = videoCodec,
  244. Container = state.OutputContainer,
  245. Framerate = framerate,
  246. CompletionPercentage = percentComplete,
  247. Width = state.OutputWidth,
  248. Height = state.OutputHeight,
  249. AudioChannels = state.OutputAudioChannels,
  250. IsAudioDirect = string.Equals(state.OutputAudioCodec, "copy", StringComparison.OrdinalIgnoreCase),
  251. IsVideoDirect = string.Equals(state.OutputVideoCodec, "copy", StringComparison.OrdinalIgnoreCase),
  252. TranscodeReasons = state.TranscodeReasons
  253. });
  254. }
  255. }
  256. /// <summary>
  257. /// <summary>
  258. /// The progressive
  259. /// </summary>
  260. /// Called when [transcode failed to start].
  261. /// </summary>
  262. /// <param name="path">The path.</param>
  263. /// <param name="type">The type.</param>
  264. /// <param name="state">The state.</param>
  265. public void OnTranscodeFailedToStart(string path, TranscodingJobType type, StreamState state)
  266. {
  267. lock (_activeTranscodingJobs)
  268. {
  269. var job = _activeTranscodingJobs.FirstOrDefault(j => j.Type == type && string.Equals(j.Path, path, StringComparison.OrdinalIgnoreCase));
  270. if (job != null)
  271. {
  272. _activeTranscodingJobs.Remove(job);
  273. }
  274. }
  275. lock (_transcodingLocks)
  276. {
  277. _transcodingLocks.Remove(path);
  278. }
  279. if (!string.IsNullOrWhiteSpace(state.Request.DeviceId))
  280. {
  281. _sessionManager.ClearTranscodingInfo(state.Request.DeviceId);
  282. }
  283. }
  284. /// <summary>
  285. /// Determines whether [has active transcoding job] [the specified path].
  286. /// </summary>
  287. /// <param name="path">The path.</param>
  288. /// <param name="type">The type.</param>
  289. /// <returns><c>true</c> if [has active transcoding job] [the specified path]; otherwise, <c>false</c>.</returns>
  290. public bool HasActiveTranscodingJob(string path, TranscodingJobType type)
  291. {
  292. return GetTranscodingJob(path, type) != null;
  293. }
  294. public TranscodingJob GetTranscodingJob(string path, TranscodingJobType type)
  295. {
  296. lock (_activeTranscodingJobs)
  297. {
  298. return _activeTranscodingJobs.FirstOrDefault(j => j.Type == type && string.Equals(j.Path, path, StringComparison.OrdinalIgnoreCase));
  299. }
  300. }
  301. public TranscodingJob GetTranscodingJob(string playSessionId)
  302. {
  303. lock (_activeTranscodingJobs)
  304. {
  305. return _activeTranscodingJobs.FirstOrDefault(j => string.Equals(j.PlaySessionId, playSessionId, StringComparison.OrdinalIgnoreCase));
  306. }
  307. }
  308. /// <summary>
  309. /// Called when [transcode begin request].
  310. /// </summary>
  311. /// <param name="path">The path.</param>
  312. /// <param name="type">The type.</param>
  313. public TranscodingJob OnTranscodeBeginRequest(string path, TranscodingJobType type)
  314. {
  315. lock (_activeTranscodingJobs)
  316. {
  317. var job = _activeTranscodingJobs.FirstOrDefault(j => j.Type == type && string.Equals(j.Path, path, StringComparison.OrdinalIgnoreCase));
  318. if (job == null)
  319. {
  320. return null;
  321. }
  322. OnTranscodeBeginRequest(job);
  323. return job;
  324. }
  325. }
  326. public void OnTranscodeBeginRequest(TranscodingJob job)
  327. {
  328. job.ActiveRequestCount++;
  329. if (string.IsNullOrWhiteSpace(job.PlaySessionId) || job.Type == TranscodingJobType.Progressive)
  330. {
  331. job.StopKillTimer();
  332. }
  333. }
  334. public void OnTranscodeEndRequest(TranscodingJob job)
  335. {
  336. job.ActiveRequestCount--;
  337. _logger.LogDebug("OnTranscodeEndRequest job.ActiveRequestCount={0}", job.ActiveRequestCount);
  338. if (job.ActiveRequestCount <= 0)
  339. {
  340. PingTimer(job, false);
  341. }
  342. }
  343. internal void PingTranscodingJob(string playSessionId, bool? isUserPaused)
  344. {
  345. if (string.IsNullOrEmpty(playSessionId))
  346. {
  347. throw new ArgumentNullException(nameof(playSessionId));
  348. }
  349. _logger.LogDebug("PingTranscodingJob PlaySessionId={0} isUsedPaused: {1}", playSessionId, isUserPaused);
  350. List<TranscodingJob> jobs;
  351. lock (_activeTranscodingJobs)
  352. {
  353. // This is really only needed for HLS.
  354. // Progressive streams can stop on their own reliably
  355. jobs = _activeTranscodingJobs.Where(j => string.Equals(playSessionId, j.PlaySessionId, StringComparison.OrdinalIgnoreCase)).ToList();
  356. }
  357. foreach (var job in jobs)
  358. {
  359. if (isUserPaused.HasValue)
  360. {
  361. _logger.LogDebug("Setting job.IsUserPaused to {0}. jobId: {1}", isUserPaused, job.Id);
  362. job.IsUserPaused = isUserPaused.Value;
  363. }
  364. PingTimer(job, true);
  365. }
  366. }
  367. private void PingTimer(TranscodingJob job, bool isProgressCheckIn)
  368. {
  369. if (job.HasExited)
  370. {
  371. job.StopKillTimer();
  372. return;
  373. }
  374. var timerDuration = 10000;
  375. if (job.Type != TranscodingJobType.Progressive)
  376. {
  377. timerDuration = 60000;
  378. }
  379. job.PingTimeout = timerDuration;
  380. job.LastPingDate = DateTime.UtcNow;
  381. // Don't start the timer for playback checkins with progressive streaming
  382. if (job.Type != TranscodingJobType.Progressive || !isProgressCheckIn)
  383. {
  384. job.StartKillTimer(OnTranscodeKillTimerStopped);
  385. }
  386. else
  387. {
  388. job.ChangeKillTimerIfStarted();
  389. }
  390. }
  391. /// <summary>
  392. /// Called when [transcode kill timer stopped].
  393. /// </summary>
  394. /// <param name="state">The state.</param>
  395. private async void OnTranscodeKillTimerStopped(object state)
  396. {
  397. var job = (TranscodingJob)state;
  398. if (!job.HasExited && job.Type != TranscodingJobType.Progressive)
  399. {
  400. var timeSinceLastPing = (DateTime.UtcNow - job.LastPingDate).TotalMilliseconds;
  401. if (timeSinceLastPing < job.PingTimeout)
  402. {
  403. job.StartKillTimer(OnTranscodeKillTimerStopped, job.PingTimeout);
  404. return;
  405. }
  406. }
  407. _logger.LogInformation("Transcoding kill timer stopped for JobId {0} PlaySessionId {1}. Killing transcoding", job.Id, job.PlaySessionId);
  408. await KillTranscodingJob(job, true, path => true);
  409. }
  410. /// <summary>
  411. /// Kills the single transcoding job.
  412. /// </summary>
  413. /// <param name="deviceId">The device id.</param>
  414. /// <param name="playSessionId">The play session identifier.</param>
  415. /// <param name="deleteFiles">The delete files.</param>
  416. /// <returns>Task.</returns>
  417. internal Task KillTranscodingJobs(string deviceId, string playSessionId, Func<string, bool> deleteFiles)
  418. {
  419. return KillTranscodingJobs(j =>
  420. {
  421. if (!string.IsNullOrWhiteSpace(playSessionId))
  422. {
  423. return string.Equals(playSessionId, j.PlaySessionId, StringComparison.OrdinalIgnoreCase);
  424. }
  425. return string.Equals(deviceId, j.DeviceId, StringComparison.OrdinalIgnoreCase);
  426. }, deleteFiles);
  427. }
  428. /// <summary>
  429. /// Kills the transcoding jobs.
  430. /// </summary>
  431. /// <param name="killJob">The kill job.</param>
  432. /// <param name="deleteFiles">The delete files.</param>
  433. /// <returns>Task.</returns>
  434. private Task KillTranscodingJobs(Func<TranscodingJob, bool> killJob, Func<string, bool> deleteFiles)
  435. {
  436. var jobs = new List<TranscodingJob>();
  437. lock (_activeTranscodingJobs)
  438. {
  439. // This is really only needed for HLS.
  440. // Progressive streams can stop on their own reliably
  441. jobs.AddRange(_activeTranscodingJobs.Where(killJob));
  442. }
  443. if (jobs.Count == 0)
  444. {
  445. return Task.CompletedTask;
  446. }
  447. IEnumerable<Task> GetKillJobs()
  448. {
  449. foreach (var job in jobs)
  450. {
  451. yield return KillTranscodingJob(job, false, deleteFiles);
  452. }
  453. }
  454. return Task.WhenAll(GetKillJobs());
  455. }
  456. /// <summary>
  457. /// Kills the transcoding job.
  458. /// </summary>
  459. /// <param name="job">The job.</param>
  460. /// <param name="closeLiveStream">if set to <c>true</c> [close live stream].</param>
  461. /// <param name="delete">The delete.</param>
  462. private async Task KillTranscodingJob(TranscodingJob job, bool closeLiveStream, Func<string, bool> delete)
  463. {
  464. job.DisposeKillTimer();
  465. _logger.LogDebug("KillTranscodingJob - JobId {0} PlaySessionId {1}. Killing transcoding", job.Id, job.PlaySessionId);
  466. lock (_activeTranscodingJobs)
  467. {
  468. _activeTranscodingJobs.Remove(job);
  469. if (!job.CancellationTokenSource.IsCancellationRequested)
  470. {
  471. job.CancellationTokenSource.Cancel();
  472. }
  473. }
  474. lock (_transcodingLocks)
  475. {
  476. _transcodingLocks.Remove(job.Path);
  477. }
  478. lock (job.ProcessLock)
  479. {
  480. if (job.TranscodingThrottler != null)
  481. {
  482. job.TranscodingThrottler.Stop().GetAwaiter().GetResult();
  483. }
  484. var process = job.Process;
  485. var hasExited = job.HasExited;
  486. if (!hasExited)
  487. {
  488. try
  489. {
  490. _logger.LogInformation("Stopping ffmpeg process with q command for {Path}", job.Path);
  491. process.StandardInput.WriteLine("q");
  492. // Need to wait because killing is asynchronous
  493. if (!process.WaitForExit(5000))
  494. {
  495. _logger.LogInformation("Killing ffmpeg process for {Path}", job.Path);
  496. process.Kill();
  497. }
  498. }
  499. catch (InvalidOperationException)
  500. {
  501. }
  502. }
  503. }
  504. if (delete(job.Path))
  505. {
  506. await DeletePartialStreamFiles(job.Path, job.Type, 0, 1500).ConfigureAwait(false);
  507. }
  508. if (closeLiveStream && !string.IsNullOrWhiteSpace(job.LiveStreamId))
  509. {
  510. try
  511. {
  512. await _mediaSourceManager.CloseLiveStream(job.LiveStreamId).ConfigureAwait(false);
  513. }
  514. catch (Exception ex)
  515. {
  516. _logger.LogError(ex, "Error closing live stream for {Path}", job.Path);
  517. }
  518. }
  519. }
  520. private async Task DeletePartialStreamFiles(string path, TranscodingJobType jobType, int retryCount, int delayMs)
  521. {
  522. if (retryCount >= 10)
  523. {
  524. return;
  525. }
  526. _logger.LogInformation("Deleting partial stream file(s) {Path}", path);
  527. await Task.Delay(delayMs).ConfigureAwait(false);
  528. try
  529. {
  530. if (jobType == TranscodingJobType.Progressive)
  531. {
  532. DeleteProgressivePartialStreamFiles(path);
  533. }
  534. else
  535. {
  536. DeleteHlsPartialStreamFiles(path);
  537. }
  538. }
  539. catch (IOException ex)
  540. {
  541. _logger.LogError(ex, "Error deleting partial stream file(s) {Path}", path);
  542. await DeletePartialStreamFiles(path, jobType, retryCount + 1, 500).ConfigureAwait(false);
  543. }
  544. catch (Exception ex)
  545. {
  546. _logger.LogError(ex, "Error deleting partial stream file(s) {Path}", path);
  547. }
  548. }
  549. /// <summary>
  550. /// Deletes the progressive partial stream files.
  551. /// </summary>
  552. /// <param name="outputFilePath">The output file path.</param>
  553. private void DeleteProgressivePartialStreamFiles(string outputFilePath)
  554. {
  555. if (File.Exists(outputFilePath))
  556. {
  557. _fileSystem.DeleteFile(outputFilePath);
  558. }
  559. }
  560. /// <summary>
  561. /// Deletes the HLS partial stream files.
  562. /// </summary>
  563. /// <param name="outputFilePath">The output file path.</param>
  564. private void DeleteHlsPartialStreamFiles(string outputFilePath)
  565. {
  566. var directory = Path.GetDirectoryName(outputFilePath);
  567. var name = Path.GetFileNameWithoutExtension(outputFilePath);
  568. var filesToDelete = _fileSystem.GetFilePaths(directory)
  569. .Where(f => f.IndexOf(name, StringComparison.OrdinalIgnoreCase) != -1);
  570. List<Exception> exs = null;
  571. foreach (var file in filesToDelete)
  572. {
  573. try
  574. {
  575. _logger.LogDebug("Deleting HLS file {0}", file);
  576. _fileSystem.DeleteFile(file);
  577. }
  578. catch (IOException ex)
  579. {
  580. (exs ??= new List<Exception>(4)).Add(ex);
  581. _logger.LogError(ex, "Error deleting HLS file {Path}", file);
  582. }
  583. }
  584. if (exs != null)
  585. {
  586. throw new AggregateException("Error deleting HLS files", exs);
  587. }
  588. }
  589. }
  590. }