fix: FFmpeg握手失败后自动切换协议重试 + 修复表格横向滚动条

This commit is contained in:
2026-04-28 00:18:23 +08:00
parent 3f5f832617
commit 5a635a40f9
4 changed files with 160 additions and 8 deletions
@@ -756,6 +756,7 @@ async function handleLogout() {
.app-main { .app-main {
padding: var(--content-padding); padding: var(--content-padding);
min-height: 0; min-height: 0;
overflow-x: hidden;
} }
.backend-alert { .backend-alert {
+2 -1
View File
@@ -1247,7 +1247,8 @@ onBeforeUnmount(() => {
} }
.rooms-table-shell { .rooms-table-shell {
overflow: visible; overflow-x: auto;
overflow-y: hidden;
} }
.rooms-table { .rooms-table {
+2 -1
View File
@@ -940,7 +940,7 @@ onBeforeUnmount(() => {
} }
.tasks-table-shell { .tasks-table-shell {
overflow: visible; overflow-x: auto;
} }
.sessions-card :deep(.el-card__body) { .sessions-card :deep(.el-card__body) {
@@ -1040,6 +1040,7 @@ onBeforeUnmount(() => {
display: grid; display: grid;
gap: 16px; gap: 16px;
padding: 18px 20px 20px; padding: 18px 20px 20px;
overflow-x: hidden;
} }
.session-summary { .session-summary {
@@ -155,7 +155,7 @@ public sealed partial class FfmpegService
} }
var observedAt = DateTimeOffset.UtcNow; var observedAt = DateTimeOffset.UtcNow;
var adapter = adapterFactory.GetByPlatform(session.LiveRoom.Platform); var adapter = adapterFactory.GetByPlatform(session.LiveRoom!.Platform);
var liveStatus = await adapter.GetLiveStatusAsync(session.LiveRoom.RoomId); var liveStatus = await adapter.GetLiveStatusAsync(session.LiveRoom.RoomId);
await liveRoomStatusService.ApplySnapshotAsync(session.LiveRoom, liveStatus, observedAt); await liveRoomStatusService.ApplySnapshotAsync(session.LiveRoom, liveStatus, observedAt);
await dbContext.SaveChangesAsync(); await dbContext.SaveChangesAsync();
@@ -577,14 +577,24 @@ public sealed partial class FfmpegService
return true; return true;
} }
if (runtime.StartupFailureKind != StartupFailureKind.StreamHandshake || runtime.HasRetriedWithRefreshedStream) if (runtime.StartupFailureKind != StartupFailureKind.StreamHandshake)
{ {
return false; return false;
} }
if (runtime.HasRetriedWithRefreshedStream)
{
if (runtime.HasRetriedWithAlternateProtocol)
{
return false;
}
return await TryRetryWithAlternateProtocolAsync(runtime, session, currentTask, observedAt, scope);
}
var adapterFactory = scope.ServiceProvider.GetRequiredService<ILivePlatformAdapterFactory>(); var adapterFactory = scope.ServiceProvider.GetRequiredService<ILivePlatformAdapterFactory>();
var liveRoomStatusService = scope.ServiceProvider.GetRequiredService<LiveRoomStatusService>(); var liveRoomStatusService = scope.ServiceProvider.GetRequiredService<LiveRoomStatusService>();
var adapter = adapterFactory.GetByPlatform(session.LiveRoom.Platform); var adapter = adapterFactory.GetByPlatform(session.LiveRoom!.Platform);
var liveStatus = await adapter.GetLiveStatusAsync(session.LiveRoom.RoomId); var liveStatus = await adapter.GetLiveStatusAsync(session.LiveRoom.RoomId);
await liveRoomStatusService.ApplySnapshotAsync(session.LiveRoom, liveStatus, observedAt); await liveRoomStatusService.ApplySnapshotAsync(session.LiveRoom, liveStatus, observedAt);
await dbContext.SaveChangesAsync(); await dbContext.SaveChangesAsync();
@@ -604,15 +614,52 @@ public sealed partial class FfmpegService
currentTask.Id); currentTask.Id);
var refreshedStream = await adapter.GetStreamUrlAsync(session.LiveRoom.RoomId, session.PreferredQuality); var refreshedStream = await adapter.GetStreamUrlAsync(session.LiveRoom.RoomId, session.PreferredQuality);
session.MarkStarting(refreshedStream.SelectedUrl, session.OutputPathPattern ?? runtime.OutputPathPattern, observedAt);
var selectedUrl = refreshedStream.SelectedUrl;
var selectedProtocol = refreshedStream.SelectedProtocol;
var selectedQuality = refreshedStream.SelectedQuality;
if (refreshedStream.AvailableQualities.Count > 1)
{
var alternateProtocol = refreshedStream.SelectedProtocol.Equals("flv", StringComparison.OrdinalIgnoreCase)
? "hls"
: "flv";
var alternateOption = refreshedStream.AvailableQualities
.Where(o => o.Protocol.Equals(alternateProtocol, StringComparison.OrdinalIgnoreCase) &&
o.QualityKey == refreshedStream.SelectedQuality)
.MaxBy(o => o.Rank);
if (alternateOption is not null)
{
selectedUrl = alternateOption.Url;
selectedProtocol = alternateOption.Protocol;
selectedQuality = alternateOption.QualityKey;
await logService.WriteAsync(
SystemLogLevel.Info,
"FFmpeg",
$"ffmpeg retry will use alternate protocol {selectedProtocol.ToUpperInvariant()} for the same quality tier.",
liveRoomId: session.LiveRoomId,
recordSessionId: session.Id,
recordTaskId: currentTask.Id);
}
}
var streamForRetry = new StreamUrlResult(
selectedQuality,
selectedProtocol,
selectedUrl,
refreshedStream.InputHeaders,
refreshedStream.AvailableQualities,
refreshedStream.SelectedVideoCodec);
session.MarkStarting(streamForRetry.SelectedUrl, session.OutputPathPattern ?? runtime.OutputPathPattern, observedAt);
session.ActivateSegment(Math.Max(1, runtime.CurrentSegmentIndex), observedAt); session.ActivateSegment(Math.Max(1, runtime.CurrentSegmentIndex), observedAt);
currentTask.MarkStarting(refreshedStream.SelectedUrl, currentTask.OutputFilePath ?? runtime.CurrentOutputFilePath, observedAt); currentTask.MarkStarting(streamForRetry.SelectedUrl, currentTask.OutputFilePath ?? runtime.CurrentOutputFilePath, observedAt);
await dbContext.SaveChangesAsync(); await dbContext.SaveChangesAsync();
await StartInternalAsync( await StartInternalAsync(
session, session,
currentTask, currentTask,
refreshedStream, streamForRetry,
runtime.RecordingSettings, runtime.RecordingSettings,
runtime.InputOptionProfile, runtime.InputOptionProfile,
hasRetriedWithCompatibilityProfile: runtime.HasRetriedWithCompatibilityProfile, hasRetriedWithCompatibilityProfile: runtime.HasRetriedWithCompatibilityProfile,
@@ -641,6 +688,107 @@ public sealed partial class FfmpegService
} }
} }
private async Task<bool> TryRetryWithAlternateProtocolAsync(
SessionProcessRuntime runtime,
RecordSession session,
RecordTask currentTask,
DateTimeOffset observedAt,
IServiceScope scope)
{
try
{
var dbContext = scope.ServiceProvider.GetRequiredService<LiveRecorderDbContext>();
var logService = scope.ServiceProvider.GetRequiredService<ISystemLogService>();
var adapterFactory = scope.ServiceProvider.GetRequiredService<ILivePlatformAdapterFactory>();
var liveRoomStatusService = scope.ServiceProvider.GetRequiredService<LiveRoomStatusService>();
var adapter = adapterFactory.GetByPlatform(session.LiveRoom!.Platform);
var liveStatus = await adapter.GetLiveStatusAsync(session.LiveRoom.RoomId);
await liveRoomStatusService.ApplySnapshotAsync(session.LiveRoom, liveStatus, observedAt);
await dbContext.SaveChangesAsync();
if (!liveStatus.IsLive)
{
return false;
}
var refreshedStream = await adapter.GetStreamUrlAsync(session.LiveRoom.RoomId, session.PreferredQuality);
var alternateProtocol = runtime.SelectedProtocol.Equals("flv", StringComparison.OrdinalIgnoreCase)
? "hls"
: "flv";
var alternateOption = refreshedStream.AvailableQualities
.Where(o => o.Protocol.Equals(alternateProtocol, StringComparison.OrdinalIgnoreCase))
.OrderByDescending(o => o.Rank)
.FirstOrDefault();
if (alternateOption is null)
{
await logService.WriteAsync(
SystemLogLevel.Warning,
"FFmpeg",
$"No {alternateProtocol.ToUpperInvariant()} stream option available for alternate protocol fallback.",
liveRoomId: session.LiveRoomId,
recordSessionId: session.Id,
recordTaskId: currentTask.Id);
return false;
}
await logService.WriteAsync(
SystemLogLevel.Warning,
"FFmpeg",
$"Refreshed stream URL also failed. Trying alternate protocol {alternateProtocol.ToUpperInvariant()} as last fallback.",
runtime.LastStartupFailureLine,
session.LiveRoomId,
session.Id,
currentTask.Id);
var retryStream = new StreamUrlResult(
alternateOption.QualityKey,
alternateOption.Protocol,
alternateOption.Url,
refreshedStream.InputHeaders,
refreshedStream.AvailableQualities,
refreshedStream.SelectedVideoCodec);
session.MarkStarting(retryStream.SelectedUrl, session.OutputPathPattern ?? runtime.OutputPathPattern, observedAt);
session.ActivateSegment(Math.Max(1, runtime.CurrentSegmentIndex), observedAt);
currentTask.MarkStarting(retryStream.SelectedUrl, currentTask.OutputFilePath ?? runtime.CurrentOutputFilePath, observedAt);
await dbContext.SaveChangesAsync();
runtime.HasRetriedWithAlternateProtocol = true;
await StartInternalAsync(
session,
currentTask,
retryStream,
runtime.RecordingSettings,
runtime.InputOptionProfile,
hasRetriedWithCompatibilityProfile: runtime.HasRetriedWithCompatibilityProfile,
hasRetriedWithRefreshedStream: true,
runtime.RetryAttemptCount + 1);
var retryStartedAt = DateTimeOffset.UtcNow;
session.MarkRunning(retryStartedAt);
currentTask.MarkRunning(retryStartedAt);
await dbContext.SaveChangesAsync();
await logService.WriteAsync(
SystemLogLevel.Info,
"FFmpeg",
$"ffmpeg alternate protocol ({alternateProtocol.ToUpperInvariant()}) retry started.",
liveRoomId: session.LiveRoomId,
recordSessionId: session.Id,
recordTaskId: currentTask.Id);
return true;
}
catch (Exception ex)
{
_logger.LogWarning(ex, "Alternate protocol fallback failed for session {RecordSessionId}", runtime.RecordSessionId);
return false;
}
}
private async Task FinalizeExitedSessionAsync( private async Task FinalizeExitedSessionAsync(
SessionProcessRuntime runtime, SessionProcessRuntime runtime,
Process process, Process process,
@@ -1399,6 +1547,7 @@ public sealed partial class FfmpegService
public FfmpegInputOptionProfile InputOptionProfile { get; } public FfmpegInputOptionProfile InputOptionProfile { get; }
public bool HasRetriedWithCompatibilityProfile { get; } public bool HasRetriedWithCompatibilityProfile { get; }
public bool HasRetriedWithRefreshedStream { get; } public bool HasRetriedWithRefreshedStream { get; }
public bool HasRetriedWithAlternateProtocol { get; set; }
public Process? Process { get; private set; } public Process? Process { get; private set; }
public int ProcessId => Process?.Id ?? 0; public int ProcessId => Process?.Id ?? 0;
public bool CompletionRequested { get; private set; } public bool CompletionRequested { get; private set; }