let video_list = {}; //element_id -- socket
let run_list = {}; //element_id -- runtag
let berror_state_list = {}; //element_id -- 错误信息显示
let m_count = 0;
let connection_version = {}; // 保存每个 element_id 的版本号
let channel_list = null;
const fourViewButton = document.getElementById('fourView');
const nineViewButton = document.getElementById('nineView');
//页面即将卸载时执行
window.addEventListener('beforeunload', function (event) {
// 关闭所有 WebSocket 连接或执行其他清理操作
for(let key in video_list){
const videoFrame = document.getElementById(`video-${key}`);
const event = new Event('closeVideo');
videoFrame.dispatchEvent(event);
delete video_list[key];
berror_state_list[key] = false;
}
});
//页面加载时执行
document.addEventListener('DOMContentLoaded', async function() {
console.log('DOM fully loaded and parsed');
// 发送请求获取额外数据 --- 这个接口功能有点大了---暂时只是更新通道树2024-7-29
try {
let response = await fetch('/api/channel/list');
if (!response.ok) {
throw new Error('Network response was not ok');
}
channel_list = await response.json();
// 遍历输出每个元素的信息
let area_name = ""
let html = '
';
channel_list.forEach(channel => {
// console.log(`Area Name: ${channel.area_name}`);
// console.log(`ID: ${channel.ID}`);
// console.log(`Channel Name: ${channel.channel_name}`);
// console.log(`URL: ${channel.url}`);
// console.log(`Type: ${channel.type}`);
// console.log(`Status: ${channel.status}`);
// console.log(`Element ID: ${channel.element_id}`);
if(area_name !== `${channel.area_name}`){
if(area_name !== ""){
html += '
';
html += '';
}
area_name = `${channel.area_name}`;
html += `${area_name}`;
html += '';
}
//html += `- ${channel.channel_name}
`;
html += `-
${channel.channel_name}
`;
});
if(area_name !== ""){
html += '
';
html += '';
}
html += '';
const treeView = document.getElementById('treeView');
treeView.innerHTML = html
generateVideoNodes(4);
} catch (error) {
console.error('Failed to fetch data:', error);
}
});
document.addEventListener('click', function() {
console.log("第一次页面点击,开始显示视频--已注释",m_count);
// if(m_count != 0){
// count = m_count
// //获取视频接口
// const url = `/api/viewlist?count=${count}`;
// fetch(url)
// .then(response => response.json())
// .then(data => {
// console.log('Success:', data);
// clist = data.clist;
// elist = data.elist;
// nlist = data.nlist;
// for(let i=0;i {
// console.error('Error:', error);
// });
// }
}, { once: true });
//视频窗口
document.getElementById('fourView').addEventListener('click', function() {
if (fourViewButton.classList.contains('btn-primary')) {
return; // 如果按钮已经是选中状态,直接返回
}
const videoGrid = document.getElementById('videoGrid');
videoGrid.classList.remove('nine');
videoGrid.classList.add('four');
generateVideoNodes(4);
//更新按钮点击状态
fourViewButton.classList.remove('btn-secondary');
fourViewButton.classList.add('btn-primary');
nineViewButton.classList.remove('btn-primary');
nineViewButton.classList.add('btn-secondary');
});
document.getElementById('nineView').addEventListener('click', function() {
if (nineViewButton.classList.contains('btn-primary')) {
return; // 如果按钮已经是选中状态,直接返回
}
const videoGrid = document.getElementById('videoGrid');
videoGrid.classList.remove('four');
videoGrid.classList.add('nine');
generateVideoNodes(9);
//更新按钮点击状态
fourViewButton.classList.remove('btn-primary');
fourViewButton.classList.add('btn-secondary');
nineViewButton.classList.remove('btn-secondary');
nineViewButton.classList.add('btn-primary');
});
function generateVideoNodes(count) { //在这里显示视频-初始化 ---这里使用重置逻辑
//结束在播放的socket
for(let key in video_list){
//flv使用
// const videoFrame = document.getElementById(`video-${key}`);
// const event = new Event('closeVideo');
// videoFrame.dispatchEvent(event);
//通用关闭
run_list[key] = false;
video_list[key].close();
berror_state_list[key] = false;
delete video_list[key];
}
//切换窗口布局
const videoGrid = document.getElementById('videoGrid');
let html = '';
for (let i = 0; i < count; i++) {
let frameWidth = count === 4 ? 'calc(50% - 10px)' : 'calc(33.33% - 10px)';
html += `
`;
}
videoGrid.innerHTML = html;
//开始还原视频,获取视频接口
// if(m_count != 0){
const url = `/api/viewlist?count=${count}`;
fetch(url)
.then(response => response.json())
.then(data => {
console.log('Success:', data);
clist = data.clist;
elist = data.elist;
nlist = data.nlist;
for(let i=0;i {
console.error('Error:', error);
});
// }
//m_count = count
}
function toggleFullScreen(id) {
console.log('toggleFullScreen');
const videoFrame = document.querySelector(`[data-frame-id="${id}"]`);
if (!document.fullscreenElement) {
videoFrame.requestFullscreen().catch(err => {
alert(`Error attempting to enable full-screen mode: ${err.message} (${err.name})`);
});
} else {
document.exitFullscreen();
};
}
function allowDrop(event) {
event.preventDefault();
}
function drag(event) {
event.dataTransfer.setData("text", event.target.dataset.nodeId);
event.dataTransfer.setData("name", event.target.dataset.nodeName);
}
function drop(event) {
event.preventDefault();
const nodeId = event.dataTransfer.getData("text");
const nodeName = event.dataTransfer.getData("name");
const frameId = event.currentTarget.dataset.frameId;
//需要判断下当前窗口是否已经在播放视频
const imgElement = document.getElementById(`video-${frameId}`);
const titleElement = document.querySelector(`[data-frame-id="${frameId}"] .video-title`);
if (titleElement.textContent !== `Video Stream ${Number(frameId)+1}`) {
showModal('请先关闭当前窗口视频,然后再播放新的视频。');
return;
};
//发送视频链接接口
const url = '/api/start_stream';
const data = {"channel_id":nodeId,"element_id":frameId};
// 发送 POST 请求
fetch(url, {
method: 'POST', // 指定请求方法为 POST
headers: {
'Content-Type': 'application/json' // 设置请求头,告诉服务器请求体的数据类型为 JSON
},
body: JSON.stringify(data) // 将 JavaScript 对象转换为 JSON 字符串
})
.then(response => response.json()) // 将响应解析为 JSON
.then(data => {
const istatus = data.status;
if(istatus === 0){
showModal(data.msg); // 使用 Modal 显示消息
return;
}
else{
//获取视频流
console.log("drop触发")
connectToStream(frameId,nodeId,nodeName);
//startFLVStream(frameId,nodeId,nodeName); #基于FLV的开发程度:后端直接用RTSP流转发是有画面的,但CPU占用太高,用不了。2024-8-30
}
})
.catch((error) => {
showModal(`Error: ${error.message}`); // 使用 Modal 显示错误信息
return;
});
//console.log('retrun 只是把fetch结束,这里的代码还是会执行');
}
function connect(channel_id,element_id,imgcanvas,ctx,offscreenCtx,offscreenCanvas,streamUrl) {
//判断是否有重复socket,进行删除
if(element_id in video_list) {
run_list[element_id] = false;
video_list[element_id].close();
delete video_list[element_id];
console.log("有历史数据未删干净!!---",element_id) //要不要等待待定
}
// 每次连接时增加版本号
const current_version = (connection_version[element_id] || 0) + 1;
connection_version[element_id] = current_version;
const socket = new WebSocket(streamUrl);
socket.binaryType = 'arraybuffer'; // 设置为二进制数据接收
socket.customData = { channel_id: channel_id, element_id: element_id,
imgcanvas:imgcanvas,ctx:ctx,offscreenCtx:offscreenCtx,offscreenCanvas:offscreenCanvas,
version_id: current_version,streamUrl:streamUrl}; // 自定义属性 -- JS异步事件只能等到当前同步任务(代码块)完成之后才有可能被触发。
//新的连接
video_list[element_id] = socket;
run_list[element_id] = true;
berror_state_list[element_id] = false;
imgcanvas.style.display = 'block';
// 处理连接打开事件
socket.onopen = function(){
console.log('WebSocket connection established--',socket.customData.channel_id);
};
socket.onmessage = function(event) {
let el_id = socket.customData.element_id
let cl_id = socket.customData.channel_id
let imgcanvas = socket.customData.imgcanvas
let ctx = socket.customData.ctx
let offctx = socket.customData.offscreenCtx
let offscreenCanvas = socket.customData.offscreenCanvas
// 转换为字符串来检查前缀
let message = new TextDecoder().decode(event.data.slice(0, 6)); // 取前6个字节
if (message.startsWith('frame:')){
//如有错误信息显示 -- 清除错误信息
if(berror_state_list[el_id]){
removeErrorMessage(imgcanvas);
console.log("清除错误信息!")
berror_state_list[el_id] = false;
}
// 接收到 JPG 图像数据,转换为 Blob
let img = new Image();
let blob = new Blob([event.data.slice(6)], { type: 'image/jpeg' });
// 将 Blob 转换为可用的图像 URL
img.src = URL.createObjectURL(blob);
//定义图片加载函数
img.onload = function() {
imgcanvas.width = offscreenCanvas.width = img.width;
imgcanvas.height = offscreenCanvas.height = img.height;
// 在 OffscreenCanvas 上绘制
offctx.clearRect(0, 0, imgcanvas.width, imgcanvas.height);
offctx.drawImage(img, 0, 0, imgcanvas.width, imgcanvas.height);
// 将 OffscreenCanvas 的内容复制到主 canvas
ctx.drawImage(offscreenCanvas, 0, 0);
// 用完就释放
URL.revokeObjectURL(img.src);
// blob = null
// img = null
// message = null
// event.data = null
// event = null
};
}else if(message.startsWith('error:')){
const errorText = new TextDecoder().decode(event.data.slice(6)); // 截掉前缀 'error:'
//目前只处理一个错误信息,暂不区分
displayErrorMessage(imgcanvas, "该视频源未获取到画面,请检查后刷新重试,默认两分钟后重连");
berror_state_list[el_id] = true;
}
};
socket.onclose = function() {
let el_id = socket.customData.element_id;
let cl_id = socket.customData.channel_id;
if(run_list[el_id] && socket.customData.version_id === connection_version[el_id]){
console.log(`尝试重新连接... Channel ID: ${cl_id}`);
setTimeout(() => connect(cl_id, el_id, socket.customData.imgcanvas,
socket.customData.ctx,socket.customData.streamUrl), 1000*10); // 尝试在10秒后重新连接
}
};
socket.onerror = function() {
console.log(`WebSocket错误,Channel ID: ${socket.customData.channel_id}`);
socket.close(1000, "Normal Closure");
};
}
function connectToStream(element_id,channel_id,channel_name) {
console.log("开始连接视频",element_id,channel_id);
//更新控件状态--设置视频区域的标题
const titleElement = document.querySelector(`[data-frame-id="${element_id}"] .video-title`);
titleElement.textContent = channel_name;
//视频控件
//const imgElement = document.getElementById(`video-${element_id}`);
//imgElement.alt = `Stream ${channel_name}`;
const imgcanvas = document.getElementById(`video-${element_id}`);
const ctx = imgcanvas.getContext('2d')
// 创建 OffscreenCanvas
const offscreenCanvas = new OffscreenCanvas(imgcanvas.width, imgcanvas.height);
const offscreenCtx = offscreenCanvas.getContext('2d');
const streamUrl = `ws://${window.location.host}/api/ws/video_feed/${channel_id}`;
//创建websocket连接,并接收和显示图片
connect(channel_id,element_id,imgcanvas,ctx,offscreenCtx,offscreenCanvas,streamUrl); //执行websocket连接 -- 异步的应该会直接返回
}
function closeVideo(id) {
if(id in video_list) {
const imgcanvas = document.getElementById(`video-${id}`);
const titleElement = document.querySelector(`[data-frame-id="${id}"] .video-title`);
//断socket
run_list[id] = false;
video_list[id].close();
delete video_list[id];
//清空控件状态
imgcanvas.style.display = 'none'; // 停止播放时隐藏元素
titleElement.textContent = `Video Stream ${id+1}`;
removeErrorMessage(imgcanvas);
berror_state_list[id] = false;
//删记录
const url = '/api/close_stream';
const data = {"element_id":id};
// 发送 POST 请求
fetch(url, {
method: 'POST', // 指定请求方法为 POST
headers: {
'Content-Type': 'application/json' // 设置请求头,告诉服务器请求体的数据类型为 JSON
},
body: JSON.stringify(data) // 将 JavaScript 对象转换为 JSON 字符串
})
.then(response => response.json()) // 将响应解析为 JSON
.then(data => {
console.log('Success:', data);
const istatus = data.status;
if(istatus == 0){
showModal(data.msg); // 使用 Modal 显示消息
return;
}
})
.catch((error) => {
showModal(`Error: ${error.message}`); // 使用 Modal 显示错误信息
return;
});
}
else{
showModal('当前视频窗口未播放视频。');
return;
}
}
function startFLVStream(element_id,channel_id,channel_name) {
// 设置视频区域的标题
const titleElement = document.querySelector(`[data-frame-id="${element_id}"] .video-title`);
titleElement.textContent = channel_name;
//获取视频
// const imgElement = document.getElementById(`video-${element_id}`);
// imgElement.alt = `Stream ${channel_name}`;
const videoElement = document.getElementById(`video-${element_id}`);
let reconnectAttempts = 0;
const maxReconnectAttempts = 3;
const flvUrl = `ws://${window.location.host}/api/ws/video_feed/${channel_id}`;
function initFLVPlayer() {
if (flvjs.isSupported()) {
//要避免重复播放
if(element_id in video_list) {
closeFLVStream(element_id)
}else{
video_list[element_id] = element_id;
berror_state_list[element_id] = true;
}
flvPlayer = flvjs.createPlayer({
type: 'flv',
url: flvUrl,
});
flvPlayer.attachMediaElement(videoElement);
flvPlayer.load();
flvPlayer.play();
// 设定超时时间,例如10秒
timeoutId = setTimeout(() => {
console.error('No video data received. Closing connection.');
flvPlayer.destroy(); // 停止视频
// 显示错误信息或提示
displayErrorMessage(videoElement, "该视频源获取画面超时,请检查后刷新重试,默认两分钟后重连");
berror_state_list[element_id] = true;
}, 130000); // 130秒
// 错误处理
flvPlayer.on(flvjs.Events.ERROR, (errorType, errorDetail) => {
console.error(`FLV Error: ${errorType} - ${errorDetail}`);
if (reconnectAttempts < maxReconnectAttempts) {
reconnectAttempts++;
console.log("开始重连")
setTimeout(initFLVPlayer, 30000); // 尝试重连
} else {
displayErrorMessage(videoElement, "重连超时,请检查后重试,可联系技术支持!");
berror_state_list[element_id] = true;
}
});
// 监听播放事件,如果播放成功则清除超时计时器
flvPlayer.on(flvjs.Events.STATISTICS_INFO, () => {
clearTimeout(timeoutId);
timeoutId = null;
removeErrorMessage(videoElement);
berror_state_list[element_id] = false;
});
// 关闭视频流时销毁播放器
videoElement.addEventListener('closeVideo', () => {
if(flvPlayer){
flvPlayer.destroy();
videoElement.removeEventListener('closeVideo', onCloseVideo);
flvPlayer.off(flvjs.Events.ERROR);
flvPlayer.off(flvjs.Events.STATISTICS_INFO);
delete flvPlayer
}
});
} else {
console.error('FLV is not supported in this browser.');
}
}
initFLVPlayer();
}
// 主动关闭视频的函数
function closeFLVStream(id) {
const titleElement = document.querySelector(`[data-frame-id="${id}"] .video-title`);
if (titleElement.textContent === `Video Stream ${Number(id)+1}`) {
showModal('当前视频窗口未播放视频。');
return;
};
//发送视频链接接口
const url = '/api/close_stream';
const data = {"element_id":id};
// 发送 POST 请求
fetch(url, {
method: 'POST', // 指定请求方法为 POST
headers: {
'Content-Type': 'application/json' // 设置请求头,告诉服务器请求体的数据类型为 JSON
},
body: JSON.stringify(data) // 将 JavaScript 对象转换为 JSON 字符串
})
.then(response => response.json()) // 将响应解析为 JSON
.then(data => {
console.log('Success:', data);
const istatus = data.status;
if(istatus == 0){
showModal(data.msg); // 使用 Modal 显示消息
return;
}
else{
const videoFrame = document.getElementById(`video-${element_id}`);
const event = new Event('closeVideo');
videoFrame.dispatchEvent(event);
//videoFrame.style.display = 'none'; // 停止播放时隐藏 img 元素
titleElement.textContent = `Video Stream ${id+1}`;
removeErrorMessage(videoFrame);
berror_state_list[key] = false;
delete video_list[id];
}
})
.catch((error) => {
showModal(`Error: ${error.message}`); // 使用 Modal 显示错误信息
return;
});
}
function displayErrorMessage(imgElement, message) {
removeErrorMessage(imgElement)
imgElement.style.display = 'none'; // 隐藏图片
const errorElement = document.createElement('div');
errorElement.textContent = message;
errorElement.classList.add('error-message');
imgElement.parentNode.appendChild(errorElement);
}
function removeErrorMessage(imgElement) {
const errorElement = imgElement.parentNode.querySelector('.error-message');
if (errorElement) {
imgElement.parentNode.removeChild(errorElement);
}
}