|
|
@@ -61,6 +61,7 @@
|
|
|
<el-tag size="small">{{ row.pushMethod || 'ffmpeg' }}</el-tag>
|
|
|
</template>
|
|
|
</el-table-column>
|
|
|
+
|
|
|
<el-table-column prop="commandTemplate" :label="t('命令模板')" align="center">
|
|
|
<template #default="{ row }">
|
|
|
<el-link type="primary" @click="openCommandDialog(row)">{{ t('查看') }}</el-link>
|
|
|
@@ -117,68 +118,285 @@
|
|
|
/>
|
|
|
</div>
|
|
|
|
|
|
- <!-- 新增/编辑抽屉 -->
|
|
|
+ <!-- 合并的编辑/播放抽屉 -->
|
|
|
<el-drawer
|
|
|
v-model="drawerVisible"
|
|
|
direction="rtl"
|
|
|
- size="550px"
|
|
|
+ :size="activeDrawerTab === 'edit' ? '550px' : '90%'"
|
|
|
:with-header="false"
|
|
|
destroy-on-close
|
|
|
- class="stream-drawer"
|
|
|
+ class="combined-drawer"
|
|
|
>
|
|
|
<div class="drawer-content">
|
|
|
- <div class="drawer-header">{{ drawerTitle }}</div>
|
|
|
- <div class="drawer-body">
|
|
|
- <el-form ref="formRef" :model="form" :rules="rules" label-width="auto" class="stream-form">
|
|
|
- <el-form-item label="名称:" prop="name">
|
|
|
- <el-input v-model="form.name" placeholder="例如: 测试推流-001" style="width: 300px" />
|
|
|
- </el-form-item>
|
|
|
- <el-form-item label="LSS 节点:" prop="lssId">
|
|
|
- <el-select v-model="form.lssId" placeholder="请选择 LSS 节点" clearable filterable style="width: 300px">
|
|
|
- <el-option
|
|
|
- v-for="lss in lssOptions"
|
|
|
- :key="lss.lssId"
|
|
|
- :label="`${lss.lssId} - ${lss.lssName}`"
|
|
|
- :value="lss.lssId"
|
|
|
- />
|
|
|
- </el-select>
|
|
|
- </el-form-item>
|
|
|
- <el-form-item label="摄像头:" prop="cameraId">
|
|
|
- <el-select v-model="form.cameraId" placeholder="请选择摄像头" clearable filterable style="width: 300px">
|
|
|
- <el-option
|
|
|
- v-for="camera in cameraOptions"
|
|
|
- :key="camera.cameraId"
|
|
|
- :label="`${camera.cameraId} - ${camera.cameraName}`"
|
|
|
- :value="camera.cameraId"
|
|
|
+ <!-- 顶部 Tabs -->
|
|
|
+ <el-tabs v-model="activeDrawerTab" class="drawer-tabs">
|
|
|
+ <el-tab-pane :label="t('编辑')" name="edit" />
|
|
|
+ <el-tab-pane :label="t('播放')" name="play" :disabled="!isEdit" />
|
|
|
+ </el-tabs>
|
|
|
+
|
|
|
+ <!-- 编辑 Tab 内容 -->
|
|
|
+ <div v-show="activeDrawerTab === 'edit'" class="tab-content edit-content">
|
|
|
+ <div class="drawer-body">
|
|
|
+ <el-form ref="formRef" :model="form" :rules="rules" label-width="auto" class="stream-form">
|
|
|
+ <el-form-item label="名称:" prop="name">
|
|
|
+ <el-input v-model="form.name" placeholder="例如: 测试推流-001" style="width: 300px" />
|
|
|
+ </el-form-item>
|
|
|
+ <el-form-item label="LSS 节点:" prop="lssId">
|
|
|
+ <el-select v-model="form.lssId" placeholder="请选择 LSS 节点" clearable filterable style="width: 300px">
|
|
|
+ <el-option
|
|
|
+ v-for="lss in lssOptions"
|
|
|
+ :key="lss.lssId"
|
|
|
+ :label="`${lss.lssId} - ${lss.lssName}`"
|
|
|
+ :value="lss.lssId"
|
|
|
+ />
|
|
|
+ </el-select>
|
|
|
+ </el-form-item>
|
|
|
+ <el-form-item label="摄像头:" prop="cameraId">
|
|
|
+ <el-select v-model="form.cameraId" placeholder="请选择摄像头" clearable filterable style="width: 300px">
|
|
|
+ <el-option
|
|
|
+ v-for="camera in cameraOptions"
|
|
|
+ :key="camera.cameraId"
|
|
|
+ :label="`${camera.cameraId} - ${camera.cameraName}`"
|
|
|
+ :value="camera.cameraId"
|
|
|
+ />
|
|
|
+ </el-select>
|
|
|
+ </el-form-item>
|
|
|
+ <el-form-item label="推流方式:" prop="pushMethod">
|
|
|
+ <el-select disabled v-model="form.pushMethod" placeholder="请选择" style="width: 300px">
|
|
|
+ <el-option label="ffmpeg" value="ffmpeg" />
|
|
|
+ </el-select>
|
|
|
+ </el-form-item>
|
|
|
+ <el-form-item label="命令模板:" prop="commandTemplate">
|
|
|
+ <div class="code-editor-wrapper">
|
|
|
+ <CodeEditor
|
|
|
+ v-model="form.commandTemplate"
|
|
|
+ language="bash"
|
|
|
+ height="200px"
|
|
|
+ placeholder="#!/bin/bash # FFmpeg 命令模板,可使用 {RTSP_URL} 等变量"
|
|
|
+ />
|
|
|
+ </div>
|
|
|
+ </el-form-item>
|
|
|
+ </el-form>
|
|
|
+ </div>
|
|
|
+ <div class="drawer-footer">
|
|
|
+ <el-button @click="drawerVisible = false">{{ t('取消') }}</el-button>
|
|
|
+ <el-button type="primary" :loading="submitLoading" @click="handleSubmit">
|
|
|
+ {{ isEdit ? t('更新') : t('添加') }}
|
|
|
+ </el-button>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <!-- 播放 Tab 内容 -->
|
|
|
+ <div v-show="activeDrawerTab === 'play'" class="tab-content play-content">
|
|
|
+ <div class="media-drawer-content">
|
|
|
+ <!-- 左侧:视频播放区域 -->
|
|
|
+ <div class="video-area">
|
|
|
+ <div class="video-header">
|
|
|
+ <div class="header-left">
|
|
|
+ <span class="title">{{ currentMediaStream?.name || t('流媒体播放') }}</span>
|
|
|
+ <el-tag v-if="playbackInfo.isLive" type="success" size="small">{{ t('直播中') }}</el-tag>
|
|
|
+ <el-tag v-else type="info" size="small">{{ t('离线') }}</el-tag>
|
|
|
+ </div>
|
|
|
+ <el-button type="danger" size="small" @click="drawerVisible = false">
|
|
|
+ <Icon icon="mdi:close" width="16" height="16" />
|
|
|
+ {{ t('关闭') }}
|
|
|
+ </el-button>
|
|
|
+ </div>
|
|
|
+ <div class="player-container">
|
|
|
+ <div v-if="!playbackInfo.videoId" class="player-placeholder">
|
|
|
+ <el-icon :size="80" color="#666">
|
|
|
+ <VideoPlay />
|
|
|
+ </el-icon>
|
|
|
+ <p>{{ t('暂无视频流') }}</p>
|
|
|
+ </div>
|
|
|
+ <VideoPlayer
|
|
|
+ v-else
|
|
|
+ ref="playerRef"
|
|
|
+ player-type="cloudflare"
|
|
|
+ :video-id="playbackInfo.videoId"
|
|
|
+ :customer-domain="playbackInfo.customerDomain"
|
|
|
+ :use-iframe="true"
|
|
|
+ :autoplay="playConfig.autoplay"
|
|
|
+ :muted="playConfig.muted"
|
|
|
+ :controls="true"
|
|
|
/>
|
|
|
- </el-select>
|
|
|
- </el-form-item>
|
|
|
- <el-form-item label="推流方式:" prop="pushMethod">
|
|
|
- <el-select disabled v-model="form.pushMethod" placeholder="请选择" style="width: 300px">
|
|
|
- <el-option label="ffmpeg" value="ffmpeg" />
|
|
|
- </el-select>
|
|
|
- </el-form-item>
|
|
|
- <!-- <el-form-item label="超时时间:" prop="timeoutSeconds">
|
|
|
- <el-input-number v-model="form.timeoutSeconds" :min="1" :max="300" placeholder="秒" style="width: 150px" />
|
|
|
- <span style="margin-left: 8px; color: #909399">秒</span>
|
|
|
- </el-form-item> -->
|
|
|
- <el-form-item label="命令模板:" prop="commandTemplate">
|
|
|
- <div class="code-editor-wrapper">
|
|
|
- <CodeEditor
|
|
|
- v-model="form.commandTemplate"
|
|
|
- language="bash"
|
|
|
- height="200px"
|
|
|
- placeholder="#!/bin/bash # FFmpeg 命令模板,可使用 {RTSP_URL} 等变量"
|
|
|
+ <!-- 开始推流按钮 -->
|
|
|
+ <div v-if="currentMediaStream && currentMediaStream.status !== '1'" class="stream-control-overlay">
|
|
|
+ <el-button type="success" size="large" :loading="streamStarting" @click="handleStartStreamFromPlayer">
|
|
|
+ {{ t('开始推流') }}
|
|
|
+ </el-button>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ <!-- 底部播放控制 -->
|
|
|
+ <div class="player-controls">
|
|
|
+ <el-button type="primary" size="small" @click="handlePlay">{{ t('播放') }}</el-button>
|
|
|
+ <el-button size="small" @click="handlePause">{{ t('暂停') }}</el-button>
|
|
|
+ <el-button type="danger" size="small" @click="handlePlayerStop">{{ t('停止') }}</el-button>
|
|
|
+ <el-button size="small" @click="handleScreenshot">{{ t('截图') }}</el-button>
|
|
|
+ <el-button size="small" @click="handleFullscreen">{{ t('全屏') }}</el-button>
|
|
|
+ <el-switch
|
|
|
+ v-model="playConfig.muted"
|
|
|
+ :active-text="t('静音')"
|
|
|
+ :inactive-text="t('有声')"
|
|
|
+ style="margin-left: 16px"
|
|
|
/>
|
|
|
</div>
|
|
|
- </el-form-item>
|
|
|
- </el-form>
|
|
|
- </div>
|
|
|
- <div class="drawer-footer">
|
|
|
- <el-button @click="drawerVisible = false">{{ t('取消') }}</el-button>
|
|
|
- <el-button type="primary" :loading="submitLoading" @click="handleSubmit">
|
|
|
- {{ isEdit ? t('更新') : t('添加') }}
|
|
|
- </el-button>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <!-- 右侧:PTZ 控制面板 -->
|
|
|
+ <div class="control-panel">
|
|
|
+ <!-- PTZ 方向控制 -->
|
|
|
+ <div class="panel-section">
|
|
|
+ <div class="section-title">{{ t('PTZ') }}</div>
|
|
|
+ <div class="ptz-grid">
|
|
|
+ <div
|
|
|
+ class="ptz-btn"
|
|
|
+ @mousedown="handlePTZ('UP_LEFT')"
|
|
|
+ @mouseup="handlePTZStop"
|
|
|
+ @mouseleave="handlePTZStop"
|
|
|
+ >
|
|
|
+ <el-icon>
|
|
|
+ <TopLeft />
|
|
|
+ </el-icon>
|
|
|
+ </div>
|
|
|
+ <div
|
|
|
+ class="ptz-btn"
|
|
|
+ @mousedown="handlePTZ('UP')"
|
|
|
+ @mouseup="handlePTZStop"
|
|
|
+ @mouseleave="handlePTZStop"
|
|
|
+ >
|
|
|
+ <el-icon>
|
|
|
+ <Top />
|
|
|
+ </el-icon>
|
|
|
+ </div>
|
|
|
+ <div
|
|
|
+ class="ptz-btn"
|
|
|
+ @mousedown="handlePTZ('UP_RIGHT')"
|
|
|
+ @mouseup="handlePTZStop"
|
|
|
+ @mouseleave="handlePTZStop"
|
|
|
+ >
|
|
|
+ <el-icon>
|
|
|
+ <TopRight />
|
|
|
+ </el-icon>
|
|
|
+ </div>
|
|
|
+ <div
|
|
|
+ class="ptz-btn"
|
|
|
+ @mousedown="handlePTZ('LEFT')"
|
|
|
+ @mouseup="handlePTZStop"
|
|
|
+ @mouseleave="handlePTZStop"
|
|
|
+ >
|
|
|
+ <el-icon>
|
|
|
+ <Back />
|
|
|
+ </el-icon>
|
|
|
+ </div>
|
|
|
+ <div class="ptz-btn ptz-center" @click="handlePTZStop">
|
|
|
+ <el-icon>
|
|
|
+ <Refresh />
|
|
|
+ </el-icon>
|
|
|
+ </div>
|
|
|
+ <div
|
|
|
+ class="ptz-btn"
|
|
|
+ @mousedown="handlePTZ('RIGHT')"
|
|
|
+ @mouseup="handlePTZStop"
|
|
|
+ @mouseleave="handlePTZStop"
|
|
|
+ >
|
|
|
+ <el-icon>
|
|
|
+ <Right />
|
|
|
+ </el-icon>
|
|
|
+ </div>
|
|
|
+ <div
|
|
|
+ class="ptz-btn"
|
|
|
+ @mousedown="handlePTZ('DOWN_LEFT')"
|
|
|
+ @mouseup="handlePTZStop"
|
|
|
+ @mouseleave="handlePTZStop"
|
|
|
+ >
|
|
|
+ <el-icon>
|
|
|
+ <BottomLeft />
|
|
|
+ </el-icon>
|
|
|
+ </div>
|
|
|
+ <div
|
|
|
+ class="ptz-btn"
|
|
|
+ @mousedown="handlePTZ('DOWN')"
|
|
|
+ @mouseup="handlePTZStop"
|
|
|
+ @mouseleave="handlePTZStop"
|
|
|
+ >
|
|
|
+ <el-icon>
|
|
|
+ <Bottom />
|
|
|
+ </el-icon>
|
|
|
+ </div>
|
|
|
+ <div
|
|
|
+ class="ptz-btn"
|
|
|
+ @mousedown="handlePTZ('DOWN_RIGHT')"
|
|
|
+ @mouseup="handlePTZStop"
|
|
|
+ @mouseleave="handlePTZStop"
|
|
|
+ >
|
|
|
+ <el-icon>
|
|
|
+ <BottomRight />
|
|
|
+ </el-icon>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <!-- 缩放按钮 -->
|
|
|
+ <div class="zoom-buttons">
|
|
|
+ <el-button
|
|
|
+ size="small"
|
|
|
+ @mousedown="handleZoomIn"
|
|
|
+ @mouseup="handlePTZStop"
|
|
|
+ @mouseleave="handlePTZStop"
|
|
|
+ >
|
|
|
+ <el-icon>
|
|
|
+ <ZoomIn />
|
|
|
+ </el-icon>
|
|
|
+ </el-button>
|
|
|
+ <el-button
|
|
|
+ size="small"
|
|
|
+ @mousedown="handleZoomOut"
|
|
|
+ @mouseup="handlePTZStop"
|
|
|
+ @mouseleave="handlePTZStop"
|
|
|
+ >
|
|
|
+ <el-icon>
|
|
|
+ <ZoomOut />
|
|
|
+ </el-icon>
|
|
|
+ </el-button>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <!-- 速度滑块 -->
|
|
|
+ <div class="speed-slider">
|
|
|
+ <span class="label">{{ t('速度') }}</span>
|
|
|
+ <el-slider v-model="ptzSpeed" :min="10" :max="100" :step="10" size="small" />
|
|
|
+ <span class="value">{{ ptzSpeed }}</span>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <!-- 预置位列表 -->
|
|
|
+ <div class="panel-section preset-section">
|
|
|
+ <div class="section-title">
|
|
|
+ <span>{{ t('预置位') }}</span>
|
|
|
+ <el-button type="primary" link size="small" @click="loadPresets" :loading="presetsLoading">
|
|
|
+ <el-icon>
|
|
|
+ <Refresh />
|
|
|
+ </el-icon>
|
|
|
+ </el-button>
|
|
|
+ </div>
|
|
|
+ <div class="preset-list" v-loading="presetsLoading">
|
|
|
+ <div
|
|
|
+ v-for="preset in presetList"
|
|
|
+ :key="preset.token"
|
|
|
+ :class="['preset-item', { active: activePresetToken === preset.token }]"
|
|
|
+ @click="handleGotoPreset(preset)"
|
|
|
+ >
|
|
|
+ <span class="preset-index">{{ preset.token }}</span>
|
|
|
+ <span class="preset-name">{{ preset.name || `Preset ${preset.token}` }}</span>
|
|
|
+ </div>
|
|
|
+ <el-empty
|
|
|
+ v-if="!presetsLoading && presetList.length === 0"
|
|
|
+ :description="t('暂无预置位')"
|
|
|
+ :image-size="60"
|
|
|
+ />
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
</div>
|
|
|
</div>
|
|
|
</el-drawer>
|
|
|
@@ -198,196 +416,6 @@
|
|
|
</el-button>
|
|
|
</template>
|
|
|
</el-dialog>
|
|
|
-
|
|
|
- <!-- 流媒体播放抽屉 -->
|
|
|
- <el-drawer
|
|
|
- v-model="mediaDrawerVisible"
|
|
|
- direction="rtl"
|
|
|
- size="90%"
|
|
|
- :with-header="false"
|
|
|
- destroy-on-close
|
|
|
- class="media-drawer"
|
|
|
- >
|
|
|
- <!-- 左上角关闭按钮 -->
|
|
|
- <div class="drawer-close-btn" @click="mediaDrawerVisible = false">
|
|
|
- <el-icon :size="20">
|
|
|
- <Close />
|
|
|
- </el-icon>
|
|
|
- </div>
|
|
|
- <div class="media-drawer-content">
|
|
|
- <!-- 左侧:视频播放区域 -->
|
|
|
- <div class="video-area">
|
|
|
- <div class="video-header">
|
|
|
- <div class="header-left">
|
|
|
- <span class="title">{{ currentMediaStream?.name || t('流媒体播放') }}</span>
|
|
|
- <el-tag v-if="playbackInfo.isLive" type="success" size="small">{{ t('直播中') }}</el-tag>
|
|
|
- <el-tag v-else type="info" size="small">{{ t('离线') }}</el-tag>
|
|
|
- </div>
|
|
|
- <el-button type="danger" size="small" @click="mediaDrawerVisible = false">
|
|
|
- <Icon icon="mdi:close" width="16" height="16" />
|
|
|
- {{ t('关闭') }}
|
|
|
- </el-button>
|
|
|
- </div>
|
|
|
- <div class="player-container">
|
|
|
- <div v-if="!playbackInfo.videoId" class="player-placeholder">
|
|
|
- <el-icon :size="80" color="#666">
|
|
|
- <VideoPlay />
|
|
|
- </el-icon>
|
|
|
- <p>{{ t('暂无视频流') }}</p>
|
|
|
- </div>
|
|
|
- <VideoPlayer
|
|
|
- v-else
|
|
|
- ref="playerRef"
|
|
|
- player-type="cloudflare"
|
|
|
- :video-id="playbackInfo.videoId"
|
|
|
- :customer-domain="playbackInfo.customerDomain"
|
|
|
- :use-iframe="true"
|
|
|
- :autoplay="playConfig.autoplay"
|
|
|
- :muted="playConfig.muted"
|
|
|
- :controls="true"
|
|
|
- />
|
|
|
- </div>
|
|
|
- <!-- 底部播放控制 -->
|
|
|
- <div class="player-controls">
|
|
|
- <el-button type="primary" size="small" @click="handlePlay">{{ t('播放') }}</el-button>
|
|
|
- <el-button size="small" @click="handlePause">{{ t('暂停') }}</el-button>
|
|
|
- <el-button type="danger" size="small" @click="handlePlayerStop">{{ t('停止') }}</el-button>
|
|
|
- <el-button size="small" @click="handleScreenshot">{{ t('截图') }}</el-button>
|
|
|
- <el-button size="small" @click="handleFullscreen">{{ t('全屏') }}</el-button>
|
|
|
- <el-switch
|
|
|
- v-model="playConfig.muted"
|
|
|
- :active-text="t('静音')"
|
|
|
- :inactive-text="t('有声')"
|
|
|
- style="margin-left: 16px"
|
|
|
- />
|
|
|
- </div>
|
|
|
- </div>
|
|
|
-
|
|
|
- <!-- 右侧:PTZ 控制面板 -->
|
|
|
- <div class="control-panel">
|
|
|
- <!-- PTZ 方向控制 -->
|
|
|
- <div class="panel-section">
|
|
|
- <div class="section-title">{{ t('PTZ') }}</div>
|
|
|
- <div class="ptz-grid">
|
|
|
- <div
|
|
|
- class="ptz-btn"
|
|
|
- @mousedown="handlePTZ('UP_LEFT')"
|
|
|
- @mouseup="handlePTZStop"
|
|
|
- @mouseleave="handlePTZStop"
|
|
|
- >
|
|
|
- <el-icon>
|
|
|
- <TopLeft />
|
|
|
- </el-icon>
|
|
|
- </div>
|
|
|
- <div class="ptz-btn" @mousedown="handlePTZ('UP')" @mouseup="handlePTZStop" @mouseleave="handlePTZStop">
|
|
|
- <el-icon>
|
|
|
- <Top />
|
|
|
- </el-icon>
|
|
|
- </div>
|
|
|
- <div
|
|
|
- class="ptz-btn"
|
|
|
- @mousedown="handlePTZ('UP_RIGHT')"
|
|
|
- @mouseup="handlePTZStop"
|
|
|
- @mouseleave="handlePTZStop"
|
|
|
- >
|
|
|
- <el-icon>
|
|
|
- <TopRight />
|
|
|
- </el-icon>
|
|
|
- </div>
|
|
|
- <div class="ptz-btn" @mousedown="handlePTZ('LEFT')" @mouseup="handlePTZStop" @mouseleave="handlePTZStop">
|
|
|
- <el-icon>
|
|
|
- <Back />
|
|
|
- </el-icon>
|
|
|
- </div>
|
|
|
- <div class="ptz-btn ptz-center" @click="handlePTZStop">
|
|
|
- <el-icon>
|
|
|
- <Refresh />
|
|
|
- </el-icon>
|
|
|
- </div>
|
|
|
- <div class="ptz-btn" @mousedown="handlePTZ('RIGHT')" @mouseup="handlePTZStop" @mouseleave="handlePTZStop">
|
|
|
- <el-icon>
|
|
|
- <Right />
|
|
|
- </el-icon>
|
|
|
- </div>
|
|
|
- <div
|
|
|
- class="ptz-btn"
|
|
|
- @mousedown="handlePTZ('DOWN_LEFT')"
|
|
|
- @mouseup="handlePTZStop"
|
|
|
- @mouseleave="handlePTZStop"
|
|
|
- >
|
|
|
- <el-icon>
|
|
|
- <BottomLeft />
|
|
|
- </el-icon>
|
|
|
- </div>
|
|
|
- <div class="ptz-btn" @mousedown="handlePTZ('DOWN')" @mouseup="handlePTZStop" @mouseleave="handlePTZStop">
|
|
|
- <el-icon>
|
|
|
- <Bottom />
|
|
|
- </el-icon>
|
|
|
- </div>
|
|
|
- <div
|
|
|
- class="ptz-btn"
|
|
|
- @mousedown="handlePTZ('DOWN_RIGHT')"
|
|
|
- @mouseup="handlePTZStop"
|
|
|
- @mouseleave="handlePTZStop"
|
|
|
- >
|
|
|
- <el-icon>
|
|
|
- <BottomRight />
|
|
|
- </el-icon>
|
|
|
- </div>
|
|
|
- </div>
|
|
|
-
|
|
|
- <!-- 缩放按钮 -->
|
|
|
- <div class="zoom-buttons">
|
|
|
- <el-button size="small" @mousedown="handleZoomIn" @mouseup="handlePTZStop" @mouseleave="handlePTZStop">
|
|
|
- <el-icon>
|
|
|
- <ZoomIn />
|
|
|
- </el-icon>
|
|
|
- </el-button>
|
|
|
- <el-button size="small" @mousedown="handleZoomOut" @mouseup="handlePTZStop" @mouseleave="handlePTZStop">
|
|
|
- <el-icon>
|
|
|
- <ZoomOut />
|
|
|
- </el-icon>
|
|
|
- </el-button>
|
|
|
- </div>
|
|
|
-
|
|
|
- <!-- 速度滑块 -->
|
|
|
- <div class="speed-slider">
|
|
|
- <span class="label">{{ t('速度') }}</span>
|
|
|
- <el-slider v-model="ptzSpeed" :min="10" :max="100" :step="10" size="small" />
|
|
|
- <span class="value">{{ ptzSpeed }}</span>
|
|
|
- </div>
|
|
|
- </div>
|
|
|
-
|
|
|
- <!-- 预置位列表 -->
|
|
|
- <div class="panel-section preset-section">
|
|
|
- <div class="section-title">
|
|
|
- <span>{{ t('预置位') }}</span>
|
|
|
- <el-button type="primary" link size="small" @click="loadPresets" :loading="presetsLoading">
|
|
|
- <el-icon>
|
|
|
- <Refresh />
|
|
|
- </el-icon>
|
|
|
- </el-button>
|
|
|
- </div>
|
|
|
- <div class="preset-list" v-loading="presetsLoading">
|
|
|
- <div
|
|
|
- v-for="preset in presetList"
|
|
|
- :key="preset.token"
|
|
|
- :class="['preset-item', { active: activePresetToken === preset.token }]"
|
|
|
- @click="handleGotoPreset(preset)"
|
|
|
- >
|
|
|
- <span class="preset-index">{{ preset.token }}</span>
|
|
|
- <span class="preset-name">{{ preset.name || `Preset ${preset.token}` }}</span>
|
|
|
- </div>
|
|
|
- <el-empty
|
|
|
- v-if="!presetsLoading && presetList.length === 0"
|
|
|
- :description="t('暂无预置位')"
|
|
|
- :image-size="60"
|
|
|
- />
|
|
|
- </div>
|
|
|
- </div>
|
|
|
- </div>
|
|
|
- </div>
|
|
|
- </el-drawer>
|
|
|
</div>
|
|
|
</template>
|
|
|
|
|
|
@@ -447,9 +475,10 @@ const currentCommandTemplate = ref('')
|
|
|
const currentStreamId = ref<number | null>(null)
|
|
|
const commandUpdateLoading = ref(false)
|
|
|
|
|
|
-// 流媒体播放抽屉
|
|
|
-const mediaDrawerVisible = ref(false)
|
|
|
+// 合并抽屉的 tab 状态
|
|
|
+const activeDrawerTab = ref<'edit' | 'play'>('edit')
|
|
|
const currentMediaStream = ref<LiveStreamDTO | null>(null)
|
|
|
+const streamStarting = ref(false)
|
|
|
const playerRef = ref<InstanceType<typeof VideoPlayer>>()
|
|
|
const playbackInfo = ref<{
|
|
|
videoId: string
|
|
|
@@ -641,6 +670,8 @@ function handleAdd() {
|
|
|
remark: '',
|
|
|
enabled: true
|
|
|
})
|
|
|
+ currentMediaStream.value = null
|
|
|
+ activeDrawerTab.value = 'edit'
|
|
|
drawerVisible.value = true
|
|
|
}
|
|
|
|
|
|
@@ -657,6 +688,8 @@ function handleEdit(row: LiveStreamDTO) {
|
|
|
remark: row.remark || '',
|
|
|
enabled: row.enabled
|
|
|
})
|
|
|
+ currentMediaStream.value = row
|
|
|
+ activeDrawerTab.value = 'edit'
|
|
|
drawerVisible.value = true
|
|
|
}
|
|
|
|
|
|
@@ -813,7 +846,8 @@ async function handleStartStream(row: LiveStreamDTO) {
|
|
|
const res = await startStreamTask({
|
|
|
name: row.name,
|
|
|
lssId: row.lssId,
|
|
|
- cameraId: row.cameraId
|
|
|
+ cameraId: row.cameraId,
|
|
|
+ commandTemplate: row.commandTemplate
|
|
|
})
|
|
|
if (res.success) {
|
|
|
ElMessage.success(t('推流任务已启动'))
|
|
|
@@ -829,6 +863,55 @@ async function handleStartStream(row: LiveStreamDTO) {
|
|
|
}
|
|
|
}
|
|
|
|
|
|
+// 从播放器窗口启动推流
|
|
|
+async function handleStartStreamFromPlayer() {
|
|
|
+ if (!currentMediaStream.value) return
|
|
|
+
|
|
|
+ if (!currentMediaStream.value.cameraId) {
|
|
|
+ ElMessage.warning(t('请先配置摄像头'))
|
|
|
+ return
|
|
|
+ }
|
|
|
+
|
|
|
+ streamStarting.value = true
|
|
|
+ try {
|
|
|
+ const res = await startStreamTask({
|
|
|
+ name: currentMediaStream.value.name,
|
|
|
+ lssId: currentMediaStream.value.lssId,
|
|
|
+ cameraId: currentMediaStream.value.cameraId,
|
|
|
+ commandTemplate: currentMediaStream.value.commandTemplate
|
|
|
+ })
|
|
|
+ if (res.success) {
|
|
|
+ ElMessage.success(t('推流任务已启动'))
|
|
|
+ // 更新当前流的状态
|
|
|
+ currentMediaStream.value.status = '1'
|
|
|
+ // 刷新播放信息
|
|
|
+ if (currentMediaStream.value.streamSn) {
|
|
|
+ try {
|
|
|
+ const playbackRes = await getStreamPlayback(currentMediaStream.value.streamSn)
|
|
|
+ if (playbackRes.success && playbackRes.data) {
|
|
|
+ playbackInfo.value = {
|
|
|
+ ...playbackInfo.value,
|
|
|
+ hlsUrl: playbackRes.data.hlsUrl,
|
|
|
+ whepUrl: playbackRes.data.whepUrl,
|
|
|
+ isLive: playbackRes.data.isLive
|
|
|
+ }
|
|
|
+ }
|
|
|
+ } catch (e) {
|
|
|
+ console.error('刷新播放信息失败', e)
|
|
|
+ }
|
|
|
+ }
|
|
|
+ getList()
|
|
|
+ } else {
|
|
|
+ ElMessage.error(res.errMessage || t('启动失败'))
|
|
|
+ }
|
|
|
+ } catch (error) {
|
|
|
+ console.error('启动推流失败', error)
|
|
|
+ ElMessage.error(t('启动推流失败'))
|
|
|
+ } finally {
|
|
|
+ streamStarting.value = false
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
// 停止推流
|
|
|
async function handleStopStream(row: LiveStreamDTO) {
|
|
|
try {
|
|
|
@@ -876,6 +959,20 @@ function parsePlaybackUrl(playbackUrl: string): { videoId: string; customerDomai
|
|
|
async function handleViewCloudflare(row: LiveStreamDTO) {
|
|
|
currentMediaStream.value = row
|
|
|
|
|
|
+ // 同时填充表单数据,以便用户可以切换到编辑 tab
|
|
|
+ Object.assign(form, {
|
|
|
+ id: row.id,
|
|
|
+ name: row.name,
|
|
|
+ lssId: row.lssId || '',
|
|
|
+ cameraId: row.cameraId || '',
|
|
|
+ channelId: row.channelId,
|
|
|
+ pushMethod: row.pushMethod || 'ffmpeg',
|
|
|
+ commandTemplate: row.commandTemplate || '',
|
|
|
+ timeoutSeconds: row.timeoutSeconds || 30,
|
|
|
+ remark: row.remark || '',
|
|
|
+ enabled: row.enabled
|
|
|
+ })
|
|
|
+
|
|
|
// 默认值
|
|
|
let videoId = ''
|
|
|
let customerDomain = 'customer-pj89kn2ke2tcuh19.cloudflarestream.com'
|
|
|
@@ -924,7 +1021,8 @@ async function handleViewCloudflare(row: LiveStreamDTO) {
|
|
|
}
|
|
|
}
|
|
|
|
|
|
- mediaDrawerVisible.value = true
|
|
|
+ activeDrawerTab.value = 'play'
|
|
|
+ drawerVisible.value = true
|
|
|
}
|
|
|
|
|
|
// 播放控制
|
|
|
@@ -1181,8 +1279,8 @@ onMounted(async () => {
|
|
|
}
|
|
|
}
|
|
|
|
|
|
-// 抽屉样式
|
|
|
-.stream-drawer {
|
|
|
+// 合并抽屉样式
|
|
|
+.combined-drawer {
|
|
|
:deep(.el-drawer__body) {
|
|
|
padding: 0;
|
|
|
display: flex;
|
|
|
@@ -1197,6 +1295,46 @@ onMounted(async () => {
|
|
|
height: 100%;
|
|
|
}
|
|
|
|
|
|
+.drawer-tabs {
|
|
|
+ flex-shrink: 0;
|
|
|
+ padding: 0 20px;
|
|
|
+ border-bottom: 1px solid #e5e7eb;
|
|
|
+
|
|
|
+ :deep(.el-tabs__header) {
|
|
|
+ margin: 0;
|
|
|
+ }
|
|
|
+
|
|
|
+ :deep(.el-tabs__nav-wrap::after) {
|
|
|
+ display: none;
|
|
|
+ }
|
|
|
+
|
|
|
+ :deep(.el-tabs__item) {
|
|
|
+ font-size: 15px;
|
|
|
+ padding: 0 20px;
|
|
|
+ height: 48px;
|
|
|
+ line-height: 48px;
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+.tab-content {
|
|
|
+ flex: 1;
|
|
|
+ min-height: 0;
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+ overflow: hidden;
|
|
|
+}
|
|
|
+
|
|
|
+.edit-content {
|
|
|
+ .drawer-body {
|
|
|
+ flex: 1;
|
|
|
+ overflow-y: auto;
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+.play-content {
|
|
|
+ background-color: #f5f7fa;
|
|
|
+}
|
|
|
+
|
|
|
.drawer-header {
|
|
|
flex-shrink: 0;
|
|
|
padding: 16px 20px;
|
|
|
@@ -1312,6 +1450,7 @@ onMounted(async () => {
|
|
|
display: flex;
|
|
|
align-items: center;
|
|
|
justify-content: center;
|
|
|
+ position: relative;
|
|
|
|
|
|
.player-placeholder {
|
|
|
display: flex;
|
|
|
@@ -1325,6 +1464,20 @@ onMounted(async () => {
|
|
|
font-size: 14px;
|
|
|
}
|
|
|
}
|
|
|
+
|
|
|
+ .stream-control-overlay {
|
|
|
+ position: absolute;
|
|
|
+ top: 50%;
|
|
|
+ left: 50%;
|
|
|
+ transform: translate(-50%, -50%);
|
|
|
+ z-index: 10;
|
|
|
+
|
|
|
+ .el-button {
|
|
|
+ font-size: 18px;
|
|
|
+ padding: 16px 32px;
|
|
|
+ border-radius: 0;
|
|
|
+ }
|
|
|
+ }
|
|
|
}
|
|
|
|
|
|
.player-controls {
|