From 29692d28158ad50e1d404293f592679c10dbf3f5 Mon Sep 17 00:00:00 2001 From: Harvey Zhao Date: Sat, 12 Sep 2026 08:52:51 +0800 Subject: [PATCH] fix(ads): [PKG-ADS-05] preserve ownership and verify native browser lifecycle --- docs/compiled/artplayer-plugin-ads.js | 2 +- docs/compiled/artplayer-plugin-ads.legacy.js | 2 +- docs/compiled/artplayer-plugin-ads.mjs | 2 +- package.json | 3 +- packages/artplayer-plugin-ads/ARCHITECTURE.md | 15 +- .../dist/artplayer-plugin-ads.js | 2 +- .../dist/artplayer-plugin-ads.legacy.js | 2 +- .../dist/artplayer-plugin-ads.mjs | 2 +- packages/artplayer-plugin-ads/src/session.ts | 2 +- refactor/ads-validation.md | 17 + .../ads-ui-visibility-validation.json | 538 ++++++++++++++++++ .../changes/2026-09-12-PKG-ADS-05-browser.md | 55 ++ refactor/environment-matrix.md | 7 +- refactor/plan.md | 5 +- refactor/progress.md | 11 + refactor/risk-table.md | 6 +- refactor/risks.json | 57 +- refactor/scripts/ads-native-visibility.mjs | 141 +++++ refactor/tasks.json | 7 +- test/ads-lifecycle.test.js | 11 + test/browser/ads-ui.spec.js | 136 +++++ test/helpers/native-chrome.js | 66 +++ 22 files changed, 1060 insertions(+), 29 deletions(-) create mode 100644 refactor/baselines/ads-ui-visibility-validation.json create mode 100644 refactor/changes/2026-09-12-PKG-ADS-05-browser.md create mode 100644 refactor/scripts/ads-native-visibility.mjs create mode 100644 test/browser/ads-ui.spec.js create mode 100644 test/helpers/native-chrome.js diff --git a/docs/compiled/artplayer-plugin-ads.js b/docs/compiled/artplayer-plugin-ads.js index e7515e91b..94810bdaf 100644 --- a/docs/compiled/artplayer-plugin-ads.js +++ b/docs/compiled/artplayer-plugin-ads.js @@ -4,4 +4,4 @@ * (c) 2017-2026 Harvey Zhao * Released under the MIT License. */ -!function(e,n){"object"===typeof exports&&"undefined"!==typeof module?module.exports=n():"function"==typeof define&&define.amd?(e.artplayerPluginAds=n(),define(function(){return e.artplayerPluginAds})):(e="undefined"!==typeof globalThis?globalThis:e||self).artplayerPluginAds=n()}(this,function(){"use strict";function e(e){console.warn("Artplayer Ads:",e)}function n(n){const t=[];let i=!1;function l(e,n){if(!i){t.push(n);try{e()}finally{i&&n()}}}return{on(e,t){const a=()=>{i||t()};l(()=>{n.on(e,a)},()=>{n.off(e,a)})},dom(e,n,t){const a=()=>{i||t()};l(()=>e.addEventListener(n,a),()=>e.removeEventListener(n,a))},dispose(){if(!i){i=!0;for(const i of t.splice(0).reverse())try{i()}catch(n){e(n)}}}}}const t=(e,n)=>n.replace("%s",String(e));function i(i,l,a,o){const r=n(i),s=n(i),d=i.template;let u,p,c="waiting",y=!1,f=!1,g=!1;const v=function(e,n,t){let i,l=0,a=!1,o=!1,r=!1;function s(){a&&o&&!r&&void 0===i&&(i=setTimeout(()=>{i=void 0,!r&&o&&(l+=1,n(l),r||(l>=e()?t():s()))},1e3))}function d(){o=!1,void 0!==i&&clearTimeout(i),i=void 0}return{start(){a||r||(a=!0,o=!0,s())},play(){a&&!r&&(o=!0,s())},pause:d,stop(){r=!0,d()}}}(()=>l.totalDuration,e=>p?.render(e),w),m=()=>"active"===c&&!i.isDestroy,h=()=>"destroyed"===c||i.isDestroy;function x(n){try{n?.pause()}catch(t){e(t)}}function b(n,t,i){try{Promise.resolve(n()).then(()=>{m()||i?.()},t).catch(e)}catch(l){t(l)}}function w(){if("ended"!==c&&"destroyed"!==c&&!i.isDestroy){if(c="ended",v.stop(),r.dispose(),y){if(b(()=>i.play(),n=>{"destroyed"!==c&&e(n)}),h())return;if(x(p?.video),h())return;p?.hide()}h()||i.emit("artplayerPluginAds:skip",l)}}function D(){const n=p?.video;if(x(n),n)try{n.removeAttribute("src"),n.load()}catch(t){e(t)}try{u?.remove()}catch(t){e(t)}finally{d.$ads===u&&delete d.$ads}}function k(){"destroyed"!==c&&(c="destroyed",v.stop(),r.dispose(),s.dispose(),D())}function O(){if("waiting"===c&&!i.isDestroy){c="active",y=!0;try{if(p=function(e,n,i,l,a){const{append:o,query:r,setStyle:s}=l,d=o(e,'
');a(d);const u=o(d,i.video?'':`
${i.html}
`),p=i.video?u:null,c=o(d,'
');o(c,n.loading);const y=o(d,'
\n
\n
\n
'),f=r(".artplayer-plugin-ads-close",y),g=r(".artplayer-plugin-ads-countdown",y),v=o(d,`
\n
${i.i18n.detail}
\n
\n
\n
`),m=r(".artplayer-plugin-ads-detail",v),h=r(".artplayer-plugin-ads-muted",v),x=r(".artplayer-plugin-ads-fullscreen",v);let b=i.playDuration<=0;function w(){s(n.volume,"display",p?.muted?"none":"inline-flex"),s(n.volumeClose,"display",p?.muted?"inline-flex":"none")}function D(e){const n=i.playDuration-e;b=0===e?i.playDuration<=0:n<1||Number.isNaN(n),f.innerHTML=b?i.i18n.close:t(n,i.i18n.canBeClosed),g.innerHTML=t(i.totalDuration-e,i.i18n.countdown)}return i.playDuration>=i.totalDuration&&s(f,"display","none"),i.url||s(m,"display","none"),p?(o(h,n.volume),o(h,n.volumeClose),p.muted=Boolean(i.muted),w()):s(h,"display","none"),o(x,n.fullscreenOn),o(x,n.fullscreenOff),D(0),{root:d,video:p,render:D,ready(){s(y,"display","flex"),s(v,"display","flex"),s(c,"display","none")},hide(){s(d,"display","none")},fullscreen(e){s(n.fullscreenOn,"display",e?"none":"inline-flex"),s(n.fullscreenOff,"display",e?"inline-flex":"none")},bind(e,n){e.dom(f,"click",()=>{b&&n.skip()}),e.dom(u,"click",n.click),i.url&&e.dom(m,"click",n.click),p&&e.dom(h,"click",()=>{p.muted=!p.muted,w()}),e.dom(x,"click",n.fullscreen)}}}(d.$player,a,l,o,e=>{u=e,d.$ads=e}),!m())return void D();if(i.pause(),!m())return;const n=p,s=u.ownerDocument;n.fullscreen(i.fullscreen),n.bind(r,{skip:w,click(){l.url&&(s.defaultView||window).open(l.url),m()&&i.emit("artplayerPluginAds:click",l)},fullscreen(){i.fullscreen=!i.fullscreen,m()&&n.fullscreen(i.fullscreen)}}),r.on("fullscreen",()=>n.fullscreen(i.fullscreen));const c=()=>s.hidden?v.pause():v.play();r.dom(s,"visibilitychange",c),r.on("document:visibilitychange",c);const y=()=>{if(m()&&!g){if(g=!0,v.start(),s.hidden&&v.pause(),n.video){const t=n.video;b(()=>t.play(),n=>{m()&&(e(n),w())},()=>x(t))}m()&&n.ready()}};n.video?(r.dom(n.video,"error",w),r.dom(n.video,"loadedmetadata",y),n.video.src=l.video):y()}catch(n){throw k(),n}}}try{s.on("destroy",k),r.on("ready",()=>{f||"waiting"!==c||(f=!0,r.on("play",O),r.on("video:playing",O))}),i.isDestroy&&k()}catch(A){throw k(),A}return{name:"artplayerPluginAds",skip:w,pause:v.pause,play:v.play}}function l(e){return n=>{const t=n.constructor;if("function"!==typeof t.validator||!["append","query","setStyle"].every(e=>"function"===typeof Reflect.get(t.utils||{},e)))throw new Error("Artplayer Ads requires the core validator and DOM utilities");const l=function(e,n){return n({html:"",video:"",url:"",playDuration:5,totalDuration:10,muted:!1,i18n:{close:"\u5173\u95ed\u5e7f\u544a",countdown:"%s\u79d2",detail:"\u67e5\u770b\u8be6\u60c5",canBeClosed:"%s\u79d2\u540e\u53ef\u5173\u95ed\u5e7f\u544a"},...e},{html:"?string",video:"?string",url:"?string",playDuration:"number",totalDuration:"number",muted:"?boolean",i18n:{close:"string",countdown:"string",detail:"string",canBeClosed:"string"}})}(e,t.validator),{volume:a,volumeClose:o,fullscreenOn:r,fullscreenOff:s,loading:d}=n.icons;return i(n,l,{volume:a,volumeClose:o,fullscreenOn:r,fullscreenOff:s,loading:d},t.utils)}}const a=Object.assign(l,{default:l});if("undefined"!==typeof document&&!document.getElementById("artplayer-plugin-ads")){const e=document.createElement("style");e.id="artplayer-plugin-ads",e.textContent=".artplayer-plugin-ads{position:absolute;z-index:150;inset:0;width:100%;height:100%;overflow:hidden;font-size:13px;line-height:1;color:#fff;background-color:#000}.artplayer-plugin-ads .artplayer-plugin-ads-html{display:flex;align-items:center;justify-content:center;width:100%;height:100%}.artplayer-plugin-ads .artplayer-plugin-ads-video{width:100%;height:100%}.artplayer-plugin-ads .artplayer-plugin-ads-timer{display:none;position:absolute;top:10px;right:10px}.artplayer-plugin-ads .artplayer-plugin-ads-timer>div{display:flex;align-items:center;background-color:#00000080;border-radius:15px;margin-left:5px;padding:5px 10px;cursor:pointer}.artplayer-plugin-ads .artplayer-plugin-ads-control{display:none;position:absolute;bottom:10px;right:10px}.artplayer-plugin-ads .artplayer-plugin-ads-control>div{display:flex;align-items:center;background-color:#00000080;border-radius:15px;margin-left:5px;padding:5px 10px;cursor:pointer}.artplayer-plugin-ads .artplayer-plugin-ads-control .art-icon svg{width:20px;height:20px}.artplayer-plugin-ads .artplayer-plugin-ads-loading{display:flex;align-items:center;justify-content:center;position:absolute;inset:0;width:100%;height:100%}",document.head.appendChild(e)}return a}); +!function(e,n){"object"===typeof exports&&"undefined"!==typeof module?module.exports=n():"function"==typeof define&&define.amd?(e.artplayerPluginAds=n(),define(function(){return e.artplayerPluginAds})):(e="undefined"!==typeof globalThis?globalThis:e||self).artplayerPluginAds=n()}(this,function(){"use strict";function e(e){console.warn("Artplayer Ads:",e)}function n(n){const t=[];let i=!1;function l(e,n){if(!i){t.push(n);try{e()}finally{i&&n()}}}return{on(e,t){const a=()=>{i||t()};l(()=>{n.on(e,a)},()=>{n.off(e,a)})},dom(e,n,t){const a=()=>{i||t()};l(()=>e.addEventListener(n,a),()=>e.removeEventListener(n,a))},dispose(){if(!i){i=!0;for(const i of t.splice(0).reverse())try{i()}catch(n){e(n)}}}}}const t=(e,n)=>n.replace("%s",String(e));function i(i,l,a,o){const r=n(i),s=n(i),d=i.template;let u,p,c="waiting",y=!1,f=!1,g=!1;const v=function(e,n,t){let i,l=0,a=!1,o=!1,r=!1;function s(){a&&o&&!r&&void 0===i&&(i=setTimeout(()=>{i=void 0,!r&&o&&(l+=1,n(l),r||(l>=e()?t():s()))},1e3))}function d(){o=!1,void 0!==i&&clearTimeout(i),i=void 0}return{start(){a||r||(a=!0,o=!0,s())},play(){a&&!r&&(o=!0,s())},pause:d,stop(){r=!0,d()}}}(()=>l.totalDuration,e=>p?.render(e),w),m=()=>"active"===c&&!i.isDestroy,h=()=>"destroyed"===c||i.isDestroy;function x(n){try{n?.pause()}catch(t){e(t)}}function b(n,t,i){try{Promise.resolve(n()).then(()=>{m()||i?.()},t).catch(e)}catch(l){t(l)}}function w(){if("ended"!==c&&"destroyed"!==c&&!i.isDestroy){if(c="ended",v.stop(),r.dispose(),y){if(b(()=>i.play(),n=>{"destroyed"!==c&&e(n)}),h())return;if(x(p?.video),h())return;p?.hide()}h()||i.emit("artplayerPluginAds:skip",l)}}function D(){const n=p?.video;if(x(n),n)try{n.removeAttribute("src"),n.load()}catch(t){e(t)}try{u?.remove()}catch(t){e(t)}finally{u&&d.$ads===u&&delete d.$ads}}function k(){"destroyed"!==c&&(c="destroyed",v.stop(),r.dispose(),s.dispose(),D())}function O(){if("waiting"===c&&!i.isDestroy){c="active",y=!0;try{if(p=function(e,n,i,l,a){const{append:o,query:r,setStyle:s}=l,d=o(e,'
');a(d);const u=o(d,i.video?'':`
${i.html}
`),p=i.video?u:null,c=o(d,'
');o(c,n.loading);const y=o(d,'
\n
\n
\n
'),f=r(".artplayer-plugin-ads-close",y),g=r(".artplayer-plugin-ads-countdown",y),v=o(d,`
\n
${i.i18n.detail}
\n
\n
\n
`),m=r(".artplayer-plugin-ads-detail",v),h=r(".artplayer-plugin-ads-muted",v),x=r(".artplayer-plugin-ads-fullscreen",v);let b=i.playDuration<=0;function w(){s(n.volume,"display",p?.muted?"none":"inline-flex"),s(n.volumeClose,"display",p?.muted?"inline-flex":"none")}function D(e){const n=i.playDuration-e;b=0===e?i.playDuration<=0:n<1||Number.isNaN(n),f.innerHTML=b?i.i18n.close:t(n,i.i18n.canBeClosed),g.innerHTML=t(i.totalDuration-e,i.i18n.countdown)}return i.playDuration>=i.totalDuration&&s(f,"display","none"),i.url||s(m,"display","none"),p?(o(h,n.volume),o(h,n.volumeClose),p.muted=Boolean(i.muted),w()):s(h,"display","none"),o(x,n.fullscreenOn),o(x,n.fullscreenOff),D(0),{root:d,video:p,render:D,ready(){s(y,"display","flex"),s(v,"display","flex"),s(c,"display","none")},hide(){s(d,"display","none")},fullscreen(e){s(n.fullscreenOn,"display",e?"none":"inline-flex"),s(n.fullscreenOff,"display",e?"inline-flex":"none")},bind(e,n){e.dom(f,"click",()=>{b&&n.skip()}),e.dom(u,"click",n.click),i.url&&e.dom(m,"click",n.click),p&&e.dom(h,"click",()=>{p.muted=!p.muted,w()}),e.dom(x,"click",n.fullscreen)}}}(d.$player,a,l,o,e=>{u=e,d.$ads=e}),!m())return void D();if(i.pause(),!m())return;const n=p,s=u.ownerDocument;n.fullscreen(i.fullscreen),n.bind(r,{skip:w,click(){l.url&&(s.defaultView||window).open(l.url),m()&&i.emit("artplayerPluginAds:click",l)},fullscreen(){i.fullscreen=!i.fullscreen,m()&&n.fullscreen(i.fullscreen)}}),r.on("fullscreen",()=>n.fullscreen(i.fullscreen));const c=()=>s.hidden?v.pause():v.play();r.dom(s,"visibilitychange",c),r.on("document:visibilitychange",c);const y=()=>{if(m()&&!g){if(g=!0,v.start(),s.hidden&&v.pause(),n.video){const t=n.video;b(()=>t.play(),n=>{m()&&(e(n),w())},()=>x(t))}m()&&n.ready()}};n.video?(r.dom(n.video,"error",w),r.dom(n.video,"loadedmetadata",y),n.video.src=l.video):y()}catch(n){throw k(),n}}}try{s.on("destroy",k),r.on("ready",()=>{f||"waiting"!==c||(f=!0,r.on("play",O),r.on("video:playing",O))}),i.isDestroy&&k()}catch(A){throw k(),A}return{name:"artplayerPluginAds",skip:w,pause:v.pause,play:v.play}}function l(e){return n=>{const t=n.constructor;if("function"!==typeof t.validator||!["append","query","setStyle"].every(e=>"function"===typeof Reflect.get(t.utils||{},e)))throw new Error("Artplayer Ads requires the core validator and DOM utilities");const l=function(e,n){return n({html:"",video:"",url:"",playDuration:5,totalDuration:10,muted:!1,i18n:{close:"\u5173\u95ed\u5e7f\u544a",countdown:"%s\u79d2",detail:"\u67e5\u770b\u8be6\u60c5",canBeClosed:"%s\u79d2\u540e\u53ef\u5173\u95ed\u5e7f\u544a"},...e},{html:"?string",video:"?string",url:"?string",playDuration:"number",totalDuration:"number",muted:"?boolean",i18n:{close:"string",countdown:"string",detail:"string",canBeClosed:"string"}})}(e,t.validator),{volume:a,volumeClose:o,fullscreenOn:r,fullscreenOff:s,loading:d}=n.icons;return i(n,l,{volume:a,volumeClose:o,fullscreenOn:r,fullscreenOff:s,loading:d},t.utils)}}const a=Object.assign(l,{default:l});if("undefined"!==typeof document&&!document.getElementById("artplayer-plugin-ads")){const e=document.createElement("style");e.id="artplayer-plugin-ads",e.textContent=".artplayer-plugin-ads{position:absolute;z-index:150;inset:0;width:100%;height:100%;overflow:hidden;font-size:13px;line-height:1;color:#fff;background-color:#000}.artplayer-plugin-ads .artplayer-plugin-ads-html{display:flex;align-items:center;justify-content:center;width:100%;height:100%}.artplayer-plugin-ads .artplayer-plugin-ads-video{width:100%;height:100%}.artplayer-plugin-ads .artplayer-plugin-ads-timer{display:none;position:absolute;top:10px;right:10px}.artplayer-plugin-ads .artplayer-plugin-ads-timer>div{display:flex;align-items:center;background-color:#00000080;border-radius:15px;margin-left:5px;padding:5px 10px;cursor:pointer}.artplayer-plugin-ads .artplayer-plugin-ads-control{display:none;position:absolute;bottom:10px;right:10px}.artplayer-plugin-ads .artplayer-plugin-ads-control>div{display:flex;align-items:center;background-color:#00000080;border-radius:15px;margin-left:5px;padding:5px 10px;cursor:pointer}.artplayer-plugin-ads .artplayer-plugin-ads-control .art-icon svg{width:20px;height:20px}.artplayer-plugin-ads .artplayer-plugin-ads-loading{display:flex;align-items:center;justify-content:center;position:absolute;inset:0;width:100%;height:100%}",document.head.appendChild(e)}return a}); diff --git a/docs/compiled/artplayer-plugin-ads.legacy.js b/docs/compiled/artplayer-plugin-ads.legacy.js index 56f333132..ce8ae6c7a 100644 --- a/docs/compiled/artplayer-plugin-ads.legacy.js +++ b/docs/compiled/artplayer-plugin-ads.legacy.js @@ -4,4 +4,4 @@ * (c) 2017-2026 Harvey Zhao * Released under the MIT License. */ -!function(e,n){"object"===typeof exports&&"undefined"!==typeof module?module.exports=n():"function"==typeof define&&define.amd?(e.artplayerPluginAds=n(),define(function(){return e.artplayerPluginAds})):(e="undefined"!==typeof globalThis?globalThis:e||self).artplayerPluginAds=n()}(this,function(){"use strict";var e=Object.defineProperty,n=Object.getOwnPropertySymbols,t=Object.prototype.hasOwnProperty,l=Object.prototype.propertyIsEnumerable,i=(n,t,l)=>t in n?e(n,t,{enumerable:!0,configurable:!0,writable:!0,value:l}):n[t]=l;function r(e,r){return r(((e,r)=>{for(var o in r||(r={}))t.call(r,o)&&i(e,o,r[o]);if(n)for(var o of n(r))l.call(r,o)&&i(e,o,r[o]);return e})({html:"",video:"",url:"",playDuration:5,totalDuration:10,muted:!1,i18n:{close:"\u5173\u95ed\u5e7f\u544a",countdown:"%s\u79d2",detail:"\u67e5\u770b\u8be6\u60c5",canBeClosed:"%s\u79d2\u540e\u53ef\u5173\u95ed\u5e7f\u544a"}},e),{html:"?string",video:"?string",url:"?string",playDuration:"number",totalDuration:"number",muted:"?boolean",i18n:{close:"string",countdown:"string",detail:"string",canBeClosed:"string"}})}function o(e){console.warn("Artplayer Ads:",e)}function a(e){const n=[];let t=!1;function l(e,l){if(!t){n.push(l);try{e()}finally{t&&l()}}}return{on(n,i){const r=()=>{t||i()};l(()=>{e.on(n,r)},()=>{e.off(n,r)})},dom(e,n,i){const r=()=>{t||i()};l(()=>e.addEventListener(n,r),()=>e.removeEventListener(n,r))},dispose(){if(!t){t=!0;for(const t of n.splice(0).reverse())try{t()}catch(e){o(e)}}}}}const s=(e,n)=>n.replace("%s",String(e));function d(e,n,t,l){const i=a(e),r=a(e),d=e.template;let u,p,c="waiting",y=!1,f=!1,g=!1;const v=function(e,n,t){let l,i=0,r=!1,o=!1,a=!1;function s(){r&&o&&!a&&void 0===l&&(l=setTimeout(()=>{l=void 0,!a&&o&&(i+=1,n(i),a||(i>=e()?t():s()))},1e3))}function d(){o=!1,void 0!==l&&clearTimeout(l),l=void 0}return{start(){r||a||(r=!0,o=!0,s())},play(){r&&!a&&(o=!0,s())},pause:d,stop(){a=!0,d()}}}(()=>n.totalDuration,e=>null==p?void 0:p.render(e),w),m=()=>"active"===c&&!e.isDestroy,h=()=>"destroyed"===c||e.isDestroy;function b(e){try{null==e||e.pause()}catch(n){o(n)}}function x(e,n,t){try{Promise.resolve(e()).then(()=>{m()||null==t||t()},n).catch(o)}catch(l){n(l)}}function w(){if("ended"!==c&&"destroyed"!==c&&!e.isDestroy){if(c="ended",v.stop(),i.dispose(),y){if(x(()=>e.play(),e=>{"destroyed"!==c&&o(e)}),h())return;if(b(null==p?void 0:p.video),h())return;null==p||p.hide()}h()||e.emit("artplayerPluginAds:skip",n)}}function D(){const e=null==p?void 0:p.video;if(b(e),e)try{e.removeAttribute("src"),e.load()}catch(n){o(n)}try{null==u||u.remove()}catch(n){o(n)}finally{d.$ads===u&&delete d.$ads}}function k(){"destroyed"!==c&&(c="destroyed",v.stop(),i.dispose(),r.dispose(),D())}function O(){if("waiting"===c&&!e.isDestroy){c="active",y=!0;try{if(p=function(e,n,t,l,i){const{append:r,query:o,setStyle:a}=l,d=r(e,'
');i(d);const u=r(d,t.video?'':`
${t.html}
`),p=t.video?u:null,c=r(d,'
');r(c,n.loading);const y=r(d,'
\n
\n
\n
'),f=o(".artplayer-plugin-ads-close",y),g=o(".artplayer-plugin-ads-countdown",y),v=r(d,`
\n
${t.i18n.detail}
\n
\n
\n
`),m=o(".artplayer-plugin-ads-detail",v),h=o(".artplayer-plugin-ads-muted",v),b=o(".artplayer-plugin-ads-fullscreen",v);let x=t.playDuration<=0;function w(){a(n.volume,"display",(null==p?void 0:p.muted)?"none":"inline-flex"),a(n.volumeClose,"display",(null==p?void 0:p.muted)?"inline-flex":"none")}function D(e){const n=t.playDuration-e;x=0===e?t.playDuration<=0:n<1||Number.isNaN(n),f.innerHTML=x?t.i18n.close:s(n,t.i18n.canBeClosed),g.innerHTML=s(t.totalDuration-e,t.i18n.countdown)}return t.playDuration>=t.totalDuration&&a(f,"display","none"),t.url||a(m,"display","none"),p?(r(h,n.volume),r(h,n.volumeClose),p.muted=Boolean(t.muted),w()):a(h,"display","none"),r(b,n.fullscreenOn),r(b,n.fullscreenOff),D(0),{root:d,video:p,render:D,ready(){a(y,"display","flex"),a(v,"display","flex"),a(c,"display","none")},hide(){a(d,"display","none")},fullscreen(e){a(n.fullscreenOn,"display",e?"none":"inline-flex"),a(n.fullscreenOff,"display",e?"inline-flex":"none")},bind(e,n){e.dom(f,"click",()=>{x&&n.skip()}),e.dom(u,"click",n.click),t.url&&e.dom(m,"click",n.click),p&&e.dom(h,"click",()=>{p.muted=!p.muted,w()}),e.dom(b,"click",n.fullscreen)}}}(d.$player,t,n,l,e=>{u=e,d.$ads=e}),!m())return void D();if(e.pause(),!m())return;const r=p,a=u.ownerDocument;r.fullscreen(e.fullscreen),r.bind(i,{skip:w,click(){n.url&&(a.defaultView||window).open(n.url),m()&&e.emit("artplayerPluginAds:click",n)},fullscreen(){e.fullscreen=!e.fullscreen,m()&&r.fullscreen(e.fullscreen)}}),i.on("fullscreen",()=>r.fullscreen(e.fullscreen));const c=()=>a.hidden?v.pause():v.play();i.dom(a,"visibilitychange",c),i.on("document:visibilitychange",c);const y=()=>{if(m()&&!g){if(g=!0,v.start(),a.hidden&&v.pause(),r.video){const e=r.video;x(()=>e.play(),e=>{m()&&(o(e),w())},()=>b(e))}m()&&r.ready()}};r.video?(i.dom(r.video,"error",w),i.dom(r.video,"loadedmetadata",y),r.video.src=n.video):y()}catch(r){throw k(),r}}}try{r.on("destroy",k),i.on("ready",()=>{f||"waiting"!==c||(f=!0,i.on("play",O),i.on("video:playing",O))}),e.isDestroy&&k()}catch(A){throw k(),A}return{name:"artplayerPluginAds",skip:w,pause:v.pause,play:v.play}}function u(e){return n=>{const t=n.constructor;if("function"!==typeof t.validator||!["append","query","setStyle"].every(e=>"function"===typeof Reflect.get(t.utils||{},e)))throw new Error("Artplayer Ads requires the core validator and DOM utilities");const l=r(e,t.validator),{volume:i,volumeClose:o,fullscreenOn:a,fullscreenOff:s,loading:u}=n.icons;return d(n,l,{volume:i,volumeClose:o,fullscreenOn:a,fullscreenOff:s,loading:u},t.utils)}}const p=Object.assign(u,{default:u});if("undefined"!==typeof document&&!document.getElementById("artplayer-plugin-ads")){const e=document.createElement("style");e.id="artplayer-plugin-ads",e.textContent=".artplayer-plugin-ads{position:absolute;z-index:150;inset:0;width:100%;height:100%;overflow:hidden;font-size:13px;line-height:1;color:#fff;background-color:#000}.artplayer-plugin-ads .artplayer-plugin-ads-html{display:flex;align-items:center;justify-content:center;width:100%;height:100%}.artplayer-plugin-ads .artplayer-plugin-ads-video{width:100%;height:100%}.artplayer-plugin-ads .artplayer-plugin-ads-timer{display:none;position:absolute;top:10px;right:10px}.artplayer-plugin-ads .artplayer-plugin-ads-timer>div{display:flex;align-items:center;background-color:#00000080;border-radius:15px;margin-left:5px;padding:5px 10px;cursor:pointer}.artplayer-plugin-ads .artplayer-plugin-ads-control{display:none;position:absolute;bottom:10px;right:10px}.artplayer-plugin-ads .artplayer-plugin-ads-control>div{display:flex;align-items:center;background-color:#00000080;border-radius:15px;margin-left:5px;padding:5px 10px;cursor:pointer}.artplayer-plugin-ads .artplayer-plugin-ads-control .art-icon svg{width:20px;height:20px}.artplayer-plugin-ads .artplayer-plugin-ads-loading{display:flex;align-items:center;justify-content:center;position:absolute;inset:0;width:100%;height:100%}",document.head.appendChild(e)}return p}); +!function(e,n){"object"===typeof exports&&"undefined"!==typeof module?module.exports=n():"function"==typeof define&&define.amd?(e.artplayerPluginAds=n(),define(function(){return e.artplayerPluginAds})):(e="undefined"!==typeof globalThis?globalThis:e||self).artplayerPluginAds=n()}(this,function(){"use strict";var e=Object.defineProperty,n=Object.getOwnPropertySymbols,t=Object.prototype.hasOwnProperty,l=Object.prototype.propertyIsEnumerable,i=(n,t,l)=>t in n?e(n,t,{enumerable:!0,configurable:!0,writable:!0,value:l}):n[t]=l;function r(e,r){return r(((e,r)=>{for(var o in r||(r={}))t.call(r,o)&&i(e,o,r[o]);if(n)for(var o of n(r))l.call(r,o)&&i(e,o,r[o]);return e})({html:"",video:"",url:"",playDuration:5,totalDuration:10,muted:!1,i18n:{close:"\u5173\u95ed\u5e7f\u544a",countdown:"%s\u79d2",detail:"\u67e5\u770b\u8be6\u60c5",canBeClosed:"%s\u79d2\u540e\u53ef\u5173\u95ed\u5e7f\u544a"}},e),{html:"?string",video:"?string",url:"?string",playDuration:"number",totalDuration:"number",muted:"?boolean",i18n:{close:"string",countdown:"string",detail:"string",canBeClosed:"string"}})}function o(e){console.warn("Artplayer Ads:",e)}function a(e){const n=[];let t=!1;function l(e,l){if(!t){n.push(l);try{e()}finally{t&&l()}}}return{on(n,i){const r=()=>{t||i()};l(()=>{e.on(n,r)},()=>{e.off(n,r)})},dom(e,n,i){const r=()=>{t||i()};l(()=>e.addEventListener(n,r),()=>e.removeEventListener(n,r))},dispose(){if(!t){t=!0;for(const t of n.splice(0).reverse())try{t()}catch(e){o(e)}}}}}const s=(e,n)=>n.replace("%s",String(e));function d(e,n,t,l){const i=a(e),r=a(e),d=e.template;let u,p,c="waiting",y=!1,f=!1,g=!1;const v=function(e,n,t){let l,i=0,r=!1,o=!1,a=!1;function s(){r&&o&&!a&&void 0===l&&(l=setTimeout(()=>{l=void 0,!a&&o&&(i+=1,n(i),a||(i>=e()?t():s()))},1e3))}function d(){o=!1,void 0!==l&&clearTimeout(l),l=void 0}return{start(){r||a||(r=!0,o=!0,s())},play(){r&&!a&&(o=!0,s())},pause:d,stop(){a=!0,d()}}}(()=>n.totalDuration,e=>null==p?void 0:p.render(e),w),m=()=>"active"===c&&!e.isDestroy,h=()=>"destroyed"===c||e.isDestroy;function b(e){try{null==e||e.pause()}catch(n){o(n)}}function x(e,n,t){try{Promise.resolve(e()).then(()=>{m()||null==t||t()},n).catch(o)}catch(l){n(l)}}function w(){if("ended"!==c&&"destroyed"!==c&&!e.isDestroy){if(c="ended",v.stop(),i.dispose(),y){if(x(()=>e.play(),e=>{"destroyed"!==c&&o(e)}),h())return;if(b(null==p?void 0:p.video),h())return;null==p||p.hide()}h()||e.emit("artplayerPluginAds:skip",n)}}function D(){const e=null==p?void 0:p.video;if(b(e),e)try{e.removeAttribute("src"),e.load()}catch(n){o(n)}try{null==u||u.remove()}catch(n){o(n)}finally{u&&d.$ads===u&&delete d.$ads}}function k(){"destroyed"!==c&&(c="destroyed",v.stop(),i.dispose(),r.dispose(),D())}function O(){if("waiting"===c&&!e.isDestroy){c="active",y=!0;try{if(p=function(e,n,t,l,i){const{append:r,query:o,setStyle:a}=l,d=r(e,'
');i(d);const u=r(d,t.video?'':`
${t.html}
`),p=t.video?u:null,c=r(d,'
');r(c,n.loading);const y=r(d,'
\n
\n
\n
'),f=o(".artplayer-plugin-ads-close",y),g=o(".artplayer-plugin-ads-countdown",y),v=r(d,`
\n
${t.i18n.detail}
\n
\n
\n
`),m=o(".artplayer-plugin-ads-detail",v),h=o(".artplayer-plugin-ads-muted",v),b=o(".artplayer-plugin-ads-fullscreen",v);let x=t.playDuration<=0;function w(){a(n.volume,"display",(null==p?void 0:p.muted)?"none":"inline-flex"),a(n.volumeClose,"display",(null==p?void 0:p.muted)?"inline-flex":"none")}function D(e){const n=t.playDuration-e;x=0===e?t.playDuration<=0:n<1||Number.isNaN(n),f.innerHTML=x?t.i18n.close:s(n,t.i18n.canBeClosed),g.innerHTML=s(t.totalDuration-e,t.i18n.countdown)}return t.playDuration>=t.totalDuration&&a(f,"display","none"),t.url||a(m,"display","none"),p?(r(h,n.volume),r(h,n.volumeClose),p.muted=Boolean(t.muted),w()):a(h,"display","none"),r(b,n.fullscreenOn),r(b,n.fullscreenOff),D(0),{root:d,video:p,render:D,ready(){a(y,"display","flex"),a(v,"display","flex"),a(c,"display","none")},hide(){a(d,"display","none")},fullscreen(e){a(n.fullscreenOn,"display",e?"none":"inline-flex"),a(n.fullscreenOff,"display",e?"inline-flex":"none")},bind(e,n){e.dom(f,"click",()=>{x&&n.skip()}),e.dom(u,"click",n.click),t.url&&e.dom(m,"click",n.click),p&&e.dom(h,"click",()=>{p.muted=!p.muted,w()}),e.dom(b,"click",n.fullscreen)}}}(d.$player,t,n,l,e=>{u=e,d.$ads=e}),!m())return void D();if(e.pause(),!m())return;const r=p,a=u.ownerDocument;r.fullscreen(e.fullscreen),r.bind(i,{skip:w,click(){n.url&&(a.defaultView||window).open(n.url),m()&&e.emit("artplayerPluginAds:click",n)},fullscreen(){e.fullscreen=!e.fullscreen,m()&&r.fullscreen(e.fullscreen)}}),i.on("fullscreen",()=>r.fullscreen(e.fullscreen));const c=()=>a.hidden?v.pause():v.play();i.dom(a,"visibilitychange",c),i.on("document:visibilitychange",c);const y=()=>{if(m()&&!g){if(g=!0,v.start(),a.hidden&&v.pause(),r.video){const e=r.video;x(()=>e.play(),e=>{m()&&(o(e),w())},()=>b(e))}m()&&r.ready()}};r.video?(i.dom(r.video,"error",w),i.dom(r.video,"loadedmetadata",y),r.video.src=n.video):y()}catch(r){throw k(),r}}}try{r.on("destroy",k),i.on("ready",()=>{f||"waiting"!==c||(f=!0,i.on("play",O),i.on("video:playing",O))}),e.isDestroy&&k()}catch(A){throw k(),A}return{name:"artplayerPluginAds",skip:w,pause:v.pause,play:v.play}}function u(e){return n=>{const t=n.constructor;if("function"!==typeof t.validator||!["append","query","setStyle"].every(e=>"function"===typeof Reflect.get(t.utils||{},e)))throw new Error("Artplayer Ads requires the core validator and DOM utilities");const l=r(e,t.validator),{volume:i,volumeClose:o,fullscreenOn:a,fullscreenOff:s,loading:u}=n.icons;return d(n,l,{volume:i,volumeClose:o,fullscreenOn:a,fullscreenOff:s,loading:u},t.utils)}}const p=Object.assign(u,{default:u});if("undefined"!==typeof document&&!document.getElementById("artplayer-plugin-ads")){const e=document.createElement("style");e.id="artplayer-plugin-ads",e.textContent=".artplayer-plugin-ads{position:absolute;z-index:150;inset:0;width:100%;height:100%;overflow:hidden;font-size:13px;line-height:1;color:#fff;background-color:#000}.artplayer-plugin-ads .artplayer-plugin-ads-html{display:flex;align-items:center;justify-content:center;width:100%;height:100%}.artplayer-plugin-ads .artplayer-plugin-ads-video{width:100%;height:100%}.artplayer-plugin-ads .artplayer-plugin-ads-timer{display:none;position:absolute;top:10px;right:10px}.artplayer-plugin-ads .artplayer-plugin-ads-timer>div{display:flex;align-items:center;background-color:#00000080;border-radius:15px;margin-left:5px;padding:5px 10px;cursor:pointer}.artplayer-plugin-ads .artplayer-plugin-ads-control{display:none;position:absolute;bottom:10px;right:10px}.artplayer-plugin-ads .artplayer-plugin-ads-control>div{display:flex;align-items:center;background-color:#00000080;border-radius:15px;margin-left:5px;padding:5px 10px;cursor:pointer}.artplayer-plugin-ads .artplayer-plugin-ads-control .art-icon svg{width:20px;height:20px}.artplayer-plugin-ads .artplayer-plugin-ads-loading{display:flex;align-items:center;justify-content:center;position:absolute;inset:0;width:100%;height:100%}",document.head.appendChild(e)}return p}); diff --git a/docs/compiled/artplayer-plugin-ads.mjs b/docs/compiled/artplayer-plugin-ads.mjs index 73386eb0d..7b7e55437 100644 --- a/docs/compiled/artplayer-plugin-ads.mjs +++ b/docs/compiled/artplayer-plugin-ads.mjs @@ -279,7 +279,7 @@ function createSession(art, option, icons, utils) { } catch (error) { report(error); } finally { - if (template.$ads === root) + if (root && template.$ads === root) delete template.$ads; } } diff --git a/package.json b/package.json index f5fe967e6..e8cfe5528 100644 --- a/package.json +++ b/package.json @@ -63,7 +63,8 @@ "build:types": "node scripts/build-types.mjs --write", "check:types": "node scripts/build-types.mjs --check", "test:dash-types-package": "node refactor/scripts/dash-package-types.mjs", - "test:ads-types-package": "node refactor/scripts/ads-package-types.mjs" + "test:ads-types-package": "node refactor/scripts/ads-package-types.mjs", + "test:ads-native-visibility": "node refactor/scripts/ads-native-visibility.mjs" }, "browserslist": "last 1 Chrome version", "devDependencies": { diff --git a/packages/artplayer-plugin-ads/ARCHITECTURE.md b/packages/artplayer-plugin-ads/ARCHITECTURE.md index 5d5aa0374..1f118e6be 100644 --- a/packages/artplayer-plugin-ads/ARCHITECTURE.md +++ b/packages/artplayer-plugin-ads/ARCHITECTURE.md @@ -84,7 +84,8 @@ The session owns one countdown, active core/native listeners, a separate destroy and the overlay. Finishing releases active listeners and pauses video, but keeps the hidden overlay and source/error state available until destroy. Destroy cancels the timer, disposes all owned subscriptions, pauses/releases the ad src via load, and removes the overlay even -when the core keeps its HTML. Only an owned template.$ads value is removed. Foreign DOM +when the core keeps its HTML. Only an actually created, owned template.$ads value is removed; +destroy before initialization preserves a caller's own undefined-valued property. Foreign DOM listeners and caller-owned replacement template references are not removed. Native DOM listeners are locally owned; the plugin does not mutate the core event registry @@ -109,5 +110,15 @@ reconciliation is recorded in PKG-ADS-04; complete historical distribution and d remain PKG-ADS-06. `yarn test:ads-types-package` packs and installs outside the workspace, checks all installed bytes, frozen offline install, five compiler modes and Node exports. `yarn test:browser test/browser/ads-editor-types.spec.js` verifies actual Monaco integration. -Real hidden-page/device behavior and complete media-resource +`yarn test:browser test/browser/ads-ui.spec.js` covers native desktop fullscreen, two real +ad videos, independent mute/destroy and popup/narrow-layout behavior with three core versions. +`yarn test:ads-native-visibility` launches an owned fresh installed Chrome profile and server +to verify background-tab countdown pause, foreground resume and hidden-page destruction. +It uses CDP noDefaults to avoid Playwright focus emulation; OS-window occlusion is excluded +by one browser flag. It does not test minimizing or mobile background lifecycle. Override +ARTPLAYER_NATIVE_CHROME for another installed Chrome path; set ARTPLAYER_BROWSER_ARTIFACTS +and ARTPLAYER_ADS_ARTIFACT to verify a specific core/plugin build. The script preserves a +report and trace under refactor/.cache, never uses the user's daily browser profile, and +closes its owned browser and server. Run it sequentially with other port-8084 browser suites. +Additional minimized-window/mobile lifecycle behavior and complete media-resource acceptance remain PKG-ADS-05. No claim of compatibility with every historical 4.x core is made. diff --git a/packages/artplayer-plugin-ads/dist/artplayer-plugin-ads.js b/packages/artplayer-plugin-ads/dist/artplayer-plugin-ads.js index e7515e91b..94810bdaf 100644 --- a/packages/artplayer-plugin-ads/dist/artplayer-plugin-ads.js +++ b/packages/artplayer-plugin-ads/dist/artplayer-plugin-ads.js @@ -4,4 +4,4 @@ * (c) 2017-2026 Harvey Zhao * Released under the MIT License. */ -!function(e,n){"object"===typeof exports&&"undefined"!==typeof module?module.exports=n():"function"==typeof define&&define.amd?(e.artplayerPluginAds=n(),define(function(){return e.artplayerPluginAds})):(e="undefined"!==typeof globalThis?globalThis:e||self).artplayerPluginAds=n()}(this,function(){"use strict";function e(e){console.warn("Artplayer Ads:",e)}function n(n){const t=[];let i=!1;function l(e,n){if(!i){t.push(n);try{e()}finally{i&&n()}}}return{on(e,t){const a=()=>{i||t()};l(()=>{n.on(e,a)},()=>{n.off(e,a)})},dom(e,n,t){const a=()=>{i||t()};l(()=>e.addEventListener(n,a),()=>e.removeEventListener(n,a))},dispose(){if(!i){i=!0;for(const i of t.splice(0).reverse())try{i()}catch(n){e(n)}}}}}const t=(e,n)=>n.replace("%s",String(e));function i(i,l,a,o){const r=n(i),s=n(i),d=i.template;let u,p,c="waiting",y=!1,f=!1,g=!1;const v=function(e,n,t){let i,l=0,a=!1,o=!1,r=!1;function s(){a&&o&&!r&&void 0===i&&(i=setTimeout(()=>{i=void 0,!r&&o&&(l+=1,n(l),r||(l>=e()?t():s()))},1e3))}function d(){o=!1,void 0!==i&&clearTimeout(i),i=void 0}return{start(){a||r||(a=!0,o=!0,s())},play(){a&&!r&&(o=!0,s())},pause:d,stop(){r=!0,d()}}}(()=>l.totalDuration,e=>p?.render(e),w),m=()=>"active"===c&&!i.isDestroy,h=()=>"destroyed"===c||i.isDestroy;function x(n){try{n?.pause()}catch(t){e(t)}}function b(n,t,i){try{Promise.resolve(n()).then(()=>{m()||i?.()},t).catch(e)}catch(l){t(l)}}function w(){if("ended"!==c&&"destroyed"!==c&&!i.isDestroy){if(c="ended",v.stop(),r.dispose(),y){if(b(()=>i.play(),n=>{"destroyed"!==c&&e(n)}),h())return;if(x(p?.video),h())return;p?.hide()}h()||i.emit("artplayerPluginAds:skip",l)}}function D(){const n=p?.video;if(x(n),n)try{n.removeAttribute("src"),n.load()}catch(t){e(t)}try{u?.remove()}catch(t){e(t)}finally{d.$ads===u&&delete d.$ads}}function k(){"destroyed"!==c&&(c="destroyed",v.stop(),r.dispose(),s.dispose(),D())}function O(){if("waiting"===c&&!i.isDestroy){c="active",y=!0;try{if(p=function(e,n,i,l,a){const{append:o,query:r,setStyle:s}=l,d=o(e,'
');a(d);const u=o(d,i.video?'':`
${i.html}
`),p=i.video?u:null,c=o(d,'
');o(c,n.loading);const y=o(d,'
\n
\n
\n
'),f=r(".artplayer-plugin-ads-close",y),g=r(".artplayer-plugin-ads-countdown",y),v=o(d,`
\n
${i.i18n.detail}
\n
\n
\n
`),m=r(".artplayer-plugin-ads-detail",v),h=r(".artplayer-plugin-ads-muted",v),x=r(".artplayer-plugin-ads-fullscreen",v);let b=i.playDuration<=0;function w(){s(n.volume,"display",p?.muted?"none":"inline-flex"),s(n.volumeClose,"display",p?.muted?"inline-flex":"none")}function D(e){const n=i.playDuration-e;b=0===e?i.playDuration<=0:n<1||Number.isNaN(n),f.innerHTML=b?i.i18n.close:t(n,i.i18n.canBeClosed),g.innerHTML=t(i.totalDuration-e,i.i18n.countdown)}return i.playDuration>=i.totalDuration&&s(f,"display","none"),i.url||s(m,"display","none"),p?(o(h,n.volume),o(h,n.volumeClose),p.muted=Boolean(i.muted),w()):s(h,"display","none"),o(x,n.fullscreenOn),o(x,n.fullscreenOff),D(0),{root:d,video:p,render:D,ready(){s(y,"display","flex"),s(v,"display","flex"),s(c,"display","none")},hide(){s(d,"display","none")},fullscreen(e){s(n.fullscreenOn,"display",e?"none":"inline-flex"),s(n.fullscreenOff,"display",e?"inline-flex":"none")},bind(e,n){e.dom(f,"click",()=>{b&&n.skip()}),e.dom(u,"click",n.click),i.url&&e.dom(m,"click",n.click),p&&e.dom(h,"click",()=>{p.muted=!p.muted,w()}),e.dom(x,"click",n.fullscreen)}}}(d.$player,a,l,o,e=>{u=e,d.$ads=e}),!m())return void D();if(i.pause(),!m())return;const n=p,s=u.ownerDocument;n.fullscreen(i.fullscreen),n.bind(r,{skip:w,click(){l.url&&(s.defaultView||window).open(l.url),m()&&i.emit("artplayerPluginAds:click",l)},fullscreen(){i.fullscreen=!i.fullscreen,m()&&n.fullscreen(i.fullscreen)}}),r.on("fullscreen",()=>n.fullscreen(i.fullscreen));const c=()=>s.hidden?v.pause():v.play();r.dom(s,"visibilitychange",c),r.on("document:visibilitychange",c);const y=()=>{if(m()&&!g){if(g=!0,v.start(),s.hidden&&v.pause(),n.video){const t=n.video;b(()=>t.play(),n=>{m()&&(e(n),w())},()=>x(t))}m()&&n.ready()}};n.video?(r.dom(n.video,"error",w),r.dom(n.video,"loadedmetadata",y),n.video.src=l.video):y()}catch(n){throw k(),n}}}try{s.on("destroy",k),r.on("ready",()=>{f||"waiting"!==c||(f=!0,r.on("play",O),r.on("video:playing",O))}),i.isDestroy&&k()}catch(A){throw k(),A}return{name:"artplayerPluginAds",skip:w,pause:v.pause,play:v.play}}function l(e){return n=>{const t=n.constructor;if("function"!==typeof t.validator||!["append","query","setStyle"].every(e=>"function"===typeof Reflect.get(t.utils||{},e)))throw new Error("Artplayer Ads requires the core validator and DOM utilities");const l=function(e,n){return n({html:"",video:"",url:"",playDuration:5,totalDuration:10,muted:!1,i18n:{close:"\u5173\u95ed\u5e7f\u544a",countdown:"%s\u79d2",detail:"\u67e5\u770b\u8be6\u60c5",canBeClosed:"%s\u79d2\u540e\u53ef\u5173\u95ed\u5e7f\u544a"},...e},{html:"?string",video:"?string",url:"?string",playDuration:"number",totalDuration:"number",muted:"?boolean",i18n:{close:"string",countdown:"string",detail:"string",canBeClosed:"string"}})}(e,t.validator),{volume:a,volumeClose:o,fullscreenOn:r,fullscreenOff:s,loading:d}=n.icons;return i(n,l,{volume:a,volumeClose:o,fullscreenOn:r,fullscreenOff:s,loading:d},t.utils)}}const a=Object.assign(l,{default:l});if("undefined"!==typeof document&&!document.getElementById("artplayer-plugin-ads")){const e=document.createElement("style");e.id="artplayer-plugin-ads",e.textContent=".artplayer-plugin-ads{position:absolute;z-index:150;inset:0;width:100%;height:100%;overflow:hidden;font-size:13px;line-height:1;color:#fff;background-color:#000}.artplayer-plugin-ads .artplayer-plugin-ads-html{display:flex;align-items:center;justify-content:center;width:100%;height:100%}.artplayer-plugin-ads .artplayer-plugin-ads-video{width:100%;height:100%}.artplayer-plugin-ads .artplayer-plugin-ads-timer{display:none;position:absolute;top:10px;right:10px}.artplayer-plugin-ads .artplayer-plugin-ads-timer>div{display:flex;align-items:center;background-color:#00000080;border-radius:15px;margin-left:5px;padding:5px 10px;cursor:pointer}.artplayer-plugin-ads .artplayer-plugin-ads-control{display:none;position:absolute;bottom:10px;right:10px}.artplayer-plugin-ads .artplayer-plugin-ads-control>div{display:flex;align-items:center;background-color:#00000080;border-radius:15px;margin-left:5px;padding:5px 10px;cursor:pointer}.artplayer-plugin-ads .artplayer-plugin-ads-control .art-icon svg{width:20px;height:20px}.artplayer-plugin-ads .artplayer-plugin-ads-loading{display:flex;align-items:center;justify-content:center;position:absolute;inset:0;width:100%;height:100%}",document.head.appendChild(e)}return a}); +!function(e,n){"object"===typeof exports&&"undefined"!==typeof module?module.exports=n():"function"==typeof define&&define.amd?(e.artplayerPluginAds=n(),define(function(){return e.artplayerPluginAds})):(e="undefined"!==typeof globalThis?globalThis:e||self).artplayerPluginAds=n()}(this,function(){"use strict";function e(e){console.warn("Artplayer Ads:",e)}function n(n){const t=[];let i=!1;function l(e,n){if(!i){t.push(n);try{e()}finally{i&&n()}}}return{on(e,t){const a=()=>{i||t()};l(()=>{n.on(e,a)},()=>{n.off(e,a)})},dom(e,n,t){const a=()=>{i||t()};l(()=>e.addEventListener(n,a),()=>e.removeEventListener(n,a))},dispose(){if(!i){i=!0;for(const i of t.splice(0).reverse())try{i()}catch(n){e(n)}}}}}const t=(e,n)=>n.replace("%s",String(e));function i(i,l,a,o){const r=n(i),s=n(i),d=i.template;let u,p,c="waiting",y=!1,f=!1,g=!1;const v=function(e,n,t){let i,l=0,a=!1,o=!1,r=!1;function s(){a&&o&&!r&&void 0===i&&(i=setTimeout(()=>{i=void 0,!r&&o&&(l+=1,n(l),r||(l>=e()?t():s()))},1e3))}function d(){o=!1,void 0!==i&&clearTimeout(i),i=void 0}return{start(){a||r||(a=!0,o=!0,s())},play(){a&&!r&&(o=!0,s())},pause:d,stop(){r=!0,d()}}}(()=>l.totalDuration,e=>p?.render(e),w),m=()=>"active"===c&&!i.isDestroy,h=()=>"destroyed"===c||i.isDestroy;function x(n){try{n?.pause()}catch(t){e(t)}}function b(n,t,i){try{Promise.resolve(n()).then(()=>{m()||i?.()},t).catch(e)}catch(l){t(l)}}function w(){if("ended"!==c&&"destroyed"!==c&&!i.isDestroy){if(c="ended",v.stop(),r.dispose(),y){if(b(()=>i.play(),n=>{"destroyed"!==c&&e(n)}),h())return;if(x(p?.video),h())return;p?.hide()}h()||i.emit("artplayerPluginAds:skip",l)}}function D(){const n=p?.video;if(x(n),n)try{n.removeAttribute("src"),n.load()}catch(t){e(t)}try{u?.remove()}catch(t){e(t)}finally{u&&d.$ads===u&&delete d.$ads}}function k(){"destroyed"!==c&&(c="destroyed",v.stop(),r.dispose(),s.dispose(),D())}function O(){if("waiting"===c&&!i.isDestroy){c="active",y=!0;try{if(p=function(e,n,i,l,a){const{append:o,query:r,setStyle:s}=l,d=o(e,'
');a(d);const u=o(d,i.video?'':`
${i.html}
`),p=i.video?u:null,c=o(d,'
');o(c,n.loading);const y=o(d,'
\n
\n
\n
'),f=r(".artplayer-plugin-ads-close",y),g=r(".artplayer-plugin-ads-countdown",y),v=o(d,`
\n
${i.i18n.detail}
\n
\n
\n
`),m=r(".artplayer-plugin-ads-detail",v),h=r(".artplayer-plugin-ads-muted",v),x=r(".artplayer-plugin-ads-fullscreen",v);let b=i.playDuration<=0;function w(){s(n.volume,"display",p?.muted?"none":"inline-flex"),s(n.volumeClose,"display",p?.muted?"inline-flex":"none")}function D(e){const n=i.playDuration-e;b=0===e?i.playDuration<=0:n<1||Number.isNaN(n),f.innerHTML=b?i.i18n.close:t(n,i.i18n.canBeClosed),g.innerHTML=t(i.totalDuration-e,i.i18n.countdown)}return i.playDuration>=i.totalDuration&&s(f,"display","none"),i.url||s(m,"display","none"),p?(o(h,n.volume),o(h,n.volumeClose),p.muted=Boolean(i.muted),w()):s(h,"display","none"),o(x,n.fullscreenOn),o(x,n.fullscreenOff),D(0),{root:d,video:p,render:D,ready(){s(y,"display","flex"),s(v,"display","flex"),s(c,"display","none")},hide(){s(d,"display","none")},fullscreen(e){s(n.fullscreenOn,"display",e?"none":"inline-flex"),s(n.fullscreenOff,"display",e?"inline-flex":"none")},bind(e,n){e.dom(f,"click",()=>{b&&n.skip()}),e.dom(u,"click",n.click),i.url&&e.dom(m,"click",n.click),p&&e.dom(h,"click",()=>{p.muted=!p.muted,w()}),e.dom(x,"click",n.fullscreen)}}}(d.$player,a,l,o,e=>{u=e,d.$ads=e}),!m())return void D();if(i.pause(),!m())return;const n=p,s=u.ownerDocument;n.fullscreen(i.fullscreen),n.bind(r,{skip:w,click(){l.url&&(s.defaultView||window).open(l.url),m()&&i.emit("artplayerPluginAds:click",l)},fullscreen(){i.fullscreen=!i.fullscreen,m()&&n.fullscreen(i.fullscreen)}}),r.on("fullscreen",()=>n.fullscreen(i.fullscreen));const c=()=>s.hidden?v.pause():v.play();r.dom(s,"visibilitychange",c),r.on("document:visibilitychange",c);const y=()=>{if(m()&&!g){if(g=!0,v.start(),s.hidden&&v.pause(),n.video){const t=n.video;b(()=>t.play(),n=>{m()&&(e(n),w())},()=>x(t))}m()&&n.ready()}};n.video?(r.dom(n.video,"error",w),r.dom(n.video,"loadedmetadata",y),n.video.src=l.video):y()}catch(n){throw k(),n}}}try{s.on("destroy",k),r.on("ready",()=>{f||"waiting"!==c||(f=!0,r.on("play",O),r.on("video:playing",O))}),i.isDestroy&&k()}catch(A){throw k(),A}return{name:"artplayerPluginAds",skip:w,pause:v.pause,play:v.play}}function l(e){return n=>{const t=n.constructor;if("function"!==typeof t.validator||!["append","query","setStyle"].every(e=>"function"===typeof Reflect.get(t.utils||{},e)))throw new Error("Artplayer Ads requires the core validator and DOM utilities");const l=function(e,n){return n({html:"",video:"",url:"",playDuration:5,totalDuration:10,muted:!1,i18n:{close:"\u5173\u95ed\u5e7f\u544a",countdown:"%s\u79d2",detail:"\u67e5\u770b\u8be6\u60c5",canBeClosed:"%s\u79d2\u540e\u53ef\u5173\u95ed\u5e7f\u544a"},...e},{html:"?string",video:"?string",url:"?string",playDuration:"number",totalDuration:"number",muted:"?boolean",i18n:{close:"string",countdown:"string",detail:"string",canBeClosed:"string"}})}(e,t.validator),{volume:a,volumeClose:o,fullscreenOn:r,fullscreenOff:s,loading:d}=n.icons;return i(n,l,{volume:a,volumeClose:o,fullscreenOn:r,fullscreenOff:s,loading:d},t.utils)}}const a=Object.assign(l,{default:l});if("undefined"!==typeof document&&!document.getElementById("artplayer-plugin-ads")){const e=document.createElement("style");e.id="artplayer-plugin-ads",e.textContent=".artplayer-plugin-ads{position:absolute;z-index:150;inset:0;width:100%;height:100%;overflow:hidden;font-size:13px;line-height:1;color:#fff;background-color:#000}.artplayer-plugin-ads .artplayer-plugin-ads-html{display:flex;align-items:center;justify-content:center;width:100%;height:100%}.artplayer-plugin-ads .artplayer-plugin-ads-video{width:100%;height:100%}.artplayer-plugin-ads .artplayer-plugin-ads-timer{display:none;position:absolute;top:10px;right:10px}.artplayer-plugin-ads .artplayer-plugin-ads-timer>div{display:flex;align-items:center;background-color:#00000080;border-radius:15px;margin-left:5px;padding:5px 10px;cursor:pointer}.artplayer-plugin-ads .artplayer-plugin-ads-control{display:none;position:absolute;bottom:10px;right:10px}.artplayer-plugin-ads .artplayer-plugin-ads-control>div{display:flex;align-items:center;background-color:#00000080;border-radius:15px;margin-left:5px;padding:5px 10px;cursor:pointer}.artplayer-plugin-ads .artplayer-plugin-ads-control .art-icon svg{width:20px;height:20px}.artplayer-plugin-ads .artplayer-plugin-ads-loading{display:flex;align-items:center;justify-content:center;position:absolute;inset:0;width:100%;height:100%}",document.head.appendChild(e)}return a}); diff --git a/packages/artplayer-plugin-ads/dist/artplayer-plugin-ads.legacy.js b/packages/artplayer-plugin-ads/dist/artplayer-plugin-ads.legacy.js index 56f333132..ce8ae6c7a 100644 --- a/packages/artplayer-plugin-ads/dist/artplayer-plugin-ads.legacy.js +++ b/packages/artplayer-plugin-ads/dist/artplayer-plugin-ads.legacy.js @@ -4,4 +4,4 @@ * (c) 2017-2026 Harvey Zhao * Released under the MIT License. */ -!function(e,n){"object"===typeof exports&&"undefined"!==typeof module?module.exports=n():"function"==typeof define&&define.amd?(e.artplayerPluginAds=n(),define(function(){return e.artplayerPluginAds})):(e="undefined"!==typeof globalThis?globalThis:e||self).artplayerPluginAds=n()}(this,function(){"use strict";var e=Object.defineProperty,n=Object.getOwnPropertySymbols,t=Object.prototype.hasOwnProperty,l=Object.prototype.propertyIsEnumerable,i=(n,t,l)=>t in n?e(n,t,{enumerable:!0,configurable:!0,writable:!0,value:l}):n[t]=l;function r(e,r){return r(((e,r)=>{for(var o in r||(r={}))t.call(r,o)&&i(e,o,r[o]);if(n)for(var o of n(r))l.call(r,o)&&i(e,o,r[o]);return e})({html:"",video:"",url:"",playDuration:5,totalDuration:10,muted:!1,i18n:{close:"\u5173\u95ed\u5e7f\u544a",countdown:"%s\u79d2",detail:"\u67e5\u770b\u8be6\u60c5",canBeClosed:"%s\u79d2\u540e\u53ef\u5173\u95ed\u5e7f\u544a"}},e),{html:"?string",video:"?string",url:"?string",playDuration:"number",totalDuration:"number",muted:"?boolean",i18n:{close:"string",countdown:"string",detail:"string",canBeClosed:"string"}})}function o(e){console.warn("Artplayer Ads:",e)}function a(e){const n=[];let t=!1;function l(e,l){if(!t){n.push(l);try{e()}finally{t&&l()}}}return{on(n,i){const r=()=>{t||i()};l(()=>{e.on(n,r)},()=>{e.off(n,r)})},dom(e,n,i){const r=()=>{t||i()};l(()=>e.addEventListener(n,r),()=>e.removeEventListener(n,r))},dispose(){if(!t){t=!0;for(const t of n.splice(0).reverse())try{t()}catch(e){o(e)}}}}}const s=(e,n)=>n.replace("%s",String(e));function d(e,n,t,l){const i=a(e),r=a(e),d=e.template;let u,p,c="waiting",y=!1,f=!1,g=!1;const v=function(e,n,t){let l,i=0,r=!1,o=!1,a=!1;function s(){r&&o&&!a&&void 0===l&&(l=setTimeout(()=>{l=void 0,!a&&o&&(i+=1,n(i),a||(i>=e()?t():s()))},1e3))}function d(){o=!1,void 0!==l&&clearTimeout(l),l=void 0}return{start(){r||a||(r=!0,o=!0,s())},play(){r&&!a&&(o=!0,s())},pause:d,stop(){a=!0,d()}}}(()=>n.totalDuration,e=>null==p?void 0:p.render(e),w),m=()=>"active"===c&&!e.isDestroy,h=()=>"destroyed"===c||e.isDestroy;function b(e){try{null==e||e.pause()}catch(n){o(n)}}function x(e,n,t){try{Promise.resolve(e()).then(()=>{m()||null==t||t()},n).catch(o)}catch(l){n(l)}}function w(){if("ended"!==c&&"destroyed"!==c&&!e.isDestroy){if(c="ended",v.stop(),i.dispose(),y){if(x(()=>e.play(),e=>{"destroyed"!==c&&o(e)}),h())return;if(b(null==p?void 0:p.video),h())return;null==p||p.hide()}h()||e.emit("artplayerPluginAds:skip",n)}}function D(){const e=null==p?void 0:p.video;if(b(e),e)try{e.removeAttribute("src"),e.load()}catch(n){o(n)}try{null==u||u.remove()}catch(n){o(n)}finally{d.$ads===u&&delete d.$ads}}function k(){"destroyed"!==c&&(c="destroyed",v.stop(),i.dispose(),r.dispose(),D())}function O(){if("waiting"===c&&!e.isDestroy){c="active",y=!0;try{if(p=function(e,n,t,l,i){const{append:r,query:o,setStyle:a}=l,d=r(e,'
');i(d);const u=r(d,t.video?'':`
${t.html}
`),p=t.video?u:null,c=r(d,'
');r(c,n.loading);const y=r(d,'
\n
\n
\n
'),f=o(".artplayer-plugin-ads-close",y),g=o(".artplayer-plugin-ads-countdown",y),v=r(d,`
\n
${t.i18n.detail}
\n
\n
\n
`),m=o(".artplayer-plugin-ads-detail",v),h=o(".artplayer-plugin-ads-muted",v),b=o(".artplayer-plugin-ads-fullscreen",v);let x=t.playDuration<=0;function w(){a(n.volume,"display",(null==p?void 0:p.muted)?"none":"inline-flex"),a(n.volumeClose,"display",(null==p?void 0:p.muted)?"inline-flex":"none")}function D(e){const n=t.playDuration-e;x=0===e?t.playDuration<=0:n<1||Number.isNaN(n),f.innerHTML=x?t.i18n.close:s(n,t.i18n.canBeClosed),g.innerHTML=s(t.totalDuration-e,t.i18n.countdown)}return t.playDuration>=t.totalDuration&&a(f,"display","none"),t.url||a(m,"display","none"),p?(r(h,n.volume),r(h,n.volumeClose),p.muted=Boolean(t.muted),w()):a(h,"display","none"),r(b,n.fullscreenOn),r(b,n.fullscreenOff),D(0),{root:d,video:p,render:D,ready(){a(y,"display","flex"),a(v,"display","flex"),a(c,"display","none")},hide(){a(d,"display","none")},fullscreen(e){a(n.fullscreenOn,"display",e?"none":"inline-flex"),a(n.fullscreenOff,"display",e?"inline-flex":"none")},bind(e,n){e.dom(f,"click",()=>{x&&n.skip()}),e.dom(u,"click",n.click),t.url&&e.dom(m,"click",n.click),p&&e.dom(h,"click",()=>{p.muted=!p.muted,w()}),e.dom(b,"click",n.fullscreen)}}}(d.$player,t,n,l,e=>{u=e,d.$ads=e}),!m())return void D();if(e.pause(),!m())return;const r=p,a=u.ownerDocument;r.fullscreen(e.fullscreen),r.bind(i,{skip:w,click(){n.url&&(a.defaultView||window).open(n.url),m()&&e.emit("artplayerPluginAds:click",n)},fullscreen(){e.fullscreen=!e.fullscreen,m()&&r.fullscreen(e.fullscreen)}}),i.on("fullscreen",()=>r.fullscreen(e.fullscreen));const c=()=>a.hidden?v.pause():v.play();i.dom(a,"visibilitychange",c),i.on("document:visibilitychange",c);const y=()=>{if(m()&&!g){if(g=!0,v.start(),a.hidden&&v.pause(),r.video){const e=r.video;x(()=>e.play(),e=>{m()&&(o(e),w())},()=>b(e))}m()&&r.ready()}};r.video?(i.dom(r.video,"error",w),i.dom(r.video,"loadedmetadata",y),r.video.src=n.video):y()}catch(r){throw k(),r}}}try{r.on("destroy",k),i.on("ready",()=>{f||"waiting"!==c||(f=!0,i.on("play",O),i.on("video:playing",O))}),e.isDestroy&&k()}catch(A){throw k(),A}return{name:"artplayerPluginAds",skip:w,pause:v.pause,play:v.play}}function u(e){return n=>{const t=n.constructor;if("function"!==typeof t.validator||!["append","query","setStyle"].every(e=>"function"===typeof Reflect.get(t.utils||{},e)))throw new Error("Artplayer Ads requires the core validator and DOM utilities");const l=r(e,t.validator),{volume:i,volumeClose:o,fullscreenOn:a,fullscreenOff:s,loading:u}=n.icons;return d(n,l,{volume:i,volumeClose:o,fullscreenOn:a,fullscreenOff:s,loading:u},t.utils)}}const p=Object.assign(u,{default:u});if("undefined"!==typeof document&&!document.getElementById("artplayer-plugin-ads")){const e=document.createElement("style");e.id="artplayer-plugin-ads",e.textContent=".artplayer-plugin-ads{position:absolute;z-index:150;inset:0;width:100%;height:100%;overflow:hidden;font-size:13px;line-height:1;color:#fff;background-color:#000}.artplayer-plugin-ads .artplayer-plugin-ads-html{display:flex;align-items:center;justify-content:center;width:100%;height:100%}.artplayer-plugin-ads .artplayer-plugin-ads-video{width:100%;height:100%}.artplayer-plugin-ads .artplayer-plugin-ads-timer{display:none;position:absolute;top:10px;right:10px}.artplayer-plugin-ads .artplayer-plugin-ads-timer>div{display:flex;align-items:center;background-color:#00000080;border-radius:15px;margin-left:5px;padding:5px 10px;cursor:pointer}.artplayer-plugin-ads .artplayer-plugin-ads-control{display:none;position:absolute;bottom:10px;right:10px}.artplayer-plugin-ads .artplayer-plugin-ads-control>div{display:flex;align-items:center;background-color:#00000080;border-radius:15px;margin-left:5px;padding:5px 10px;cursor:pointer}.artplayer-plugin-ads .artplayer-plugin-ads-control .art-icon svg{width:20px;height:20px}.artplayer-plugin-ads .artplayer-plugin-ads-loading{display:flex;align-items:center;justify-content:center;position:absolute;inset:0;width:100%;height:100%}",document.head.appendChild(e)}return p}); +!function(e,n){"object"===typeof exports&&"undefined"!==typeof module?module.exports=n():"function"==typeof define&&define.amd?(e.artplayerPluginAds=n(),define(function(){return e.artplayerPluginAds})):(e="undefined"!==typeof globalThis?globalThis:e||self).artplayerPluginAds=n()}(this,function(){"use strict";var e=Object.defineProperty,n=Object.getOwnPropertySymbols,t=Object.prototype.hasOwnProperty,l=Object.prototype.propertyIsEnumerable,i=(n,t,l)=>t in n?e(n,t,{enumerable:!0,configurable:!0,writable:!0,value:l}):n[t]=l;function r(e,r){return r(((e,r)=>{for(var o in r||(r={}))t.call(r,o)&&i(e,o,r[o]);if(n)for(var o of n(r))l.call(r,o)&&i(e,o,r[o]);return e})({html:"",video:"",url:"",playDuration:5,totalDuration:10,muted:!1,i18n:{close:"\u5173\u95ed\u5e7f\u544a",countdown:"%s\u79d2",detail:"\u67e5\u770b\u8be6\u60c5",canBeClosed:"%s\u79d2\u540e\u53ef\u5173\u95ed\u5e7f\u544a"}},e),{html:"?string",video:"?string",url:"?string",playDuration:"number",totalDuration:"number",muted:"?boolean",i18n:{close:"string",countdown:"string",detail:"string",canBeClosed:"string"}})}function o(e){console.warn("Artplayer Ads:",e)}function a(e){const n=[];let t=!1;function l(e,l){if(!t){n.push(l);try{e()}finally{t&&l()}}}return{on(n,i){const r=()=>{t||i()};l(()=>{e.on(n,r)},()=>{e.off(n,r)})},dom(e,n,i){const r=()=>{t||i()};l(()=>e.addEventListener(n,r),()=>e.removeEventListener(n,r))},dispose(){if(!t){t=!0;for(const t of n.splice(0).reverse())try{t()}catch(e){o(e)}}}}}const s=(e,n)=>n.replace("%s",String(e));function d(e,n,t,l){const i=a(e),r=a(e),d=e.template;let u,p,c="waiting",y=!1,f=!1,g=!1;const v=function(e,n,t){let l,i=0,r=!1,o=!1,a=!1;function s(){r&&o&&!a&&void 0===l&&(l=setTimeout(()=>{l=void 0,!a&&o&&(i+=1,n(i),a||(i>=e()?t():s()))},1e3))}function d(){o=!1,void 0!==l&&clearTimeout(l),l=void 0}return{start(){r||a||(r=!0,o=!0,s())},play(){r&&!a&&(o=!0,s())},pause:d,stop(){a=!0,d()}}}(()=>n.totalDuration,e=>null==p?void 0:p.render(e),w),m=()=>"active"===c&&!e.isDestroy,h=()=>"destroyed"===c||e.isDestroy;function b(e){try{null==e||e.pause()}catch(n){o(n)}}function x(e,n,t){try{Promise.resolve(e()).then(()=>{m()||null==t||t()},n).catch(o)}catch(l){n(l)}}function w(){if("ended"!==c&&"destroyed"!==c&&!e.isDestroy){if(c="ended",v.stop(),i.dispose(),y){if(x(()=>e.play(),e=>{"destroyed"!==c&&o(e)}),h())return;if(b(null==p?void 0:p.video),h())return;null==p||p.hide()}h()||e.emit("artplayerPluginAds:skip",n)}}function D(){const e=null==p?void 0:p.video;if(b(e),e)try{e.removeAttribute("src"),e.load()}catch(n){o(n)}try{null==u||u.remove()}catch(n){o(n)}finally{u&&d.$ads===u&&delete d.$ads}}function k(){"destroyed"!==c&&(c="destroyed",v.stop(),i.dispose(),r.dispose(),D())}function O(){if("waiting"===c&&!e.isDestroy){c="active",y=!0;try{if(p=function(e,n,t,l,i){const{append:r,query:o,setStyle:a}=l,d=r(e,'
');i(d);const u=r(d,t.video?'':`
${t.html}
`),p=t.video?u:null,c=r(d,'
');r(c,n.loading);const y=r(d,'
\n
\n
\n
'),f=o(".artplayer-plugin-ads-close",y),g=o(".artplayer-plugin-ads-countdown",y),v=r(d,`
\n
${t.i18n.detail}
\n
\n
\n
`),m=o(".artplayer-plugin-ads-detail",v),h=o(".artplayer-plugin-ads-muted",v),b=o(".artplayer-plugin-ads-fullscreen",v);let x=t.playDuration<=0;function w(){a(n.volume,"display",(null==p?void 0:p.muted)?"none":"inline-flex"),a(n.volumeClose,"display",(null==p?void 0:p.muted)?"inline-flex":"none")}function D(e){const n=t.playDuration-e;x=0===e?t.playDuration<=0:n<1||Number.isNaN(n),f.innerHTML=x?t.i18n.close:s(n,t.i18n.canBeClosed),g.innerHTML=s(t.totalDuration-e,t.i18n.countdown)}return t.playDuration>=t.totalDuration&&a(f,"display","none"),t.url||a(m,"display","none"),p?(r(h,n.volume),r(h,n.volumeClose),p.muted=Boolean(t.muted),w()):a(h,"display","none"),r(b,n.fullscreenOn),r(b,n.fullscreenOff),D(0),{root:d,video:p,render:D,ready(){a(y,"display","flex"),a(v,"display","flex"),a(c,"display","none")},hide(){a(d,"display","none")},fullscreen(e){a(n.fullscreenOn,"display",e?"none":"inline-flex"),a(n.fullscreenOff,"display",e?"inline-flex":"none")},bind(e,n){e.dom(f,"click",()=>{x&&n.skip()}),e.dom(u,"click",n.click),t.url&&e.dom(m,"click",n.click),p&&e.dom(h,"click",()=>{p.muted=!p.muted,w()}),e.dom(b,"click",n.fullscreen)}}}(d.$player,t,n,l,e=>{u=e,d.$ads=e}),!m())return void D();if(e.pause(),!m())return;const r=p,a=u.ownerDocument;r.fullscreen(e.fullscreen),r.bind(i,{skip:w,click(){n.url&&(a.defaultView||window).open(n.url),m()&&e.emit("artplayerPluginAds:click",n)},fullscreen(){e.fullscreen=!e.fullscreen,m()&&r.fullscreen(e.fullscreen)}}),i.on("fullscreen",()=>r.fullscreen(e.fullscreen));const c=()=>a.hidden?v.pause():v.play();i.dom(a,"visibilitychange",c),i.on("document:visibilitychange",c);const y=()=>{if(m()&&!g){if(g=!0,v.start(),a.hidden&&v.pause(),r.video){const e=r.video;x(()=>e.play(),e=>{m()&&(o(e),w())},()=>b(e))}m()&&r.ready()}};r.video?(i.dom(r.video,"error",w),i.dom(r.video,"loadedmetadata",y),r.video.src=n.video):y()}catch(r){throw k(),r}}}try{r.on("destroy",k),i.on("ready",()=>{f||"waiting"!==c||(f=!0,i.on("play",O),i.on("video:playing",O))}),e.isDestroy&&k()}catch(A){throw k(),A}return{name:"artplayerPluginAds",skip:w,pause:v.pause,play:v.play}}function u(e){return n=>{const t=n.constructor;if("function"!==typeof t.validator||!["append","query","setStyle"].every(e=>"function"===typeof Reflect.get(t.utils||{},e)))throw new Error("Artplayer Ads requires the core validator and DOM utilities");const l=r(e,t.validator),{volume:i,volumeClose:o,fullscreenOn:a,fullscreenOff:s,loading:u}=n.icons;return d(n,l,{volume:i,volumeClose:o,fullscreenOn:a,fullscreenOff:s,loading:u},t.utils)}}const p=Object.assign(u,{default:u});if("undefined"!==typeof document&&!document.getElementById("artplayer-plugin-ads")){const e=document.createElement("style");e.id="artplayer-plugin-ads",e.textContent=".artplayer-plugin-ads{position:absolute;z-index:150;inset:0;width:100%;height:100%;overflow:hidden;font-size:13px;line-height:1;color:#fff;background-color:#000}.artplayer-plugin-ads .artplayer-plugin-ads-html{display:flex;align-items:center;justify-content:center;width:100%;height:100%}.artplayer-plugin-ads .artplayer-plugin-ads-video{width:100%;height:100%}.artplayer-plugin-ads .artplayer-plugin-ads-timer{display:none;position:absolute;top:10px;right:10px}.artplayer-plugin-ads .artplayer-plugin-ads-timer>div{display:flex;align-items:center;background-color:#00000080;border-radius:15px;margin-left:5px;padding:5px 10px;cursor:pointer}.artplayer-plugin-ads .artplayer-plugin-ads-control{display:none;position:absolute;bottom:10px;right:10px}.artplayer-plugin-ads .artplayer-plugin-ads-control>div{display:flex;align-items:center;background-color:#00000080;border-radius:15px;margin-left:5px;padding:5px 10px;cursor:pointer}.artplayer-plugin-ads .artplayer-plugin-ads-control .art-icon svg{width:20px;height:20px}.artplayer-plugin-ads .artplayer-plugin-ads-loading{display:flex;align-items:center;justify-content:center;position:absolute;inset:0;width:100%;height:100%}",document.head.appendChild(e)}return p}); diff --git a/packages/artplayer-plugin-ads/dist/artplayer-plugin-ads.mjs b/packages/artplayer-plugin-ads/dist/artplayer-plugin-ads.mjs index 73386eb0d..7b7e55437 100644 --- a/packages/artplayer-plugin-ads/dist/artplayer-plugin-ads.mjs +++ b/packages/artplayer-plugin-ads/dist/artplayer-plugin-ads.mjs @@ -279,7 +279,7 @@ function createSession(art, option, icons, utils) { } catch (error) { report(error); } finally { - if (template.$ads === root) + if (root && template.$ads === root) delete template.$ads; } } diff --git a/packages/artplayer-plugin-ads/src/session.ts b/packages/artplayer-plugin-ads/src/session.ts index 46172dff3..0c72de342 100644 --- a/packages/artplayer-plugin-ads/src/session.ts +++ b/packages/artplayer-plugin-ads/src/session.ts @@ -72,7 +72,7 @@ export function createSession(art: Host, option: Options, icons: Icons, utils: U } catch (error) { report(error) } finally { - if (template.$ads === root) + if (root && template.$ads === root) delete template.$ads } } diff --git a/refactor/ads-validation.md b/refactor/ads-validation.md index e830dc20a..fd260eb3f 100644 --- a/refactor/ads-validation.md +++ b/refactor/ads-validation.md @@ -50,6 +50,23 @@ accepted-with-scope;保留诊断并编译迁移示例,不把批准范围扩 `test/ads.test.js` 增加候选 `.default === factory` 及同步取消断言,覆盖三种产物。 这些检查不替代全部历史子路径、媒体/设备与发布验收。 +## 05 真实 UI 与标签可见性 + +ads-ui.spec.js 以三个实际核心和三个浏览器引擎覆盖原生全屏、双实例媒体/静音/销毁 +隔离、实际详情导航及390px视口布局。不把视口模拟称为真机。main/legacy各27项。 + +`yarn test:ads-native-visibility` 使用本机Chrome的独立profile和默认context,通过 +connectOverCDP(noDefaults:true)避免框架强制可见。保留真实页面visibilitychange及 +isTrusted,只禁用桌面窗口遮挡背景化以排除其他应用干扰。测试开始先切换到本页并 +确认可见,再等待元数据;不放宽超时。三个核心分别验证HTML/视频广告后台倒计时 +停止、返回恢复、再次后台销毁后不触发skip。视频后台是否暂停作为浏览器状态记录, +不假定不同浏览器策略相同。main/legacy、失败尝试和trace分别保存,见05变更记录。 + +脚本独占8084,报告使用独立目录;关闭自建浏览器和服务后才运行下一浏览器套件。 +DevToolsActivePort刚创建时可能EBUSY/未写完,启动器仅对这两个已确认瞬态条件做 +有界重读,不重启浏览器,也不读取用户profile。缺安装路径直接失败,支持显式 +ARTPLAYER_NATIVE_CHROME。实际设备、最小化、全量分发仍是后续门槛。 + ## 边界与文件职责 - `test/helpers/ads.js`:来源加载、冻结 bundle 的 VM、可控时钟和最小 ArtPlayer 宿主。 diff --git a/refactor/baselines/ads-ui-visibility-validation.json b/refactor/baselines/ads-ui-visibility-validation.json new file mode 100644 index 000000000..17dd77f20 --- /dev/null +++ b/refactor/baselines/ads-ui-visibility-validation.json @@ -0,0 +1,538 @@ +{ + "schemaVersion": 1, + "task": "PKG-ADS-05", + "status": "doing", + "sourceCommit": "fb1cfa70", + "inputs": [ + { + "file": "package.json", + "sha256LF": "55b35b5738356fb88755ff55f7b3baa274ff78ed02dbd84888aac34eb490f4a3" + }, + { + "file": "packages/artplayer-plugin-ads/src/session.ts", + "sha256LF": "c45b0c63870975cf8fbd6f93340427de387d9dc15c56dfb3ad2ddacad8380e63" + }, + { + "file": "test/ads-lifecycle.test.js", + "sha256LF": "3411000b63e5bee5ec1f4a422d333e90a856d476eb9f544d5b1049dbd63fbbfd" + }, + { + "file": "test/browser/ads-ui.spec.js", + "sha256LF": "90a215d534703aa69654a3258fa5a46c29c308645356348db0229e1bc950bf4f" + }, + { + "file": "test/helpers/native-chrome.js", + "sha256LF": "4f486db36f3e5738ee835005b0bd073b3878507b6ea417a01e5663ab6954d93b" + }, + { + "file": "refactor/scripts/ads-native-visibility.mjs", + "sha256LF": "7521c78c3874cdc4b5029fe85b00a9afc6b8a14fb71728d64e588717219f19d0" + } + ], + "artifacts": [ + { + "file": "packages/artplayer-plugin-ads/dist/artplayer-plugin-ads.js", + "sha256": "1429541242f49c4d558eda242a63b6e57a876c2dba06ce9f631d71d315028023", + "bytes": 7279, + "copy": { + "file": "docs/compiled/artplayer-plugin-ads.js", + "sha256": "1429541242f49c4d558eda242a63b6e57a876c2dba06ce9f631d71d315028023", + "bytes": 7279 + } + }, + { + "file": "packages/artplayer-plugin-ads/dist/artplayer-plugin-ads.legacy.js", + "sha256": "4f64ac015e0850b71f51cf01d9fc6f238c95414730c71e91d1240c40e28499d8", + "bytes": 7718, + "copy": { + "file": "docs/compiled/artplayer-plugin-ads.legacy.js", + "sha256": "4f64ac015e0850b71f51cf01d9fc6f238c95414730c71e91d1240c40e28499d8", + "bytes": 7718 + } + }, + { + "file": "packages/artplayer-plugin-ads/dist/artplayer-plugin-ads.mjs", + "sha256": "8ad7df8145b2514467cac0f733151619c36fa965e620df575c0779604372503f", + "bytes": 13067, + "copy": { + "file": "docs/compiled/artplayer-plugin-ads.mjs", + "sha256": "8ad7df8145b2514467cac0f733151619c36fa965e620df575c0779604372503f", + "bytes": 13067 + } + } + ], + "fixed": "Destroy before initialization no longer deletes a caller-owned undefined template.$ads descriptor. Reproduced first, then verified in source and all three generated artifacts.", + "node": { + "before": { + "file": "refactor/.cache/ads05-ownership-before.log", + "sha256": "05a9e9a3cfed36b11ea62c3695c3e6d4cebb08da03ed6832fd2d23c97110d9ce", + "bytes": 1308 + }, + "after": { + "file": "refactor/.cache/ads05-artifacts-node.log", + "sha256": "b89fdd0efe47c96c170ce3e6d97a47c919334832c7ef792359f0b5f41fa2dc1e", + "bytes": 21921 + }, + "passed": 194, + "failed": 0 + }, + "build": { + "file": "refactor/.cache/ads05-build.log", + "sha256": "be382dde6782d27827ddec67d4bd93b6e45e4ca6a88fad953316cb0c9ded107a", + "bytes": 1438 + }, + "browser": [ + { + "format": "main", + "stats": { + "startTime": "2026-09-12T00:34:58.186Z", + "duration": 30596.369, + "expected": 27, + "skipped": 0, + "unexpected": 0, + "flaky": 0 + }, + "report": { + "file": "refactor/.cache/ads05-ui-main-report.json", + "sha256": "dc641b693ded43d4bcd43e67c4772e0874243b8c50f1fddba22f5b5ee3ec26ea", + "bytes": 541550 + }, + "log": { + "file": "refactor/.cache/ads05-ui-main.log", + "sha256": "521308ceca482e1cb6584039c63e00601802a5f5ceb4bb140596969c94dfa9e1", + "bytes": 6563 + }, + "results": [ + { + "file": "refactor/.cache/ads05-ui-main-results/.last-run.json", + "sha256": "91d1c43004802cd49950d78eb11c8fa7d05da8ffffe219a8b13b2f561bc00903", + "bytes": 45 + } + ] + }, + { + "format": "legacy", + "stats": { + "startTime": "2026-09-12T00:44:58.841Z", + "duration": 25158.441, + "expected": 27, + "skipped": 0, + "unexpected": 0, + "flaky": 0 + }, + "report": { + "file": "refactor/.cache/ads05-ui-legacy-report.json", + "sha256": "33ddb6f0f8ccb62af712c0fcd386e0aa86b88bd54aafc03b11e53c2ed9540ecc", + "bytes": 542840 + }, + "log": { + "file": "refactor/.cache/ads05-ui-legacy.log", + "sha256": "c1bd76322c2aeda4a348edb7b5953fd717efa9e53058a77110af2a19cc37c701", + "bytes": 6563 + }, + "results": [ + { + "file": "refactor/.cache/ads05-ui-legacy-results/.last-run.json", + "sha256": "91d1c43004802cd49950d78eb11c8fa7d05da8ffffe219a8b13b2f561bc00903", + "bytes": 45 + } + ] + } + ], + "native": [ + { + "format": "main", + "browser": "152.0.7977.84", + "executable": "C:/Program Files/Google/Chrome/Application/chrome.exe", + "scope": "Native selected/background tabs in isolated installed Chrome. Desktop window occlusion excluded by --disable-backgrounding-occluded-windows; no focus or visibility property emulation. Does not prove minimized-window or mobile lifecycle behavior.", + "cases": [ + { + "core": "published-4.5.5", + "media": "html", + "hiddenText": "30秒", + "events": [ + { + "hidden": true, + "trusted": true + }, + { + "hidden": false, + "trusted": true + } + ], + "hiddenVideo": null, + "restoredVideo": null, + "destroyed": true + }, + { + "core": "published-4.5.5", + "media": "video", + "hiddenText": "30秒", + "events": [ + { + "hidden": true, + "trusted": true + }, + { + "hidden": false, + "trusted": true + } + ], + "hiddenVideo": { + "time": 0.428142, + "paused": true + }, + "restoredVideo": { + "time": 2.350778, + "paused": false + }, + "destroyed": true + }, + { + "core": "published", + "media": "html", + "hiddenText": "30秒", + "events": [ + { + "hidden": true, + "trusted": true + }, + { + "hidden": false, + "trusted": true + } + ], + "hiddenVideo": null, + "restoredVideo": null, + "destroyed": true + }, + { + "core": "published", + "media": "video", + "hiddenText": "30秒", + "events": [ + { + "hidden": true, + "trusted": true + }, + { + "hidden": false, + "trusted": true + } + ], + "hiddenVideo": { + "time": 0.423085, + "paused": true + }, + "restoredVideo": { + "time": 2.34293, + "paused": false + }, + "destroyed": true + }, + { + "core": "candidate", + "media": "html", + "hiddenText": "30秒", + "events": [ + { + "hidden": true, + "trusted": true + }, + { + "hidden": false, + "trusted": true + } + ], + "hiddenVideo": null, + "restoredVideo": null, + "destroyed": true + }, + { + "core": "candidate", + "media": "video", + "hiddenText": "30秒", + "events": [ + { + "hidden": true, + "trusted": true + }, + { + "hidden": false, + "trusted": true + } + ], + "hiddenVideo": { + "time": 0.431513, + "paused": true + }, + "restoredVideo": { + "time": 2.356181, + "paused": false + }, + "destroyed": true + } + ], + "report": { + "file": "refactor/.cache/ads-native-visibility-KDPUQX/report.json", + "sha256": "73a9cba3b6cb6772cd89f83fe8318f942d9502f764b39bf91b0d25050aaaa6ff", + "bytes": 8852 + }, + "files": [ + { + "file": "refactor/.cache/ads-native-visibility-KDPUQX/report.json", + "sha256": "73a9cba3b6cb6772cd89f83fe8318f942d9502f764b39bf91b0d25050aaaa6ff", + "bytes": 8852 + }, + { + "file": "refactor/.cache/ads-native-visibility-KDPUQX/server.log", + "sha256": "f8605bce374d9ef389168f86c0cb76d3c35d68089a3c609bb9634f17ce455e57", + "bytes": 43 + }, + { + "file": "refactor/.cache/ads-native-visibility-KDPUQX/trace.zip", + "sha256": "cf8dc9ae78d9cfc441aa5d9ad907cdf23cb76456ccda0f32200ec83fb9a172e3", + "bytes": 2383611 + } + ], + "log": { + "file": "refactor/.cache/ads05-native-main-video.log", + "sha256": "25249bdf4cb22e1277931a6393c9b036d593d694da210a057b249cf83137c598", + "bytes": 510 + } + }, + { + "format": "legacy", + "browser": "152.0.7977.84", + "executable": "C:/Program Files/Google/Chrome/Application/chrome.exe", + "scope": "Native selected/background tabs in isolated installed Chrome. Desktop window occlusion excluded by --disable-backgrounding-occluded-windows; no focus or visibility property emulation. Does not prove minimized-window or mobile lifecycle behavior.", + "cases": [ + { + "core": "published-4.5.5", + "media": "html", + "hiddenText": "30秒", + "events": [ + { + "hidden": true, + "trusted": true + }, + { + "hidden": false, + "trusted": true + } + ], + "hiddenVideo": null, + "restoredVideo": null, + "destroyed": true + }, + { + "core": "published-4.5.5", + "media": "video", + "hiddenText": "30秒", + "events": [ + { + "hidden": true, + "trusted": true + }, + { + "hidden": false, + "trusted": true + } + ], + "hiddenVideo": { + "time": 0.448822, + "paused": true + }, + "restoredVideo": { + "time": 2.388307, + "paused": false + }, + "destroyed": true + }, + { + "core": "published", + "media": "html", + "hiddenText": "30秒", + "events": [ + { + "hidden": true, + "trusted": true + }, + { + "hidden": false, + "trusted": true + } + ], + "hiddenVideo": null, + "restoredVideo": null, + "destroyed": true + }, + { + "core": "published", + "media": "video", + "hiddenText": "30秒", + "events": [ + { + "hidden": true, + "trusted": true + }, + { + "hidden": false, + "trusted": true + } + ], + "hiddenVideo": { + "time": 0.430689, + "paused": true + }, + "restoredVideo": { + "time": 2.361555, + "paused": false + }, + "destroyed": true + }, + { + "core": "candidate", + "media": "html", + "hiddenText": "30秒", + "events": [ + { + "hidden": true, + "trusted": true + }, + { + "hidden": false, + "trusted": true + } + ], + "hiddenVideo": null, + "restoredVideo": null, + "destroyed": true + }, + { + "core": "candidate", + "media": "video", + "hiddenText": "30秒", + "events": [ + { + "hidden": true, + "trusted": true + }, + { + "hidden": false, + "trusted": true + } + ], + "hiddenVideo": { + "time": 0.441339, + "paused": true + }, + "restoredVideo": { + "time": 2.365354, + "paused": false + }, + "destroyed": true + } + ], + "report": { + "file": "refactor/.cache/ads-native-visibility-lSst7b/report.json", + "sha256": "dcd60b88cff93a9843819ce7ceb8aab71c43f58eddf57bcdd68a2b62c7e79e07", + "bytes": 8902 + }, + "files": [ + { + "file": "refactor/.cache/ads-native-visibility-lSst7b/report.json", + "sha256": "dcd60b88cff93a9843819ce7ceb8aab71c43f58eddf57bcdd68a2b62c7e79e07", + "bytes": 8902 + }, + { + "file": "refactor/.cache/ads-native-visibility-lSst7b/server.log", + "sha256": "f8605bce374d9ef389168f86c0cb76d3c35d68089a3c609bb9634f17ce455e57", + "bytes": 43 + }, + { + "file": "refactor/.cache/ads-native-visibility-lSst7b/trace.zip", + "sha256": "890ef0d36e9f32b98590279281d732767f21a6b4c474be924adc7d7756a6c018", + "bytes": 2453467 + } + ], + "log": { + "file": "refactor/.cache/ads05-native-legacy-video.log", + "sha256": "6e01a308c3c218fff108f47be871b379c0d0c38279980540fbee3623ef351979", + "bytes": 510 + } + } + ], + "visual": { + "file": "refactor/.cache/ads05-narrow-review.png", + "sha256": "ce01253a61890514a79d66a693c8839a659038451f938b8995ed9d03435a91ca", + "bytes": 5387, + "inspected": true, + "note": "390px viewport, 360px player; translated close/countdown/details and fullscreen controls visible without overflow. Not a physical phone." + }, + "ci": { + "file": "refactor/.cache/ads05-ci.log", + "sha256": "cb77116e93e65ef47e9118b3b6ffbcf1868f6b0c535593bea480a9a9d9031db3", + "bytes": 87478, + "groups": [ + 772, + 14, + 49 + ], + "passed": 835, + "productionTS": 269 + }, + "diagnosticAttempts": [ + { + "file": "refactor/.cache/ads05-ui-source-report.json", + "sha256": "144a699807c807ee15e753ef2fd1a79939bceef959875da037600f6f91008db1", + "bytes": 533140 + }, + { + "file": "refactor/.cache/ads05-visibility-probe.json", + "sha256": "e8e8fec96c14429e03669fedef2c4cdb85de614d4e0aac7b8088e57643b91eef", + "bytes": 1356 + }, + { + "file": "refactor/.cache/ads05-visibility-chrome-probe.json", + "sha256": "436d44baa09185e3ea198cce9bda37c88fa2499ae77147f65fab910286827021", + "bytes": 541 + }, + { + "file": "refactor/.cache/ads05-native-cdp-probe.json", + "sha256": "f4ee12f981fdc5000ae43cd46844772898bed4eafcdc3843b09b71bdaf5086f4", + "bytes": 643 + }, + { + "file": "refactor/.cache/ads05-headed-install-script.log", + "sha256": "7a3bc4655c858bccff0130b36575da3dc10982a716bd269b49bdb49677eeb13c", + "bytes": 5297 + }, + { + "file": "refactor/.cache/ads-native-visibility-tJauB0/report.json", + "sha256": "6a04f099e92d8bba7396dc15955ff319487ebf16d6e551e22617ba86141eb65a", + "bytes": 6703 + }, + { + "file": "refactor/.cache/ads-native-visibility-ORaYRO/report.json", + "sha256": "d3601fd60d6da0b7178b8620c7ba3f6f684f192e915f19ece71c236c877b8a8d", + "bytes": 7109 + }, + { + "file": "refactor/.cache/ads-native-visibility-ghHsLL/report.json", + "sha256": "8aad442bf3f144d45deaa8ce134b8c2c6a6b2e7701cc8b1a1000250d26c3d295", + "bytes": 6379 + }, + { + "file": "refactor/.cache/ads-native-visibility-U9bOpa/report.json", + "sha256": "3839f6778d1ff6b3945b17a44baf6d68b50136b9f2c1ca37a5d1f16d7c9d5236", + "bytes": 6843 + } + ], + "remaining": [ + "Physical mobile/Safari lifecycle and minimize/background policy are not covered by Chromium/Firefox/WebKit automation or native Chrome tab checks; task05 remains doing.", + "Complete historical package distribution and docs demo belong to task06.", + "VAST-only VPN script loading exception recorded after user correction; not applied to Ads.", + "No push, tag, merge or publish." + ] +} diff --git a/refactor/changes/2026-09-12-PKG-ADS-05-browser.md b/refactor/changes/2026-09-12-PKG-ADS-05-browser.md new file mode 100644 index 000000000..bbe0b076f --- /dev/null +++ b/refactor/changes/2026-09-12-PKG-ADS-05-browser.md @@ -0,0 +1,55 @@ +# PKG-ADS-05 页面与媒体组合:进行中 + +起点 fb1cfa70;本步仍 doing。Ads-04 的类型推导修正已由用户明确接受,范围不扩大。 + +2026-09-12,用户更正:可能被 VPN 规则阻止的是 VAST 脚本,若始终无法加载,可以 +记录并跳过,由用户后续修复。授权范围仅 VAST 的 VPN 加载限制,不适用于 Ads, +也不将未执行检查记为通过。Ads 本地脚本加载成功,继续正常验收。 + +## 已修复与已验证 + +- 复现销毁前从未初始化广告时,调用方预设的 `template.$ads = undefined` 属性描述符 + 被误删。session 的清理增加 root 存在判断,只删除实际拥有的节点引用。 + 原失败见 `.cache/ads05-ownership-before.log`;当前源码及三格式194项Node测试通过, + 见 `.cache/ads05-artifacts-node.log`。构建由仓库脚本生成。 +- 新增 ads-ui.spec.js,三个核心(实际4.5.5、5.4.1、候选)与三个浏览器引擎, + 验证原生全屏前初始化、外部退出、广告按钮切换、双实例真实视频/静音/销毁隔离、 + 实际详情popup与窄屏翻译控件布局。源码、正式main和legacy矩阵分别27项通过。 + 报告/完整results分别归档于 `.cache/ads05-ui-source-*`、`.cache/ads05-ui-main-*`、 + `.cache/ads05-ui-legacy-*`。实际查看390px截图,控件和翻译文案无溢出;不是手机真机。 + +## 真实后台页面验证 + +无头三引擎和普通headed Playwright实例切换标签均保持 visible,不能算验收。 +浏览器连接工具当前返回 nodeRepl.fetch request failed;完整测试Chromium下载多次 +30秒超时,日志保留。未改变锁文件或浏览器版本声明。 + +本机Chrome152.0.7977.84以独立临时profile启动,通过connectOverCDP的noDefaults +关闭自动框架默认模拟。探针实际获得可信 hidden/visible 事件,见 +`.cache/ads05-native-cdp-probe.json`。不会连接用户日常profile,不修改document.hidden。 + +新增可重跑 `yarn test:ads-native-visibility` 和 native-chrome helper:独占8084服务、 +读取候选产物、保存trace及报告,最终关闭自建浏览器和服务,临时profile清理有目录校验。 +首轮广告集成在4.5.5的isReady等待失败;报告和trace保留在 +`.cache/ads-native-visibility-tJauB0/`,日志 `.cache/ads05-native-main.log`。 +追加状态观测证实页面初始hidden、无元数据请求,不是脚本下载失败。显式切到测试页 +后进一步确认桌面其他窗口遮挡影响;通过单一 --disable-backgrounding-occluded-windows +排除桌面遮挡,只测真实标签选择切换。noDefaults仍保留,不模拟focus或document.hidden。 +Chrome刚创建DevToolsActivePort时曾出现EBUSY,启动器有界等待文件完整内容,不重启进程。 + +最终main/legacy各6组:4.5.5/5.4.1/候选核心分别使用HTML和视频广告,真实可信 +hidden/visible事件、后台2200ms倒计时不变、返回后恢复倒计时,视频时间继续推进且 +paused=false;再次后台销毁后回前台不出现广告或skip。报告包含浏览器版本、实际 +核心manifest、广告产物SHA、各例隐藏/恢复媒体状态和trace。此范围不包含桌面 +最小化和真实移动设备生命周期。 + +最终代码/产物指纹、原失败、main/legacy UI和原生Chrome结果、CI统计见 +[验证证据](../baselines/ads-ui-visibility-validation.json)。源码和三产物Node194通过, +本地完整CI835项通过、269个生产TS严格检查、37份核心声明无漂移。 + +## 尚待完成 + +真机Safari/移动端和最小化等实际后台策略仍待对应设备证据,05继续doing。Ads核心 +支持门槛、资源所有权和桌面UI具体问题已有闭环证据;媒体设备与完整分发不提前关闭。 +全包发布、npm分发完整验收不属于本次通过结论。 +本批以PKG-ADS-05独立本地检查点提交,保留明确未完成项,无推送或发布。 diff --git a/refactor/environment-matrix.md b/refactor/environment-matrix.md index 6bba3627f..1575ffda0 100644 --- a/refactor/environment-matrix.md +++ b/refactor/environment-matrix.md @@ -4,6 +4,11 @@ BASE-08;2026-09-10;来源提交 ed293231。矩阵建立完成,矩阵内未 ## 版本和分发维度 +用户环境例外(2026-09-12):VAST 脚本可能被 VPN 规则阻止;若确认始终无法加载, +用户授权记录失败证据并跳过该项,留待用户后续修复,继续其他重构。用户已明确更正 +对象为 VAST,不是 Ads。执行时保存实际 URL、错误及未验证范围;跳过不等于通过, +不扩大为其他 SDK、运行时缺陷或设备检查的豁免。 + 工作区版本不是发布证据。当前只冻结 core 5.4.0 和 chapter 1.1.0 的发布 tarball,核心工作区为 5.4.1;其余包的真实发布版本、旧核心支持窗口由各包 01 任务核实。历史最低浏览器/TS 版本尚未证明,不把本机版本设为用户最低版本。全部包仍按自身下一 major 准备,版本目标见 [版本策略](version-policy.md)。 | 检查 ID | 内容及执行方法 | 当前证据/状态 | 责任与发布影响 | @@ -33,7 +38,7 @@ Node10 是 TS 模块解析模式名,不代表支持 Node.js 10。现代 es2020 | 包 / 工作区版本 | 能力与固定样本/资源要求 | 当前状态、待补环境 | 负责验证任务 | | --- | --- | --- | --- | | artplayer / 5.4.1 | 原生媒体、字幕、切源、控件/设置、全屏/PiP、SSR;ENV-MEDIA + VTT/SRT | 发布 5.4.0 已冻结;桌面部分基线,移动/原生全屏/PiP/多引擎待测 | CORE-07/16/17/18/22/23、ENG-05 | -| artplayer-plugin-ads / npm 1.0.6、工作区 2.1.0 未发布 | 五个 1.0.x 归档及实际核心 4.5.5 归档已冻结;HTML/视频、本片恢复、跳过、错误/销毁;本地短广告和主片 | 4.5.5/5.4.1/候选核心 × 发布/候选插件实际组合见 ads-validation.md;设备/隐藏页/全屏与完整分发仍待,不扩大到全部历史核心。无需借 IMA 结果替代自有逻辑 | PKG-ADS-01/02/03/05/06 | +| artplayer-plugin-ads / npm 1.0.6、工作区 2.1.0 未发布 | 五个 1.0.x 归档及实际核心 4.5.5 归档已冻结;HTML/视频、本片恢复、跳过、错误/销毁;本地短广告和主片 | 4.5.5/5.4.1/候选核心组合、三引擎原生全屏/双实例/UI的main和legacy各27项;本机Chrome152真实标签暂停/视频恢复/后台销毁各6项见ads-validation.md。实际设备/最小化及完整分发仍待,不扩大到全部历史核心 | PKG-ADS-01/02/03/05/06 | | artplayer-plugin-ambilight / 1.1.0 | Canvas 采样、CORS、resize、后台/销毁;同源及受控跨源 MP4 | 待真实画布和资源测量 | PKG-AMBILIGHT-01/02/05/06 | | artplayer-plugin-asr / 2.1.0 | PCM 音频输出/暂停/销毁;AudioContext + 调用方服务 | 待音频环境与调用方 WebSocket 验证;示例服务版本未知,不在 PR 调线上服务 | PKG-ASR-01/02/05/06、EX-03 | | artplayer-plugin-audio-track / 1.1.0 | 独立外部 Audio 与主视频时钟同步;本地 MP4+AAC,偏移/切源/缓冲/失败样本 | 1.1.0 发布及调用契约见 [包基线](baselines/audio-track-contract.md);旧核心 5.4.0 为具体对照点,桌面/Safari 实测待完成 | PKG-AUDIO-01/02/05/06 | diff --git a/refactor/plan.md b/refactor/plan.md index 75abc9e6a..c48929999 100644 --- a/refactor/plan.md +++ b/refactor/plan.md @@ -4,7 +4,7 @@ 基线:`40fcda6a37d0049d42e49c1e64e70d4fd9ba5f7f`。总任务 217 项,范围 22 个包及工作区/示例。 -状态:todo 135 / doing 4 / blocked 0 / done 78 / deferred 0。风险 L/M/H 表示兼容风险,不表示工期。 +状态:todo 134 / doing 5 / blocked 0 / done 78 / deferred 0。风险 L/M/H 表示兼容风险,不表示工期。 前置依赖是启动条件;验收是完成条件。任务可以继续拆分,但不能复用或悄悄删除旧 ID。 @@ -260,7 +260,7 @@ | PKG-ADS-02 | artplayer-plugin-ads
建立特有行为与错误测试 | PKG-ADS-01, ENG-03, ENG-05 | 固定 npm 1.0.6 与工作区契约;HTML/视频/图片 HTML、跳过阈值、计时链、工厂复用、多实例、过早/重复方法、播放拒绝与销毁 | 旧版本行为可重跑,成功/失败/切源/销毁有必要断言 | H | done | | PKG-ADS-03 | artplayer-plugin-ads
整理内部职责与资源 | PKG-ADS-02, CORE-10, CORE-13 | 配置/广告状态/计时/视图/主视频恢复分离,保留产品规则;资源与版本检查改用经验证的旧核心能力边界 | 结构变化和缺陷修复分开记录;原 API/事件/资源生命周期通过 | H | done | | PKG-ADS-04 | artplayer-plugin-ads
迁移自有源码和公开类型 | PKG-ADS-03, ENG-04, ENG-06, CORE-07 | 兼容真实 html/video/url/i18n、旧 export= 与工作区 source/type 类型接受面;时长 string/number 漂移与未实现别名单独处置 | 严格类型检查、旧消费样例通过;声明路径/导出和同步异步兼容 | H | done | -| PKG-ADS-05 | artplayer-plugin-ads
验证新旧核心和组合 | PKG-ADS-04, CORE-22 | 本地广告 fixture、多实例、正常结束/跳过/销毁;核验历史关联核心 4.5.5、发布 5.4.1 与候选核心的实际归档/能力/媒体组合 | 最终核心与原支持范围核心分别通过;设备/SDK 缺证据不能标完成 | H | todo | +| PKG-ADS-05 | artplayer-plugin-ads
验证新旧核心和组合 | PKG-ADS-04, CORE-22 | 本地广告 fixture、多实例、正常结束/跳过/销毁;核验历史关联核心 4.5.5、发布 5.4.1 与候选核心的实际归档/能力/媒体组合 | 最终核心与原支持范围核心分别通过;设备/SDK 缺证据不能标完成 | H | doing | | PKG-ADS-06 | artplayer-plugin-ads
验证分发并同步文档 | PKG-ADS-05, ENG-07 | ads.js/README/声明一致;旧 require(pkg).default、原 dist 路径、当前 callable/ESM/legacy 的隔离 tarball 及浏览器验收 | tarball 入口/资源、类型、8082 demo 和 README 一致,有回退记录 | H | todo | ## 5 包迁移:artplayer-plugin-vast @@ -488,3 +488,4 @@ - PKG-ADS-02: [记录](changes/2026-09-12-PKG-ADS-02-tests.md) [记录](ads-validation.md) [记录](baselines/ads-validation.json) - PKG-ADS-03: [记录](changes/2026-09-12-PKG-ADS-03-lifecycle.md) [记录](ads-validation.md) [记录](baselines/ads-lifecycle-validation.json) - PKG-ADS-04: [记录](changes/2026-09-12-PKG-ADS-04-types.md) [记录](baselines/ads-types-validation.json) +- PKG-ADS-05: [记录](changes/2026-09-12-PKG-ADS-05-browser.md) [记录](baselines/ads-ui-visibility-validation.json) diff --git a/refactor/progress.md b/refactor/progress.md index 7d4975f59..9932f1f43 100644 --- a/refactor/progress.md +++ b/refactor/progress.md @@ -1,5 +1,16 @@ # 进度与证据 +## 当前实施:Ads-05 页面与媒体组合 + +补上27项原生全屏/多实例真实视频/详情导航/窄屏布局验证,源码、正式main/legacy均通过。 +修复销毁前未创建广告时误删调用方undefined模板属性;源码及三格式Node194项通过。 +独立Chrome实际标签切换的main/legacy各6组通过,覆盖后台倒计时暂停、恢复后视频 +继续播放和后台销毁。首轮失败已定位为初始隐藏/桌面遮挡及启动端口文件竞争,保留 +原始失败,未放宽超时或模拟document.hidden。完整CI835项通过。 +关闭具体核心兼容/生命周期/桌面UI问题,实际设备媒体策略仍待验;VAST脚本VPN例外 +按用户更正单独登记。详见 [进行中记录](changes/2026-09-12-PKG-ADS-05-browser.md)。 +当前217项:78 done、5 doing、134 todo。05仍未完成,本批独立检查点提交。 + ## 最新完成:Ads-04 类型与导入兼容 公开声明补齐 html/video/url/i18n,增加 CJS/ESM 类型桥和同一运行时的准确 /runtime diff --git a/refactor/risk-table.md b/refactor/risk-table.md index 4af7c6277..0e89ca121 100644 --- a/refactor/risk-table.md +++ b/refactor/risk-table.md @@ -143,7 +143,7 @@ | DASH-SEEK-01 | open / 已复现 | dash.js 4.5.2 空裁剪漏更新缓冲量,稳定暂停 seek 后调度停滞 | PKG-DASH-05, REVIEW-02 | | ADS-TYPE-01 | accepted-with-scope / 已复现 | Ads 声明丢失真实 html/video/url,旧 totalDuration 为 string,当前 source/type 未实现 | PKG-ADS-02, PKG-ADS-04 | | ADS-DIST-01 | open / 已复现 | Ads npm 1.0.6 default namespace 与未发布 2.1.0 callable/exports 分发不同 | PKG-ADS-04, PKG-ADS-06, REL-01 | -| ADS-CORE-01 | open / 源码/产物事实 | Ads 旧发布关联核心 4.5.5,当前源码新增 >=5 门槛和 document 事件桥依赖 | PKG-ADS-02, PKG-ADS-03, PKG-ADS-05 | -| ADS-LIFE-01 | open / 已复现 | Ads 计时链、重复/过早调用、工厂复用与销毁缺乏资源边界 | PKG-ADS-02, PKG-ADS-03, PKG-ADS-05 | +| ADS-CORE-01 | resolved / 源码/产物事实 | Ads 旧发布关联核心 4.5.5,当前源码新增 >=5 门槛和 document 事件桥依赖 | PKG-ADS-02, PKG-ADS-03, PKG-ADS-05 | +| ADS-LIFE-01 | resolved / 已复现 | Ads 计时链、重复/过早调用、工厂复用与销毁缺乏资源边界 | PKG-ADS-02, PKG-ADS-03, PKG-ADS-05 | | ADS-MEDIA-01 | open / 已复现 | Ads metadata 与 play 拒绝、隐藏页媒体状态和内容恢复仍缺真实验证 | PKG-ADS-02, PKG-ADS-03, PKG-ADS-05 | -| ADS-UI-01 | open / 已复现 | Ads 零等待跳过与初始状态不符,全屏图标/外部状态需核验 | PKG-ADS-02, PKG-ADS-03, PKG-ADS-05 | +| ADS-UI-01 | resolved / 已复现 | Ads 零等待跳过与初始状态不符,全屏图标/外部状态需核验 | PKG-ADS-02, PKG-ADS-03, PKG-ADS-05 | diff --git a/refactor/risks.json b/refactor/risks.json index ec18d165e..bff932cfe 100644 --- a/refactor/risks.json +++ b/refactor/risks.json @@ -3034,7 +3034,7 @@ "id": "ADS-CORE-01", "title": "Ads 旧发布关联核心 4.5.5,当前源码新增 >=5 门槛和 document 事件桥依赖", "confirmation": "source-observed", - "status": "open", + "status": "resolved", "owners": [ "PKG-ADS-02", "PKG-ADS-03", @@ -3046,16 +3046,27 @@ "packages/artplayer-plugin-ads/src/index.ts", "refactor/baselines/ads-lifecycle-validation.json", "refactor/changes/2026-09-12-PKG-ADS-03-lifecycle.md", - "packages/artplayer-plugin-ads/ARCHITECTURE.md" + "packages/artplayer-plugin-ads/ARCHITECTURE.md", + "refactor/baselines/ads-ui-visibility-validation.json", + "refactor/changes/2026-09-12-PKG-ADS-05-browser.md" ], "compatibleResolution": "先核验实际核心归档和能力,通过本地 fallback 保留原用户调用;不能用未发布源码门槛强制全部旧消费者升级,也不泛称全部 4.x 通过。", - "closureCriteria": "4.5.5 关联来源和实际可用组合明确,5.4.1/候选核心与原生 visibility/事件桥路径有真实浏览器证据。" + "closureCriteria": "4.5.5 关联来源和实际可用组合明确,5.4.1/候选核心与原生 visibility/事件桥路径有真实浏览器证据。", + "resolutionEvidence": [ + "refactor/baselines/ads-ui-visibility-validation.json", + "refactor/changes/2026-09-12-PKG-ADS-05-browser.md", + "refactor/baselines/ads-lifecycle-validation.json", + "test/browser/ads-ui.spec.js", + "refactor/scripts/ads-native-visibility.mjs", + "test/ads-lifecycle.test.js" + ], + "resolutionRationale": "实际4.5.5归档已冻结;候选入口按能力检查,没有>=5门槛。4.5.5/5.4.1/候选核心main/legacy的三引擎UI及原生Chrome标签可见性通过,覆盖原生监听和较新核心事件桥,不泛化为全部4.x。" }, { "id": "ADS-LIFE-01", "title": "Ads 计时链、重复/过早调用、工厂复用与销毁缺乏资源边界", "confirmation": "reproduced", - "status": "open", + "status": "resolved", "owners": [ "PKG-ADS-02", "PKG-ADS-03", @@ -3070,10 +3081,21 @@ "refactor/baselines/ads-validation.json", "refactor/baselines/ads-lifecycle-validation.json", "refactor/changes/2026-09-12-PKG-ADS-03-lifecycle.md", - "packages/artplayer-plugin-ads/ARCHITECTURE.md" + "packages/artplayer-plugin-ads/ARCHITECTURE.md", + "refactor/baselines/ads-ui-visibility-validation.json", + "refactor/changes/2026-09-12-PKG-ADS-05-browser.md" ], "compatibleResolution": "先复现重复 play 的计时链、skip 后计时、destroy 后恢复与复用工厂 options;以实例所有权和幂等清理修正,不更改广告时长/跳过规则。", - "closureCriteria": "时钟/订阅/DOM 与主片恢复的成功、异常、重入、销毁、复用和多实例回归通过。" + "closureCriteria": "时钟/订阅/DOM 与主片恢复的成功、异常、重入、销毁、复用和多实例回归通过。", + "resolutionEvidence": [ + "refactor/baselines/ads-ui-visibility-validation.json", + "refactor/changes/2026-09-12-PKG-ADS-05-browser.md", + "refactor/baselines/ads-lifecycle-validation.json", + "test/browser/ads-ui.spec.js", + "refactor/scripts/ads-native-visibility.mjs", + "test/ads-lifecycle.test.js" + ], + "resolutionRationale": "03的可重跑重复/过早/复用/重入/拒绝修复与本批194项源码及三格式回归通过;真实双实例、后台销毁及自有资源清理通过。新增未创建root时误删调用方undefined属性的复现与修复,不触碰未拥有的引用。" }, { "id": "ADS-MEDIA-01", @@ -3095,16 +3117,18 @@ "refactor/baselines/ads-validation.json", "refactor/baselines/ads-lifecycle-validation.json", "refactor/changes/2026-09-12-PKG-ADS-03-lifecycle.md", - "packages/artplayer-plugin-ads/ARCHITECTURE.md" + "packages/artplayer-plugin-ads/ARCHITECTURE.md", + "refactor/baselines/ads-ui-visibility-validation.json", + "refactor/changes/2026-09-12-PKG-ADS-05-browser.md" ], - "compatibleResolution": "用本地媒体验证事件安装顺序、加载失败与拒绝、倒计时和广告播放的独立语义,恢复主片时不吞公共拒绝或在销毁后播放。", + "compatibleResolution": "用本地媒体验证事件安装顺序、加载失败与拒绝、倒计时和广告播放的独立语义,恢复主片时不吞公共拒绝或在销毁后播放。 05已验证桌面Chrome真实后台标签暂停/恢复、视频继续播放和后台销毁,main/legacy各6组合;最小化和实际移动端媒体策略仍缺设备证据,保持open。", "closureCriteria": "实际广告/主片的加载、暂停/恢复、失败/换源/销毁和隐藏状态有新旧核心组合证据。" }, { "id": "ADS-UI-01", "title": "Ads 零等待跳过与初始状态不符,全屏图标/外部状态需核验", "confirmation": "reproduced", - "status": "open", + "status": "resolved", "owners": [ "PKG-ADS-02", "PKG-ADS-03", @@ -3120,10 +3144,21 @@ "refactor/baselines/ads-validation.json", "refactor/baselines/ads-lifecycle-validation.json", "refactor/changes/2026-09-12-PKG-ADS-03-lifecycle.md", - "packages/artplayer-plugin-ads/ARCHITECTURE.md" + "packages/artplayer-plugin-ads/ARCHITECTURE.md", + "refactor/baselines/ads-ui-visibility-validation.json", + "refactor/changes/2026-09-12-PKG-ADS-05-browser.md" ], "compatibleResolution": "复现 playDuration<=0 文案可关闭但 isCanClose 尚为 false;按既有 demo 规则修复,验证全屏按钮和外部退出状态,保留 CSS/DOM 钩子。", - "closureCriteria": "零/正/不可跳阈值、真实点击、倒计时/详情/静音/全屏与外部状态组合均有回归。" + "closureCriteria": "零/正/不可跳阈值、真实点击、倒计时/详情/静音/全屏与外部状态组合均有回归。", + "resolutionEvidence": [ + "refactor/baselines/ads-ui-visibility-validation.json", + "refactor/changes/2026-09-12-PKG-ADS-05-browser.md", + "refactor/baselines/ads-lifecycle-validation.json", + "test/browser/ads-ui.spec.js", + "refactor/scripts/ads-native-visibility.mjs", + "test/ads-lifecycle.test.js" + ], + "resolutionRationale": "零/正/不可关闭阈值及错误输入由现有Node/媒体回归保持;main/legacy分别27项真实原生全屏(初始/外部退出/按钮)、静音、详情popup和窄屏翻译控件通过。截图已查看;手机设备策略仍由05媒体门槛承接。" } ] } diff --git a/refactor/scripts/ads-native-visibility.mjs b/refactor/scripts/ads-native-visibility.mjs new file mode 100644 index 000000000..695766d8f --- /dev/null +++ b/refactor/scripts/ads-native-visibility.mjs @@ -0,0 +1,141 @@ +import assert from 'node:assert/strict' +import { spawn } from 'node:child_process' +import fs from 'node:fs' +import path from 'node:path' +import process from 'node:process' +import { setTimeout as delay } from 'node:timers/promises' +import { expect } from '@playwright/test' +import { nativeChrome } from '../../test/helpers/native-chrome.js' +import { hash } from './releases.mjs' + +async function main() { + const output = fs.mkdtempSync(path.resolve('refactor/.cache/ads-native-visibility-')) + const artifact = path.resolve(process.env.ARTPLAYER_ADS_ARTIFACT || 'packages/artplayer-plugin-ads/dist/artplayer-plugin-ads.js') + const code = fs.readFileSync(artifact, 'utf8') + const port = Number(process.env.ARTPLAYER_BROWSER_PORT || 8084) + const baseURL = `http://127.0.0.1:${port}` + const server = spawn(process.execPath, ['test/browser/server.mjs'], { windowsHide: true, stdio: ['ignore', 'pipe', 'pipe'], env: process.env }) + let serverLog = '' + server.stderr.on('data', (bytes) => { + serverLog += bytes + }) + let native + let page + const report = { task: 'PKG-ADS-05', artifact, sha256: hash(code), noDefaults: true, scope: 'Native selected/background tabs in isolated installed Chrome. Desktop window occlusion excluded by --disable-backgrounding-occluded-windows; no focus or visibility property emulation. Does not prove minimized-window or mobile lifecycle behavior.', cases: [], passed: false } + try { + await new Promise((resolve, reject) => { + const timeout = setTimeout(() => reject(new Error('Owned browser server startup timed out')), 30000) + server.on('error', (error) => { + clearTimeout(timeout) + reject(error) + }) + server.on('exit', (code) => { + clearTimeout(timeout) + reject(new Error(`Owned browser server exited: ${code}; ${serverLog}`)) + }) + server.stdout.on('data', (bytes) => { + serverLog += bytes + if (serverLog.includes('Browser test server:')) { + clearTimeout(timeout) + resolve() + } + }) + }) + report.manifest = await (await fetch(`${baseURL}/test/manifest.json`)).json() + native = await nativeChrome() + report.browser = native.browser.version() + report.executable = native.executable + page = native.context.pages()[0] + const errors = [] + page.on('pageerror', error => errors.push(error.message)) + await native.context.tracing.start({ screenshots: true, snapshots: true, sources: true }) + for (const core of ['published-4.5.5', 'published', 'candidate']) { + for (const media of ['html', 'video']) { + report.currentCase = { core, media } + await page.goto(`${baseURL}/test/player.html?core=${core}`) + // A stock browser may open this owned tab behind a startup tab/window. + await page.bringToFront() + await expect.poll(() => page.evaluate(() => document.hidden)).toBe(false) + await page.addScriptTag({ content: code }) + await page.evaluate((media) => { + window.visibilityEvents = [] + document.addEventListener('visibilitychange', event => window.visibilityEvents.push({ hidden: document.hidden, trusted: event.isTrusted })) + window.adsSkips = 0 + window.art = new window.Artplayer({ container: '.player', url: '/test/pattern.mp4', muted: true, plugins: [window.artplayerPluginAds({ html: 'Native visibility ad', video: media === 'video' ? '/test/pattern.mp4' : '', muted: true, totalDuration: 30, playDuration: 0 })] }) + window.art.on('artplayerPluginAds:skip', () => window.adsSkips++) + document.querySelector('#play').onclick = () => window.art.play() + }, media) + await expect.poll(() => page.evaluate(() => window.art.isReady)).toBe(true) + await page.locator('#play').click() + const countdown = page.locator('.artplayer-plugin-ads-countdown') + await expect(countdown).toBeVisible() + if (media === 'video') + await expect.poll(() => page.locator('.artplayer-plugin-ads-video').evaluate(video => video.currentTime)).toBeGreaterThan(0.2) + const foreground = await native.context.newPage() + await foreground.goto('data:text/html,Ads visibility foregroundForeground test tab') + await foreground.bringToFront() + await expect.poll(() => page.evaluate(() => document.hidden)).toBe(true) + const hiddenText = await countdown.textContent() + await delay(2200) + assert.equal(await countdown.textContent(), hiddenText, 'Countdown advanced in a genuinely hidden page') + const hiddenVideo = media === 'video' ? await page.locator('.artplayer-plugin-ads-video').evaluate(video => ({ time: video.currentTime, paused: video.paused })) : null + await page.bringToFront() + await expect.poll(() => page.evaluate(() => document.hidden)).toBe(false) + await expect.poll(() => countdown.textContent()).not.toBe(hiddenText) + let restoredVideo = null + if (media === 'video') { + await expect.poll(() => page.locator('.artplayer-plugin-ads-video').evaluate(video => video.currentTime)).toBeGreaterThan(hiddenVideo.time + 0.2) + restoredVideo = await page.locator('.artplayer-plugin-ads-video').evaluate(video => ({ time: video.currentTime, paused: video.paused })) + assert.equal(restoredVideo.paused, false, 'Ad video remained paused after the tab returned') + } + const events = await page.evaluate(() => window.visibilityEvents) + assert(events.some(event => event.hidden && event.trusted)) + assert(events.some(event => !event.hidden && event.trusted)) + await foreground.bringToFront() + await expect.poll(() => page.evaluate(() => document.hidden)).toBe(true) + await page.evaluate(() => window.art.destroy(false)) + await page.bringToFront() + await delay(1200) + assert.equal(await page.locator('.artplayer-plugin-ads').count(), 0) + assert.equal(await page.evaluate(() => window.adsSkips), 0) + report.cases.push({ core, media, hiddenText, events, hiddenVideo, restoredVideo, destroyed: true }) + await foreground.close() + } + } + assert.deepEqual(errors, []) + assert.equal(report.cases.length, 6) + report.passed = true + } + catch (error) { + report.error = error.stack + if (page) { + report.failureState = await page.evaluate(() => { + const video = window.art?.template.$video + return { + hidden: document.hidden, + focused: document.hasFocus(), + ready: window.art?.isReady, + visibility: window.visibilityEvents, + video: video && { source: video.currentSrc, src: video.src, ready: video.readyState, network: video.networkState, preload: video.preload, error: video.error?.message, paused: video.paused }, + } + }).catch(() => null) + await page.screenshot({ path: path.join(output, 'failure.png') }).catch(() => {}) + } + throw error + } + finally { + if (native) { + await native.context.tracing.stop({ path: path.join(output, 'trace.zip') }).catch(() => {}) + await native.close() + } + server.kill() + fs.writeFileSync(path.join(output, 'server.log'), serverLog) + fs.writeFileSync(path.join(output, 'report.json'), `${JSON.stringify(report, null, 2)}\n`) + console.log(`Ads native visibility evidence: ${output}; passed=${report.passed}, cases=${report.cases.length}`) + } +} + +main().catch((error) => { + console.error(error) + process.exitCode = 1 +}) diff --git a/refactor/tasks.json b/refactor/tasks.json index 18c9ef21c..ca06fee9c 100644 --- a/refactor/tasks.json +++ b/refactor/tasks.json @@ -2610,11 +2610,14 @@ "PKG-ADS-04", "CORE-22" ], - "status": "todo", + "status": "doing", "risk": "H", "deliverable": "本地广告 fixture、多实例、正常结束/跳过/销毁;核验历史关联核心 4.5.5、发布 5.4.1 与候选核心的实际归档/能力/媒体组合", "acceptance": "最终核心与原支持范围核心分别通过;设备/SDK 缺证据不能标完成", - "evidence": [] + "evidence": [ + "changes/2026-09-12-PKG-ADS-05-browser.md", + "baselines/ads-ui-visibility-validation.json" + ] }, { "id": "PKG-ADS-06", diff --git a/test/ads-lifecycle.test.js b/test/ads-lifecycle.test.js index bc3277ee3..15694c14b 100644 --- a/test/ads-lifecycle.test.js +++ b/test/ads-lifecycle.test.js @@ -10,6 +10,17 @@ const skips = host => host.calls.filter(call => call[0] === 'emit' && call[1] == const contentPlays = host => host.calls.filter(call => call[0] === 'content.play') for (const { name, factory } of implementations) { + test(`${name}: destroy before initialization preserves caller-owned undefined template property`, (t) => { + const env = adsEnvironment(t) + const host = env.host() + Object.defineProperty(host.art.template, '$ads', { configurable: true, enumerable: true, writable: true, value: undefined }) + const before = Object.getOwnPropertyDescriptor(host.art.template, '$ads') + factory({ html: 'ad' })(host.art) + host.art.destroy() + assert.deepEqual(Object.getOwnPropertyDescriptor(host.art.template, '$ads'), before) + assert.equal(env.timers.size, 0) + }) + test(`${name}: repeated play owns one timer and pause clears all countdown work`, (t) => { const env = adsEnvironment(t) const host = env.host() diff --git a/test/browser/ads-ui.spec.js b/test/browser/ads-ui.spec.js new file mode 100644 index 000000000..8466e38c2 --- /dev/null +++ b/test/browser/ads-ui.spec.js @@ -0,0 +1,136 @@ +import fs from 'node:fs' +import process from 'node:process' +import { hash } from '../../refactor/scripts/releases.mjs' +import { compilePackage } from '../helpers/load.js' +import { expect, test } from './fixtures.js' + +let candidate +test.beforeAll(async () => { + candidate = process.env.ARTPLAYER_ADS_ARTIFACT + ? fs.readFileSync(process.env.ARTPLAYER_ADS_ARTIFACT, 'utf8') + : await compilePackage('artplayer-plugin-ads', 'umd') +}) + +async function setup(page, core, testInfo, option = {}) { + await page.goto(`/test/player.html?core=${core}`) + await page.addScriptTag({ content: candidate }) + await testInfo.attach('ads-ui-inputs', { contentType: 'application/json', body: JSON.stringify({ core, artifact: process.env.ARTPLAYER_ADS_ARTIFACT || 'workspace source build', sha256: hash(candidate) }) }) + await page.evaluate((option) => { + window.adsEvents = [] + window.art = new window.Artplayer({ container: '.player', url: '/test/pattern.mp4', muted: true, mutex: false, plugins: [window.artplayerPluginAds({ html: 'ad', totalDuration: 60, playDuration: 0, ...option })] }) + window.art.on('artplayerPluginAds:skip', () => window.adsEvents.push('skip')) + document.querySelector('#play').onclick = () => window.art.play() + }, option) + await expect.poll(() => page.evaluate(() => window.art.isReady)).toBe(true) +} + +test.afterEach(async ({ page }) => { + await page.evaluate(() => { + for (const art of [window.art, window.otherArt]) { + if (art && !art.isDestroy) + art.destroy() + } + }) +}) + +async function fullscreenState(page, active) { + await expect.poll(() => page.evaluate(() => Boolean(document.fullscreenElement || document.webkitFullscreenElement))).toBe(active) + await expect.poll(() => page.evaluate(() => window.art.fullscreen)).toBe(active) + const icons = page.locator('.artplayer-plugin-ads-fullscreen > *') + await expect(icons).toHaveCount(2) + await expect(icons.nth(active ? 1 : 0)).toBeVisible() + await expect(icons.nth(active ? 0 : 1)).toBeHidden() +} + +for (const core of ['published-4.5.5', 'published', 'candidate']) { + test(`${core}: Ads initializes inside native fullscreen and follows external exit and its own button`, async ({ page }, testInfo) => { + await setup(page, core, testInfo) + const native = await page.evaluate(() => Boolean(document.fullscreenEnabled || document.webkitFullscreenEnabled)) + await testInfo.attach('ads-native-capability', { contentType: 'application/json', body: JSON.stringify({ native }) }) + expect(native, 'Native desktop fullscreen must be exercised, not simulated').toBe(true) + await page.evaluate(() => { + document.querySelector('#play').onclick = () => { + window.art.fullscreen = true + } + }) + await page.locator('#play').click() + await expect.poll(() => page.evaluate(() => (document.fullscreenElement || document.webkitFullscreenElement) === window.art.template.$player)).toBe(true) + await page.evaluate(() => window.art.play()) + await fullscreenState(page, true) + await expect(page.locator('.artplayer-plugin-ads')).toBeVisible() + expect(await page.evaluate(() => (document.fullscreenElement || document.webkitFullscreenElement).contains(window.art.template.$ads))).toBe(true) + await page.evaluate(() => { + window.art.fullscreen = false + }) + await fullscreenState(page, false) + await page.locator('.artplayer-plugin-ads-fullscreen').click() + await fullscreenState(page, true) + await page.locator('.artplayer-plugin-ads-fullscreen').click() + await fullscreenState(page, false) + await testInfo.attach('ads-fullscreen-return', { contentType: 'image/png', body: await page.screenshot() }) + await page.locator('.artplayer-plugin-ads-close').click() + await expect(page.locator('.artplayer-plugin-ads')).toBeHidden() + await expect.poll(() => page.evaluate(() => window.art.currentTime)).toBeGreaterThan(0.2) + expect(await page.evaluate(() => window.adsEvents)).toEqual(['skip']) + }) + + test(`${core}: two Ads players own independent decoded videos, mute state and destruction`, async ({ page }, testInfo) => { + await setup(page, core, testInfo, { video: '/test/pattern.mp4', muted: true }) + await page.evaluate(() => { + const container = document.createElement('div') + container.className = 'other-player' + container.style.cssText = 'width:320px;height:180px' + document.body.append(container) + window.otherArt = new window.Artplayer({ container, url: '/test/pattern.mp4', muted: true, mutex: false, plugins: [window.artplayerPluginAds({ video: '/test/pattern.mp4', muted: true, totalDuration: 60, playDuration: 0 })] }) + document.querySelector('#play').onclick = () => Promise.all([window.art.play(), window.otherArt.play()]) + }) + await expect.poll(() => page.evaluate(() => window.otherArt.isReady)).toBe(true) + await page.locator('#play').click() + for (const selector of ['.player', '.other-player']) { + await expect.poll(() => page.locator(`${selector} .artplayer-plugin-ads-video`).evaluate(video => video.currentTime)).toBeGreaterThan(0.2) + expect(await page.locator(`${selector} .artplayer-plugin-ads-video`).evaluate(video => video.readyState >= 2 && video.videoHeight > 0)).toBe(true) + } + await page.locator('.player .artplayer-plugin-ads-muted').click() + expect(await page.locator('.player .artplayer-plugin-ads-video').evaluate(video => video.muted)).toBe(false) + expect(await page.locator('.other-player .artplayer-plugin-ads-video').evaluate(video => video.muted)).toBe(true) + const otherTime = await page.locator('.other-player .artplayer-plugin-ads-video').evaluate(video => video.currentTime) + await page.evaluate(() => { + window.retainedAd = window.art.template.$ads.querySelector('video') + window.art.destroy(false) + }) + await expect(page.locator('.player .artplayer-plugin-ads')).toHaveCount(0) + expect(await page.evaluate(() => ({ src: window.retainedAd.getAttribute('src'), paused: window.retainedAd.paused }))).toEqual({ src: null, paused: true }) + await expect.poll(() => page.locator('.other-player .artplayer-plugin-ads-video').evaluate(video => video.currentTime)).toBeGreaterThan(otherTime + 0.2) + await page.locator('.other-player .artplayer-plugin-ads-close').click() + await expect.poll(() => page.evaluate(() => window.otherArt.currentTime)).toBeGreaterThan(0.2) + expect(await page.evaluate(() => window.adsEvents)).toEqual([]) + }) + + test(`${core}: Ads details use real popup navigation and translated controls stay within a narrow player`, async ({ page }, testInfo) => { + await page.setViewportSize({ width: 390, height: 844 }) + await setup(page, core, testInfo, { url: '/test/player.html?ad-details=true', i18n: { close: 'Close ad', countdown: '%s seconds', detail: 'Details', canBeClosed: 'Wait %s seconds' } }) + await page.locator('.player').evaluate((element) => { + element.style.width = '360px' + }) + await page.evaluate(() => window.art.on('artplayerPluginAds:click', value => window.adsEvents.push(value.url))) + await page.locator('#play').click() + await expect(page.locator('.artplayer-plugin-ads-close')).toHaveText('Close ad') + await expect(page.locator('.artplayer-plugin-ads-detail')).toHaveText('Details') + const popupPromise = page.waitForEvent('popup') + await page.locator('.artplayer-plugin-ads-detail').click() + const popup = await popupPromise + await popup.waitForLoadState('domcontentloaded') + expect(new URL(popup.url()).searchParams.get('ad-details')).toBe('true') + await popup.close() + expect(await page.evaluate(() => window.adsEvents)).toEqual(['/test/player.html?ad-details=true']) + const bounds = await page.locator('.artplayer-plugin-ads').evaluate((root) => { + const parent = root.getBoundingClientRect() + return [...root.querySelectorAll('.artplayer-plugin-ads-timer, .artplayer-plugin-ads-control')].every((element) => { + const box = element.getBoundingClientRect() + return box.left >= parent.left && box.right <= parent.right && box.top >= parent.top && box.bottom <= parent.bottom + }) + }) + expect(bounds).toBe(true) + await testInfo.attach('ads-narrow-layout', { contentType: 'image/png', body: await page.screenshot() }) + }) +} diff --git a/test/helpers/native-chrome.js b/test/helpers/native-chrome.js new file mode 100644 index 000000000..7f62e4884 --- /dev/null +++ b/test/helpers/native-chrome.js @@ -0,0 +1,66 @@ +import assert from 'node:assert/strict' +import { spawn } from 'node:child_process' +import fs from 'node:fs' +import path from 'node:path' +import process from 'node:process' +import { setTimeout as delay } from 'node:timers/promises' +import { chromium } from '@playwright/test' + +// Use only an owned fresh profile. Playwright's normal launch forces document visibility. +export async function nativeChrome() { + const executable = process.env.ARTPLAYER_NATIVE_CHROME || 'C:/Program Files/Google/Chrome/Application/chrome.exe' + assert(fs.existsSync(executable), 'Set ARTPLAYER_NATIVE_CHROME to an installed Chrome executable') + const cache = fs.realpathSync('refactor/.cache') + const profile = fs.mkdtempSync(path.join(cache, 'native-chrome-')) + // Isolate native tab visibility from unrelated desktop windows covering this test window. + const child = spawn(executable, ['--remote-debugging-port=0', `--user-data-dir=${profile}`, '--no-first-run', '--no-default-browser-check', '--disable-backgrounding-occluded-windows', 'about:blank'], { windowsHide: true, stdio: 'ignore' }) + let launchError + child.on('error', (error) => { + launchError = error + }) + let browser + async function close() { + if (browser?.isConnected()) { + const session = await browser.newBrowserCDPSession() + await session.send('Browser.close').catch(() => {}) + await browser.close() + } + if (child.exitCode === null && !child.killed) + child.kill() + assert.equal(path.dirname(fs.realpathSync(profile)), cache) + assert(path.basename(profile).startsWith('native-chrome-')) + // Chrome may still be releasing files; keep the isolated profile on cleanup failure. + try { + fs.rmSync(profile, { recursive: true, force: true, maxRetries: 10, retryDelay: 100 }) + } + catch (error) { console.warn(`Isolated Chrome profile retained: ${profile}`, error.message) } + } + try { + const activePort = path.join(profile, 'DevToolsActivePort') + let port + for (let attempt = 0; !port && attempt < 100; attempt++) { + if (launchError) + throw launchError + assert.equal(child.exitCode, null, 'Isolated Chrome exited before exposing its endpoint') + try { + const content = fs.readFileSync(activePort, 'utf8') + const match = /^(\d+)\r?\n\/devtools\/browser\/[^\r\n]+/.exec(content) + if (match && Number(match[1]) > 0 && Number(match[1]) < 65536) + port = match[1] + } + catch (error) { + if (!['ENOENT', 'EBUSY'].includes(error.code)) + throw error + } + if (!port) + await delay(100) + } + assert(port, 'Isolated Chrome did not expose a complete endpoint') + browser = await chromium.connectOverCDP(`http://127.0.0.1:${port}`, { noDefaults: true }) + return { browser, context: browser.contexts()[0], executable, close } + } + catch (error) { + await close() + throw error + } +}