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 {
padding: var(--content-padding);
min-height: 0;
overflow-x: hidden;
}
.backend-alert {
+2 -1
View File
@@ -1247,7 +1247,8 @@ onBeforeUnmount(() => {
}
.rooms-table-shell {
overflow: visible;
overflow-x: auto;
overflow-y: hidden;
}
.rooms-table {
+2 -1
View File
@@ -940,7 +940,7 @@ onBeforeUnmount(() => {
}
.tasks-table-shell {
overflow: visible;
overflow-x: auto;
}
.sessions-card :deep(.el-card__body) {
@@ -1040,6 +1040,7 @@ onBeforeUnmount(() => {
display: grid;
gap: 16px;
padding: 18px 20px 20px;
overflow-x: hidden;
}
.session-summary {
@@ -155,7 +155,7 @@ public sealed partial class FfmpegService
}
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);
await liveRoomStatusService.ApplySnapshotAsync(session.LiveRoom, liveStatus, observedAt);
await dbContext.SaveChangesAsync();
@@ -577,14 +577,24 @@ public sealed partial class FfmpegService
return true;
}
if (runtime.StartupFailureKind != StartupFailureKind.StreamHandshake || runtime.HasRetriedWithRefreshedStream)
if (runtime.StartupFailureKind != StartupFailureKind.StreamHandshake)
{
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 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);
await liveRoomStatusService.ApplySnapshotAsync(session.LiveRoom, liveStatus, observedAt);
await dbContext.SaveChangesAsync();
@@ -604,15 +614,52 @@ public sealed partial class FfmpegService
currentTask.Id);
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);
currentTask.MarkStarting(refreshedStream.SelectedUrl, currentTask.OutputFilePath ?? runtime.CurrentOutputFilePath, observedAt);
currentTask.MarkStarting(streamForRetry.SelectedUrl, currentTask.OutputFilePath ?? runtime.CurrentOutputFilePath, observedAt);
await dbContext.SaveChangesAsync();
await StartInternalAsync(
session,
currentTask,
refreshedStream,
streamForRetry,
runtime.RecordingSettings,
runtime.InputOptionProfile,
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(
SessionProcessRuntime runtime,
Process process,
@@ -1399,6 +1547,7 @@ public sealed partial class FfmpegService
public FfmpegInputOptionProfile InputOptionProfile { get; }
public bool HasRetriedWithCompatibilityProfile { get; }
public bool HasRetriedWithRefreshedStream { get; }
public bool HasRetriedWithAlternateProtocol { get; set; }
public Process? Process { get; private set; }
public int ProcessId => Process?.Id ?? 0;
public bool CompletionRequested { get; private set; }