664 lines
23 KiB
HTML
664 lines
23 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;
|
|
}
|
|
|
|
/* 全屏模式下的样式调整 */
|
|
.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> |