miaofuhao 2 mesi fa
parent
commit
0cc4c60e08
1 ha cambiato i file con 121 aggiunte e 272 eliminazioni
  1. 121 272
      bigScreen3/src/views/dashboard/cpns/VideoSurveillance.vue

+ 121 - 272
bigScreen3/src/views/dashboard/cpns/VideoSurveillance.vue

@@ -8,21 +8,14 @@
8 8
         class="video-item"
9 9
         @click="closeAllVideosAndOpenModal(video)">
10 10
         <div class="video-thumbnail">
11
-          <!-- 直接显示设备的fImg图片,如果没有则显示默认图片 -->
12
-            <div class="video-thumbnail-img">
13
-              <img 
14
-                :src="video.fImg || defaultImg" 
15
-                :alt="video.name" 
16
-                class="camera-image"
17
-                @error="handleImageError($event, index)"
18
-              />
19
-              <div class="play-indicator">
20
-                <svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
21
-                  <circle cx="12" cy="12" r="10" stroke="rgba(7, 185, 255, 0.8)" stroke-width="2"/>
22
-                  <path d="M9 8L17 12L9 16V8Z" fill="rgba(7, 185, 255, 0.9)"/>
23
-                </svg>
24
-              </div>
25
-            </div>
11
+          <video
12
+            :id="`video-${index}`"
13
+            :ref="el => videoEls[index] = el"
14
+            preload="metadata"
15
+            muted
16
+            playsinline
17
+            autoplay
18
+          ></video>
26 19
           <div class="name-badge">{{ video.name }}</div>
27 20
           <div class="time-badge">{{ video.time }}</div>
28 21
         </div>
@@ -41,6 +34,7 @@
41 34
               playsinline 
42 35
               controls 
43 36
               class="video-player" 
37
+              style="width: 100%; height: 100%; object-fit: contain;"
44 38
               autoplay
45 39
               muted
46 40
             ></video>
@@ -57,12 +51,6 @@
57 51
 import { ref, onMounted, watch, onUnmounted } from 'vue'
58 52
 import { getPageListData } from '@/api/index'
59 53
 import videoSrc from '@/assets/mp4/ziyuan.mp4'
60
-import defaultImg from '@/assets/defaultImg.png'
61
-import axios from 'axios'
62
-import { getToken } from '@/utils/auth'
63
-import { ElMessage } from 'element-plus'
64
-
65
-
66 54
 
67 55
 // 监控设备列表(响应式数据)
68 56
 const videoList = ref([])
@@ -89,13 +77,15 @@ const fetchDriveInfoList = async () => {
89 77
         return {
90 78
           name: device.deviceName || device.fRemark || '未知监控设备',
91 79
           time: formattedTime,
92
-          deviceId: device.fDriveid,
93
-          streamUrl: device.url,
94
-          fImg: device.fImg || '' // 保存设备图片地址
80
+          deviceId: device.id || device.deviceId || '',
81
+          streamUrl: 'rtsp://'+device.fAccount+':'+device.fPassword+'@'+device.fIp
95 82
         }
96 83
       })
97 84
       
98
-      // 默认不再自动播放视频,改为点击时播放
85
+      // 数据加载完成后,自动播放所有视频
86
+      setTimeout(() => {
87
+        playAllVideos();
88
+      }, 100);
99 89
     } else {
100 90
       console.warn('接口返回数据格式不正确,使用默认数据')
101 91
     }
