Vue 3 + TypeScript 开发的视频直播页面组件
主要用于机场或无人机场景的 RTMP/RTSP/GB28181 协议直播
一、组件核心功能概览
该组件的核心是实现 “直播源选择 - 直播参数配置 - 直播播放 / 控制” 的完整流程,支持三种主流直播协议,具体功能如下:
功能模块 | 具体能力 |
---|---|
直播源管理 | 加载机场 / 无人机设备列表、摄像头列表、镜头类型列表 |
协议与质量配置 | 支持 RTMP(Web 播放)、RTSP(VLC 播放)、GB28181(安防协议),可选 5 档画质 |
播放控制 | 直播启动、暂停、镜头切换、画质更新、播放状态监听 |
适配能力 | 响应式布局(PC / 移动端视频尺寸适配)、多协议播放器兼容 |
二、核心变量定义
变量名 | 类型 | 用途 |
---|---|---|
liveTypeList | SelectOption[] | 直播协议选项(RTMP=1、RTSP=2、GB28181=3) |
clarityList | SelectOption[] | 画质选项(0 = 自适应、1 = 流畅、2 = 标清、3 = 高清、4 = 超清) |
videowebrtc | Ref<HTMLDivElement> | 视频播放容器的 DOM 引用 |
jessibuca | any | Jessibuca 播放器实例(核心播放对象) |
liveState | Ref<boolean> | 直播状态(true = 正在播放、false = 已暂停) |
droneSelected /cameraSelected | Ref<any> | 已选择的无人机 / 摄像头 ID(用于拼接直播参数) |
osdVisible | ComputedRef | 从 Store 中获取 OSD(屏幕显示信息)状态 |
三、代码
<template><div class="flex-column flex-justify-start flex-align-center" style="position:relative;height:100%"><div id="video-webrtc" ref="videowebrtc"></div><div class="mt10 flex-row flex-justify-center flex-align-center"></div></div></template><script lang="ts" setup>
import { message } from 'ant-design-vue'
import { onMounted, reactive, ref, computed } from 'vue'
import { CURRENT_CONFIG as config } from '/@/api/http/config'
import {changeLivestreamLens,getLiveCapacity,setLivestreamQuality,startLivestream,stopLivestream,oneStartLivestream
} from '/@/api/manage'
import { getRoot } from '/@/root'
import jswebrtc from '/@/vendors/jswebrtc.min.js'
import Jessibuca from '../../public/js/jessibuca'
import { useMyStore } from '/@/store'
import { ELocalStorageKey, EBizCode } from '/@/types'
const store = useMyStore()
const osdVisible = computed(() => {return store.state.osdVisible
})
const root = getRoot()interface SelectOption {value: any,label: string,more?: any}const liveTypeList: SelectOption[] = [{value: 1,label: 'RTMP'},{value: 2,label: 'RTSP'},{value: 3,label: 'GB28181'}
]
const clarityList: SelectOption[] = [{value: 0,label: '自适应'},{value: 1,label: '流畅'},{value: 2,label: '标清'},{value: 3,label: '高清'},{value: 4,label: '超清'}
]
const workspaceId = localStorage.getItem(ELocalStorageKey.WorkspaceId)!
const deviceSn = localStorage.getItem(EBizCode.DeviceSn)!
const dock_sn = localStorage.getItem(EBizCode.DockSn)!
const videowebrtc = ref(null)
const livestreamSource = ref()
const droneList = ref()
const cameraList = ref()
const videoList = ref()
const droneSelected = ref()
const cameraSelected = ref()
const videoSelected = ref()
const claritySelected = ref()
const videoId = ref()
const liveState = ref<boolean>(false)
const livetypeSelected = ref()
const rtspData = ref()
const lensList = ref<string[]>([])
const lensSelected = ref<String>()
const isDockLive = ref(false)
const nonSwitchable = 'normal'
const cameraId = ref<string[]>([])
const wrjbianId = ref<string[]>([])
const onRefresh = async () => {// console.log('onRefresh进入')droneList.value = []cameraList.value = []videoList.value = []droneSelected.value = nullcameraSelected.value = nullvideoSelected.value = nullawait getLiveCapacity({}).then(res => {// console.log(res)if (res.code === 0) {if (res.data === null) {console.warn('warning: get live capacity is null!!!')return}const resData: Array<[]> = res.dataconsole.log('live_capacity:', resData)livestreamSource.value = resDatacameraId.value = resData[0].cameras_list[0].index // 相机id// wrjbianId.value = resData[1].cameras_list[1].index// console.log('相机编号', cameraId.value)const temp: Array<SelectOption> = []if (livestreamSource.value) {livestreamSource.value.forEach((ele: any) => {temp.push({ label: ele.name + '-' + ele.sn, value: ele.sn, more: ele.cameras_list })})droneList.value = temp}}onStart()}).catch(error => {message.error('数据获取异常', error)console.error(error)})
}
const type = ref(null)onMounted(() => {type.value = sessionStorage.getItem('type')// console.log('deviceSn', deviceSn)// onRefresh()// setInterval(onOpen, 5000)onOpen()
})
function onOpen () {const params = {workspace_id: workspaceId,sn: deviceSn}oneStartLivestream(params).then(res => {play(res.data.url)console.log('直播指令', res)}).catch(error => {message.error('数据获取异常', error)console.error(error)})
}
let jessibuca: any = null
// const playUrl = ref('http://flv.bdplay.nodemedia.cn/live/bbb.flv')
const playing = ref(false)
const quieting = ref(true)
const loaded = ref(false)const createJessibuca = () => {jessibuca = new (window as any).Jessibuca({container: videowebrtc.value,videoBuffer: 0.2, // 缓存时长isResize: false,text: '',// background: "bg.jpg",loadingText: '加载中',// hasAudio:false,debug: true,showBandwidth: false, // showBandwidth.value, // 显示网速operateBtns: {fullscreen: false, // showOperateBtns.value,screenshot: false, // showOperateBtns.value,play: false, // showOperateBtns.value,audio: false, // showOperateBtns.value,},forceNoOffscreen: false, // forceNoOffscreen.value,isNotMute: false,decoder: '/js/decoder.js'}) as Jessibucajessibuca.on('load', function () {// console.log('on load')})jessibuca.on('log', function (msg: any) {// console.log('on log', msg)})jessibuca.on('record', function (msg: any) {// console.log('on record:', msg)})jessibuca.on('pause', function () {// console.log('on pause')playing.value = false})jessibuca.on('play', function () {// console.log('on play')playing.value = trueloaded.value = truequieting.value = jessibuca.isMute()})jessibuca.on('fullscreen', function (msg: any) {// console.log('on fullscreen', msg)})jessibuca.on('mute', function (msg: any) {// console.log('on mute', msg)quieting.value = msg})jessibuca.on('mute', function (msg: any) {// console.log('on mute2', msg)})jessibuca.on('audioInfo', function (msg: any) {// console.log('audioInfo', msg)})// jessibuca.on("bps", function (bps) {// // console.log('bps', bps);// });// let _ts = 0;// jessibuca.on("timeUpdate", function (ts) {// // console.log('timeUpdate,old,new,timestamp', _ts, ts, ts - _ts);// // _ts = ts;// });jessibuca.on('videoInfo', function (info: any) {// console.log('videoInfo', info)})jessibuca.on('error', function (error: any) {console.log('error', error)})jessibuca.on('timeout', function () {// console.log('timeout')})jessibuca.on('start', function () {// console.log('start')})// jessibuca.on("stats", function (stats) {// console.log('stats', JSON.stringify(stats));// });jessibuca.on('performance', function (performance: any) {let show = '卡顿'if (performance === 2) {show = '非常流畅'} else if (performance === 1) {show = '流畅'}})jessibuca.on('buffer', function (buffer: any) {// console.log('buffer', buffer)})jessibuca.on('stats', function (stats: any) {// console.log('stats', stats)})jessibuca.on('kBps', function (kBps: any) {// console.log('kBps', kBps)})// 显示时间戳 PTSjessibuca.on('videoFrame', function () {})//jessibuca.on('metadata', function () {})
}// const play = () => {
// if (playUrl.value) {
// jessibuca.play(playUrl.value)
// }
// }
// const pause = () => {
// jessibuca.pause()
// playing.value = false
// }const play = (url: string) => {if (jessibuca) {jessibuca.destroy()}createJessibuca()playing.value = falseloaded.value = falseif (url) {jessibuca.play(url)}
}const onStart = async () => {// console.log(131231, osdVisible.value)// livetypeSelected.value = 1// isDockLive.value = true// console.log(// 'Param:',// livetypeSelected.value,// droneSelected.value,// cameraSelected.value,// videoSelected.value,// claritySelected.value// )// 1 7CTDM1600BS774 165-0-7 normal-0 0livetypeSelected.value = 1 // 强制设置为rtmp类型droneSelected.value = osdVisible.value.gateway_sn// cameraSelected.value = '165-0-7'cameraSelected.value = cameraId.value// console.log('相机编号2', cameraSelected.value)videoSelected.value = 'normal-0'claritySelected.value = 0const timestamp = new Date().getTime().toString()if (livetypeSelected.value == null ||droneSelected.value == null ||cameraSelected.value == null ||claritySelected.value == null) {message.warn('waring: not select live para!!!')return}videoId.value =droneSelected.value + '/' + cameraSelected.value + '/' + (videoSelected.value || nonSwitchable + '-0')let liveURL = ''switch (livetypeSelected.value) {case 1: {// RTMP// liveURL = config.rtmpURL + timestampliveURL = config.rtmpURL + droneSelected.valuebreak}case 2: {// RTSP 浏览器和 Jessibuca 都不支持直接播放 RTSP 流,需要一个中间件来将 RTSP 流转成 HTTP-FLV 或 HLS 等 Web 可播放的格式。// liveURL = config.rtspURL + droneSelected.valueliveURL = config.rtspURL// liveURL = `userName=${config.rtspUserName}&password=${config.rtspPassword}&port=${config.rtspPort}`break}case 3: {liveURL = `serverIP=${config.gbServerIp}&serverPort=${config.gbServerPort}&serverID=${config.gbServerId}&agentID=${config.gbAgentId}&agentPassword=${config.gbPassword}&localPort=${config.gbAgentPort}&channel=${config.gbAgentChannel}`break}default:console.warn('warning: live type is not correct!!!')break}// console.log('是否进入')console.log(osdVisible.value.gateway_sn)console.log(osdVisible.value.sn)// console.log('编号', cameraSelected.value)const jichang = osdVisible.value.gateway_snconst wurenji = osdVisible.value.snconst bianId = cameraSelected.valueawait startLivestream({url: type.value === '1' ? `${config.rtmpURL}${jichang}` : `${config.rtmpURL}${wurenji}`,video_id: type.value === '1' ? jichang + '/' + bianId + '/normal-0' : wurenji + '/98-0-0/normal-0',url_type: 1, // 默认rtmp视频流,可更改选择video_quality: 0}).then(res => {if (res.code !== 0) {return}if (livetypeSelected.value === 3) {const url = res.data.urlconst videoElement = videowebrtc.value// gb28181,it will fail if not wait.message.loading({content: 'Loding...',duration: 4,onClose () {const player = new jswebrtc.Player(url, {video: videoElement,autoplay: true,onPlay: (obj: any) => {console.log('start play livestream')}})}})// } else if (livetypeSelected.value === 2) {// console.log(res)// rtspData.value =// 'url:' +// res.data.url +// '&username:' +// res.data.username +// '&password:' +// res.data.password} else if (livetypeSelected.value === 1 || livetypeSelected.value === 2) {play(res.data.url)// const url = res.data.url// const videoElement = videowebrtc.value// console.log('start live:', url)// console.log(videoElement)// const player = new jswebrtc.Player(url, {// video: videoElement,// autoplay: true,// onPlay: (obj: any) => {// console.log('start play livestream')// }// })}liveState.value = true}).catch(err => {console.error(err)})
}
const onStop = () => {videoId.value = droneSelected.value + '/' + cameraSelected.value + '/' + (videoSelected.value || nonSwitchable + '-0')stopLivestream({video_id: videoId.value}).then(res => {if (res.code === 0) {message.success(res.message)liveState.value = falselensSelected.value = undefinedconsole.log('stop play livestream')}})
}const onUpdateQuality = () => {if (!liveState.value) {message.info('Please turn on the livestream first.')return}setLivestreamQuality({video_id: videoId.value,video_quality: claritySelected.value}).then(res => {if (res.code === 0) {message.success('Set the clarity to ' + clarityList[claritySelected.value].label)}})
}const onLiveTypeSelect = (val: any) => {livetypeSelected.value = val
}
const onDroneSelect = (val: SelectOption) => {droneSelected.value = val.valueconst temp: Array<SelectOption> = []cameraList.value = []cameraSelected.value = undefinedvideoSelected.value = undefinedvideoList.value = []lensList.value = []if (!val.more) {return}val.more.forEach((ele: any) => {temp.push({ label: ele.name, value: ele.index, more: ele.videos_list })})cameraList.value = temp
}
const onCameraSelect = (val: SelectOption) => {cameraSelected.value = val.valueconst result: Array<SelectOption> = []videoSelected.value = undefinedvideoList.value = []lensList.value = []if (!val.more) {return}val.more.forEach((ele: any) => {result.push({ label: ele.type, value: ele.index, more: ele.switch_video_types })})videoList.value = resultif (videoList.value.length === 0) {return}const firstVideo: SelectOption = videoList.value[0]videoSelected.value = firstVideo.valuelensList.value = firstVideo.morelensSelected.value = firstVideo.labelisDockLive.value = lensList.value?.length > 0
}
const onVideoSelect = (val: SelectOption) => {videoSelected.value = val.valuelensList.value = val.morelensSelected.value = val.label
}
const onClaritySelect = (val: any) => {claritySelected.value = val
}
const onSwitch = () => {if (lensSelected.value === undefined || lensSelected.value === nonSwitchable) {message.info('The ' + nonSwitchable + ' lens cannot be switched, please select the lens to be switched.', 8)return}changeLivestreamLens({video_id: videoId.value,video_type: lensSelected.value}).then(res => {if (res.code === 0) {message.success('Switching live camera successfully.')}})
}
</script><style lang="scss" scoped>@import '/@/styles/index.scss';#video-webrtc {background: rgba(27, 39, 61, 0.8);width: 100%;height: 100%;}@media (max-width: 720px) {#video-webrtc {width: 90vw;height: 52.7vw;}}</style>
livetypeSelected.value
的值主要来自两个地方:
- 用户交互(主要来源):用户在界面上操作
a-select
下拉框时,Vue 的v-model:value
指令会自动更新它的值。 - 代码逻辑(次要来源):在某些特定函数(如
onStart
)中,代码会强制给它赋一个默认值。