aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--CONTRIBUTORS.md1
-rw-r--r--Jellyfin.Api/Controllers/DynamicHlsController.cs11
-rw-r--r--MediaBrowser.Controller/MediaEncoding/TranscodingJob.cs36
-rw-r--r--MediaBrowser.Controller/MediaEncoding/TranscodingSegmentCleaner.cs8
-rw-r--r--tests/Jellyfin.Controller.Tests/MediaEncoding/TranscodingJobTests.cs56
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));
+ }
+}