|
|
|
@@ -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; }
|
|
|
|
|