BodyBalanceEvaluation/frontend_websocket_example.html

538 lines
21 KiB
HTML
Raw Normal View History

2025-07-29 18:28:40 +08:00
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>前端WebSocket连接示例</title>
<script src="https://cdn.socket.io/4.7.2/socket.io.min.js"></script>
<style>
body {
font-family: Arial, sans-serif;
max-width: 800px;
margin: 0 auto;
padding: 20px;
background-color: #f5f5f5;
}
.container {
background: white;
padding: 20px;
border-radius: 8px;
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
}
.status {
padding: 10px;
margin: 10px 0;
border-radius: 4px;
font-weight: bold;
}
.connected { background-color: #d4edda; color: #155724; }
.disconnected { background-color: #f8d7da; color: #721c24; }
.error { background-color: #fff3cd; color: #856404; }
button {
background-color: #007bff;
color: white;
border: none;
padding: 10px 20px;
margin: 5px;
border-radius: 4px;
cursor: pointer;
}
button:hover { background-color: #0056b3; }
button:disabled {
background-color: #6c757d;
cursor: not-allowed;
}
#videoContainer {
margin-top: 20px;
text-align: center;
}
#rtspImage {
max-width: 100%;
height: auto;
border: 2px solid #ddd;
border-radius: 4px;
2025-07-30 13:47:47 +08:00
/* 防止图像缓存 */
image-rendering: auto;
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
2025-07-29 18:28:40 +08:00
}
.log {
background-color: #f8f9fa;
border: 1px solid #dee2e6;
border-radius: 4px;
padding: 10px;
margin-top: 20px;
height: 200px;
overflow-y: auto;
font-family: monospace;
font-size: 12px;
}
</style>
</head>
<body>
<div class="container">
<h1>前端WebSocket连接示例</h1>
<div id="status" class="status disconnected">未连接</div>
<div>
<button id="connectBtn" onclick="connectWebSocket()">连接WebSocket</button>
<button id="disconnectBtn" onclick="disconnectWebSocket()" disabled>断开连接</button>
<button id="startRtspBtn" onclick="startRtsp()" disabled>启动RTSP</button>
<button id="stopRtspBtn" onclick="stopRtsp()" disabled>停止RTSP</button>
</div>
<div style="margin-top: 10px;">
<button onclick="checkBackendStatus()">检查后端状态</button>
<button onclick="testSocketConnection()">测试Socket连接</button>
<button onclick="clearLog()">清空日志</button>
<button onclick="showDebugInfo()">显示调试信息</button>
<button onclick="showMemoryInfo()">内存使用情况</button>
<button onclick="forceGarbageCollection()">强制垃圾回收</button>
2025-07-29 18:28:40 +08:00
</div>
<div id="debugInfo" style="display: none; margin-top: 10px; padding: 10px; background-color: #e9ecef; border-radius: 4px;">
<h4>调试信息</h4>
<div id="debugContent"></div>
</div>
<div id="memoryInfo" style="display: none; margin-top: 10px; padding: 10px; background-color: #fff3cd; border-radius: 4px;">
<h4>内存使用情况</h4>
<div id="memoryContent"></div>
</div>
2025-07-29 18:28:40 +08:00
<div id="videoContainer">
<h3>RTSP视频流</h3>
<img id="rtspImage" src="" alt="RTSP视频流" style="display: none;">
<div id="noVideo">暂无视频流</div>
</div>
<div class="log" id="logContainer"></div>
</div>
<script>
let socket = null;
let frameCount = 0;
let lastFrameTime = 0;
let imageCache = null; // 图像缓存,避免重复创建
let frameSkipCount = 0; // 跳帧计数
2025-07-29 18:28:40 +08:00
// 后端服务器地址配置
const BACKEND_URL = 'http://localhost:5000'; // 根据实际情况修改
// 如果是远程服务器,使用: 'http://192.168.1.173:5000'
// 内存优化配置
const FRAME_SKIP_THRESHOLD = 1; // 每3帧显示1帧
const MAX_FPS = 30; // 最大显示帧率
const MIN_FRAME_INTERVAL = 1000 / MAX_FPS; // 最小帧间隔(ms)
2025-07-29 18:28:40 +08:00
function log(message) {
const logContainer = document.getElementById('logContainer');
const timestamp = new Date().toLocaleTimeString();
logContainer.innerHTML += `[${timestamp}] ${message}<br>`;
logContainer.scrollTop = logContainer.scrollHeight;
}
function updateStatus(status, className) {
const statusEl = document.getElementById('status');
statusEl.textContent = status;
statusEl.className = `status ${className}`;
}
function updateButtons(connected, rtspRunning = false) {
document.getElementById('connectBtn').disabled = connected;
document.getElementById('disconnectBtn').disabled = !connected;
document.getElementById('startRtspBtn').disabled = !connected || rtspRunning;
document.getElementById('stopRtspBtn').disabled = !connected || !rtspRunning;
}
function connectWebSocket() {
try {
log('正在连接到 ' + BACKEND_URL);
// 创建Socket.IO连接
socket = io(BACKEND_URL, {
transports: ['websocket', 'polling'],
timeout: 10000,
forceNew: true
});
// 连接成功事件
socket.on('connect', () => {
log('✅ WebSocket连接成功Socket ID: ' + socket.id);
log('🔍 连接详情: ' + JSON.stringify({
connected: socket.connected,
id: socket.id,
transport: socket.io.engine.transport.name
}));
updateStatus('已连接', 'connected');
updateButtons(true);
});
// 连接失败事件
socket.on('connect_error', (error) => {
log('❌ 连接失败: ' + error.message);
log('🔍 错误详情: ' + JSON.stringify(error));
updateStatus('连接失败', 'error');
updateButtons(false);
});
// 断开连接事件
socket.on('disconnect', (reason) => {
log('⚠️ 连接断开: ' + reason);
updateStatus('已断开', 'disconnected');
updateButtons(false);
hideVideo();
});
// 监听所有事件(调试用)
socket.onAny((eventName, ...args) => {
2025-07-30 13:47:47 +08:00
// log(`📨 收到事件: ${eventName}, 数据: ${JSON.stringify(args)}`);
2025-07-29 18:28:40 +08:00
});
// 监听RTSP状态事件
socket.on('rtsp_status', (data) => {
log('📺 RTSP状态: ' + JSON.stringify(data));
if (data.status === 'started') {
updateButtons(true, true);
} else if (data.status === 'stopped') {
updateButtons(true, false);
hideVideo();
}
});
// 监听RTSP帧数据
socket.on('rtsp_frame', (data) => {
if (data.image) {
frameCount++;
// 帧率控制和跳帧优化
const currentTime = Date.now();
if (currentTime - lastFrameTime < MIN_FRAME_INTERVAL) {
frameSkipCount++;
return; // 跳过此帧以控制帧率
}
// 每隔几帧显示一次,减少内存压力
if (frameCount % (FRAME_SKIP_THRESHOLD + 1) === 0) {
displayFrameOptimized(data.image);
lastFrameTime = currentTime;
}
if (frameCount % 60 === 0) { // 每60帧记录一次
log(`🎬 已接收 ${frameCount} 帧,跳过 ${frameSkipCount} 帧`);
2025-07-29 18:28:40 +08:00
}
2025-07-30 13:47:47 +08:00
2025-07-29 18:28:40 +08:00
} else {
log('⚠️ 收到rtsp_frame事件但无图像数据: ' + JSON.stringify(data));
}
});
// 监听错误事件
socket.on('error', (error) => {
log('❌ Socket错误: ' + JSON.stringify(error));
});
} catch (error) {
log('💥 连接异常: ' + error.message);
log('🔍 异常堆栈: ' + error.stack);
updateStatus('连接异常', 'error');
}
}
function disconnectWebSocket() {
if (socket) {
socket.disconnect();
socket = null;
log('主动断开连接');
updateStatus('已断开', 'disconnected');
updateButtons(false);
hideVideo();
}
}
function startRtsp() {
if (socket && socket.connected) {
log('🚀 发送start_rtsp事件');
log('🔍 Socket状态: ' + JSON.stringify({
connected: socket.connected,
id: socket.id,
transport: socket.io.engine.transport.name
}));
// 发送事件并监听确认
socket.emit('start_rtsp', {}, (ack) => {
if (ack) {
log('✅ start_rtsp事件已确认: ' + JSON.stringify(ack));
} else {
log('⚠️ start_rtsp事件无确认响应');
}
});
frameCount = 0;
// 设置超时检查
setTimeout(() => {
if (frameCount === 0) {
2025-07-30 13:47:47 +08:00
log('⏰ 5秒后仍未收到视频帧可能存在问题');
2025-07-29 18:28:40 +08:00
}
}, 5000);
} else {
log('❌ WebSocket未连接无法启动RTSP');
log('🔍 Socket状态: ' + (socket ? '存在但未连接' : '不存在'));
}
}
function stopRtsp() {
if (socket && socket.connected) {
log('🛑 发送stop_rtsp事件');
socket.emit('stop_rtsp', {}, (ack) => {
if (ack) {
log('✅ stop_rtsp事件已确认: ' + JSON.stringify(ack));
} else {
log('⚠️ stop_rtsp事件无确认响应');
}
});
} else {
log('❌ WebSocket未连接无法停止RTSP');
}
}
// 优化的帧显示函数,减少内存泄漏
function displayFrameOptimized(base64Image) {
2025-07-29 18:28:40 +08:00
const img = document.getElementById('rtspImage');
const noVideo = document.getElementById('noVideo');
try {
// 直接设置图像源避免创建额外的Image对象
const dataUrl = 'data:image/jpeg;base64,' + base64Image;
2025-07-30 13:47:47 +08:00
// 清理之前的图像缓存
if (imageCache) {
imageCache.src = '';
imageCache = null;
}
// 使用requestAnimationFrame优化渲染
requestAnimationFrame(() => {
img.src = dataUrl;
img.style.display = 'block';
noVideo.style.display = 'none';
});
// 定期清理base64数据URL缓存
if (frameCount % 100 === 0) {
// 强制垃圾回收(如果浏览器支持)
if (window.gc) {
window.gc();
}
}
} catch (error) {
console.error('显示帧失败:', error);
log('❌ 显示帧失败: ' + error.message);
}
}
// 保留原始函数作为备用
function displayFrame(base64Image) {
displayFrameOptimized(base64Image);
2025-07-29 18:28:40 +08:00
}
function hideVideo() {
const img = document.getElementById('rtspImage');
const noVideo = document.getElementById('noVideo');
// 清理图像资源
2025-07-29 18:28:40 +08:00
img.style.display = 'none';
img.src = '';
noVideo.style.display = 'block';
// 清理缓存
if (imageCache) {
imageCache.src = '';
imageCache = null;
}
// 重置计数器
frameCount = 0;
frameSkipCount = 0;
lastFrameTime = 0;
// 强制垃圾回收
if (window.gc) {
window.gc();
}
2025-07-29 18:28:40 +08:00
}
// 检查后端服务状态
function checkBackendStatus() {
log('🔍 检查后端服务状态...');
fetch(BACKEND_URL + '/health')
.then(response => {
if (response.ok) {
log('✅ 后端HTTP服务正常');
return response.text();
} else {
log('⚠️ 后端HTTP服务响应异常: ' + response.status);
}
})
.then(data => {
if (data) {
log('📄 后端响应: ' + data);
}
})
.catch(error => {
log('❌ 无法连接到后端服务: ' + error.message);
log('💡 请检查后端服务是否已启动,地址是否正确');
});
}
// 测试Socket.IO连接
function testSocketConnection() {
log('🧪 测试Socket.IO连接...');
const testSocket = io(BACKEND_URL + '/socket.io/', {
transports: ['polling'],
timeout: 5000
});
testSocket.on('connect', () => {
log('✅ Socket.IO测试连接成功');
testSocket.disconnect();
});
testSocket.on('connect_error', (error) => {
log('❌ Socket.IO测试连接失败: ' + error.message);
});
}
// 清空日志
function clearLog() {
document.getElementById('logContainer').innerHTML = '';
log('🧹 日志已清空');
}
// 显示调试信息
function showDebugInfo() {
const debugInfo = document.getElementById('debugInfo');
const debugContent = document.getElementById('debugContent');
let info = '<strong>当前状态:</strong><br>';
info += `Socket对象: ${socket ? '存在' : '不存在'}<br>`;
if (socket) {
info += `连接状态: ${socket.connected ? '已连接' : '未连接'}<br>`;
info += `Socket ID: ${socket.id || '无'}<br>`;
info += `传输方式: ${socket.io?.engine?.transport?.name || '未知'}<br>`;
info += `URL: ${socket.io?.uri || '未知'}<br>`;
}
info += `<br><strong>配置信息:</strong><br>`;
info += `后端地址: ${BACKEND_URL}<br>`;
info += `接收帧数: ${frameCount}<br>`;
info += `页面URL: ${window.location.href}<br>`;
info += `用户代理: ${navigator.userAgent}<br>`;
debugContent.innerHTML = info;
debugInfo.style.display = debugInfo.style.display === 'none' ? 'block' : 'none';
}
// 显示内存使用情况
function showMemoryInfo() {
const memoryInfo = document.getElementById('memoryInfo');
const memoryContent = document.getElementById('memoryContent');
let info = '<strong>内存使用情况:</strong><br>';
// 检查浏览器内存API支持
if (performance.memory) {
const memory = performance.memory;
info += `已使用内存: ${(memory.usedJSHeapSize / 1024 / 1024).toFixed(2)} MB<br>`;
info += `总分配内存: ${(memory.totalJSHeapSize / 1024 / 1024).toFixed(2)} MB<br>`;
info += `内存限制: ${(memory.jsHeapSizeLimit / 1024 / 1024).toFixed(2)} MB<br>`;
info += `内存使用率: ${((memory.usedJSHeapSize / memory.jsHeapSizeLimit) * 100).toFixed(2)}%<br>`;
} else {
info += '浏览器不支持内存API<br>';
}
info += `<br><strong>优化状态:</strong><br>`;
info += `接收帧数: ${frameCount}<br>`;
info += `跳过帧数: ${frameSkipCount}<br>`;
info += `跳帧率: ${frameCount > 0 ? ((frameSkipCount / frameCount) * 100).toFixed(2) : 0}%<br>`;
info += `最大FPS限制: ${MAX_FPS}<br>`;
info += `帧跳过阈值: 每${FRAME_SKIP_THRESHOLD + 1}帧显示1帧<br>`;
memoryContent.innerHTML = info;
memoryInfo.style.display = memoryInfo.style.display === 'none' ? 'block' : 'none';
}
// 强制垃圾回收
function forceGarbageCollection() {
log('🧹 尝试强制垃圾回收...');
// 清理可能的内存泄漏
if (imageCache) {
imageCache.src = '';
imageCache = null;
}
// 强制垃圾回收(如果浏览器支持)
if (window.gc) {
window.gc();
log('✅ 强制垃圾回收完成');
} else {
log('⚠️ 浏览器不支持强制垃圾回收');
}
// 显示内存使用情况
setTimeout(() => {
showMemoryInfo();
}, 100);
}
// 定期内存监控
function startMemoryMonitoring() {
setInterval(() => {
if (performance.memory && frameCount > 0) {
const memory = performance.memory;
const usagePercent = (memory.usedJSHeapSize / memory.jsHeapSizeLimit) * 100;
// 如果内存使用率超过80%,自动进行垃圾回收
if (usagePercent > 80) {
log(`⚠️ 内存使用率过高: ${usagePercent.toFixed(2)}%,自动清理...`);
forceGarbageCollection();
}
}
}, 10000); // 每10秒检查一次
}
2025-07-29 18:28:40 +08:00
// 页面加载完成后的初始化
window.onload = function() {
log('📄 页面加载完成可以开始连接WebSocket');
log('🌐 后端地址: ' + BACKEND_URL);
log('⚠️ 请确保后端服务已启动');
log('🔧 内存优化已启用: 最大FPS=' + MAX_FPS + ', 跳帧阈值=' + (FRAME_SKIP_THRESHOLD + 1));
// 启动内存监控
startMemoryMonitoring();
2025-07-29 18:28:40 +08:00
// 自动检查后端状态
2025-07-30 13:47:47 +08:00
setTimeout(() => {
2025-07-29 18:28:40 +08:00
testSocketConnection();
}, 1000);
};
// 页面关闭时清理连接
window.onbeforeunload = function() {
if (socket) {
socket.disconnect();
}
};
</script>
</body>
</html>