feat: accelerate ffmpeg encoding with intel gpu
This commit is contained in:
@@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user