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 = ''; html += ''; } area_name = `${channel.area_name}`; html += `
  • ${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 += `
    Video Stream ${i+1}
    `; } 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); 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); } }