BodyBalanceEvaluation/frontend_websocket_example.html
2025-07-30 19:09:15 +08:00

538 lines
21 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!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;
/* 防止图像缓存 */
image-rendering: auto;
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
}
.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>
</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>
<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; // 跳帧计数
// 后端服务器地址配置
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)
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) => {
// log(`📨 收到事件: ${eventName}, 数据: ${JSON.stringify(args)}`);
});
// 监听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}`);
}
} 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) {
log('⏰ 5秒后仍未收到视频帧可能存在问题');
}
}, 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) {
const img = document.getElementById('rtspImage');
const noVideo = document.getElementById('noVideo');
try {
// 直接设置图像源避免创建额外的Image对象
const dataUrl = 'data:image/jpeg;base64,' + base64Image;
// 清理之前的图像缓存
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);
}
function hideVideo() {
const img = document.getElementById('rtspImage');
const noVideo = document.getElementById('noVideo');
// 清理图像资源
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();
}
}
// 检查后端服务状态
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秒检查一次
}
// 页面加载完成后的初始化
window.onload = function() {
log('📄 页面加载完成可以开始连接WebSocket');
log('🌐 后端地址: ' + BACKEND_URL);
log('⚠️ 请确保后端服务已启动');
log('🔧 内存优化已启用: 最大FPS=' + MAX_FPS + ', 跳帧阈值=' + (FRAME_SKIP_THRESHOLD + 1));
// 启动内存监控
startMemoryMonitoring();
// 自动检查后端状态
setTimeout(() => {
testSocketConnection();
}, 1000);
};
// 页面关闭时清理连接
window.onbeforeunload = function() {
if (socket) {
socket.disconnect();
}
};
</script>
</body>
</html>