diff options
5 files changed, 102 insertions, 10 deletions
diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md index 0b36dfa529..aca9d6b790 100644 --- a/CONTRIBUTORS.md +++ b/CONTRIBUTORS.md @@ -245,6 +245,7 @@ - [scr4bble](https://github.com/scr4bble) - [Mateus Bandeira](https://github.com/mateusbandeiraa) - [TheDreadPirate](https://github.com/thedreaddpirate) + - [klizas](https://github.com/klizas) # Emby Contributors diff --git a/Jellyfin.Api/Controllers/DynamicHlsController.cs b/Jellyfin.Api/Controllers/DynamicHlsController.cs index 034a9dea55..dc0a6b2da9 100644 --- a/Jellyfin.Api/Controllers/DynamicHlsController.cs +++ b/Jellyfin.Api/Controllers/DynamicHlsController.cs @@ -1928,7 +1928,7 @@ public class DynamicHlsController : BaseJellyfinApiController { // Transcoding job is over, so assume all existing files are ready _logger.LogDebug("serving up {0} as transcode is over", segmentPath); - return GetSegmentResult(state, segmentPath, transcodingJob); + return GetSegmentResult(state, segmentPath, segmentIndex, transcodingJob); } var currentTranscodingIndex = GetCurrentTranscodingIndex(playlistPath, segmentExtension); @@ -1937,7 +1937,7 @@ public class DynamicHlsController : BaseJellyfinApiController if (segmentIndex < currentTranscodingIndex) { _logger.LogDebug("serving up {0} as transcode index {1} is past requested point {2}", segmentPath, currentTranscodingIndex, segmentIndex); - return GetSegmentResult(state, segmentPath, transcodingJob); + return GetSegmentResult(state, segmentPath, segmentIndex, transcodingJob); } } @@ -1953,7 +1953,7 @@ public class DynamicHlsController : BaseJellyfinApiController if (transcodingJob.HasExited || System.IO.File.Exists(nextSegmentPath)) { _logger.LogDebug("Serving up {SegmentPath} as it deemed ready", segmentPath); - return GetSegmentResult(state, segmentPath, transcodingJob); + return GetSegmentResult(state, segmentPath, segmentIndex, transcodingJob); } } else @@ -1984,10 +1984,10 @@ public class DynamicHlsController : BaseJellyfinApiController _logger.LogWarning("cannot serve {0} as it doesn't exist and no transcode is running", segmentPath); } - return GetSegmentResult(state, segmentPath, transcodingJob); + return GetSegmentResult(state, segmentPath, segmentIndex, transcodingJob); } - private ActionResult GetSegmentResult(StreamState state, string segmentPath, TranscodingJob? transcodingJob) + private ActionResult GetSegmentResult(StreamState state, string segmentPath, int segmentIndex, TranscodingJob? transcodingJob) { var segmentEndingPositionTicks = state.Request.CurrentRuntimeTicks + state.Request.ActualSegmentLengthTicks; @@ -1997,6 +1997,7 @@ public class DynamicHlsController : BaseJellyfinApiController if (transcodingJob is not null) { transcodingJob.DownloadPositionTicks = Math.Max(transcodingJob.DownloadPositionTicks ?? segmentEndingPositionTicks, segmentEndingPositionTicks); + transcodingJob.ReportSegmentDownloaded(segmentIndex, segmentEndingPositionTicks); _transcodeManager.OnTranscodeEndRequest(transcodingJob); } diff --git a/MediaBrowser.Controller/MediaEncoding/TranscodingJob.cs b/MediaBrowser.Controller/MediaEncoding/TranscodingJob.cs index 5045030b9b..12600af384 100644 --- a/MediaBrowser.Controller/MediaEncoding/TranscodingJob.cs +++ b/MediaBrowser.Controller/MediaEncoding/TranscodingJob.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Concurrent; using System.Diagnostics; using System.Threading; using MediaBrowser.Model.Dto; @@ -14,6 +15,7 @@ public sealed class TranscodingJob : IDisposable private readonly ILogger<TranscodingJob> _logger; private readonly Lock _processLock = new(); private readonly Lock _timerLock = new(); + private readonly ConcurrentDictionary<int, long> _segmentEndPositionTicks = new(); private int _activeRequestCount; private Timer? _killTimer; @@ -157,6 +159,40 @@ public sealed class TranscodingJob : IDisposable public int PingTimeout { get; set; } /// <summary> + /// Records the end position of a served segment. + /// </summary> + /// <param name="segmentIndex">The segment index.</param> + /// <param name="segmentEndPositionTicks">The segment end position ticks.</param> + public void ReportSegmentDownloaded(int segmentIndex, long segmentEndPositionTicks) + { + if (segmentIndex < 0) + { + return; + } + + _segmentEndPositionTicks[segmentIndex] = segmentEndPositionTicks; + } + + /// <summary> + /// Gets the highest index among served segments whose end position is at or before the given position. + /// </summary> + /// <param name="positionTicks">The position ticks.</param> + /// <returns>The segment index, or <c>null</c> if there is none.</returns> + public int? GetHighestServedSegmentIndexEndingAtOrBefore(long positionTicks) + { + int? result = null; + foreach (var (index, endPositionTicks) in _segmentEndPositionTicks) + { + if (endPositionTicks <= positionTicks && (result is null || index > result)) + { + result = index; + } + } + + return result; + } + + /// <summary> /// Increments the active request count. /// </summary> /// <returns>The incremented count.</returns> diff --git a/MediaBrowser.Controller/MediaEncoding/TranscodingSegmentCleaner.cs b/MediaBrowser.Controller/MediaEncoding/TranscodingSegmentCleaner.cs index 67bfcb02fd..924fc2e659 100644 --- a/MediaBrowser.Controller/MediaEncoding/TranscodingSegmentCleaner.cs +++ b/MediaBrowser.Controller/MediaEncoding/TranscodingSegmentCleaner.cs @@ -22,7 +22,6 @@ public class TranscodingSegmentCleaner : IDisposable 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. @@ -32,7 +31,7 @@ public class TranscodingSegmentCleaner : IDisposable /// <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> + /// <param name="segmentLength">The segment length of this transcoding job. Unused.</param> public TranscodingSegmentCleaner(TranscodingJob job, ILogger<TranscodingSegmentCleaner> logger, IConfigurationManager config, IFileSystem fileSystem, IMediaEncoder mediaEncoder, int segmentLength) { _job = job; @@ -40,7 +39,6 @@ public class TranscodingSegmentCleaner : IDisposable _config = config; _fileSystem = fileSystem; _mediaEncoder = mediaEncoder; - _segmentLength = segmentLength; } /// <summary> @@ -104,11 +102,11 @@ public class TranscodingSegmentCleaner : IDisposable if (downloadPositionSeconds > 0 && segmentKeepSeconds > 0 && downloadPositionSeconds > segmentKeepSeconds) { - var idxMaxToDelete = (downloadPositionSeconds - segmentKeepSeconds) / _segmentLength; + var idxMaxToDelete = _job.GetHighestServedSegmentIndexEndingAtOrBefore(downloadPositionTicks - TimeSpan.FromSeconds(segmentKeepSeconds).Ticks); if (idxMaxToDelete > 0) { - await DeleteSegmentFiles(_job, 0, idxMaxToDelete, 1500).ConfigureAwait(false); + await DeleteSegmentFiles(_job, 0, idxMaxToDelete.Value, 1500).ConfigureAwait(false); } } } diff --git a/tests/Jellyfin.Controller.Tests/MediaEncoding/TranscodingJobTests.cs b/tests/Jellyfin.Controller.Tests/MediaEncoding/TranscodingJobTests.cs new file mode 100644 index 0000000000..9dc279a5b2 --- /dev/null +++ b/tests/Jellyfin.Controller.Tests/MediaEncoding/TranscodingJobTests.cs @@ -0,0 +1,56 @@ +using System; +using MediaBrowser.Controller.MediaEncoding; +using Microsoft.Extensions.Logging.Abstractions; +using Xunit; + +namespace Jellyfin.Controller.Tests.MediaEncoding; + +public class TranscodingJobTests +{ + private static TranscodingJob CreateJob() + => new TranscodingJob(NullLogger<TranscodingJob>.Instance); + + [Fact] + public void GetHighestServedSegmentIndexEndingAtOrBefore_NoSegmentsServed_ReturnsNull() + { + using var job = CreateJob(); + + Assert.Null(job.GetHighestServedSegmentIndexEndingAtOrBefore(TimeSpan.FromHours(1).Ticks)); + } + + [Fact] + public void GetHighestServedSegmentIndexEndingAtOrBefore_InitSegment_IsIgnored() + { + using var job = CreateJob(); + job.ReportSegmentDownloaded(-1, 0); + + Assert.Null(job.GetHighestServedSegmentIndexEndingAtOrBefore(TimeSpan.FromHours(1).Ticks)); + } + + [Fact] + public void GetHighestServedSegmentIndexEndingAtOrBefore_SegmentEndingExactlyAtPosition_IsIncluded() + { + using var job = CreateJob(); + job.ReportSegmentDownloaded(0, TimeSpan.FromSeconds(6).Ticks); + job.ReportSegmentDownloaded(1, TimeSpan.FromSeconds(12).Ticks); + + Assert.Equal(1, job.GetHighestServedSegmentIndexEndingAtOrBefore(TimeSpan.FromSeconds(12).Ticks)); + Assert.Equal(0, job.GetHighestServedSegmentIndexEndingAtOrBefore(TimeSpan.FromSeconds(11.9).Ticks)); + } + + [Fact] + public void GetHighestServedSegmentIndexEndingAtOrBefore_SegmentsLongerThanDesired_ReturnsServedIndex() + { + // Keyframe-based playlists average longer segments than the desired 6s. + const double SegmentSeconds = 6.6; + using var job = CreateJob(); + for (var i = 0; i < 520; i++) + { + job.ReportSegmentDownloaded(i, TimeSpan.FromSeconds((i + 1) * SegmentSeconds).Ticks); + } + + var keepFrom = TimeSpan.FromSeconds((520 * SegmentSeconds) - 120); + + Assert.Equal(500, job.GetHighestServedSegmentIndexEndingAtOrBefore(keepFrom.Ticks)); + } +} |
