Implement Discontinuity Index for Live Streaming Enhancements
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

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.
This commit is contained in:
Leonid Pershin
2026-08-04 12:43:35 +03:00
parent 7a164dbd90
commit 0f609db4e9
16 changed files with 1851 additions and 26 deletions
@@ -27,7 +27,7 @@ public class LiveWindowCalculatorTests
public void SingleEntry_ProducesWindowWithMonotonicMediaSequence()
{
var asset = Guid.NewGuid();
var entries = new List<LiveEntry> { new(Epoch, Epoch.AddSeconds(100), asset, 50) };
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));
@@ -38,6 +38,8 @@ public class LiveWindowCalculatorTests
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]
@@ -47,8 +49,8 @@ public class LiveWindowCalculatorTests
b = Guid.NewGuid();
var entries = new List<LiveEntry>
{
new(Epoch, Epoch.AddSeconds(16), a, 8), // g0..7
new(Epoch.AddSeconds(16), Epoch.AddSeconds(40), b, 12), // g8..
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));
@@ -60,6 +62,8 @@ public class LiveWindowCalculatorTests
[false, false, true, false, false],
playlist.Segments.Select(s => s.Discontinuity)
);
// Первый сегмент окна принадлежит A — база берётся из его номера.
Assert.Equal(5, playlist.DiscontinuitySequence);
}
[Fact]
@@ -73,6 +77,8 @@ public class LiveWindowCalculatorTests
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]
@@ -81,7 +87,7 @@ public class LiveWindowCalculatorTests
var b = Guid.NewGuid();
var entries = new List<LiveEntry>
{
new(Epoch.AddSeconds(16), Epoch.AddSeconds(40), b, 12),
new(Epoch.AddSeconds(16), Epoch.AddSeconds(40), b, 12, 4),
};
// Окно [6..10]: g6,g7 без записи и без филлера → отброшены, старт с g8.
@@ -91,6 +97,35 @@ public class LiveWindowCalculatorTests
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]