Files
SupervisorAI/web_page/monitor_dashboard.html
2026-01-15 12:53:10 +08:00

690 lines
24 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>监控大屏 - 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: '', // 请替换为实际的m3u8地址
title: '监控点 3 - 大厅'
},
4: {
url: '', // 请替换为实际的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);
// todo 后期改为页面加载后调用获取视频url接口取得视频url后再调用下面代码延迟时间可以设为更短的时间例如300ms
// 页面加载3秒后调用重新加载逻辑
setTimeout(() => {
console.log('页面加载3秒后开始重新加载所有直播流');
for (let i = 1; i <= 4; i++) {
const config = streamConfigs[i];
// 检查URL是否为空只有配置了有效URL才重新加载
if (config && config.url) {
console.log(`重新加载屏幕 ${i} 的直播流`);
refreshStream(i);
} else {
console.log(`屏幕 ${i} 未配置有效URL跳过重新加载`);
}
}
}, 1500);
// 添加键盘快捷键
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>