feat: 视频监控
This commit is contained in:
parent
4540dfb3d7
commit
97ca74a225
@ -12,7 +12,7 @@ export const conf = {
|
||||
startTime: 0, // 点播模式下,初始起播时间
|
||||
videoAttributes: {}, // video扩展属性,暂且不配置
|
||||
lang: 'zh-cn', // 播放器初始显示语言,设置为中文
|
||||
fluid: true, // 是否流式布局(宽高优先于流失布局,默认16:9)注掉上方宽高看效果
|
||||
fluid: false, // 是否流式布局(宽高优先于流失布局,默认16:9)注掉上方宽高看效果
|
||||
fitVideoSize: 'fixed', // 保持容器宽/高,不做适配,按照容器来
|
||||
videoFillMode: 'auto', // 宽高不够自动底色填充(fill拉伸填充等...)
|
||||
seekedStatus: 'play', // 跳转后继续播放
|
||||
|
||||
@ -9,10 +9,14 @@ import { ref, onMounted } from 'vue';
|
||||
import Player, { Events } from 'xgplayer'; // 引入西瓜视频模块
|
||||
import 'xgplayer/dist/index.min.css'; // 引入西瓜视频样式
|
||||
|
||||
const { videoUrl } = defineProps({
|
||||
const { videoUrl, isLive } = defineProps({
|
||||
videoUrl: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
isLive: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
})
|
||||
|
||||
@ -29,7 +33,7 @@ const init = () => {
|
||||
el: xgPlayerRef.value,
|
||||
// width: 600,
|
||||
// height: 400, // 视频宽高尺寸
|
||||
isLive: false,
|
||||
isLive: isLive,
|
||||
url: videoUrl, // 视频源
|
||||
// poster: "http://ashuai.work/static/img/avantar.png", // 视频封面
|
||||
...conf,
|
||||
@ -53,8 +57,11 @@ const init = () => {
|
||||
.player_container {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
min-height: 0;
|
||||
overflow: hidden;
|
||||
|
||||
.xgplayer {
|
||||
:deep(.xgplayer) {
|
||||
width: 100% !important;
|
||||
height: 100% !important;
|
||||
}
|
||||
}
|
||||
|
||||
@ -57,17 +57,7 @@
|
||||
<p>当前状态:<strong>{{ statusMeta.label }}</strong></p>
|
||||
<span>{{ statusMeta.description }}</span>
|
||||
</div>
|
||||
|
||||
<el-alert
|
||||
v-if="errorMessage"
|
||||
class="error-alert"
|
||||
:title="errorMessage"
|
||||
type="error"
|
||||
:closable="true"
|
||||
show-icon
|
||||
@close="errorMessage = ''"
|
||||
/>
|
||||
|
||||
|
||||
<div class="feature-tags">
|
||||
<span>高清语音</span>
|
||||
<span>低延迟</span>
|
||||
@ -87,8 +77,11 @@ import { computed, onBeforeUnmount, ref } from 'vue'
|
||||
import { ElMessage, ElNotification } from 'element-plus'
|
||||
|
||||
// 请替换为实际部署的信令服务器地址。
|
||||
const SIGNALING_SERVER_URL = 'wss://your-signaling-server.com'
|
||||
const ROOM_NAME = 'voice-room'
|
||||
const SIGNALING_SERVER_URL = 'ws://192.168.1.222:13080/ws/signaling'
|
||||
// 后端创建语音会话所需的固定终端、音频设备和操作员标识。
|
||||
const TERMINAL_ID = 'c2f8a06826baf03843925c1a2a13bcfd'
|
||||
const DEVICE_ID = '1234'
|
||||
const OPERATOR_ID = 'user001'
|
||||
const RTC_CONFIGURATION = {
|
||||
iceServers: [{ urls: 'stun:stun.l.google.com:19302' }]
|
||||
}
|
||||
@ -107,12 +100,13 @@ const isStarting = ref(false)
|
||||
const isConnected = ref(false)
|
||||
const isMuted = ref(false)
|
||||
const connectionStatus = ref('idle')
|
||||
const errorMessage = ref('')
|
||||
|
||||
// 音量分析和异步通话过程使用的非响应式资源。
|
||||
let audioContext = null
|
||||
let analyser = null
|
||||
let volumeAnimationFrame = null
|
||||
// 后端返回 joined 后生成的业务会话 ID,后续 Offer、Candidate、心跳和停止消息都必须携带。
|
||||
let signalingSessionId = ''
|
||||
// 远端 SDP 设置完成前收到的 ICE 候选会暂存在这里,避免 addIceCandidate 调用失败。
|
||||
let pendingIceCandidates = []
|
||||
// 每次开始或清理通话都会更新会话编号,用于让旧会话的异步回调自动失效。
|
||||
@ -129,18 +123,6 @@ const statusMeta = computed(() => {
|
||||
return statusMap[connectionStatus.value] || statusMap.idle
|
||||
})
|
||||
|
||||
/**
|
||||
* 生成当前房间的 WebSocket 信令地址。
|
||||
* 将固定房间名放入 URL 查询参数,使信令服务器可以把消息转发给同房间的另一端。
|
||||
* @returns {string} 带 room 参数的完整 WebSocket 地址
|
||||
*/
|
||||
function getSignalingUrl() {
|
||||
// 如果原地址已经带有查询参数,则使用 & 继续拼接,否则使用 ? 开始查询参数。
|
||||
const separator = SIGNALING_SERVER_URL.includes('?') ? '&' : '?'
|
||||
// 对房间名进行 URL 编码,避免特殊字符破坏 WebSocket 地址。
|
||||
return `${SIGNALING_SERVER_URL}${separator}room=${encodeURIComponent(ROOM_NAME)}`
|
||||
}
|
||||
|
||||
/**
|
||||
* 统一处理通话过程中的异常。
|
||||
* 该方法会记录错误、更新页面状态,并使用 Element Plus 通知用户。
|
||||
@ -150,8 +132,6 @@ function getSignalingUrl() {
|
||||
function showError(message, error) {
|
||||
// 将完整错误输出到控制台,方便开发环境定位具体异常。
|
||||
console.error(message, error || '')
|
||||
// 保存用户可读的错误文案,以便状态卡显示错误提示。
|
||||
errorMessage.value = message
|
||||
// 将内部状态切换为错误状态,驱动状态灯和说明文字更新。
|
||||
connectionStatus.value = 'error'
|
||||
// 明确标记当前已经不处于 WebRTC 已连接状态。
|
||||
@ -180,7 +160,7 @@ function sendSignalingMessage(message) {
|
||||
|
||||
/**
|
||||
* 创建并初始化 RTCPeerConnection。
|
||||
* 作用包括:加入本地麦克风轨道、接收并播放远端音频、发送 ICE 候选,
|
||||
* 作用包括:接收并播放远端音频、发送 ICE 候选,
|
||||
* 以及监听连接状态来同步页面上的“连接中/通话中/未连接”状态。
|
||||
* @param {number} sessionId 本次通话的会话编号,用于忽略过期回调
|
||||
* @returns {RTCPeerConnection} 初始化完成的 WebRTC 连接实例
|
||||
@ -196,12 +176,6 @@ function setupPeerConnection(sessionId) {
|
||||
// 将远端媒体流绑定到远端 audio 元素,轨道到达后即可播放。
|
||||
if (remoteAudio.value) remoteAudio.value.srcObject = remoteStream.value
|
||||
|
||||
// 遍历本地麦克风流中的所有轨道,并将其发布到 WebRTC 连接中。
|
||||
localStream.value.getTracks().forEach((track) => {
|
||||
// addTrack 会触发 SDP 中相应媒体能力的协商。
|
||||
pc.addTrack(track, localStream.value)
|
||||
})
|
||||
|
||||
// 对端音频轨道到达后,将轨道加入远端媒体流并交给 audio 元素播放。
|
||||
pc.ontrack = (event) => {
|
||||
// 如果回调属于已经结束的旧会话,则直接忽略,避免污染新通话。
|
||||
@ -227,10 +201,13 @@ function setupPeerConnection(sessionId) {
|
||||
try {
|
||||
// 按约定协议发送候选地址、媒体行索引和媒体标识。
|
||||
sendSignalingMessage({
|
||||
type: 'ice-candidate',
|
||||
candidate: event.candidate.candidate,
|
||||
sdpMLineIndex: event.candidate.sdpMLineIndex,
|
||||
sdpMid: event.candidate.sdpMid
|
||||
type: 'candidate',
|
||||
sessionId: signalingSessionId,
|
||||
candidate: {
|
||||
candidate: event.candidate.candidate,
|
||||
sdpMid: event.candidate.sdpMid,
|
||||
sdpMLineIndex: event.candidate.sdpMLineIndex
|
||||
}
|
||||
})
|
||||
} catch (error) {
|
||||
// 信令连接异常时进入统一错误处理流程。
|
||||
@ -248,8 +225,6 @@ function setupPeerConnection(sessionId) {
|
||||
isConnected.value = true
|
||||
// 切换为通话中状态,启用页面声波和光晕动画。
|
||||
connectionStatus.value = 'connected'
|
||||
// 成功连接后清除之前可能存在的错误信息。
|
||||
errorMessage.value = ''
|
||||
// 向用户提示语音通道已经成功建立。
|
||||
ElMessage.success('实时语音连接已建立')
|
||||
// failed 或 disconnected 表示媒体通道已经不可用。
|
||||
@ -281,9 +256,8 @@ async function flushPendingIceCandidates(pc) {
|
||||
|
||||
/**
|
||||
* 处理信令服务器发来的消息。
|
||||
* - offer:设置远端描述,创建并返回 answer;
|
||||
* - answer:完成发起端的远端描述设置;
|
||||
* - ice-candidate:立即添加候选,或等待远端 SDP 就绪后再添加。
|
||||
* - candidate:立即添加候选,或等待远端 SDP 就绪后再添加。
|
||||
* @param {MessageEvent} event WebSocket 消息事件
|
||||
* @param {number} sessionId 本次通话的会话编号
|
||||
* @returns {Promise<void>}
|
||||
@ -304,42 +278,40 @@ async function handleSignalingMessage(event, sessionId) {
|
||||
return
|
||||
}
|
||||
|
||||
// 后端消息携带其他会话 ID 时直接忽略,防止串话或旧会话消息污染当前连接。
|
||||
if (message.sessionId && signalingSessionId && message.sessionId !== signalingSessionId) return
|
||||
|
||||
// 获取当前 WebRTC 实例,后续 SDP 和 ICE 都需要写入该实例。
|
||||
const pc = peerConnection.value
|
||||
// 通话可能已经被清理,没有连接实例时直接忽略消息。
|
||||
if (!pc) return
|
||||
|
||||
try {
|
||||
// 收到 Offer 表示对端希望发起或重新进行媒体能力协商。
|
||||
if (message.type === 'offer') {
|
||||
// 将对端 Offer 设置为远端描述,使浏览器了解对端媒体能力。
|
||||
await pc.setRemoteDescription(new RTCSessionDescription({ type: 'offer', sdp: message.sdp }))
|
||||
// 远端描述就绪后,补充此前因时序原因暂存的 ICE 候选。
|
||||
await flushPendingIceCandidates(pc)
|
||||
// 根据远端 Offer 和本地能力生成应答 SDP。
|
||||
const answer = await pc.createAnswer()
|
||||
// 保存本地 Answer,使 WebRTC 连接进入正确的信令状态。
|
||||
await pc.setLocalDescription(answer)
|
||||
// 将 Answer 通过信令服务器返回给发起方。
|
||||
sendSignalingMessage({ type: 'answer', sdp: answer.sdp })
|
||||
// 收到 Answer 表示对端已经响应了本地发送的 Offer。
|
||||
} else if (message.type === 'answer') {
|
||||
// 当前协议固定由浏览器发送 Offer,后端只需返回对应的 Answer。
|
||||
if (message.type === 'answer') {
|
||||
// 设置远端 Answer,完成 SDP Offer/Answer 协商闭环。
|
||||
await pc.setRemoteDescription(new RTCSessionDescription({ type: 'answer', sdp: message.sdp }))
|
||||
// SDP 就绪后添加在 Answer 到达前收到的 ICE 候选。
|
||||
await flushPendingIceCandidates(pc)
|
||||
// ICE 候选用于告知本端对端可能可用的网络地址和传输路径。
|
||||
} else if (message.type === 'ice-candidate') {
|
||||
} else if (message.type === 'candidate') {
|
||||
// 使用协议字段还原浏览器可识别的 RTCIceCandidate 对象。
|
||||
const candidate = new RTCIceCandidate({
|
||||
candidate: message.candidate,
|
||||
sdpMLineIndex: message.sdpMLineIndex,
|
||||
sdpMid: message.sdpMid
|
||||
candidate: message.candidate?.candidate,
|
||||
sdpMLineIndex: message.candidate?.sdpMLineIndex,
|
||||
sdpMid: message.candidate?.sdpMid
|
||||
})
|
||||
// 已有远端描述时可以立即把候选加入 WebRTC 连接。
|
||||
if (pc.remoteDescription) await pc.addIceCandidate(candidate)
|
||||
// 远端描述尚未到达时先缓存候选,等待 SDP 设置完成后处理。
|
||||
else pendingIceCandidates.push(candidate)
|
||||
// 后端主动报告会话错误时停止继续协商并显示错误原因。
|
||||
} else if (message.type === 'error') {
|
||||
showError(message.message || '语音会话发生异常')
|
||||
// 后端确认会话停止后同步恢复页面状态。
|
||||
} else if (message.type === 'stopped' || message.type === 'stop') {
|
||||
cleanupResources(false)
|
||||
connectionStatus.value = 'idle'
|
||||
}
|
||||
} catch (error) {
|
||||
// 捕获 SDP 设置、Answer 创建或候选添加过程中的所有异常。
|
||||
@ -354,24 +326,75 @@ async function handleSignalingMessage(event, sessionId) {
|
||||
* @returns {Promise<WebSocket>} 连接成功后返回 WebSocket 实例
|
||||
*/
|
||||
function connectSignalingServer(sessionId) {
|
||||
// 返回 Promise,让 startCall 可以等待 WebSocket 真正连接成功后再发送 Offer。
|
||||
// Promise 只有在后端返回 joined 后才完成,确保 Offer 一定携带有效 sessionId。
|
||||
return new Promise((resolve, reject) => {
|
||||
// 使用带房间参数的地址创建信令 WebSocket。
|
||||
const socket = new WebSocket(getSignalingUrl())
|
||||
// 保存实例,供发送信令及结束通话时关闭连接。
|
||||
// 直接连接后端提供的语音信令地址。
|
||||
const socket = new WebSocket(SIGNALING_SERVER_URL)
|
||||
// 保存实例,供发送信令以及通话结束时关闭连接。
|
||||
webSocket.value = socket
|
||||
// 标记 joined 是否已经返回,用于区分建连失败和通话中断线。
|
||||
let hasJoined = false
|
||||
|
||||
// 连接成功时兑现 Promise,允许后续 SDP 协商继续执行。
|
||||
socket.onopen = () => resolve(socket)
|
||||
// 每条服务端消息都交由统一的信令处理方法按 type 分发。
|
||||
socket.onmessage = (event) => handleSignalingMessage(event, sessionId)
|
||||
// 建连阶段发生错误时拒绝 Promise,由 startCall 捕获并清理资源。
|
||||
socket.onerror = () => reject(new Error('WebSocket 连接失败'))
|
||||
// 监听连接关闭,识别通话过程中意外断开信令服务器的情况。
|
||||
// WebSocket 建立后必须先发送 join,由后端创建会话并打开机器人 gRPC 音频流。
|
||||
socket.onopen = () => {
|
||||
sendSignalingMessage({
|
||||
type: 'join',
|
||||
terminalId: TERMINAL_ID,
|
||||
deviceId: DEVICE_ID,
|
||||
operatorId: OPERATOR_ID
|
||||
})
|
||||
}
|
||||
|
||||
// joined 消息在此处完成启动流程,其他消息继续交给 WebRTC 信令处理方法。
|
||||
socket.onmessage = (event) => {
|
||||
let message
|
||||
try {
|
||||
// 先解析一次消息,以识别建立后端会话所需的 joined 响应。
|
||||
message = JSON.parse(event.data)
|
||||
} catch (error) {
|
||||
// 非法消息交给统一处理方法展示协议错误。
|
||||
handleSignalingMessage(event, sessionId)
|
||||
return
|
||||
}
|
||||
|
||||
// 后端成功创建会话后保存业务 sessionId,并开始前端心跳。
|
||||
if (message.type === 'joined') {
|
||||
// 缺少 sessionId 时无法发送后续信令,因此直接判定加入失败。
|
||||
if (!message.sessionId) {
|
||||
reject(new Error('后端 joined 响应缺少 sessionId'))
|
||||
return
|
||||
}
|
||||
// 忽略旧通话延迟到达的 joined 响应。
|
||||
if (sessionId !== callSessionId) return
|
||||
// 保存后端会话 ID,供本次通话的全部后续消息使用。
|
||||
signalingSessionId = message.sessionId
|
||||
// 标记后端会话已经创建成功。
|
||||
hasJoined = true
|
||||
// 将 joined 数据返回 startCall,允许继续创建并发送 Offer。
|
||||
resolve(message)
|
||||
return
|
||||
}
|
||||
|
||||
// join 阶段收到后端错误时立即终止等待,让 startCall 进入异常清理流程。
|
||||
if (message.type === 'error' && !hasJoined) {
|
||||
reject(new Error(message.message || '后端创建语音会话失败'))
|
||||
return
|
||||
}
|
||||
|
||||
// joined 之外的 Answer、Candidate、错误等消息按 WebRTC 协议处理。
|
||||
handleSignalingMessage(event, sessionId)
|
||||
}
|
||||
|
||||
// WebSocket 底层异常时让尚未完成的启动流程失败。
|
||||
socket.onerror = () => {
|
||||
if (!hasJoined) reject(new Error('WebSocket 连接失败'))
|
||||
}
|
||||
|
||||
// 监听信令连接关闭,识别加入过程中失败或通话期间意外断线。
|
||||
socket.onclose = () => {
|
||||
// 仅当前会话仍处于通话状态时才提示异常,主动挂断不应报错。
|
||||
if (sessionId === callSessionId && isCalling.value) {
|
||||
// 信令连接意外关闭时更新错误状态并提醒用户。
|
||||
if (!hasJoined) {
|
||||
reject(new Error('信令服务器在会话建立前已断开'))
|
||||
} else if (sessionId === callSessionId && isCalling.value) {
|
||||
showError('信令服务器连接已断开')
|
||||
}
|
||||
}
|
||||
@ -429,10 +452,13 @@ async function startCall() {
|
||||
isStarting.value = true
|
||||
// 立即将 UI 更新为连接中状态。
|
||||
connectionStatus.value = 'connecting'
|
||||
// 新一轮连接开始前清空上一次的错误提示。
|
||||
errorMessage.value = ''
|
||||
|
||||
try {
|
||||
// 第一步连接信令服务器、发送 join,并等待后端返回 joined 和业务 sessionId。
|
||||
await connectSignalingServer(sessionId)
|
||||
// joined 成功后创建 WebRTC 连接,用于承载后续双向音频轨道。
|
||||
const pc = setupPeerConnection(sessionId)
|
||||
|
||||
// 请求用户授权并采集仅包含音频的本地媒体流。
|
||||
const stream = await navigator.mediaDevices.getUserMedia({
|
||||
audio: {
|
||||
@ -460,17 +486,17 @@ async function startCall() {
|
||||
if (localAudio.value) localAudio.value.srcObject = stream
|
||||
// 启动麦克风音量分析,为声波可视化提供数据基础。
|
||||
startVolumeMeter(stream)
|
||||
// 创建 WebRTC 实例并把本地音频轨道加入连接。
|
||||
const pc = setupPeerConnection(sessionId)
|
||||
// 等待信令服务器连接成功,确保随后的 Offer 不会发送失败。
|
||||
await connectSignalingServer(sessionId)
|
||||
// 将麦克风音轨加入 WebRTC;连接建立后浏览器会自动通过 WebRTC 发送音频。
|
||||
stream.getAudioTracks().forEach((track) => {
|
||||
pc.addTrack(track, stream)
|
||||
})
|
||||
|
||||
// 根据本地音频能力生成发起协商所需的 SDP Offer。
|
||||
const offer = await pc.createOffer()
|
||||
const offer = await pc.createOffer({ offerToReceiveAudio: true })
|
||||
// 设置本地描述,正式启动 ICE 候选收集过程。
|
||||
await pc.setLocalDescription(offer)
|
||||
// 按约定协议将 Offer 发送给同一房间的远端用户。
|
||||
sendSignalingMessage({ type: 'offer', sdp: offer.sdp })
|
||||
sendSignalingMessage({ type: 'offer', sessionId: signalingSessionId, sdp: offer.sdp })
|
||||
// 标记通话已启动;最终媒体连通状态仍由 connectionstatechange 更新。
|
||||
isCalling.value = true
|
||||
} catch (error) {
|
||||
@ -517,7 +543,17 @@ function toggleMute() {
|
||||
* 包括停止音量分析、关闭 AudioContext/WebRTC/WebSocket、停止本地和远端轨道,
|
||||
* 清空 audio 元素并重置通话相关状态。该方法可重复调用,供异常、挂断和卸载共同使用。
|
||||
*/
|
||||
function cleanupResources() {
|
||||
function cleanupResources(notifyBackend = true) {
|
||||
// 主动挂断或启动失败时,先通知后端停止 gRPC 音频流并释放机器人会话。
|
||||
if (notifyBackend && signalingSessionId && webSocket.value?.readyState === WebSocket.OPEN) {
|
||||
try {
|
||||
// 按后端协议发送停止消息;即使发送失败也必须继续释放本地资源。
|
||||
sendSignalingMessage({ type: 'stop', sessionId: signalingSessionId })
|
||||
} catch (error) {
|
||||
// 仅记录停止消息的发送异常,避免它中断后续清理流程。
|
||||
console.warn('停止会话消息发送失败', error)
|
||||
}
|
||||
}
|
||||
// 更新会话编号,使当前会话尚未完成的所有异步回调立即失效。
|
||||
callSessionId += 1
|
||||
// 清空未处理 ICE 候选,防止带入下一次通话。
|
||||
@ -587,6 +623,8 @@ function cleanupResources() {
|
||||
isConnected.value = false
|
||||
// 下次通话默认从非静音状态开始。
|
||||
isMuted.value = false
|
||||
// 清空后端业务会话 ID,避免下一次通话误用旧 sessionId。
|
||||
signalingSessionId = ''
|
||||
}
|
||||
|
||||
/**
|
||||
@ -599,7 +637,6 @@ function endCall() {
|
||||
// 将状态恢复为未连接,更新状态标签和说明文案。
|
||||
connectionStatus.value = 'idle'
|
||||
// 主动结束后清除错误信息,避免旧错误继续显示。
|
||||
errorMessage.value = ''
|
||||
// 向用户确认通话已经结束。
|
||||
ElMessage.success('通话已结束')
|
||||
}
|
||||
|
||||
@ -99,7 +99,9 @@
|
||||
</div>
|
||||
<div class="right-container">
|
||||
<div class="container-title">车载监控</div>
|
||||
<div class="monitor-container"></div>
|
||||
<div class="monitor-container">
|
||||
<IPlayer :isLive="true" :videoUrl="videoUrl" />
|
||||
</div>
|
||||
<div class="split-line"></div>
|
||||
<div class="container-title">语音对话</div>
|
||||
<div class="voice-container">
|
||||
@ -118,6 +120,7 @@ import { getRobotList } from '@/api/inspection/robot'
|
||||
import { getJointStateApi, moveJApi, torqueOnApi, speedJApi, stopMotionApi } from '@/api/inspection/cockpit'
|
||||
import { Remove, CirclePlus } from '@element-plus/icons-vue'
|
||||
import VoiceConversation from "./VoiceConversation.vue";
|
||||
import IPlayer from "@/components/IPlayer/index.vue";
|
||||
|
||||
const robotList = ref([])
|
||||
|
||||
@ -259,6 +262,8 @@ const stop = async () => {
|
||||
})
|
||||
}
|
||||
|
||||
const videoUrl = ref('https://test-streams.mux.dev/x36xhzz/x36xhzz.m3u8')
|
||||
|
||||
onMounted(() => {
|
||||
fetchRobotList()
|
||||
})
|
||||
|
||||
@ -20,7 +20,16 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="center-container">监控</div>
|
||||
<div class="center-container">
|
||||
<div class="video-container" v-if="activeBot == 0">
|
||||
<div class="ttt" v-for="value in 4">
|
||||
<IPlayer :isLive="true" :videoUrl="videoUrl" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="single-video-container" v-else>
|
||||
<IPlayer :isLive="true" :videoUrl="videoUrl" />
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="activeBot != 0" class="right-container">
|
||||
<div class="container-title">OCR识别</div>
|
||||
<div class="ocr-container"></div>
|
||||
@ -31,6 +40,9 @@
|
||||
</template>
|
||||
<script setup>
|
||||
import SvgIcon from "@/components/SvgIcon";
|
||||
import IPlayer from "@/components/IPlayer/index.vue";
|
||||
|
||||
const videoUrl = ref('https://test-streams.mux.dev/x36xhzz/x36xhzz.m3u8')
|
||||
|
||||
const colorList = ['#00D4FF', '#FFB300', '#00FF88', '#fd00ff']
|
||||
|
||||
@ -95,8 +107,38 @@ const changeBot = (index) => {
|
||||
|
||||
.center-container {
|
||||
flex: 1;
|
||||
border: 1px solid #1E3A5F;
|
||||
min-width: 0;
|
||||
min-height: 0;
|
||||
overflow: hidden;
|
||||
border: 1px solid #1e3a5f;
|
||||
border-top: 0;
|
||||
|
||||
.video-container {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
grid-template-rows: repeat(2, minmax(0, 1fr));
|
||||
gap: 16px;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
min-height: 0;
|
||||
overflow: hidden;
|
||||
|
||||
.ttt {
|
||||
width: auto;
|
||||
height: auto;
|
||||
min-width: 0;
|
||||
min-height: 0;
|
||||
overflow: hidden;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
}
|
||||
|
||||
.single-video-container {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
min-height: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
}
|
||||
|
||||
.right-container {
|
||||
|
||||
Loading…
Reference in New Issue
Block a user