feat: 自动建立实时监看媒体路径 (#51)

This commit is contained in:
QiuSW
2026-08-13 12:39:12 +08:00
parent 73d775b199
commit b9f92dd46e
12 changed files with 181 additions and 18 deletions
@@ -19,6 +19,7 @@
<el-empty v-if="!result.status" description="选择设备并开始检查" />
<template v-else>
<el-result :icon="result.status === 'ready' ? 'success' : 'warning'" :title="statusLabel(result.status)" :sub-title="result.detail" />
<el-alert v-if="result.media_status" :title="result.media_detail" :type="result.media_status === 'ready' ? 'success' : 'warning'" show-icon :closable="false" class="media-result" />
<el-table v-if="result.profiles?.length" :data="result.profiles" border>
<el-table-column label="用途" width="90"><template #default="scope"><el-tag>{{ scope.row.kind === 'main' ? '主码流' : scope.row.kind === 'sub' ? '子码流' : '其他' }}</el-tag></template></el-table-column>
<el-table-column prop="name" label="Profile" min-width="120" />
@@ -50,4 +51,4 @@ async function discover(){discovering.value=true;try{discovered.value=(await dis
function useAddress(value){form.address=value;discoveryDialog.value=false}
onMounted(loadDevices)
</script>
<style scoped>.admission-form{margin-top:18px}.field-hint{color:#86909c;font-size:12px;line-height:1.5}.result-header{display:flex;align-items:center;justify-content:space-between}</style>
<style scoped>.admission-form{margin-top:18px}.field-hint{color:#86909c;font-size:12px;line-height:1.5}.result-header{display:flex;align-items:center;justify-content:space-between}.media-result{margin-bottom:16px}</style>
@@ -2,14 +2,14 @@
<section class="page-container">
<div class="page-heading"><div><h1>实时监看</h1><p>一次打开一路视频,可在已验证的主、子码流之间切换。</p></div><el-tag v-if="session.expires_at" type="info">会话短期有效</el-tag></div>
<el-row :gutter="16">
<el-col :span="7"><el-card shadow="never"><template #header><strong>选择视频</strong></template><el-form label-position="top"><el-form-item label="设备与 Profile"><el-select v-model="selectedRoute" filterable placeholder="选择一路视频" style="width:100%" @change="open"><el-option v-for="item in routes" :key="item.id" :label="`${item.device_id} · ${profileLabel(item.profile_token)}`" :value="item.id"><span>{{ item.device_id }}</span><span class="option-detail">{{ profileLabel(item.profile_token) }} · {{ stateLabel(item.actual) }}</span></el-option></el-select></el-form-item></el-form><el-descriptions v-if="current" :column="1" border><el-descriptions-item label="设备">{{ current.device_id }}</el-descriptions-item><el-descriptions-item label="Profile">{{ profileLabel(current.profile_token) }}</el-descriptions-item><el-descriptions-item label="媒体状态"><el-tag :type="current.actual === 'ready' ? 'success' : 'warning'">{{ stateLabel(current.actual) }}</el-tag></el-descriptions-item><el-descriptions-item label="观看连接">{{ current.readers }}</el-descriptions-item></el-descriptions><el-alert v-else title="先在视频服务中建立并对账媒体路径" type="info" show-icon :closable="false" /></el-card></el-col>
<el-col :span="7"><el-card shadow="never"><template #header><strong>选择视频</strong></template><el-form label-position="top"><el-form-item label="设备与码流"><el-select v-model="selectedRoute" filterable placeholder="选择一路视频" style="width:100%" @change="open"><el-option v-for="item in routes" :key="item.id" :label="`${item.device_name} · ${profileLabel(item)}`" :value="item.id"><span>{{ item.device_name }}</span><span class="option-detail">{{ profileLabel(item) }} · {{ stateLabel(item.actual) }}</span></el-option></el-select></el-form-item></el-form><el-descriptions v-if="current" :column="1" border><el-descriptions-item label="设备">{{ current.device_name }}</el-descriptions-item><el-descriptions-item label="位置">{{ current.device_location || '未填写' }}</el-descriptions-item><el-descriptions-item label="码流">{{ profileLabel(current) }}</el-descriptions-item><el-descriptions-item label="媒体状态"><el-tag :type="current.actual === 'ready' ? 'success' : 'warning'">{{ stateLabel(current.actual) }}</el-tag></el-descriptions-item><el-descriptions-item label="观看连接">{{ current.readers }}</el-descriptions-item></el-descriptions><el-alert v-else title="请先在视频接入完成检查;系统会自动建立媒体路径。若仍无视频,请到视频服务排错。" type="info" show-icon :closable="false" /></el-card></el-col>
<el-col :span="17"><el-card shadow="never"><template #header><div class="player-header"><strong>单路画面</strong><el-button v-if="selectedRoute" :icon="Refresh" @click="retry">重新连接</el-button></div></template><StreamPlayer :state="playerState" :detail="playerDetail" :player-url="session.player_url" @retry="retry" /></el-card></el-col>
</el-row>
</section>
</template>
<script setup>
import{computed,onBeforeUnmount,onMounted,reactive,ref}from'vue';import{ElMessage}from'element-plus';import{Refresh}from'@element-plus/icons-vue';import{createSession,getSession,playbackRoutes}from'../../../api/sense/liveview';import StreamPlayer from'../../../components/sense/liveview/StreamPlayer.vue'
const routes=ref([]),selectedRoute=ref(''),session=reactive({}),playerState=ref('waiting'),playerDetail=ref('请选择一路视频'),timer=ref();const current=computed(()=>routes.value.find(item=>item.id===selectedRoute.value));function profileLabel(value){return value?.toLowerCase().includes('sub')?'子码流':value?.toLowerCase().includes('main')?'主码流':value}function stateLabel(value){return({ready:'正常',waiting:'等待视频',stopped:'已停止',process_failed:'服务未启动',apply_failed:'配置失败',unconverged:'状态未同步'})[value]||value}
const routes=ref([]),selectedRoute=ref(''),session=reactive({}),playerState=ref('waiting'),playerDetail=ref('请选择一路视频'),timer=ref();const current=computed(()=>routes.value.find(item=>item.id===selectedRoute.value));function profileLabel(item){const kind=({main:'主码流',sub:'子码流',other:'其他码流'})[item?.profile_kind];return kind?`${kind}${item.profile_name?`(${item.profile_name})`:''}`:(item?.profile_name||item?.profile_token)}function stateLabel(value){return({ready:'正常',waiting:'等待视频',stopped:'已停止',process_failed:'服务未启动',apply_failed:'配置失败',unconverged:'状态未同步'})[value]||value}
async function load(){routes.value=(await playbackRoutes()).items||[]}
async function open(){if(!selectedRoute.value)return;playerState.value='loading';try{Object.assign(session,await createSession(selectedRoute.value));playerState.value=session.status==='ready'?'ready':session.status;playerDetail.value=session.detail;startPolling()}catch(error){playerState.value='offline';playerDetail.value=error.message||'无法创建播放会话'}}
function startPolling(){clearInterval(timer.value);timer.value=setInterval(async()=>{if(!session.id)return;try{const latest=await getSession(session.id);Object.assign(session,latest);playerState.value=latest.status==='ready'?'ready':latest.status;playerDetail.value=latest.detail}catch{playerState.value='expired';playerDetail.value='播放会话已过期,请重新连接';clearInterval(timer.value)}},5000)}