feat: improve recording automation and task workflows
This commit is contained in:
@@ -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_EVENT、LIVE_RECORDER_PLATFORM、LIVE_RECORDER_ROOM_ID、LIVE_RECORDER_ANCHOR、LIVE_RECORDER_RECORD_SESSION_ID、LIVE_RECORDER_RECORD_TASK_ID、LIVE_RECORDER_SEGMENT_INDEX、LIVE_RECORDER_SEGMENT_FILE_PATH、LIVE_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>
|
||||
|
||||
Reference in New Issue
Block a user