421 lines
8.3 KiB
Vue
421 lines
8.3 KiB
Vue
<template>
|
||
<div class="websocket-example">
|
||
<div class="header">
|
||
<h2>Vue WebSocket连接示例</h2>
|
||
<div class="status" :class="connectionStatus">
|
||
{{ statusText }}
|
||
</div>
|
||
</div>
|
||
|
||
<div class="controls">
|
||
<button
|
||
@click="connectWebSocket"
|
||
:disabled="isConnected"
|
||
class="btn btn-primary"
|
||
>
|
||
连接WebSocket
|
||
</button>
|
||
|
||
<button
|
||
@click="disconnectWebSocket"
|
||
:disabled="!isConnected"
|
||
class="btn btn-secondary"
|
||
>
|
||
断开连接
|
||
</button>
|
||
|
||
<button
|
||
@click="startRtsp"
|
||
:disabled="!isConnected || isRtspRunning"
|
||
class="btn btn-success"
|
||
>
|
||
启动RTSP
|
||
</button>
|
||
|
||
<button
|
||
@click="stopRtsp"
|
||
:disabled="!isConnected || !isRtspRunning"
|
||
class="btn btn-danger"
|
||
>
|
||
停止RTSP
|
||
</button>
|
||
</div>
|
||
|
||
<div class="video-container">
|
||
<h3>RTSP视频流</h3>
|
||
<div v-if="rtspImageSrc" class="video-wrapper">
|
||
<img :src="rtspImageSrc" alt="RTSP视频流" class="rtsp-image" />
|
||
<div class="frame-info">已接收帧数: {{ frameCount }}</div>
|
||
</div>
|
||
<div v-else class="no-video">
|
||
暂无视频流
|
||
</div>
|
||
</div>
|
||
|
||
<div class="log-container">
|
||
<h3>连接日志</h3>
|
||
<div class="log-content" ref="logContainer">
|
||
<div v-for="(log, index) in logs" :key="index" class="log-item">
|
||
<span class="log-time">[{{ log.time }}]</span>
|
||
<span class="log-message">{{ log.message }}</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</template>
|
||
|
||
<script setup>
|
||
import { ref, computed, onMounted, onUnmounted, nextTick } from 'vue'
|
||
import { io } from 'socket.io-client'
|
||
|
||
// 响应式数据
|
||
const socket = ref(null)
|
||
const isConnected = ref(false)
|
||
const isRtspRunning = ref(false)
|
||
const rtspImageSrc = ref('')
|
||
const frameCount = ref(0)
|
||
const logs = ref([])
|
||
const logContainer = ref(null)
|
||
|
||
// 后端服务器配置
|
||
const BACKEND_URL = 'http://localhost:5000' // 根据实际情况修改
|
||
// 如果是远程服务器,使用: 'http://192.168.1.173:5000'
|
||
|
||
// 计算属性
|
||
const connectionStatus = computed(() => {
|
||
if (isConnected.value) return 'connected'
|
||
return 'disconnected'
|
||
})
|
||
|
||
const statusText = computed(() => {
|
||
if (isConnected.value) {
|
||
return isRtspRunning.value ? '已连接 - RTSP运行中' : '已连接'
|
||
}
|
||
return '未连接'
|
||
})
|
||
|
||
// 日志记录函数
|
||
const addLog = (message) => {
|
||
const now = new Date()
|
||
const time = now.toLocaleTimeString()
|
||
logs.value.push({ time, message })
|
||
|
||
// 限制日志数量
|
||
if (logs.value.length > 100) {
|
||
logs.value.shift()
|
||
}
|
||
|
||
// 自动滚动到底部
|
||
nextTick(() => {
|
||
if (logContainer.value) {
|
||
logContainer.value.scrollTop = logContainer.value.scrollHeight
|
||
}
|
||
})
|
||
}
|
||
|
||
// WebSocket连接函数
|
||
const connectWebSocket = () => {
|
||
try {
|
||
addLog(`正在连接到 ${BACKEND_URL}`)
|
||
|
||
// 创建Socket.IO连接
|
||
socket.value = io(BACKEND_URL, {
|
||
transports: ['websocket', 'polling'],
|
||
timeout: 10000,
|
||
forceNew: true
|
||
})
|
||
|
||
// 连接成功事件
|
||
socket.value.on('connect', () => {
|
||
isConnected.value = true
|
||
addLog(`WebSocket连接成功!Socket ID: ${socket.value.id}`)
|
||
})
|
||
|
||
// 连接失败事件
|
||
socket.value.on('connect_error', (error) => {
|
||
isConnected.value = false
|
||
addLog(`连接失败: ${error.message}`)
|
||
})
|
||
|
||
// 断开连接事件
|
||
socket.value.on('disconnect', (reason) => {
|
||
isConnected.value = false
|
||
isRtspRunning.value = false
|
||
rtspImageSrc.value = ''
|
||
addLog(`连接断开: ${reason}`)
|
||
})
|
||
|
||
// 监听RTSP状态事件
|
||
socket.value.on('rtsp_status', (data) => {
|
||
addLog(`RTSP状态: ${JSON.stringify(data)}`)
|
||
if (data.status === 'started') {
|
||
isRtspRunning.value = true
|
||
frameCount.value = 0
|
||
} else if (data.status === 'stopped') {
|
||
isRtspRunning.value = false
|
||
rtspImageSrc.value = ''
|
||
} else if (data.status === 'already_running') {
|
||
addLog('RTSP已在运行中')
|
||
}
|
||
})
|
||
|
||
// 监听RTSP帧数据
|
||
socket.value.on('rtsp_frame', (data) => {
|
||
if (data.image) {
|
||
frameCount.value++
|
||
rtspImageSrc.value = 'data:image/jpeg;base64,' + data.image
|
||
|
||
// 每30帧记录一次
|
||
if (frameCount.value % 30 === 0) {
|
||
addLog(`已接收 ${frameCount.value} 帧`)
|
||
}
|
||
}
|
||
})
|
||
|
||
} catch (error) {
|
||
addLog(`连接异常: ${error.message}`)
|
||
}
|
||
}
|
||
|
||
// 断开WebSocket连接
|
||
const disconnectWebSocket = () => {
|
||
if (socket.value) {
|
||
socket.value.disconnect()
|
||
socket.value = null
|
||
isConnected.value = false
|
||
isRtspRunning.value = false
|
||
rtspImageSrc.value = ''
|
||
addLog('主动断开连接')
|
||
}
|
||
}
|
||
|
||
// 启动RTSP
|
||
const startRtsp = () => {
|
||
if (socket.value && isConnected.value) {
|
||
addLog('发送start_rtsp事件')
|
||
socket.value.emit('start_rtsp')
|
||
} else {
|
||
addLog('WebSocket未连接,无法启动RTSP')
|
||
}
|
||
}
|
||
|
||
// 停止RTSP
|
||
const stopRtsp = () => {
|
||
if (socket.value && isConnected.value) {
|
||
addLog('发送stop_rtsp事件')
|
||
socket.value.emit('stop_rtsp')
|
||
} else {
|
||
addLog('WebSocket未连接,无法停止RTSP')
|
||
}
|
||
}
|
||
|
||
// 生命周期钩子
|
||
onMounted(() => {
|
||
addLog('组件已挂载,可以开始连接WebSocket')
|
||
addLog(`后端地址: ${BACKEND_URL}`)
|
||
addLog('请确保后端服务已启动')
|
||
})
|
||
|
||
onUnmounted(() => {
|
||
// 组件卸载时清理连接
|
||
if (socket.value) {
|
||
socket.value.disconnect()
|
||
}
|
||
})
|
||
</script>
|
||
|
||
<style scoped>
|
||
.websocket-example {
|
||
max-width: 1000px;
|
||
margin: 0 auto;
|
||
padding: 20px;
|
||
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
||
}
|
||
|
||
.header {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
margin-bottom: 20px;
|
||
padding-bottom: 15px;
|
||
border-bottom: 2px solid #e9ecef;
|
||
}
|
||
|
||
.header h2 {
|
||
margin: 0;
|
||
color: #333;
|
||
}
|
||
|
||
.status {
|
||
padding: 8px 16px;
|
||
border-radius: 20px;
|
||
font-weight: bold;
|
||
font-size: 14px;
|
||
}
|
||
|
||
.status.connected {
|
||
background-color: #d4edda;
|
||
color: #155724;
|
||
border: 1px solid #c3e6cb;
|
||
}
|
||
|
||
.status.disconnected {
|
||
background-color: #f8d7da;
|
||
color: #721c24;
|
||
border: 1px solid #f5c6cb;
|
||
}
|
||
|
||
.controls {
|
||
display: flex;
|
||
gap: 10px;
|
||
margin-bottom: 20px;
|
||
flex-wrap: wrap;
|
||
}
|
||
|
||
.btn {
|
||
padding: 10px 20px;
|
||
border: none;
|
||
border-radius: 5px;
|
||
cursor: pointer;
|
||
font-size: 14px;
|
||
font-weight: 500;
|
||
transition: all 0.3s ease;
|
||
}
|
||
|
||
.btn:disabled {
|
||
opacity: 0.6;
|
||
cursor: not-allowed;
|
||
}
|
||
|
||
.btn-primary {
|
||
background-color: #007bff;
|
||
color: white;
|
||
}
|
||
|
||
.btn-primary:hover:not(:disabled) {
|
||
background-color: #0056b3;
|
||
}
|
||
|
||
.btn-secondary {
|
||
background-color: #6c757d;
|
||
color: white;
|
||
}
|
||
|
||
.btn-secondary:hover:not(:disabled) {
|
||
background-color: #545b62;
|
||
}
|
||
|
||
.btn-success {
|
||
background-color: #28a745;
|
||
color: white;
|
||
}
|
||
|
||
.btn-success:hover:not(:disabled) {
|
||
background-color: #1e7e34;
|
||
}
|
||
|
||
.btn-danger {
|
||
background-color: #dc3545;
|
||
color: white;
|
||
}
|
||
|
||
.btn-danger:hover:not(:disabled) {
|
||
background-color: #c82333;
|
||
}
|
||
|
||
.video-container {
|
||
margin-bottom: 20px;
|
||
padding: 20px;
|
||
background-color: #f8f9fa;
|
||
border-radius: 8px;
|
||
border: 1px solid #dee2e6;
|
||
}
|
||
|
||
.video-container h3 {
|
||
margin-top: 0;
|
||
margin-bottom: 15px;
|
||
color: #333;
|
||
}
|
||
|
||
.video-wrapper {
|
||
text-align: center;
|
||
position: relative;
|
||
}
|
||
|
||
.rtsp-image {
|
||
max-width: 100%;
|
||
height: auto;
|
||
border: 2px solid #ddd;
|
||
border-radius: 8px;
|
||
box-shadow: 0 4px 8px rgba(0,0,0,0.1);
|
||
}
|
||
|
||
.frame-info {
|
||
margin-top: 10px;
|
||
font-size: 14px;
|
||
color: #666;
|
||
font-weight: 500;
|
||
}
|
||
|
||
.no-video {
|
||
text-align: center;
|
||
padding: 40px;
|
||
color: #6c757d;
|
||
font-style: italic;
|
||
background-color: #e9ecef;
|
||
border-radius: 8px;
|
||
}
|
||
|
||
.log-container {
|
||
margin-top: 20px;
|
||
}
|
||
|
||
.log-container h3 {
|
||
margin-bottom: 10px;
|
||
color: #333;
|
||
}
|
||
|
||
.log-content {
|
||
background-color: #f8f9fa;
|
||
border: 1px solid #dee2e6;
|
||
border-radius: 8px;
|
||
padding: 15px;
|
||
height: 200px;
|
||
overflow-y: auto;
|
||
font-family: 'Courier New', monospace;
|
||
font-size: 12px;
|
||
line-height: 1.4;
|
||
}
|
||
|
||
.log-item {
|
||
margin-bottom: 5px;
|
||
word-wrap: break-word;
|
||
}
|
||
|
||
.log-time {
|
||
color: #6c757d;
|
||
font-weight: bold;
|
||
}
|
||
|
||
.log-message {
|
||
margin-left: 8px;
|
||
color: #333;
|
||
}
|
||
|
||
/* 响应式设计 */
|
||
@media (max-width: 768px) {
|
||
.header {
|
||
flex-direction: column;
|
||
align-items: flex-start;
|
||
gap: 10px;
|
||
}
|
||
|
||
.controls {
|
||
flex-direction: column;
|
||
}
|
||
|
||
.btn {
|
||
width: 100%;
|
||
}
|
||
}
|
||
</style> |