feat: accelerate ffmpeg encoding with intel gpu

This commit is contained in:
2026-08-14 01:46:08 +08:00
parent 83b5ea042d
commit 7a33b2c6e3
5 changed files with 261 additions and 58 deletions
@@ -1,6 +1,7 @@
using System.Diagnostics; using System.Diagnostics;
using System.Text.RegularExpressions; using System.Text.RegularExpressions;
using LiveRecorder.Domain.Enums; using LiveRecorder.Domain.Enums;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
namespace LiveRecorder.Infrastructure.Services; namespace LiveRecorder.Infrastructure.Services;
@@ -53,7 +54,7 @@ public sealed partial class FfmpegService
TimeSpan processRuntime) => TimeSpan processRuntime) =>
status == RecordSessionStatus.Failed && processRuntime < StableRuntimeResetThreshold; status == RecordSessionStatus.Failed && processRuntime < StableRuntimeResetThreshold;
private async Task<RecoveryVideoEncoderSelection> ResolveRecoveryVideoEncoderAsync( private async Task<RecoveryVideoEncoderSelection> ResolveVideoEncoderAsync(
string ffmpegPath, string ffmpegPath,
bool forceSoftware, bool forceSoftware,
CancellationToken cancellationToken) CancellationToken cancellationToken)
@@ -76,11 +77,6 @@ public sealed partial class FfmpegService
return _cachedRecoveryVideoEncoder; return _cachedRecoveryVideoEncoder;
} }
var candidates = new List<RecoveryVideoEncoderSelection>
{
new(RecoveryVideoEncoderKind.Nvenc, null)
};
IReadOnlyList<string> renderDevices = []; IReadOnlyList<string> renderDevices = [];
try try
{ {
@@ -93,18 +89,10 @@ public sealed partial class FfmpegService
} }
catch (Exception ex) when (ex is IOException or UnauthorizedAccessException) catch (Exception ex) when (ex is IOException or UnauthorizedAccessException)
{ {
_logger.LogDebug(ex, "Unable to enumerate /dev/dri render devices for recovery encoding."); _logger.LogDebug(ex, "Unable to enumerate /dev/dri render devices for hardware encoding.");
} }
foreach (var devicePath in renderDevices) var candidates = BuildIntelVideoEncoderCandidates(renderDevices);
{
candidates.Add(new RecoveryVideoEncoderSelection(RecoveryVideoEncoderKind.Qsv, devicePath));
}
foreach (var devicePath in renderDevices)
{
candidates.Add(new RecoveryVideoEncoderSelection(RecoveryVideoEncoderKind.Vaapi, devicePath));
}
foreach (var candidate in candidates) foreach (var candidate in candidates)
{ {
@@ -113,16 +101,21 @@ public sealed partial class FfmpegService
_cachedRecoveryVideoEncoder = candidate; _cachedRecoveryVideoEncoder = candidate;
_hasProbedRecoveryVideoEncoder = true; _hasProbedRecoveryVideoEncoder = true;
_logger.LogInformation( _logger.LogInformation(
"Recovery video encoder probe selected {EncoderKind} using device {DevicePath}", "Video encoder probe selected {EncoderKind} using device {DevicePath}",
candidate.Kind, candidate.Kind,
candidate.DevicePath ?? "default"); candidate.DevicePath ?? "default");
await WriteVideoEncoderSelectionLogAsync(candidate, null, cancellationToken);
return candidate; return candidate;
} }
} }
_cachedRecoveryVideoEncoder = RecoveryVideoEncoderSelection.Software; _cachedRecoveryVideoEncoder = RecoveryVideoEncoderSelection.Software;
_hasProbedRecoveryVideoEncoder = true; _hasProbedRecoveryVideoEncoder = true;
_logger.LogInformation("No usable hardware recovery encoder was detected; libx264 will be used."); _logger.LogInformation("No usable Intel hardware video encoder was detected; libx264 will be used.");
await WriteVideoEncoderSelectionLogAsync(
_cachedRecoveryVideoEncoder,
"No usable QSV or VAAPI encoder was detected.",
cancellationToken);
return _cachedRecoveryVideoEncoder; return _cachedRecoveryVideoEncoder;
} }
finally finally
@@ -190,7 +183,63 @@ public sealed partial class FfmpegService
} }
} }
private void DisableRecoveryVideoEncoder(RecoveryVideoEncoderSelection encoder) internal static IReadOnlyList<RecoveryVideoEncoderSelection> BuildIntelVideoEncoderCandidates(
IReadOnlyList<string> renderDevices) =>
renderDevices
.Select(devicePath => new RecoveryVideoEncoderSelection(RecoveryVideoEncoderKind.Qsv, devicePath))
.Concat(renderDevices.Select(devicePath =>
new RecoveryVideoEncoderSelection(RecoveryVideoEncoderKind.Vaapi, devicePath)))
.ToArray();
private async Task WriteVideoEncoderSelectionLogAsync(
RecoveryVideoEncoderSelection encoder,
string? reason,
CancellationToken cancellationToken)
{
try
{
using var scope = _serviceScopeFactory.CreateScope();
var logService = scope.ServiceProvider.GetRequiredService<LiveRecorder.Application.Abstractions.Logging.ISystemLogService>();
await logService.WriteAsync(
Domain.Enums.SystemLogLevel.Info,
"FFmpeg",
encoder.Kind == RecoveryVideoEncoderKind.Software
? "FFmpeg hardware encoding is unavailable; using libx264."
: $"FFmpeg hardware encoding selected {encoder.Kind}.",
$"encoder={encoder.Kind}; device={encoder.DevicePath ?? "default"}; reason={reason ?? "probe succeeded"}",
cancellationToken: cancellationToken);
}
catch (Exception ex)
{
_logger.LogDebug(ex, "Unable to persist the FFmpeg encoder selection log.");
}
}
private async Task WriteHardwareFallbackLogAsync(
Guid recordTaskId,
RecoveryVideoEncoderSelection encoder,
string error,
CancellationToken cancellationToken)
{
try
{
using var scope = _serviceScopeFactory.CreateScope();
var logService = scope.ServiceProvider.GetRequiredService<LiveRecorder.Application.Abstractions.Logging.ISystemLogService>();
await logService.WriteAsync(
Domain.Enums.SystemLogLevel.Warning,
"FFmpeg",
$"Hardware encoder {encoder.Kind} failed; retrying once with libx264.",
$"encoder={encoder.Kind}; device={encoder.DevicePath ?? "default"}; error={error}",
recordTaskId: recordTaskId,
cancellationToken: cancellationToken);
}
catch (Exception ex)
{
_logger.LogDebug(ex, "Unable to persist the FFmpeg hardware fallback log.");
}
}
private void DisableVideoEncoder(RecoveryVideoEncoderSelection encoder)
{ {
if (encoder.Kind == RecoveryVideoEncoderKind.Software || if (encoder.Kind == RecoveryVideoEncoderKind.Software ||
_cachedRecoveryVideoEncoder != encoder) _cachedRecoveryVideoEncoder != encoder)
@@ -201,7 +250,7 @@ public sealed partial class FfmpegService
_cachedRecoveryVideoEncoder = RecoveryVideoEncoderSelection.Software; _cachedRecoveryVideoEncoder = RecoveryVideoEncoderSelection.Software;
_hasProbedRecoveryVideoEncoder = true; _hasProbedRecoveryVideoEncoder = true;
_logger.LogWarning( _logger.LogWarning(
"Recovery video encoder {EncoderKind} failed with a live input and was disabled until the service restarts.", "Video encoder {EncoderKind} failed and was disabled until the service restarts.",
encoder.Kind); encoder.Kind);
} }
@@ -71,7 +71,6 @@ public sealed partial class FfmpegService
} }
if (!runtime.HasOpenedFirstSegment && if (!runtime.HasOpenedFirstSegment &&
runtime.InputOptionProfile == FfmpegInputOptionProfile.TimestampTranscode &&
runtime.RecoveryVideoEncoder.Kind != RecoveryVideoEncoderKind.Software && runtime.RecoveryVideoEncoder.Kind != RecoveryVideoEncoderKind.Software &&
IsHardwareEncoderFailureLine(line)) IsHardwareEncoderFailureLine(line))
{ {
@@ -223,7 +222,11 @@ public sealed partial class FfmpegService
line.Contains("Device creation failed", StringComparison.OrdinalIgnoreCase) || line.Contains("Device creation failed", StringComparison.OrdinalIgnoreCase) ||
line.Contains("No VA display found", StringComparison.OrdinalIgnoreCase) || line.Contains("No VA display found", StringComparison.OrdinalIgnoreCase) ||
line.Contains("Failed to initialise VAAPI connection", StringComparison.OrdinalIgnoreCase) || line.Contains("Failed to initialise VAAPI connection", StringComparison.OrdinalIgnoreCase) ||
line.Contains("Failed to create a VAAPI device", StringComparison.OrdinalIgnoreCase) ||
line.Contains("Failed to create QSV device", StringComparison.OrdinalIgnoreCase) ||
line.Contains("Error initializing an internal MFX session", StringComparison.OrdinalIgnoreCase) || line.Contains("Error initializing an internal MFX session", StringComparison.OrdinalIgnoreCase) ||
line.Contains("Error creating a MFX session", StringComparison.OrdinalIgnoreCase) ||
line.Contains("Failed to upload frame", StringComparison.OrdinalIgnoreCase) ||
line.Contains("Error initializing output stream", StringComparison.OrdinalIgnoreCase) || line.Contains("Error initializing output stream", StringComparison.OrdinalIgnoreCase) ||
line.Contains("Impossible to convert between the formats", StringComparison.OrdinalIgnoreCase) || line.Contains("Impossible to convert between the formats", StringComparison.OrdinalIgnoreCase) ||
line.Contains("A hardware device reference is required", StringComparison.OrdinalIgnoreCase); line.Contains("A hardware device reference is required", StringComparison.OrdinalIgnoreCase);
@@ -810,7 +813,7 @@ public sealed partial class FfmpegService
session.Id, session.Id,
currentTask.Id); currentTask.Id);
DisableRecoveryVideoEncoder(runtime.RecoveryVideoEncoder); DisableVideoEncoder(runtime.RecoveryVideoEncoder);
var retryStream = new StreamUrlResult( var retryStream = new StreamUrlResult(
runtime.SelectedQuality, runtime.SelectedQuality,
@@ -174,8 +174,10 @@ public sealed partial class FfmpegService
async Task<string?> RunFinalizeAttemptAsync( async Task<string?> RunFinalizeAttemptAsync(
Mp4FinalizeStrategy strategy, Mp4FinalizeStrategy strategy,
string stage, string stage,
string detail) string detail,
RecoveryVideoEncoderSelection? videoEncoder = null)
{ {
videoEncoder ??= RecoveryVideoEncoderSelection.Software;
if (File.Exists(tempPath)) if (File.Exists(tempPath))
{ {
File.Delete(tempPath); File.Delete(tempPath);
@@ -212,7 +214,8 @@ public sealed partial class FfmpegService
concatInputPath ?? normalizedSourcePaths[0], concatInputPath ?? normalizedSourcePaths[0],
tempPath, tempPath,
strategy, strategy,
concatInputPath is not null)) concatInputPath is not null,
videoEncoder))
{ {
finalizeProcess.StartInfo.ArgumentList.Add(argument); finalizeProcess.StartInfo.ArgumentList.Add(argument);
} }
@@ -367,10 +370,25 @@ public sealed partial class FfmpegService
if (!string.IsNullOrWhiteSpace(finalizationError) && IsRepairableMp4FinalizeError(finalizationError)) if (!string.IsNullOrWhiteSpace(finalizationError) && IsRepairableMp4FinalizeError(finalizationError))
{ {
var videoEncoder = await ResolveVideoEncoderAsync(ffmpegPath, forceSoftware: false, cancellationToken);
var repairError = await RunFinalizeAttemptAsync( var repairError = await RunFinalizeAttemptAsync(
Mp4FinalizeStrategy.RepairTranscode, Mp4FinalizeStrategy.RepairTranscode,
"Repairing MP4", "Repairing MP4",
$"Repairing stream metadata for {Path.GetFileName(targetPath)}"); $"Repairing stream metadata for {Path.GetFileName(targetPath)} with {videoEncoder.Kind}",
videoEncoder);
if (!string.IsNullOrWhiteSpace(repairError) &&
videoEncoder.Kind != RecoveryVideoEncoderKind.Software &&
IsHardwareEncoderFailureLine(repairError))
{
DisableVideoEncoder(videoEncoder);
await WriteHardwareFallbackLogAsync(recordTaskId, videoEncoder, repairError, cancellationToken);
repairError = await RunFinalizeAttemptAsync(
Mp4FinalizeStrategy.RepairTranscode,
"Repairing MP4",
$"Hardware encoding failed; retrying {Path.GetFileName(targetPath)} with libx264",
RecoveryVideoEncoderSelection.Software);
}
finalizationError = string.IsNullOrWhiteSpace(repairError) finalizationError = string.IsNullOrWhiteSpace(repairError)
? null ? null
@@ -615,8 +633,10 @@ public sealed partial class FfmpegService
string sourcePath, string sourcePath,
string targetPath, string targetPath,
Mp4FinalizeStrategy strategy, Mp4FinalizeStrategy strategy,
bool useConcatDemuxer = false) bool useConcatDemuxer = false,
RecoveryVideoEncoderSelection? videoEncoder = null)
{ {
videoEncoder ??= RecoveryVideoEncoderSelection.Software;
var arguments = new List<string> var arguments = new List<string>
{ {
"-hide_banner", "-hide_banner",
@@ -639,6 +659,11 @@ public sealed partial class FfmpegService
arguments.AddRange(["-f", "concat", "-safe", "0"]); arguments.AddRange(["-f", "concat", "-safe", "0"]);
} }
if (strategy == Mp4FinalizeStrategy.RepairTranscode)
{
AddRecoveryEncoderDeviceArguments(arguments, videoEncoder);
}
arguments.AddRange( arguments.AddRange(
[ [
"-i", sourcePath, "-i", sourcePath,
@@ -652,14 +677,10 @@ public sealed partial class FfmpegService
if (strategy == Mp4FinalizeStrategy.RepairTranscode) if (strategy == Mp4FinalizeStrategy.RepairTranscode)
{ {
arguments.AddRange( arguments.AddRange(videoEncoder.Kind == RecoveryVideoEncoderKind.Software
[ ? BuildSoftwareVideoCodecArguments()
"-c:v", "libx264", : BuildRecoveryVideoCodecArguments(videoEncoder));
"-preset", "veryfast", arguments.AddRange(["-c:a", "aac", "-b:a", "128k"]);
"-crf", "23",
"-c:a", "aac",
"-b:a", "128k"
]);
} }
else else
{ {
@@ -700,8 +721,27 @@ public sealed partial class FfmpegService
var temporaryPath = $"{targetPath}.repairing"; var temporaryPath = $"{targetPath}.repairing";
string? lastError = null; string? lastError = null;
foreach (var strategy in new[] { Mp4FinalizeStrategy.StreamCopy, Mp4FinalizeStrategy.RepairTranscode }) var videoEncoder = await ResolveVideoEncoderAsync(ffmpegPath, forceSoftware: false, cancellationToken);
var attempts = new List<(Mp4FinalizeStrategy Strategy, RecoveryVideoEncoderSelection Encoder)>
{ {
(Mp4FinalizeStrategy.StreamCopy, RecoveryVideoEncoderSelection.Software),
(Mp4FinalizeStrategy.RepairTranscode, videoEncoder)
};
if (videoEncoder.Kind != RecoveryVideoEncoderKind.Software)
{
attempts.Add((Mp4FinalizeStrategy.RepairTranscode, RecoveryVideoEncoderSelection.Software));
}
foreach (var attempt in attempts)
{
if (attempt.Strategy == Mp4FinalizeStrategy.RepairTranscode &&
attempt.Encoder.Kind == RecoveryVideoEncoderKind.Software &&
videoEncoder.Kind != RecoveryVideoEncoderKind.Software &&
!IsHardwareEncoderFailureLine(lastError ?? string.Empty))
{
break;
}
if (File.Exists(temporaryPath)) if (File.Exists(temporaryPath))
{ {
File.Delete(temporaryPath); File.Delete(temporaryPath);
@@ -720,7 +760,11 @@ public sealed partial class FfmpegService
} }
}; };
var processStarted = false; var processStarted = false;
foreach (var argument in BuildMp4FinalizeArgumentList(sourcePath, temporaryPath, strategy)) foreach (var argument in BuildMp4FinalizeArgumentList(
sourcePath,
temporaryPath,
attempt.Strategy,
videoEncoder: attempt.Encoder))
{ {
process.StartInfo.ArgumentList.Add(argument); process.StartInfo.ArgumentList.Add(argument);
} }
@@ -752,6 +796,15 @@ public sealed partial class FfmpegService
lastError = validation.ErrorMessage; lastError = validation.ErrorMessage;
} }
if (attempt.Strategy == Mp4FinalizeStrategy.RepairTranscode &&
attempt.Encoder.Kind != RecoveryVideoEncoderKind.Software &&
!string.IsNullOrWhiteSpace(lastError) &&
IsHardwareEncoderFailureLine(lastError))
{
DisableVideoEncoder(attempt.Encoder);
await WriteHardwareFallbackLogAsync(recordTaskId, attempt.Encoder, lastError, cancellationToken);
}
} }
catch (OperationCanceledException) catch (OperationCanceledException)
{ {
@@ -885,7 +938,9 @@ public sealed partial class FfmpegService
AddNativeHttpInputHeaders(arguments, streamUrl, inputHeaders); AddNativeHttpInputHeaders(arguments, streamUrl, inputHeaders);
} }
if (inputOptionProfile == FfmpegInputOptionProfile.TimestampTranscode) if (recoveryVideoEncoder.Kind != RecoveryVideoEncoderKind.Software &&
(inputOptionProfile == FfmpegInputOptionProfile.TimestampTranscode ||
recordingTemplate == RecordingTemplateType.BalancedMp4))
{ {
AddRecoveryEncoderDeviceArguments(arguments, recoveryVideoEncoder); AddRecoveryEncoderDeviceArguments(arguments, recoveryVideoEncoder);
} }
@@ -964,15 +1019,7 @@ public sealed partial class FfmpegService
{ {
return recordingTemplate switch return recordingTemplate switch
{ {
RecordingTemplateType.BalancedMp4 => RecordingTemplateType.BalancedMp4 => BuildBalancedMp4CodecArguments(recoveryVideoEncoder, repairTimestamps: true),
[
"-c:v", "libx264",
"-preset", "veryfast",
"-crf", "23",
"-c:a", "aac",
"-af", "aresample=async=1:first_pts=0",
"-b:a", "128k"
],
RecordingTemplateType.ArchiveTs => RecordingTemplateType.ArchiveTs =>
[ [
"-map", "0", "-map", "0",
@@ -995,14 +1042,7 @@ public sealed partial class FfmpegService
return recordingTemplate switch return recordingTemplate switch
{ {
RecordingTemplateType.BalancedMp4 => RecordingTemplateType.BalancedMp4 => BuildBalancedMp4CodecArguments(recoveryVideoEncoder, repairTimestamps: false),
[
"-c:v", "libx264",
"-preset", "veryfast",
"-crf", "23",
"-c:a", "aac",
"-b:a", "128k"
],
RecordingTemplateType.ArchiveTs => RecordingTemplateType.ArchiveTs =>
[ [
"-map", "0", "-map", "0",
@@ -1015,6 +1055,30 @@ public sealed partial class FfmpegService
}; };
} }
private static IReadOnlyList<string> BuildBalancedMp4CodecArguments(
RecoveryVideoEncoderSelection videoEncoder,
bool repairTimestamps)
{
var arguments = new List<string>();
arguments.AddRange(videoEncoder.Kind == RecoveryVideoEncoderKind.Software
? BuildSoftwareVideoCodecArguments()
: BuildRecoveryVideoCodecArguments(videoEncoder));
arguments.AddRange(["-c:a", "aac"]);
if (repairTimestamps)
{
arguments.AddRange(["-af", "aresample=async=1:first_pts=0"]);
}
arguments.AddRange(["-b:a", "128k"]);
return arguments;
}
private static IReadOnlyList<string> BuildSoftwareVideoCodecArguments() =>
[
"-c:v", "libx264",
"-preset", "veryfast",
"-crf", "23"
];
internal static void AddRecoveryEncoderDeviceArguments( internal static void AddRecoveryEncoderDeviceArguments(
ICollection<string> arguments, ICollection<string> arguments,
RecoveryVideoEncoderSelection encoder) RecoveryVideoEncoderSelection encoder)
@@ -1051,7 +1115,7 @@ public sealed partial class FfmpegService
], ],
RecoveryVideoEncoderKind.Qsv => RecoveryVideoEncoderKind.Qsv =>
[ [
"-vf", "settb=AVTB,setpts=PTS-STARTPTS,format=nv12,hwupload=extra_hw_frames=64", "-vf", "settb=AVTB,setpts=PTS-STARTPTS,format=nv12",
"-c:v", "h264_qsv", "-c:v", "h264_qsv",
"-preset", "veryfast", "-preset", "veryfast",
"-global_quality", "23", "-global_quality", "23",
@@ -246,8 +246,10 @@ public sealed partial class FfmpegService : IFfmpegService
using var settingsScope = _serviceScopeFactory.CreateScope(); using var settingsScope = _serviceScopeFactory.CreateScope();
var settingsService = settingsScope.ServiceProvider.GetRequiredService<ISystemSettingsService>(); var settingsService = settingsScope.ServiceProvider.GetRequiredService<ISystemSettingsService>();
var settings = await settingsService.GetAsync(cancellationToken); var settings = await settingsService.GetAsync(cancellationToken);
var recoveryVideoEncoder = recoveryContext.InputOptionProfile == FfmpegInputOptionProfile.TimestampTranscode var requiresVideoEncoding = recoveryContext.InputOptionProfile == FfmpegInputOptionProfile.TimestampTranscode ||
? await ResolveRecoveryVideoEncoderAsync(settings.FfmpegPath, recoveryContext.ForceSoftwareEncoder, cancellationToken) recordingSettings.RecordingTemplate == RecordingTemplateType.BalancedMp4;
var recoveryVideoEncoder = requiresVideoEncoding
? await ResolveVideoEncoderAsync(settings.FfmpegPath, recoveryContext.ForceSoftwareEncoder, cancellationToken)
: RecoveryVideoEncoderSelection.Software; : RecoveryVideoEncoderSelection.Software;
var outputPathPattern = Path.IsPathRooted(recordSession.OutputPathPattern) var outputPathPattern = Path.IsPathRooted(recordSession.OutputPathPattern)
@@ -419,6 +419,90 @@ public sealed class FfmpegFailureClassificationTests
Assert.Contains("/dev/dri/renderD129", 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] [Fact]
public void TimestampTranscode_WithQsv_PlacesDeviceBeforeInputAndKeepsTimestampFilters() public void TimestampTranscode_WithQsv_PlacesDeviceBeforeInputAndKeepsTimestampFilters()
{ {
@@ -443,7 +527,8 @@ public sealed class FfmpegFailureClassificationTests
Assert.True(argumentList.IndexOf("-qsv_device") < argumentList.IndexOf("-i")); Assert.True(argumentList.IndexOf("-qsv_device") < argumentList.IndexOf("-i"));
Assert.Contains("h264_qsv", arguments); Assert.Contains("h264_qsv", arguments);
Assert.Contains(arguments, item => item.Contains("setpts=PTS-STARTPTS", StringComparison.Ordinal)); Assert.Contains(arguments, item => item.Contains("setpts=PTS-STARTPTS", StringComparison.Ordinal));
Assert.Contains(arguments, item => item.Contains("hwupload", 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); Assert.Contains("aresample=async=1:first_pts=0,asetpts=PTS-STARTPTS", arguments);
} }
} }