TranscodeManager.cs 28 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769
  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.Runtime.CompilerServices;
  8. using System.Text;
  9. using System.Text.Json;
  10. using System.Threading;
  11. using System.Threading.Tasks;
  12. using AsyncKeyedLock;
  13. using Jellyfin.Data;
  14. using Jellyfin.Database.Implementations.Enums;
  15. using Jellyfin.Extensions;
  16. using MediaBrowser.Common;
  17. using MediaBrowser.Common.Configuration;
  18. using MediaBrowser.Common.Extensions;
  19. using MediaBrowser.Controller.Configuration;
  20. using MediaBrowser.Controller.Library;
  21. using MediaBrowser.Controller.MediaEncoding;
  22. using MediaBrowser.Controller.Session;
  23. using MediaBrowser.Controller.Streaming;
  24. using MediaBrowser.Model.Dlna;
  25. using MediaBrowser.Model.Entities;
  26. using MediaBrowser.Model.IO;
  27. using MediaBrowser.Model.MediaInfo;
  28. using MediaBrowser.Model.Session;
  29. using Microsoft.Extensions.Logging;
  30. namespace MediaBrowser.MediaEncoding.Transcoding;
  31. /// <inheritdoc cref="ITranscodeManager"/>
  32. public sealed class TranscodeManager : ITranscodeManager, IDisposable
  33. {
  34. private readonly ILoggerFactory _loggerFactory;
  35. private readonly ILogger<TranscodeManager> _logger;
  36. private readonly IFileSystem _fileSystem;
  37. private readonly IApplicationPaths _appPaths;
  38. private readonly IServerConfigurationManager _serverConfigurationManager;
  39. private readonly IUserManager _userManager;
  40. private readonly ISessionManager _sessionManager;
  41. private readonly EncodingHelper _encodingHelper;
  42. private readonly IMediaEncoder _mediaEncoder;
  43. private readonly IMediaSourceManager _mediaSourceManager;
  44. private readonly IAttachmentExtractor _attachmentExtractor;
  45. private readonly List<TranscodingJob> _activeTranscodingJobs = new();
  46. private readonly AsyncKeyedLocker<string> _transcodingLocks = new(o =>
  47. {
  48. o.PoolSize = 20;
  49. o.PoolInitialFill = 1;
  50. });
  51. private readonly Version _maxFFmpegCkeyPauseSupported = new Version(6, 1);
  52. /// <summary>
  53. /// Initializes a new instance of the <see cref="TranscodeManager"/> class.
  54. /// </summary>
  55. /// <param name="loggerFactory">The <see cref="ILoggerFactory"/>.</param>
  56. /// <param name="fileSystem">The <see cref="IFileSystem"/>.</param>
  57. /// <param name="appPaths">The <see cref="IApplicationPaths"/>.</param>
  58. /// <param name="serverConfigurationManager">The <see cref="IServerConfigurationManager"/>.</param>
  59. /// <param name="userManager">The <see cref="IUserManager"/>.</param>
  60. /// <param name="sessionManager">The <see cref="ISessionManager"/>.</param>
  61. /// <param name="encodingHelper">The <see cref="EncodingHelper"/>.</param>
  62. /// <param name="mediaEncoder">The <see cref="IMediaEncoder"/>.</param>
  63. /// <param name="mediaSourceManager">The <see cref="IMediaSourceManager"/>.</param>
  64. /// <param name="attachmentExtractor">The <see cref="IAttachmentExtractor"/>.</param>
  65. public TranscodeManager(
  66. ILoggerFactory loggerFactory,
  67. IFileSystem fileSystem,
  68. IApplicationPaths appPaths,
  69. IServerConfigurationManager serverConfigurationManager,
  70. IUserManager userManager,
  71. ISessionManager sessionManager,
  72. EncodingHelper encodingHelper,
  73. IMediaEncoder mediaEncoder,
  74. IMediaSourceManager mediaSourceManager,
  75. IAttachmentExtractor attachmentExtractor)
  76. {
  77. _loggerFactory = loggerFactory;
  78. _fileSystem = fileSystem;
  79. _appPaths = appPaths;
  80. _serverConfigurationManager = serverConfigurationManager;
  81. _userManager = userManager;
  82. _sessionManager = sessionManager;
  83. _encodingHelper = encodingHelper;
  84. _mediaEncoder = mediaEncoder;
  85. _mediaSourceManager = mediaSourceManager;
  86. _attachmentExtractor = attachmentExtractor;
  87. _logger = loggerFactory.CreateLogger<TranscodeManager>();
  88. DeleteEncodedMediaCache();
  89. _sessionManager.PlaybackProgress += OnPlaybackProgress;
  90. _sessionManager.PlaybackStart += OnPlaybackProgress;
  91. }
  92. /// <inheritdoc />
  93. public TranscodingJob? GetTranscodingJob(string playSessionId)
  94. {
  95. lock (_activeTranscodingJobs)
  96. {
  97. return _activeTranscodingJobs.FirstOrDefault(j => string.Equals(j.PlaySessionId, playSessionId, StringComparison.OrdinalIgnoreCase));
  98. }
  99. }
  100. /// <inheritdoc />
  101. public TranscodingJob? GetTranscodingJob(string path, TranscodingJobType type)
  102. {
  103. lock (_activeTranscodingJobs)
  104. {
  105. return _activeTranscodingJobs.FirstOrDefault(j => j.Type == type && string.Equals(j.Path, path, StringComparison.OrdinalIgnoreCase));
  106. }
  107. }
  108. /// <inheritdoc />
  109. public void PingTranscodingJob(string playSessionId, bool? isUserPaused)
  110. {
  111. ArgumentException.ThrowIfNullOrEmpty(playSessionId);
  112. _logger.LogDebug("PingTranscodingJob PlaySessionId={0} isUsedPaused: {1}", playSessionId, isUserPaused);
  113. List<TranscodingJob> jobs;
  114. lock (_activeTranscodingJobs)
  115. {
  116. // This is really only needed for HLS.
  117. // Progressive streams can stop on their own reliably.
  118. jobs = _activeTranscodingJobs.Where(j => string.Equals(playSessionId, j.PlaySessionId, StringComparison.OrdinalIgnoreCase)).ToList();
  119. }
  120. foreach (var job in jobs)
  121. {
  122. if (isUserPaused.HasValue)
  123. {
  124. _logger.LogDebug("Setting job.IsUserPaused to {0}. jobId: {1}", isUserPaused, job.Id);
  125. job.IsUserPaused = isUserPaused.Value;
  126. }
  127. PingTimer(job, true);
  128. }
  129. }
  130. private void PingTimer(TranscodingJob job, bool isProgressCheckIn)
  131. {
  132. if (job.HasExited)
  133. {
  134. job.StopKillTimer();
  135. return;
  136. }
  137. var timerDuration = 10000;
  138. if (job.Type != TranscodingJobType.Progressive)
  139. {
  140. timerDuration = 60000;
  141. }
  142. job.PingTimeout = timerDuration;
  143. job.LastPingDate = DateTime.UtcNow;
  144. // Don't start the timer for playback checkins with progressive streaming
  145. if (job.Type != TranscodingJobType.Progressive || !isProgressCheckIn)
  146. {
  147. job.StartKillTimer(OnTranscodeKillTimerStopped);
  148. }
  149. else
  150. {
  151. job.ChangeKillTimerIfStarted();
  152. }
  153. }
  154. private async void OnTranscodeKillTimerStopped(object? state)
  155. {
  156. var job = state as TranscodingJob ?? throw new ArgumentException($"{nameof(state)} is not of type {nameof(TranscodingJob)}", nameof(state));
  157. if (!job.HasExited && job.Type != TranscodingJobType.Progressive)
  158. {
  159. var timeSinceLastPing = (DateTime.UtcNow - job.LastPingDate).TotalMilliseconds;
  160. if (timeSinceLastPing < job.PingTimeout)
  161. {
  162. job.StartKillTimer(OnTranscodeKillTimerStopped, job.PingTimeout);
  163. return;
  164. }
  165. }
  166. _logger.LogInformation("Transcoding kill timer stopped for JobId {0} PlaySessionId {1}. Killing transcoding", job.Id, job.PlaySessionId);
  167. await KillTranscodingJob(job, true, path => true).ConfigureAwait(false);
  168. }
  169. /// <inheritdoc />
  170. public Task KillTranscodingJobs(string deviceId, string? playSessionId, Func<string, bool> deleteFiles)
  171. {
  172. var jobs = new List<TranscodingJob>();
  173. lock (_activeTranscodingJobs)
  174. {
  175. // This is really only needed for HLS.
  176. // Progressive streams can stop on their own reliably.
  177. jobs.AddRange(_activeTranscodingJobs.Where(j => string.IsNullOrWhiteSpace(playSessionId)
  178. ? string.Equals(deviceId, j.DeviceId, StringComparison.OrdinalIgnoreCase)
  179. : string.Equals(playSessionId, j.PlaySessionId, StringComparison.OrdinalIgnoreCase)));
  180. }
  181. return Task.WhenAll(GetKillJobs());
  182. IEnumerable<Task> GetKillJobs()
  183. {
  184. foreach (var job in jobs)
  185. {
  186. yield return KillTranscodingJob(job, false, deleteFiles);
  187. }
  188. }
  189. }
  190. private async Task KillTranscodingJob(TranscodingJob job, bool closeLiveStream, Func<string, bool> delete)
  191. {
  192. job.DisposeKillTimer();
  193. _logger.LogDebug("KillTranscodingJob - JobId {0} PlaySessionId {1}. Killing transcoding", job.Id, job.PlaySessionId);
  194. lock (_activeTranscodingJobs)
  195. {
  196. _activeTranscodingJobs.Remove(job);
  197. if (job.CancellationTokenSource?.IsCancellationRequested == false)
  198. {
  199. #pragma warning disable CA1849 // Can't await in lock block
  200. job.CancellationTokenSource.Cancel();
  201. #pragma warning restore CA1849
  202. }
  203. }
  204. job.Stop();
  205. if (delete(job.Path!))
  206. {
  207. await DeletePartialStreamFiles(job.Path!, job.Type, 0, 1500).ConfigureAwait(false);
  208. }
  209. if (closeLiveStream && !string.IsNullOrWhiteSpace(job.LiveStreamId))
  210. {
  211. try
  212. {
  213. await _mediaSourceManager.CloseLiveStream(job.LiveStreamId).ConfigureAwait(false);
  214. }
  215. catch (Exception ex)
  216. {
  217. _logger.LogError(ex, "Error closing live stream for {Path}", job.Path);
  218. }
  219. }
  220. }
  221. private async Task DeletePartialStreamFiles(string path, TranscodingJobType jobType, int retryCount, int delayMs)
  222. {
  223. if (retryCount >= 10)
  224. {
  225. return;
  226. }
  227. _logger.LogInformation("Deleting partial stream file(s) {Path}", path);
  228. await Task.Delay(delayMs).ConfigureAwait(false);
  229. try
  230. {
  231. if (jobType == TranscodingJobType.Progressive)
  232. {
  233. DeleteProgressivePartialStreamFiles(path);
  234. }
  235. else
  236. {
  237. DeleteHlsPartialStreamFiles(path);
  238. }
  239. }
  240. catch (IOException ex)
  241. {
  242. _logger.LogError(ex, "Error deleting partial stream file(s) {Path}", path);
  243. await DeletePartialStreamFiles(path, jobType, retryCount + 1, 500).ConfigureAwait(false);
  244. }
  245. catch (Exception ex)
  246. {
  247. _logger.LogError(ex, "Error deleting partial stream file(s) {Path}", path);
  248. }
  249. }
  250. private void DeleteProgressivePartialStreamFiles(string outputFilePath)
  251. {
  252. if (File.Exists(outputFilePath))
  253. {
  254. _fileSystem.DeleteFile(outputFilePath);
  255. }
  256. }
  257. private void DeleteHlsPartialStreamFiles(string outputFilePath)
  258. {
  259. var directory = Path.GetDirectoryName(outputFilePath)
  260. ?? throw new ArgumentException("Path can't be a root directory.", nameof(outputFilePath));
  261. var name = Path.GetFileNameWithoutExtension(outputFilePath);
  262. var filesToDelete = _fileSystem.GetFilePaths(directory)
  263. .Where(f => f.Contains(name, StringComparison.OrdinalIgnoreCase));
  264. List<Exception>? exs = null;
  265. foreach (var file in filesToDelete)
  266. {
  267. try
  268. {
  269. _logger.LogDebug("Deleting HLS file {0}", file);
  270. _fileSystem.DeleteFile(file);
  271. }
  272. catch (IOException ex)
  273. {
  274. (exs ??= new List<Exception>()).Add(ex);
  275. _logger.LogError(ex, "Error deleting HLS file {Path}", file);
  276. }
  277. }
  278. if (exs is not null)
  279. {
  280. throw new AggregateException("Error deleting HLS files", exs);
  281. }
  282. }
  283. /// <inheritdoc />
  284. public void ReportTranscodingProgress(
  285. TranscodingJob job,
  286. StreamState state,
  287. TimeSpan? transcodingPosition,
  288. float? framerate,
  289. double? percentComplete,
  290. long? bytesTranscoded,
  291. int? bitRate)
  292. {
  293. var ticks = transcodingPosition?.Ticks;
  294. if (job is not null)
  295. {
  296. job.Framerate = framerate;
  297. job.CompletionPercentage = percentComplete;
  298. job.TranscodingPositionTicks = ticks;
  299. job.BytesTranscoded = bytesTranscoded;
  300. job.BitRate = bitRate;
  301. }
  302. var deviceId = state.Request.DeviceId;
  303. if (!string.IsNullOrWhiteSpace(deviceId))
  304. {
  305. var audioCodec = state.ActualOutputAudioCodec;
  306. var videoCodec = state.ActualOutputVideoCodec;
  307. var hardwareAccelerationType = _serverConfigurationManager.GetEncodingOptions().HardwareAccelerationType;
  308. _sessionManager.ReportTranscodingInfo(deviceId, new TranscodingInfo
  309. {
  310. Bitrate = bitRate ?? state.TotalOutputBitrate,
  311. AudioCodec = audioCodec,
  312. VideoCodec = videoCodec,
  313. Container = state.OutputContainer,
  314. Framerate = framerate,
  315. CompletionPercentage = percentComplete,
  316. Width = state.OutputWidth,
  317. Height = state.OutputHeight,
  318. AudioChannels = state.OutputAudioChannels,
  319. IsAudioDirect = EncodingHelper.IsCopyCodec(state.OutputAudioCodec),
  320. IsVideoDirect = EncodingHelper.IsCopyCodec(state.OutputVideoCodec),
  321. HardwareAccelerationType = hardwareAccelerationType,
  322. TranscodeReasons = state.TranscodeReasons
  323. });
  324. }
  325. }
  326. /// <inheritdoc />
  327. public async Task<TranscodingJob> StartFfMpeg(
  328. StreamState state,
  329. string outputPath,
  330. string commandLineArguments,
  331. Guid userId,
  332. TranscodingJobType transcodingJobType,
  333. CancellationTokenSource cancellationTokenSource,
  334. string? workingDirectory = null)
  335. {
  336. var directory = Path.GetDirectoryName(outputPath) ?? throw new ArgumentException($"Provided path ({outputPath}) is not valid.", nameof(outputPath));
  337. Directory.CreateDirectory(directory);
  338. await AcquireResources(state, cancellationTokenSource).ConfigureAwait(false);
  339. if (state.VideoRequest is not null && !EncodingHelper.IsCopyCodec(state.OutputVideoCodec))
  340. {
  341. var user = userId.IsEmpty() ? null : _userManager.GetUserById(userId);
  342. if (user is not null && !user.HasPermission(PermissionKind.EnableVideoPlaybackTranscoding))
  343. {
  344. OnTranscodeFailedToStart(outputPath, transcodingJobType, state);
  345. throw new ArgumentException("User does not have access to video transcoding.");
  346. }
  347. }
  348. ArgumentException.ThrowIfNullOrEmpty(_mediaEncoder.EncoderPath);
  349. // If subtitles get burned in fonts may need to be extracted from the media file
  350. if (state.SubtitleStream is not null && state.SubtitleDeliveryMethod == SubtitleDeliveryMethod.Encode)
  351. {
  352. var attachmentPath = Path.Combine(_appPaths.CachePath, "attachments", state.MediaSource.Id);
  353. if (state.MediaSource.VideoType == VideoType.Dvd || state.MediaSource.VideoType == VideoType.BluRay)
  354. {
  355. var concatPath = Path.Join(_appPaths.CachePath, "concat", state.MediaSource.Id + ".concat");
  356. await _attachmentExtractor.ExtractAllAttachments(concatPath, state.MediaSource, attachmentPath, cancellationTokenSource.Token).ConfigureAwait(false);
  357. }
  358. else
  359. {
  360. await _attachmentExtractor.ExtractAllAttachments(state.MediaPath, state.MediaSource, attachmentPath, cancellationTokenSource.Token).ConfigureAwait(false);
  361. }
  362. if (state.SubtitleStream.IsExternal && Path.GetExtension(state.SubtitleStream.Path.AsSpan()).Equals(".mks", StringComparison.OrdinalIgnoreCase))
  363. {
  364. string subtitlePath = state.SubtitleStream.Path;
  365. string subtitlePathArgument = string.Format(CultureInfo.InvariantCulture, "file:\"{0}\"", subtitlePath.Replace("\"", "\\\"", StringComparison.Ordinal));
  366. string subtitleId = subtitlePath.GetMD5().ToString("N", CultureInfo.InvariantCulture);
  367. await _attachmentExtractor.ExtractAllAttachmentsExternal(subtitlePathArgument, subtitleId, attachmentPath, cancellationTokenSource.Token).ConfigureAwait(false);
  368. }
  369. }
  370. var process = new Process
  371. {
  372. StartInfo = new ProcessStartInfo
  373. {
  374. WindowStyle = ProcessWindowStyle.Hidden,
  375. CreateNoWindow = true,
  376. UseShellExecute = false,
  377. // Must consume both stdout and stderr or deadlocks may occur
  378. // RedirectStandardOutput = true,
  379. RedirectStandardError = true,
  380. RedirectStandardInput = true,
  381. FileName = _mediaEncoder.EncoderPath,
  382. Arguments = commandLineArguments,
  383. WorkingDirectory = string.IsNullOrWhiteSpace(workingDirectory) ? string.Empty : workingDirectory,
  384. ErrorDialog = false
  385. },
  386. EnableRaisingEvents = true
  387. };
  388. var transcodingJob = OnTranscodeBeginning(
  389. outputPath,
  390. state.Request.PlaySessionId,
  391. state.MediaSource.LiveStreamId,
  392. Guid.NewGuid().ToString("N", CultureInfo.InvariantCulture),
  393. transcodingJobType,
  394. process,
  395. state.Request.DeviceId,
  396. state,
  397. cancellationTokenSource);
  398. _logger.LogInformation("{Filename} {Arguments}", process.StartInfo.FileName, process.StartInfo.Arguments);
  399. var logFilePrefix = "FFmpeg.Transcode-";
  400. if (state.VideoRequest is not null
  401. && EncodingHelper.IsCopyCodec(state.OutputVideoCodec))
  402. {
  403. logFilePrefix = EncodingHelper.IsCopyCodec(state.OutputAudioCodec)
  404. ? "FFmpeg.Remux-"
  405. : "FFmpeg.DirectStream-";
  406. }
  407. if (state.VideoRequest is null && EncodingHelper.IsCopyCodec(state.OutputAudioCodec))
  408. {
  409. logFilePrefix = "FFmpeg.Remux-";
  410. }
  411. var logFilePath = Path.Combine(
  412. _serverConfigurationManager.ApplicationPaths.LogDirectoryPath,
  413. $"{logFilePrefix}{DateTime.Now:yyyy-MM-dd_HH-mm-ss}_{state.Request.MediaSourceId}_{Guid.NewGuid().ToString()[..8]}.log");
  414. // FFmpeg writes debug/error info to stderr. This is useful when debugging so let's put it in the log directory.
  415. Stream logStream = new FileStream(
  416. logFilePath,
  417. FileMode.Create,
  418. FileAccess.Write,
  419. FileShare.Read,
  420. IODefaults.FileStreamBufferSize,
  421. FileOptions.Asynchronous);
  422. await JsonSerializer.SerializeAsync(logStream, state.MediaSource, cancellationToken: cancellationTokenSource.Token).ConfigureAwait(false);
  423. var commandLineLogMessageBytes = Encoding.UTF8.GetBytes(
  424. Environment.NewLine
  425. + Environment.NewLine
  426. + process.StartInfo.FileName + " " + process.StartInfo.Arguments
  427. + Environment.NewLine
  428. + Environment.NewLine);
  429. await logStream.WriteAsync(commandLineLogMessageBytes, cancellationTokenSource.Token).ConfigureAwait(false);
  430. process.Exited += (_, _) => OnFfMpegProcessExited(process, transcodingJob, state);
  431. try
  432. {
  433. process.Start();
  434. }
  435. catch (Exception ex)
  436. {
  437. _logger.LogError(ex, "Error starting FFmpeg");
  438. OnTranscodeFailedToStart(outputPath, transcodingJobType, state);
  439. throw;
  440. }
  441. _logger.LogDebug("Launched FFmpeg process");
  442. state.TranscodingJob = transcodingJob;
  443. // Important - don't await the log task or we won't be able to kill FFmpeg when the user stops playback
  444. _ = new JobLogger(_logger).StartStreamingLog(state, process.StandardError, logStream);
  445. // Wait for the file to exist before proceeding
  446. var ffmpegTargetFile = state.WaitForPath ?? outputPath;
  447. _logger.LogDebug("Waiting for the creation of {0}", ffmpegTargetFile);
  448. while (!File.Exists(ffmpegTargetFile) && !transcodingJob.HasExited)
  449. {
  450. await Task.Delay(100, cancellationTokenSource.Token).ConfigureAwait(false);
  451. }
  452. _logger.LogDebug("File {0} created or transcoding has finished", ffmpegTargetFile);
  453. if (state.IsInputVideo && transcodingJob.Type == TranscodingJobType.Progressive && !transcodingJob.HasExited)
  454. {
  455. await Task.Delay(1000, cancellationTokenSource.Token).ConfigureAwait(false);
  456. if (state.ReadInputAtNativeFramerate && !transcodingJob.HasExited)
  457. {
  458. await Task.Delay(1500, cancellationTokenSource.Token).ConfigureAwait(false);
  459. }
  460. }
  461. if (!transcodingJob.HasExited)
  462. {
  463. StartThrottler(state, transcodingJob);
  464. StartSegmentCleaner(state, transcodingJob);
  465. }
  466. else if (transcodingJob.ExitCode != 0)
  467. {
  468. throw new FfmpegException(string.Format(CultureInfo.InvariantCulture, "FFmpeg exited with code {0}", transcodingJob.ExitCode));
  469. }
  470. _logger.LogDebug("StartFfMpeg() finished successfully");
  471. return transcodingJob;
  472. }
  473. private void StartThrottler(StreamState state, TranscodingJob transcodingJob)
  474. {
  475. if (EnableThrottling(state)
  476. && (_mediaEncoder.IsPkeyPauseSupported
  477. || _mediaEncoder.EncoderVersion <= _maxFFmpegCkeyPauseSupported))
  478. {
  479. transcodingJob.TranscodingThrottler = new TranscodingThrottler(transcodingJob, _loggerFactory.CreateLogger<TranscodingThrottler>(), _serverConfigurationManager, _fileSystem, _mediaEncoder);
  480. transcodingJob.TranscodingThrottler.Start();
  481. }
  482. }
  483. private static bool EnableThrottling(StreamState state)
  484. => state.InputProtocol == MediaProtocol.File
  485. && state.RunTimeTicks.HasValue
  486. && state.RunTimeTicks.Value >= TimeSpan.FromMinutes(5).Ticks
  487. && state.IsInputVideo
  488. && state.VideoType == VideoType.VideoFile;
  489. private void StartSegmentCleaner(StreamState state, TranscodingJob transcodingJob)
  490. {
  491. if (EnableSegmentCleaning(state))
  492. {
  493. transcodingJob.TranscodingSegmentCleaner = new TranscodingSegmentCleaner(transcodingJob, _loggerFactory.CreateLogger<TranscodingSegmentCleaner>(), _serverConfigurationManager, _fileSystem, _mediaEncoder, state.SegmentLength);
  494. transcodingJob.TranscodingSegmentCleaner.Start();
  495. }
  496. }
  497. private static bool EnableSegmentCleaning(StreamState state)
  498. => state.InputProtocol is MediaProtocol.File or MediaProtocol.Http
  499. && state.IsInputVideo
  500. && state.TranscodingType == TranscodingJobType.Hls
  501. && state.RunTimeTicks.HasValue
  502. && state.RunTimeTicks.Value >= TimeSpan.FromMinutes(5).Ticks;
  503. private TranscodingJob OnTranscodeBeginning(
  504. string path,
  505. string? playSessionId,
  506. string? liveStreamId,
  507. string transcodingJobId,
  508. TranscodingJobType type,
  509. Process process,
  510. string? deviceId,
  511. StreamState state,
  512. CancellationTokenSource cancellationTokenSource)
  513. {
  514. lock (_activeTranscodingJobs)
  515. {
  516. var job = new TranscodingJob(_loggerFactory.CreateLogger<TranscodingJob>())
  517. {
  518. Type = type,
  519. Path = path,
  520. Process = process,
  521. ActiveRequestCount = 1,
  522. DeviceId = deviceId,
  523. CancellationTokenSource = cancellationTokenSource,
  524. Id = transcodingJobId,
  525. PlaySessionId = playSessionId,
  526. LiveStreamId = liveStreamId,
  527. MediaSource = state.MediaSource
  528. };
  529. _activeTranscodingJobs.Add(job);
  530. ReportTranscodingProgress(job, state, null, null, null, null, null);
  531. return job;
  532. }
  533. }
  534. /// <inheritdoc />
  535. public void OnTranscodeEndRequest(TranscodingJob job)
  536. {
  537. job.ActiveRequestCount--;
  538. _logger.LogDebug("OnTranscodeEndRequest job.ActiveRequestCount={ActiveRequestCount}", job.ActiveRequestCount);
  539. if (job.ActiveRequestCount <= 0)
  540. {
  541. PingTimer(job, false);
  542. }
  543. }
  544. private void OnTranscodeFailedToStart(string path, TranscodingJobType type, StreamState state)
  545. {
  546. lock (_activeTranscodingJobs)
  547. {
  548. var job = _activeTranscodingJobs.FirstOrDefault(j => j.Type == type && string.Equals(j.Path, path, StringComparison.OrdinalIgnoreCase));
  549. if (job is not null)
  550. {
  551. _activeTranscodingJobs.Remove(job);
  552. }
  553. }
  554. if (!string.IsNullOrWhiteSpace(state.Request.DeviceId))
  555. {
  556. _sessionManager.ClearTranscodingInfo(state.Request.DeviceId);
  557. }
  558. }
  559. private void OnFfMpegProcessExited(Process process, TranscodingJob job, StreamState state)
  560. {
  561. job.HasExited = true;
  562. job.ExitCode = process.ExitCode;
  563. ReportTranscodingProgress(job, state, null, null, null, null, null);
  564. _logger.LogDebug("Disposing stream resources");
  565. state.Dispose();
  566. if (process.ExitCode == 0)
  567. {
  568. _logger.LogInformation("FFmpeg exited with code 0");
  569. }
  570. else
  571. {
  572. _logger.LogError("FFmpeg exited with code {0}", process.ExitCode);
  573. }
  574. job.Dispose();
  575. }
  576. private async Task AcquireResources(StreamState state, CancellationTokenSource cancellationTokenSource)
  577. {
  578. if (state.MediaSource.RequiresOpening && string.IsNullOrWhiteSpace(state.Request.LiveStreamId))
  579. {
  580. var liveStreamResponse = await _mediaSourceManager.OpenLiveStream(
  581. new LiveStreamRequest { OpenToken = state.MediaSource.OpenToken },
  582. cancellationTokenSource.Token)
  583. .ConfigureAwait(false);
  584. var encodingOptions = _serverConfigurationManager.GetEncodingOptions();
  585. _encodingHelper.AttachMediaSourceInfo(state, encodingOptions, liveStreamResponse.MediaSource, state.RequestedUrl);
  586. if (state.VideoRequest is not null)
  587. {
  588. _encodingHelper.TryStreamCopy(state);
  589. }
  590. }
  591. if (state.MediaSource.BufferMs.HasValue)
  592. {
  593. await Task.Delay(state.MediaSource.BufferMs.Value, cancellationTokenSource.Token).ConfigureAwait(false);
  594. }
  595. }
  596. /// <inheritdoc />
  597. public TranscodingJob? OnTranscodeBeginRequest(string path, TranscodingJobType type)
  598. {
  599. lock (_activeTranscodingJobs)
  600. {
  601. var job = _activeTranscodingJobs
  602. .FirstOrDefault(j => j.Type == type && string.Equals(j.Path, path, StringComparison.OrdinalIgnoreCase));
  603. if (job is null)
  604. {
  605. return null;
  606. }
  607. job.ActiveRequestCount++;
  608. if (string.IsNullOrWhiteSpace(job.PlaySessionId) || job.Type == TranscodingJobType.Progressive)
  609. {
  610. job.StopKillTimer();
  611. }
  612. return job;
  613. }
  614. }
  615. private void OnPlaybackProgress(object? sender, PlaybackProgressEventArgs e)
  616. {
  617. if (!string.IsNullOrWhiteSpace(e.PlaySessionId))
  618. {
  619. PingTranscodingJob(e.PlaySessionId, e.IsPaused);
  620. }
  621. }
  622. private void DeleteEncodedMediaCache()
  623. {
  624. var path = _serverConfigurationManager.GetTranscodePath();
  625. if (!Directory.Exists(path))
  626. {
  627. return;
  628. }
  629. foreach (var file in _fileSystem.GetFilePaths(path, true))
  630. {
  631. try
  632. {
  633. _fileSystem.DeleteFile(file);
  634. }
  635. catch (Exception ex)
  636. {
  637. _logger.LogError(ex, "Error deleting encoded media cache file {Path}", path);
  638. }
  639. }
  640. }
  641. /// <summary>
  642. /// Transcoding lock.
  643. /// </summary>
  644. /// <param name="outputPath">The output path of the transcoded file.</param>
  645. /// <param name="cancellationToken">The cancellation token.</param>
  646. /// <returns>An <see cref="IDisposable"/>.</returns>
  647. [MethodImpl(MethodImplOptions.AggressiveInlining)]
  648. public ValueTask<IDisposable> LockAsync(string outputPath, CancellationToken cancellationToken)
  649. {
  650. return _transcodingLocks.LockAsync(outputPath, cancellationToken);
  651. }
  652. /// <inheritdoc />
  653. public void Dispose()
  654. {
  655. _sessionManager.PlaybackProgress -= OnPlaybackProgress;
  656. _sessionManager.PlaybackStart -= OnPlaybackProgress;
  657. _transcodingLocks.Dispose();
  658. }
  659. }