You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 

529 lines
20 KiB

let video_list = {}; //element_id -- socket
let run_list = {}; //element_id -- runtag
let berror_state_list = {}; //element_id -- 错误信息显示
let m_count = 0;
var 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 = '<ul class="list-group">';
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 += '</ul>';
html += '</li>';
}
area_name = `${channel.area_name}`;
html += `<li class="list-group-item"><strong>${area_name}</strong>`;
html += '<ul class="list-group">';
}
//html += `<li class="list-group-item">${channel.channel_name}</li>`;
html += `<li class="list-group-item" draggable="true" ondragstart="drag(event)"
data-node-id="${channel.ID}" data-node-name="${area_name}--${channel.channel_name}">
<svg class="bi" width="16" height="16"><use xlink:href="#view"/></svg>
${channel.channel_name}
</li>`;
});
if(area_name !== ""){
html += '</ul>';
html += '</li>';
}
html += '</ul>';
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<clist.length;i++){
// if(parseInt(elist[i]) < count){
// console.log("切换窗口时进行连接",clist[i])
// connectToStream(elist[i],clist[i],nlist[i])
// //startFLVStream(elist[i],clist[i],nlist[i]);
// }
// }
// })
// .catch(error => {
// 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){
const videoFrame = document.getElementById(`video-${key}`);
const event = new Event('closeVideo');
videoFrame.dispatchEvent(event);
delete video_list[key];
berror_state_list[key] = false;
}
//切换窗口布局
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 += `
<div class="video-frame" data-frame-id="${i}" style="width: ${frameWidth};"
ondrop="drop(event)" ondragover="allowDrop(event)">
<div class="video-header">
<div class="video-title">Video Stream ${i+1}</div>
<div class="video-buttons">
<button onclick="toggleFullScreen(${i})">🔲</button>
<button onclick="closeVideo(${i})">❌</button>
<!-- <button onclick="closeFLVStream(${i})">❌</button> -->
</div>
</div>
<div class="video-area"><img id="video-${i}" alt="Video Stream" /></div>
<!-- <div class="video-area"><video id="video-${i}" controls></video></div> -->
</div>`;
}
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<clist.length;i++){
if(parseInt(elist[i]) < count){
console.log("切换窗口时进行连接",clist[i])
connectToStream(elist[i],clist[i],nlist[i])
//startFLVStream(elist[i],clist[i],nlist[i]);
}
}
})
.catch(error => {
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 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 streamUrl = `ws://${window.location.host}/api/ws/video_feed/${channel_id}`;
let berror = false;
function connect() {
const socket = new WebSocket(streamUrl);
console.log("socket建立----",element_id);
//全局变量需要维护好
if(element_id in video_list) {
video_list[element_id].close();
}else{
berror_state_list[element_id] = false;
}
video_list[element_id] = socket;
run_list[element_id] = true;
imgElement.style.display = 'block';
// 处理连接打开事件
socket.onopen = () => {
console.log('WebSocket connection established');
};
socket.onmessage = function(event) {
const reader = new FileReader();
reader.readAsArrayBuffer(event.data);
reader.onload = () => {
const arrayBuffer = reader.result;
const decoder = new TextDecoder("utf-8");
const decodedData = decoder.decode(arrayBuffer);
if (decodedData === "video_error") { //video_error
displayErrorMessage(imgElement, "该视频源未获取到画面,请检查后刷新重试,默认两分钟后重连");
berror_state_list[element_id] = true;
//socket.close(1000, "Normal Closure"); // 停止连接
} else if(decodedData === "client_error"){ //client_error
run_list[element_id] = false;
displayErrorMessage(imgElement, "该通道节点数据存在问题,请重启或联系技术支持!");
socket.close(1000, "Normal Closure"); // 停止连接
berror_state_list[element_id] = true;
}
else {
if(berror_state_list[element_id]){
removeErrorMessage(imgElement);
berror_state_list[element_id] = false;
//console.log("移除错误信息!");
}
// 释放旧的对象URL
if (imgElement.src) {
URL.revokeObjectURL(imgElement.src);
}
//blob = new Blob([arrayBuffer], { type: 'image/jpeg' });
imgElement.src = URL.createObjectURL(event.data);
}
};
//图片显示方案二
// if (imgElement.src) {
// URL.revokeObjectURL(imgElement.src);
// }
// imgElement.src = URL.createObjectURL(event.data);
};
socket.onclose = function() {
if(run_list[element_id]){
console.log(`尝试重新连接... Channel ID: ${channel_id}`);
setTimeout(connect, 1000*10); // 尝试在10秒后重新连接
}
};
socket.onerror = function() {
console.log(`WebSocket错误,Channel ID: ${channel_id}`);
socket.close(1000, "Normal Closure");
};
};
connect();
}
function closeVideo(id) {
const titleElement = document.querySelector(`[data-frame-id="${id}"] .video-title`);
if (titleElement.textContent === `Video Stream ${Number(id)+1}`) {
showModal('当前视频窗口未播放视频。');
return;
};
console.log('closeVideo');
//发送视频链接接口
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.querySelector(`[data-frame-id="${id}"] .video-area img`);
const titleElement = document.querySelector(`[data-frame-id="${id}"] .video-title`);
run_list[id] = false;
video_list[id].close();
delete video_list[id];
videoFrame.src = ''; // 清空画面
videoFrame.style.display = 'none'; // 停止播放时隐藏 img 元素
titleElement.textContent = `Video Stream ${id+1}`;
removeErrorMessage(videoFrame);
berror_state_list[id] = false;
}
})
.catch((error) => {
showModal(`Error: ${error.message}`); // 使用 Modal 显示错误信息
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);
}
}