478 lines
12 KiB
Vue
478 lines
12 KiB
Vue
|
|
<template>
|
|||
|
|
<div class="jy-media-container">
|
|||
|
|
<!-- 设备选择区域 -->
|
|||
|
|
<div class="device-selector">
|
|||
|
|
<el-select v-model="selectedDevice" placeholder="选择设备" filterable>
|
|||
|
|
<el-option
|
|||
|
|
v-for="device in onlineDevices"
|
|||
|
|
:key="device.hostbody"
|
|||
|
|
:label="`${device.hostname} (${device.hostbody})`"
|
|||
|
|
:value="device.hostbody"
|
|||
|
|
/>
|
|||
|
|
</el-select>
|
|||
|
|
|
|||
|
|
<el-button-group class="control-buttons">
|
|||
|
|
<el-button
|
|||
|
|
type="primary"
|
|||
|
|
:disabled="!selectedDevice || videoLoading"
|
|||
|
|
@click="startVideoCall"
|
|||
|
|
>
|
|||
|
|
{{ videoLoading ? '呼叫中...' : '视频呼叫' }}
|
|||
|
|
</el-button>
|
|||
|
|
|
|||
|
|
<el-button
|
|||
|
|
type="success"
|
|||
|
|
:disabled="!selectedDevice || audioLoading"
|
|||
|
|
@click="startAudioCall"
|
|||
|
|
>
|
|||
|
|
{{ audioLoading ? '呼叫中...' : '音频呼叫' }}
|
|||
|
|
</el-button>
|
|||
|
|
|
|||
|
|
<el-button
|
|||
|
|
type="warning"
|
|||
|
|
:disabled="!isVideoActive || isAudioInVideoActive"
|
|||
|
|
@click="startAudioInVideo"
|
|||
|
|
>
|
|||
|
|
视频中开启音频
|
|||
|
|
</el-button>
|
|||
|
|
|
|||
|
|
<el-button
|
|||
|
|
type="danger"
|
|||
|
|
:disabled="!isVideoActive && !isAudioActive"
|
|||
|
|
@click="stopAllCalls"
|
|||
|
|
>
|
|||
|
|
停止所有
|
|||
|
|
</el-button>
|
|||
|
|
</el-button-group>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<!-- 视频播放区域 -->
|
|||
|
|
<div class="video-container">
|
|||
|
|
<video
|
|||
|
|
ref="videoPlayer"
|
|||
|
|
controls
|
|||
|
|
autoplay
|
|||
|
|
playsinline
|
|||
|
|
class="video-element"
|
|||
|
|
:class="{ active: isVideoActive }"
|
|||
|
|
/>
|
|||
|
|
|
|||
|
|
<div v-if="!isVideoActive" class="video-placeholder">
|
|||
|
|
<el-icon :size="50"><VideoCamera /></el-icon>
|
|||
|
|
<p>视频未开启</p>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<!-- 状态信息 -->
|
|||
|
|
<div class="status-info">
|
|||
|
|
<el-alert
|
|||
|
|
v-if="errorMessage"
|
|||
|
|
:title="errorMessage"
|
|||
|
|
type="error"
|
|||
|
|
show-icon
|
|||
|
|
closable
|
|||
|
|
/>
|
|||
|
|
|
|||
|
|
<el-alert
|
|||
|
|
v-if="callStatus"
|
|||
|
|
:title="callStatus"
|
|||
|
|
:type="callStatusType"
|
|||
|
|
show-icon
|
|||
|
|
/>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</template>
|
|||
|
|
|
|||
|
|
<script setup>
|
|||
|
|
import { ref, onMounted, onBeforeUnmount } from 'vue'
|
|||
|
|
import { VideoCamera } from '@element-plus/icons-vue'
|
|||
|
|
import axios from 'axios'
|
|||
|
|
|
|||
|
|
// 响应式数据
|
|||
|
|
const selectedDevice = ref('')
|
|||
|
|
const onlineDevices = ref([])
|
|||
|
|
const isVideoActive = ref(false)
|
|||
|
|
const isAudioActive = ref(false)
|
|||
|
|
const isAudioInVideoActive = ref(false)
|
|||
|
|
const videoLoading = ref(false)
|
|||
|
|
const audioLoading = ref(false)
|
|||
|
|
const errorMessage = ref('')
|
|||
|
|
const callStatus = ref('')
|
|||
|
|
const callStatusType = ref('info')
|
|||
|
|
const videoPlayer = ref(null)
|
|||
|
|
const websocket = ref(null)
|
|||
|
|
|
|||
|
|
// WebSocket连接配置
|
|||
|
|
const WS_URL = 'ws://localhost:5000' // 替换为实际WS地址
|
|||
|
|
const API_BASE = '' // 替换为实际API地址
|
|||
|
|
|
|||
|
|
// 获取在线设备列表
|
|||
|
|
const fetchOnlineDevices = async () => {
|
|||
|
|
try {
|
|||
|
|
const response = await axios.post(`${API_BASE}/rest/other/unitjson/gdlist`, {
|
|||
|
|
id: "1001", // 替换为单位ID
|
|||
|
|
bh: "bh",
|
|||
|
|
text: "dname"
|
|||
|
|
}, {
|
|||
|
|
headers: {
|
|||
|
|
'Content-Type': 'application/json',
|
|||
|
|
'Cookie': `PHPSESSID=${localStorage.getItem('sessionId')}`
|
|||
|
|
}
|
|||
|
|
})
|
|||
|
|
|
|||
|
|
onlineDevices.value = response.data.data[0]?.sub || []
|
|||
|
|
} catch (error) {
|
|||
|
|
console.error('获取设备列表失败:', error)
|
|||
|
|
errorMessage.value = `获取设备列表失败: ${error.message}`
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 初始化WebSocket连接
|
|||
|
|
const initWebSocket = () => {
|
|||
|
|
websocket.value = new WebSocket(WS_URL)
|
|||
|
|
|
|||
|
|
websocket.value.onopen = () => {
|
|||
|
|
console.log('WebSocket连接已建立')
|
|||
|
|
callStatus.value = '实时通讯连接已建立'
|
|||
|
|
callStatusType.value = 'success'
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
websocket.value.onmessage = (event) => {
|
|||
|
|
const data = JSON.parse(event.data)
|
|||
|
|
console.log('WebSocket消息:', data)
|
|||
|
|
|
|||
|
|
// 处理视频流回调
|
|||
|
|
if (data.zk_start_video) {
|
|||
|
|
handleVideoStream(data.zk_start_video)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 处理音频流回调
|
|||
|
|
if (data.zk_start_audio) {
|
|||
|
|
handleAudioStream(data.zk_start_audio)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 处理平台推流回调
|
|||
|
|
if (data.zk_platform_push_video || data.zk_platform_push_audio) {
|
|||
|
|
handlePlatformStream(data)
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
websocket.value.onerror = (error) => {
|
|||
|
|
console.error('WebSocket错误:', error)
|
|||
|
|
errorMessage.value = `实时通讯连接错误: ${error.message}`
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
websocket.value.onclose = () => {
|
|||
|
|
console.log('WebSocket连接已关闭')
|
|||
|
|
callStatus.value = '实时通讯连接已断开'
|
|||
|
|
callStatusType.value = 'warning'
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 开始视频呼叫
|
|||
|
|
const startVideoCall = async () => {
|
|||
|
|
if (!selectedDevice.value) return
|
|||
|
|
|
|||
|
|
videoLoading.value = true
|
|||
|
|
callStatus.value = '正在发起视频呼叫...'
|
|||
|
|
callStatusType.value = 'info'
|
|||
|
|
|
|||
|
|
try {
|
|||
|
|
const response = await axios.post(`${API_BASE}/rest/live/chrome/startLive`, {
|
|||
|
|
hostbody_arr: [selectedDevice.value],
|
|||
|
|
callId: "",
|
|||
|
|
type: ""
|
|||
|
|
}, {
|
|||
|
|
headers: {
|
|||
|
|
'Content-Type': 'application/json',
|
|||
|
|
'Cookie': `PHPSESSID=${localStorage.getItem('sessionId')}`
|
|||
|
|
}
|
|||
|
|
})
|
|||
|
|
|
|||
|
|
if (response.data.code === 200) {
|
|||
|
|
callStatus.value = '视频呼叫已发起,等待设备响应...'
|
|||
|
|
callStatusType.value = 'success'
|
|||
|
|
} else {
|
|||
|
|
throw new Error(response.data.msg)
|
|||
|
|
}
|
|||
|
|
} catch (error) {
|
|||
|
|
console.error('视频呼叫失败:', error)
|
|||
|
|
errorMessage.value = `视频呼叫失败: ${error.message}`
|
|||
|
|
callStatus.value = `视频呼叫失败: ${error.message}`
|
|||
|
|
callStatusType.value = 'error'
|
|||
|
|
} finally {
|
|||
|
|
videoLoading.value = false
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 开始音频呼叫
|
|||
|
|
const startAudioCall = async () => {
|
|||
|
|
if (!selectedDevice.value) return
|
|||
|
|
|
|||
|
|
audioLoading.value = true
|
|||
|
|
callStatus.value = '正在发起音频呼叫...'
|
|||
|
|
callStatusType.value = 'info'
|
|||
|
|
|
|||
|
|
try {
|
|||
|
|
const response = await axios.post(`${API_BASE}/rest/live/chrome/startAudio`, {
|
|||
|
|
hostbody_arr: [selectedDevice.value],
|
|||
|
|
callId: ""
|
|||
|
|
}, {
|
|||
|
|
headers: {
|
|||
|
|
'Content-Type': 'application/json',
|
|||
|
|
'Cookie': `PHPSESSID=${localStorage.getItem('sessionId')}`
|
|||
|
|
}
|
|||
|
|
})
|
|||
|
|
|
|||
|
|
if (response.data.code === 200) {
|
|||
|
|
callStatus.value = '音频呼叫已发起,等待设备响应...'
|
|||
|
|
callStatusType.value = 'success'
|
|||
|
|
} else {
|
|||
|
|
throw new Error(response.data.msg)
|
|||
|
|
}
|
|||
|
|
} catch (error) {
|
|||
|
|
console.error('音频呼叫失败:', error)
|
|||
|
|
errorMessage.value = `音频呼叫失败: ${error.message}`
|
|||
|
|
callStatus.value = `音频呼叫失败: ${error.message}`
|
|||
|
|
callStatusType.value = 'error'
|
|||
|
|
} finally {
|
|||
|
|
audioLoading.value = false
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 视频中开启音频
|
|||
|
|
const startAudioInVideo = async () => {
|
|||
|
|
if (!selectedDevice.value || !isVideoActive.value) return
|
|||
|
|
|
|||
|
|
callStatus.value = '正在视频通话中开启音频...'
|
|||
|
|
callStatusType.value = 'info'
|
|||
|
|
|
|||
|
|
try {
|
|||
|
|
const response = await axios.post(`${API_BASE}/rest/live/chrome/startAudioInVideo`, {
|
|||
|
|
hostbody: selectedDevice.value
|
|||
|
|
}, {
|
|||
|
|
headers: {
|
|||
|
|
'Content-Type': 'application/json',
|
|||
|
|
'Cookie': `PHPSESSID=${localStorage.getItem('sessionId')}`
|
|||
|
|
}
|
|||
|
|
})
|
|||
|
|
|
|||
|
|
if (response.data.code === 200) {
|
|||
|
|
isAudioInVideoActive.value = true
|
|||
|
|
callStatus.value = '已在视频通话中开启音频'
|
|||
|
|
callStatusType.value = 'success'
|
|||
|
|
|
|||
|
|
// 处理平台音频流
|
|||
|
|
if (response.data.data.platform_rtsp) {
|
|||
|
|
setupAudioStream(response.data.data.platform_rtsp)
|
|||
|
|
}
|
|||
|
|
} else {
|
|||
|
|
throw new Error(response.data.msg)
|
|||
|
|
}
|
|||
|
|
} catch (error) {
|
|||
|
|
console.error('开启音频失败:', error)
|
|||
|
|
errorMessage.value = `开启音频失败: ${error.message}`
|
|||
|
|
callStatus.value = `开启音频失败: ${error.message}`
|
|||
|
|
callStatusType.value = 'error'
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 处理视频流
|
|||
|
|
const handleVideoStream = (streamData) => {
|
|||
|
|
if (streamData.hostbody !== selectedDevice.value) return
|
|||
|
|
|
|||
|
|
isVideoActive.value = true
|
|||
|
|
callStatus.value = '视频流已连接'
|
|||
|
|
callStatusType.value = 'success'
|
|||
|
|
|
|||
|
|
// 优先使用WebRTC流,回退到RTSP
|
|||
|
|
const streamUrl = streamData.webrtc_url || streamData.rtsp_url
|
|||
|
|
if (streamUrl) {
|
|||
|
|
setupVideoStream(streamUrl)
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 处理音频流
|
|||
|
|
const handleAudioStream = (streamData) => {
|
|||
|
|
if (streamData.hostbody !== selectedDevice.value) return
|
|||
|
|
|
|||
|
|
isAudioActive.value = true
|
|||
|
|
callStatus.value = '音频流已连接'
|
|||
|
|
callStatusType.value = 'success'
|
|||
|
|
|
|||
|
|
// 优先使用WebRTC流,回退到RTSP
|
|||
|
|
const streamUrl = streamData.webrtc_url || streamData.rtsp_url
|
|||
|
|
if (streamUrl) {
|
|||
|
|
setupAudioStream(streamUrl)
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 处理平台推流
|
|||
|
|
const handlePlatformStream = (data) => {
|
|||
|
|
if (data.zk_platform_push_video && data.zk_platform_push_video.status === 1) {
|
|||
|
|
callStatus.value = '平台视频推流已开始'
|
|||
|
|
callStatusType.value = 'success'
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if (data.zk_platform_push_audio && data.zk_platform_push_audio.status === 1) {
|
|||
|
|
callStatus.value = '平台音频推流已开始'
|
|||
|
|
callStatusType.value = 'success'
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 设置视频流
|
|||
|
|
const setupVideoStream = (streamUrl) => {
|
|||
|
|
const video = videoPlayer.value
|
|||
|
|
if (!video) return
|
|||
|
|
|
|||
|
|
// 实际项目中可能需要使用专门的播放器库(如hls.js)处理流媒体
|
|||
|
|
if (streamUrl.startsWith('rtsp://')) {
|
|||
|
|
// RTSP流需要转协议,实际项目中应使用转码服务
|
|||
|
|
console.warn('浏览器不支持直接播放RTSP流,需要转协议')
|
|||
|
|
errorMessage.value = '浏览器不支持直接播放RTSP流,需要后端转协议'
|
|||
|
|
} else {
|
|||
|
|
video.src = streamUrl
|
|||
|
|
video.play().catch(err => {
|
|||
|
|
console.error('视频播放错误:', err)
|
|||
|
|
errorMessage.value = `视频播放错误: ${err.message}`
|
|||
|
|
})
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 设置音频流
|
|||
|
|
const setupAudioStream = (streamUrl) => {
|
|||
|
|
// 在实际项目中,可能需要创建单独的audio元素处理音频流
|
|||
|
|
console.log('音频流地址:', streamUrl)
|
|||
|
|
// 实现逻辑与视频流类似
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 停止所有呼叫
|
|||
|
|
const stopAllCalls = async () => {
|
|||
|
|
if (!selectedDevice.value) return
|
|||
|
|
|
|||
|
|
callStatus.value = '正在停止所有呼叫...'
|
|||
|
|
callStatusType.value = 'info'
|
|||
|
|
|
|||
|
|
try {
|
|||
|
|
// 停止视频
|
|||
|
|
if (isVideoActive.value) {
|
|||
|
|
await axios.post(`${API_BASE}/rest/live/chrome/stopLive`, {
|
|||
|
|
hostbody_arr: [selectedDevice.value]
|
|||
|
|
}, {
|
|||
|
|
headers: {
|
|||
|
|
'Content-Type': 'application/json',
|
|||
|
|
'Cookie': `PHPSESSID=${localStorage.getItem('sessionId')}`
|
|||
|
|
}
|
|||
|
|
})
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 停止音频
|
|||
|
|
if (isAudioActive.value || isAudioInVideoActive.value) {
|
|||
|
|
await axios.post(`${API_BASE}/rest/live/chrome/stopAudio`, {
|
|||
|
|
hostbody_arr: [selectedDevice.value],
|
|||
|
|
wsChannelId_arr: [] // 实际项目中需要填充正确的wsChannelId
|
|||
|
|
}, {
|
|||
|
|
headers: {
|
|||
|
|
'Content-Type': 'application/json',
|
|||
|
|
'Cookie': `PHPSESSID=${localStorage.getItem('sessionId')}`
|
|||
|
|
}
|
|||
|
|
})
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 重置状态
|
|||
|
|
isVideoActive.value = false
|
|||
|
|
isAudioActive.value = false
|
|||
|
|
isAudioInVideoActive.value = false
|
|||
|
|
|
|||
|
|
// 清除视频源
|
|||
|
|
if (videoPlayer.value) {
|
|||
|
|
videoPlayer.value.src = ''
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
callStatus.value = '所有呼叫已停止'
|
|||
|
|
callStatusType.value = 'success'
|
|||
|
|
} catch (error) {
|
|||
|
|
console.error('停止呼叫失败:', error)
|
|||
|
|
errorMessage.value = `停止呼叫失败: ${error.message}`
|
|||
|
|
callStatus.value = `停止呼叫失败: ${error.message}`
|
|||
|
|
callStatusType.value = 'error'
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 组件生命周期
|
|||
|
|
onMounted(() => {
|
|||
|
|
fetchOnlineDevices()
|
|||
|
|
initWebSocket()
|
|||
|
|
})
|
|||
|
|
|
|||
|
|
onBeforeUnmount(() => {
|
|||
|
|
if (websocket.value) {
|
|||
|
|
websocket.value.close()
|
|||
|
|
}
|
|||
|
|
})
|
|||
|
|
</script>
|
|||
|
|
|
|||
|
|
<style scoped>
|
|||
|
|
.jy-media-container {
|
|||
|
|
width: 100%;
|
|||
|
|
max-width: 1200px;
|
|||
|
|
margin: 0 auto;
|
|||
|
|
padding: 20px;
|
|||
|
|
background-color: #f5f7fa;
|
|||
|
|
border-radius: 8px;
|
|||
|
|
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.1);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.device-selector {
|
|||
|
|
display: flex;
|
|||
|
|
gap: 10px;
|
|||
|
|
margin-bottom: 20px;
|
|||
|
|
align-items: center;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.control-buttons {
|
|||
|
|
margin-left: 10px;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.video-container {
|
|||
|
|
position: relative;
|
|||
|
|
width: 100%;
|
|||
|
|
height: 0;
|
|||
|
|
padding-bottom: 56.25%; /* 16:9 比例 */
|
|||
|
|
background-color: #000;
|
|||
|
|
border-radius: 4px;
|
|||
|
|
overflow: hidden;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.video-element {
|
|||
|
|
position: absolute;
|
|||
|
|
top: 0;
|
|||
|
|
left: 0;
|
|||
|
|
width: 100%;
|
|||
|
|
height: 100%;
|
|||
|
|
object-fit: contain;
|
|||
|
|
display: none;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.video-element.active {
|
|||
|
|
display: block;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.video-placeholder {
|
|||
|
|
position: absolute;
|
|||
|
|
top: 0;
|
|||
|
|
left: 0;
|
|||
|
|
width: 100%;
|
|||
|
|
height: 100%;
|
|||
|
|
display: flex;
|
|||
|
|
flex-direction: column;
|
|||
|
|
justify-content: center;
|
|||
|
|
align-items: center;
|
|||
|
|
color: #fff;
|
|||
|
|
background-color: rgba(0, 0, 0, 0.5);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.status-info {
|
|||
|
|
margin-top: 20px;
|
|||
|
|
}
|
|||
|
|
</style>
|