@@ -106,21 +96,13 @@ const fetchDriveInfoList = async () => {
106 96
 
107 97
 
108 98
 
109
-// 处理图片加载错误,显示默认图片
110
-function handleImageError(event, index) {
111
-  event.target.src = defaultImg;
112
-  // 更新videoList中的fImg为默认图片,避免重复触发错误
113
-  if (videoList.value[index]) {
114
-    videoList.value[index].fImg = defaultImg;
115
-  }
116
-}
117
-// WebRTC服务器配置
118
-const WEBRTC_CONFIG = {
119
-  server: "http://127.0.0.1:8000",
120
-  connectParams: "rtptransport=tcp&timeout=30&connectiontimeout=5",
121
-  reconnectDelay: 3000,
122
-  timeoutDelay: 10000
123
-}
99
+// 视频元素引用数组
100
+const videoEls = ref([])
101
+
102
+// WebRTC实例数组,用于管理多个视频流
103
+const webRtcServers = ref([]);
104
+// WebRTC服务器地址
105
+const IP = "http://127.0.0.1:8000";
124 106
 
125 107
 // 模态框相关状态
126 108
 const isModalOpen = ref(false)
@@ -130,25 +112,81 @@ const selectedVideo = ref(null)
130 112
 const videoElement = ref(null)
131 113
 // 模态框中使用的WebRTC实例
132 114
 const modalWebRtcServer = ref(null);
133
-// 模态框连接超时引用
134
-let modalConnectTimeout = null;
135 115
 // 监听项目ID变化,重新获取数据
136 116
 watch(() => props.selectedProjectId, () => {
117
+  // 先停止所有视频播放
118
+  stopAllVideos();
137 119
   // 重新获取数据
138 120
   fetchDriveInfoList();
139 121
 }, { immediate: true })
140
-const fDriveid = ref('')
141
-// 关闭所有视频并打开模态框
142
-function closeAllVideosAndOpenModal(video) {
143
-  if (!video.streamUrl) {
144
-    ElMessage.warning('视频流URL为空,无法播放');
145
-
122
+// 播放所有视频
123
+function playAllVideos() {
124
+  // 检查WebRtcStreamer是否可用
125
+  if (!window.WebRtcStreamer) {
126
+    console.warn('WebRtcStreamer不可用,无法播放视频流');
146 127
     return;
147 128
   }
129
+  
130
+  // 为每个视频创建WebRTC连接
131
+  videoList.value.forEach((video, index) => {
132
+    playVideoWithWebRTC(video, index);
133
+  });
134
+}
135
+
136
+// 使用WebRTC播放单个视频
137
+function playVideoWithWebRTC(video, index) {
138
+  const videoEl = videoEls.value[index];
139
+  if (!videoEl) return;
140
+  
141
+  try {
142
+    // 停止之前可能存在的流
143
+    if (webRtcServers.value[index]) {
144
+      webRtcServers.value[index].disconnect();
145
+      webRtcServers.value[index] = null;
146
+    }
147
+    
148
+    // 创建新的WebRTC实例
149
+    webRtcServers.value[index] = new WebRtcStreamer(`video-${index}`, IP);
150
+    console.log(`创建视频${index}的WebRTC实例: ${video.name}`);
151
+    
152
+    // 连接视频流
153
+    if (webRtcServers.value[index]) {
154
+      webRtcServers.value[index].connect(video.streamUrl, null, "rtptransport=tcp&timeout=60", null, null);
155
+      console.log(`视频${index}的WebRTC流已连接: ${video.streamUrl}`);
156
+    }
157
+  } catch (err) {
158
+    console.error(`播放视频${index}失败:`, err);
159
+  }
160
+}
161
+
162
+// 停止所有视频播放
163
+function stopAllVideos() {
164
+  // 停止网格中的所有视频
165
+  webRtcServers.value.forEach((server, index) => {
166
+    if (server) {
167
+      try {
168
+        server.disconnect();
169
+        console.log(`停止视频${index}的播放`);
170
+      } catch (err) {
171
+        console.error(`停止视频${index}播放失败:`, err);
172
+      }
173
+      webRtcServers.value[index] = null;
174
+    }
175
+  });
176
+  
177
+  // 清空数组
178
+  webRtcServers.value = [];
179
+}
180
+
181
+// 关闭所有视频并打开模态框
182
+function closeAllVideosAndOpenModal(video) {
183
+  // 关闭网格中所有视频的播放
184
+  stopAllVideos();
185
+  
148 186
   // 打开模态框播放当前视频
149 187
   selectedVideo.value = video
150 188
   isModalOpen.value = true
151
-  fDriveid.value = video.deviceId
189
+  
152 190
   // 等待模态框完全渲染后初始化视频元素并自动播放
153 191
   setTimeout(() => {
154 192
     if (videoElement.value) {
@@ -161,13 +199,8 @@ function closeAllVideosAndOpenModal(video) {
161 199
   }, 100)
162 200
 }
163 201
 
164
-
165
-
166 202
 // 关闭视频播放模态框
167 203
 function closeModal() {
168
-  // 先触发截图并上传
169
-  captureAndUploadSnapshot();
170
-  
171 204
   // 停止视频播放
172 205
   if (videoElement.value) {
173 206
     videoElement.value.pause()
@@ -178,100 +211,11 @@ function closeModal() {
178 211
   
179 212
   isModalOpen.value = false
180 213
   selectedVideo.value = null
181
-}
182
-
183
-// 截图并上传图片
184
-async function captureAndUploadSnapshot() {
185
-  try {
186
-    if (!videoElement.value || !isModalOpen.value) return;
187
-    
188
-    const video = videoElement.value;
189
-    
190
-    // 检查视频是否有有效画面
191
-    if (!video.videoWidth || !video.videoHeight) {
192
-      console.warn('视频无有效画面,跳过截图');
193
-      return;
194
-    }
195
-    
196
-    // 创建canvas用于截图
197
-    const canvas = document.createElement('canvas');
198
-    
199
-    // 设置canvas尺寸与视频一致
200
-    canvas.width = video.videoWidth;
201
-    canvas.height = video.videoHeight;
202
-    
203
-    try {
204
-      // 绘制视频当前帧到canvas
205
-      const ctx = canvas.getContext('2d');
206
-      ctx.drawImage(video, 0, 0, canvas.width, canvas.height);
207
-      
208
-      // 将canvas转换为Blob
209
-      canvas.toBlob(async (blob) => {
210
-        if (!blob) {
211
-          console.warn('创建截图失败');
212
-          return;
213
-        }
214
-        console.log('截图创建成功,大小:', blob.size);
215
-        try {
216
-          // 创建FormData用于上传,添加文件名参数
217
-          const formData = new FormData();
218
-          // 为文件添加文件名参数,格式为:监控截图_时间戳.jpg
219
-          const fileName = `监控截图_${Date.now()}.jpg`;
220
-          formData.append('file', blob, fileName);
221
-          
222
-          // 上传图片到指定接口
223
-          const response = await axios.post(import.meta.env.VITE_APP_BASE_API +'/file/uploadFile', formData, {
224
-            headers: {
225
-              'Content-Type': 'multipart/form-data',
226
-              'fSource':2,
227
-              'token':getToken()
228
-            },
229
-            timeout: 10000 // 设置10秒超时
230
-          });
231
-          
232
-          console.log('截图上传成功:', response.data);
233
-          // 上传成功后保存图片路径到设备信息
234
-          if (response.data && response.data.data && response.data.data[0]) {
235
-            const fFileurl = response.data.data[0].fFileurl;
236
-            // 获取当前设备ID
237
-           
238
-            
239
-            // 调用更新设备信息接口保存图片路径
240
-            try {
241
-              if (!fDriveid.value) {
242
-                console.warn('设备ID为空,无法保存图片路径');
243
-                return;
244
-              }
245
-              await axios.post(import.meta.env.VITE_APP_BASE_API + 'tbasedriveinfo/updateDriveInfo', {
246
-                data: {
247
-                  fDriveid: fDriveid.value,
248
-                  fImg: fFileurl,
249
-                }
250
-              }, {
251
-                headers: {
252
-                  'Content-Type': 'application/json',
253
-                  'token': getToken(),
254
-                  'fSource':2
255
-                }
256
-              });
257
-              fDriveid.value = ''
258
-              fetchDriveInfoList()
259
-              console.log('图片路径保存成功');
260
-            } catch (saveError) {
261
-              console.error('图片路径保存失败:', saveError.message || saveError);
262
-            }
263
-          }
264
-
265
-        } catch (uploadError) {
266
-          console.error('截图上传失败:', uploadError.message || uploadError);
267
-        }
268
-      }, 'image/jpeg', 0.8); // 质量参数0.8,平衡文件大小和图像质量
269
-    } catch (drawError) {
270
-      console.error('绘制视频帧失败:', drawError);
271
-    }
272
-  } catch (err) {
273
-    console.error('截图失败:', err.message || err);
274
-  }
214
+  
215
+  // 重新加载所有视频
216
+  setTimeout(() => {
217
+    playAllVideos();
218
+  }, 100);
275 219
 }
276 220
 
277 221
 // 初始化并尝试播放视频流
@@ -296,65 +240,13 @@ async function initAndPlayStream() {
296 240
       
297 241
       // 初始化WebRTC实例
298 242
       if (!modalWebRtcServer.value) {
299
-        modalWebRtcServer.value = new WebRtcStreamer("video", WEBRTC_CONFIG.server);
243
+        modalWebRtcServer.value = new WebRtcStreamer("video", IP);
300 244
         console.log('模态框WebRTC实例创建成功')
301
-        
302
-        // 添加错误处理和状态监听
303
-        modalWebRtcServer.value.onStateChange = (state) => {
304
-          console.log('模态框视频状态变化:', state);
305
-          // 当连接断开时尝试重连
306
-          if (state === 'DISCONNECTED' && isModalOpen.value && selectedVideo.value) {
307
-            console.log('模态框视频连接已断开,将在3秒后尝试重连');
308
-            setTimeout(() => {
309
-              if (modalWebRtcServer.value && selectedVideo.value && selectedVideo.value.streamUrl) {
310
-                console.log('尝试重新连接模态框视频');
311
-                try {
312
-                  modalWebRtcServer.value.connect(
313
-                    selectedVideo.value.streamUrl, 
314
-                    null, 
315
-                    WEBRTC_CONFIG.connectParams, 
316
-                    null, 
317
-                    null
318
-                  );
319
-                } catch (reconnectErr) {
320
-                  console.error('模态框视频重连失败:', reconnectErr);
321
-                }
322
-              }
323
-            }, WEBRTC_CONFIG.reconnectDelay);
324
-          }
325
-        };
326 245
       }
327 246
       
328
-      // 设置连接超时处理
329
-      if (!modalConnectTimeout) {
330
-        modalConnectTimeout = setTimeout(() => {
331
-          if (modalWebRtcServer.value && modalWebRtcServer.value.state !== 'STREAMING') {
332
-            console.warn('模态框视频连接超时,尝试重连');
333
-            try {
334
-              modalWebRtcServer.value.disconnect();
335
-              modalWebRtcServer.value.connect(
336
-                streamUrl, 
337
-                null, 
338
-                WEBRTC_CONFIG.connectParams, 
339
-                  null, 
340
-                  null
341
-              );
342
-            } catch (timeoutErr) {
343
-              console.error('模态框视频超时重连失败:', timeoutErr);
344
-            }
345
-          }
346
-        }, WEBRTC_CONFIG.timeoutDelay);
347
-      }
348
-      
349
-      // 连接视频流,使用更可靠的参数
247
+      // 连接视频流
350 248
       if (modalWebRtcServer.value) {
351
-        modalWebRtcServer.value.connect(
352
-          streamUrl, 
353
-          null, 
354
-          WEBRTC_CONFIG.connectParams, 
355
-          null, 
356
-          null
357
-        );
249
+        modalWebRtcServer.value.connect(streamUrl, null , "rtptransport=tcp&timeout=60", null,null);
358 250
         console.log('WebRTC视频流已连接');
359 251
       }
360 252
     } else {
@@ -364,10 +256,12 @@ async function initAndPlayStream() {
364 256
     }
365 257
   } catch (err) {
366 258
     console.error('启动视频流失败:', err);
367
-    // 错误后立即尝试重连
368
-    setTimeout(() => {
369
-      initAndPlayStream();
370
-    }, 2000);
259
+    // 尝试播放默认视频作为后备
260
+    try {
261
+      await playDefaultVideo();
262
+    } catch (fallbackErr) {
263
+      console.error('播放默认视频也失败:', fallbackErr);
264
+    }
371 265
   }
372 266
 }
373 267
 
@@ -391,15 +285,7 @@ async function playDefaultVideo() {
391 285
 // 销毁模态框视频实例
392 286
 const destroyModalVideo = () => {
393 287
   try {
394
-    // 清除模态框连接超时定时器
395
-    if (modalConnectTimeout) {
396
-      clearTimeout(modalConnectTimeout);
397
-      modalConnectTimeout = null;
398
-    }
399
-    
400 288
     if (modalWebRtcServer.value) {
401
-      // 移除事件监听
402
-      modalWebRtcServer.value.onStateChange = null;
403 289
       modalWebRtcServer.value.disconnect()
404 290
       modalWebRtcServer.value = null
405 291
       console.log('模态框WebRTC实例已销毁')
@@ -412,14 +298,15 @@ const destroyModalVideo = () => {
412 298
 // 组件挂载时初始化
413 299
 onMounted(() => {
414 300
   fetchDriveInfoList();
415
-})
416
-  // 移除自动播放逻辑,改为点击时播放
301
+});
417 302
 
418 303
 // 组件卸载时清理资源
419 304
 onUnmounted(() => {
305
+  // 停止所有视频播放
306
+  stopAllVideos();
420 307
   // 销毁模态框视频实例
421 308
   destroyModalVideo();
422
-})
309
+});
423 310
 </script>
424 311
 
425 312
 <style lang="scss" scoped>
@@ -484,58 +371,19 @@ onUnmounted(() => {
484 371
       }
485 372
       
486 373
       .video-thumbnail {
487
-            width: 100%;
488
-            height: 100%;
489
-            position: relative;
490
-            border-radius: 4px;
491
-            overflow: hidden;
492
-            border: 1px solid rgba(7, 185, 255, 0.6);
493
-            box-shadow: 0 0 0 1px rgba(7, 185, 255, 0.2) inset;
494
-            
495
-            .video-thumbnail-img {
496
-              width: 100%;
497
-              height: 100%;
498
-              position: relative;
499
-              overflow: hidden;
500
-              background: linear-gradient(135deg, rgba(0, 30, 60, 0.8) 0%, rgba(0, 51, 102, 0.9) 100%);
501
-            }
502
-            
503
-            .camera-image {
504
-              width: 100%;
505
-              height: 100%;
506
-              object-fit: cover;
507
-              transition: transform 0.3s ease;
508
-            }
509
-            
510
-            .video-item:hover .camera-image {
511
-              transform: scale(1.05);
512
-            }
513
-            
514
-            .play-indicator {
515
-              position: absolute;
516
-              top: 50%;
517
-              left: 50%;
518
-              transform: translate(-50%, -50%);
519
-              opacity: 0.8;
520
-              transition: all 0.3s ease;
521
-              filter: drop-shadow(0 0 5px rgba(7, 185, 255, 0.5));
522
-              background: rgba(0, 0, 0, 0.3);
523
-              border-radius: 50%;
524
-              padding: 8px;
525
-            }
526
-            
527
-            .video-item:hover .play-indicator {
528
-              opacity: 1;
529
-              transform: translate(-50%, -50%) scale(1.1);
530
-              filter: drop-shadow(0 0 8px rgba(7, 185, 255, 0.8));
531
-              background: rgba(0, 0, 0, 0.5);
532
-            }
533
-            
534
-            video {
535
-              width: 100%;
536
-              height: 100%;
537
-              object-fit: cover;
538
-            }
374
+        width: 100%;
375
+        height: 100%;
376
+        position: relative;
377
+        border-radius: 4px;
378
+        overflow: hidden;
379
+        border: 1px solid rgba(7, 185, 255, 0.6);
380
+        box-shadow: 0 0 0 1px rgba(7, 185, 255, 0.2) inset;
381
+        
382
+        video {
383
+          width: 100%;
384
+          height: 100%;
385
+          object-fit: cover;
386
+        }
539 387
 
540 388
         .name-badge {
541 389
           position: absolute;
@@ -586,6 +434,7 @@ onUnmounted(() => {
586 434
     height: 72vh;
587 435
     background-image: url('@/assets/bj2.png');
588 436
     background-size: 100% 100%;
437
+    background-size: cover;
589 438
     background-position: center;
590 439
     background-repeat: no-repeat;
591 440
     border-radius: 6px;
@@ -598,7 +447,7 @@ onUnmounted(() => {
598 447
       flex: 1;
599 448
       display: flex;
600 449
       flex-direction: column;
601
-      height: 100%;
450
+      
602 451
       .video-wrapper {
603 452
         position: relative;
604 453
         flex: 1;
@@ -611,7 +460,7 @@ onUnmounted(() => {
611 460
         .video-player {
612 461
           width: 100%;
613 462
           height: 100%;
614
-          object-fit: cover;
463
+          object-fit: contain;
615 464
           background: #000;
616 465
           cursor: pointer;
617 466
         }