aboutsummaryrefslogtreecommitdiff
path: root/MediaBrowser.Controller/MediaEncoding/TranscodingSegmentCleaner.cs
diff options
context:
space:
mode:
authornyanmisaka <nst799610810@gmail.com>2024-03-16 03:40:14 +0800
committernyanmisaka <nst799610810@gmail.com>2024-03-16 07:35:05 +0800
commiteca9bf41bcf536708ad74236793b363db3af1e4d (patch)
tree9c63f5ed565bc039162125eecad92a90f9f05089 /MediaBrowser.Controller/MediaEncoding/TranscodingSegmentCleaner.cs
parent1e964c9bc23b598135590f5b29f25836158a4305 (diff)
Add TranscodingSegmentCleaner to replace ffmpeg's hlsenc deletion
FFmpeg deletes segments based on its own transcoding progress, but we need to delete segments based on client download progress. Since disk and GPU speeds vary, using hlsenc's built-in deletion will result in premature deletion of some segments. As a consequence, the server has to constantly respin new ffmpeg instances, resulting in choppy video playback. Signed-off-by: nyanmisaka <nst799610810@gmail.com>
Diffstat (limited to 'MediaBrowser.Controller/MediaEncoding/TranscodingSegmentCleaner.cs')
-rw-r--r--MediaBrowser.Controller/MediaEncoding/TranscodingSegmentCleaner.cs188
1 files changed, 188 insertions, 0 deletions
diff --git a/MediaBrowser.Controller/MediaEncoding/TranscodingSegmentCleaner.cs b/MediaBrowser.Controller/MediaEncoding/TranscodingSegmentCleaner.cs
new file mode 100644
index 000000000..6cbda8e0a
--- /dev/null
+++ b/MediaBrowser.Controller/MediaEncoding/TranscodingSegmentCleaner.cs
@@ -0,0 +1,188 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+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;
+
+/// <summary>
+/// Transcoding segment cleaner.
+/// </summary>
+public class TranscodingSegmentCleaner : IDisposable
+{
+ private readonly TranscodingJob _job;
+ private readonly ILogger<TranscodingSegmentCleaner> _logger;
+ private readonly IConfigurationManager _config;
+ private readonly IFileSystem _fileSystem;
+ private readonly IMediaEncoder _mediaEncoder;
+ private Timer? _timer;
+ private int _segmentLength;
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="TranscodingSegmentCleaner"/> class.
+ /// </summary>
+ /// <param name="job">Transcoding job dto.</param>
+ /// <param name="logger">Instance of the <see cref="ILogger{TranscodingSegmentCleaner}"/> interface.</param>
+ /// <param name="config">Instance of the <see cref="IConfigurationManager"/> interface.</param>
+ /// <param name="fileSystem">Instance of the <see cref="IFileSystem"/> interface.</param>
+ /// <param name="mediaEncoder">Instance of the <see cref="IMediaEncoder"/> interface.</param>
+ /// <param name="segmentLength">The segment length of this transcoding job.</param>
+ public TranscodingSegmentCleaner(TranscodingJob job, ILogger<TranscodingSegmentCleaner> logger, IConfigurationManager config, IFileSystem fileSystem, IMediaEncoder mediaEncoder, int segmentLength)
+ {
+ _job = job;
+ _logger = logger;
+ _config = config;
+ _fileSystem = fileSystem;
+ _mediaEncoder = mediaEncoder;
+ _segmentLength = segmentLength;
+ }
+
+ /// <summary>
+ /// Start timer.
+ /// </summary>
+ public void Start()
+ {
+ _timer = new Timer(TimerCallback, null, 20000, 20000);
+ }
+
+ /// <summary>
+ /// Stop cleaner.
+ /// </summary>
+ public void Stop()
+ {
+ DisposeTimer();
+ }
+
+ /// <summary>
+ /// Dispose cleaner.
+ /// </summary>
+ public void Dispose()
+ {
+ Dispose(true);
+ GC.SuppressFinalize(this);
+ }
+
+ /// <summary>
+ /// Dispose cleaner.
+ /// </summary>
+ /// <param name="disposing">Disposing.</param>
+ 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();
+ var enableSegmentDeletion = options.EnableSegmentDeletion;
+ var segmentKeepSeconds = Math.Max(options.SegmentKeepSeconds, 20);
+
+ if (enableSegmentDeletion)
+ {
+ var downloadPositionTicks = _job.DownloadPositionTicks ?? 0;
+ var downloadPositionSeconds = Convert.ToInt64(TimeSpan.FromTicks(downloadPositionTicks).TotalSeconds);
+
+ if (downloadPositionSeconds > 0 && segmentKeepSeconds > 0 && downloadPositionSeconds > segmentKeepSeconds)
+ {
+ var idxMaxToRemove = (downloadPositionSeconds - segmentKeepSeconds) / _segmentLength;
+
+ if (idxMaxToRemove > 0)
+ {
+ await DeleteSegmentFiles(_job, 0, idxMaxToRemove, 0, 1500).ConfigureAwait(false);
+ }
+ }
+ }
+ }
+
+ private async Task DeleteSegmentFiles(TranscodingJob job, long idxMin, long idxMax, int retryCount, int delayMs)
+ {
+ if (retryCount >= 10)
+ {
+ return;
+ }
+
+ var path = job.Path ?? throw new ArgumentException("Path can't be null.");
+
+ _logger.LogDebug("Deleting segment file(s) index {Min} to {Max} from {Path}", idxMin, idxMax, path);
+
+ await Task.Delay(delayMs).ConfigureAwait(false);
+
+ try
+ {
+ if (job.Type == TranscodingJobType.Hls)
+ {
+ DeleteHlsSegmentFiles(path, idxMin, idxMax);
+ }
+ }
+ catch (IOException ex)
+ {
+ _logger.LogError(ex, "Error deleting segment file(s) {Path}", path);
+
+ await DeleteSegmentFiles(job, idxMin, idxMax, retryCount + 1, 500).ConfigureAwait(false);
+ }
+ catch (Exception ex)
+ {
+ _logger.LogError(ex, "Error deleting segment file(s) {Path}", path);
+ }
+ }
+
+ private void DeleteHlsSegmentFiles(string outputFilePath, long idxMin, long idxMax)
+ {
+ var directory = Path.GetDirectoryName(outputFilePath)
+ ?? throw new ArgumentException("Path can't be a root directory.", nameof(outputFilePath));
+
+ var name = Path.GetFileNameWithoutExtension(outputFilePath);
+
+ var filesToDelete = _fileSystem.GetFilePaths(directory)
+ .Where(f => long.TryParse(Path.GetFileNameWithoutExtension(f).Replace(name, string.Empty, StringComparison.Ordinal), out var idx) && idx >= idxMin && idx <= idxMax);
+
+ List<Exception>? exs = null;
+ foreach (var file in filesToDelete)
+ {
+ try
+ {
+ _logger.LogDebug("Deleting HLS segment file {0}", file);
+ _fileSystem.DeleteFile(file);
+ }
+ catch (IOException ex)
+ {
+ (exs ??= new List<Exception>(4)).Add(ex);
+ _logger.LogError(ex, "Error deleting HLS segment file {Path}", file);
+ }
+ }
+
+ if (exs is not null)
+ {
+ throw new AggregateException("Error deleting HLS segment files", exs);
+ }
+ }
+
+ private void DisposeTimer()
+ {
+ if (_timer is not null)
+ {
+ _timer.Dispose();
+ _timer = null;
+ }
+ }
+}