feat: improve recording automation and task workflows

This commit is contained in:
2026-04-23 23:18:11 +08:00
parent 1c892259a9
commit 23ead56781
88 changed files with 9579 additions and 1581 deletions
+133 -1
View File
@@ -1,13 +1,14 @@
<script setup lang="ts">
import { computed, onMounted, reactive, ref } from "vue";
import { ElMessage } from "element-plus";
import apiClient from "@/api/client";
import apiClient, { getApiErrorMessage } from "@/api/client";
import type { SystemSettings } from "@/types";
import { outputFormatLabelMap, recordingTemplateLabelMap, saveModeLabelMap } from "@/types";
const loading = ref(false);
const saving = ref(false);
const testingEmail = ref(false);
const loadError = ref("");
const form = reactive<SystemSettings>({
ffmpegPath: "ffmpeg",
@@ -19,6 +20,11 @@ const form = reactive<SystemSettings>({
saveMode: 0,
recordingTemplate: 0,
segmentDurationMinutes: 30,
maxConcurrentFfmpegTranscodeTasks: 1,
mp4FinalizeTimeoutMinutes: 60,
enableStorageGuard: true,
pauseRecordingWhenFreeSpaceBelowMegabytes: 1024,
resumeRecordingWhenFreeSpaceAboveMegabytes: 4096,
enableAutoReconnect: true,
reconnectDelayMaxSeconds: 5,
readWriteTimeoutMilliseconds: 15000000,
@@ -29,6 +35,11 @@ const form = reactive<SystemSettings>({
enableBackgroundPolling: true,
autoStartRecordingOnLive: true,
pollingIntervalSeconds: 60,
enableEventScripts: false,
liveStartedScriptPath: "",
liveEndedScriptPath: "",
segmentCompletedScriptPath: "",
eventScriptTimeoutSeconds: 60,
enableEmailNotification: false,
emailSmtpHost: "",
emailSmtpPort: 587,
@@ -120,10 +131,13 @@ const nestedSegmentedExamplePath = computed(() => {
async function loadSettings() {
loading.value = true;
loadError.value = "";
try {
const { data } = await apiClient.get<SystemSettings>("/settings");
Object.assign(form, data);
} catch (error) {
loadError.value = getApiErrorMessage(error, "系统设置加载失败,请稍后重试。");
} finally {
loading.value = false;
}
@@ -185,6 +199,8 @@ onMounted(loadSettings);
<el-button type="primary" :loading="saving" @click="saveSettings">保存设置</el-button>
</div>
<el-alert v-if="loadError" class="page-error-alert" type="error" :closable="false" show-icon :title="loadError" />
<div class="settings-grid" v-loading="loading">
<el-card class="surface-card settings-card" shadow="never">
<h3 class="section-title">录制基础</h3>
@@ -248,6 +264,16 @@ onMounted(loadSettings);
<el-input-number v-model="form.segmentDurationMinutes" :min="1" :max="720" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="同时转码任务数">
<el-input-number v-model="form.maxConcurrentFfmpegTranscodeTasks" :min="1" :max="16" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="MP4 转码超时(分钟)">
<el-input-number v-model="form.mp4FinalizeTimeoutMinutes" :min="1" :max="1440" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="最大重连延迟(秒)">
<el-input-number v-model="form.reconnectDelayMaxSeconds" :min="1" :max="300" />
@@ -267,6 +293,45 @@ onMounted(loadSettings);
</el-form>
</el-card>
<el-card class="surface-card settings-card" shadow="never">
<h3 class="section-title">存储保护</h3>
<p class="section-subtitle">磁盘空间低于暂停阈值时会停止活跃录制并暂停 MP4 转码空间恢复到恢复阈值后后台巡检会重新启动录制并补转码</p>
<el-form label-position="top">
<el-row :gutter="16">
<el-col :span="12">
<el-form-item label="启用存储空间保护">
<el-switch v-model="form.enableStorageGuard" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="低于该空间暂停录制(MB">
<el-input-number
v-model="form.pauseRecordingWhenFreeSpaceBelowMegabytes"
:min="0"
:max="1048576"
:disabled="!form.enableStorageGuard"
/>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="高于该空间恢复录制/转码(MB)">
<el-input-number
v-model="form.resumeRecordingWhenFreeSpaceAboveMegabytes"
:min="0"
:max="1048576"
:disabled="!form.enableStorageGuard"
/>
</el-form-item>
</el-col>
</el-row>
</el-form>
<div class="helper-panel">
恢复阈值建议大于暂停阈值避免磁盘空间在临界值附近反复暂停和恢复MP4 转码会额外预留源 TS 文件大小避免再次出现 No space left on device
</div>
</el-card>
<el-card class="surface-card settings-card" shadow="never">
<h3 class="section-title">弹幕录制</h3>
<p class="section-subtitle">控制是否并行录制弹幕 XML是否保留非聊天事件以及轮询和失败重连节奏</p>
@@ -375,6 +440,50 @@ onMounted(loadSettings);
</el-form>
</el-card>
<el-card class="surface-card settings-card settings-grid__full" shadow="never">
<h3 class="section-title">事件脚本</h3>
<p class="section-subtitle">主播开播下播分片完成时可执行自定义脚本脚本通过环境变量获取上下文分片完成时可读取 LIVE_RECORDER_SEGMENT_FILE_PATH</p>
<el-form label-position="top">
<el-row :gutter="16">
<el-col :span="8">
<el-form-item label="启用事件脚本">
<el-switch v-model="form.enableEventScripts" />
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="脚本超时(秒)">
<el-input-number
v-model="form.eventScriptTimeoutSeconds"
:min="1"
:max="3600"
:disabled="!form.enableEventScripts"
/>
</el-form-item>
</el-col>
<el-col :span="24">
<el-form-item label="开播脚本路径">
<el-input v-model="form.liveStartedScriptPath" :disabled="!form.enableEventScripts" placeholder="/app/scripts/live-started.sh" />
</el-form-item>
</el-col>
<el-col :span="24">
<el-form-item label="下播脚本路径">
<el-input v-model="form.liveEndedScriptPath" :disabled="!form.enableEventScripts" placeholder="/app/scripts/live-ended.sh" />
</el-form-item>
</el-col>
<el-col :span="24">
<el-form-item label="分片完成脚本路径">
<el-input v-model="form.segmentCompletedScriptPath" :disabled="!form.enableEventScripts" placeholder="/app/scripts/segment-completed.sh" />
</el-form-item>
</el-col>
</el-row>
</el-form>
<div class="helper-panel">
常用环境变量LIVE_RECORDER_EVENTLIVE_RECORDER_PLATFORMLIVE_RECORDER_ROOM_IDLIVE_RECORDER_ANCHORLIVE_RECORDER_RECORD_SESSION_IDLIVE_RECORDER_RECORD_TASK_IDLIVE_RECORDER_SEGMENT_INDEXLIVE_RECORDER_SEGMENT_FILE_PATHLIVE_RECORDER_DANMAKU_FILE_PATH
</div>
</el-card>
<el-card class="surface-card settings-card settings-grid__full" shadow="never">
<h3 class="section-title">邮件通知</h3>
<p class="section-subtitle">支持 SMTP开播提醒和异常提醒 HTML 模板测试发送会直接使用当前表单中的 SMTP 与模板</p>
@@ -531,6 +640,10 @@ onMounted(loadSettings);
gap: 24px;
}
.page-error-alert {
border-radius: 14px;
}
.settings-grid {
display: grid;
grid-template-columns: minmax(0, 1.15fr) minmax(320px, 0.85fr);
@@ -649,9 +762,28 @@ onMounted(loadSettings);
}
@media (max-width: 900px) {
.page-header :deep(.el-button) {
width: 100%;
}
.email-actions {
flex-direction: column;
align-items: flex-start;
}
}
@media (max-width: 768px) {
.settings-card :deep(.el-col) {
flex: 0 0 100%;
max-width: 100%;
}
.template-section {
padding: 16px 16px 2px;
}
.email-actions :deep(.el-button) {
width: 100%;
}
}
</style>