using System; using System.Globalization; using System.IO; using Jellyfin.Api.Controllers; using MediaBrowser.Controller.MediaEncoding; using MediaBrowser.Model.Configuration; using Xunit; namespace Jellyfin.Api.Tests.Controllers { /// /// Tests for the HA behaviour of : the session record it /// registers in and the ffmpeg segmenting options it /// picks once a session is resumed on another pod. /// public class DynamicHlsHaModeTests { /// /// The registered session has to name the files it owns, otherwise /// DeleteTranscodeFileTask cannot recognise and protect them. /// [Fact] [Trait("Category", "UnitTest")] public void CreateSessionRecord_PopulatesPathsFromPlaylistPath() { const string PlaylistPath = "/transcodes/9e1c6f.m3u8"; var session = DynamicHlsController.CreateSessionRecord("play-1", "media-1", PlaylistPath, TimeSpan.FromSeconds(30)); Assert.Equal(PlaylistPath, session.ManifestPath); Assert.Equal("/transcodes/9e1c6f", session.SegmentPathPrefix); Assert.Equal("play-1", session.PlaySessionId); Assert.Equal("media-1", session.MediaSourceId); Assert.NotEmpty(session.OwnerPod); Assert.True(session.LeaseExpiresUtc > DateTime.UtcNow); } /// /// Every segment ffmpeg writes for the playlist has to start with the recorded prefix. /// Segment paths are built as <playlist without extension><index><extension>. /// [Theory] [InlineData(".ts")] [InlineData(".mp4")] [Trait("Category", "UnitTest")] public void GetSegmentPathPrefix_CoversEverySegmentPath(string segmentExtension) { const string PlaylistPath = "/transcodes/9e1c6f.m3u8"; var prefix = TranscodeSession.GetSegmentPathPrefix(PlaylistPath); for (var index = 0; index < 5; index++) { var segmentPath = Path.Combine( Path.GetDirectoryName(PlaylistPath)!, Path.GetFileNameWithoutExtension(PlaylistPath) + index.ToString(CultureInfo.InvariantCulture) + segmentExtension); Assert.StartsWith(prefix, segmentPath, StringComparison.Ordinal); } } /// /// HA mode shortens segments and bounds the rolling buffer; the configured values are /// user-editable so they are clamped. /// [Theory] [InlineData(2, 2)] [InlineData(0, 1)] [InlineData(60, 6)] [Trait("Category", "UnitTest")] public void GetEffectiveSegmentLength_InHaMode_ClampsConfiguredRecoveryLength(int configured, int expected) { var options = new EncodingOptions { RecoverySegmentLengthSeconds = configured }; Assert.Equal(expected, DynamicHlsController.GetEffectiveSegmentLength(true, 6, options)); } /// /// Outside HA mode the requested segment length is used unchanged and the playlist is unbounded. /// [Fact] [Trait("Category", "UnitTest")] public void GetSegmentingOptions_OutsideHaMode_UsesRequestedValues() { var options = new EncodingOptions { RecoverySegmentLengthSeconds = 2, RecoverySegmentBufferCount = 5 }; Assert.Equal(6, DynamicHlsController.GetEffectiveSegmentLength(false, 6, options)); Assert.Equal(0, DynamicHlsController.GetHlsListSize(false, options)); } /// /// HA mode keeps a bounded rolling buffer of segments for a takeover pod to serve. /// [Theory] [InlineData(5, 5)] [InlineData(0, 2)] [InlineData(100, 10)] [Trait("Category", "UnitTest")] public void GetHlsListSize_InHaMode_ClampsConfiguredBufferCount(int configured, int expected) { var options = new EncodingOptions { RecoverySegmentBufferCount = configured }; Assert.Equal(expected, DynamicHlsController.GetHlsListSize(true, options)); } } }