TranscodingJobHelper.cs 37 KB

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