Commit b73e5e103db93a7edfad3a1b51906a27efa1a046

Authored by 王鑫
1 parent 3252e971

fix():修复播放器组件轮播和全部关闭销毁播放器冲突问题

web_src/src/components/DeviceList1078.vue
@@ -148,6 +148,7 @@ export default { @@ -148,6 +148,7 @@ export default {
148 // --- 侧边栏折叠逻辑 --- 148 // --- 侧边栏折叠逻辑 ---
149 updateSidebarState() { 149 updateSidebarState() {
150 this.sidebarState = !this.sidebarState; 150 this.sidebarState = !this.sidebarState;
  151 + // 触发 resize 事件让播放器自适应
151 setTimeout(() => { 152 setTimeout(() => {
152 const event = new Event('resize'); 153 const event = new Event('resize');
153 window.dispatchEvent(event); 154 window.dispatchEvent(event);
@@ -180,37 +181,39 @@ export default { @@ -180,37 +181,39 @@ export default {
180 }, 181 },
181 182
182 // ========================================== 183 // ==========================================
183 - // 【核心修复】1. 左键点击播放逻辑 184 + // 1. 左键点击播放逻辑
184 // ========================================== 185 // ==========================================
185 nodeClick(data, node) { 186 nodeClick(data, node) {
186 if (this.isCarouselRunning) { 187 if (this.isCarouselRunning) {
187 this.$message.warning("请先停止轮播再手动播放"); 188 this.$message.warning("请先停止轮播再手动播放");
188 return; 189 return;
189 } 190 }
190 - // 判断是否为叶子节点(通道),没有子节点视为通道 191 + // 判断是否为叶子节点(通道)
191 if (!data.children || data.children.length === 0) { 192 if (!data.children || data.children.length === 0) {
192 this.playSingleChannel(data); 193 this.playSingleChannel(data);
193 } 194 }
194 }, 195 },
195 196
196 - // 单路播放 API 请求  
197 playSingleChannel(data) { 197 playSingleChannel(data) {
198 - // 假设 data.code 格式为 "deviceId_sim_channel"  
199 - // 根据您的接口调整这里的解析逻辑 198 + // data.code 格式假设为 "deviceId_sim_channel"
200 let stream = data.code.replace('-', '_'); // 容错处理 199 let stream = data.code.replace('-', '_'); // 容错处理
201 let arr = stream.split("_"); 200 let arr = stream.split("_");
202 201
203 - // 假设 ID 结构是: id_sim_channel,取后两段  
204 - if(arr.length < 3) return; 202 + // 容错: 确保能解析出 sim 和 channel
  203 + if (arr.length < 3) {
  204 + console.warn("Invalid channel code:", data.code);
  205 + return;
  206 + }
205 207
206 this.$axios.get(`/api/jt1078/query/send/request/io/${arr[1]}/${arr[2]}`).then(res => { 208 this.$axios.get(`/api/jt1078/query/send/request/io/${arr[1]}/${arr[2]}`).then(res => {
207 - if(res.data.code === 0 || res.data.code === 200) {  
208 - const url = res.data.data.ws_flv; // 或者 wss_flv 209 + if (res.data.code === 0 || res.data.code === 200) {
  210 + // 兼容 http/https 协议
  211 + const url = (location.protocol === "https:") ? res.data.data.wss_flv : res.data.data.ws_flv;
209 const idx = this.windowClickIndex - 1; 212 const idx = this.windowClickIndex - 1;
210 213
211 - // 更新播放地址和信息 214 + // 使用 $set 确保数组响应式更新
212 this.$set(this.videoUrl, idx, url); 215 this.$set(this.videoUrl, idx, url);
213 - this.$set(this.videoDataList, idx, { ...data, videoUrl: url }); 216 + this.$set(this.videoDataList, idx, {...data, videoUrl: url});
214 217
215 // 自动跳到下一个窗口 218 // 自动跳到下一个窗口
216 const maxWindow = parseInt(this.windowNum) || 4; 219 const maxWindow = parseInt(this.windowNum) || 4;
@@ -224,18 +227,15 @@ export default { @@ -224,18 +227,15 @@ export default {
224 }, 227 },
225 228
226 // ========================================== 229 // ==========================================
227 - // 【核心修复】2. 右键菜单逻辑 230 + // 2. 右键菜单逻辑
228 // ========================================== 231 // ==========================================
229 nodeContextmenu(event, data, node) { 232 nodeContextmenu(event, data, node) {
230 - // 只有设备节点(有子级)才显示菜单,通道节点不显示 233 + // 只有父设备节点(有子级)才显示菜单
231 if (data.children && data.children.length > 0) { 234 if (data.children && data.children.length > 0) {
232 this.rightClickNode = node; 235 this.rightClickNode = node;
233 this.contextMenuVisible = true; 236 this.contextMenuVisible = true;
234 this.contextMenuLeft = event.clientX; 237 this.contextMenuLeft = event.clientX;
235 this.contextMenuTop = event.clientY; 238 this.contextMenuTop = event.clientY;
236 -  
237 - // 阻止默认浏览器右键菜单  
238 - // 注意:VehicleList 组件内部也需要处理 @contextmenu.prevent  
239 } 239 }
240 }, 240 },
241 241
@@ -243,7 +243,6 @@ export default { @@ -243,7 +243,6 @@ export default {
243 this.contextMenuVisible = false; 243 this.contextMenuVisible = false;
244 }, 244 },
245 245
246 - // 处理右键菜单命令(一键播放该设备)  
247 handleContextCommand(command) { 246 handleContextCommand(command) {
248 this.hideContextMenu(); 247 this.hideContextMenu();
249 if (command === 'playback') { 248 if (command === 'playback') {
@@ -262,21 +261,20 @@ export default { @@ -262,21 +261,20 @@ export default {
262 else this.windowNum = '4'; 261 else this.windowNum = '4';
263 262
264 // 3. 构造批量请求参数 263 // 3. 构造批量请求参数
265 - // 假设后端接受的格式处理  
266 const ids = channels.map(c => { 264 const ids = channels.map(c => {
267 - // 假设 code 是 id_sim_channel 265 + // 假设 code 是 id_sim_channel,后端需要 sim-channel
268 const parts = c.code.replaceAll('_', '-').split('-'); 266 const parts = c.code.replaceAll('_', '-').split('-');
269 - // 取 sim-channel 部分  
270 return parts.slice(1).join('-'); 267 return parts.slice(1).join('-');
271 }); 268 });
272 269
273 this.$axios.post('/api/jt1078/query/beachSend/request/io', ids).then(res => { 270 this.$axios.post('/api/jt1078/query/beachSend/request/io', ids).then(res => {
274 - if(res.data && res.data.data) { 271 + if (res.data && res.data.data) {
275 const list = res.data.data || []; 272 const list = res.data.data || [];
276 list.forEach((item, i) => { 273 list.forEach((item, i) => {
277 if (channels[i]) { 274 if (channels[i]) {
278 - this.$set(this.videoUrl, i, item.ws_flv);  
279 - this.$set(this.videoDataList, i, { ...channels[i], videoUrl: item.ws_flv }); 275 + const url = (location.protocol === "https:") ? item.wss_flv : item.ws_flv;
  276 + this.$set(this.videoUrl, i, url);
  277 + this.$set(this.videoDataList, i, {...channels[i], videoUrl: url});
280 } 278 }
281 }); 279 });
282 } else { 280 } else {
@@ -297,10 +295,12 @@ export default { @@ -297,10 +295,12 @@ export default {
297 async checkCarouselPermission(actionName) { 295 async checkCarouselPermission(actionName) {
298 if (!this.isCarouselRunning) return true; 296 if (!this.isCarouselRunning) return true;
299 try { 297 try {
300 - await this.$confirm(`正在轮播,"${actionName}"将停止轮播,是否继续?`,'提示',{ type: 'warning' }); 298 + await this.$confirm(`正在轮播,"${actionName}"将停止轮播,是否继续?`, '提示', {type: 'warning'});
301 this.stopCarousel(); 299 this.stopCarousel();
302 return true; 300 return true;
303 - } catch (e) { return false; } 301 + } catch (e) {
  302 + return false;
  303 + }
304 }, 304 },
305 305
306 async closeAllVideo() { 306 async closeAllVideo() {
@@ -313,16 +313,17 @@ export default { @@ -313,16 +313,17 @@ export default {
313 if (!(await this.checkCarouselPermission('关闭当前窗口'))) return; 313 if (!(await this.checkCarouselPermission('关闭当前窗口'))) return;
314 const idx = Number(this.windowClickIndex) - 1; 314 const idx = Number(this.windowClickIndex) - 1;
315 if (this.videoUrl && this.videoUrl[idx]) { 315 if (this.videoUrl && this.videoUrl[idx]) {
316 - this.$confirm(`确认关闭窗口 [${this.windowClickIndex}] ?`, '提示', { type: 'warning' }) 316 + this.$confirm(`确认关闭窗口 [${this.windowClickIndex}] ?`, '提示', {type: 'warning'})
317 .then(() => { 317 .then(() => {
318 this.$set(this.videoUrl, idx, null); 318 this.$set(this.videoUrl, idx, null);
319 this.$set(this.videoDataList, idx, null); 319 this.$set(this.videoDataList, idx, null);
320 - }).catch(()=>{}); 320 + }).catch(() => {
  321 + });
321 } 322 }
322 }, 323 },
323 324
324 // ========================================== 325 // ==========================================
325 - // 4. 轮播逻辑 (保持之前定义的逻辑) 326 + // 4. 轮播逻辑 (补全部分)
326 // ========================================== 327 // ==========================================
327 openCarouselConfig() { 328 openCarouselConfig() {
328 if (this.isCarouselRunning) { 329 if (this.isCarouselRunning) {
@@ -334,25 +335,182 @@ export default { @@ -334,25 +335,182 @@ export default {
334 335
335 stopCarousel() { 336 stopCarousel() {
336 this.isCarouselRunning = false; 337 this.isCarouselRunning = false;
337 - if (this.carouselTimer) clearTimeout(this.carouselTimer); 338 + if (this.carouselTimer) {
  339 + clearTimeout(this.carouselTimer);
  340 + this.carouselTimer = null;
  341 + }
  342 + this.$message.info("轮播已停止");
338 }, 343 },
339 344
340 async startCarousel(config) { 345 async startCarousel(config) {
341 this.carouselConfig = config; 346 this.carouselConfig = config;
  347 +
  348 + // 1. 筛选目标设备
342 let targetNodes = []; 349 let targetNodes = [];
  350 + if (config.sourceType === 'all_online') {
  351 + const collectOnline = (nodes) => {
  352 + nodes.forEach(node => {
  353 + if (node.abnormalStatus === 1) targetNodes.push(node);
  354 + if (node.children && node.children.length > 0) collectOnline(node.children);
  355 + });
  356 + };
  357 + collectOnline(this.deviceTreeData);
  358 + } else {
  359 + targetNodes = config.selectedNodes.filter(n => n.abnormalStatus === 1);
  360 + }
  361 +
  362 + if (targetNodes.length === 0) {
  363 + this.$message.warning("当前范围内没有在线设备可供轮播");
  364 + return;
  365 + }
343 366
344 - // ... (保留您之前的 startCarousel 完整逻辑,这里简略以节省篇幅,请确保您之前的代码已粘贴进来) ...  
345 - // 如果没有之前的代码,请告诉我,我再发一遍完整的 startCarousel  
346 - // 这里简单模拟一个启动 367 + // 2. 初始化状态
  368 + this.carouselDeviceList = targetNodes;
  369 + this.channelBuffer = [];
  370 + this.deviceCursor = 0;
347 this.isCarouselRunning = true; 371 this.isCarouselRunning = true;
348 - this.$message.success("轮播已启动 (需要补全 startCarousel 完整逻辑)"); 372 + this.isWithinSchedule = true;
  373 +
  374 + this.$message.success(`轮播已启动,共 ${targetNodes.length} 台在线设备`);
  375 +
  376 + // 3. 立即执行第一轮
  377 + await this.executeFirstRound();
  378 + },
  379 +
  380 + async executeFirstRound() {
  381 + if (this.windowNum !== this.carouselConfig.layout) {
  382 + this.windowNum = this.carouselConfig.layout;
  383 + }
  384 +
  385 + const batch = await this.fetchNextBatchData();
  386 + if (batch) {
  387 + this.applyVideoBatch(batch);
  388 + this.runCarouselLoop();
  389 + } else {
  390 + this.$message.error("首轮加载失败,尝试重试...");
  391 + this.runCarouselLoop();
  392 + }
  393 + },
  394 +
  395 + runCarouselLoop() {
  396 + if (!this.isCarouselRunning) return;
  397 +
  398 + const {runMode, timeRange, interval} = this.carouselConfig;
  399 +
  400 + // 1. 检查定时
  401 + if (runMode === 'schedule') {
  402 + if (!this.checkTimeRange(timeRange[0], timeRange[1])) {
  403 + this.isWithinSchedule = false;
  404 + if (this.videoUrl.some(v => v)) this.closeAllVideoNoConfirm();
  405 + this.carouselTimer = setTimeout(() => this.runCarouselLoop(), 10000);
  406 + return;
  407 + }
  408 + this.isWithinSchedule = true;
  409 + }
  410 +
  411 + // 2. 计算时间轴
  412 + const PRELOAD_TIME = 15;
  413 + const intervalSec = Math.max(interval, 30);
  414 + const waitTime = (intervalSec - PRELOAD_TIME) * 1000;
  415 +
  416 + // 3. 计时循环
  417 + this.carouselTimer = setTimeout(async () => {
  418 + if (!this.isCarouselRunning) return;
  419 +
  420 + const nextBatch = await this.fetchNextBatchData();
  421 +
  422 + this.carouselTimer = setTimeout(() => {
  423 + if (!this.isCarouselRunning) return;
  424 + if (nextBatch) this.applyVideoBatch(nextBatch);
  425 + this.runCarouselLoop();
  426 + }, PRELOAD_TIME * 1000);
  427 +
  428 + }, waitTime);
  429 + },
  430 +
  431 + applyVideoBatch({urls, infos}) {
  432 + this.videoUrl = new Array(parseInt(this.windowNum)).fill(null);
  433 + setTimeout(() => {
  434 + urls.forEach((url, index) => {
  435 + setTimeout(() => {
  436 + this.$set(this.videoUrl, index, url);
  437 + this.$set(this.videoDataList, index, infos[index]);
  438 + }, index * 100);
  439 + });
  440 + }, 200);
  441 + },
  442 +
  443 + async fetchNextBatchData() {
  444 + let pageSize = parseInt(this.windowNum) || 4;
  445 + if (isNaN(pageSize)) pageSize = 4;
  446 +
  447 + // 填充缓冲区
  448 + let safetyCounter = 0;
  449 + while (this.channelBuffer.length < pageSize && safetyCounter < 100) {
  450 + safetyCounter++;
  451 + if (this.deviceCursor >= this.carouselDeviceList.length) {
  452 + this.deviceCursor = 0;
  453 + }
  454 +
  455 + const device = this.carouselDeviceList[this.deviceCursor];
  456 + if (device && device.children && device.children.length > 0) {
  457 + const codes = device.children
  458 + .filter(child => !child.disabled)
  459 + .map(child => child.code);
  460 + this.channelBuffer.push(...codes);
  461 + }
  462 + this.deviceCursor++;
  463 + }
  464 +
  465 + if (this.channelBuffer.length === 0) return null;
  466 +
  467 + const currentCodes = this.channelBuffer.splice(0, pageSize);
  468 + const streamParams = currentCodes.map(c => c.replaceAll('_', '-').split('-').slice(1).join('-'));
  469 +
  470 + try {
  471 + const res = await this.$axios.post('/api/jt1078/query/beachSend/request/io', streamParams, {timeout: 20000});
  472 + if (res.data && res.data.data) {
  473 + const resultList = res.data.data;
  474 + const urls = new Array(pageSize).fill('');
  475 + const infos = new Array(pageSize).fill(null);
  476 +
  477 + resultList.forEach((item, i) => {
  478 + if (i < currentCodes.length) {
  479 + const url = (location.protocol === "https:") ? item.wss_flv : item.ws_flv;
  480 + urls[i] = url;
  481 + infos[i] = {
  482 + code: currentCodes[i],
  483 + name: `通道 ${i + 1}`,
  484 + videoUrl: url
  485 + };
  486 + }
  487 + });
  488 + return {urls, infos};
  489 + }
  490 + } catch (e) {
  491 + console.error("批量请求流地址失败", e);
  492 + }
  493 + return null;
349 }, 494 },
350 495
351 - // ... 其他轮播辅助函数 (fetchNextBatchData, applyVideoBatch 等) ...  
352 - // 请确保这些函数存在,否则轮播会报错  
353 - fetchNextBatchData() {},  
354 - runCarouselLoop() {},  
355 - applyVideoBatch() {}, 496 + checkTimeRange(startStr, endStr) {
  497 + if (!startStr || !endStr) return true;
  498 + const now = new Date();
  499 + const current = now.getHours() * 3600 + now.getMinutes() * 60 + now.getSeconds();
  500 + const parse = (str) => {
  501 + const [h, m, s] = str.split(':').map(Number);
  502 + return h * 3600 + m * 60 + s;
  503 + };
  504 + const start = parse(startStr);
  505 + const end = parse(endStr);
  506 + if (end < start) return current >= start || current <= end;
  507 + return current >= start && current <= end;
  508 + },
  509 +
  510 + closeAllVideoNoConfirm() {
  511 + this.videoUrl = new Array(parseInt(this.windowNum)).fill(null);
  512 + this.videoDataList = new Array(parseInt(this.windowNum)).fill(null);
  513 + },
356 514
357 handleBeforeUnload(e) { 515 handleBeforeUnload(e) {
358 if (this.isCarouselRunning) { 516 if (this.isCarouselRunning) {
@@ -419,7 +577,10 @@ export default { @@ -419,7 +577,10 @@ export default {
419 cursor: pointer; 577 cursor: pointer;
420 color: #606266; 578 color: #606266;
421 } 579 }
422 -.fold-btn:hover { color: #409EFF; } 580 +
  581 +.fold-btn:hover {
  582 + color: #409EFF;
  583 +}
423 584
424 .header-right-info { 585 .header-right-info {
425 margin-left: auto; 586 margin-left: auto;
@@ -442,19 +603,24 @@ export default { @@ -442,19 +603,24 @@ export default {
442 position: fixed; 603 position: fixed;
443 background: #fff; 604 background: #fff;
444 border: 1px solid #EBEEF5; 605 border: 1px solid #EBEEF5;
445 - box-shadow: 0 2px 12px 0 rgba(0,0,0,.1); 606 + box-shadow: 0 2px 12px 0 rgba(0, 0, 0, .1);
446 z-index: 3000; 607 z-index: 3000;
447 border-radius: 4px; 608 border-radius: 4px;
448 padding: 5px 0; 609 padding: 5px 0;
449 min-width: 120px; 610 min-width: 120px;
450 } 611 }
  612 +
451 .menu-item { 613 .menu-item {
452 padding: 8px 15px; 614 padding: 8px 15px;
453 font-size: 14px; 615 font-size: 14px;
454 color: #606266; 616 color: #606266;
455 cursor: pointer; 617 cursor: pointer;
456 } 618 }
457 -.menu-item:hover { background: #ecf5ff; color: #409EFF; } 619 +
  620 +.menu-item:hover {
  621 + background: #ecf5ff;
  622 + color: #409EFF;
  623 +}
458 624
459 .carousel-status { 625 .carousel-status {
460 margin-left: 15px; 626 margin-left: 15px;
web_src/src/components/common/EasyPlayer.vue
@@ -17,9 +17,9 @@ @@ -17,9 +17,9 @@
17 </div> 17 </div>
18 </div> 18 </div>
19 19
20 - <div :id="uniqueId" ref="container" class="player-box"></div> 20 + <div :id="uniqueId" :key="uniqueId" ref="container" class="player-box"></div>
21 21
22 - <div v-if="!hasUrl" class="idle-mask"> 22 + <div v-show="!hasUrl" class="idle-mask">
23 <div class="idle-text">无信号</div> 23 <div class="idle-text">无信号</div>
24 </div> 24 </div>
25 25
@@ -53,6 +53,7 @@ export default { @@ -53,6 +53,7 @@ export default {
53 }, 53 },
54 data() { 54 data() {
55 return { 55 return {
  56 + // 初始 ID
56 uniqueId: `player-box-${Date.now()}-${Math.floor(Math.random() * 1000)}`, 57 uniqueId: `player-box-${Date.now()}-${Math.floor(Math.random() * 1000)}`,
57 playerInstance: null, 58 playerInstance: null,
58 59
@@ -68,19 +69,17 @@ export default { @@ -68,19 +69,17 @@ export default {
68 controlTimer: null, 69 controlTimer: null,
69 retryCount: 0, 70 retryCount: 0,
70 71
71 - // 【配置区域】在这里修改 true/false 即可生效  
72 controlsConfig: { 72 controlsConfig: {
73 - showBottomBar: true, // 是否显示底栏  
74 - showSpeed: false, // 【关键】设为 false 隐藏底部网速  
75 - showCodeSelect: false, // 【关键】设为 false 隐藏解码选择  
76 -  
77 - showPlay: true, // 播放暂停按钮  
78 - showAudio: true, // 音量按钮  
79 - showStretch: true, // 拉伸按钮  
80 - showScreenshot: true, // 截图按钮  
81 - showRecord: true, // 录制按钮  
82 - showZoom: true, // 电子放大  
83 - showFullscreen: true, // 全屏按钮 73 + showBottomBar: true,
  74 + showSpeed: false,
  75 + showCodeSelect: false,
  76 + showPlay: true,
  77 + showAudio: true,
  78 + showStretch: true,
  79 + showScreenshot: true,
  80 + showRecord: true,
  81 + showZoom: true,
  82 + showFullscreen: true,
84 } 83 }
85 }; 84 };
86 }, 85 },
@@ -91,14 +90,12 @@ export default { @@ -91,14 +90,12 @@ export default {
91 if (typeof url === 'string') return url.length > 0; 90 if (typeof url === 'string') return url.length > 0;
92 return !!url.videoUrl; 91 return !!url.videoUrl;
93 }, 92 },
94 - // 生成控制 CSS 类名  
95 playerClassOptions() { 93 playerClassOptions() {
96 const c = this.controlsConfig; 94 const c = this.controlsConfig;
97 return { 95 return {
98 'hide-bottom-bar': !c.showBottomBar, 96 'hide-bottom-bar': !c.showBottomBar,
99 - 'hide-speed': !c.showSpeed, // 对应下方 CSS  
100 - 'hide-code-select': !c.showCodeSelect, // 对应下方 CSS  
101 - 97 + 'hide-speed': !c.showSpeed,
  98 + 'hide-code-select': !c.showCodeSelect,
102 'hide-btn-play': !c.showPlay, 99 'hide-btn-play': !c.showPlay,
103 'hide-btn-audio': !c.showAudio, 100 'hide-btn-audio': !c.showAudio,
104 'hide-btn-stretch': !c.showStretch, 101 'hide-btn-stretch': !c.showStretch,
@@ -113,25 +110,35 @@ export default { @@ -113,25 +110,35 @@ export default {
113 initialPlayUrl: { 110 initialPlayUrl: {
114 handler(newUrl) { 111 handler(newUrl) {
115 const url = typeof newUrl === 'string' ? newUrl : (newUrl && newUrl.videoUrl) || ''; 112 const url = typeof newUrl === 'string' ? newUrl : (newUrl && newUrl.videoUrl) || '';
  113 +
116 if (url) { 114 if (url) {
  115 + // 有地址:准备播放
117 this.isLoading = true; 116 this.isLoading = true;
118 this.isError = false; 117 this.isError = false;
119 - this.$nextTick(() => {  
120 - if (this.playerInstance) this.destroy();  
121 - setTimeout(() => { 118 +
  119 + if (this.playerInstance) {
  120 + // 实例已存在(比如轮播切换下一路),直接切换地址,不要销毁
  121 + this.play(url);
  122 + } else {
  123 + // 实例不存在(比如从全部关闭状态恢复),创建并播放
  124 + this.$nextTick(() => {
122 this.create(); 125 this.create();
123 - this.play(url);  
124 - }, 50);  
125 - }); 126 + setTimeout(() => this.play(url), 100);
  127 + });
  128 + }
126 } else { 129 } else {
  130 + // 地址为空(全部关闭):彻底销毁
127 this.destroy(); 131 this.destroy();
128 this.isLoading = false; 132 this.isLoading = false;
  133 + this.isError = false;
129 } 134 }
130 }, 135 },
131 immediate: true 136 immediate: true
132 }, 137 },
133 hasAudio() { 138 hasAudio() {
134 - if (this.hasUrl) this.destroyAndReplay(this.initialPlayUrl); 139 + if (this.hasUrl && this.playerInstance) {
  140 + this.destroyAndReplay(this.initialPlayUrl);
  141 + }
135 } 142 }
136 }, 143 },
137 beforeDestroy() { 144 beforeDestroy() {
@@ -156,16 +163,20 @@ export default { @@ -156,16 +163,20 @@ export default {
156 163
157 create() { 164 create() {
158 if (this.playerInstance) return; 165 if (this.playerInstance) return;
159 - const container = this.$refs.container;  
160 - if (!container) return;  
161 - container.innerHTML = '';  
162 166
163 - if (container.clientWidth === 0 && this.retryCount < 5) {  
164 - this.retryCount++;  
165 - setTimeout(() => this.create(), 200); 167 + const container = this.$refs.container;
  168 + // 容错:如果容器不存在,或者刚才被销毁还没渲染出来,延迟重试
  169 + if (!container || container.clientWidth === 0) {
  170 + if (this.retryCount < 10) { // 增加重试次数
  171 + this.retryCount++;
  172 + setTimeout(() => this.create(), 100);
  173 + }
166 return; 174 return;
167 } 175 }
168 176
  177 + // 再次确保清空内容
  178 + container.innerHTML = '';
  179 +
169 if (!window.EasyPlayerPro) return; 180 if (!window.EasyPlayerPro) return;
170 181
171 try { 182 try {
@@ -178,10 +189,8 @@ export default { @@ -178,10 +189,8 @@ export default {
178 WCS: true, 189 WCS: true,
179 hasAudio: this.hasAudio, 190 hasAudio: this.hasAudio,
180 isLive: true, 191 isLive: true,
181 - loading: false,  
182 - isBand: true, // 保持开启以获取数据  
183 -  
184 - // btns 配置只能控制原生有开关的按钮 192 + loading: false, // 使用我们的自定义 loading
  193 + isBand: true,
185 btns: { 194 btns: {
186 play: c.showPlay, 195 play: c.showPlay,
187 audio: c.showAudio, 196 audio: c.showAudio,
@@ -200,9 +209,11 @@ export default { @@ -200,9 +209,11 @@ export default {
200 }); 209 });
201 210
202 this.playerInstance.on('play', () => { 211 this.playerInstance.on('play', () => {
  212 + console.log(`[${this.uniqueId}] 播放成功`);
203 this.hasStarted = true; 213 this.hasStarted = true;
204 this.isLoading = false; 214 this.isLoading = false;
205 this.isError = false; 215 this.isError = false;
  216 + // 播放成功后显示一下控制栏
206 this.showControls = true; 217 this.showControls = true;
207 if (this.controlTimer) clearTimeout(this.controlTimer); 218 if (this.controlTimer) clearTimeout(this.controlTimer);
208 this.controlTimer = setTimeout(() => { 219 this.controlTimer = setTimeout(() => {
@@ -212,61 +223,73 @@ export default { @@ -212,61 +223,73 @@ export default {
212 223
213 this.playerInstance.on('error', (err) => { 224 this.playerInstance.on('error', (err) => {
214 console.error('Player Error:', err); 225 console.error('Player Error:', err);
215 - this.triggerError('流媒体连接失败'); 226 + this.triggerError('视频流连接异常');
216 }); 227 });
217 228
218 } catch (e) { 229 } catch (e) {
219 - console.error("Create Error:", e); 230 + console.error("Create Instance Error:", e);
  231 + // 如果创建报错,可能是容器脏了,执行销毁逻辑刷新DOM
  232 + this.destroy();
220 } 233 }
221 }, 234 },
222 235
223 play(url) { 236 play(url) {
224 if (!url) return; 237 if (!url) return;
  238 +
225 if (!this.playerInstance) { 239 if (!this.playerInstance) {
226 this.create(); 240 this.create();
227 setTimeout(() => this.play(url), 200); 241 setTimeout(() => this.play(url), 200);
228 return; 242 return;
229 } 243 }
  244 +
230 this.isLoading = true; 245 this.isLoading = true;
231 this.isError = false; 246 this.isError = false;
232 this.errorMessage = ''; 247 this.errorMessage = '';
233 248
  249 + // 设置超时检测
234 setTimeout(() => { 250 setTimeout(() => {
235 if (this.isLoading) this.triggerError('连接超时,请重试'); 251 if (this.isLoading) this.triggerError('连接超时,请重试');
236 }, this.loadTimeout); 252 }, this.loadTimeout);
237 253
238 this.playerInstance.play(url).catch(e => { 254 this.playerInstance.play(url).catch(e => {
  255 + console.warn("Play error:", e);
239 this.triggerError('请求播放失败'); 256 this.triggerError('请求播放失败');
240 }); 257 });
241 }, 258 },
242 259
  260 + // 【核心修复】彻底销毁并刷新DOM ID
243 destroy() { 261 destroy() {
  262 + // 1. 重置状态
244 this.hasStarted = false; 263 this.hasStarted = false;
245 this.showControls = false; 264 this.showControls = false;
246 this.netSpeed = '0KB/s'; 265 this.netSpeed = '0KB/s';
  266 + this.isLoading = false;
  267 + this.isError = false;
  268 +
247 const container = this.$refs.container; 269 const container = this.$refs.container;
248 - if (container) {  
249 - const video = container.querySelector('video');  
250 - if (video) {  
251 - video.pause();  
252 - video.src = "";  
253 - video.load();  
254 - video.remove();  
255 - }  
256 - container.innerHTML = '';  
257 - } 270 +
  271 + // 2. 销毁实例
258 if (this.playerInstance) { 272 if (this.playerInstance) {
259 try { 273 try {
260 this.playerInstance.destroy(); 274 this.playerInstance.destroy();
261 - } catch (e) {  
262 - } 275 + } catch(e) {}
263 this.playerInstance = null; 276 this.playerInstance = null;
264 } 277 }
  278 +
  279 + // 3. 暴力清理旧 DOM
  280 + if (container) {
  281 + container.innerHTML = '';
  282 + }
  283 +
  284 + // 4. 【关键】更新 uniqueId
  285 + // 这会强制 Vue 在下一次渲染时移除当前的 div,并创建一个全新的 div
  286 + // 从而彻底解决 "EasyPlayerPro err container" 错误
  287 + this.uniqueId = `player-box-${Date.now()}-${Math.floor(Math.random() * 1000)}`;
265 }, 288 },
266 289
267 destroyAndReplay(url) { 290 destroyAndReplay(url) {
268 - this.isLoading = true;  
269 this.destroy(); 291 this.destroy();
  292 + this.isLoading = true;
270 this.$nextTick(() => { 293 this.$nextTick(() => {
271 this.create(); 294 this.create();
272 if (url) { 295 if (url) {
@@ -289,276 +312,93 @@ export default { @@ -289,276 +312,93 @@ export default {
289 }, 312 },
290 313
291 setControls(config) { 314 setControls(config) {
292 - this.controlsConfig = {...this.controlsConfig, ...config}; 315 + this.controlsConfig = { ...this.controlsConfig, ...config };
293 } 316 }
294 } 317 }
295 }; 318 };
296 </script> 319 </script>
297 320
298 <style scoped> 321 <style scoped>
299 -/* --------------------------------------------------  
300 - 这里是组件内部样式,仅处理非 EasyPlayer 插件的部分  
301 - --------------------------------------------------  
302 -*/ 322 +/* 基础布局 */
303 .player-wrapper { 323 .player-wrapper {
304 - width: 100%;  
305 - height: 100%;  
306 - display: flex;  
307 - flex-direction: column;  
308 - position: relative;  
309 - background: #000;  
310 - overflow: hidden; 324 + width: 100%; height: 100%; display: flex; flex-direction: column;
  325 + position: relative; background: #000; overflow: hidden;
311 } 326 }
312 -  
313 .player-box { 327 .player-box {
314 - flex: 1;  
315 - width: 100%;  
316 - height: 100%;  
317 - background: #000;  
318 - position: relative;  
319 - z-index: 1; 328 + flex: 1; width: 100%; height: 100%; background: #000; position: relative; z-index: 1;
320 } 329 }
321 330
322 /* 顶部栏 */ 331 /* 顶部栏 */
323 .custom-top-bar { 332 .custom-top-bar {
324 - position: absolute;  
325 - top: 0;  
326 - left: 0;  
327 - width: 100%;  
328 - height: 40px;  
329 - background: linear-gradient(to bottom, rgba(0, 0, 0, 0.8), rgba(0, 0, 0, 0));  
330 - z-index: 200;  
331 - display: flex;  
332 - justify-content: space-between;  
333 - align-items: center;  
334 - padding: 0 15px;  
335 - box-sizing: border-box;  
336 - pointer-events: none;  
337 - transition: opacity 0.3s ease;  
338 -}  
339 -  
340 -.custom-top-bar.hide-bar {  
341 - opacity: 0;  
342 -}  
343 -  
344 -.top-bar-left .video-title {  
345 - color: #fff;  
346 - font-size: 14px;  
347 - font-weight: bold;  
348 -}  
349 -  
350 -.top-bar-right .net-speed {  
351 - color: #00ff00;  
352 - font-size: 12px;  
353 - font-family: monospace;  
354 -}  
355 -  
356 -/* 蒙层 */  
357 -.status-mask {  
358 - position: absolute;  
359 - top: 0;  
360 - left: 0;  
361 - width: 100%;  
362 - height: 100%;  
363 - background-color: #000;  
364 - z-index: 50;  
365 - display: flex;  
366 - flex-direction: column;  
367 - align-items: center;  
368 - justify-content: center;  
369 -}  
370 - 333 + position: absolute; top: 0; left: 0; width: 100%; height: 40px;
  334 + background: linear-gradient(to bottom, rgba(0,0,0,0.8), rgba(0,0,0,0));
  335 + z-index: 200; display: flex; justify-content: space-between; align-items: center;
  336 + padding: 0 15px; box-sizing: border-box; pointer-events: none; transition: opacity 0.3s ease;
  337 +}
  338 +.custom-top-bar.hide-bar { opacity: 0; }
  339 +.top-bar-left .video-title { color: #fff; font-size: 14px; font-weight: bold; }
  340 +.top-bar-right .net-speed { color: #00ff00; font-size: 12px; font-family: monospace; }
  341 +
  342 +/* 无信号遮罩
  343 + z-index 必须高于 player-box (z-index 1)
  344 + 并且背景设为纯黑,以盖住可能残留的画面
  345 +*/
371 .idle-mask { 346 .idle-mask {
372 - position: absolute;  
373 - top: 0;  
374 - left: 0;  
375 - width: 100%;  
376 - height: 100%;  
377 - background-color: #000;  
378 - z-index: 15;  
379 - display: flex;  
380 - align-items: center;  
381 - justify-content: center;  
382 -}  
383 -  
384 -.idle-text {  
385 - color: #555;  
386 - font-size: 14px;  
387 -}  
388 -  
389 -.status-text {  
390 - color: #fff;  
391 - margin-top: 15px;  
392 - font-size: 14px;  
393 - letter-spacing: 1px; 347 + position: absolute; top: 0; left: 0; width: 100%; height: 100%;
  348 + background-color: #000; z-index: 10;
  349 + display: flex; align-items: center; justify-content: center;
394 } 350 }
  351 +.idle-text { color: #555; font-size: 14px; }
395 352
396 -.error-content {  
397 - display: flex;  
398 - flex-direction: column;  
399 - align-items: center;  
400 - justify-content: center;  
401 -}  
402 -  
403 -.error-text {  
404 - color: #ff6d6d;  
405 -}  
406 -  
407 -/* Loading 动画 */  
408 -.spinner-box {  
409 - width: 50px;  
410 - height: 50px;  
411 - display: flex;  
412 - justify-content: center;  
413 - align-items: center; 353 +/* 状态蒙层 */
  354 +.status-mask {
  355 + position: absolute; top: 0; left: 0; width: 100%; height: 100%;
  356 + background-color: #000; z-index: 50;
  357 + display: flex; flex-direction: column; align-items: center; justify-content: center;
414 } 358 }
  359 +.status-text { color: #fff; margin-top: 15px; font-size: 14px; letter-spacing: 1px; }
  360 +.error-content { display: flex; flex-direction: column; align-items: center; justify-content: center; }
  361 +.error-text { color: #ff6d6d; }
415 362
  363 +/* Loading */
  364 +.spinner-box { width: 50px; height: 50px; display: flex; justify-content: center; align-items: center; }
416 .simple-spinner { 365 .simple-spinner {
417 - width: 40px;  
418 - height: 40px;  
419 - border: 3px solid rgba(255, 255, 255, 0.2);  
420 - border-radius: 50%;  
421 - border-top-color: #409EFF;  
422 - animation: spin 0.8s linear infinite;  
423 -}  
424 -  
425 -@keyframes spin {  
426 - to {  
427 - transform: rotate(360deg);  
428 - } 366 + width: 40px; height: 40px; border: 3px solid rgba(255, 255, 255, 0.2);
  367 + border-radius: 50%; border-top-color: #409EFF; animation: spin 0.8s linear infinite;
429 } 368 }
  369 +@keyframes spin { to { transform: rotate(360deg); } }
430 </style> 370 </style>
431 371
432 <style> 372 <style>
433 -/* 1. 控制网速显示显隐 */  
434 -.player-wrapper.hide-speed .easyplayer-speed {  
435 - display: none !important;  
436 -}  
437 -  
438 -/* 2. 控制解码面板显隐 */  
439 -.player-wrapper.hide-code-select .easyplayer-controls-code-wrap {  
440 - display: none !important;  
441 -}  
442 -  
443 -/* 3. 控制其他按钮显隐 (基于您提供的 controlsConfig) */  
444 -.player-wrapper.hide-bottom-bar .easyplayer-controls {  
445 - display: none !important;  
446 -}  
447 -  
448 -/* 播放按钮 */  
449 -.player-wrapper.hide-btn-play .easyplayer-play,  
450 -.player-wrapper.hide-btn-play .easyplayer-pause {  
451 - display: none !important;  
452 -}  
453 -  
454 -/* 音量按钮 */  
455 -.player-wrapper.hide-btn-audio .easyplayer-audio-box {  
456 - display: none !important;  
457 -}  
458 -  
459 -/* 截图按钮 */  
460 -.player-wrapper.hide-btn-screenshot .easyplayer-screenshot {  
461 - display: none !important;  
462 -}  
463 -  
464 -/* 全屏按钮 */  
465 -.player-wrapper.hide-btn-fullscreen .easyplayer-fullscreen,  
466 -.player-wrapper.hide-btn-fullscreen .easyplayer-fullscreen-exit {  
467 - display: none !important;  
468 -}  
469 -  
470 -  
471 -/* --- 修正录制按钮样式 (强制应用) --- */  
472 -.player-wrapper.hide-btn-record .easyplayer-record,  
473 -.player-wrapper.hide-btn-record .easyplayer-record-stop {  
474 - display: none !important;  
475 -}  
476 -  
477 -/* 强制覆盖录制按钮位置 */ 373 +.player-wrapper.hide-speed .easyplayer-speed { display: none !important; }
  374 +.player-wrapper.hide-code-select .easyplayer-controls-code-wrap { display: none !important; }
  375 +.player-wrapper.hide-bottom-bar .easyplayer-controls { display: none !important; }
  376 +.player-wrapper.hide-btn-play .easyplayer-play, .player-wrapper.hide-btn-play .easyplayer-pause { display: none !important; }
  377 +.player-wrapper.hide-btn-audio .easyplayer-audio-box { display: none !important; }
  378 +.player-wrapper.hide-btn-screenshot .easyplayer-screenshot { display: none !important; }
  379 +.player-wrapper.hide-btn-fullscreen .easyplayer-fullscreen, .player-wrapper.hide-btn-fullscreen .easyplayer-fullscreen-exit { display: none !important; }
  380 +.player-wrapper.hide-btn-record .easyplayer-record, .player-wrapper.hide-btn-record .easyplayer-record-stop { display: none !important; }
478 .player-wrapper .easyplayer-recording { 381 .player-wrapper .easyplayer-recording {
479 - display: none;  
480 - position: absolute !important;  
481 - top: 50px !important;  
482 - left: 50% !important;  
483 - transform: translateX(-50%) !important;  
484 - z-index: 200 !important;  
485 - background: rgba(255, 0, 0, 0.6);  
486 - border-radius: 4px;  
487 - padding: 4px 12px;  
488 -}  
489 -  
490 -.player-wrapper .easyplayer-recording[style*="block"] {  
491 - display: flex !important;  
492 - align-items: center !important;  
493 - justify-content: center !important;  
494 -}  
495 -  
496 -.player-wrapper .easyplayer-recording-time {  
497 - margin: 0 8px;  
498 - font-size: 14px;  
499 - color: #fff;  
500 -}  
501 -  
502 -.player-wrapper .easyplayer-recording-stop {  
503 - height: auto !important;  
504 - cursor: pointer;  
505 -}  
506 -  
507 -  
508 -/* --- 修正拉伸按钮样式 (SVG图标) --- */  
509 -.player-wrapper.hide-btn-stretch .easyplayer-stretch {  
510 - display: none !important;  
511 -}  
512 -  
513 -.player-wrapper .easyplayer-stretch {  
514 - font-size: 0 !important;  
515 - width: 34px !important;  
516 - height: 100% !important;  
517 - display: flex !important;  
518 - align-items: center;  
519 - justify-content: center;  
520 - cursor: pointer;  
521 -}  
522 - 382 + display: none; position: absolute !important; top: 50px !important; left: 50% !important;
  383 + transform: translateX(-50%) !important; z-index: 200 !important;
  384 + background: rgba(255, 0, 0, 0.6); border-radius: 4px; padding: 4px 12px;
  385 +}
  386 +.player-wrapper .easyplayer-recording[style*="block"] { display: flex !important; align-items: center !important; justify-content: center !important; }
  387 +.player-wrapper .easyplayer-recording-time { margin: 0 8px; font-size: 14px; color: #fff; }
  388 +.player-wrapper .easyplayer-recording-stop { height: auto !important; cursor: pointer; }
  389 +.player-wrapper.hide-btn-stretch .easyplayer-stretch { display: none !important; }
  390 +.player-wrapper .easyplayer-stretch { font-size: 0 !important; width: 34px !important; height: 100% !important; display: flex !important; align-items: center; justify-content: center; cursor: pointer; }
523 .player-wrapper .easyplayer-stretch::after { 391 .player-wrapper .easyplayer-stretch::after {
524 - content: '';  
525 - display: block;  
526 - width: 20px;  
527 - height: 20px; 392 + content: ''; display: block; width: 20px; height: 20px;
528 background-image: url("data:image/svg+xml;charset=utf-8,%3Csvg viewBox='0 0 1024 1024' xmlns='http://www.w3.org/2000/svg' width='200' height='200'%3E%3Cpath d='M128 128h298.667v85.333H195.2l201.6 201.6-60.373 60.374-201.6-201.6v231.466H49.067V49.067h78.933V128zM896 896H597.333v-85.333H828.8l-201.6-201.6 60.373-60.374 201.6 201.6V518.933h85.334v377.067h-78.934V896z' fill='%23ffffff'/%3E%3C/svg%3E"); 393 background-image: url("data:image/svg+xml;charset=utf-8,%3Csvg viewBox='0 0 1024 1024' xmlns='http://www.w3.org/2000/svg' width='200' height='200'%3E%3Cpath d='M128 128h298.667v85.333H195.2l201.6 201.6-60.373 60.374-201.6-201.6v231.466H49.067V49.067h78.933V128zM896 896H597.333v-85.333H828.8l-201.6-201.6 60.373-60.374 201.6 201.6V518.933h85.334v377.067h-78.934V896z' fill='%23ffffff'/%3E%3C/svg%3E");
529 - background-repeat: no-repeat;  
530 - background-position: center;  
531 - background-size: contain;  
532 - opacity: 0.8;  
533 -}  
534 -  
535 -.player-wrapper .easyplayer-stretch:hover::after {  
536 - opacity: 1; 394 + background-repeat: no-repeat; background-position: center; background-size: contain; opacity: 0.8;
537 } 395 }
538 -  
539 -  
540 -/* --- 修正电子放大样式 --- */  
541 -.player-wrapper.hide-btn-zoom .easyplayer-zoom,  
542 -.player-wrapper.hide-btn-zoom .easyplayer-zoom-stop {  
543 - display: none !important;  
544 -}  
545 - 396 +.player-wrapper .easyplayer-stretch:hover::after { opacity: 1; }
  397 +.player-wrapper.hide-btn-zoom .easyplayer-zoom, .player-wrapper.hide-btn-zoom .easyplayer-zoom-stop { display: none !important; }
546 .player-wrapper .easyplayer-zoom-controls { 398 .player-wrapper .easyplayer-zoom-controls {
547 - position: absolute !important;  
548 - top: 50px !important;  
549 - left: 50% !important;  
550 - transform: translateX(-50%);  
551 - z-index: 199 !important;  
552 - background: rgba(0, 0, 0, 0.6);  
553 - border-radius: 20px;  
554 - padding: 0 10px;  
555 -}  
556 -  
557 -  
558 -/* --- 整体显隐控制 --- */  
559 -.player-wrapper.force-hide-controls .easyplayer-controls {  
560 - opacity: 0 !important;  
561 - visibility: hidden !important;  
562 - transition: opacity 0.3s ease; 399 + position: absolute !important; top: 50px !important; left: 50% !important;
  400 + transform: translateX(-50%); z-index: 199 !important;
  401 + background: rgba(0,0,0,0.6); border-radius: 20px; padding: 0 10px;
563 } 402 }
  403 +.player-wrapper.force-hide-controls .easyplayer-controls { opacity: 0 !important; visibility: hidden !important; transition: opacity 0.3s ease; }
564 </style> 404 </style>