Files
SupervisorAI/web_page/monitor_dashboard.html
2026-01-15 11:58:26 +08:00

674 lines
24 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>监控大屏 - 4路直播</title>
<link href="https://vjs.zencdn.net/7.20.3/video-js.css" rel="stylesheet" />
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
background: #000;
color: #fff;
overflow: hidden;
height: 100vh;
}
.header {
position: fixed;
top: 0;
left: 0;
right: 0;
height: 60px;
background: linear-gradient(90deg, #1a1a2e 0%, #16213e 50%, #0f3460 100%);
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 20px;
z-index: 1000;
border-bottom: 2px solid #00b4d8;
}
.header-title {
font-size: 24px;
font-weight: bold;
color: #00b4d8;
text-shadow: 0 0 10px rgba(0, 180, 216, 0.5);
}
.header-info {
display: flex;
align-items: center;
gap: 20px;
font-size: 14px;
}
.time-display {
background: rgba(0, 180, 216, 0.1);
padding: 8px 16px;
border-radius: 20px;
border: 1px solid rgba(0, 180, 216, 0.3);
}
.monitor-container {
margin-top: 60px;
height: calc(100vh - 60px);
display: grid;
grid-template-columns: 1fr 1fr;
grid-template-rows: 1fr 1fr;
gap: 2px;
background: #000;
}
.monitor-screen {
background: #111;
position: relative;
overflow: hidden;
border: 1px solid #333;
}
.monitor-header {
position: absolute;
top: 0;
left: 0;
right: 0;
height: 40px;
background: rgba(0, 0, 0, 0.8);
display: flex;
align-items: center;
padding: 0 15px;
z-index: 100;
border-bottom: 1px solid #00b4d8;
}
.screen-title {
font-size: 16px;
font-weight: bold;
color: #00b4d8;
}
.screen-status {
margin-left: auto;
font-size: 12px;
color: #4ade80;
display: flex;
align-items: center;
gap: 5px;
}
.status-dot {
width: 8px;
height: 8px;
border-radius: 50%;
background: #4ade80;
animation: pulse 2s infinite;
}
.video-container {
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
}
.video-js {
width: 100% !important;
height: 100% !important;
background: #000;
}
.loading-overlay {
position: absolute;
top: 40px;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.8);
display: none; /* 默认隐藏 */
align-items: center;
justify-content: center;
flex-direction: column;
z-index: 50;
}
.loading-text {
color: #00b4d8;
font-size: 18px;
margin-top: 15px;
}
.spinner {
width: 40px;
height: 40px;
border: 4px solid rgba(0, 180, 216, 0.3);
border-top: 4px solid #00b4d8;
border-radius: 50%;
animation: spin 1s linear infinite;
}
.error-overlay {
position: absolute;
top: 40px;
left: 0;
right: 0;
bottom: 0;
background: rgba(139, 0, 0, 0.8);
display: none;
align-items: center;
justify-content: center;
flex-direction: column;
z-index: 50;
}
.error-text {
color: #ff6b6b;
font-size: 16px;
margin-top: 15px;
text-align: center;
}
.refresh-btn {
margin-top: 10px;
padding: 8px 16px;
background: #ff6b6b;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 14px;
}
.refresh-btn:hover {
background: #ff5252;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
@keyframes pulse {
0%, 100% { opacity: 1; }
50% { opacity: 0.5; }
}
/* 隐藏视频播放器的控制条 */
.video-js .vjs-control-bar {
display: none !important;
}
/* 隐藏播放器自带的错误显示 */
.video-js .vjs-error-display {
display: none !important;
}
/* 隐藏播放器自带的错误提示文字 */
.video-js .vjs-modal-dialog {
display: none !important;
}
/* 全屏模式下的样式调整 */
.monitor-screen.fullscreen {
position: fixed;
top: 0;
left: 0;
width: 100vw !important;
height: 100vh !important;
z-index: 2000;
border: none;
}
.fullscreen-btn {
position: absolute;
top: 10px;
right: 10px;
background: rgba(0, 0, 0, 0.7);
color: white;
border: none;
border-radius: 4px;
padding: 5px 10px;
cursor: pointer;
font-size: 12px;
z-index: 101;
}
.fullscreen-btn:hover {
background: rgba(0, 180, 216, 0.8);
}
</style>
</head>
<body>
<div class="header">
<div class="header-title">智能监控大屏系统</div>
<div class="header-info">
<div class="time-display" id="currentTime">--:--:--</div>
<div>4路高清直播</div>
</div>
</div>
<div class="monitor-container">
<!-- 屏幕1 -->
<div class="monitor-screen" id="screen1">
<div class="monitor-header">
<div class="screen-title">监控点 1 - 主入口</div>
<div class="screen-status">
<div class="status-dot"></div>
<span>直播中</span>
</div>
</div>
<div class="video-container">
<video
id="video1"
class="video-js vjs-default-skin"
controls
preload="auto">
<source src="" type="application/x-mpegURL">
</video>
<div class="loading-overlay" id="loading1">
<div class="spinner"></div>
<div class="loading-text">正在连接直播流...</div>
</div>
<div class="error-overlay" id="error1">
<div style="color: #ff6b6b; font-size: 24px;"></div>
<div class="error-text">直播流连接失败</div>
<button class="refresh-btn" onclick="refreshStream(1)">重新连接</button>
</div>
</div>
<button class="fullscreen-btn" onclick="toggleFullscreen(1)">全屏</button>
</div>
<!-- 屏幕2 -->
<div class="monitor-screen" id="screen2">
<div class="monitor-header">
<div class="screen-title">监控点 2 - 走廊</div>
<div class="screen-status">
<div class="status-dot"></div>
<span>直播中</span>
</div>
</div>
<div class="video-container">
<video
id="video2"
class="video-js vjs-default-skin"
controls
preload="auto">
<source src="" type="application/x-mpegURL">
</video>
<div class="loading-overlay" id="loading2">
<div class="spinner"></div>
<div class="loading-text">正在连接直播流...</div>
</div>
<div class="error-overlay" id="error2">
<div style="color: #ff6b6b; font-size: 24px;"></div>
<div class="error-text">直播流连接失败</div>
<button class="refresh-btn" onclick="refreshStream(2)">重新连接</button>
</div>
</div>
<button class="fullscreen-btn" onclick="toggleFullscreen(2)">全屏</button>
</div>
<!-- 屏幕3 -->
<div class="monitor-screen" id="screen3">
<div class="monitor-header">
<div class="screen-title">监控点 3 - 大厅</div>
<div class="screen-status">
<div class="status-dot"></div>
<span>直播中</span>
</div>
</div>
<div class="video-container">
<video
id="video3"
class="video-js vjs-default-skin"
controls
preload="auto">
<source src="" type="application/x-mpegURL">
</video>
<div class="loading-overlay" id="loading3">
<div class="spinner"></div>
<div class="loading-text">正在连接直播流...</div>
</div>
<div class="error-overlay" id="error3">
<div style="color: #ff6b6b; font-size: 24px;"></div>
<div class="error-text">直播流连接失败</div>
<button class="refresh-btn" onclick="refreshStream(3)">重新连接</button>
</div>
</div>
<button class="fullscreen-btn" onclick="toggleFullscreen(3)">全屏</button>
</div>
<!-- 屏幕4 -->
<div class="monitor-screen" id="screen4">
<div class="monitor-header">
<div class="screen-title">监控点 4 - 出口</div>
<div class="screen-status">
<div class="status-dot"></div>
<span>直播中</span>
</div>
</div>
<div class="video-container">
<video
id="video4"
class="video-js vjs-default-skin"
controls
preload="auto">
<source src="" type="application/x-mpegURL">
</video>
<div class="loading-overlay" id="loading4">
<div class="spinner"></div>
<div class="loading-text">正在连接直播流...</div>
</div>
<div class="error-overlay" id="error4">
<div style="color: #ff6b6b; font-size: 24px;"></div>
<div class="error-text">直播流连接失败</div>
<button class="refresh-btn" onclick="refreshStream(4)">重新连接</button>
</div>
</div>
<button class="fullscreen-btn" onclick="toggleFullscreen(4)">全屏</button>
</div>
</div>
<script src="https://vjs.zencdn.net/7.20.3/video.min.js"></script>
<script>
// 配置直播流地址 - 请根据实际情况修改这些URL
const streamConfigs = {
1: {
url: 'https://demo.unified-streaming.com/k8s/live/scte35.isml/.m3u8', // 请替换为实际的m3u8地址
title: '监控点 1 - 主入口'
},
2: {
url: 'https://stream-akamai.castr.com/5b9352dbda7b8c769937e459/live_2361c920455111ea85db6911fe397b9e/index.fmp4.m3u8', // 请替换为实际的m3u8地址
title: '监控点 2 - 走廊'
},
3: {
url: 'http://example.com/stream3.m3u8', // 请替换为实际的m3u8地址
title: '监控点 3 - 大厅'
},
4: {
url: 'http://example.com/stream4.m3u8', // 请替换为实际的m3u8地址
title: '监控点 4 - 出口'
}
};
// 视频播放器实例
const players = {};
// 初始化所有视频播放器
function initializePlayers() {
for (let i = 1; i <= 4; i++) {
const videoElement = document.getElementById(`video${i}`);
// 检查是否已经初始化
if (videojs.getPlayer(`video${i}`)) {
console.log(`播放器 ${i} 已经初始化,跳过`);
players[i] = videojs.getPlayer(`video${i}`);
continue;
}
const player = videojs(videoElement, {
liveui: true,
autoplay: 'any', // 使用更宽松的自动播放策略
muted: true, // 静音播放,避免多个视频同时播放声音
controls: false, // 隐藏控制条
fluid: true,
html5: {
hls: {
enableLowInitialPlaylist: true,
smoothQualityChange: true,
overrideNative: true
}
}
});
players[i] = player;
// 监听播放器事件
player.ready(() => {
console.log(`播放器 ${i} 准备就绪`);
// 显示初始状态
showLoading(i);
updateStatus(i, '正在连接...');
// 延迟加载流,确保播放器完全初始化
setTimeout(() => {
loadStream(i);
}, 100 * i); // 错开加载时间,避免同时请求
});
player.on('loadstart', () => {
console.log(`播放器 ${i} 开始加载`);
showLoading(i);
hideError(i);
updateStatus(i, '正在连接...');
});
player.on('loadedmetadata', () => {
console.log(`播放器 ${i} 元数据加载完成`);
updateStatus(i, '连接成功');
});
player.on('loadeddata', () => {
console.log(`播放器 ${i} 数据加载完成`);
hideLoading(i);
updateStatus(i, '直播中');
});
player.on('error', (e) => {
console.error(`播放器 ${i} 发生错误:`, player.error());
showError(i);
updateStatus(i, '连接失败');
});
player.on('waiting', () => {
console.log(`播放器 ${i} 等待数据`);
showLoading(i);
updateStatus(i, '缓冲中...');
});
player.on('playing', () => {
console.log(`播放器 ${i} 开始播放`);
hideLoading(i);
hideError(i);
updateStatus(i, '直播中');
});
player.on('canplay', () => {
console.log(`播放器 ${i} 可以播放`);
hideLoading(i);
});
}
}
// 加载直播流
function loadStream(screenId) {
const config = streamConfigs[screenId];
if (!config || !config.url || config.url === 'http://example.com/stream3.m3u8' || config.url === 'http://example.com/stream4.m3u8') {
console.error(`屏幕 ${screenId} 未配置有效的直播流地址`);
showError(screenId);
updateStatus(screenId, '未配置');
return;
}
showLoading(screenId);
updateStatus(screenId, '正在连接...');
const player = players[screenId];
// 先设置源,然后尝试播放
player.src({
src: config.url,
type: 'application/x-mpegURL'
});
// 添加播放超时检测
const playTimeout = setTimeout(() => {
console.warn(`播放器 ${screenId} 播放超时`);
if (!player.hasStarted()) {
// 如果播放器没有开始播放,显示错误
showError(screenId);
updateStatus(screenId, '连接超时');
}
}, 10000); // 10秒超时
player.play().then(() => {
console.log(`播放器 ${screenId} 播放成功`);
clearTimeout(playTimeout);
}).catch(error => {
console.error(`播放器 ${screenId} 自动播放失败:`, error);
clearTimeout(playTimeout);
// 如果是自动播放策略限制,等待用户交互
if (error.name === 'NotAllowedError') {
console.log(`播放器 ${screenId} 等待用户交互`);
updateStatus(screenId, '点击播放');
// 添加点击播放功能
const videoElement = document.getElementById(`video${screenId}`);
const playHandler = () => {
player.play().then(() => {
console.log(`播放器 ${screenId} 用户交互后播放成功`);
videoElement.removeEventListener('click', playHandler);
}).catch(e => {
console.error(`播放器 ${screenId} 用户交互后仍然播放失败:`, e);
showError(screenId);
updateStatus(screenId, '播放失败');
});
};
videoElement.addEventListener('click', playHandler);
} else {
showError(screenId);
updateStatus(screenId, '播放失败');
}
});
}
// 显示加载状态
function showLoading(screenId) {
const loadingEl = document.getElementById(`loading${screenId}`);
if (loadingEl) loadingEl.style.display = 'flex';
}
// 隐藏加载状态
function hideLoading(screenId) {
const loadingEl = document.getElementById(`loading${screenId}`);
if (loadingEl) loadingEl.style.display = 'none';
}
// 显示错误状态
function showError(screenId) {
const errorEl = document.getElementById(`error${screenId}`);
if (errorEl) errorEl.style.display = 'flex';
}
// 隐藏错误状态
function hideError(screenId) {
const errorEl = document.getElementById(`error${screenId}`);
if (errorEl) errorEl.style.display = 'none';
}
// 更新状态显示
function updateStatus(screenId, status) {
const statusEl = document.querySelector(`#screen${screenId} .screen-status span`);
if (statusEl) {
statusEl.textContent = status;
// 根据状态改变颜色
if (status === '直播中') {
statusEl.style.color = '#4ade80';
document.querySelector(`#screen${screenId} .status-dot`).style.background = '#4ade80';
} else if (status === '连接失败') {
statusEl.style.color = '#ff6b6b';
document.querySelector(`#screen${screenId} .status-dot`).style.background = '#ff6b6b';
} else {
statusEl.style.color = '#fbbf24';
document.querySelector(`#screen${screenId} .status-dot`).style.background = '#fbbf24';
}
}
}
// 刷新直播流
function refreshStream(screenId) {
console.log(`刷新屏幕 ${screenId} 的直播流`);
hideError(screenId);
showLoading(screenId);
updateStatus(screenId, '重新连接中...');
// 重新加载流
loadStream(screenId);
}
// 全屏切换
function toggleFullscreen(screenId) {
const screen = document.getElementById(`screen${screenId}`);
const isFullscreen = screen.classList.contains('fullscreen');
if (isFullscreen) {
// 退出全屏
screen.classList.remove('fullscreen');
document.querySelector(`#screen${screenId} .fullscreen-btn`).textContent = '全屏';
} else {
// 进入全屏
screen.classList.add('fullscreen');
document.querySelector(`#screen${screenId} .fullscreen-btn`).textContent = '退出全屏';
}
}
// 更新当前时间
function updateTime() {
const now = new Date();
const timeString = now.toLocaleTimeString('zh-CN', {
hour12: false,
hour: '2-digit',
minute: '2-digit',
second: '2-digit'
});
document.getElementById('currentTime').textContent = timeString;
}
// 页面加载完成后初始化
document.addEventListener('DOMContentLoaded', function() {
// 初始化视频播放器
initializePlayers();
// 启动时间更新
updateTime();
setInterval(updateTime, 1000);
// 添加键盘快捷键
document.addEventListener('keydown', function(e) {
// ESC键退出全屏
if (e.key === 'Escape') {
document.querySelectorAll('.monitor-screen').forEach(screen => {
screen.classList.remove('fullscreen');
screen.querySelector('.fullscreen-btn').textContent = '全屏';
});
}
// F1-F4键切换全屏
if (e.key >= 'F1' && e.key <= 'F4') {
const screenId = parseInt(e.key.substring(1));
toggleFullscreen(screenId);
}
});
console.log('监控大屏系统初始化完成');
});
// 页面卸载时清理资源
window.addEventListener('beforeunload', function() {
for (let i = 1; i <= 4; i++) {
if (players[i]) {
players[i].dispose();
}
}
});
</script>
</body>
</html>