baa16b6586
Cleanup never matched a live session because the controller registered empty manifest and segment paths, renewal was a read-modify-write that could revert a takeover, and the takeover script compared an ISO date to a number, so it errored. - populate the session record's manifest and segment paths from the playlist path - renew the lease via a Lua compare-and-set on the owning pod - store the lease expiry as unix milliseconds so the scripts can compare it - retain the session record past its lease so an orphan can still be taken over - test the Redis store against a real Redis, including the renew-vs-takeover race - drop the live stream record nothing ever read back
106 lines
4.3 KiB
C#
106 lines
4.3 KiB
C#
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
|
|
{
|
|
/// <summary>
|
|
/// Tests for the HA behaviour of <see cref="DynamicHlsController"/>: the session record it
|
|
/// registers in <see cref="ITranscodeSessionStore"/> and the ffmpeg segmenting options it
|
|
/// picks once a session is resumed on another pod.
|
|
/// </summary>
|
|
public class DynamicHlsHaModeTests
|
|
{
|
|
/// <summary>
|
|
/// The registered session has to name the files it owns, otherwise
|
|
/// <c>DeleteTranscodeFileTask</c> cannot recognise and protect them.
|
|
/// </summary>
|
|
[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);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Every segment ffmpeg writes for the playlist has to start with the recorded prefix.
|
|
/// Segment paths are built as <c><playlist without extension><index><extension></c>.
|
|
/// </summary>
|
|
[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);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// HA mode shortens segments and bounds the rolling buffer; the configured values are
|
|
/// user-editable so they are clamped.
|
|
/// </summary>
|
|
[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));
|
|
}
|
|
|
|
/// <summary>
|
|
/// Outside HA mode the requested segment length is used unchanged and the playlist is unbounded.
|
|
/// </summary>
|
|
[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));
|
|
}
|
|
|
|
/// <summary>
|
|
/// HA mode keeps a bounded rolling buffer of segments for a takeover pod to serve.
|
|
/// </summary>
|
|
[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));
|
|
}
|
|
}
|
|
}
|