<output id="qn6qe"></output>

    1. <output id="qn6qe"><tt id="qn6qe"></tt></output>
    2. <strike id="qn6qe"></strike>

      亚洲 日本 欧洲 欧美 视频,日韩中文字幕有码av,一本一道av中文字幕无码,国产线播放免费人成视频播放,人妻少妇偷人无码视频,日夜啪啪一区二区三区,国产尤物精品自在拍视频首页,久热这里只有精品12

      通過Canvas在網頁中將后端發來的一幀幀圖片渲染成“視頻”的實現過程

      1.背景

      最近有這樣的場景,網頁端需要顯示現場無人系統(機器人)的攝像頭數據(圖片)。值得注意的是,一個無人系統(機器人)它身上可能掛載若干個攝像頭,這若干個攝像頭都需要在前端的若干個小區域內顯示;另外不同的用戶訪問前端網頁,每個用戶都訪問他自己想關注的無人系統(機器人)攝像頭數據。而前端直接和現場的無人系統對接是不合適的:因為對于同一個無人系統,可能不同的用戶同一時間或相近時間都訪問它,導致該無人系統要處理反饋多份資源請求,并且很容易導致超過機器人的處理負荷;另外對于前端來講,他并不知知道應該和現場的哪一個無人系統進行對接(因為前端并沒有現場的無人系統相關身份數據,無法做識別)。

      為此,設計了如下方案,現場的無人系統統一和數據中轉服務器對接,每個機器人都只給一份實時攝像頭數據給數據中轉服務器。數據中轉服務器建立websocket服務端程序,并處理網頁端的請求(請求獲取特定機器人的所有攝像頭信息),數據中轉服務器根據網頁端的請求,對請求信息進行解析,并創建特定的websocket服務實例。具體通信示意圖如下:

      圖片

       這里所提到的前端網頁,實際是業務中的可視化大屏,他對之前項目的已有功能有些注意點:

      • 總控大屏現有對接無人系統的視頻使用的是后端發給前端的rtsp流地址,默認使用的是該方式。但后續無人系統(機器人)傳輸的數據也有可能是一幀幀二進制圖片數據
      • 原有前端使用的組件適用接收rtsp流方式,不適用新的接收圖片幀的方式,前端需要做兩套模式區分(區別開發:一套<video>,一套<canvas>
      • 在無人系統(機器人)傳輸的數據是一幀幀二進制圖片數據的情況下,有可能該無人系統有多個攝像頭,它會傳輸多組獨立的圖片幀數據(前端最多支持4個攝像頭數據)

      2.約定接口

      針對以上內容進行分析,并為了兼容已有實現的功能,約定如下大屏與數據中轉器的接口方式:

      網頁端通過GET請求,調用數據中轉服務器接口,請求接口地址為:

      http://ip:port/api/usdisplay?usid=2 。其中請求參數usid代表前端給數據中轉服務器(后端)傳遞的無人系統id.

      數據中轉服務器需要根據無人系統id,判斷該無人系統攝像頭數據傳遞是使用的哪種方式?并根據特定的方式返回前端結果,前端根據不同的模式,執行不同的渲染方式。

      數據中轉服務器(后端)返回前端的結果格式為:

       

      • rtsp模式,如果一個無人系統有3個攝像頭舉例

       

       1 {
       2     "code": 200,
       3     "success": true,
       4     "data": {
       5         "mode": "rtspurl",
       6         "url": [
       7             "rtsp: //127.0.0.1:8081",
       8             "rtsp: //127.0.0.1:8082",
       9             "rtsp: //127.0.0.1:8083"
      10         ]
      11     }
      12 }
      • websocket模式,如果一個無人系統有3個攝像頭舉例
      {
          "code": 200,
          "success": true,
          "data": {
              "mode": "websocketurl",
              "url": [
                  "ws://127.0.0.1:8080/api/websocket?usid=2&cam=0",
                  "ws://127.0.0.1:8080/api/websocket?usid=2&cam=1",
                  "ws://127.0.0.1:8080/api/websocket?usid=2&cam=2",
              ]
          }
      }

       3.前端開發過程

      3.1 div結構設計

       1     <div class="chartarea">
       2             <div class="charttitle"><span>態勢總覽</span></div>
       3             <div class="chartdata" id="videoGrid">
       4               <!-- 四個視頻區域 -->
       5               <div class="video-container" data-camera="1">
       6                 <video class="video-stream" autoplay muted></video>
       7                 <div class="camera-label"></div>
       8               </div>
       9               <div class="video-container" data-camera="2">
      10                 <video class="video-stream" autoplay muted></video>
      11                 <div class="camera-label"></div>
      12               </div>
      13               <div class="video-container" data-camera="3">
      14                 <video class="video-stream" autoplay muted></video>
      15                 <div class="camera-label"></div>
      16               </div>
      17               <div class="video-container" data-camera="4">
      18                 <video class="video-stream" autoplay muted></video>
      19                 <div class="camera-label"></div>
      20               </div>
      21             </div>
      22           </div>

      主要是在一個區域內預先占用4個小區域,每個小區域用于顯示同一個無人系統的一個攝像頭信息,最多支持顯示同一個無人系統的4個攝像頭信息(實際顯示其中的1-4個小區域是以實際同一個無人系統的攝像頭個數而定的)。

      以上的html結構最先是為了支持rtsp視頻流而設計的,對于當前的圖片幀顯示使用的Canvas技術不適用,所以如果是在圖片幀顯示的模式下,后續需要通過js動態的修改html結果,切換為<canvas>相關標簽結構。

      以上現有的html結構對應的CSS樣式如下:

       1 .chartarea {
       2   width: 95%;
       3   height: 31%;
       4   margin-top: 3.5%;
       5 }
       6 .innerright .chartarea {
       7   margin-left: 3%;
       8   margin-right: 2%;
       9 }
      10 .charttitle {
      11   width: 100%;
      12   height: 15%;
      13   background-image: url("/img/visualImages/20_chart_title.png");
      14   background-size: 100% 100%;
      15 }
      16 .charttitle>span {
      17   height: 100%;
      18   margin-left: 5%;
      19   display: flex;
      20   align-items: center;
      21   font-size: 0.8vw;
      22   color: #fff;
      23   font-weight: 700;
      24 }
      25 .chartdata {
      26   width: 100%;
      27   height: 85%;
      28   /* background-image: url("/img/visualImages/21_chart_background.png");
      29     background-size: cover;
      30     background-repeat: no-repeat;
      31     background-position:top left; */
      32 
      33   /* 當背景圖片無法完整鋪滿整個div,但自己又想即時圖片變形不合比例拉伸,也要鋪滿,這是種好方式! */
      34   /* 這種方法會將背景圖片拉伸以完全覆蓋div的寬度和高度,可能會導致圖片變形,特別是如果圖片的原始寬高比與div的寬高比不匹配時。 */
      35   background-image: url("/img/visualImages/21_chart_background.png");
      36   background-size: 100% 100%;
      37 }
      38 #videoGrid {
      39   flex: 1;
      40   display: grid;
      41   grid-template-columns: 0.48fr 0.48fr;
      42   grid-template-rows: 0.49fr 0.49fr;
      43   /* gap: 5px; */
      44   gap: 2%;
      45   padding: 1.5%;
      46 }
      47 
      48 .video-container {
      49   position: relative;
      50   background-color: #000;
      51   border-radius: 4px;
      52   overflow: hidden;
      53 }
      54 .video-stream
      55  {
      56   width: 100%;
      57   height: 100%;
      58   object-fit: cover;
      59 }
      60 
      61 .camera-label {
      62   position: absolute;
      63   bottom: 5px;
      64   left: 5px;
      65   color: white;
      66   background-color: rgba(0, 0, 0, 0.5);
      67   padding: 2px 5px;
      68   border-radius: 3px;
      69   font-size: 12px;
      70 }

      在上面的4個小視頻區域,當用戶點擊其中任意一個有視頻的小區域時,會彈出一個視頻放大顯示的彈出框,其對應的html結構和css如下:

      1 <!-- 視頻放大彈出框構建 -->
      2           <div id="videoModal" class="modal">
      3             <div class="modal-content">
      4               <span class="close-btn">&times;</span>
      5               <video id="modalVideo" autoplay controls></video>
      6               <div class="modal-camera-label"></div>
      7             </div>
      8           </div>

       

       1 /* 彈窗樣式 */
       2 .modal {
       3   display: none;
       4   position: fixed;
       5   z-index: 1100;
       6   left: 0;
       7   top: 0;
       8   width: 100%;
       9   height: 100%;
      10   background-color: rgba(0, 0, 0, 0.8);
      11   justify-content: center;
      12   align-items: center;
      13 }
      14 .modal-content {
      15   position: relative;
      16   width: 70vw;
      17   height: 75vh;
      18   background-color: #000;
      19   border-radius: 5px;
      20   overflow: hidden;
      21 }
      22 
      23 .close-btn {
      24   position: absolute;
      25   top: 10px;
      26   right: 15px;
      27   color: white;
      28   font-size: 28px;
      29   font-weight: bold;
      30   cursor: pointer;
      31   z-index: 1001;
      32 }
      33 .close-btn:hover {
      34   color: #ccc;
      35 }
      36 .close-btn {
      37   font-size: 24px;
      38   font-weight: bold;
      39   color: #999;
      40   cursor: pointer;
      41 }
      42 #modalVideo{
      43   width: 100%;
      44   height: 100%;
      45   object-fit: contain;
      46 }
      47 .modal-camera-label {
      48   position: absolute;
      49   bottom: 10px;
      50   left: 10px;
      51   color: white;
      52   background-color: rgba(0, 0, 0, 0.5);
      53   padding: 5px 10px;
      54   border-radius: 3px;
      55   font-size: 14px;
      56 }

      3.2 js函數設計

      3.2.1 設計統一的入口函數

        設計統一的入口函數USDisplay(),當用戶訪問特定的tab頁時觸發該函數。USDisplay()通過Get請求、以無人系統id作為請求參數,訪問數據中轉服務器程序,數據中轉服務器程序根據請求的無人系統id,分析判斷該無人系統視頻傳輸的模式,并執行模式信息反饋。

      代碼設計如下:

       1 export function USDisplay() {
       2   //1 根據無人系統id,發送請求后端,并解析后端返回的是哪種模式
       3   //Get請求
       4   var result = null;
       5   $.ajax({
       6     type: 'GET',
       7     //url: ipport + '/api/usdisplay',  //!!!!后續由后端確定ip
       8     url: 'http://127.0.0.1:8080' + '/api/usdisplay',  //20250815臨時測試用
       9     data: {
      10       //usid: clickedUnmanedDVId  //無人系統id
      11       usid: 3  //無人系統id //20250815臨時測試用
      12     },
      13     dataType: 'json', // 期望的后端返回數據格式
      14     async: false,
      15     success: function (res) {
      16       result = res;
      17       console.log('成功拿到數據了----',result);
      18     },
      19     error: function (xhr, status, error) {
      20       console.log("error result",result);
      21       console.error('USDisplay API請求失敗:', status, error);
      22 
      23       showConnectionStatus('API連接失敗', 'error');
      24     }
      25   });
      26 
      27   var urlarray = [];//用于存儲rtspurl/websocketurl地址數組
      28   //解析模式
      29   if (result.code === 200 && result.success === true && !!result.data && isNotEmptyObject(result.data)) {
      30     //模式一:直接rtsp流(也有弊端,前端直連機器人視頻,如果網頁訪問的用戶過多,會導致機器人負荷過大,后期也需要數據中臺中轉)
      31     if (result.data.mode === "rtspurl") {
      32       urlarray = result.data.url;
      33       if (urlarray.length >= 1) {
      34         //-1----清理之前的連接資源
      35         cleanupPreviousConnections();
      36         //-2----rtsp構建顯示邏輯
      37         usrtspmode(urlarray);
      38       }
      39     }
      40     //模式二:數據中臺作為websocket服務端,網頁端作為websocket客戶端
      41     else if (result.data.mode === "websocketurl") {
      42       urlarray = result.data.url;
      43       if (urlarray.length >= 1) {
      44         //-1----清理之前的連接資源
      45         cleanupPreviousConnections();
      46         //-2----websocket構建顯示邏輯
      47         uswebsocketmode(urlarray);
      48       }
      49     }
      50     //說明后端沒有返回任何模式,不做任何處理
      51     else{
      52       console.warn('機器人rtsp/圖片幀:后端未返回有效的顯示模式');
      53       showConnectionStatus('未知顯示模式', 'warning');
      54     }
      55   }else{
      56     console.error('USDisplay API返回數據無效,result:',result);
      57     showConnectionStatus('數據獲取失敗', 'error');
      58   }
      59 }

      3.2.2 模式一:rtspurl模式的處理

        1 function usrtspmode(url) {
        2   // 獲取元素
        3   const videoContainers = document.querySelectorAll('.video-container');//4個視頻div容器(各自平等獨立)
        4   const modal = document.getElementById('videoModal');//視頻彈出框
        5   const modalVideo = document.getElementById('modalVideo');//彈出框顯示視頻區域
        6   const closeBtn = document.querySelector('.close-btn');//彈出框關閉按鈕區域
        7   const modalCameraLabel = document.querySelector('.modal-camera-label');//彈出框底部顯示視頻名稱標識
        8 
        9   var cameraConfigs = [];//重新構建rtsp地址,友好前端顯示
       10   url.forEach((item, index) => {
       11     cameraConfigs.push(
       12       {
       13         id: index,
       14         name: "camera" + (index + 1),
       15         rtsp: item
       16       }
       17     );
       18   });
       19 
       20   // 用于存儲webrtc實例 (幾個視頻就需要幾個實例)
       21   const webrtcInstances = [];
       22 
       23  // 初始化視頻流函數--核心方法
       24   function setupVideoStreams() {
       25 
       26     //遍歷4個視頻div元素操作
       27     //每個視頻div結構如下:
       28     // <div class="video-container" data-camera="1">
       29     //   <video class="video-stream" autoplay muted></video>
       30     //   <div class="camera-label"></div>
       31     // </div>
       32 
       33     videoContainers.forEach((container, index) => {
       34       const videoElement = container.querySelector('.video-stream');//小區域視頻本身
       35       const cameraLabel = container.querySelector('.camera-label');//小區域視頻標識
       36 
       37       // (1)攝像頭名稱顯示(從配置讀取)
       38       if (cameraLabel && cameraConfigs[index]) {
       39         cameraLabel.textContent = cameraConfigs[index].name;//根據后臺的攝像頭名稱(位置標識)進行標識顯示
       40       }
       41 
       42       // (2)初始化webrtc-streamer
       43       if (videoElement && cameraConfigs[index]) {
       44         //----2.1 實例化WebRtcStreamer ---固定寫法
       45         const webrtc = new WebRtcStreamer(videoElement, WEBRTC_SERVER);
       46 
       47         //----2.2 執行webrtc實例連接rtsp流(地址)    ---固定寫法
       48         //webrtc.connect(cameraConfigs[index].rtsp);//優化
       49         //webrtc.connect(cameraConfigs[index].rtsp,null,"rtptransport=tcp&timeout=60&width=320&height=240",null);
       50         webrtc.connect(cameraConfigs[index].rtsp, null, "rtptransport=tcp&timeout=60", null);
       51 
       52         //----2.3 存儲實例以便管理
       53         // webrtcInstances.push({
       54         //   id: cameraConfigs[index].id,
       55         //   instance: webrtc,
       56         //   element: videoElement
       57         // });
       58 
       59         //存儲到全局數組用于資源管理
       60         globalWebrtcInstances.push({
       61           id: cameraConfigs[index].id,
       62           instance: webrtc,
       63           element: videoElement
       64         });
       65 
       66         // 錯誤處理
       67         videoElement.onerror = function () {
       68           handleStreamError(container);
       69         };
       70 
       71         //補充:連接成功反饋
       72         videoElement.onloadstart = function(){
       73           console.log(`<video>視頻方式${cameraConfigs[index].name}連接成功`);
       74           showConnectionStatus(`<video>視頻方式${cameraConfigs[index].name}連接成功`, 'success');
       75         };
       76       }
       77     });
       78   }
       79 
       80   // 處理流錯誤
       81   function handleStreamError(container) {
       82     const videoElement = container.querySelector('.video-stream');
       83     const label = container.querySelector('.camera-label');
       84 
       85     if (videoElement) {
       86       videoElement.style.display = 'none';
       87     }
       88 
       89     if (label) {
       90       label.style.color = '#ff4d4f';
       91       label.textContent = label.textContent + ' (離線)';
       92     }
       93 
       94     container.style.backgroundColor = '#333';
       95     container.innerHTML += `
       96     <div style="color:white;display:flex;justify-content:center;align-items:center;height:100%;position:absolute;top:0;left:0;right:0;bottom:0;">
       97       視頻流無法加載
       98     </div>
       99   `;
      100     //showConnectionStatus('視頻流連接失敗', 'error');
      101 
      102   }
      103 
      104   // 監聽每個視頻區域div的用戶點擊事件
      105   //每個視頻div結構如下:
      106   // <div class="video-container" data-camera="1">
      107   //   <video class="video-stream" autoplay muted></video>
      108   //   <div class="camera-label"></div>
      109   // </div>
      110   videoContainers.forEach(container => {
      111     container.addEventListener('click', function () {
      112       const videoElement = this.querySelector('.video-stream');
      113       const cameraId = this.getAttribute('data-camera');
      114       //從配置變量中獲取到對應視頻的完整配置信息
      115       const cameraConfig = cameraConfigs.find(c => c.id === Number(cameraId));
      116 
      117       if (videoElement && videoElement.srcObject && cameraConfig) {
      118         modalVideo.srcObject = videoElement.srcObject;
      119         modalCameraLabel.textContent = cameraConfig.name;
      120         modal.style.display = 'flex';
      121 
      122         modalVideo.play().catch(e => console.error('彈窗視頻播放失敗:', e));
      123       }
      124     });
      125   });
      126 
      127 
      128   // 關閉彈窗
      129   if (closeBtn) {
      130     closeBtn.addEventListener('click', function () {
      131       modal.style.display = 'none';
      132       modalVideo.pause();
      133       modalVideo.srcObject = null;
      134     });
      135   }
      136 
      137 
      138   // 通過webrtc-streamer工具顯示視頻
      139   setupVideoStreams();
      140 
      141   // 頁面卸載時清理資源----通過頁面事件監聽
      142   window.addEventListener('beforeunload', function () {
      143     // webrtcInstances.forEach(instance => {
      144     //   instance.instance.disconnect();//實例斷開連接
      145     // });
      146     //------修訂完善
      147     globalWebrtcInstances.forEach(instance => {
      148       if (instance && instance.instance) {
      149         instance.instance.disconnect();
      150       }
      151     });
      152 
      153   });
      154 
      155 
      156 
      157 }

       

      3.2.3 模式二:websocketurl模式的處理

        1 //websocket模式顯示邏輯
        2 function uswebsocketmode(url){
        3   //websocket canvas div 待切換新結構梳理
        4   // <div class="chartdata" id="videoGrid">//下面包含4個視頻區域
        5     // <div class="video-container" data-camera="1">
        6     //   <canvas class="videoCanvas"></canvas>
        7     //   <div class="camera-label"></div>
        8     // </div>
        9     // <div class="video-container" data-camera="2">
       10     //   <canvas class="videoCanvas"></canvas>
       11     //   <div class="camera-label"></div>
       12     // </div>
       13     // <div class="video-container" data-camera="3">
       14     //   <canvas class="videoCanvas"></canvas>
       15     //   <div class="camera-label"></div>
       16     // </div>
       17     // <div class="video-container" data-camera="4">
       18     //   <canvas class="videoCanvas"></canvas>
       19     //   <div class="camera-label"></div>
       20     // </div>
       21   // </div>
       22 
       23   //原有老結構
       24   // <div class="chartdata" id="videoGrid">
       25   //   <!-- 四個視頻區域 -->
       26   //   <div class="video-container" data-camera="1">
       27   //     <video class="video-stream" autoplay muted></video>
       28   //     <div class="camera-label"></div>
       29   //   </div>
       30   //   <div class="video-container" data-camera="2">
       31   //     <video class="video-stream" autoplay muted></video>
       32   //     <div class="camera-label"></div>
       33   //   </div>
       34   //   <div class="video-container" data-camera="3">
       35   //     <video class="video-stream" autoplay muted></video>
       36   //     <div class="camera-label"></div>
       37   //   </div>
       38   //   <div class="video-container" data-camera="4">
       39   //     <video class="video-stream" autoplay muted></video>
       40   //     <div class="camera-label"></div>
       41   //   </div>
       42   // </div>
       43 
       44 
       45   const modal = document.getElementById('videoModal');//視頻彈出框  //--------公共操作變量
       46   const modalCameraLabel = document.querySelector('.modal-camera-label');//彈出框底部顯示視頻名稱標識
       47   var modalcanvas = null;
       48   var modalctx = null;
       49   //var cameraId = null;
       50   var currentModalCameraId = null; // 當前彈出框顯示的攝像頭ID
       51 
       52 
       53   const videoContainers = document.querySelectorAll(".video-container");//獲取4個.video-container視頻區域元素
       54   //1- 先清掉原有默認頁面的div結構內的元素,構建新的canvas元素 
       55   //依次進行替換
       56   videoContainers.forEach(
       57     container => {
       58       //查找原有的<video>元素
       59       const videoElement = container.querySelector(".video-stream");
       60       if (videoElement) {
       61 
       62         //創建<canvas>元素
       63         const canvas = document.createElement("canvas");
       64         canvas.className = 'videoCanvas';
       65         // canvas.width = 320; //設置默認尺寸,即圖片的分辨率、畫布分辨率(和容器大小沒有關系,最終都會在指定容器100%顯示)
       66         // canvas.height = 240;
       67         //以上配置不能自動充滿div區域
       68 
       69 
       70         // // 根據容器大小動態設置,但保持最小分辨率
       71         // const containerRect = container.getBoundingClientRect();
       72         // canvas.width = Math.max(containerRect.width || 320, 160);
       73         // canvas.height = Math.max(containerRect.height || 240, 120);
       74 
       75         // 自適應容器尺寸:填滿容器
       76 
       77         const rect = container.getBoundingClientRect();
       78         canvas.width = Math.max(1, Math.floor(rect.width));
       79         canvas.height = Math.max(1, Math.floor(rect.height));
       80 
       81 
       82         //用<canvas>元素替換<video>元素  --- 通過獲取<video>元素的父節點,來將<video>替換為<canvas>
       83         videoElement.parentNode.replaceChild(canvas, videoElement);
       84       }
       85     }
       86   );
       87 
       88   //2- 初始化canvas基礎信息
       89   var canvasElementArr = [];
       90   var ctx = [];
       91   var canvasElements = document.querySelectorAll(".videoCanvas");//獲取到所有<canvas>  //注意元素是4個,但是后臺返回的不一定是4個
       92   canvasElements.forEach((canvas, index) => {
       93     //注意元素是4個,但是后臺返回的不一定是4個。只需要根據后端返回的圖片流地址個數,按需及可 (后臺若超過4個,則只操作前4個)
       94     if ( index < url.length) {
       95       canvasElementArr[index] = canvas;
       96 
       97       ctx[index] = canvas.getContext('2d');
       98       //繪制初始狀態 ---似乎沒什么用
       99        ctx[index].fillStyle = '#333';
      100       ctx[index].fillRect(0, 0, canvas.width, canvas.height);
      101       ctx[index].fillStyle = 'white';
      102       ctx[index].font = '24px Arial'
      103       ctx[index].textAlign = 'center';
      104       ctx[index].fillText('正在連接...', canvas.width / 2, canvas.height / 2);
      105 
      106       console.log("ctx["+index+"]",ctx[index]);
      107     }
      108   })
      109 
      110   //3- 構建幀展示邏輯 ---- 若干個區域同時接收圖片幀,要考慮異步和實時性
      111   function displayFrame(blob,ctx,canvas){
      112 
      113     //追加:--檢查參數有效性
      114     if (!blob || !ctx || !canvas) {
      115       console.warn('displayFrame: 無效參數');
      116       return;
      117     }
      118 
      119     const img = new Image();
      120 
      121     //追加:--設置超時機制,防止圖片加載卡死
      122     const loadTimeout = setTimeout(() => {
      123       console.warn('圖片加載超時');
      124       if (img.src) {
      125         URL.revokeObjectURL(img.src);
      126       }
      127       img.onload = null;
      128       img.onerror = null;
      129     }, 2000); // 2秒超時
      130 
      131     // 將超時定時器添加到全局管理數組
      132     globalTimeouts.push(loadTimeout);
      133 
      134 
      135     // img.onload = function(){//回調函數
      136     //   //1.先清除畫布信息
      137     //   ctx.clearRect(0,0,canvas.width,canvas.height);
      138 
      139     //   //2.計算縮放比
      140     //   const scale = Math.min(canvas.width/img.width,canvas.height/img.height);
      141     //   const x = (canvas.width - img.width * scale)/2;
      142     //   const y = (canvas.height - img.height * scale)/2;
      143 
      144     //   //3.繪制圖片在畫布
      145     //   ctx.drawImage(img,x,y,img.width*scale,img.height*scale);
      146 
      147     //   //4.將圖像引用取消
      148     //   URL.revokeObjectURL(img.src);
      149     // };
      150 
      151     // //補充圖片的加載失敗異常事件邏輯
      152     // img.onerror = function () {
      153     //   console.error('圖片幀函數----圖片加載失敗');
      154     //   ctx.fillStyle = '#ff4d4f';
      155     //   ctx.fillRect(0, 0, canvas.width, canvas.height);
      156     //   ctx.fillStyle = 'white';
      157     //   ctx.font = '14px Arial';
      158     //   ctx.textAlign = 'center';
      159     //   ctx.fillText('圖片加載失敗', canvas.width / 2, canvas.height / 2);
      160     // };
      161 
      162     //修復:內存管理
      163     //--------------重新定義onload事件和onerror事件
      164     const onLoadHandler = function(){
      165 
      166       //追加: --0. 清理超時定時器
      167       clearTimeout(loadTimeout);
      168       try {
      169         //1.先清除畫布信息
      170         ctx.clearRect(0, 0, canvas.width, canvas.height);
      171 
      172         //2.計算縮放比
      173         const scale = Math.min(canvas.width / img.width, canvas.height / img.height);
      174         const x = (canvas.width - img.width * scale) / 2;
      175         const y = (canvas.height - img.height * scale) / 2;
      176 
      177         //3.繪制圖片在畫布
      178         ctx.drawImage(img, x, y, img.width * scale, img.height * scale);
      179 
      180       } catch (error) {
      181         console.error('繪制圖片時出錯:', error);
      182       } finally {
      183         //4.清理資源
      184         URL.revokeObjectURL(img.src);
      185         img.onload = null;
      186         img.onerror = null;
      187         img.src = ''; // 清空src引用
      188       }
      189 
      190     };
      191 
      192     const onErrorHandler = function(){
      193 
      194       // 清理超時定時器
      195       clearTimeout(loadTimeout);
      196 
      197       console.error('圖片幀函數----圖片加載失敗');
      198 
      199       try {
      200         ctx.fillStyle = '#ff4d4f';
      201         ctx.fillRect(0, 0, canvas.width, canvas.height);
      202         ctx.fillStyle = 'white';
      203         ctx.font = '14px Arial';
      204         ctx.textAlign = 'center';
      205         ctx.fillText('圖片加載失敗', canvas.width / 2, canvas.height / 2);
      206       } catch (error) {
      207         console.error('繪制錯誤狀態時出錯:', error);
      208       } finally {
      209         //清理資源
      210         URL.revokeObjectURL(img.src);
      211         img.onload = null;
      212         img.onerror = null;
      213         img.src = ''; // 清空src引用
      214       }
      215 
      216     };
      217 
      218     img.onload = onLoadHandler;//配合內部的資源管理
      219     img.onerror = onErrorHandler;//配合內部的資源管理
      220     img.src = URL.createObjectURL(blob);
      221 
      222   }
      223 
      224 
      225 //4- 構建網頁客戶端連接WebSocket服務端
      226 //注意會有多個websocket(每個獨立的socket連接一個攝像頭數據(一個機器人有1-多個攝像頭))
      227 
      228 var ws=[];//用于存儲websocket連接實例(網頁客戶端連接服務端)
      229 function connectWebSocket(){
      230   // 實例化websocket,并配置特有的官方監聽事件
      231   url.forEach((urlitem,index)=>{
      232 
      233     // //0 檢查是否已連接
      234     // if(ws[index] && ws[index].readyState === WebSocket.OPEN){
      235     //   console.log(`WebSocket[${index}]已經連接,跳過重復連接`);
      236     //   return;
      237     // }
      238 
      239     //0 嚴格檢查并清理已存在的連接
      240     if (ws[index]) {
      241       if (ws[index].readyState === WebSocket.OPEN || ws[index].readyState === WebSocket.CONNECTING) {
      242         console.log(`WebSocket[${index}]已經連接或正在連接,跳過重復連接`);
      243         return;
      244       } else {
      245         // 清理無效連接
      246         try {
      247           ws[index].close();
      248           ws[index] = null;
      249         } catch (e) {
      250           console.log(`清理無效連接時出錯: ${e.message}`);
      251         }
      252       }
      253     }
      254 
      255     try {
      256       // 1 實例化
      257       ws[index] = new WebSocket(urlitem);
      258       globalWebSocketInstances[index] = ws[index];
      259 
      260       // 2 配置監聽事件
      261       //-------- 2.1 onopen事件
      262       ws[index].onopen = function () {
      263         console.log("ws[" + index + "]:" + urlitem + "連接已建立,開始監聽服務端WebSocket數據");
      264         //showConnectionStatus(`攝像頭${index + 1}連接成功`, 'success');//后面換vue框架自帶的信息提醒框!
      265         reconnectAttempts[index] = 0; //重置重連計數
      266       }
      267 
      268       //-------- 2.2 onmessage事件---核心事件
      269       ws[index].onmessage = function (event) {
      270         if (event.data instanceof Blob) {
      271           //displayFrame(event.data,ctx[index]);//調用幀顯示函數----[將幀顯示在對應的canvas區域] function displayFrame(blob,ctx) canvasElementArr
      272           displayFrame(event.data, ctx[index], canvasElementArr[index]);//始終小窗口需要渲染
      273 
      274           // 如果當前索引與彈出框顯示的攝像頭索引匹配,且彈出框正在顯示,則同時渲染彈出框
      275           if (currentModalCameraId && (index === currentModalCameraId - 1) && modal && modal.style.display === 'flex' && modalctx && modalcanvas) {
      276             displayFrame(event.data, modalctx, modalcanvas);
      277           }
      278         }
      279       }
      280 
      281       //-------- 2.3 onclose事件
      282       ws[index].onclose = function (event) {
      283         console.log("ws[" + index + "]:" + urlitem + "連接已關閉", event.code, event.reason);
      284 
      285         //------------補充:自動重連邏輯
      286         if (!reconnectAttempts[index]) reconnectAttempts[index] = 0;
      287 
      288         if (reconnectAttempts[index] < MAX_RECONNECT_ATTEMPTS) {
      289           reconnectAttempts[index]++;
      290           showConnectionStatus(`攝像頭${index + 1}重連中(${reconnectAttempts[index]}/${MAX_RECONNECT_ATTEMPTS})`, 'warning');////后續調用vue自身方法
      291 
      292           //補充
      293           // 清理該連接的舊定時器
      294           if (reconnectTimeouts[index]) {
      295             clearTimeout(reconnectTimeouts[index]);
      296           }
      297 
      298           const timeoutid = setTimeout(() => {
      299             console.log(`嘗試重連ws[${index}], 第${reconnectAttempts[index]}次`);
      300 
      301             //補充
      302             // 清理連接狀態
      303             if (ws[index]) {
      304               try {
      305                 ws[index].close();
      306               } catch (e) { }
      307               ws[index] = null;
      308             }
      309             connectSingleWebSocket(urlitem, index);
      310             //補充
      311             reconnectTimeouts[index] = null;
      312           }, RECONNECT_DELAY);
      313 
      314           //追加內存管理
      315           globalTimeouts.push(timeoutid);
      316           reconnectTimeouts[index] = timeoutid;
      317 
      318         } else {
      319           showConnectionStatus(`攝像頭${index + 1}連接失敗`, 'error');////后續調用vue自身方法
      320           //顯示連接失敗狀態
      321           if (ctx[index] && canvasElementArr[index]) {
      322             ctx[index].fillStyle = '#ff4d4f';
      323             ctx[index].fillRect(0, 0, canvasElementArr[index].width, canvasElementArr[index].height);
      324             ctx[index].fillStyle = 'white';
      325             ctx[index].font = '14px Arial';
      326             ctx[index].textAlign = 'center';
      327             ctx[index].fillText('ws[index].onclose事件連接失敗', canvasElementArr[index].width / 2, canvasElementArr[index].height / 2);
      328           }
      329         }
      330       };//onclose事件
      331 
      332       //-------- 2.4 onerror事件
      333       ws[index].onerror = function (error) {
      334         console.log("ws[" + index + "]:" + urlitem + "連接出現錯誤:" + error);
      335         showConnectionStatus(`攝像頭${index + 1}連接錯誤`, 'error');//后續調用vue自身方法
      336       };//onerror事件
      337     } catch (error) {
      338       console.error(`創建WebSocket[${index}]失敗:`, error);
      339       showConnectionStatus(`攝像頭${index + 1}創建失敗`, 'error');
      340     }
      341   });
      342 }
      343 
      344 //5- 構建網頁客戶端斷開連接WebSocket服務端
      345 function disconnectWebSocket(){
      346   if(ws){
      347     ws.forEach((wsitem,index)=>{
      348       if(wsitem){
      349         wsitem.close();
      350         ws[index] = null;//恢復初始狀態
      351       }
      352     });
      353     ws = [];//恢復暫存數組初始狀態
      354   }
      355 }
      356 
      357   //6- 執行連接函數調用 (最多內部連4個websocket)
      358   connectWebSocket();
      359 
      360   //7- 執行調用關閉
      361   // 頁面卸載時清理資源----通過頁面事件監聽
      362   window.addEventListener('beforeunload', function () {
      363     disconnectWebSocket();
      364   });
      365 
      366   //8- 視頻區域div點擊事件 彈出彈出框放大視頻顯示   ---- 彈出框<video>也需要替換為<canvas>!
      367   videoContainers.forEach(container => {
      368     container.addEventListener('click',function(){
      369       //1 先把原有彈出框<video>修改為<canvas>
      370       //原有結構參考
      371       // <div id="videoModal" class="modal">
      372       //   <div class="modal-content">
      373       //     <span class="close-btn">&times;</span>
      374       //     <video id="modalVideo" autoplay controls></video>
      375       //     <div class="modal-camera-label"></div>
      376       //   </div>
      377       // </div>
      378 
      379       // ---1.1 先查找到要被替換元素本身
      380       const videoelement = document.querySelector('#modalVideo');
      381       if(videoelement){
      382         // ---1.2 再創建一個新的替換元素
      383         const popcanvas = document.createElement("canvas");
      384         // ---1.3 新元素沿用原來的id--換個新的吧
      385         //popcanvas.id = 'modalVideo';
      386         popcanvas.id = 'modalCanvas';
      387 
      388         // //補充:設置canvas內圖片的分辨率
      389         // popcanvas.width = 800;
      390         // popcanvas.height = 600;
      391         //以上匹配會導致畫布不能充滿div區域;
      392 
      393         // 讓彈出框canvas自適應彈窗區域
      394         const modalContent = modal.querySelector('.modal-content') || modal;
      395         const mrect = modalContent.getBoundingClientRect();
      396         popcanvas.width = Math.max(1, Math.floor(mrect.width));
      397         popcanvas.height = Math.max(1, Math.floor(mrect.height));
      398         // ---1.4 通過被替換元素的直接父元素,將被替換元素替換為新元素
      399         videoelement.parentNode.replaceChild(popcanvas,videoelement);
      400 
      401       }
      402 
      403       // //2 給彈出框內的新元素<canvas>設置基礎配置:canvas、ctx
      404       // var modalcanvas = document.getElementById('modalCanvas');
      405       // var modalctx = modalcanvas.getContext('2d');
      406       // modalctx.fillRect(0,0,modalcanvas.width,modalcanvas.height);
      407       // modalctx.fillStyle = 'red';
      408       // modalctx.font = '24px Arial';
      409       // modalctx.textAlign = 'center';
      410       // modalctx.fillText('等待連接...',modalcanvas.width/2,modalcanvas.height/2);
      411       //--------------------------------------------------
      412       //注意:--------以上這些代碼可能后續調試需要放在下方if內部代碼:modal.style.display = 'flex'; //視頻彈出框整體div顯示下方。因為沒顯示前操作canvas的width和height可能不起作用
      413 
      414       //3 構建視頻配置信息對象
      415       const canvasElement = this.querySelector('.videoCanvas'); //被點擊的canvas元素
      416       //cameraId = this.getAttribute('data-camera');//獲取被點擊的視頻div區域編號(注意,從1開始)
      417       const clickedCameraId = this.getAttribute('data-camera');//獲取被點擊的視頻div區域編號(注意,從1開始)
      418       currentModalCameraId = clickedCameraId; // 更新當前彈出框顯示的攝像頭ID
      419 
      420       // const modal = document.getElementById('videoModal');//視頻彈出框
      421       // const modalCameraLabel = document.querySelector('.modal-camera-label');//彈出框底部顯示視頻名稱標識
      422 
      423       //根據點擊的下標,獲取對應的已有的ws實例,執行圖像渲染
      424       //if (canvasElement && cameraId && ws[cameraId - 1] != null) {
      425       if (canvasElement && clickedCameraId && ws[clickedCameraId - 1] != null) {
      426 
      427         //modalCameraLabel.textContent = 'camera'+ cameraId; //顯示視頻編號名稱
      428         modalCameraLabel.textContent = 'camera'+ clickedCameraId; //顯示視頻編號名稱
      429         modal.style.display = 'flex'; //視頻彈出框整體div顯示
      430 
      431         //上方外層移到此處
      432         //給彈出框內的新元素<canvas>設置基礎配置:canvas、ctx
      433         modalcanvas = document.getElementById('modalCanvas');
      434         modalctx = modalcanvas.getContext('2d');
      435 
      436         //補充:畫布自適應顯示 監聽窗口尺寸變化,保持彈窗canvas自適應
      437         const resizeModalCanvas = () => {
      438           const modalContent = modal.querySelector('.modal-content') || modal;
      439           const mrect = modalContent.getBoundingClientRect();
      440           const w = Math.max(1, Math.floor(mrect.width));
      441           const h = Math.max(1, Math.floor(mrect.height));
      442           if (modalcanvas.width !== w || modalcanvas.height !== h) {
      443             modalcanvas.width = w;
      444             modalcanvas.height = h;
      445           }
      446         };
      447 
      448         //window.addEventListener('resize', resizeModalCanvas);
      449         // 移除之前的resize監聽器,避免重復添加
      450         if (resizeHandler) {
      451           window.removeEventListener('resize', resizeHandler);
      452         }
      453         resizeHandler = resizeModalCanvas;
      454         window.addEventListener('resize', resizeHandler);
      455         globalEventListeners.push({element: window, event: 'resize', handler: resizeHandler});
      456 
      457         resizeModalCanvas();
      458         modalctx.fillStyle = '#333';
      459         modalctx.fillRect(0, 0, modalcanvas.width, modalcanvas.height);
      460         modalctx.fillStyle = 'white';
      461         modalctx.font = '20px Arial';
      462         modalctx.textAlign = 'center';
      463         modalctx.fillText('等待圖像...', modalcanvas.width / 2, modalcanvas.height / 2);
      464 
      465         //canvas 圖片幀顯示
      466         //ws[cameraId - 1].onmessage = function (event) {
      467         //修改為同時渲染小窗口和彈出框
      468         ws[clickedCameraId - 1].onmessage = function (event) {
      469           if (event.data instanceof Blob) {
      470             //displayFrame(event.data, modalctx, modalcanvas);//幀顯示
      471             // 始終渲染小窗口
      472             displayFrame(event.data, ctx[clickedCameraId - 1], canvasElementArr[clickedCameraId - 1]);
      473             // 如果彈出框顯示且是當前攝像頭,也渲染彈出框
      474             if (modal.style.display === 'flex' && modalctx && modalcanvas && currentModalCameraId == clickedCameraId) {
      475               displayFrame(event.data, modalctx, modalcanvas);
      476             }
      477           }
      478         };
      479 
      480         //console.log("ws["+(cameraId-1)+"]"+"彈出框放大顯示已執行!");
      481         console.log("ws["+(clickedCameraId-1)+"]"+"彈出框放大顯示已執行!");
      482       }
      483     });
      484   }
      485   );
      486 
      487   //9- 彈出框關閉按鈕監聽事件
      488   const closeBtn = document.querySelector('.close-btn');//彈出框關閉按鈕區域
      489   if (closeBtn) {
      490     // closeBtn.addEventListener('click', function () {
      491     //   modal.style.display = 'none';
      492     //   //-------- 9.1 恢復對應視頻區域小窗口的圖片幀顯示
      493     //   if (cameraId != null && ws[cameraId-1]) {
      494     //     ws[cameraId - 1].onmessage = function (event) {//重新覆蓋onmessage事件,在小窗口上渲染圖片幀
      495     //       if (event.data instanceof Blob) {
      496     //         displayFrame(event.data, ctx[cameraId - 1], canvasElementArr[cameraId - 1]);
      497     //       }
      498     //     };
      499     //   }
      500     //優化以上內容
      501     // 移除之前的click事件監聽器,避免重復添加
      502     const existingListeners = globalEventListeners.filter(item =>
      503       item.element === closeBtn && item.event === 'click'
      504     );
      505     existingListeners.forEach(item => {
      506       item.element.removeEventListener(item.event, item.handler);
      507     });
      508 
      509     // 定義新的事件處理函數
      510     const closeBtnHandler = function () {
      511       modal.style.display = 'none';
      512       // //-------- 9.1 恢復對應視頻區域小窗口的圖片幀顯示
      513       // if (cameraId != null && ws[cameraId - 1]) {
      514       //   ws[cameraId - 1].onmessage = function (event) {//重新覆蓋onmessage事件,在小窗口上渲染圖片幀
      515       //     if (event.data instanceof Blob) {
      516       //       displayFrame(event.data, ctx[cameraId - 1], canvasElementArr[cameraId - 1]);
      517       //     }
      518       //   };
      519       // }
      520       //以上內容不需要特殊恢復了,因為迭代代碼后,再彈出彈出框的時候,也是一直保證小窗口也在顯示的
      521 
      522       //-------- 9.2 清除彈出框canvas的圖片幀顯示
      523       if (modalctx != null && modalcanvas != null) {
      524         modalctx.clearRect(0, 0, modalcanvas.width, modalcanvas.height);
      525       }
      526 
      527       // 重置彈出框相關變量
      528       modalcanvas = null;
      529       modalctx = null;
      530       currentModalCameraId = null; // 清除當前彈出框攝像頭ID
      531 
      532       //});
      533 
      534       // 移除resize事件監聽器
      535       if (resizeHandler) {
      536         window.removeEventListener('resize', resizeHandler);
      537         // 從全局列表中移除
      538         const index = globalEventListeners.findIndex(item =>
      539           item.element === window && item.event === 'resize' && item.handler === resizeHandler
      540         );
      541         if (index !== -1) {
      542           globalEventListeners.splice(index, 1);
      543         }
      544         resizeHandler = null;
      545       }
      546     };
      547 
      548     // 添加新的事件監聽器并記錄
      549     closeBtn.addEventListener('click', closeBtnHandler);
      550     globalEventListeners.push({ element: closeBtn, event: 'click', handler: closeBtnHandler });
      551   }
      552 
      553   //}
      554 
      555   //追加: 10- 內存優化管理 
      556   // ------------10.1  頁面可見性變化時的資源管理
      557   document.addEventListener('visibilitychange', function () {
      558     if (document.hidden) {
      559       // 頁面切換到后臺時,清理資源但不斷開連接
      560       console.log('頁面切換到后臺,清理部分資源');
      561 
      562       // 清理定時器
      563       globalTimeouts.forEach(timeoutId => {
      564         clearTimeout(timeoutId);
      565       });
      566       globalTimeouts = [];
      567 
      568       // 清理狀態提示元素
      569       const statusElement = document.getElementById('connection-status');
      570       if (statusElement) {
      571         statusElement.remove();
      572       }
      573     } else {
      574       // 頁面重新可見時
      575       console.log('頁面重新可見');
      576     }
      577   });
      578 
      579   // ------------10.2  頁面失去焦點時的額外清理
      580   window.addEventListener('blur', function () {
      581     // 清理可能殘留的定時器
      582     globalTimeouts.forEach(timeoutId => {
      583       clearTimeout(timeoutId);
      584     });
      585     globalTimeouts = [];
      586   });
      587 
      588 }

      3.2.4 其他輔助變量及函數

        1 //圖片幀兼容方案
        2 //全局變量用于資源管理
        3 let globalWebrtcInstances = [];
        4 let globalWebSocketInstances = [];
        5 let reconnectAttempts = {};
        6 const MAX_RECONNECT_ATTEMPTS = 3;
        7 const RECONNECT_DELAY = 2000;
        8 
        9 //新增修復:修復圖片幀方式顯示瀏覽器內存持續增長問題 -----全局定時器和事件監聽器管理
       10 var globalTimeouts = [];
       11 var globalEventListeners = [];
       12 var resizeHandler = null;
       13 var reconnectTimeouts = []; // 管理重連定時器
       14 
       15 //清理之前的連接資源
       16 function cleanupPreviousConnections() {
       17   //清理WebRTC連接
       18   globalWebrtcInstances.forEach(instance => {
       19     if (instance && instance.instance) {
       20       instance.instance.disconnect();
       21     }
       22   });
       23   globalWebrtcInstances = [];
       24 
       25   //清理WebSocket連接
       26   globalWebSocketInstances.forEach((ws, index) => {
       27     if (ws && ws.readyState === WebSocket.OPEN) {
       28       ws.close();
       29     }
       30   });
       31   globalWebSocketInstances = [];
       32 
       33   //-------------------------------------------追加補充部分---開始------------------------------------------------
       34   //清理定時器
       35   globalTimeouts.forEach(timeoutId => {
       36     clearTimeout(timeoutId);
       37   });
       38   globalTimeouts = [];
       39 
       40   //清理事件監聽器
       41   globalEventListeners.forEach(({ element, event, handler }) => {
       42     element.removeEventListener(event, handler);
       43   });
       44   globalEventListeners = [];
       45 
       46   //清理resize監聽器
       47   if (resizeHandler) {
       48     window.removeEventListener('resize', resizeHandler);
       49     resizeHandler = null;
       50   }
       51 
       52   //清理狀態提示元素
       53   const statusElement = document.getElementById('connection-status');
       54   if (statusElement) {
       55     statusElement.remove();
       56   }
       57   //-------------------------------------------追加補充部分---結束------------------------------------------------
       58   
       59   //重置重連計數
       60   reconnectAttempts = {};
       61   
       62   //console.log('已清理所有之前的連接資源');
       63   console.log('已清理所有之前的連接資源、定時器和事件監聽器');
       64 }
       65 
       66 //單個WebSocket重連函數
       67 function connectSingleWebSocket(urlitem, index) {
       68   try {
       69     ws[index] = new WebSocket(urlitem);
       70     globalWebSocketInstances[index] = ws[index];
       71 
       72     //重新綁定事件(復用上面的邏輯)
       73     ws[index].onopen = function () {
       74       console.log(`ws[${index}]:${urlitem} 重連成功`);
       75       //showConnectionStatus(`攝像頭${index + 1}重連成功`, 'success');
       76       reconnectAttempts[index] = 0;
       77     };
       78 
       79     ws[index].onmessage = function (event) {
       80       if (event.data instanceof Blob) {
       81         // 始終渲染小窗口
       82         displayFrame(event.data, ctx[index], canvasElementArr[index]);
       83 
       84         // 如果當前索引與彈出框顯示的攝像頭索引匹配,且彈出框正在顯示,則同時渲染彈出框
       85         if (currentModalCameraId && (index === currentModalCameraId - 1) && modal && modal.style.display === 'flex' && modalctx && modalcanvas) {
       86           displayFrame(event.data, modalctx, modalcanvas);
       87         }
       88 
       89       }
       90     };
       91 
       92     //重連的onclose和onerror事件處理與初始連接相同
       93     ws[index].onclose = function (event) {
       94       console.log(`ws[${index}] 重連后又關閉了`);
       95       if (reconnectAttempts[index] < MAX_RECONNECT_ATTEMPTS) {
       96         reconnectAttempts[index]++;
       97         setTimeout(() => connectSingleWebSocket(urlitem, index), RECONNECT_DELAY);
       98       }
       99     };
      100 
      101     ws[index].onerror = function (error) {
      102       console.error(`ws[${index}] 重連錯誤:`, error);
      103     };
      104 
      105   } catch (error) {
      106     console.error(`重連WebSocket[${index}]失敗:`, error);
      107   }
      108 }

       

      4 模擬數據集構建(視頻切割成圖片幀 25fps)

      此環節是通過ffmpeg命令,將一個視頻按照指定的幀率切割成一張張幀圖片,以作為本地模擬服務端程序的模擬圖片幀數據源。具體操作步驟命令可參考之前博文:http://www.rzrgm.cn/Jesuslovesme/p/18818356

      5 模擬websocket服務端程序編寫

       這個可根據個人擅長的開發語言編寫,因為我主要是為了驗證前端顯示方案是否可以落地,所以后端程序只要能按一定頻率取本地的幀圖片并實時通過websocket發送給前端顯示即可。我通過ai生成了一個驗證測試的C#后端程序。

      基于.NET 6.0的控制臺應用程序代碼如下:

      using Fleck;
      using System.Text.Json;
      using System.Collections.Concurrent;
      using System.Net;
      using System.Net.Http;
      using System.Text;
      
      namespace WebSocketServerApp
      {
          public class Program
          {
              private static readonly ConcurrentDictionary<string, List<IWebSocketConnection>> _connections = new();
              private static readonly ConcurrentDictionary<string, CancellationTokenSource> _cancellationTokens = new();
              private static string _imagePath = @"D:\XX中心\總控系統項目\測試demo\圖片幀demo\dash-video-to-img";//圖片幀文件夾存放位置
              
              public static async Task Main(string[] args)
              {
                  // 啟動HTTP服務器------路線1 
                  var httpTask = StartHttpServer();//用于處理前端的模式請求確認 
                  
                  // 啟動WebSocket服務器  ----路線2
                  var wsTask = StartWebSocketServer(); //對接網頁websocket,傳輸圖片幀
                  
                  Console.WriteLine("=== WebSocket服務器啟動完成 ===");
                  Console.WriteLine("HTTP API服務: http://localhost:8080");
                  Console.WriteLine("WebSocket服務: ws://localhost:8081");
                  Console.WriteLine("");
                  Console.WriteLine("測試URL:");
                  Console.WriteLine("- RTSP模式: http://localhost:8080/api/usdisplay?usid=2");
                  Console.WriteLine("- WebSocket模式: http://localhost:8080/api/usdisplay?usid=3");
                  Console.WriteLine("- WebSocket連接: ws://localhost:8081/api/websocket?usid=3&cam=0");
                  Console.WriteLine("");
                  Console.WriteLine("按 Ctrl+C 停止服務器");
                  
                  // 等待兩個服務器
                  await Task.WhenAll(httpTask, wsTask);
              }
      
      
      
              //異步函數  啟動HTTP服務器
              private static async Task StartHttpServer()
              {
                  //1.
                  var listener = new HttpListener();
                  // 綁定到localhost與127.0.0.1,避免因Host不匹配導致返回系統400且無CORS頭
      
                  //2.
                  listener.Prefixes.Add("http://localhost:8080/");
                  listener.Prefixes.Add("http://127.0.0.1:8080/");
                  // 如需對外訪問,可嘗試開啟以下通配符(需要管理員權限并配置urlacl)
                  // listener.Prefixes.Add("http://+:8080/");
      
                  //3.
                  listener.Start();
                  
                  Console.WriteLine("HTTP服務器已啟動: http://localhost:8080 與 http://127.0.0.1:8080");
                  
                  //4.持續監控
                  while (true)
                  {
                      try
                      {
                          //5.獲取訪問請求上下文
                          var context = await listener.GetContextAsync(); //等待一個即將到來的請求操作
                          _ = Task.Run(() => HandleHttpRequest(context));//開啟一個線程,處理http請求
                      }
                      catch (Exception ex)
                      {
                          Console.WriteLine($"HTTP服務器錯誤: {ex.Message}");
                      }
                  }
              }
              
              //處理http請求
              private static async Task HandleHttpRequest(HttpListenerContext context)
              {
                  try
                  {
                      var request = context.Request;//請求上下文的客戶端request
                      var response = context.Response;//請求上下文的服務端response
      
                      // 設置反饋的CORS頭
                      response.Headers.Add("Access-Control-Allow-Origin", "*");
                      response.Headers.Add("Access-Control-Allow-Methods", "GET, POST, OPTIONS, PUT, DELETE");
                      response.Headers.Add("Access-Control-Allow-Headers", "Content-Type, Authorization, X-Requested-With, Accept, Origin");
                      response.Headers.Add("Access-Control-Allow-Credentials", "true");
                      response.Headers.Add("Access-Control-Max-Age", "86400");
                      
                      if (request.HttpMethod == "OPTIONS")
                      {
                          response.StatusCode = 200;
                          response.Close();
                          return;
                      }
      
                      //如果請求url不為空,且絕對地址為"/api/usdisplay"
                      if (request.Url?.AbsolutePath == "/api/usdisplay")
                      {
                          var usid = request.QueryString["usid"];//獲取到查詢參數usid的值
                          if (string.IsNullOrEmpty(usid))
                          {
                              response.StatusCode = 400;
                              var errorBytes = Encoding.UTF8.GetBytes("Missing usid parameter");
                              await response.OutputStream.WriteAsync(errorBytes, 0, errorBytes.Length);
                          }
                          else
                          {
                              //構建模式反饋json (兩種模式,反饋的json模板不一樣)
                              var configResponse = GetDisplayConfig(usid);
                              var jsonResponse = JsonSerializer.Serialize(configResponse);
                              var responseBytes = Encoding.UTF8.GetBytes(jsonResponse);
                              
                              response.ContentType = "application/json";//設置返回數據類型
                              response.StatusCode = 200;//設置返回狀態碼
                              await response.OutputStream.WriteAsync(responseBytes, 0, responseBytes.Length);
                          }
                      }
                      else
                      {
                          response.StatusCode = 404;
                          var notFoundBytes = Encoding.UTF8.GetBytes("Not Found");
                          await response.OutputStream.WriteAsync(notFoundBytes, 0, notFoundBytes.Length);
                      }
                      
                      response.Close();
                  }
                  catch (Exception ex)
                  {
                      Console.WriteLine($"處理HTTP請求錯誤: {ex.Message}");
                  }
              }
              
              private static async Task StartWebSocketServer()
              {
                  //創建websocket服務端
                  var server = new Fleck.WebSocketServer("ws://0.0.0.0:8081");
                  
                  //服務端socket執行事件監聽
                  server.Start(socket =>
                  {
                      //網頁端觸發socket請求后
                      socket.OnOpen = () =>
                      {
                          var query = ParseQuery(socket.ConnectionInfo.Path);//獲取前端連接服務端的websocket地址(網頁端websocket請求連接地址)
                          var usid = query.GetValueOrDefault("usid", "");//獲取websocket請求連接地址的usid參數值
                          var cam = query.GetValueOrDefault("cam", "");//獲取websocket請求連接地址的cam參數值
                          var connectionKey = $"{usid}-{cam}";//自定義變量,存儲連接信息{usid}-{cam}
      
                          Console.WriteLine($"WebSocket連接建立: usid={usid}, cam={cam}, IP={socket.ConnectionInfo.ClientIpAddress}");
                          //socket.ConnectionInfo.ClientIpAddress 請求連接的客戶端ip
      
                          // 添加連接到管理字典
                          _connections.AddOrUpdate(connectionKey, 
                              new List<IWebSocketConnection> { socket },
                              (key, list) => { list.Add(socket); return list; });
                          
                          // 開始發送圖片幀
                          StartSendingFrames(socket, usid, cam, connectionKey);
                      };
                      
                      socket.OnClose = () =>
                      {
                          var query = ParseQuery(socket.ConnectionInfo.Path);
                          var usid = query.GetValueOrDefault("usid", "");
                          var cam = query.GetValueOrDefault("cam", "");
                          var connectionKey = $"{usid}-{cam}";
                          
                          Console.WriteLine($"WebSocket連接關閉: usid={usid}, cam={cam}");
                          
                          try
                          {
                              // 從管理字典中移除連接
                              if (_connections.TryGetValue(connectionKey, out var connections))
                              {
                                  connections.Remove(socket);
                                  if (connections.Count == 0)
                                  {
                                      _connections.TryRemove(connectionKey, out _);
                                      
                                      // 停止發送任務并釋放資源
                                      if (_cancellationTokens.TryRemove(connectionKey, out var cts))
                                      {
                                          cts.Cancel();
                                          cts.Dispose();
                                          Console.WriteLine($"已清理連接資源: {connectionKey}");
                                      }
                                  }
                              }
                              
                              // 強制垃圾回收釋放內存
                              GC.Collect();
                              GC.WaitForPendingFinalizers();
                          }
                          catch (Exception ex)
                          {
                              Console.WriteLine($"連接關閉時清理資源出錯: {ex.Message}");
                          }
                      };
                      
                      socket.OnError = exception =>
                      {
                          Console.WriteLine($"WebSocket錯誤: {exception.Message}");
                      };
                  });
                  
                  // 保持服務器運行
                  await Task.Delay(Timeout.Infinite);
                  //await Task.Delay(Timeout.Infinite); 的意思是在一個異步方法里“無限等待”,也就是說這個任務永遠不會完成(除非有外部中斷或取消)。
                  //這通常用于讓一個后臺任務保持運行狀態、占位、或者在某些調試場景下阻止應用退出。
              }
      
              private static Dictionary<string, string> ParseQuery(string path)
              {
                  var result = new Dictionary<string, string>();
                  
                  if (string.IsNullOrEmpty(path) || !path.Contains('?'))
                      return result;
                  
                  var queryString = path.Split('?')[1];
                  var pairs = queryString.Split('&');
                  
                  foreach (var pair in pairs)
                  {
                      var keyValue = pair.Split('=');
                      if (keyValue.Length == 2)
                      {
                          result[keyValue[0]] = Uri.UnescapeDataString(keyValue[1]);
                      }
                  }
                  
                  return result;
              }
              
              private static void StartSendingFrames(IWebSocketConnection socket, string usid, string cam, string connectionKey)
              {
                  // 檢查是否已有任務在運行,如果有則先取消
                  if (_cancellationTokens.TryGetValue(connectionKey, out var existingCts))
                  {
                      try
                      {
                          existingCts.Cancel();
                          existingCts.Dispose();
                          Console.WriteLine($"取消已存在的發送任務: usid={usid}, cam={cam}");
                      }
                      catch (Exception ex)
                      {
                          Console.WriteLine($"取消已存在任務時出錯: {ex.Message}");
                      }
                  }
                  
                  var cts = new CancellationTokenSource();
                  _cancellationTokens[connectionKey] = cts;
                  
                  // 使用ConfigureAwait(false)避免上下文切換開銷
                  Task.Run(async () =>
                  {
                      try
                      {
                          if (!Directory.Exists(_imagePath))
                          {
                              Console.WriteLine($"圖片目錄不存在: {_imagePath}");
                              socket.Close();
                              return;
                          }
                          
                          // 優化:只獲取文件路徑,不讀入內存
                          var imageFiles = Directory.GetFiles(_imagePath, "*.jpg")
                                         .Concat(Directory.GetFiles(_imagePath, "*.jpeg"))
                                         .Concat(Directory.GetFiles(_imagePath, "*.png"))
                                         .OrderBy(f => f)
                                         .ToArray();
                          
                          if (imageFiles.Length == 0)
                          {
                              Console.WriteLine($"圖片目錄中沒有找到圖片文件: {_imagePath}");
                              socket.Close();
                              return;
                          }
                          
                          Console.WriteLine($"攝像頭{cam}開始發送圖片幀,共{imageFiles.Length}個文件");
                          
                          var frameIndex = 0;
                          
                          while (!cts.Token.IsCancellationRequested && socket.IsAvailable)
                          {
                              var currentImageFile = imageFiles[frameIndex % imageFiles.Length];
                              
                              try
                              {
                                  // 內存優化:使用using確保資源及時釋放
                                  using (var fileStream = new FileStream(currentImageFile, FileMode.Open, FileAccess.Read))
                                  {
                                      var imageBytes = new byte[fileStream.Length];
                                      await fileStream.ReadAsync(imageBytes, 0, imageBytes.Length, cts.Token);
                                      
                                      // 立即發送后釋放引用
                                      socket.Send(imageBytes);
                                      imageBytes = null; // 顯式釋放引用
                                  }
                                  
                                  Console.WriteLine($"發送圖片幀: usid={usid}, cam={cam}, frame={frameIndex}, file={Path.GetFileName(currentImageFile)}");
                                  
                                  frameIndex++;
                                  
                                  // 降低幀率減少內存壓力:改為10fps,即每100ms發送一幀
                                  await Task.Delay(40, cts.Token);
                                  
                                  // 每100幀強制垃圾回收一次
                                  if (frameIndex % 100 == 0)
                                  {
                                      GC.Collect();
                                      GC.WaitForPendingFinalizers();
                                      Console.WriteLine($"執行垃圾回收: frame={frameIndex}");
                                  }
                              }
                              catch (OperationCanceledException)
                              {
                                  break;
                              }
                              catch (Exception ex)
                              {
                                  Console.WriteLine($"發送圖片幀時出錯: {ex.Message}");
                                  break;
                              }
                          }
                      }
                      catch (Exception ex)
                      {
                          Console.WriteLine($"圖片發送任務異常: {ex.Message}");
                      }
                      finally
                      {
                          Console.WriteLine($"停止發送圖片幀: usid={usid}, cam={cam}");
                      }
                  }, cts.Token);
              }
              
              //獲取模式配置的反饋json
              private static object GetDisplayConfig(string usid)
              {
                  //模擬數據,模擬兩個無人系統,每個1個模式
                  switch (usid)
                  {
                      case "2":
                          // RTSP模式
                          return new
                          {
                              code = 200,
                              success = true,
                              data = new
                              {
                                  mode = "rtspurl",
                                  url = new string[]
                                  {
                                      "rtsp://127.0.0.1:8081",
                                      "rtsp://127.0.0.1:8082",
                                      "rtsp://127.0.0.1:8083"
                                  }
                              }
                          };
                          
                      case "3":
                          // WebSocket模式
                          return new
                          {
                              code = 200,
                              success = true,
                              data = new
                              {
                                  mode = "websocketurl",
                                  url = new string[]
                                  {
                                      "ws://127.0.0.1:8081/api/websocket?usid=3&cam=0",
                                      "ws://127.0.0.1:8081/api/websocket?usid=3&cam=1",
                                      "ws://127.0.0.1:8081/api/websocket?usid=3&cam=2"
                                  }
                              }
                          };
                          
                      default:
                          return new
                          {
                              code = 404,
                              success = false,
                              message = "未找到指定的機器人配置"
                          };
                  }
              }
          }
      }
      View Code

      6 效果展示

       3個小區域的圖片幀顯示:

      圖片

      點擊任意一個小區域,彈出圖片幀放大顯示彈出框:

      圖片

       

      posted @ 2025-08-19 16:26  上清風  閱讀(1312)  評論(3)    收藏  舉報
      主站蜘蛛池模板: 色情无码一区二区三区| 国产精品综合在线免费看| 国产精品乱码人妻一区二区三区| 丰满无码人妻热妇无码区 | 国产一国产看免费高清片| 久久精品国产99久久无毒不卡| 中国熟妇毛多多裸交视频| 精品国产久一区二区三区| 激情自拍校园春色中文| 日本黄页网站免费观看| 亚洲国产精品综合久久网各| 97se综合| 国产欧美精品一区aⅴ影院| 不卡一区二区三区在线视频| 国色天香中文字幕在线视频| 欧美做受视频播放| 少妇又爽又刺激视频| 免费人成年激情视频在线观看| 国产精品一区二区三区蜜臀| 国产色无码专区在线观看 | 爱如潮水日本免费观看视频| 国产一区二区三区av在线无码观看| 亚洲岛国成人免费av| 亚洲AV无码午夜嘿嘿嘿| 日本高清一区二区三| 亚洲第一国产综合| 国产盗摄xxxx视频xxxx| 极品少妇无套内射视频| 日韩国产欧美精品在线| 蜜桃草视频免费在线观看| 亚洲av无码国产在丝袜线观看| 在线精品另类自拍视频| 女人与牲口性恔配视频免费 | 男女18禁啪啪无遮挡激烈网站 | 国产精品视频一区二区三区不卡| 国产精品一二三区蜜臀av| 国产精品自在线拍国产| 国产亚洲国产精品二区| 国产成人免费午夜在线观看| 亚洲乳大丰满中文字幕| 亚洲avav天堂av在线网爱情|