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:
@@ -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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user