535 lines
21 KiB
C#
535 lines
21 KiB
C#
using LiveRecorder.Domain.Enums;
|
|
using LiveRecorder.Application.Abstractions.Platforms;
|
|
using LiveRecorder.Infrastructure.Services;
|
|
using System.Diagnostics;
|
|
|
|
namespace LiveRecorder.Tests;
|
|
|
|
public sealed class FfmpegFailureClassificationTests
|
|
{
|
|
[Fact]
|
|
public void FfprobeProcess_DoesNotInheritBundledCurlLibraries()
|
|
{
|
|
var startInfo = new ProcessStartInfo();
|
|
startInfo.Environment["LD_LIBRARY_PATH"] = "/app/runtime/lib";
|
|
|
|
FfmpegVideoMetadataService.SanitizeFfprobeProcessEnvironment(startInfo);
|
|
|
|
Assert.False(startInfo.Environment.ContainsKey("LD_LIBRARY_PATH"));
|
|
}
|
|
|
|
[Fact]
|
|
public void FfmpegHeaderFallback_ParsesValidMediaMetadata()
|
|
{
|
|
const string output = """
|
|
Input #0, mov,mp4,m4a,3gp,3g2,mj2, from '/records/sample.mp4':
|
|
Duration: 00:02:36.64, start: 0.090000, bitrate: 2788 kb/s
|
|
Stream #0:0[0x1](und): Video: h264 (High) (avc1 / 0x31637661), yuv420p(tv, progressive), 1088x1920, 2668 kb/s, 22 fps, 22 tbr, 90k tbn (default)
|
|
Stream #0:1[0x2](und): Audio: aac (LC) (mp4a / 0x6134706D), 48000 Hz, stereo, fltp, 115 kb/s (default)
|
|
Stream mapping:
|
|
""";
|
|
|
|
var metadata = FfmpegVideoMetadataService.ParseFfmpegHeaderOutput(output);
|
|
|
|
Assert.NotNull(metadata);
|
|
Assert.Equal(156.64, metadata.DurationSeconds);
|
|
Assert.Equal(1088, metadata.Width);
|
|
Assert.Equal(1920, metadata.Height);
|
|
Assert.Equal("h264", metadata.VideoCodec);
|
|
Assert.Equal("aac", metadata.AudioCodec);
|
|
Assert.Equal(22, metadata.FrameRate);
|
|
Assert.Equal(2_788_000, metadata.BitRate);
|
|
}
|
|
|
|
[Theory]
|
|
[InlineData("")]
|
|
[InlineData("Duration: N/A")]
|
|
[InlineData("Duration: 00:00:00.00, bitrate: N/A")]
|
|
public void FfmpegHeaderFallback_RejectsUnreadableMedia(string output)
|
|
{
|
|
Assert.Null(FfmpegVideoMetadataService.ParseFfmpegHeaderOutput(output));
|
|
}
|
|
|
|
[Theory]
|
|
[InlineData("pipe:0: Invalid data found when processing input")]
|
|
[InlineData("Error opening input file pipe:0.")]
|
|
[InlineData("Error writing trailer: Invalid argument")]
|
|
[InlineData("Error muxing a packet")]
|
|
[InlineData("Conversion failed!")]
|
|
public void RecoverableFfmpegLines_ArePersistedAsWarnings(string line)
|
|
{
|
|
Assert.True(FfmpegService.IsRecoverableFfmpegWarningLine(line));
|
|
Assert.True(FfmpegService.TryClassifyPersistedFfmpegLine(line, isError: true, out var level));
|
|
Assert.Equal(SystemLogLevel.Warning, level);
|
|
}
|
|
|
|
[Theory]
|
|
[InlineData("Error writing trailer: Invalid argument")]
|
|
[InlineData("Error muxing a packet")]
|
|
[InlineData("Conversion failed!")]
|
|
public void MuxingFailures_TriggerRepairTranscodeFallback(string detail)
|
|
{
|
|
Assert.True(FfmpegService.IsRepairableMp4FinalizeError(detail));
|
|
}
|
|
|
|
[Theory]
|
|
[InlineData("Application provided invalid, non monotonically increasing dts to muxer in stream 1")]
|
|
[InlineData("Non-monotonous DTS in output stream 0:1")]
|
|
public void TimestampDiscontinuityFailures_EnableTimestampRepair(string line)
|
|
{
|
|
Assert.True(FfmpegService.IsTimestampDiscontinuityFailureLine(line));
|
|
}
|
|
|
|
[Theory]
|
|
[InlineData("Error submitting a packet to the muxer: Invalid argument")]
|
|
[InlineData("Error muxing a packet")]
|
|
[InlineData("Task finished with error code: -22")]
|
|
public void TimestampRepairMuxerFailures_EnableTranscodeFallback(string line)
|
|
{
|
|
Assert.True(FfmpegService.IsTimestampMuxerFailureLine(line));
|
|
}
|
|
|
|
[Theory]
|
|
[InlineData(0.2, 85_000, false)]
|
|
[InlineData(5, 85_000, true)]
|
|
[InlineData(0.2, 1_048_576, false)]
|
|
public void UnexpectedExitArtifacts_RequireMeaningfulMediaDuration(
|
|
double durationSeconds,
|
|
long fileSizeBytes,
|
|
bool expected)
|
|
{
|
|
Assert.Equal(
|
|
expected,
|
|
FfmpegService.IsMeaningfulUnexpectedExitArtifact(durationSeconds, fileSizeBytes));
|
|
}
|
|
|
|
[Theory]
|
|
[InlineData(59, false)]
|
|
[InlineData(60, true)]
|
|
[InlineData(3600, true)]
|
|
public void StableRuntime_ResetsInSessionRecoveryBudget(int seconds, bool expected)
|
|
{
|
|
var now = DateTimeOffset.Parse("2026-08-04T20:00:00+08:00");
|
|
Assert.Equal(expected, FfmpegService.CanResetInSessionRetryBudget(now.AddSeconds(-seconds), now));
|
|
}
|
|
|
|
[Theory]
|
|
[InlineData("https://example.test/live.flv", "flv", false, false)]
|
|
[InlineData("https://example.test/live.flv", "flv", true, true)]
|
|
[InlineData("https://example.test/live.m3u8", "hls", true, false)]
|
|
[InlineData("https://example.test/playlist", "hls", true, false)]
|
|
public void CurlPipe_IsOnlyUsedForExplicitFlvFallback(
|
|
string url,
|
|
string protocol,
|
|
bool useCurlFallback,
|
|
bool expected)
|
|
{
|
|
Assert.Equal(expected, FfmpegService.ShouldUseCurlPipe(url, protocol, useCurlFallback));
|
|
}
|
|
|
|
[Fact]
|
|
public void NativeFlvInput_UsesFfmpegReconnectByDefault()
|
|
{
|
|
var arguments = FfmpegService.BuildArgumentList(
|
|
"https://cdn.example.test/live.flv",
|
|
"/tmp/output.ts",
|
|
RecordOutputFormat.Ts,
|
|
RecordSaveMode.SingleFile,
|
|
1,
|
|
RecordingTemplateType.StreamCopy,
|
|
true,
|
|
10,
|
|
30_000_000,
|
|
30,
|
|
null,
|
|
"flv",
|
|
"h264",
|
|
FfmpegService.FfmpegInputOptionProfile.Baseline);
|
|
|
|
Assert.Contains("https://cdn.example.test/live.flv", arguments);
|
|
Assert.DoesNotContain("pipe:0", arguments);
|
|
Assert.Contains("-reconnect", arguments);
|
|
Assert.Contains("-reconnect_streamed", arguments);
|
|
Assert.Contains("-reconnect_at_eof", arguments);
|
|
}
|
|
|
|
[Theory]
|
|
[InlineData("flv", true, false, true)]
|
|
[InlineData("flv", true, true, false)]
|
|
[InlineData("flv", false, false, false)]
|
|
[InlineData("hls", true, false, false)]
|
|
public void OverlongHeaders_UsesCurlFallbackAtMostOnce(
|
|
string protocol,
|
|
bool hasOverlongHeadersFailure,
|
|
bool hasTriedCurlFallback,
|
|
bool expected)
|
|
{
|
|
Assert.Equal(
|
|
expected,
|
|
FfmpegService.ShouldFallbackToCurl(protocol, hasOverlongHeadersFailure, hasTriedCurlFallback));
|
|
}
|
|
|
|
[Fact]
|
|
public void CurlFallback_UsesPipeWithFiniteRetriesAndPreservesHeaders()
|
|
{
|
|
var headers = new StreamInputHeaders(
|
|
"RecorderTest/1.0",
|
|
"https://live.example.test/room",
|
|
"session=abc",
|
|
new Dictionary<string, string> { ["X-Test"] = "yes" });
|
|
var ffmpegArguments = FfmpegService.BuildArgumentList(
|
|
"https://cdn.example.test/live.flv",
|
|
"/tmp/output.ts",
|
|
RecordOutputFormat.Ts,
|
|
RecordSaveMode.SingleFile,
|
|
1,
|
|
RecordingTemplateType.StreamCopy,
|
|
true,
|
|
10,
|
|
30_000_000,
|
|
30,
|
|
headers,
|
|
"flv",
|
|
"h264",
|
|
FfmpegService.FfmpegInputOptionProfile.Baseline,
|
|
useCurlFallback: true);
|
|
var curlArguments = FfmpegService.BuildCurlArgumentList(
|
|
"https://cdn.example.test/live.flv",
|
|
headers);
|
|
|
|
Assert.Contains("pipe:0", ffmpegArguments);
|
|
Assert.DoesNotContain("-reconnect", ffmpegArguments);
|
|
Assert.Contains("--show-error", curlArguments);
|
|
Assert.Contains("--fail", curlArguments);
|
|
Assert.Contains("--retry", curlArguments);
|
|
Assert.Contains("--retry-all-errors", curlArguments);
|
|
Assert.Contains("--retry-max-time", curlArguments);
|
|
Assert.Contains("RecorderTest/1.0", curlArguments);
|
|
Assert.Contains("https://live.example.test/room", curlArguments);
|
|
Assert.Contains("session=abc", curlArguments);
|
|
Assert.Contains("X-Test: yes", curlArguments);
|
|
}
|
|
|
|
[Fact]
|
|
public void NativeHlsInput_PreservesRequestHeaders()
|
|
{
|
|
var headers = new StreamInputHeaders(
|
|
"RecorderTest/1.0",
|
|
"https://live.example.test/room",
|
|
"session=abc",
|
|
new Dictionary<string, string> { ["X-Test"] = "yes" });
|
|
|
|
var arguments = FfmpegService.BuildArgumentList(
|
|
"https://cdn.example.test/live/index.m3u8",
|
|
"/tmp/output.ts",
|
|
RecordOutputFormat.Ts,
|
|
RecordSaveMode.SingleFile,
|
|
1,
|
|
RecordingTemplateType.StreamCopy,
|
|
true,
|
|
10,
|
|
30_000_000,
|
|
30,
|
|
headers,
|
|
"hls",
|
|
"h264",
|
|
FfmpegService.FfmpegInputOptionProfile.Baseline,
|
|
useCurlFallback: true);
|
|
|
|
Assert.Contains("https://cdn.example.test/live/index.m3u8", arguments);
|
|
Assert.DoesNotContain("pipe:0", arguments);
|
|
Assert.Contains("-user_agent", arguments);
|
|
Assert.Contains("RecorderTest/1.0", arguments);
|
|
Assert.Contains("-referer", arguments);
|
|
Assert.Contains(arguments, item => item.Contains("Cookie: session=abc", StringComparison.Ordinal));
|
|
Assert.Contains(arguments, item => item.Contains("X-Test: yes", StringComparison.Ordinal));
|
|
}
|
|
|
|
[Fact]
|
|
public void TimestampTranscodeProfile_RebuildsVideoAndAudioTimestamps()
|
|
{
|
|
var arguments = FfmpegService.BuildArgumentList(
|
|
"https://cdn.example.test/live.flv",
|
|
"/tmp/output.ts",
|
|
RecordOutputFormat.Ts,
|
|
RecordSaveMode.Segmented,
|
|
1,
|
|
RecordingTemplateType.StreamCopy,
|
|
true,
|
|
10,
|
|
30_000_000,
|
|
30,
|
|
null,
|
|
"flv",
|
|
"h264",
|
|
FfmpegService.FfmpegInputOptionProfile.TimestampTranscode);
|
|
|
|
Assert.Contains("settb=AVTB,setpts=PTS-STARTPTS", arguments);
|
|
Assert.Contains("libx264", arguments);
|
|
Assert.Contains("aresample=async=1:first_pts=0,asetpts=PTS-STARTPTS", arguments);
|
|
Assert.Contains("make_zero", arguments);
|
|
Assert.Contains("-reset_timestamps", arguments);
|
|
}
|
|
|
|
[Fact]
|
|
public void RecoveryContext_LeavesCurlAfterExitWithoutResettingOneShotBudget()
|
|
{
|
|
var curlContext = FfmpegService.InitialRecoveryContext with
|
|
{
|
|
UseCurlFallback = true,
|
|
HasTriedCurlFallback = true
|
|
};
|
|
|
|
var nextContext = FfmpegService.AdvanceRecoveryContext(
|
|
curlContext,
|
|
curlContext.InputOptionProfile,
|
|
"flv",
|
|
"flv",
|
|
refreshedStream: true);
|
|
|
|
Assert.False(nextContext.UseCurlFallback);
|
|
Assert.True(nextContext.HasTriedCurlFallback);
|
|
Assert.True(nextContext.HasRetriedWithRefreshedStream);
|
|
Assert.False(FfmpegService.ShouldFallbackToCurl("flv", true, nextContext.HasTriedCurlFallback));
|
|
}
|
|
[Fact]
|
|
public void RecoveryContext_PreservesBudgetAcrossHlsFallbackAndReachesTimestampTranscode()
|
|
{
|
|
var repairProfile = FfmpegService.ResolveRetryInputOptionProfile(
|
|
FfmpegService.FfmpegInputOptionProfile.Baseline,
|
|
hasTimestampDiscontinuityFailure: true,
|
|
hasTimestampMuxerFailure: false);
|
|
var repairHls = FfmpegService.AdvanceRecoveryContext(
|
|
FfmpegService.InitialRecoveryContext,
|
|
repairProfile,
|
|
"flv",
|
|
"hls");
|
|
|
|
Assert.Equal(FfmpegService.FfmpegInputOptionProfile.TimestampRepair, repairHls.InputOptionProfile);
|
|
Assert.Equal(1, repairHls.AttemptCount);
|
|
Assert.True(repairHls.HasRetriedWithAlternateProtocol);
|
|
Assert.True(FfmpegService.ShouldImmediatelyFallbackFromHls("hls", hasHlsOverlongHeadersFailure: true));
|
|
|
|
var repairFlv = FfmpegService.AdvanceRecoveryContext(
|
|
repairHls,
|
|
repairHls.InputOptionProfile,
|
|
"hls",
|
|
"flv");
|
|
var transcodeProfile = FfmpegService.ResolveRetryInputOptionProfile(
|
|
repairFlv.InputOptionProfile,
|
|
hasTimestampDiscontinuityFailure: false,
|
|
hasTimestampMuxerFailure: true);
|
|
var transcodeFlv = FfmpegService.AdvanceRecoveryContext(
|
|
repairFlv,
|
|
transcodeProfile,
|
|
"flv",
|
|
"flv");
|
|
|
|
Assert.Equal(2, repairFlv.AttemptCount);
|
|
Assert.Equal(3, transcodeFlv.AttemptCount);
|
|
Assert.Equal(FfmpegService.FfmpegInputOptionProfile.TimestampTranscode, transcodeFlv.InputOptionProfile);
|
|
Assert.True(transcodeFlv.HasRetriedWithAlternateProtocol);
|
|
Assert.False(transcodeFlv.HasRetriedWithRefreshedStream);
|
|
}
|
|
|
|
[Theory]
|
|
[InlineData(RecordSessionStatus.Failed, 0, true)]
|
|
[InlineData(RecordSessionStatus.Failed, 59, true)]
|
|
[InlineData(RecordSessionStatus.Failed, 60, false)]
|
|
[InlineData(RecordSessionStatus.Completed, 1, false)]
|
|
public void RuntimeFailureBackoff_UsesProcessRuntimeInsteadOfMediaDuration(
|
|
RecordSessionStatus status,
|
|
int processRuntimeSeconds,
|
|
bool expected)
|
|
{
|
|
Assert.Equal(
|
|
expected,
|
|
FfmpegService.ShouldApplyRuntimeFailureBackoff(status, TimeSpan.FromSeconds(processRuntimeSeconds)));
|
|
}
|
|
|
|
[Theory]
|
|
[InlineData(true, false, false, 2)]
|
|
[InlineData(false, true, true, 0)]
|
|
[InlineData(false, true, false, 3)]
|
|
public void DeploymentShutdown_ClassifiesFinalizedAndRecoverableArtifactsWithoutFalseFailures(
|
|
bool mediaValid,
|
|
bool hasFinalizationError,
|
|
bool hasRecoverableIntermediateOutput,
|
|
int expected)
|
|
{
|
|
var disposition = FfmpegService.ClassifyExitedRecording(
|
|
shutdownRequested: true,
|
|
stopRequested: false,
|
|
finalizationPaused: false,
|
|
hasFinalizationError,
|
|
mediaValid,
|
|
exitCode: 0,
|
|
completionRequested: true,
|
|
hasUsableOutput: true,
|
|
hasRecoverableIntermediateOutput);
|
|
|
|
Assert.Equal((ExitedRecordingDisposition)expected, disposition);
|
|
}
|
|
|
|
[Fact]
|
|
public void DeploymentShutdown_InterruptedFinalizationRemainsRecoverable()
|
|
{
|
|
var disposition = FfmpegService.ClassifyExitedRecording(
|
|
shutdownRequested: true,
|
|
stopRequested: false,
|
|
finalizationPaused: true,
|
|
hasFinalizationError: true,
|
|
mediaValid: false,
|
|
exitCode: 0,
|
|
completionRequested: true,
|
|
hasUsableOutput: true,
|
|
hasRecoverableIntermediateOutput: true);
|
|
|
|
Assert.Equal(ExitedRecordingDisposition.Processing, disposition);
|
|
}
|
|
|
|
[Theory]
|
|
[InlineData("Unknown encoder h264_nvenc")]
|
|
[InlineData("Cannot load libcuda.so.1")]
|
|
[InlineData("No VA display found for device /dev/dri/renderD128")]
|
|
[InlineData("Error initializing an internal MFX session")]
|
|
[InlineData("Impossible to convert between the formats supported by the filter")]
|
|
public void HardwareEncoderFailures_TriggerSoftwareFallback(string line)
|
|
{
|
|
Assert.True(FfmpegService.IsHardwareEncoderFailureLine(line));
|
|
}
|
|
|
|
[Fact]
|
|
public void HardwareRecoveryEncoders_UseExpectedCodecAndDeviceArguments()
|
|
{
|
|
var nvenc = new RecoveryVideoEncoderSelection(RecoveryVideoEncoderKind.Nvenc, null);
|
|
var qsv = new RecoveryVideoEncoderSelection(RecoveryVideoEncoderKind.Qsv, "/dev/dri/renderD128");
|
|
var vaapi = new RecoveryVideoEncoderSelection(RecoveryVideoEncoderKind.Vaapi, "/dev/dri/renderD129");
|
|
|
|
var nvencArguments = FfmpegService.BuildRecoveryEncoderProbeArgumentList(nvenc);
|
|
var qsvArguments = FfmpegService.BuildRecoveryEncoderProbeArgumentList(qsv);
|
|
var vaapiArguments = FfmpegService.BuildRecoveryEncoderProbeArgumentList(vaapi);
|
|
|
|
Assert.Contains("h264_nvenc", nvencArguments);
|
|
Assert.Contains("h264_qsv", qsvArguments);
|
|
Assert.Contains("-qsv_device", qsvArguments);
|
|
Assert.Contains("/dev/dri/renderD128", qsvArguments);
|
|
Assert.Contains("h264_vaapi", vaapiArguments);
|
|
Assert.Contains("-vaapi_device", vaapiArguments);
|
|
Assert.Contains("/dev/dri/renderD129", vaapiArguments);
|
|
}
|
|
|
|
[Fact]
|
|
public void IntelEncoderCandidates_PreferQsvThenVaapiForEachRenderDevice()
|
|
{
|
|
var candidates = FfmpegService.BuildIntelVideoEncoderCandidates(
|
|
["/dev/dri/renderD128", "/dev/dri/renderD129"]);
|
|
|
|
Assert.Equal(
|
|
[
|
|
new RecoveryVideoEncoderSelection(RecoveryVideoEncoderKind.Qsv, "/dev/dri/renderD128"),
|
|
new RecoveryVideoEncoderSelection(RecoveryVideoEncoderKind.Qsv, "/dev/dri/renderD129"),
|
|
new RecoveryVideoEncoderSelection(RecoveryVideoEncoderKind.Vaapi, "/dev/dri/renderD128"),
|
|
new RecoveryVideoEncoderSelection(RecoveryVideoEncoderKind.Vaapi, "/dev/dri/renderD129")
|
|
],
|
|
candidates);
|
|
}
|
|
|
|
[Fact]
|
|
public void Mp4Repair_WithQsv_UsesHardwareAndPlacesDeviceBeforeInput()
|
|
{
|
|
var arguments = FfmpegService.BuildMp4FinalizeArgumentList(
|
|
"/records/input.ts",
|
|
"/records/output.mp4",
|
|
FfmpegService.Mp4FinalizeStrategy.RepairTranscode,
|
|
videoEncoder: new RecoveryVideoEncoderSelection(RecoveryVideoEncoderKind.Qsv, "/dev/dri/renderD128"));
|
|
|
|
var argumentList = arguments.ToList();
|
|
Assert.True(argumentList.IndexOf("-qsv_device") < argumentList.IndexOf("-i"));
|
|
Assert.Contains("h264_qsv", arguments);
|
|
Assert.DoesNotContain("hwupload", string.Join(',', arguments));
|
|
Assert.DoesNotContain("libx264", arguments);
|
|
Assert.Contains("aac", arguments);
|
|
}
|
|
|
|
[Fact]
|
|
public void Mp4Repair_SoftwareFallback_UsesLibx264()
|
|
{
|
|
var arguments = FfmpegService.BuildMp4FinalizeArgumentList(
|
|
"/records/input.ts",
|
|
"/records/output.mp4",
|
|
FfmpegService.Mp4FinalizeStrategy.RepairTranscode,
|
|
videoEncoder: RecoveryVideoEncoderSelection.Software);
|
|
|
|
Assert.Contains("libx264", arguments);
|
|
Assert.DoesNotContain("h264_qsv", arguments);
|
|
Assert.DoesNotContain("h264_vaapi", arguments);
|
|
Assert.DoesNotContain("-vf", arguments);
|
|
Assert.DoesNotContain("-fps_mode", arguments);
|
|
}
|
|
|
|
[Fact]
|
|
public void BalancedMp4_WithVaapi_UsesHardwareWhileStreamCopyRemainsUnchanged()
|
|
{
|
|
var vaapi = new RecoveryVideoEncoderSelection(RecoveryVideoEncoderKind.Vaapi, "/dev/dri/renderD128");
|
|
var balanced = FfmpegService.BuildArgumentList(
|
|
"https://cdn.example.test/live.flv", "/tmp/output.mp4",
|
|
RecordOutputFormat.Mp4, RecordSaveMode.SingleFile, 1,
|
|
RecordingTemplateType.BalancedMp4, true, 10, 30_000_000, 30,
|
|
null, "flv", "h264", FfmpegService.FfmpegInputOptionProfile.Baseline, vaapi);
|
|
var streamCopy = FfmpegService.BuildArgumentList(
|
|
"https://cdn.example.test/live.flv", "/tmp/output.mp4",
|
|
RecordOutputFormat.Mp4, RecordSaveMode.SingleFile, 1,
|
|
RecordingTemplateType.StreamCopy, true, 10, 30_000_000, 30,
|
|
null, "flv", "h264", FfmpegService.FfmpegInputOptionProfile.Baseline, vaapi);
|
|
|
|
var balancedList = balanced.ToList();
|
|
Assert.True(balancedList.IndexOf("-vaapi_device") < balancedList.IndexOf("-i"));
|
|
Assert.Contains("h264_vaapi", balanced);
|
|
Assert.DoesNotContain("libx264", balanced);
|
|
Assert.Contains("copy", streamCopy);
|
|
Assert.DoesNotContain("h264_vaapi", streamCopy);
|
|
Assert.DoesNotContain("libx264", streamCopy);
|
|
}
|
|
|
|
[Theory]
|
|
[InlineData("Permission denied", false)]
|
|
[InlineData("Invalid data found when processing input", false)]
|
|
[InlineData("No VA display found for device /dev/dri/renderD128", true)]
|
|
[InlineData("Failed to create QSV device", true)]
|
|
[InlineData("Failed to upload frame: -5", true)]
|
|
public void OnlyHardwareEncoderFailures_EnableSoftwareFallback(string line, bool expected)
|
|
{
|
|
Assert.Equal(expected, FfmpegService.IsHardwareEncoderFailureLine(line));
|
|
}
|
|
|
|
[Fact]
|
|
public void TimestampTranscode_WithQsv_PlacesDeviceBeforeInputAndKeepsTimestampFilters()
|
|
{
|
|
var arguments = FfmpegService.BuildArgumentList(
|
|
"https://cdn.example.test/live.flv",
|
|
"/tmp/output.ts",
|
|
RecordOutputFormat.Ts,
|
|
RecordSaveMode.Segmented,
|
|
1,
|
|
RecordingTemplateType.StreamCopy,
|
|
true,
|
|
10,
|
|
30_000_000,
|
|
30,
|
|
null,
|
|
"flv",
|
|
"h264",
|
|
FfmpegService.FfmpegInputOptionProfile.TimestampTranscode,
|
|
new RecoveryVideoEncoderSelection(RecoveryVideoEncoderKind.Qsv, "/dev/dri/renderD128"));
|
|
|
|
var argumentList = arguments.ToList();
|
|
Assert.True(argumentList.IndexOf("-qsv_device") < argumentList.IndexOf("-i"));
|
|
Assert.Contains("h264_qsv", arguments);
|
|
Assert.Contains(arguments, item => item.Contains("setpts=PTS-STARTPTS", StringComparison.Ordinal));
|
|
Assert.Contains(arguments, item => item.Contains("format=nv12", StringComparison.Ordinal));
|
|
Assert.DoesNotContain(arguments, item => item.Contains("hwupload", StringComparison.Ordinal));
|
|
Assert.Contains("aresample=async=1:first_pts=0,asetpts=PTS-STARTPTS", arguments);
|
|
}
|
|
}
|