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,''),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,''),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,''),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,''),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,''),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,''),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,''),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,''),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
+ }
+}