Files
TeleWave/backend/tests/TeleWave.Domain.Tests/Broadcast/LiveWindowCalculatorTests.cs
T
Leonid Pershin 0f609db4e9
ci / build-backend (push) Successful in 2m17s
ci / build-frontend (push) Successful in 53s
ci / tests (push) Successful in 1m39s
ci / sonar (push) Failing after 1m23s
Implement Discontinuity Index for Live Streaming Enhancements
Added a DiscontinuityIndex property to ScheduleEntry and related DTOs to track the cumulative number of discontinuities in live streaming. Updated the LiveWindowCalculator to utilize this index for generating accurate EXT-X-DISCONTINUITY-SEQUENCE values, ensuring compatibility with ffmpeg-based players. Enhanced the WriteEntriesAsync method to calculate and persist the discontinuity index during entry creation. Updated relevant tests and documentation to reflect these changes, improving the reliability of live streaming functionality.
2026-08-04 12:43:35 +03:00

139 lines
5.6 KiB
C#

using TeleWave.Domain.Broadcast.Live;
using Xunit;
namespace TeleWave.Domain.Tests.Broadcast;
public class LiveWindowCalculatorTests
{
private static readonly DateTimeOffset Epoch = new(2026, 1, 1, 0, 0, 0, TimeSpan.Zero);
private const int Seg = 2;
private const int Window = 5;
private static LiveInput Input(
double nowOffsetSeconds,
IReadOnlyList<LiveEntry> entries,
LiveFiller? filler = null
) =>
new(
Now: Epoch.AddSeconds(nowOffsetSeconds),
Epoch: Epoch,
SegmentSeconds: Seg,
WindowSegments: Window,
Entries: entries,
Filler: filler
);
[Fact]
public void SingleEntry_ProducesWindowWithMonotonicMediaSequence()
{
var asset = Guid.NewGuid();
var entries = new List<LiveEntry> { new(Epoch, Epoch.AddSeconds(100), asset, 50, 3) };
// now = epoch + 20s → currentIndex = 10, окно [6..10].
var playlist = LiveWindowCalculator.Build(Input(20, entries));
Assert.Equal(6, playlist.MediaSequence);
Assert.Equal(Seg, playlist.TargetDuration);
Assert.Equal(5, playlist.Segments.Count);
Assert.Equal([6, 7, 8, 9, 10], playlist.Segments.Select(s => s.LocalIndex));
Assert.All(playlist.Segments, s => Assert.Equal(asset, s.AssetId));
Assert.DoesNotContain(playlist.Segments, s => s.Discontinuity);
// Окно целиком внутри записи — DISCONTINUITY-SEQUENCE равен её накопительному номеру.
Assert.Equal(3, playlist.DiscontinuitySequence);
}
[Fact]
public void EntryBoundary_InsertsDiscontinuity()
{
Guid a = Guid.NewGuid(),
b = Guid.NewGuid();
var entries = new List<LiveEntry>
{
new(Epoch, Epoch.AddSeconds(16), a, 8, 5), // g0..7
new(Epoch.AddSeconds(16), Epoch.AddSeconds(40), b, 12, 6), // g8..
};
var playlist = LiveWindowCalculator.Build(Input(20, entries));
Assert.Equal(6, playlist.MediaSequence);
Assert.Equal([a, a, b, b, b], playlist.Segments.Select(s => s.AssetId));
// DISCONTINUITY только перед первым сегментом B (индекс 2 в окне).
Assert.Equal(
[false, false, true, false, false],
playlist.Segments.Select(s => s.Discontinuity)
);
// Первый сегмент окна принадлежит A — база берётся из его номера.
Assert.Equal(5, playlist.DiscontinuitySequence);
}
[Fact]
public void Gap_WithFiller_LoopsFiller()
{
var filler = new LiveFiller(Guid.NewGuid(), 4);
var playlist = LiveWindowCalculator.Build(Input(20, [], filler));
Assert.Equal(6, playlist.MediaSequence);
Assert.Equal(5, playlist.Segments.Count);
Assert.Equal([2, 3, 0, 1, 2], playlist.Segments.Select(s => s.LocalIndex));
Assert.All(playlist.Segments, s => Assert.Equal(filler.AssetId, s.AssetId));
// Чисто-филлерное окно склеек не содержит — DISCONTINUITY-SEQUENCE равен нулю.
Assert.Equal(0, playlist.DiscontinuitySequence);
}
[Fact]
public void LeadingGap_WithoutFiller_TrimmedFromWindowStart()
{
var b = Guid.NewGuid();
var entries = new List<LiveEntry>
{
new(Epoch.AddSeconds(16), Epoch.AddSeconds(40), b, 12, 4),
};
// Окно [6..10]: g6,g7 без записи и без филлера → отброшены, старт с g8.
var playlist = LiveWindowCalculator.Build(Input(20, entries));
Assert.Equal(8, playlist.MediaSequence);
Assert.Equal(3, playlist.Segments.Count);
Assert.Equal([0, 1, 2], playlist.Segments.Select(s => s.LocalIndex));
Assert.DoesNotContain(playlist.Segments, s => s.Discontinuity);
Assert.Equal(4, playlist.DiscontinuitySequence);
}
[Fact]
public void FillerBetweenEntries_KeepsDiscontinuitySequenceConsistentAsWindowSlides()
{
// A g0..4, разрыв g5 (крутится филлер), B с g6. Номера записей отстоят на 2 (вход-в-филлер +
// выход-из-него) — ровно столько склеек и вставит калькулятор между ними.
Guid a = Guid.NewGuid(),
b = Guid.NewGuid();
var entries = new List<LiveEntry>
{
new(Epoch, Epoch.AddSeconds(10), a, 5, 10), // g0..4
new(Epoch.AddSeconds(12), Epoch.AddSeconds(40), b, 14, 12), // g6..
};
var filler = new LiveFiller(Guid.NewGuid(), 4);
// Окно [4..8]: хвост A, филлер, голова B — база отматывается к номеру A.
var mixed = LiveWindowCalculator.Build(Input(16, entries, filler));
Assert.Equal(10, mixed.DiscontinuitySequence);
Assert.Equal(
[false, true, true, false, false],
mixed.Segments.Select(s => s.Discontinuity)
);
// Окно [6..10]: только B — обе склейки (A→филлер, филлер→B) ушли за край, +2 к номеру.
var afterwards = LiveWindowCalculator.Build(Input(20, entries, filler));
Assert.Equal(12, afterwards.DiscontinuitySequence);
Assert.All(afterwards.Segments, s => Assert.Equal(b, s.AssetId));
}
[Fact]
public void BeforeEpoch_ReturnsEmpty()
{
var playlist = LiveWindowCalculator.Build(Input(-10, []));
Assert.Empty(playlist.Segments);
}
}