Files
SupervisorAI/web_page/monitor_dashboard.html

779 lines
28 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="static/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="static/video.min.js"></script>
<script>
// 配置直播流地址 - 通过接口动态获取
const streamConfigs = {
1: {
url: '', // 将通过接口获取
title: '监控点 1 - 主入口',
videoId: 'cam001'
},
2: {
url: '', // 将通过接口获取
title: '监控点 2 - 走廊',
videoId: 'cam002'
},
3: {
url: '', // 将通过接口获取
title: '监控点 3 - 大厅',
videoId: 'cam003'
},
4: {
url: '', // 将通过接口获取
title: '监控点 4 - 出口',
videoId: 'cam004'
}
};
// 视频播放器实例
const players = {};
// 获取视频流URL的接口地址通过Nginx代理
const VIDEO_API_URL = '/api/v1/video-urls'; // 使用相对路径通过Nginx代理到后端
// 获取视频流URL的接口
async function fetchVideoUrls() {
try {
const videoIds = Object.values(streamConfigs).map(config => config.videoId);
console.log('发送请求到:', VIDEO_API_URL);
console.log('请求体:', JSON.stringify({
"videoId": videoIds
}));
const response = await fetch(VIDEO_API_URL, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
"videoId": videoIds
})
});
console.log('响应状态:', response.status, response.statusText);
if (!response.ok) {
let errorDetails = '';
try {
const errorData = await response.json();
errorDetails = JSON.stringify(errorData);
} catch (e) {
errorDetails = await response.text();
}
throw new Error(`HTTP error! status: ${response.status}, details: ${errorDetails}`);
}
const data = await response.json();
console.log('响应数据:', data);
// 更新streamConfigs中的URL
if (data.videoUrl && Array.isArray(data.videoUrl)) {
Object.keys(streamConfigs).forEach((screenId, index) => {
if (data.videoUrl[index]) {
streamConfigs[screenId].url = data.videoUrl[index];
console.log(`屏幕 ${screenId} 获取到URL: ${data.videoUrl[index]}`);
}
});
return true;
} else {
throw new Error('返回数据格式错误');
}
} catch (error) {
console.error('获取视频流URL失败:', error);
// 在所有屏幕显示错误
for (let i = 1; i <= 4; i++) {
showError(i);
updateStatus(i, '获取流地址失败: ' + errorMessage);
}
return false;
}
}
// 初始化所有视频播放器
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) {
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', async function() {
// 初始化视频播放器
initializePlayers();
// 启动时间更新
updateTime();
setInterval(updateTime, 1000);
// 首先获取视频流URL
console.log('开始获取视频流URL...');
// 显示加载状态
for (let i = 1; i <= 4; i++) {
showLoading(i);
updateStatus(i, '获取流地址中...');
}
// 调用接口获取视频URL
const success = await fetchVideoUrls();
if (success) {
console.log('视频流URL获取成功开始加载直播流');
// 延迟300ms后开始加载所有直播流
setTimeout(() => {
console.log('开始加载所有直播流');
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跳过加载`);
showError(i);
updateStatus(i, '无可用流地址');
}
}
}, 300);
} else {
console.error('视频流URL获取失败无法加载直播流');
}
// 添加键盘快捷键
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>