diff --git a/assets/js/video-player.js b/assets/js/video-player.js index 32092f95..b361ec86 100644 --- a/assets/js/video-player.js +++ b/assets/js/video-player.js @@ -153,7 +153,7 @@ self.hls.loadSource(self.src); self.hls.attachMedia(self.video); } - }); + }).catch(function () { /* CDN unreachable: leave the player sourceless */ }); } } else if (this.type === 'flv') { loadScript(FLV_SRC).then(function () { @@ -162,7 +162,7 @@ self.flvPlayer.attachMediaElement(self.video); self.flvPlayer.load(); } - }); + }).catch(function () { /* CDN unreachable: leave the player sourceless */ }); } else { this.video.src = this.src; } @@ -172,9 +172,14 @@ } }; + AksrtVideoPlayer.prototype.hasSource = function () { + return Boolean(this.video.currentSrc || this.video.src || this.hls || this.flvPlayer); + }; + AksrtVideoPlayer.prototype.toggle = function () { + if (!this.hasSource()) return; if (this.video.paused) { - this.video.play(); + this.video.play().catch(function () { /* source failed or autoplay blocked */ }); } else { this.video.pause(); } @@ -220,9 +225,9 @@ this.loadSource(); }; - // ── 剧集切换按钮事件委托 ── + // ── 剧集切换按钮事件委托(iframe 剧集由 runtime main 的 .switch-iframe 链路处理)── document.addEventListener('click', function (e) { - var switchBtn = e.target.closest('.switch-video, .switch-iframe'); + var switchBtn = e.target.closest('.switch-video'); if (!switchBtn) return; var container = switchBtn.closest('.aksrt-video') || switchBtn.closest('[data-aksrt-video]'); @@ -236,7 +241,7 @@ if (!container) return; var src = switchBtn.dataset.src || switchBtn.getAttribute('video-url') || switchBtn.getAttribute('href'); - if (!src) return; + if (!src || !/^https?:/i.test(src)) return; e.preventDefault(); @@ -251,7 +256,7 @@ // 高亮当前集 var scope = switchBtn.closest('.featured-video-episode') || (container.parentElement || document); - var siblings = scope.querySelectorAll('.switch-video, .switch-iframe'); + var siblings = scope.querySelectorAll('.switch-video'); siblings.forEach(function (btn) { btn.classList.remove('is-active'); btn.classList.remove('active'); diff --git a/tests/widget-system/single-cover-height-contract.test.cjs b/tests/widget-system/single-cover-height-contract.test.cjs index 04bf8790..5127a652 100644 --- a/tests/widget-system/single-cover-height-contract.test.cjs +++ b/tests/widget-system/single-cover-height-contract.test.cjs @@ -101,6 +101,20 @@ test('video cover: the player video fills and center-crops like the image cover' ); }); +test('video cover: play never runs against a sourceless player', () => { + const player = read('assets/js/video-player.js'); + // toggle 必须先守卫无源状态,且 play() 的 promise 必须被捕获。 + assert.match(player, /hasSource\s*=\s*function\s*\(\)\s*\{[^}]*currentSrc[^}]*\}/); + assert.match(player, /toggle\s*=\s*function\s*\(\)\s*\{\s*if\s*\(!this\.hasSource\(\)\)\s*return;/); + assert.match(player, /this\.video\.play\(\)\.catch\(function/); + // hls/flv CDN 脚本加载失败必须收敛,不得产生未处理 rejection。 + assert.match(player, /loadScript\(HLS_SRC\)\.then\([\s\S]*?\}\)\.catch\(function/); + assert.match(player, /loadScript\(FLV_SRC\)\.then\([\s\S]*?\}\)\.catch\(function/); + // iframe 剧集归 runtime main 的 .switch-iframe 链路,播放器不得接管其选择器。 + assert.doesNotMatch(player, /closest\('\.switch-video, \.switch-iframe'\)/); + assert.doesNotMatch(player, /querySelectorAll\('\.switch-video, \.switch-iframe'\)/); +}); + test('resource singles get the cover leaf versioned, never via the aggregate @import', () => { // 聚合文件的 @import 无版本号:浏览器会拿旧缓存(含已废弃的 .single-cover // padding 规则)把封面高度链挤成 0,资源型文章封面直接空白。