From c5346940e797bdc64299a7ffc856289277b19680 Mon Sep 17 00:00:00 2001 From: nanxun Date: Fri, 8 May 2026 23:33:09 +0800 Subject: [PATCH] feat: add manual segment completed trigger --- frontend/src/views/RecordTaskDetailView.vue | 39 +++++++++++++- .../Scripting/IEventScriptService.cs | 1 + .../Models/RecordTasks/RecordTaskModels.cs | 9 ++++ .../Services/RecordService.cs | 54 +++++++++++++++++++ .../Services/EventScriptService.cs | 3 +- .../Controllers/RecordTasksController.cs | 6 +++ 6 files changed, 110 insertions(+), 2 deletions(-) diff --git a/frontend/src/views/RecordTaskDetailView.vue b/frontend/src/views/RecordTaskDetailView.vue index 7f6c1f8..fc35cdf 100644 --- a/frontend/src/views/RecordTaskDetailView.vue +++ b/frontend/src/views/RecordTaskDetailView.vue @@ -4,7 +4,12 @@ import { useRouter } from "vue-router"; import { ElMessage } from "element-plus"; import apiClient, { getApiErrorMessage } from "@/api/client"; import { useViewport } from "@/composables/useViewport"; -import type { RecordArtifactUploadItemResult, RecordPreviewTicket, RecordTaskDetail } from "@/types"; +import type { + ManualSegmentCompletedTriggerResult, + RecordArtifactUploadItemResult, + RecordPreviewTicket, + RecordTaskDetail +} from "@/types"; import { formatQualityLabel, logLevelLabelMap, @@ -23,6 +28,7 @@ const loading = ref(false); const previewLoading = ref(false); const manualTranscodeLoading = ref(false); const uploadLoading = ref(false); +const triggerEventLoading = ref(false); const detail = ref(null); const loadError = ref(""); const previewUrl = ref(""); @@ -43,6 +49,14 @@ const canManualTranscode = computed(() => { const errorText = `${task.errorMessage ?? ""} ${result?.errorMessage ?? ""}`.toLowerCase(); return resultPath.endsWith(".ts") || errorText.includes("finaliz") || errorText.includes("intermediate ts"); }); +const canTriggerSegmentCompleted = computed(() => { + const task = detail.value?.task; + if (!task || activeTaskStatuses.has(task.status)) { + return false; + } + + return Boolean(detail.value?.result?.filePath || task.outputFilePath); +}); async function loadDetailAndPreview() { loading.value = true; @@ -144,6 +158,22 @@ async function uploadTaskArtifacts() { } } +async function triggerSegmentCompleted() { + triggerEventLoading.value = true; + + try { + const { data } = await apiClient.post( + `/record-tasks/${props.id}/trigger-segment-completed` + ); + ElMessage[data.success ? "success" : "warning"](data.message); + await loadDetailAndPreview(); + } catch (error) { + ElMessage.error(getApiErrorMessage(error, "手动触发分片完成事件失败,请稍后重试。")); + } finally { + triggerEventLoading.value = false; + } +} + function statusTagType(status: number) { if (status === 2) { return "success"; @@ -221,6 +251,13 @@ onMounted(loadDetailAndPreview); 返回列表 刷新 + + 触发分片完成事件 + 上传文件 diff --git a/src/LiveRecorder.Application/Abstractions/Scripting/IEventScriptService.cs b/src/LiveRecorder.Application/Abstractions/Scripting/IEventScriptService.cs index 6eedff4..c97a586 100644 --- a/src/LiveRecorder.Application/Abstractions/Scripting/IEventScriptService.cs +++ b/src/LiveRecorder.Application/Abstractions/Scripting/IEventScriptService.cs @@ -16,6 +16,7 @@ public interface IEventScriptService RecordResult? recordResult, string segmentFilePath, DateTimeOffset occurredAt, + bool forceRun = false, CancellationToken cancellationToken = default); Task TestAsync( diff --git a/src/LiveRecorder.Application/Models/RecordTasks/RecordTaskModels.cs b/src/LiveRecorder.Application/Models/RecordTasks/RecordTaskModels.cs index 54f3d4e..81284af 100644 --- a/src/LiveRecorder.Application/Models/RecordTasks/RecordTaskModels.cs +++ b/src/LiveRecorder.Application/Models/RecordTasks/RecordTaskModels.cs @@ -155,3 +155,12 @@ public sealed class RecordArtifactUploadBatchResultDto public required IReadOnlyList Items { get; init; } } + +public sealed class ManualSegmentCompletedTriggerResultDto +{ + public Guid RecordTaskId { get; init; } + + public bool Success { get; init; } + + public required string Message { get; init; } +} diff --git a/src/LiveRecorder.Application/Services/RecordService.cs b/src/LiveRecorder.Application/Services/RecordService.cs index b7a77cc..74347d1 100644 --- a/src/LiveRecorder.Application/Services/RecordService.cs +++ b/src/LiveRecorder.Application/Services/RecordService.cs @@ -5,6 +5,7 @@ using LiveRecorder.Application.Abstractions.Notifications; using LiveRecorder.Application.Abstractions.Persistence; using LiveRecorder.Application.Abstractions.Platforms; using LiveRecorder.Application.Abstractions.Recording; +using LiveRecorder.Application.Abstractions.Scripting; using LiveRecorder.Application.Abstractions.Settings; using LiveRecorder.Application.Abstractions.Storage; using LiveRecorder.Application.Models.RecordTasks; @@ -29,6 +30,7 @@ public sealed class RecordService private readonly ISystemLogService _systemLogService; private readonly IEmailNotificationService _emailNotificationService; private readonly IWebhookNotificationService _webhookNotificationService; + private readonly IEventScriptService _eventScriptService; private readonly LiveRoomStatusService _liveRoomStatusService; private readonly LiveRoomRecordingSettingsResolver _liveRoomRecordingSettingsResolver; private readonly IStorageGuardService _storageGuardService; @@ -47,6 +49,7 @@ public sealed class RecordService ISystemLogService systemLogService, IEmailNotificationService emailNotificationService, IWebhookNotificationService webhookNotificationService, + IEventScriptService eventScriptService, LiveRoomStatusService liveRoomStatusService, LiveRoomRecordingSettingsResolver liveRoomRecordingSettingsResolver, IStorageGuardService storageGuardService, @@ -64,6 +67,7 @@ public sealed class RecordService _systemLogService = systemLogService; _emailNotificationService = emailNotificationService; _webhookNotificationService = webhookNotificationService; + _eventScriptService = eventScriptService; _liveRoomStatusService = liveRoomStatusService; _liveRoomRecordingSettingsResolver = liveRoomRecordingSettingsResolver; _storageGuardService = storageGuardService; @@ -536,6 +540,56 @@ public sealed class RecordService ?? throw new KeyNotFoundException("Recording task was not found after starting manual transcoding."); } + public async Task TriggerSegmentCompletedEventAsync( + Guid id, + CancellationToken cancellationToken = default) + { + var recordTask = await _recordTaskRepository.GetByIdAsync(id, cancellationToken) + ?? throw new KeyNotFoundException("Recording task was not found."); + + if (IsActiveTaskStatus(recordTask.Status)) + { + throw new InvalidOperationException("The recording task is still active. Stop or wait for the segment to finish before triggering the completion event manually."); + } + + var recordSession = await _recordSessionRepository.GetByIdAsync(recordTask.RecordSessionId, cancellationToken) + ?? throw new KeyNotFoundException("Recording session was not found."); + + var segmentFilePath = recordTask.Result?.FilePath ?? recordTask.OutputFilePath; + if (string.IsNullOrWhiteSpace(segmentFilePath)) + { + throw new InvalidOperationException("The recording task does not have a segment output path to use for the completion event."); + } + + var occurredAt = DateTimeOffset.UtcNow; + await _eventScriptService.RunSegmentCompletedAsync( + recordTask.LiveRoom ?? recordSession.LiveRoom, + recordSession, + recordTask, + recordTask.Result, + segmentFilePath, + occurredAt, + forceRun: true, + cancellationToken); + + await _systemLogService.WriteAsync( + SystemLogLevel.Info, + "Script", + "Segment completed event was triggered manually.", + segmentFilePath, + liveRoomId: recordTask.LiveRoomId, + recordSessionId: recordTask.RecordSessionId, + recordTaskId: recordTask.Id, + cancellationToken: cancellationToken); + + return new ManualSegmentCompletedTriggerResultDto + { + RecordTaskId = recordTask.Id, + Success = true, + Message = "已手动触发分片完成事件。" + }; + } + public async Task StopAsync(Guid id, CancellationToken cancellationToken = default) { var recordTask = await _recordTaskRepository.GetByIdAsync(id, cancellationToken) diff --git a/src/LiveRecorder.Infrastructure/Services/EventScriptService.cs b/src/LiveRecorder.Infrastructure/Services/EventScriptService.cs index a637417..4d91fea 100644 --- a/src/LiveRecorder.Infrastructure/Services/EventScriptService.cs +++ b/src/LiveRecorder.Infrastructure/Services/EventScriptService.cs @@ -77,6 +77,7 @@ public sealed class EventScriptService : IEventScriptService RecordResult? recordResult, string segmentFilePath, DateTimeOffset occurredAt, + bool forceRun = false, CancellationToken cancellationToken = default) { var settings = await _settingsService.GetAsync(cancellationToken); @@ -93,7 +94,7 @@ public sealed class EventScriptService : IEventScriptService environment["LIVE_RECORDER_SESSION_STATUS"] = recordSession.Status.ToString(); await RunAsync( - settings.EnableEventScripts && settings.EnableSegmentCompletedScript, + forceRun || (settings.EnableEventScripts && settings.EnableSegmentCompletedScript), settings.SegmentCompletedScriptMode, settings.SegmentCompletedScriptPath, settings.SegmentCompletedScriptContent, diff --git a/src/LiveRecorder.WebApi/Controllers/RecordTasksController.cs b/src/LiveRecorder.WebApi/Controllers/RecordTasksController.cs index 2cac459..4067bbd 100644 --- a/src/LiveRecorder.WebApi/Controllers/RecordTasksController.cs +++ b/src/LiveRecorder.WebApi/Controllers/RecordTasksController.cs @@ -80,6 +80,12 @@ public sealed class RecordTasksController : ControllerBase public async Task> StartManualTranscode(Guid id, CancellationToken cancellationToken) => Ok(await _recordService.StartManualTranscodeAsync(id, cancellationToken)); + [HttpPost("{id:guid}/trigger-segment-completed")] + public async Task> TriggerSegmentCompleted( + Guid id, + CancellationToken cancellationToken) => + Ok(await _recordService.TriggerSegmentCompletedEventAsync(id, cancellationToken)); + [HttpPost("{id:guid}/upload")] public async Task> Upload(Guid id, CancellationToken cancellationToken) => Ok(await _recordUploadService.UploadTaskAsync(id, cancellationToken));