serverRoom/test-h264-stream.html
lvfengfree ed9d1d7325 feat: 屏幕监控大规模优化 - 支持60台设备同时监控
- Agent端优化:
  * 添加质量档位定义 (Low: 320x180@3fps, High: 1280x720@15fps)
  * H.264编码器支持动态质量切换
  * 屏幕流服务支持按需推流和质量控制
  * 添加SignalR信令客户端连接服务器

- 服务器端优化:
  * 添加StreamSignalingHub处理质量控制信令
  * 支持设备注册/注销和监控状态管理
  * 支持教师端监控控制和设备选中

- 前端组件:
  * 创建H264VideoPlayer组件支持H.264和JPEG模式
  * 更新学生屏幕监控页面使用新组件

- 性能提升:
  * 带宽从120Mbps降至6-7Mbps (降低95%)
  * 监控墙模式: 60台100kbps=6Mbps
  * 单机放大模式: 1台1Mbps+59台100kbps=6.9Mbps
  * 无人观看时停止推流节省带宽
2026-01-23 15:37:37 +08:00

291 lines
9.1 KiB
HTML

<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>H.264 视频流测试</title>
<style>
body {
font-family: Arial, sans-serif;
margin: 20px;
background: #f0f0f0;
}
.container {
max-width: 1400px;
margin: 0 auto;
background: white;
padding: 20px;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
}
h1 {
color: #333;
}
.controls {
margin-bottom: 20px;
}
button {
padding: 10px 20px;
margin-right: 10px;
background: #409eff;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
}
button:hover {
background: #66b1ff;
}
.video-container {
position: relative;
width: 100%;
height: 720px;
background: #000;
display: flex;
align-items: center;
justify-content: center;
}
video {
max-width: 100%;
max-height: 100%;
}
.status {
position: absolute;
top: 10px;
left: 10px;
background: rgba(0,0,0,0.7);
color: white;
padding: 10px;
border-radius: 4px;
font-size: 14px;
}
.log {
margin-top: 20px;
padding: 10px;
background: #f5f5f5;
border-radius: 4px;
max-height: 300px;
overflow-y: auto;
font-family: monospace;
font-size: 12px;
}
.log-entry {
margin: 2px 0;
}
.log-info { color: #409eff; }
.log-error { color: #f56c6c; }
.log-success { color: #67c23a; }
</style>
</head>
<body>
<div class="container">
<h1>H.264 视频流测试</h1>
<div class="controls">
<button onclick="connect()">连接</button>
<button onclick="disconnect()">断开</button>
<button onclick="clearLog()">清空日志</button>
</div>
<div class="video-container">
<video id="video" autoplay muted playsinline></video>
<div class="status" id="status">未连接</div>
</div>
<div class="log" id="log"></div>
</div>
<script>
const DEVICE_IP = '192.168.8.111';
const WS_PORT = 9100;
let ws = null;
let mediaSource = null;
let sourceBuffer = null;
let queue = [];
let isJpegMode = false;
let lastImageUrl = '';
const video = document.getElementById('video');
const statusEl = document.getElementById('status');
const logEl = document.getElementById('log');
function log(message, type = 'info') {
const entry = document.createElement('div');
entry.className = `log-entry log-${type}`;
entry.textContent = `[${new Date().toLocaleTimeString()}] ${message}`;
logEl.appendChild(entry);
logEl.scrollTop = logEl.scrollHeight;
console.log(message);
}
function updateStatus(text, color = '#409eff') {
statusEl.textContent = text;
statusEl.style.background = `rgba(${color === '#67c23a' ? '103,194,58' : color === '#f56c6c' ? '245,108,108' : '64,158,255'},0.7)`;
}
function clearLog() {
logEl.innerHTML = '';
}
async function connect() {
if (ws) {
log('已有连接,先断开', 'info');
disconnect();
}
const wsUrl = `ws://${DEVICE_IP}:${WS_PORT}/`;
log(`正在连接到: ${wsUrl}`, 'info');
updateStatus('正在连接...', '#409eff');
ws = new WebSocket(wsUrl);
ws.binaryType = 'arraybuffer';
ws.onopen = () => {
log('WebSocket 连接成功', 'success');
updateStatus('已连接', '#67c23a');
};
ws.onmessage = async (event) => {
if (typeof event.data === 'string') {
// 初始化消息
try {
const init = JSON.parse(event.data);
log(`收到初始化消息: ${JSON.stringify(init)}`, 'success');
if (init.mode === 'h264') {
isJpegMode = false;
log('使用 H.264 模式', 'info');
await initH264Player();
} else {
isJpegMode = true;
log('使用 JPEG 模式', 'info');
initJpegPlayer();
}
} catch (e) {
log(`解析初始化消息失败: ${e.message}`, 'error');
}
} else {
// 二进制数据
const size = event.data.byteLength;
log(`收到帧数据: ${(size / 1024).toFixed(2)} KB`, 'info');
handleFrame(new Uint8Array(event.data));
}
};
ws.onerror = (error) => {
log(`WebSocket 错误: ${error}`, 'error');
updateStatus('连接错误', '#f56c6c');
};
ws.onclose = () => {
log('WebSocket 已断开', 'info');
updateStatus('已断开', '#909399');
};
}
async function initH264Player() {
try {
mediaSource = new MediaSource();
video.src = URL.createObjectURL(mediaSource);
await new Promise((resolve) => {
mediaSource.addEventListener('sourceopen', resolve, { once: true });
});
const codec = 'video/mp4; codecs="avc1.42E01E"';
if (!MediaSource.isTypeSupported(codec)) {
log(`不支持的编解码器: ${codec}`, 'error');
updateStatus('浏览器不支持 H.264', '#f56c6c');
return;
}
sourceBuffer = mediaSource.addSourceBuffer(codec);
sourceBuffer.mode = 'sequence';
sourceBuffer.addEventListener('updateend', () => {
if (queue.length > 0 && !sourceBuffer.updating) {
const data = queue.shift();
sourceBuffer.appendBuffer(data);
}
});
log('H.264 播放器初始化成功', 'success');
} catch (error) {
log(`初始化 H.264 播放器失败: ${error.message}`, 'error');
updateStatus('初始化失败', '#f56c6c');
}
}
function initJpegPlayer() {
log('JPEG 播放器初始化成功', 'success');
video.style.display = 'none';
}
function handleFrame(data) {
if (!isJpegMode && sourceBuffer) {
// H.264 模式
if (sourceBuffer.updating || queue.length > 0) {
queue.push(data);
if (queue.length > 30) {
log(`队列过长 (${queue.length}),丢弃旧帧`, 'info');
queue.shift();
}
} else {
try {
sourceBuffer.appendBuffer(data);
} catch (error) {
log(`添加缓冲区失败: ${error.message}`, 'error');
}
}
} else {
// JPEG 模式
const blob = new Blob([data], { type: 'image/jpeg' });
const url = URL.createObjectURL(blob);
if (lastImageUrl) {
URL.revokeObjectURL(lastImageUrl);
}
lastImageUrl = url;
video.poster = url;
video.style.display = 'block';
}
}
function disconnect() {
if (ws) {
ws.close();
ws = null;
}
if (mediaSource) {
if (mediaSource.readyState === 'open') {
mediaSource.endOfStream();
}
mediaSource = null;
}
sourceBuffer = null;
queue = [];
if (lastImageUrl) {
URL.revokeObjectURL(lastImageUrl);
lastImageUrl = '';
}
log('已断开连接', 'info');
updateStatus('未连接', '#909399');
}
// 页面加载时自动连接
window.addEventListener('load', () => {
log('页面加载完成', 'info');
setTimeout(connect, 500);
});
// 页面卸载时断开连接
window.addEventListener('beforeunload', disconnect);
</script>
</body>
</html>