Enhance configuration and routing for streaming features: update .env.example and appsettings.json to include new scheduler and streaming options, add streaming endpoint mapping in Program.cs, and integrate streaming services in DependencyInjection. Introduce segment path resolution in MediaPathResolver for asset management.

This commit is contained in:
Leonid Pershin
2026-07-24 16:07:37 +03:00
parent 4fa9dae37f
commit 1bbfd15907
18 changed files with 686 additions and 0 deletions
@@ -0,0 +1,100 @@
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) };
// 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);
}
[Fact]
public void EntryBoundary_InsertsDiscontinuity()
{
Guid a = Guid.NewGuid(),
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..
};
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)
);
}
[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));
}
[Fact]
public void LeadingGap_WithoutFiller_TrimmedFromWindowStart()
{
var b = Guid.NewGuid();
var entries = new List<LiveEntry> { new(Epoch.AddSeconds(16), Epoch.AddSeconds(40), b, 12) };
// Окно [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);
}
[Fact]
public void BeforeEpoch_ReturnsEmpty()
{
var playlist = LiveWindowCalculator.Build(Input(-10, []));
Assert.Empty(playlist.Segments);
}
}