feat: improve recording recovery and upload workflow
This commit is contained in:
@@ -0,0 +1,110 @@
|
||||
using LiveRecorder.Domain.Entities;
|
||||
using LiveRecorder.Domain.Enums;
|
||||
using LiveRecorder.Infrastructure.Services;
|
||||
using System.Xml.Linq;
|
||||
|
||||
namespace LiveRecorder.Tests;
|
||||
|
||||
public sealed class RecordingContinuityTests
|
||||
{
|
||||
[Theory]
|
||||
[InlineData(true, false, true)]
|
||||
[InlineData(false, true, true)]
|
||||
[InlineData(false, false, false)]
|
||||
public void SessionTransition_IsTreatedAsActive(bool process, bool transition, bool expected) =>
|
||||
Assert.Equal(expected, FfmpegService.IsSessionRuntimeActive(process, transition));
|
||||
|
||||
[Theory]
|
||||
[InlineData(1, 5)]
|
||||
[InlineData(2, 15)]
|
||||
[InlineData(3, 30)]
|
||||
[InlineData(4, 60)]
|
||||
[InlineData(5, 120)]
|
||||
[InlineData(50, 120)]
|
||||
public void RuntimeRecoveryBackoff_IsBoundedAtTwoMinutes(int attempt, int seconds) =>
|
||||
Assert.Equal(TimeSpan.FromSeconds(seconds), FfmpegService.GetRuntimeRecoveryDelay(attempt));
|
||||
|
||||
[Theory]
|
||||
[InlineData(null, false, false)]
|
||||
[InlineData(0.0, false, false)]
|
||||
[InlineData(0.1, true, false)]
|
||||
[InlineData(4.99, true, false)]
|
||||
[InlineData(5.0, true, true)]
|
||||
public void MediaDuration_SeparatesReadableFromStandalone(double? duration, bool readable, bool standalone)
|
||||
{
|
||||
Assert.Equal(readable, FfmpegService.IsReadableMediaDuration(duration));
|
||||
Assert.Equal(standalone, FfmpegService.IsStandaloneMediaDuration(duration));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void OfflineSample_RequiresConfirmation_ButLiveDoesNot()
|
||||
{
|
||||
Assert.True(LiveRoomPollingBackgroundService.RequiresOfflineConfirmation(false));
|
||||
Assert.False(LiveRoomPollingBackgroundService.RequiresOfflineConfirmation(true));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(false, RecordTaskStatus.Starting, true)]
|
||||
[InlineData(false, RecordTaskStatus.Running, true)]
|
||||
[InlineData(true, RecordTaskStatus.Running, false)]
|
||||
[InlineData(false, RecordTaskStatus.Completed, false)]
|
||||
public void ArtifactlessRecovery_ReusesOnlyActiveTask(bool hasMedia, RecordTaskStatus status, bool expected) =>
|
||||
Assert.Equal(expected, FfmpegService.ShouldReuseRecoveryTask(hasMedia, status));
|
||||
|
||||
[Fact]
|
||||
public void SessionRecoveryMarker_KeepsSessionActive()
|
||||
{
|
||||
var session = new RecordSession(Guid.NewGuid(), "origin", RecordOutputFormat.Mp4, RecordSaveMode.Segmented, DateTimeOffset.UtcNow);
|
||||
session.MarkRecovering("[runtime-recovery] attempt=4; curl EOF", DateTimeOffset.UtcNow);
|
||||
|
||||
Assert.Equal(RecordSessionStatus.Starting, session.Status);
|
||||
Assert.StartsWith("[runtime-recovery]", session.ErrorMessage);
|
||||
Assert.Null(session.EndedAt);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void MergedSource_PointsAtTargetAndBecomesHidden()
|
||||
{
|
||||
var source = new RecordTask(Guid.NewGuid(), Guid.NewGuid(), 2, "origin", RecordOutputFormat.Mp4, DateTimeOffset.UtcNow);
|
||||
var targetId = Guid.NewGuid();
|
||||
source.MarkMergedSource(targetId, DateTimeOffset.UtcNow);
|
||||
|
||||
Assert.True(source.IsHiddenArtifactSource);
|
||||
Assert.Equal(targetId, source.MergedIntoRecordTaskId);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void MergeWindow_FirstShortFragment_MergesForward()
|
||||
{
|
||||
var window = ShortFragmentConsolidationService.ResolveMergeWindow([1.2, 60], 0);
|
||||
Assert.Equal((0, 1), window);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void MergeWindow_TrailingShortFragment_MergesBackward()
|
||||
{
|
||||
var window = ShortFragmentConsolidationService.ResolveMergeWindow([60, 1.2], 0);
|
||||
Assert.Equal((0, 1), window);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void MergeWindow_ConsecutiveShortFragments_AreAccumulated()
|
||||
{
|
||||
var window = ShortFragmentConsolidationService.ResolveMergeWindow([1.2, 2.4, 60], 1);
|
||||
Assert.Equal((0, 2), window);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DanmakuMerge_ShiftsRelativeOffset_ButPreservesAbsoluteTimestamp()
|
||||
{
|
||||
var chat = XElement.Parse("<d p=\"1.5,1,25,16777215,1720000000000,0,1,0\">hi</d>");
|
||||
var shiftedChat = ShortFragmentConsolidationService.AdjustDanmakuElementOffsets(chat, 4.25);
|
||||
Assert.StartsWith("5.8,", shiftedChat.Attribute("p")!.Value);
|
||||
Assert.Contains("1720000000000", shiftedChat.Attribute("p")!.Value);
|
||||
|
||||
var eventElement = XElement.Parse("<event ts=\"1720000000123\" offset=\"2.0\" />");
|
||||
var shiftedEvent = ShortFragmentConsolidationService.AdjustDanmakuElementOffsets(eventElement, 4.25);
|
||||
Assert.Equal("6.2", shiftedEvent.Attribute("offset")!.Value);
|
||||
Assert.Equal("1720000000123", shiftedEvent.Attribute("ts")!.Value);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user