diff --git a/Jellyfin.Api/Controllers/DynamicHlsController.cs b/Jellyfin.Api/Controllers/DynamicHlsController.cs index 6b2c423632..260ed4787b 100644 --- a/Jellyfin.Api/Controllers/DynamicHlsController.cs +++ b/Jellyfin.Api/Controllers/DynamicHlsController.cs @@ -287,7 +287,7 @@ public class DynamicHlsController : BaseJellyfinApiController cancellationToken) .ConfigureAwait(false); - TranscodingJobDto? job = null; + TranscodingJob? job = null; var playlistPath = Path.ChangeExtension(state.OutputFilePath, ".m3u8"); if (!System.IO.File.Exists(playlistPath)) @@ -1431,7 +1431,7 @@ public class DynamicHlsController : BaseJellyfinApiController var segmentExtension = EncodingHelper.GetSegmentFileExtension(state.Request.SegmentContainer); - TranscodingJobDto? job; + TranscodingJob? job; if (System.IO.File.Exists(segmentPath)) { @@ -1921,7 +1921,7 @@ public class DynamicHlsController : BaseJellyfinApiController string segmentPath, string segmentExtension, int segmentIndex, - TranscodingJobDto? transcodingJob, + TranscodingJob? transcodingJob, CancellationToken cancellationToken) { var segmentExists = System.IO.File.Exists(segmentPath); @@ -1990,7 +1990,7 @@ public class DynamicHlsController : BaseJellyfinApiController return GetSegmentResult(state, segmentPath, transcodingJob); } - private ActionResult GetSegmentResult(StreamState state, string segmentPath, TranscodingJobDto? transcodingJob) + private ActionResult GetSegmentResult(StreamState state, string segmentPath, TranscodingJob? transcodingJob) { var segmentEndingPositionTicks = state.Request.CurrentRuntimeTicks + state.Request.ActualSegmentLengthTicks; diff --git a/Jellyfin.Api/Helpers/FileStreamResponseHelpers.cs b/Jellyfin.Api/Helpers/FileStreamResponseHelpers.cs index ce2e476b7d..fafa2c0551 100644 --- a/Jellyfin.Api/Helpers/FileStreamResponseHelpers.cs +++ b/Jellyfin.Api/Helpers/FileStreamResponseHelpers.cs @@ -96,7 +96,7 @@ public static class FileStreamResponseHelpers await transcodingLock.WaitAsync(cancellationTokenSource.Token).ConfigureAwait(false); try { - TranscodingJobDto? job; + TranscodingJob? job; if (!File.Exists(outputPath)) { job = await transcodingJobHelper.StartFfMpeg(state, outputPath, ffmpegCommandLineArguments, httpContext.Request, transcodingJobType, cancellationTokenSource).ConfigureAwait(false); diff --git a/Jellyfin.Api/Helpers/ProgressiveFileStream.cs b/Jellyfin.Api/Helpers/ProgressiveFileStream.cs index 379ec6e27b..18088483d3 100644 --- a/Jellyfin.Api/Helpers/ProgressiveFileStream.cs +++ b/Jellyfin.Api/Helpers/ProgressiveFileStream.cs @@ -14,7 +14,7 @@ namespace Jellyfin.Api.Helpers; public class ProgressiveFileStream : Stream { private readonly Stream _stream; - private readonly TranscodingJobDto? _job; + private readonly TranscodingJob? _job; private readonly TranscodingJobHelper? _transcodingJobHelper; private readonly int _timeoutMs; private bool _disposed; @@ -26,7 +26,7 @@ public class ProgressiveFileStream : Stream /// The transcoding job information. /// The transcoding job helper. /// The timeout duration in milliseconds. - public ProgressiveFileStream(string filePath, TranscodingJobDto? job, TranscodingJobHelper transcodingJobHelper, int timeoutMs = 30000) + public ProgressiveFileStream(string filePath, TranscodingJob? job, TranscodingJobHelper transcodingJobHelper, int timeoutMs = 30000) { _job = job; _transcodingJobHelper = transcodingJobHelper; diff --git a/Jellyfin.Api/Helpers/TranscodingJobHelper.cs b/Jellyfin.Api/Helpers/TranscodingJobHelper.cs index 14436909c9..9a6ec17fdb 100644 --- a/Jellyfin.Api/Helpers/TranscodingJobHelper.cs +++ b/Jellyfin.Api/Helpers/TranscodingJobHelper.cs @@ -36,7 +36,7 @@ public class TranscodingJobHelper : IDisposable /// /// The active transcoding jobs. /// - private static readonly List _activeTranscodingJobs = new List(); + private static readonly List _activeTranscodingJobs = new List(); /// /// The transcoding locks. @@ -105,7 +105,7 @@ public class TranscodingJobHelper : IDisposable /// /// Playback session id. /// The transcoding job. - public TranscodingJobDto? GetTranscodingJob(string playSessionId) + public TranscodingJob? GetTranscodingJob(string playSessionId) { lock (_activeTranscodingJobs) { @@ -119,7 +119,7 @@ public class TranscodingJobHelper : IDisposable /// Path to the transcoding file. /// The . /// The transcoding job. - public TranscodingJobDto? GetTranscodingJob(string path, TranscodingJobType type) + public TranscodingJob? GetTranscodingJob(string path, TranscodingJobType type) { lock (_activeTranscodingJobs) { @@ -139,7 +139,7 @@ public class TranscodingJobHelper : IDisposable _logger.LogDebug("PingTranscodingJob PlaySessionId={0} isUsedPaused: {1}", playSessionId, isUserPaused); - List jobs; + List jobs; lock (_activeTranscodingJobs) { @@ -160,7 +160,7 @@ public class TranscodingJobHelper : IDisposable } } - private void PingTimer(TranscodingJobDto job, bool isProgressCheckIn) + private void PingTimer(TranscodingJob job, bool isProgressCheckIn) { if (job.HasExited) { @@ -195,7 +195,7 @@ public class TranscodingJobHelper : IDisposable /// The state. private async void OnTranscodeKillTimerStopped(object? state) { - var job = state as TranscodingJobDto ?? throw new ArgumentException($"{nameof(state)} is not of type {nameof(TranscodingJobDto)}", nameof(state)); + var job = state as TranscodingJob ?? throw new ArgumentException($"{nameof(state)} is not of type {nameof(TranscodingJob)}", nameof(state)); if (!job.HasExited && job.Type != TranscodingJobType.Progressive) { var timeSinceLastPing = (DateTime.UtcNow - job.LastPingDate).TotalMilliseconds; @@ -234,9 +234,9 @@ public class TranscodingJobHelper : IDisposable /// The kill job. /// The delete files. /// Task. - private Task KillTranscodingJobs(Func killJob, Func deleteFiles) + private Task KillTranscodingJobs(Func killJob, Func deleteFiles) { - var jobs = new List(); + var jobs = new List(); lock (_activeTranscodingJobs) { @@ -267,7 +267,7 @@ public class TranscodingJobHelper : IDisposable /// The job. /// if set to true [close live stream]. /// The delete. - private async Task KillTranscodingJob(TranscodingJobDto job, bool closeLiveStream, Func delete) + private async Task KillTranscodingJob(TranscodingJob job, bool closeLiveStream, Func delete) { job.DisposeKillTimer(); @@ -281,6 +281,7 @@ public class TranscodingJobHelper : IDisposable { #pragma warning disable CA1849 // Can't await in lock block job.CancellationTokenSource.Cancel(); +#pragma warning restore CA1849 } } @@ -289,35 +290,7 @@ public class TranscodingJobHelper : IDisposable _transcodingLocks.Remove(job.Path!); } - lock (job.ProcessLock!) - { - job.TranscodingThrottler?.Stop().GetAwaiter().GetResult(); - - var process = job.Process; - - var hasExited = job.HasExited; - - if (!hasExited) - { - try - { - _logger.LogInformation("Stopping ffmpeg process with q command for {Path}", job.Path); - - process!.StandardInput.WriteLine("q"); - - // Need to wait because killing is asynchronous. - if (!process.WaitForExit(5000)) - { - _logger.LogInformation("Killing FFmpeg process for {Path}", job.Path); - process.Kill(); - } - } - catch (InvalidOperationException) - { - } - } -#pragma warning restore CA1849 - } + job.Stop(); if (delete(job.Path!)) { @@ -430,7 +403,7 @@ public class TranscodingJobHelper : IDisposable /// /// Report the transcoding progress to the session manager. /// - /// The of which the progress will be reported. + /// The of which the progress will be reported. /// The of the current transcoding job. /// The current transcoding position. /// The framerate of the transcoding job. @@ -438,7 +411,7 @@ public class TranscodingJobHelper : IDisposable /// The number of bytes transcoded. /// The bitrate of the transcoding job. public void ReportTranscodingProgress( - TranscodingJobDto job, + TranscodingJob job, StreamState state, TimeSpan? transcodingPosition, float? framerate, @@ -500,7 +473,7 @@ public class TranscodingJobHelper : IDisposable /// The cancellation token source. /// The working directory. /// Task. - public async Task StartFfMpeg( + public async Task StartFfMpeg( StreamState state, string outputPath, string commandLineArguments, @@ -655,7 +628,7 @@ public class TranscodingJobHelper : IDisposable return transcodingJob; } - private void StartThrottler(StreamState state, TranscodingJobDto transcodingJob) + private void StartThrottler(StreamState state, TranscodingJob transcodingJob) { if (EnableThrottling(state)) { @@ -688,7 +661,7 @@ public class TranscodingJobHelper : IDisposable /// The state. /// The cancellation token source. /// TranscodingJob. - public TranscodingJobDto OnTranscodeBeginning( + public TranscodingJob OnTranscodeBeginning( string path, string? playSessionId, string? liveStreamId, @@ -701,7 +674,7 @@ public class TranscodingJobHelper : IDisposable { lock (_activeTranscodingJobs) { - var job = new TranscodingJobDto(_loggerFactory.CreateLogger()) + var job = new TranscodingJob(_loggerFactory.CreateLogger()) { Type = type, Path = path, @@ -727,7 +700,7 @@ public class TranscodingJobHelper : IDisposable /// Called when [transcode end]. /// /// The transcode job. - public void OnTranscodeEndRequest(TranscodingJobDto job) + public void OnTranscodeEndRequest(TranscodingJob job) { job.ActiveRequestCount--; _logger.LogDebug("OnTranscodeEndRequest job.ActiveRequestCount={ActiveRequestCount}", job.ActiveRequestCount); @@ -775,7 +748,7 @@ public class TranscodingJobHelper : IDisposable /// The process. /// The job. /// The state. - private void OnFfMpegProcessExited(Process process, TranscodingJobDto job, StreamState state) + private void OnFfMpegProcessExited(Process process, TranscodingJob job, StreamState state) { job.HasExited = true; job.ExitCode = process.ExitCode; @@ -826,8 +799,8 @@ public class TranscodingJobHelper : IDisposable /// /// The path. /// The type. - /// The . - public TranscodingJobDto? OnTranscodeBeginRequest(string path, TranscodingJobType type) + /// The . + public TranscodingJob? OnTranscodeBeginRequest(string path, TranscodingJobType type) { lock (_activeTranscodingJobs) { @@ -844,7 +817,7 @@ public class TranscodingJobHelper : IDisposable } } - private void OnTranscodeBeginRequest(TranscodingJobDto job) + private void OnTranscodeBeginRequest(TranscodingJob job) { job.ActiveRequestCount++; diff --git a/Jellyfin.Api/Models/StreamingDtos/StreamState.cs b/Jellyfin.Api/Models/StreamingDtos/StreamState.cs index 4b69392ef4..439f8052c1 100644 --- a/Jellyfin.Api/Models/StreamingDtos/StreamState.cs +++ b/Jellyfin.Api/Models/StreamingDtos/StreamState.cs @@ -140,7 +140,7 @@ public class StreamState : EncodingJobInfo, IDisposable /// /// Gets or sets the transcoding job. /// - public TranscodingJobDto? TranscodingJob { get; set; } + public TranscodingJob? TranscodingJob { get; set; } /// public void Dispose() diff --git a/MediaBrowser.Controller/MediaEncoding/TranscodingJobDto.cs b/MediaBrowser.Controller/MediaEncoding/TranscodingJob.cs similarity index 65% rename from MediaBrowser.Controller/MediaEncoding/TranscodingJobDto.cs rename to MediaBrowser.Controller/MediaEncoding/TranscodingJob.cs index 6f929204ff..1e6d5933c8 100644 --- a/MediaBrowser.Controller/MediaEncoding/TranscodingJobDto.cs +++ b/MediaBrowser.Controller/MediaEncoding/TranscodingJob.cs @@ -1,6 +1,5 @@ using System; using System.Diagnostics; -using System.Diagnostics.CodeAnalysis; using System.Threading; using MediaBrowser.Model.Dto; using Microsoft.Extensions.Logging; @@ -10,39 +9,31 @@ namespace MediaBrowser.Controller.MediaEncoding; /// /// Class TranscodingJob. /// -public class TranscodingJobDto : IDisposable +public sealed class TranscodingJob : IDisposable { - /// - /// The process lock. - /// - [SuppressMessage("Microsoft.Performance", "CA1051:NoVisibleInstanceFields", MessageId = "ProcessLock", Justification = "Imported from ServiceStack")] - [SuppressMessage("Microsoft.Performance", "SA1401:PrivateField", MessageId = "ProcessLock", Justification = "Imported from ServiceStack")] - public readonly object ProcessLock = new object(); + private readonly ILogger _logger; + private readonly object _processLock = new(); + private readonly object _timerLock = new(); + + private Timer? _killTimer; /// - /// Timer lock. - /// - private readonly object _timerLock = new object(); - - /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the class. /// /// Instance of the interface. - public TranscodingJobDto(ILogger logger) + public TranscodingJob(ILogger logger) { - Logger = logger; + _logger = logger; } /// /// Gets or sets the play session identifier. /// - /// The play session identifier. public string? PlaySessionId { get; set; } /// /// Gets or sets the live stream identifier. /// - /// The live stream identifier. public string? LiveStreamId { get; set; } /// @@ -53,7 +44,6 @@ public class TranscodingJobDto : IDisposable /// /// Gets or sets the path. /// - /// The path. public MediaSourceInfo? MediaSource { get; set; } /// @@ -64,32 +54,18 @@ public class TranscodingJobDto : IDisposable /// /// Gets or sets the type. /// - /// The type. public TranscodingJobType Type { get; set; } /// /// Gets or sets the process. /// - /// The process. public Process? Process { get; set; } - /// - /// Gets logger. - /// - public ILogger Logger { get; private set; } - /// /// Gets or sets the active request count. /// - /// The active request count. public int ActiveRequestCount { get; set; } - /// - /// Gets or sets the kill timer. - /// - /// The kill timer. - private Timer? KillTimer { get; set; } - /// /// Gets or sets device id. /// @@ -177,7 +153,7 @@ public class TranscodingJobDto : IDisposable { lock (_timerLock) { - KillTimer?.Change(Timeout.Infinite, Timeout.Infinite); + _killTimer?.Change(Timeout.Infinite, Timeout.Infinite); } } @@ -188,10 +164,10 @@ public class TranscodingJobDto : IDisposable { lock (_timerLock) { - if (KillTimer is not null) + if (_killTimer is not null) { - KillTimer.Dispose(); - KillTimer = null; + _killTimer.Dispose(); + _killTimer = null; } } } @@ -219,15 +195,15 @@ public class TranscodingJobDto : IDisposable lock (_timerLock) { - if (KillTimer is null) + if (_killTimer is null) { - Logger.LogDebug("Starting kill timer at {0}ms. JobId {1} PlaySessionId {2}", intervalMs, Id, PlaySessionId); - KillTimer = new Timer(new TimerCallback(callback), this, intervalMs, Timeout.Infinite); + _logger.LogDebug("Starting kill timer at {0}ms. JobId {1} PlaySessionId {2}", intervalMs, Id, PlaySessionId); + _killTimer = new Timer(new TimerCallback(callback), this, intervalMs, Timeout.Infinite); } else { - Logger.LogDebug("Changing kill timer to {0}ms. JobId {1} PlaySessionId {2}", intervalMs, Id, PlaySessionId); - KillTimer.Change(intervalMs, Timeout.Infinite); + _logger.LogDebug("Changing kill timer to {0}ms. JobId {1} PlaySessionId {2}", intervalMs, Id, PlaySessionId); + _killTimer.Change(intervalMs, Timeout.Infinite); } } } @@ -244,39 +220,61 @@ public class TranscodingJobDto : IDisposable lock (_timerLock) { - if (KillTimer is not null) + if (_killTimer is not null) { var intervalMs = PingTimeout; - Logger.LogDebug("Changing kill timer to {0}ms. JobId {1} PlaySessionId {2}", intervalMs, Id, PlaySessionId); - KillTimer.Change(intervalMs, Timeout.Infinite); + _logger.LogDebug("Changing kill timer to {0}ms. JobId {1} PlaySessionId {2}", intervalMs, Id, PlaySessionId); + _killTimer.Change(intervalMs, Timeout.Infinite); } } } + /// + /// Stops the transcoding job. + /// + public void Stop() + { + lock (_processLock) + { +#pragma warning disable CA1849 // Can't await in lock block + TranscodingThrottler?.Stop().GetAwaiter().GetResult(); + + var process = Process; + + if (!HasExited) + { + try + { + _logger.LogInformation("Stopping ffmpeg process with q command for {Path}", Path); + + process!.StandardInput.WriteLine("q"); + + // Need to wait because killing is asynchronous. + if (!process.WaitForExit(5000)) + { + _logger.LogInformation("Killing FFmpeg process for {Path}", Path); + process.Kill(); + } + } + catch (InvalidOperationException) + { + } + } +#pragma warning restore CA1849 + } + } + /// public void Dispose() { - Dispose(true); - GC.SuppressFinalize(this); - } - - /// - /// Dispose all resources. - /// - /// Whether to dispose all resources. - protected virtual void Dispose(bool disposing) - { - if (disposing) - { - Process?.Dispose(); - Process = null; - KillTimer?.Dispose(); - KillTimer = null; - CancellationTokenSource?.Dispose(); - CancellationTokenSource = null; - TranscodingThrottler?.Dispose(); - TranscodingThrottler = null; - } + Process?.Dispose(); + Process = null; + _killTimer?.Dispose(); + _killTimer = null; + CancellationTokenSource?.Dispose(); + CancellationTokenSource = null; + TranscodingThrottler?.Dispose(); + TranscodingThrottler = null; } } diff --git a/MediaBrowser.Controller/MediaEncoding/TranscodingThrottler.cs b/MediaBrowser.Controller/MediaEncoding/TranscodingThrottler.cs index aa08af54f0..813f13eaef 100644 --- a/MediaBrowser.Controller/MediaEncoding/TranscodingThrottler.cs +++ b/MediaBrowser.Controller/MediaEncoding/TranscodingThrottler.cs @@ -13,7 +13,7 @@ namespace MediaBrowser.Controller.MediaEncoding; /// public class TranscodingThrottler : IDisposable { - private readonly TranscodingJobDto _job; + private readonly TranscodingJob _job; private readonly ILogger _logger; private readonly IConfigurationManager _config; private readonly IFileSystem _fileSystem; @@ -29,7 +29,7 @@ public class TranscodingThrottler : IDisposable /// Instance of the interface. /// Instance of the interface. /// Instance of the interface. - public TranscodingThrottler(TranscodingJobDto job, ILogger logger, IConfigurationManager config, IFileSystem fileSystem, IMediaEncoder mediaEncoder) + public TranscodingThrottler(TranscodingJob job, ILogger logger, IConfigurationManager config, IFileSystem fileSystem, IMediaEncoder mediaEncoder) { _job = job; _logger = logger; @@ -145,7 +145,7 @@ public class TranscodingThrottler : IDisposable } } - private bool IsThrottleAllowed(TranscodingJobDto job, int thresholdSeconds) + private bool IsThrottleAllowed(TranscodingJob job, int thresholdSeconds) { var bytesDownloaded = job.BytesDownloaded; var transcodingPositionTicks = job.TranscodingPositionTicks ?? 0;