538 lines
21 KiB
HTML
538 lines
21 KiB
HTML
<!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> |