@@ -0,0 +1,19 @@
< template >
< 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 = "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 }
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 ) }
async function retry ( ) { if ( ! selectedRoute . value ) { ElMessage . info ( '请先选择视频' ) ; return } await open ( ) }
onMounted ( load ) ; onBeforeUnmount ( ( ) => clearInterval ( timer . value ) )
< / script >
< style scoped > . player - header { display : flex ; align - items : center ; justify - content : space - between } . option - detail { float : right ; color : # 86909 c ; font - size : 12 px } < / style >