fix(ads): [PKG-ADS-05] preserve ownership and verify native browser lifecycle

This commit is contained in:
Harvey Zhao committed 2026-09-12 08:52:51 +08:00
1 parent fb1cfa701d
commit 29692d2815
22 files changed
+1060 -29

No files matched your search

+1 -1
View File
@@ -4,4 +4,4 @@
* (c) 2017-2026 Harvey Zhao * (c) 2017-2026 Harvey Zhao
* Released under the MIT License. * 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,'<div class="artplayer-plugin-ads"></div>');a(d);const u=o(d,i.video?'<video class="artplayer-plugin-ads-video" loop playsInline></video>':`<div class="artplayer-plugin-ads-html">${i.html}</div>`),p=i.video?u:null,c=o(d,'<div class="artplayer-plugin-ads-loading"></div>');o(c,n.loading);const y=o(d,'<div class="artplayer-plugin-ads-timer">\n <div class="artplayer-plugin-ads-close"></div>\n <div class="artplayer-plugin-ads-countdown"></div>\n </div>'),f=r(".artplayer-plugin-ads-close",y),g=r(".artplayer-plugin-ads-countdown",y),v=o(d,`<div class="artplayer-plugin-ads-control">\n <div class="artplayer-plugin-ads-detail">${i.i18n.detail}</div>\n <div class="artplayer-plugin-ads-muted"></div>\n <div class="artplayer-plugin-ads-fullscreen"></div>\n </div>`),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)))throwLine truncated !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,'<div class="artplayer-plugin-ads"></div>');a(d);const u=o(d,i.video?'<video class="artplayer-plugin-ads-video" loop playsInline></video>':`<div class="artplayer-plugin-ads-html">${i.html}</div>`),p=i.video?u:null,c=o(d,'<div class="artplayer-plugin-ads-loading"></div>');o(c,n.loading);const y=o(d,'<div class="artplayer-plugin-ads-timer">\n <div class="artplayer-plugin-ads-close"></div>\n <div class="artplayer-plugin-ads-countdown"></div>\n </div>'),f=r(".artplayer-plugin-ads-close",y),g=r(".artplayer-plugin-ads-countdown",y),v=o(d,`<div class="artplayer-plugin-ads-control">\n <div class="artplayer-plugin-ads-detail">${i.i18n.detail}</div>\n <div class="artplayer-plugin-ads-muted"></div>\n <div class="artplayer-plugin-ads-fullscreen"></div>\n </div>`),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)))thLine truncated
+1 -1
View File
@@ -4,4 +4,4 @@
* (c) 2017-2026 Harvey Zhao * (c) 2017-2026 Harvey Zhao
* Released under the MIT License. * 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,'<div class="artplayer-plugin-ads"></div>');i(d);const u=r(d,t.video?'<video class="artplayer-plugin-ads-video" loop playsInline></video>':`<div class="artplayer-plugin-ads-html">${t.html}</div>`),p=t.video?u:null,c=r(d,'<div class="artplayer-plugin-ads-loading"></div>');r(c,n.loading);const y=r(d,'<div class="artplayer-plugin-ads-timer">\n <div class="artplayer-plugin-ads-close"></div>\n <div class="artplayer-plugin-ads-countdown"></div>\n </div>'),f=o(".artplayer-plugin-ads-close",y),g=o(".artplayer-plugin-ads-countdown",y),v=r(d,`<div class="artplayer-plugin-ads-control">\n <div class="artplayer-plugin-ads-detail">${t.i18n.detail}</div>\n <div class="artplayer-plugin-ads-muted"></div>\n <div class="artplayer-plugin-ads-fullscreen"></div>\n </div>`),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)},fullscreenLine truncated !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,'<div class="artplayer-plugin-ads"></div>');i(d);const u=r(d,t.video?'<video class="artplayer-plugin-ads-video" loop playsInline></video>':`<div class="artplayer-plugin-ads-html">${t.html}</div>`),p=t.video?u:null,c=r(d,'<div class="artplayer-plugin-ads-loading"></div>');r(c,n.loading);const y=r(d,'<div class="artplayer-plugin-ads-timer">\n <div class="artplayer-plugin-ads-close"></div>\n <div class="artplayer-plugin-ads-countdown"></div>\n </div>'),f=o(".artplayer-plugin-ads-close",y),g=o(".artplayer-plugin-ads-countdown",y),v=r(d,`<div class="artplayer-plugin-ads-control">\n <div class="artplayer-plugin-ads-detail">${t.i18n.detail}</div>\n <div class="artplayer-plugin-ads-muted"></div>\n <div class="artplayer-plugin-ads-fullscreen"></div>\n </div>`),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)},fullscrLine truncated
+1 -1
View File
@@ -279,7 +279,7 @@ function createSession(art, option, icons, utils) {
} catch (error) { } catch (error) {
report(error); report(error);
} finally { } finally {
if (template.$ads === root) if (root && template.$ads === root)
delete template.$ads; delete template.$ads;
} }
} }
+2 -1
View File
@@ -63,7 +63,8 @@
"build:types": "node scripts/build-types.mjs --write", "build:types": "node scripts/build-types.mjs --write",
"check:types": "node scripts/build-types.mjs --check", "check:types": "node scripts/build-types.mjs --check",
"test:dash-types-package": "node refactor/scripts/dash-package-types.mjs", "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", "browserslist": "last 1 Chrome version",
"devDependencies": { "devDependencies": {
+13 -2
View File
@@ -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 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 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 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. 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 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, 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. 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. `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. acceptance remain PKG-ADS-05. No claim of compatibility with every historical 4.x core is made.
+1 -1
View File
@@ -4,4 +4,4 @@
* (c) 2017-2026 Harvey Zhao * (c) 2017-2026 Harvey Zhao
* Released under the MIT License. * 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,'<div class="artplayer-plugin-ads"></div>');a(d);const u=o(d,i.video?'<video class="artplayer-plugin-ads-video" loop playsInline></video>':`<div class="artplayer-plugin-ads-html">${i.html}</div>`),p=i.video?u:null,c=o(d,'<div class="artplayer-plugin-ads-loading"></div>');o(c,n.loading);const y=o(d,'<div class="artplayer-plugin-ads-timer">\n <div class="artplayer-plugin-ads-close"></div>\n <div class="artplayer-plugin-ads-countdown"></div>\n </div>'),f=r(".artplayer-plugin-ads-close",y),g=r(".artplayer-plugin-ads-countdown",y),v=o(d,`<div class="artplayer-plugin-ads-control">\n <div class="artplayer-plugin-ads-detail">${i.i18n.detail}</div>\n <div class="artplayer-plugin-ads-muted"></div>\n <div class="artplayer-plugin-ads-fullscreen"></div>\n </div>`),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)))throwLine truncated !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,'<div class="artplayer-plugin-ads"></div>');a(d);const u=o(d,i.video?'<video class="artplayer-plugin-ads-video" loop playsInline></video>':`<div class="artplayer-plugin-ads-html">${i.html}</div>`),p=i.video?u:null,c=o(d,'<div class="artplayer-plugin-ads-loading"></div>');o(c,n.loading);const y=o(d,'<div class="artplayer-plugin-ads-timer">\n <div class="artplayer-plugin-ads-close"></div>\n <div class="artplayer-plugin-ads-countdown"></div>\n </div>'),f=r(".artplayer-plugin-ads-close",y),g=r(".artplayer-plugin-ads-countdown",y),v=o(d,`<div class="artplayer-plugin-ads-control">\n <div class="artplayer-plugin-ads-detail">${i.i18n.detail}</div>\n <div class="artplayer-plugin-ads-muted"></div>\n <div class="artplayer-plugin-ads-fullscreen"></div>\n </div>`),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)))thLine truncated
@@ -4,4 +4,4 @@
* (c) 2017-2026 Harvey Zhao * (c) 2017-2026 Harvey Zhao
* Released under the MIT License. * 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,'<div class="artplayer-plugin-ads"></div>');i(d);const u=r(d,t.video?'<video class="artplayer-plugin-ads-video" loop playsInline></video>':`<div class="artplayer-plugin-ads-html">${t.html}</div>`),p=t.video?u:null,c=r(d,'<div class="artplayer-plugin-ads-loading"></div>');r(c,n.loading);const y=r(d,'<div class="artplayer-plugin-ads-timer">\n <div class="artplayer-plugin-ads-close"></div>\n <div class="artplayer-plugin-ads-countdown"></div>\n </div>'),f=o(".artplayer-plugin-ads-close",y),g=o(".artplayer-plugin-ads-countdown",y),v=r(d,`<div class="artplayer-plugin-ads-control">\n <div class="artplayer-plugin-ads-detail">${t.i18n.detail}</div>\n <div class="artplayer-plugin-ads-muted"></div>\n <div class="artplayer-plugin-ads-fullscreen"></div>\n </div>`),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)},fullscreenLine truncated !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,'<div class="artplayer-plugin-ads"></div>');i(d);const u=r(d,t.video?'<video class="artplayer-plugin-ads-video" loop playsInline></video>':`<div class="artplayer-plugin-ads-html">${t.html}</div>`),p=t.video?u:null,c=r(d,'<div class="artplayer-plugin-ads-loading"></div>');r(c,n.loading);const y=r(d,'<div class="artplayer-plugin-ads-timer">\n <div class="artplayer-plugin-ads-close"></div>\n <div class="artplayer-plugin-ads-countdown"></div>\n </div>'),f=o(".artplayer-plugin-ads-close",y),g=o(".artplayer-plugin-ads-countdown",y),v=r(d,`<div class="artplayer-plugin-ads-control">\n <div class="artplayer-plugin-ads-detail">${t.i18n.detail}</div>\n <div class="artplayer-plugin-ads-muted"></div>\n <div class="artplayer-plugin-ads-fullscreen"></div>\n </div>`),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)},fullscrLine truncated
@@ -279,7 +279,7 @@ function createSession(art, option, icons, utils) {
} catch (error) { } catch (error) {
report(error); report(error);
} finally { } finally {
if (template.$ads === root) if (root && template.$ads === root)
delete template.$ads; delete template.$ads;
} }
} }
+1 -1
View File
@@ -72,7 +72,7 @@ export function createSession(art: Host, option: Options, icons: Icons, utils: U
} }
catch (error) { report(error) } catch (error) { report(error) }
finally { finally {
if (template.$ads === root) if (root && template.$ads === root)
delete template.$ads delete template.$ads
} }
} }
+17
View File
@@ -50,6 +50,23 @@ accepted-with-scope;保留诊断并编译迁移示例,不把批准范围扩
`test/ads.test.js` 增加候选 `.default === factory` 及同步取消断言,覆盖三种产物。 `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 宿主。 - `test/helpers/ads.js`:来源加载、冻结 bundle 的 VM、可控时钟和最小 ArtPlayer 宿主。
@@ -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."
]
}
@@ -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独立本地检查点提交,保留明确未完成项,无推送或发布。
+6 -1
View File
@@ -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)。 工作区版本不是发布证据。当前只冻结 core 5.4.0 和 chapter 1.1.0 的发布 tarball,核心工作区为 5.4.1;其余包的真实发布版本、旧核心支持窗口由各包 01 任务核实。历史最低浏览器/TS 版本尚未证明,不把本机版本设为用户最低版本。全部包仍按自身下一 major 准备,版本目标见 [版本策略](version-policy.md)。
| 检查 ID | 内容及执行方法 | 当前证据/状态 | 责任与发布影响 | | 检查 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 / 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-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-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 | | 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 |
+3 -2
View File
@@ -4,7 +4,7 @@
基线:`40fcda6a37d0049d42e49c1e64e70d4fd9ba5f7f`。总任务 217 项,范围 22 个包及工作区/示例。 基线:`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。 前置依赖是启动条件;验收是完成条件。任务可以继续拆分,但不能复用或悄悄删除旧 ID。
@@ -260,7 +260,7 @@
| PKG-ADS-02 | artplayer-plugin-ads<br>建立特有行为与错误测试 | PKG-ADS-01, ENG-03, ENG-05 | 固定 npm 1.0.6 与工作区契约;HTML/视频/图片 HTML、跳过阈值、计时链、工厂复用、多实例、过早/重复方法、播放拒绝与销毁 | 旧版本行为可重跑,成功/失败/切源/销毁有必要断言 | H | done | | PKG-ADS-02 | artplayer-plugin-ads<br>建立特有行为与错误测试 | PKG-ADS-01, ENG-03, ENG-05 | 固定 npm 1.0.6 与工作区契约;HTML/视频/图片 HTML、跳过阈值、计时链、工厂复用、多实例、过早/重复方法、播放拒绝与销毁 | 旧版本行为可重跑,成功/失败/切源/销毁有必要断言 | H | done |
| PKG-ADS-03 | artplayer-plugin-ads<br>整理内部职责与资源 | PKG-ADS-02, CORE-10, CORE-13 | 配置/广告状态/计时/视图/主视频恢复分离,保留产品规则;资源与版本检查改用经验证的旧核心能力边界 | 结构变化和缺陷修复分开记录;原 API/事件/资源生命周期通过 | H | done | | PKG-ADS-03 | artplayer-plugin-ads<br>整理内部职责与资源 | PKG-ADS-02, CORE-10, CORE-13 | 配置/广告状态/计时/视图/主视频恢复分离,保留产品规则;资源与版本检查改用经验证的旧核心能力边界 | 结构变化和缺陷修复分开记录;原 API/事件/资源生命周期通过 | H | done |
| PKG-ADS-04 | artplayer-plugin-ads<br>迁移自有源码和公开类型 | PKG-ADS-03, ENG-04, ENG-06, CORE-07 | 兼容真实 html/video/url/i18n、旧 export= 与工作区 source/type 类型接受面;时长 string/number 漂移与未实现别名单独处置 | 严格类型检查、旧消费样例通过;声明路径/导出和同步异步兼容 | H | done | | PKG-ADS-04 | artplayer-plugin-ads<br>迁移自有源码和公开类型 | 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<br>验证新旧核心和组合 | PKG-ADS-04, CORE-22 | 本地广告 fixture、多实例、正常结束/跳过/销毁;核验历史关联核心 4.5.5、发布 5.4.1 与候选核心的实际归档/能力/媒体组合 | 最终核心与原支持范围核心分别通过;设备/SDK 缺证据不能标完成 | H | todo | | PKG-ADS-05 | artplayer-plugin-ads<br>验证新旧核心和组合 | PKG-ADS-04, CORE-22 | 本地广告 fixture、多实例、正常结束/跳过/销毁;核验历史关联核心 4.5.5、发布 5.4.1 与候选核心的实际归档/能力/媒体组合 | 最终核心与原支持范围核心分别通过;设备/SDK 缺证据不能标完成 | H | doing |
| PKG-ADS-06 | artplayer-plugin-ads<br>验证分发并同步文档 | PKG-ADS-05, ENG-07 | ads.js/README/声明一致;旧 require(pkg).default、原 dist 路径、当前 callable/ESM/legacy 的隔离 tarball 及浏览器验收 | tarball 入口/资源、类型、8082 demo 和 README 一致,有回退记录 | H | todo | | PKG-ADS-06 | artplayer-plugin-ads<br>验证分发并同步文档 | 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 ## 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-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-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-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)
+11
View File
@@ -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 类型与导入兼容 ## 最新完成:Ads-04 类型与导入兼容
公开声明补齐 html/video/url/i18n,增加 CJS/ESM 类型桥和同一运行时的准确 /runtime 公开声明补齐 html/video/url/i18n,增加 CJS/ESM 类型桥和同一运行时的准确 /runtime
+3 -3
View File
@@ -143,7 +143,7 @@
| DASH-SEEK-01 | open / 已复现 | dash.js 4.5.2 空裁剪漏更新缓冲量,稳定暂停 seek 后调度停滞 | PKG-DASH-05, REVIEW-02 | | 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-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-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-CORE-01 | resolved / 源码/产物事实 | 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-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-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 |
+46 -11
View File
@@ -3034,7 +3034,7 @@
"id": "ADS-CORE-01", "id": "ADS-CORE-01",
"title": "Ads 旧发布关联核心 4.5.5,当前源码新增 >=5 门槛和 document 事件桥依赖", "title": "Ads 旧发布关联核心 4.5.5,当前源码新增 >=5 门槛和 document 事件桥依赖",
"confirmation": "source-observed", "confirmation": "source-observed",
"status": "open", "status": "resolved",
"owners": [ "owners": [
"PKG-ADS-02", "PKG-ADS-02",
"PKG-ADS-03", "PKG-ADS-03",
@@ -3046,16 +3046,27 @@
"packages/artplayer-plugin-ads/src/index.ts", "packages/artplayer-plugin-ads/src/index.ts",
"refactor/baselines/ads-lifecycle-validation.json", "refactor/baselines/ads-lifecycle-validation.json",
"refactor/changes/2026-09-12-PKG-ADS-03-lifecycle.md", "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 通过。", "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", "id": "ADS-LIFE-01",
"title": "Ads 计时链、重复/过早调用、工厂复用与销毁缺乏资源边界", "title": "Ads 计时链、重复/过早调用、工厂复用与销毁缺乏资源边界",
"confirmation": "reproduced", "confirmation": "reproduced",
"status": "open", "status": "resolved",
"owners": [ "owners": [
"PKG-ADS-02", "PKG-ADS-02",
"PKG-ADS-03", "PKG-ADS-03",
@@ -3070,10 +3081,21 @@
"refactor/baselines/ads-validation.json", "refactor/baselines/ads-validation.json",
"refactor/baselines/ads-lifecycle-validation.json", "refactor/baselines/ads-lifecycle-validation.json",
"refactor/changes/2026-09-12-PKG-ADS-03-lifecycle.md", "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;以实例所有权和幂等清理修正,不更改广告时长/跳过规则。", "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", "id": "ADS-MEDIA-01",
@@ -3095,16 +3117,18 @@
"refactor/baselines/ads-validation.json", "refactor/baselines/ads-validation.json",
"refactor/baselines/ads-lifecycle-validation.json", "refactor/baselines/ads-lifecycle-validation.json",
"refactor/changes/2026-09-12-PKG-ADS-03-lifecycle.md", "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": "实际广告/主片的加载、暂停/恢复、失败/换源/销毁和隐藏状态有新旧核心组合证据。" "closureCriteria": "实际广告/主片的加载、暂停/恢复、失败/换源/销毁和隐藏状态有新旧核心组合证据。"
}, },
{ {
"id": "ADS-UI-01", "id": "ADS-UI-01",
"title": "Ads 零等待跳过与初始状态不符,全屏图标/外部状态需核验", "title": "Ads 零等待跳过与初始状态不符,全屏图标/外部状态需核验",
"confirmation": "reproduced", "confirmation": "reproduced",
"status": "open", "status": "resolved",
"owners": [ "owners": [
"PKG-ADS-02", "PKG-ADS-02",
"PKG-ADS-03", "PKG-ADS-03",
@@ -3120,10 +3144,21 @@
"refactor/baselines/ads-validation.json", "refactor/baselines/ads-validation.json",
"refactor/baselines/ads-lifecycle-validation.json", "refactor/baselines/ads-lifecycle-validation.json",
"refactor/changes/2026-09-12-PKG-ADS-03-lifecycle.md", "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 钩子。", "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媒体门槛承接。"
} }
] ]
} }
+141
View File
@@ -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,<title>Ads visibility foreground</title>Foreground 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
})
+5 -2
View File
@@ -2610,11 +2610,14 @@
"PKG-ADS-04", "PKG-ADS-04",
"CORE-22" "CORE-22"
], ],
"status": "todo", "status": "doing",
"risk": "H", "risk": "H",
"deliverable": "本地广告 fixture、多实例、正常结束/跳过/销毁;核验历史关联核心 4.5.5、发布 5.4.1 与候选核心的实际归档/能力/媒体组合", "deliverable": "本地广告 fixture、多实例、正常结束/跳过/销毁;核验历史关联核心 4.5.5、发布 5.4.1 与候选核心的实际归档/能力/媒体组合",
"acceptance": "最终核心与原支持范围核心分别通过;设备/SDK 缺证据不能标完成", "acceptance": "最终核心与原支持范围核心分别通过;设备/SDK 缺证据不能标完成",
"evidence": [] "evidence": [
"changes/2026-09-12-PKG-ADS-05-browser.md",
"baselines/ads-ui-visibility-validation.json"
]
}, },
{ {
"id": "PKG-ADS-06", "id": "PKG-ADS-06",
+11
View File
@@ -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') const contentPlays = host => host.calls.filter(call => call[0] === 'content.play')
for (const { name, factory } of implementations) { 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) => { test(`${name}: repeated play owns one timer and pause clears all countdown work`, (t) => {
const env = adsEnvironment(t) const env = adsEnvironment(t)
const host = env.host() const host = env.host()
+136
View File
@@ -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() })
})
}
+66
View File
@@ -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
}
}