TranscodingJobHelper.cs 33 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913
  1. using System;
  2. using System.Collections.Generic;
  3. using System.Diagnostics;
  4. using System.Globalization;
  5. using System.IO;
  6. using System.Linq;
  7. using System.Text;
  8. using System.Text.Json;
  9. using System.Threading;
  10. using System.Threading.Tasks;
  11. using Jellyfin.Api.Extensions;
  12. using Jellyfin.Api.Models.PlaybackDtos;
  13. using Jellyfin.Api.Models.StreamingDtos;
  14. using Jellyfin.Data.Enums;
  15. using MediaBrowser.Common;
  16. using MediaBrowser.Common.Configuration;
  17. using MediaBrowser.Common.Extensions;
  18. using MediaBrowser.Controller.Configuration;
  19. using MediaBrowser.Controller.Library;
  20. using MediaBrowser.Controller.MediaEncoding;
  21. using MediaBrowser.Controller.Session;
  22. using MediaBrowser.Model.Dlna;
  23. using MediaBrowser.Model.Entities;
  24. using MediaBrowser.Model.IO;
  25. using MediaBrowser.Model.MediaInfo;
  26. using MediaBrowser.Model.Session;
  27. using Microsoft.AspNetCore.Http;
  28. using Microsoft.Extensions.Logging;
  29. namespace Jellyfin.Api.Helpers;
  30. /// <summary>
  31. /// Transcoding job helpers.
  32. /// </summary>
  33. public class TranscodingJobHelper : IDisposable
  34. {
  35. /// <summary>
  36. /// The active transcoding jobs.
  37. /// </summary>
  38. private static readonly List<TranscodingJobDto> _activeTranscodingJobs = new List<TranscodingJobDto>();
  39. /// <summary>
  40. /// The transcoding locks.
  41. /// </summary>
  42. private static readonly Dictionary<string, SemaphoreSlim> _transcodingLocks = new Dictionary<string, SemaphoreSlim>();
  43. private readonly IAttachmentExtractor _attachmentExtractor;
  44. private readonly IApplicationPaths _appPaths;
  45. private readonly EncodingHelper _encodingHelper;
  46. private readonly IFileSystem _fileSystem;
  47. private readonly ILogger<TranscodingJobHelper> _logger;
  48. private readonly IMediaEncoder _mediaEncoder;
  49. private readonly IMediaSourceManager _mediaSourceManager;
  50. private readonly IServerConfigurationManager _serverConfigurationManager;
  51. private readonly ISessionManager _sessionManager;
  52. private readonly ILoggerFactory _loggerFactory;
  53. private readonly IUserManager _userManager;
  54. /// <summary>
  55. /// Initializes a new instance of the <see cref="TranscodingJobHelper"/> class.
  56. /// </summary>
  57. /// <param name="attachmentExtractor">Instance of the <see cref="IAttachmentExtractor"/> interface.</param>
  58. /// <param name="appPaths">Instance of the <see cref="IApplicationPaths"/> interface.</param>
  59. /// <param name="logger">Instance of the <see cref="ILogger{TranscodingJobHelpers}"/> interface.</param>
  60. /// <param name="mediaSourceManager">Instance of the <see cref="IMediaSourceManager"/> interface.</param>
  61. /// <param name="fileSystem">Instance of the <see cref="IFileSystem"/> interface.</param>
  62. /// <param name="mediaEncoder">Instance of the <see cref="IMediaEncoder"/> interface.</param>
  63. /// <param name="serverConfigurationManager">Instance of the <see cref="IServerConfigurationManager"/> interface.</param>
  64. /// <param name="sessionManager">Instance of the <see cref="ISessionManager"/> interface.</param>
  65. /// <param name="encodingHelper">Instance of <see cref="EncodingHelper"/>.</param>
  66. /// <param name="loggerFactory">Instance of the <see cref="ILoggerFactory"/> interface.</param>
  67. /// <param name="userManager">Instance of the <see cref="IUserManager"/> interface.</param>
  68. public TranscodingJobHelper(
  69. IAttachmentExtractor attachmentExtractor,
  70. IApplicationPaths appPaths,
  71. ILogger<TranscodingJobHelper> logger,
  72. IMediaSourceManager mediaSourceManager,
  73. IFileSystem fileSystem,
  74. IMediaEncoder mediaEncoder,
  75. IServerConfigurationManager serverConfigurationManager,
  76. ISessionManager sessionManager,
  77. EncodingHelper encodingHelper,
  78. ILoggerFactory loggerFactory,
  79. IUserManager userManager)
  80. {
  81. _attachmentExtractor = attachmentExtractor;
  82. _appPaths = appPaths;
  83. _logger = logger;
  84. _mediaSourceManager = mediaSourceManager;
  85. _fileSystem = fileSystem;
  86. _mediaEncoder = mediaEncoder;
  87. _serverConfigurationManager = serverConfigurationManager;
  88. _sessionManager = sessionManager;
  89. _encodingHelper = encodingHelper;
  90. _loggerFactory = loggerFactory;
  91. _userManager = userManager;
  92. DeleteEncodedMediaCache();
  93. sessionManager.PlaybackProgress += OnPlaybackProgress;
  94. sessionManager.PlaybackStart += OnPlaybackProgress;
  95. }
  96. /// <summary>
  97. /// Get transcoding job.
  98. /// </summary>
  99. /// <param name="playSessionId">Playback session id.</param>
  100. /// <returns>The transcoding job.</returns>
  101. public TranscodingJobDto? GetTranscodingJob(string playSessionId)
  102. {
  103. lock (_activeTranscodingJobs)
  104. {
  105. return _activeTranscodingJobs.FirstOrDefault(j => string.Equals(j.PlaySessionId, playSessionId, StringComparison.OrdinalIgnoreCase));
  106. }
  107. }
  108. /// <summary>
  109. /// Get transcoding job.
  110. /// </summary>
  111. /// <param name="path">Path to the transcoding file.</param>
  112. /// <param name="type">The <see cref="TranscodingJobType"/>.</param>
  113. /// <returns>The transcoding job.</returns>
  114. public TranscodingJobDto? GetTranscodingJob(string path, TranscodingJobType type)
  115. {
  116. lock (_activeTranscodingJobs)
  117. {
  118. return _activeTranscodingJobs.FirstOrDefault(j => j.Type == type && string.Equals(j.Path, path, StringComparison.OrdinalIgnoreCase));
  119. }
  120. }
  121. /// <summary>
  122. /// Ping transcoding job.
  123. /// </summary>
  124. /// <param name="playSessionId">Play session id.</param>
  125. /// <param name="isUserPaused">Is user paused.</param>
  126. /// <exception cref="ArgumentNullException">Play session id is null.</exception>
  127. public void PingTranscodingJob(string playSessionId, bool? isUserPaused)
  128. {
  129. ArgumentException.ThrowIfNullOrEmpty(playSessionId);
  130. _logger.LogDebug("PingTranscodingJob PlaySessionId={0} isUsedPaused: {1}", playSessionId, isUserPaused);
  131. List<TranscodingJobDto> jobs;
  132. lock (_activeTranscodingJobs)
  133. {
  134. // This is really only needed for HLS.
  135. // Progressive streams can stop on their own reliably.
  136. jobs = _activeTranscodingJobs.Where(j => string.Equals(playSessionId, j.PlaySessionId, StringComparison.OrdinalIgnoreCase)).ToList();
  137. }
  138. foreach (var job in jobs)
  139. {
  140. if (isUserPaused.HasValue)
  141. {
  142. _logger.LogDebug("Setting job.IsUserPaused to {0}. jobId: {1}", isUserPaused, job.Id);
  143. job.IsUserPaused = isUserPaused.Value;
  144. }
  145. PingTimer(job, true);
  146. }
  147. }
  148. private void PingTimer(TranscodingJobDto job, bool isProgressCheckIn)
  149. {
  150. if (job.HasExited)
  151. {
  152. job.StopKillTimer();
  153. return;
  154. }
  155. var timerDuration = 10000;
  156. if (job.Type != TranscodingJobType.Progressive)
  157. {
  158. timerDuration = 60000;
  159. }
  160. job.PingTimeout = timerDuration;
  161. job.LastPingDate = DateTime.UtcNow;
  162. // Don't start the timer for playback checkins with progressive streaming
  163. if (job.Type != TranscodingJobType.Progressive || !isProgressCheckIn)
  164. {
  165. job.StartKillTimer(OnTranscodeKillTimerStopped);
  166. }
  167. else
  168. {
  169. job.ChangeKillTimerIfStarted();
  170. }
  171. }
  172. /// <summary>
  173. /// Called when [transcode kill timer stopped].
  174. /// </summary>
  175. /// <param name="state">The state.</param>
  176. private async void OnTranscodeKillTimerStopped(object? state)
  177. {
  178. var job = state as TranscodingJobDto ?? throw new ArgumentException($"{nameof(state)} is not of type {nameof(TranscodingJobDto)}", nameof(state));
  179. if (!job.HasExited && job.Type != TranscodingJobType.Progressive)
  180. {
  181. var timeSinceLastPing = (DateTime.UtcNow - job.LastPingDate).TotalMilliseconds;
  182. if (timeSinceLastPing < job.PingTimeout)
  183. {
  184. job.StartKillTimer(OnTranscodeKillTimerStopped, job.PingTimeout);
  185. return;
  186. }
  187. }
  188. _logger.LogInformation("Transcoding kill timer stopped for JobId {0} PlaySessionId {1}. Killing transcoding", job.Id, job.PlaySessionId);
  189. await KillTranscodingJob(job, true, path => true).ConfigureAwait(false);
  190. }
  191. /// <summary>
  192. /// Kills the single transcoding job.
  193. /// </summary>
  194. /// <param name="deviceId">The device id.</param>
  195. /// <param name="playSessionId">The play session identifier.</param>
  196. /// <param name="deleteFiles">The delete files.</param>
  197. /// <returns>Task.</returns>
  198. public Task KillTranscodingJobs(string deviceId, string? playSessionId, Func<string, bool> deleteFiles)
  199. {
  200. return KillTranscodingJobs(
  201. j => string.IsNullOrWhiteSpace(playSessionId)
  202. ? string.Equals(deviceId, j.DeviceId, StringComparison.OrdinalIgnoreCase)
  203. : string.Equals(playSessionId, j.PlaySessionId, StringComparison.OrdinalIgnoreCase),
  204. deleteFiles);
  205. }
  206. /// <summary>
  207. /// Kills the transcoding jobs.
  208. /// </summary>
  209. /// <param name="killJob">The kill job.</param>
  210. /// <param name="deleteFiles">The delete files.</param>
  211. /// <returns>Task.</returns>
  212. private Task KillTranscodingJobs(Func<TranscodingJobDto, bool> killJob, Func<string, bool> deleteFiles)
  213. {
  214. var jobs = new List<TranscodingJobDto>();
  215. lock (_activeTranscodingJobs)
  216. {
  217. // This is really only needed for HLS.
  218. // Progressive streams can stop on their own reliably.
  219. jobs.AddRange(_activeTranscodingJobs.Where(killJob));
  220. }
  221. if (jobs.Count == 0)
  222. {
  223. return Task.CompletedTask;
  224. }
  225. IEnumerable<Task> GetKillJobs()
  226. {
  227. foreach (var job in jobs)
  228. {
  229. yield return KillTranscodingJob(job, false, deleteFiles);
  230. }
  231. }
  232. return Task.WhenAll(GetKillJobs());
  233. }
  234. /// <summary>
  235. /// Kills the transcoding job.
  236. /// </summary>
  237. /// <param name="job">The job.</param>
  238. /// <param name="closeLiveStream">if set to <c>true</c> [close live stream].</param>
  239. /// <param name="delete">The delete.</param>
  240. private async Task KillTranscodingJob(TranscodingJobDto job, bool closeLiveStream, Func<string, bool> delete)
  241. {
  242. job.DisposeKillTimer();
  243. _logger.LogDebug("KillTranscodingJob - JobId {0} PlaySessionId {1}. Killing transcoding", job.Id, job.PlaySessionId);
  244. lock (_activeTranscodingJobs)
  245. {
  246. _activeTranscodingJobs.Remove(job);
  247. if (job.CancellationTokenSource?.IsCancellationRequested == false)
  248. {
  249. job.CancellationTokenSource.Cancel();
  250. }
  251. }
  252. lock (_transcodingLocks)
  253. {
  254. _transcodingLocks.Remove(job.Path!);
  255. }
  256. lock (job.ProcessLock!)
  257. {
  258. #pragma warning disable CA1849 // Can't await in lock block
  259. job.TranscodingThrottler?.Stop().GetAwaiter().GetResult();
  260. var process = job.Process;
  261. var hasExited = job.HasExited;
  262. if (!hasExited)
  263. {
  264. try
  265. {
  266. _logger.LogInformation("Stopping ffmpeg process with q command for {Path}", job.Path);
  267. process!.StandardInput.WriteLine("q");
  268. // Need to wait because killing is asynchronous.
  269. if (!process.WaitForExit(5000))
  270. {
  271. _logger.LogInformation("Killing FFmpeg process for {Path}", job.Path);
  272. process.Kill();
  273. }
  274. }
  275. catch (InvalidOperationException)
  276. {
  277. }
  278. }
  279. #pragma warning restore CA1849
  280. }
  281. if (delete(job.Path!))
  282. {
  283. await DeletePartialStreamFiles(job.Path!, job.Type, 0, 1500).ConfigureAwait(false);
  284. }
  285. if (closeLiveStream && !string.IsNullOrWhiteSpace(job.LiveStreamId))
  286. {
  287. try
  288. {
  289. await _mediaSourceManager.CloseLiveStream(job.LiveStreamId).ConfigureAwait(false);
  290. }
  291. catch (Exception ex)
  292. {
  293. _logger.LogError(ex, "Error closing live stream for {Path}", job.Path);
  294. }
  295. }
  296. }
  297. private async Task DeletePartialStreamFiles(string path, TranscodingJobType jobType, int retryCount, int delayMs)
  298. {
  299. if (retryCount >= 10)
  300. {
  301. return;
  302. }
  303. _logger.LogInformation("Deleting partial stream file(s) {Path}", path);
  304. await Task.Delay(delayMs).ConfigureAwait(false);
  305. try
  306. {
  307. if (jobType == TranscodingJobType.Progressive)
  308. {
  309. DeleteProgressivePartialStreamFiles(path);
  310. }
  311. else
  312. {
  313. DeleteHlsPartialStreamFiles(path);
  314. }
  315. }
  316. catch (IOException ex)
  317. {
  318. _logger.LogError(ex, "Error deleting partial stream file(s) {Path}", path);
  319. await DeletePartialStreamFiles(path, jobType, retryCount + 1, 500).ConfigureAwait(false);
  320. }
  321. catch (Exception ex)
  322. {
  323. _logger.LogError(ex, "Error deleting partial stream file(s) {Path}", path);
  324. }
  325. }
  326. /// <summary>
  327. /// Deletes the progressive partial stream files.
  328. /// </summary>
  329. /// <param name="outputFilePath">The output file path.</param>
  330. private void DeleteProgressivePartialStreamFiles(string outputFilePath)
  331. {
  332. if (File.Exists(outputFilePath))
  333. {
  334. _fileSystem.DeleteFile(outputFilePath);
  335. }
  336. }
  337. /// <summary>
  338. /// Deletes the HLS partial stream files.
  339. /// </summary>
  340. /// <param name="outputFilePath">The output file path.</param>
  341. private void DeleteHlsPartialStreamFiles(string outputFilePath)
  342. {
  343. var directory = Path.GetDirectoryName(outputFilePath)
  344. ?? throw new ArgumentException("Path can't be a root directory.", nameof(outputFilePath));
  345. var name = Path.GetFileNameWithoutExtension(outputFilePath);
  346. var filesToDelete = _fileSystem.GetFilePaths(directory)
  347. .Where(f => f.IndexOf(name, StringComparison.OrdinalIgnoreCase) != -1);
  348. List<Exception>? exs = null;
  349. foreach (var file in filesToDelete)
  350. {
  351. try
  352. {
  353. _logger.LogDebug("Deleting HLS file {0}", file);
  354. _fileSystem.DeleteFile(file);
  355. }
  356. catch (IOException ex)
  357. {
  358. (exs ??= new List<Exception>(4)).Add(ex);
  359. _logger.LogError(ex, "Error deleting HLS file {Path}", file);
  360. }
  361. }
  362. if (exs is not null)
  363. {
  364. throw new AggregateException("Error deleting HLS files", exs);
  365. }
  366. }
  367. /// <summary>
  368. /// Report the transcoding progress to the session manager.
  369. /// </summary>
  370. /// <param name="job">The <see cref="TranscodingJobDto"/> of which the progress will be reported.</param>
  371. /// <param name="state">The <see cref="StreamState"/> of the current transcoding job.</param>
  372. /// <param name="transcodingPosition">The current transcoding position.</param>
  373. /// <param name="framerate">The framerate of the transcoding job.</param>
  374. /// <param name="percentComplete">The completion percentage of the transcode.</param>
  375. /// <param name="bytesTranscoded">The number of bytes transcoded.</param>
  376. /// <param name="bitRate">The bitrate of the transcoding job.</param>
  377. public void ReportTranscodingProgress(
  378. TranscodingJobDto job,
  379. StreamState state,
  380. TimeSpan? transcodingPosition,
  381. float? framerate,
  382. double? percentComplete,
  383. long? bytesTranscoded,
  384. int? bitRate)
  385. {
  386. var ticks = transcodingPosition?.Ticks;
  387. if (job is not null)
  388. {
  389. job.Framerate = framerate;
  390. job.CompletionPercentage = percentComplete;
  391. job.TranscodingPositionTicks = ticks;
  392. job.BytesTranscoded = bytesTranscoded;
  393. job.BitRate = bitRate;
  394. }
  395. var deviceId = state.Request.DeviceId;
  396. if (!string.IsNullOrWhiteSpace(deviceId))
  397. {
  398. var audioCodec = state.ActualOutputAudioCodec;
  399. var videoCodec = state.ActualOutputVideoCodec;
  400. var hardwareAccelerationTypeString = _serverConfigurationManager.GetEncodingOptions().HardwareAccelerationType;
  401. HardwareEncodingType? hardwareAccelerationType = null;
  402. if (!string.IsNullOrEmpty(hardwareAccelerationTypeString)
  403. && Enum.TryParse<HardwareEncodingType>(hardwareAccelerationTypeString, out var parsedHardwareAccelerationType))
  404. {
  405. hardwareAccelerationType = parsedHardwareAccelerationType;
  406. }
  407. _sessionManager.ReportTranscodingInfo(deviceId, new TranscodingInfo
  408. {
  409. Bitrate = bitRate ?? state.TotalOutputBitrate,
  410. AudioCodec = audioCodec,
  411. VideoCodec = videoCodec,
  412. Container = state.OutputContainer,
  413. Framerate = framerate,
  414. CompletionPercentage = percentComplete,
  415. Width = state.OutputWidth,
  416. Height = state.OutputHeight,
  417. AudioChannels = state.OutputAudioChannels,
  418. IsAudioDirect = EncodingHelper.IsCopyCodec(state.OutputAudioCodec),
  419. IsVideoDirect = EncodingHelper.IsCopyCodec(state.OutputVideoCodec),
  420. HardwareAccelerationType = hardwareAccelerationType,
  421. TranscodeReasons = state.TranscodeReasons
  422. });
  423. }
  424. }
  425. /// <summary>
  426. /// Starts FFmpeg.
  427. /// </summary>
  428. /// <param name="state">The state.</param>
  429. /// <param name="outputPath">The output path.</param>
  430. /// <param name="commandLineArguments">The command line arguments for FFmpeg.</param>
  431. /// <param name="request">The <see cref="HttpRequest"/>.</param>
  432. /// <param name="transcodingJobType">The <see cref="TranscodingJobType"/>.</param>
  433. /// <param name="cancellationTokenSource">The cancellation token source.</param>
  434. /// <param name="workingDirectory">The working directory.</param>
  435. /// <returns>Task.</returns>
  436. public async Task<TranscodingJobDto> StartFfMpeg(
  437. StreamState state,
  438. string outputPath,
  439. string commandLineArguments,
  440. HttpRequest request,
  441. TranscodingJobType transcodingJobType,
  442. CancellationTokenSource cancellationTokenSource,
  443. string? workingDirectory = null)
  444. {
  445. var directory = Path.GetDirectoryName(outputPath) ?? throw new ArgumentException($"Provided path ({outputPath}) is not valid.", nameof(outputPath));
  446. Directory.CreateDirectory(directory);
  447. await AcquireResources(state, cancellationTokenSource).ConfigureAwait(false);
  448. if (state.VideoRequest is not null && !EncodingHelper.IsCopyCodec(state.OutputVideoCodec))
  449. {
  450. var userId = request.HttpContext.User.GetUserId();
  451. var user = userId.Equals(default) ? null : _userManager.GetUserById(userId);
  452. if (user is not null && !user.HasPermission(PermissionKind.EnableVideoPlaybackTranscoding))
  453. {
  454. this.OnTranscodeFailedToStart(outputPath, transcodingJobType, state);
  455. throw new ArgumentException("User does not have access to video transcoding.");
  456. }
  457. }
  458. ArgumentException.ThrowIfNullOrEmpty(_mediaEncoder.EncoderPath);
  459. // If subtitles get burned in fonts may need to be extracted from the media file
  460. if (state.SubtitleStream is not null && state.SubtitleDeliveryMethod == SubtitleDeliveryMethod.Encode)
  461. {
  462. var attachmentPath = Path.Combine(_appPaths.CachePath, "attachments", state.MediaSource.Id);
  463. await _attachmentExtractor.ExtractAllAttachments(state.MediaPath, state.MediaSource, attachmentPath, cancellationTokenSource.Token).ConfigureAwait(false);
  464. if (state.SubtitleStream.IsExternal && string.Equals(Path.GetExtension(state.SubtitleStream.Path), ".mks", StringComparison.OrdinalIgnoreCase))
  465. {
  466. string subtitlePath = state.SubtitleStream.Path;
  467. string subtitlePathArgument = string.Format(CultureInfo.InvariantCulture, "file:\"{0}\"", subtitlePath.Replace("\"", "\\\"", StringComparison.Ordinal));
  468. string subtitleId = subtitlePath.GetMD5().ToString("N", CultureInfo.InvariantCulture);
  469. await _attachmentExtractor.ExtractAllAttachmentsExternal(subtitlePathArgument, subtitleId, attachmentPath, cancellationTokenSource.Token).ConfigureAwait(false);
  470. }
  471. }
  472. var process = new Process
  473. {
  474. StartInfo = new ProcessStartInfo
  475. {
  476. WindowStyle = ProcessWindowStyle.Hidden,
  477. CreateNoWindow = true,
  478. UseShellExecute = false,
  479. // Must consume both stdout and stderr or deadlocks may occur
  480. // RedirectStandardOutput = true,
  481. RedirectStandardError = true,
  482. RedirectStandardInput = true,
  483. FileName = _mediaEncoder.EncoderPath,
  484. Arguments = commandLineArguments,
  485. WorkingDirectory = string.IsNullOrWhiteSpace(workingDirectory) ? string.Empty : workingDirectory,
  486. ErrorDialog = false
  487. },
  488. EnableRaisingEvents = true
  489. };
  490. var transcodingJob = this.OnTranscodeBeginning(
  491. outputPath,
  492. state.Request.PlaySessionId,
  493. state.MediaSource.LiveStreamId,
  494. Guid.NewGuid().ToString("N", CultureInfo.InvariantCulture),
  495. transcodingJobType,
  496. process,
  497. state.Request.DeviceId,
  498. state,
  499. cancellationTokenSource);
  500. _logger.LogInformation("{Filename} {Arguments}", process.StartInfo.FileName, process.StartInfo.Arguments);
  501. var logFilePrefix = "FFmpeg.Transcode-";
  502. if (state.VideoRequest is not null
  503. && EncodingHelper.IsCopyCodec(state.OutputVideoCodec))
  504. {
  505. logFilePrefix = EncodingHelper.IsCopyCodec(state.OutputAudioCodec)
  506. ? "FFmpeg.Remux-"
  507. : "FFmpeg.DirectStream-";
  508. }
  509. var logFilePath = Path.Combine(
  510. _serverConfigurationManager.ApplicationPaths.LogDirectoryPath,
  511. $"{logFilePrefix}{DateTime.Now:yyyy-MM-dd_HH-mm-ss}_{state.Request.MediaSourceId}_{Guid.NewGuid().ToString()[..8]}.log");
  512. // FFmpeg writes debug/error info to stderr. This is useful when debugging so let's put it in the log directory.
  513. Stream logStream = new FileStream(logFilePath, FileMode.Create, FileAccess.Write, FileShare.Read, IODefaults.FileStreamBufferSize, FileOptions.Asynchronous);
  514. var commandLineLogMessage = process.StartInfo.FileName + " " + process.StartInfo.Arguments;
  515. var commandLineLogMessageBytes = Encoding.UTF8.GetBytes(request.Path + Environment.NewLine + Environment.NewLine + JsonSerializer.Serialize(state.MediaSource) + Environment.NewLine + Environment.NewLine + commandLineLogMessage + Environment.NewLine + Environment.NewLine);
  516. await logStream.WriteAsync(commandLineLogMessageBytes, cancellationTokenSource.Token).ConfigureAwait(false);
  517. process.Exited += (sender, args) => OnFfMpegProcessExited(process, transcodingJob, state);
  518. try
  519. {
  520. process.Start();
  521. }
  522. catch (Exception ex)
  523. {
  524. _logger.LogError(ex, "Error starting FFmpeg");
  525. this.OnTranscodeFailedToStart(outputPath, transcodingJobType, state);
  526. throw;
  527. }
  528. _logger.LogDebug("Launched FFmpeg process");
  529. state.TranscodingJob = transcodingJob;
  530. // Important - don't await the log task or we won't be able to kill FFmpeg when the user stops playback
  531. _ = new JobLogger(_logger).StartStreamingLog(state, process.StandardError.BaseStream, logStream);
  532. // Wait for the file to exist before proceeding
  533. var ffmpegTargetFile = state.WaitForPath ?? outputPath;
  534. _logger.LogDebug("Waiting for the creation of {0}", ffmpegTargetFile);
  535. while (!File.Exists(ffmpegTargetFile) && !transcodingJob.HasExited)
  536. {
  537. await Task.Delay(100, cancellationTokenSource.Token).ConfigureAwait(false);
  538. }
  539. _logger.LogDebug("File {0} created or transcoding has finished", ffmpegTargetFile);
  540. if (state.IsInputVideo && transcodingJob.Type == TranscodingJobType.Progressive && !transcodingJob.HasExited)
  541. {
  542. await Task.Delay(1000, cancellationTokenSource.Token).ConfigureAwait(false);
  543. if (state.ReadInputAtNativeFramerate && !transcodingJob.HasExited)
  544. {
  545. await Task.Delay(1500, cancellationTokenSource.Token).ConfigureAwait(false);
  546. }
  547. }
  548. if (!transcodingJob.HasExited)
  549. {
  550. StartThrottler(state, transcodingJob);
  551. }
  552. else if (transcodingJob.ExitCode != 0)
  553. {
  554. throw new FfmpegException(string.Format(CultureInfo.InvariantCulture, "FFmpeg exited with code {0}", transcodingJob.ExitCode));
  555. }
  556. _logger.LogDebug("StartFfMpeg() finished successfully");
  557. return transcodingJob;
  558. }
  559. private void StartThrottler(StreamState state, TranscodingJobDto transcodingJob)
  560. {
  561. if (EnableThrottling(state))
  562. {
  563. transcodingJob.TranscodingThrottler = new TranscodingThrottler(transcodingJob, new Logger<TranscodingThrottler>(new LoggerFactory()), _serverConfigurationManager, _fileSystem, _mediaEncoder);
  564. transcodingJob.TranscodingThrottler.Start();
  565. }
  566. }
  567. private bool EnableThrottling(StreamState state)
  568. {
  569. var encodingOptions = _serverConfigurationManager.GetEncodingOptions();
  570. return state.InputProtocol == MediaProtocol.File &&
  571. state.RunTimeTicks.HasValue &&
  572. state.RunTimeTicks.Value >= TimeSpan.FromMinutes(5).Ticks &&
  573. state.IsInputVideo &&
  574. state.VideoType == VideoType.VideoFile;
  575. }
  576. /// <summary>
  577. /// Called when [transcode beginning].
  578. /// </summary>
  579. /// <param name="path">The path.</param>
  580. /// <param name="playSessionId">The play session identifier.</param>
  581. /// <param name="liveStreamId">The live stream identifier.</param>
  582. /// <param name="transcodingJobId">The transcoding job identifier.</param>
  583. /// <param name="type">The type.</param>
  584. /// <param name="process">The process.</param>
  585. /// <param name="deviceId">The device id.</param>
  586. /// <param name="state">The state.</param>
  587. /// <param name="cancellationTokenSource">The cancellation token source.</param>
  588. /// <returns>TranscodingJob.</returns>
  589. public TranscodingJobDto OnTranscodeBeginning(
  590. string path,
  591. string? playSessionId,
  592. string? liveStreamId,
  593. string transcodingJobId,
  594. TranscodingJobType type,
  595. Process process,
  596. string? deviceId,
  597. StreamState state,
  598. CancellationTokenSource cancellationTokenSource)
  599. {
  600. lock (_activeTranscodingJobs)
  601. {
  602. var job = new TranscodingJobDto(_loggerFactory.CreateLogger<TranscodingJobDto>())
  603. {
  604. Type = type,
  605. Path = path,
  606. Process = process,
  607. ActiveRequestCount = 1,
  608. DeviceId = deviceId,
  609. CancellationTokenSource = cancellationTokenSource,
  610. Id = transcodingJobId,
  611. PlaySessionId = playSessionId,
  612. LiveStreamId = liveStreamId,
  613. MediaSource = state.MediaSource
  614. };
  615. _activeTranscodingJobs.Add(job);
  616. ReportTranscodingProgress(job, state, null, null, null, null, null);
  617. return job;
  618. }
  619. }
  620. /// <summary>
  621. /// Called when [transcode end].
  622. /// </summary>
  623. /// <param name="job">The transcode job.</param>
  624. public void OnTranscodeEndRequest(TranscodingJobDto job)
  625. {
  626. job.ActiveRequestCount--;
  627. _logger.LogDebug("OnTranscodeEndRequest job.ActiveRequestCount={ActiveRequestCount}", job.ActiveRequestCount);
  628. if (job.ActiveRequestCount <= 0)
  629. {
  630. PingTimer(job, false);
  631. }
  632. }
  633. /// <summary>
  634. /// <summary>
  635. /// The progressive
  636. /// </summary>
  637. /// Called when [transcode failed to start].
  638. /// </summary>
  639. /// <param name="path">The path.</param>
  640. /// <param name="type">The type.</param>
  641. /// <param name="state">The state.</param>
  642. public void OnTranscodeFailedToStart(string path, TranscodingJobType type, StreamState state)
  643. {
  644. lock (_activeTranscodingJobs)
  645. {
  646. var job = _activeTranscodingJobs.FirstOrDefault(j => j.Type == type && string.Equals(j.Path, path, StringComparison.OrdinalIgnoreCase));
  647. if (job is not null)
  648. {
  649. _activeTranscodingJobs.Remove(job);
  650. }
  651. }
  652. lock (_transcodingLocks)
  653. {
  654. _transcodingLocks.Remove(path);
  655. }
  656. if (!string.IsNullOrWhiteSpace(state.Request.DeviceId))
  657. {
  658. _sessionManager.ClearTranscodingInfo(state.Request.DeviceId);
  659. }
  660. }
  661. /// <summary>
  662. /// Processes the exited.
  663. /// </summary>
  664. /// <param name="process">The process.</param>
  665. /// <param name="job">The job.</param>
  666. /// <param name="state">The state.</param>
  667. private void OnFfMpegProcessExited(Process process, TranscodingJobDto job, StreamState state)
  668. {
  669. job.HasExited = true;
  670. job.ExitCode = process.ExitCode;
  671. ReportTranscodingProgress(job, state, null, null, null, null, null);
  672. _logger.LogDebug("Disposing stream resources");
  673. state.Dispose();
  674. if (process.ExitCode == 0)
  675. {
  676. _logger.LogInformation("FFmpeg exited with code 0");
  677. }
  678. else
  679. {
  680. _logger.LogError("FFmpeg exited with code {0}", process.ExitCode);
  681. }
  682. job.Dispose();
  683. }
  684. private async Task AcquireResources(StreamState state, CancellationTokenSource cancellationTokenSource)
  685. {
  686. if (state.MediaSource.RequiresOpening && string.IsNullOrWhiteSpace(state.Request.LiveStreamId))
  687. {
  688. var liveStreamResponse = await _mediaSourceManager.OpenLiveStream(
  689. new LiveStreamRequest { OpenToken = state.MediaSource.OpenToken },
  690. cancellationTokenSource.Token)
  691. .ConfigureAwait(false);
  692. var encodingOptions = _serverConfigurationManager.GetEncodingOptions();
  693. _encodingHelper.AttachMediaSourceInfo(state, encodingOptions, liveStreamResponse.MediaSource, state.RequestedUrl);
  694. if (state.VideoRequest is not null)
  695. {
  696. _encodingHelper.TryStreamCopy(state);
  697. }
  698. }
  699. if (state.MediaSource.BufferMs.HasValue)
  700. {
  701. await Task.Delay(state.MediaSource.BufferMs.Value, cancellationTokenSource.Token).ConfigureAwait(false);
  702. }
  703. }
  704. /// <summary>
  705. /// Called when [transcode begin request].
  706. /// </summary>
  707. /// <param name="path">The path.</param>
  708. /// <param name="type">The type.</param>
  709. /// <returns>The <see cref="TranscodingJobDto"/>.</returns>
  710. public TranscodingJobDto? OnTranscodeBeginRequest(string path, TranscodingJobType type)
  711. {
  712. lock (_activeTranscodingJobs)
  713. {
  714. var job = _activeTranscodingJobs.FirstOrDefault(j => j.Type == type && string.Equals(j.Path, path, StringComparison.OrdinalIgnoreCase));
  715. if (job is null)
  716. {
  717. return null;
  718. }
  719. OnTranscodeBeginRequest(job);
  720. return job;
  721. }
  722. }
  723. private void OnTranscodeBeginRequest(TranscodingJobDto job)
  724. {
  725. job.ActiveRequestCount++;
  726. if (string.IsNullOrWhiteSpace(job.PlaySessionId) || job.Type == TranscodingJobType.Progressive)
  727. {
  728. job.StopKillTimer();
  729. }
  730. }
  731. /// <summary>
  732. /// Gets the transcoding lock.
  733. /// </summary>
  734. /// <param name="outputPath">The output path of the transcoded file.</param>
  735. /// <returns>A <see cref="SemaphoreSlim"/>.</returns>
  736. public SemaphoreSlim GetTranscodingLock(string outputPath)
  737. {
  738. lock (_transcodingLocks)
  739. {
  740. if (!_transcodingLocks.TryGetValue(outputPath, out SemaphoreSlim? result))
  741. {
  742. result = new SemaphoreSlim(1, 1);
  743. _transcodingLocks[outputPath] = result;
  744. }
  745. return result;
  746. }
  747. }
  748. private void OnPlaybackProgress(object? sender, PlaybackProgressEventArgs e)
  749. {
  750. if (!string.IsNullOrWhiteSpace(e.PlaySessionId))
  751. {
  752. PingTranscodingJob(e.PlaySessionId, e.IsPaused);
  753. }
  754. }
  755. /// <summary>
  756. /// Deletes the encoded media cache.
  757. /// </summary>
  758. private void DeleteEncodedMediaCache()
  759. {
  760. var path = _serverConfigurationManager.GetTranscodePath();
  761. if (!Directory.Exists(path))
  762. {
  763. return;
  764. }
  765. foreach (var file in _fileSystem.GetFilePaths(path, true))
  766. {
  767. _fileSystem.DeleteFile(file);
  768. }
  769. }
  770. /// <summary>
  771. /// Dispose transcoding job helper.
  772. /// </summary>
  773. public void Dispose()
  774. {
  775. Dispose(true);
  776. GC.SuppressFinalize(this);
  777. }
  778. /// <summary>
  779. /// Dispose throttler.
  780. /// </summary>
  781. /// <param name="disposing">Disposing.</param>
  782. protected virtual void Dispose(bool disposing)
  783. {
  784. if (disposing)
  785. {
  786. _loggerFactory.Dispose();
  787. _sessionManager.PlaybackProgress -= OnPlaybackProgress;
  788. _sessionManager.PlaybackStart -= OnPlaybackProgress;
  789. }
  790. }
  791. }