From abd74fd5a481d67af1414960be7b7b19c9ee7e82 Mon Sep 17 00:00:00 2001 From: Patrick Barron Date: Tue, 31 Oct 2023 11:12:09 -0400 Subject: Move TranscodingJobDto and TranscodingThrottler to Controller --- .../MediaEncoding/TranscodingThrottler.cs | 218 +++++++++++++++++++++ 1 file changed, 218 insertions(+) create mode 100644 MediaBrowser.Controller/MediaEncoding/TranscodingThrottler.cs (limited to 'MediaBrowser.Controller/MediaEncoding/TranscodingThrottler.cs') diff --git a/MediaBrowser.Controller/MediaEncoding/TranscodingThrottler.cs b/MediaBrowser.Controller/MediaEncoding/TranscodingThrottler.cs new file mode 100644 index 000000000..aa08af54f --- /dev/null +++ b/MediaBrowser.Controller/MediaEncoding/TranscodingThrottler.cs @@ -0,0 +1,218 @@ +using System; +using System.Threading; +using System.Threading.Tasks; +using MediaBrowser.Common.Configuration; +using MediaBrowser.Model.Configuration; +using MediaBrowser.Model.IO; +using Microsoft.Extensions.Logging; + +namespace MediaBrowser.Controller.MediaEncoding; + +/// +/// Transcoding throttler. +/// +public class TranscodingThrottler : IDisposable +{ + private readonly TranscodingJobDto _job; + private readonly ILogger _logger; + private readonly IConfigurationManager _config; + private readonly IFileSystem _fileSystem; + private readonly IMediaEncoder _mediaEncoder; + private Timer? _timer; + private bool _isPaused; + + /// + /// Initializes a new instance of the class. + /// + /// Transcoding job dto. + /// Instance of the interface. + /// Instance of the interface. + /// Instance of the interface. + /// Instance of the interface. + public TranscodingThrottler(TranscodingJobDto job, ILogger logger, IConfigurationManager config, IFileSystem fileSystem, IMediaEncoder mediaEncoder) + { + _job = job; + _logger = logger; + _config = config; + _fileSystem = fileSystem; + _mediaEncoder = mediaEncoder; + } + + /// + /// Start timer. + /// + public void Start() + { + _timer = new Timer(TimerCallback, null, 5000, 5000); + } + + /// + /// Unpause transcoding. + /// + /// A . + public async Task UnpauseTranscoding() + { + if (_isPaused) + { + _logger.LogDebug("Sending resume command to ffmpeg"); + + try + { + var resumeKey = _mediaEncoder.IsPkeyPauseSupported ? "u" : Environment.NewLine; + await _job.Process!.StandardInput.WriteAsync(resumeKey).ConfigureAwait(false); + _isPaused = false; + } + catch (Exception ex) + { + _logger.LogError(ex, "Error resuming transcoding"); + } + } + } + + /// + /// Stop throttler. + /// + /// A . + public async Task Stop() + { + DisposeTimer(); + await UnpauseTranscoding().ConfigureAwait(false); + } + + /// + /// Dispose throttler. + /// + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + /// + /// Dispose throttler. + /// + /// Disposing. + protected virtual void Dispose(bool disposing) + { + if (disposing) + { + DisposeTimer(); + } + } + + private EncodingOptions GetOptions() + { + return _config.GetEncodingOptions(); + } + + private async void TimerCallback(object? state) + { + if (_job.HasExited) + { + DisposeTimer(); + return; + } + + var options = GetOptions(); + + if (options.EnableThrottling && IsThrottleAllowed(_job, options.ThrottleDelaySeconds)) + { + await PauseTranscoding().ConfigureAwait(false); + } + else + { + await UnpauseTranscoding().ConfigureAwait(false); + } + } + + private async Task PauseTranscoding() + { + if (!_isPaused) + { + var pauseKey = _mediaEncoder.IsPkeyPauseSupported ? "p" : "c"; + + _logger.LogDebug("Sending pause command [{Key}] to ffmpeg", pauseKey); + + try + { + await _job.Process!.StandardInput.WriteAsync(pauseKey).ConfigureAwait(false); + _isPaused = true; + } + catch (Exception ex) + { + _logger.LogError(ex, "Error pausing transcoding"); + } + } + } + + private bool IsThrottleAllowed(TranscodingJobDto job, int thresholdSeconds) + { + var bytesDownloaded = job.BytesDownloaded; + var transcodingPositionTicks = job.TranscodingPositionTicks ?? 0; + var downloadPositionTicks = job.DownloadPositionTicks ?? 0; + + var path = job.Path ?? throw new ArgumentException("Path can't be null."); + + var gapLengthInTicks = TimeSpan.FromSeconds(thresholdSeconds).Ticks; + + if (downloadPositionTicks > 0 && transcodingPositionTicks > 0) + { + // HLS - time-based consideration + + var targetGap = gapLengthInTicks; + var gap = transcodingPositionTicks - downloadPositionTicks; + + if (gap < targetGap) + { + _logger.LogDebug("Not throttling transcoder gap {0} target gap {1}", gap, targetGap); + return false; + } + + _logger.LogDebug("Throttling transcoder gap {0} target gap {1}", gap, targetGap); + return true; + } + + if (bytesDownloaded > 0 && transcodingPositionTicks > 0) + { + // Progressive Streaming - byte-based consideration + + try + { + var bytesTranscoded = job.BytesTranscoded ?? _fileSystem.GetFileInfo(path).Length; + + // Estimate the bytes the transcoder should be ahead + double gapFactor = gapLengthInTicks; + gapFactor /= transcodingPositionTicks; + var targetGap = bytesTranscoded * gapFactor; + + var gap = bytesTranscoded - bytesDownloaded; + + if (gap < targetGap) + { + _logger.LogDebug("Not throttling transcoder gap {0} target gap {1} bytes downloaded {2}", gap, targetGap, bytesDownloaded); + return false; + } + + _logger.LogDebug("Throttling transcoder gap {0} target gap {1} bytes downloaded {2}", gap, targetGap, bytesDownloaded); + return true; + } + catch (Exception ex) + { + _logger.LogError(ex, "Error getting output size"); + return false; + } + } + + _logger.LogDebug("No throttle data for {Path}", path); + return false; + } + + private void DisposeTimer() + { + if (_timer is not null) + { + _timer.Dispose(); + _timer = null; + } + } +} -- cgit v1.2.3 From c2081955c8b2a81eb214f321697d3462709164e0 Mon Sep 17 00:00:00 2001 From: Patrick Barron Date: Tue, 31 Oct 2023 11:31:09 -0400 Subject: Rename and clean up TranscodingJob --- Jellyfin.Api/Controllers/DynamicHlsController.cs | 8 +- Jellyfin.Api/Helpers/FileStreamResponseHelpers.cs | 2 +- Jellyfin.Api/Helpers/ProgressiveFileStream.cs | 4 +- Jellyfin.Api/Helpers/TranscodingJobHelper.cs | 71 ++---- Jellyfin.Api/Models/StreamingDtos/StreamState.cs | 2 +- .../MediaEncoding/TranscodingJob.cs | 280 ++++++++++++++++++++ .../MediaEncoding/TranscodingJobDto.cs | 282 --------------------- .../MediaEncoding/TranscodingThrottler.cs | 6 +- 8 files changed, 313 insertions(+), 342 deletions(-) create mode 100644 MediaBrowser.Controller/MediaEncoding/TranscodingJob.cs delete mode 100644 MediaBrowser.Controller/MediaEncoding/TranscodingJobDto.cs (limited to 'MediaBrowser.Controller/MediaEncoding/TranscodingThrottler.cs') diff --git a/Jellyfin.Api/Controllers/DynamicHlsController.cs b/Jellyfin.Api/Controllers/DynamicHlsController.cs index 6b2c42363..260ed4787 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 ce2e476b7..fafa2c055 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 379ec6e27..18088483d 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 14436909c..9a6ec17fd 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 4b69392ef..439f8052c 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/TranscodingJob.cs b/MediaBrowser.Controller/MediaEncoding/TranscodingJob.cs new file mode 100644 index 000000000..1e6d5933c --- /dev/null +++ b/MediaBrowser.Controller/MediaEncoding/TranscodingJob.cs @@ -0,0 +1,280 @@ +using System; +using System.Diagnostics; +using System.Threading; +using MediaBrowser.Model.Dto; +using Microsoft.Extensions.Logging; + +namespace MediaBrowser.Controller.MediaEncoding; + +/// +/// Class TranscodingJob. +/// +public sealed class TranscodingJob : IDisposable +{ + private readonly ILogger _logger; + private readonly object _processLock = new(); + private readonly object _timerLock = new(); + + private Timer? _killTimer; + + /// + /// Initializes a new instance of the class. + /// + /// Instance of the interface. + public TranscodingJob(ILogger logger) + { + _logger = logger; + } + + /// + /// Gets or sets the play session identifier. + /// + public string? PlaySessionId { get; set; } + + /// + /// Gets or sets the live stream identifier. + /// + public string? LiveStreamId { get; set; } + + /// + /// Gets or sets a value indicating whether is live output. + /// + public bool IsLiveOutput { get; set; } + + /// + /// Gets or sets the path. + /// + public MediaSourceInfo? MediaSource { get; set; } + + /// + /// Gets or sets path. + /// + public string? Path { get; set; } + + /// + /// Gets or sets the type. + /// + public TranscodingJobType Type { get; set; } + + /// + /// Gets or sets the process. + /// + public Process? Process { get; set; } + + /// + /// Gets or sets the active request count. + /// + public int ActiveRequestCount { get; set; } + + /// + /// Gets or sets device id. + /// + public string? DeviceId { get; set; } + + /// + /// Gets or sets cancellation token source. + /// + public CancellationTokenSource? CancellationTokenSource { get; set; } + + /// + /// Gets or sets a value indicating whether has exited. + /// + public bool HasExited { get; set; } + + /// + /// Gets or sets exit code. + /// + public int ExitCode { get; set; } + + /// + /// Gets or sets a value indicating whether is user paused. + /// + public bool IsUserPaused { get; set; } + + /// + /// Gets or sets id. + /// + public string? Id { get; set; } + + /// + /// Gets or sets framerate. + /// + public float? Framerate { get; set; } + + /// + /// Gets or sets completion percentage. + /// + public double? CompletionPercentage { get; set; } + + /// + /// Gets or sets bytes downloaded. + /// + public long BytesDownloaded { get; set; } + + /// + /// Gets or sets bytes transcoded. + /// + public long? BytesTranscoded { get; set; } + + /// + /// Gets or sets bit rate. + /// + public int? BitRate { get; set; } + + /// + /// Gets or sets transcoding position ticks. + /// + public long? TranscodingPositionTicks { get; set; } + + /// + /// Gets or sets download position ticks. + /// + public long? DownloadPositionTicks { get; set; } + + /// + /// Gets or sets transcoding throttler. + /// + public TranscodingThrottler? TranscodingThrottler { get; set; } + + /// + /// Gets or sets last ping date. + /// + public DateTime LastPingDate { get; set; } + + /// + /// Gets or sets ping timeout. + /// + public int PingTimeout { get; set; } + + /// + /// Stop kill timer. + /// + public void StopKillTimer() + { + lock (_timerLock) + { + _killTimer?.Change(Timeout.Infinite, Timeout.Infinite); + } + } + + /// + /// Dispose kill timer. + /// + public void DisposeKillTimer() + { + lock (_timerLock) + { + if (_killTimer is not null) + { + _killTimer.Dispose(); + _killTimer = null; + } + } + } + + /// + /// Start kill timer. + /// + /// Callback action. + public void StartKillTimer(Action callback) + { + StartKillTimer(callback, PingTimeout); + } + + /// + /// Start kill timer. + /// + /// Callback action. + /// Callback interval. + public void StartKillTimer(Action callback, int intervalMs) + { + if (HasExited) + { + return; + } + + lock (_timerLock) + { + 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); + } + else + { + _logger.LogDebug("Changing kill timer to {0}ms. JobId {1} PlaySessionId {2}", intervalMs, Id, PlaySessionId); + _killTimer.Change(intervalMs, Timeout.Infinite); + } + } + } + + /// + /// Change kill timer if started. + /// + public void ChangeKillTimerIfStarted() + { + if (HasExited) + { + return; + } + + lock (_timerLock) + { + 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); + } + } + } + + /// + /// 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() + { + Process?.Dispose(); + Process = null; + _killTimer?.Dispose(); + _killTimer = null; + CancellationTokenSource?.Dispose(); + CancellationTokenSource = null; + TranscodingThrottler?.Dispose(); + TranscodingThrottler = null; + } +} diff --git a/MediaBrowser.Controller/MediaEncoding/TranscodingJobDto.cs b/MediaBrowser.Controller/MediaEncoding/TranscodingJobDto.cs deleted file mode 100644 index 6f929204f..000000000 --- a/MediaBrowser.Controller/MediaEncoding/TranscodingJobDto.cs +++ /dev/null @@ -1,282 +0,0 @@ -using System; -using System.Diagnostics; -using System.Diagnostics.CodeAnalysis; -using System.Threading; -using MediaBrowser.Model.Dto; -using Microsoft.Extensions.Logging; - -namespace MediaBrowser.Controller.MediaEncoding; - -/// -/// Class TranscodingJob. -/// -public class TranscodingJobDto : 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(); - - /// - /// Timer lock. - /// - private readonly object _timerLock = new object(); - - /// - /// Initializes a new instance of the class. - /// - /// Instance of the interface. - public TranscodingJobDto(ILogger 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; } - - /// - /// Gets or sets a value indicating whether is live output. - /// - public bool IsLiveOutput { get; set; } - - /// - /// Gets or sets the path. - /// - /// The path. - public MediaSourceInfo? MediaSource { get; set; } - - /// - /// Gets or sets path. - /// - public string? Path { get; set; } - - /// - /// 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. - /// - public string? DeviceId { get; set; } - - /// - /// Gets or sets cancellation token source. - /// - public CancellationTokenSource? CancellationTokenSource { get; set; } - - /// - /// Gets or sets a value indicating whether has exited. - /// - public bool HasExited { get; set; } - - /// - /// Gets or sets exit code. - /// - public int ExitCode { get; set; } - - /// - /// Gets or sets a value indicating whether is user paused. - /// - public bool IsUserPaused { get; set; } - - /// - /// Gets or sets id. - /// - public string? Id { get; set; } - - /// - /// Gets or sets framerate. - /// - public float? Framerate { get; set; } - - /// - /// Gets or sets completion percentage. - /// - public double? CompletionPercentage { get; set; } - - /// - /// Gets or sets bytes downloaded. - /// - public long BytesDownloaded { get; set; } - - /// - /// Gets or sets bytes transcoded. - /// - public long? BytesTranscoded { get; set; } - - /// - /// Gets or sets bit rate. - /// - public int? BitRate { get; set; } - - /// - /// Gets or sets transcoding position ticks. - /// - public long? TranscodingPositionTicks { get; set; } - - /// - /// Gets or sets download position ticks. - /// - public long? DownloadPositionTicks { get; set; } - - /// - /// Gets or sets transcoding throttler. - /// - public TranscodingThrottler? TranscodingThrottler { get; set; } - - /// - /// Gets or sets last ping date. - /// - public DateTime LastPingDate { get; set; } - - /// - /// Gets or sets ping timeout. - /// - public int PingTimeout { get; set; } - - /// - /// Stop kill timer. - /// - public void StopKillTimer() - { - lock (_timerLock) - { - KillTimer?.Change(Timeout.Infinite, Timeout.Infinite); - } - } - - /// - /// Dispose kill timer. - /// - public void DisposeKillTimer() - { - lock (_timerLock) - { - if (KillTimer is not null) - { - KillTimer.Dispose(); - KillTimer = null; - } - } - } - - /// - /// Start kill timer. - /// - /// Callback action. - public void StartKillTimer(Action callback) - { - StartKillTimer(callback, PingTimeout); - } - - /// - /// Start kill timer. - /// - /// Callback action. - /// Callback interval. - public void StartKillTimer(Action callback, int intervalMs) - { - if (HasExited) - { - return; - } - - lock (_timerLock) - { - 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); - } - else - { - Logger.LogDebug("Changing kill timer to {0}ms. JobId {1} PlaySessionId {2}", intervalMs, Id, PlaySessionId); - KillTimer.Change(intervalMs, Timeout.Infinite); - } - } - } - - /// - /// Change kill timer if started. - /// - public void ChangeKillTimerIfStarted() - { - if (HasExited) - { - return; - } - - lock (_timerLock) - { - 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); - } - } - } - - /// - 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; - } - } -} diff --git a/MediaBrowser.Controller/MediaEncoding/TranscodingThrottler.cs b/MediaBrowser.Controller/MediaEncoding/TranscodingThrottler.cs index aa08af54f..813f13eae 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; -- cgit v1.2.3