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.
556 lines
22 KiB
556 lines
22 KiB
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 = '<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){
|
|
//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 += `
|
|
<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>
|
|
</div>
|
|
</div>
|
|
<div class="video-area"><canvas id="video-${i}"></canvas></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){
|
|
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 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);
|
|
}
|
|
}
|
|
|
|
|