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
* 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
* 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) {
report(error);
} finally {
if (template.$ads === root)
if (root && template.$ads === root)
delete template.$ads;
}
}
+2 -1
View File
@@ -63,7 +63,8 @@
"build:types": "node scripts/build-types.mjs --write",
"check:types": "node scripts/build-types.mjs --check",
"test:dash-types-package": "node refactor/scripts/dash-package-types.mjs",
"test:ads-types-package": "node refactor/scripts/ads-package-types.mjs"
"test:ads-types-package": "node refactor/scripts/ads-package-types.mjs",
"test:ads-native-visibility": "node refactor/scripts/ads-native-visibility.mjs"
},
"browserslist": "last 1 Chrome version",
"devDependencies": {
+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
overlay and source/error state available until destroy. Destroy cancels the timer, disposes
all owned subscriptions, pauses/releases the ad src via load, and removes the overlay even
when the core keeps its HTML. Only an owned template.$ads value is removed. Foreign DOM
when the core keeps its HTML. Only an actually created, owned template.$ads value is removed;
destroy before initialization preserves a caller's own undefined-valued property. Foreign DOM
listeners and caller-owned replacement template references are not removed.
Native DOM listeners are locally owned; the plugin does not mutate the core event registry
@@ -109,5 +110,15 @@ reconciliation is recorded in PKG-ADS-04; complete historical distribution and d
remain PKG-ADS-06. `yarn test:ads-types-package` packs and installs outside the workspace,
checks all installed bytes, frozen offline install, five compiler modes and Node exports.
`yarn test:browser test/browser/ads-editor-types.spec.js` verifies actual Monaco integration.
Real hidden-page/device behavior and complete media-resource
`yarn test:browser test/browser/ads-ui.spec.js` covers native desktop fullscreen, two real
ad videos, independent mute/destroy and popup/narrow-layout behavior with three core versions.
`yarn test:ads-native-visibility` launches an owned fresh installed Chrome profile and server
to verify background-tab countdown pause, foreground resume and hidden-page destruction.
It uses CDP noDefaults to avoid Playwright focus emulation; OS-window occlusion is excluded
by one browser flag. It does not test minimizing or mobile background lifecycle. Override
ARTPLAYER_NATIVE_CHROME for another installed Chrome path; set ARTPLAYER_BROWSER_ARTIFACTS
and ARTPLAYER_ADS_ARTIFACT to verify a specific core/plugin build. The script preserves a
report and trace under refactor/.cache, never uses the user's daily browser profile, and
closes its owned browser and server. Run it sequentially with other port-8084 browser suites.
Additional minimized-window/mobile lifecycle behavior and complete media-resource
acceptance remain PKG-ADS-05. No claim of compatibility with every historical 4.x core is made.
+1 -1
View File
@@ -4,4 +4,4 @@
* (c) 2017-2026 Harvey Zhao
* Released under the MIT License.
*/
!function(e,n){"object"===typeof exports&&"undefined"!==typeof module?module.exports=n():"function"==typeof define&&define.amd?(e.artplayerPluginAds=n(),define(function(){return e.artplayerPluginAds})):(e="undefined"!==typeof globalThis?globalThis:e||self).artplayerPluginAds=n()}(this,function(){"use strict";function e(e){console.warn("Artplayer Ads:",e)}function n(n){const t=[];let i=!1;function l(e,n){if(!i){t.push(n);try{e()}finally{i&&n()}}}return{on(e,t){const a=()=>{i||t()};l(()=>{n.on(e,a)},()=>{n.off(e,a)})},dom(e,n,t){const a=()=>{i||t()};l(()=>e.addEventListener(n,a),()=>e.removeEventListener(n,a))},dispose(){if(!i){i=!0;for(const i of t.splice(0).reverse())try{i()}catch(n){e(n)}}}}}const t=(e,n)=>n.replace("%s",String(e));function i(i,l,a,o){const r=n(i),s=n(i),d=i.template;let u,p,c="waiting",y=!1,f=!1,g=!1;const v=function(e,n,t){let i,l=0,a=!1,o=!1,r=!1;function s(){a&&o&&!r&&void 0===i&&(i=setTimeout(()=>{i=void 0,!r&&o&&(l+=1,n(l),r||(l>=e()?t():s()))},1e3))}function d(){o=!1,void 0!==i&&clearTimeout(i),i=void 0}return{start(){a||r||(a=!0,o=!0,s())},play(){a&&!r&&(o=!0,s())},pause:d,stop(){r=!0,d()}}}(()=>l.totalDuration,e=>p?.render(e),w),m=()=>"active"===c&&!i.isDestroy,h=()=>"destroyed"===c||i.isDestroy;function x(n){try{n?.pause()}catch(t){e(t)}}function b(n,t,i){try{Promise.resolve(n()).then(()=>{m()||i?.()},t).catch(e)}catch(l){t(l)}}function w(){if("ended"!==c&&"destroyed"!==c&&!i.isDestroy){if(c="ended",v.stop(),r.dispose(),y){if(b(()=>i.play(),n=>{"destroyed"!==c&&e(n)}),h())return;if(x(p?.video),h())return;p?.hide()}h()||i.emit("artplayerPluginAds:skip",l)}}function D(){const n=p?.video;if(x(n),n)try{n.removeAttribute("src"),n.load()}catch(t){e(t)}try{u?.remove()}catch(t){e(t)}finally{d.$ads===u&&delete d.$ads}}function k(){"destroyed"!==c&&(c="destroyed",v.stop(),r.dispose(),s.dispose(),D())}function O(){if("waiting"===c&&!i.isDestroy){c="active",y=!0;try{if(p=function(e,n,i,l,a){const{append:o,query:r,setStyle:s}=l,d=o(e,'<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
* 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) {
report(error);
} finally {
if (template.$ads === root)
if (root && template.$ads === root)
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) }
finally {
if (template.$ads === root)
if (root && template.$ads === root)
delete template.$ads
}
}
+17
View File
@@ -50,6 +50,23 @@ accepted-with-scope;保留诊断并编译迁移示例,不把批准范围扩
`test/ads.test.js` 增加候选 `.default === factory` 及同步取消断言,覆盖三种产物。
这些检查不替代全部历史子路径、媒体/设备与发布验收。
## 05 真实 UI 与标签可见性
ads-ui.spec.js 以三个实际核心和三个浏览器引擎覆盖原生全屏、双实例媒体/静音/销毁
隔离、实际详情导航及390px视口布局。不把视口模拟称为真机。main/legacy各27项。
`yarn test:ads-native-visibility` 使用本机Chrome的独立profile和默认context,通过
connectOverCDP(noDefaults:true)避免框架强制可见。保留真实页面visibilitychange及
isTrusted,只禁用桌面窗口遮挡背景化以排除其他应用干扰。测试开始先切换到本页并
确认可见,再等待元数据;不放宽超时。三个核心分别验证HTML/视频广告后台倒计时
停止、返回恢复、再次后台销毁后不触发skip。视频后台是否暂停作为浏览器状态记录,
不假定不同浏览器策略相同。main/legacy、失败尝试和trace分别保存,见05变更记录。
脚本独占8084,报告使用独立目录;关闭自建浏览器和服务后才运行下一浏览器套件。
DevToolsActivePort刚创建时可能EBUSY/未写完,启动器仅对这两个已确认瞬态条件做
有界重读,不重启浏览器,也不读取用户profile。缺安装路径直接失败,支持显式
ARTPLAYER_NATIVE_CHROME。实际设备、最小化、全量分发仍是后续门槛。
## 边界与文件职责
- `test/helpers/ads.js`:来源加载、冻结 bundle 的 VM、可控时钟和最小 ArtPlayer 宿主。
@@ -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)。
| 检查 ID | 内容及执行方法 | 当前证据/状态 | 责任与发布影响 |
@@ -33,7 +38,7 @@ Node10 是 TS 模块解析模式名,不代表支持 Node.js 10。现代 es2020
| 包 / 工作区版本 | 能力与固定样本/资源要求 | 当前状态、待补环境 | 负责验证任务 |
| --- | --- | --- | --- |
| artplayer / 5.4.1 | 原生媒体、字幕、切源、控件/设置、全屏/PiP、SSR;ENV-MEDIA + VTT/SRT | 发布 5.4.0 已冻结;桌面部分基线,移动/原生全屏/PiP/多引擎待测 | CORE-07/16/17/18/22/23、ENG-05 |
| artplayer-plugin-ads / npm 1.0.6、工作区 2.1.0 未发布 | 五个 1.0.x 归档及实际核心 4.5.5 归档已冻结;HTML/视频、本片恢复、跳过、错误/销毁;本地短广告和主片 | 4.5.5/5.4.1/候选核心 × 发布/候选插件实际组合见 ads-validation.md;设备/隐藏页/全屏与完整分发仍待,不扩大到全部历史核心。无需借 IMA 结果替代自有逻辑 | PKG-ADS-01/02/03/05/06 |
| artplayer-plugin-ads / npm 1.0.6、工作区 2.1.0 未发布 | 五个 1.0.x 归档及实际核心 4.5.5 归档已冻结;HTML/视频、本片恢复、跳过、错误/销毁;本地短广告和主片 | 4.5.5/5.4.1/候选核心组合、三引擎原生全屏/双实例/UI的main和legacy各27项;本机Chrome152真实标签暂停/视频恢复/后台销毁各6项见ads-validation.md。实际设备/最小化及完整分发仍待,不扩大到全部历史核心 | PKG-ADS-01/02/03/05/06 |
| artplayer-plugin-ambilight / 1.1.0 | Canvas 采样、CORS、resize、后台/销毁;同源及受控跨源 MP4 | 待真实画布和资源测量 | PKG-AMBILIGHT-01/02/05/06 |
| artplayer-plugin-asr / 2.1.0 | PCM 音频输出/暂停/销毁;AudioContext + 调用方服务 | 待音频环境与调用方 WebSocket 验证;示例服务版本未知,不在 PR 调线上服务 | PKG-ASR-01/02/05/06、EX-03 |
| artplayer-plugin-audio-track / 1.1.0 | 独立外部 Audio 与主视频时钟同步;本地 MP4+AAC,偏移/切源/缓冲/失败样本 | 1.1.0 发布及调用契约见 [包基线](baselines/audio-track-contract.md);旧核心 5.4.0 为具体对照点,桌面/Safari 实测待完成 | PKG-AUDIO-01/02/05/06 |
+3 -2
View File
@@ -4,7 +4,7 @@
基线:`40fcda6a37d0049d42e49c1e64e70d4fd9ba5f7f`。总任务 217 项,范围 22 个包及工作区/示例。
状态:todo 135 / doing 4 / blocked 0 / done 78 / deferred 0。风险 L/M/H 表示兼容风险,不表示工期。
状态:todo 134 / doing 5 / blocked 0 / done 78 / deferred 0。风险 L/M/H 表示兼容风险,不表示工期。
前置依赖是启动条件;验收是完成条件。任务可以继续拆分,但不能复用或悄悄删除旧 ID。
@@ -260,7 +260,7 @@
| PKG-ADS-02 | artplayer-plugin-ads<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-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 |
## 5 包迁移:artplayer-plugin-vast
@@ -488,3 +488,4 @@
- PKG-ADS-02: [记录](changes/2026-09-12-PKG-ADS-02-tests.md) [记录](ads-validation.md) [记录](baselines/ads-validation.json)
- PKG-ADS-03: [记录](changes/2026-09-12-PKG-ADS-03-lifecycle.md) [记录](ads-validation.md) [记录](baselines/ads-lifecycle-validation.json)
- PKG-ADS-04: [记录](changes/2026-09-12-PKG-ADS-04-types.md) [记录](baselines/ads-types-validation.json)
- PKG-ADS-05: [记录](changes/2026-09-12-PKG-ADS-05-browser.md) [记录](baselines/ads-ui-visibility-validation.json)
+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 类型与导入兼容
公开声明补齐 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 |
| ADS-TYPE-01 | accepted-with-scope / 已复现 | Ads 声明丢失真实 html/video/url,旧 totalDuration 为 string,当前 source/type 未实现 | PKG-ADS-02, PKG-ADS-04 |
| ADS-DIST-01 | open / 已复现 | Ads npm 1.0.6 default namespace 与未发布 2.1.0 callable/exports 分发不同 | PKG-ADS-04, PKG-ADS-06, REL-01 |
| ADS-CORE-01 | open / 源码/产物事实 | Ads 旧发布关联核心 4.5.5,当前源码新增 >=5 门槛和 document 事件桥依赖 | PKG-ADS-02, PKG-ADS-03, PKG-ADS-05 |
| ADS-LIFE-01 | open / 已复现 | Ads 计时链、重复/过早调用、工厂复用与销毁缺乏资源边界 | PKG-ADS-02, PKG-ADS-03, PKG-ADS-05 |
| ADS-CORE-01 | resolved / 源码/产物事实 | Ads 旧发布关联核心 4.5.5,当前源码新增 >=5 门槛和 document 事件桥依赖 | PKG-ADS-02, PKG-ADS-03, PKG-ADS-05 |
| ADS-LIFE-01 | resolved / 已复现 | Ads 计时链、重复/过早调用、工厂复用与销毁缺乏资源边界 | PKG-ADS-02, PKG-ADS-03, PKG-ADS-05 |
| ADS-MEDIA-01 | open / 已复现 | Ads metadata 与 play 拒绝、隐藏页媒体状态和内容恢复仍缺真实验证 | PKG-ADS-02, PKG-ADS-03, PKG-ADS-05 |
| ADS-UI-01 | open / 已复现 | Ads 零等待跳过与初始状态不符,全屏图标/外部状态需核验 | PKG-ADS-02, PKG-ADS-03, PKG-ADS-05 |
| ADS-UI-01 | resolved / 已复现 | Ads 零等待跳过与初始状态不符,全屏图标/外部状态需核验 | PKG-ADS-02, PKG-ADS-03, PKG-ADS-05 |
+46 -11
View File
@@ -3034,7 +3034,7 @@
"id": "ADS-CORE-01",
"title": "Ads 旧发布关联核心 4.5.5,当前源码新增 >=5 门槛和 document 事件桥依赖",
"confirmation": "source-observed",
"status": "open",
"status": "resolved",
"owners": [
"PKG-ADS-02",
"PKG-ADS-03",
@@ -3046,16 +3046,27 @@
"packages/artplayer-plugin-ads/src/index.ts",
"refactor/baselines/ads-lifecycle-validation.json",
"refactor/changes/2026-09-12-PKG-ADS-03-lifecycle.md",
"packages/artplayer-plugin-ads/ARCHITECTURE.md"
"packages/artplayer-plugin-ads/ARCHITECTURE.md",
"refactor/baselines/ads-ui-visibility-validation.json",
"refactor/changes/2026-09-12-PKG-ADS-05-browser.md"
],
"compatibleResolution": "先核验实际核心归档和能力,通过本地 fallback 保留原用户调用;不能用未发布源码门槛强制全部旧消费者升级,也不泛称全部 4.x 通过。",
"closureCriteria": "4.5.5 关联来源和实际可用组合明确,5.4.1/候选核心与原生 visibility/事件桥路径有真实浏览器证据。"
"closureCriteria": "4.5.5 关联来源和实际可用组合明确,5.4.1/候选核心与原生 visibility/事件桥路径有真实浏览器证据。",
"resolutionEvidence": [
"refactor/baselines/ads-ui-visibility-validation.json",
"refactor/changes/2026-09-12-PKG-ADS-05-browser.md",
"refactor/baselines/ads-lifecycle-validation.json",
"test/browser/ads-ui.spec.js",
"refactor/scripts/ads-native-visibility.mjs",
"test/ads-lifecycle.test.js"
],
"resolutionRationale": "实际4.5.5归档已冻结;候选入口按能力检查,没有>=5门槛。4.5.5/5.4.1/候选核心main/legacy的三引擎UI及原生Chrome标签可见性通过,覆盖原生监听和较新核心事件桥,不泛化为全部4.x。"
},
{
"id": "ADS-LIFE-01",
"title": "Ads 计时链、重复/过早调用、工厂复用与销毁缺乏资源边界",
"confirmation": "reproduced",
"status": "open",
"status": "resolved",
"owners": [
"PKG-ADS-02",
"PKG-ADS-03",
@@ -3070,10 +3081,21 @@
"refactor/baselines/ads-validation.json",
"refactor/baselines/ads-lifecycle-validation.json",
"refactor/changes/2026-09-12-PKG-ADS-03-lifecycle.md",
"packages/artplayer-plugin-ads/ARCHITECTURE.md"
"packages/artplayer-plugin-ads/ARCHITECTURE.md",
"refactor/baselines/ads-ui-visibility-validation.json",
"refactor/changes/2026-09-12-PKG-ADS-05-browser.md"
],
"compatibleResolution": "先复现重复 play 的计时链、skip 后计时、destroy 后恢复与复用工厂 options;以实例所有权和幂等清理修正,不更改广告时长/跳过规则。",
"closureCriteria": "时钟/订阅/DOM 与主片恢复的成功、异常、重入、销毁、复用和多实例回归通过。"
"closureCriteria": "时钟/订阅/DOM 与主片恢复的成功、异常、重入、销毁、复用和多实例回归通过。",
"resolutionEvidence": [
"refactor/baselines/ads-ui-visibility-validation.json",
"refactor/changes/2026-09-12-PKG-ADS-05-browser.md",
"refactor/baselines/ads-lifecycle-validation.json",
"test/browser/ads-ui.spec.js",
"refactor/scripts/ads-native-visibility.mjs",
"test/ads-lifecycle.test.js"
],
"resolutionRationale": "03的可重跑重复/过早/复用/重入/拒绝修复与本批194项源码及三格式回归通过;真实双实例、后台销毁及自有资源清理通过。新增未创建root时误删调用方undefined属性的复现与修复,不触碰未拥有的引用。"
},
{
"id": "ADS-MEDIA-01",
@@ -3095,16 +3117,18 @@
"refactor/baselines/ads-validation.json",
"refactor/baselines/ads-lifecycle-validation.json",
"refactor/changes/2026-09-12-PKG-ADS-03-lifecycle.md",
"packages/artplayer-plugin-ads/ARCHITECTURE.md"
"packages/artplayer-plugin-ads/ARCHITECTURE.md",
"refactor/baselines/ads-ui-visibility-validation.json",
"refactor/changes/2026-09-12-PKG-ADS-05-browser.md"
],
"compatibleResolution": "用本地媒体验证事件安装顺序、加载失败与拒绝、倒计时和广告播放的独立语义,恢复主片时不吞公共拒绝或在销毁后播放。",
"compatibleResolution": "用本地媒体验证事件安装顺序、加载失败与拒绝、倒计时和广告播放的独立语义,恢复主片时不吞公共拒绝或在销毁后播放。 05已验证桌面Chrome真实后台标签暂停/恢复、视频继续播放和后台销毁,main/legacy各6组合;最小化和实际移动端媒体策略仍缺设备证据,保持open。",
"closureCriteria": "实际广告/主片的加载、暂停/恢复、失败/换源/销毁和隐藏状态有新旧核心组合证据。"
},
{
"id": "ADS-UI-01",
"title": "Ads 零等待跳过与初始状态不符,全屏图标/外部状态需核验",
"confirmation": "reproduced",
"status": "open",
"status": "resolved",
"owners": [
"PKG-ADS-02",
"PKG-ADS-03",
@@ -3120,10 +3144,21 @@
"refactor/baselines/ads-validation.json",
"refactor/baselines/ads-lifecycle-validation.json",
"refactor/changes/2026-09-12-PKG-ADS-03-lifecycle.md",
"packages/artplayer-plugin-ads/ARCHITECTURE.md"
"packages/artplayer-plugin-ads/ARCHITECTURE.md",
"refactor/baselines/ads-ui-visibility-validation.json",
"refactor/changes/2026-09-12-PKG-ADS-05-browser.md"
],
"compatibleResolution": "复现 playDuration<=0 文案可关闭但 isCanClose 尚为 false;按既有 demo 规则修复,验证全屏按钮和外部退出状态,保留 CSS/DOM 钩子。",
"closureCriteria": "零/正/不可跳阈值、真实点击、倒计时/详情/静音/全屏与外部状态组合均有回归。"
"closureCriteria": "零/正/不可跳阈值、真实点击、倒计时/详情/静音/全屏与外部状态组合均有回归。",
"resolutionEvidence": [
"refactor/baselines/ads-ui-visibility-validation.json",
"refactor/changes/2026-09-12-PKG-ADS-05-browser.md",
"refactor/baselines/ads-lifecycle-validation.json",
"test/browser/ads-ui.spec.js",
"refactor/scripts/ads-native-visibility.mjs",
"test/ads-lifecycle.test.js"
],
"resolutionRationale": "零/正/不可关闭阈值及错误输入由现有Node/媒体回归保持;main/legacy分别27项真实原生全屏(初始/外部退出/按钮)、静音、详情popup和窄屏翻译控件通过。截图已查看;手机设备策略仍由05媒体门槛承接。"
}
]
}
+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",
"CORE-22"
],
"status": "todo",
"status": "doing",
"risk": "H",
"deliverable": "本地广告 fixture、多实例、正常结束/跳过/销毁;核验历史关联核心 4.5.5、发布 5.4.1 与候选核心的实际归档/能力/媒体组合",
"acceptance": "最终核心与原支持范围核心分别通过;设备/SDK 缺证据不能标完成",
"evidence": []
"evidence": [
"changes/2026-09-12-PKG-ADS-05-browser.md",
"baselines/ads-ui-visibility-validation.json"
]
},
{
"id": "PKG-ADS-06",
+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')
for (const { name, factory } of implementations) {
test(`${name}: destroy before initialization preserves caller-owned undefined template property`, (t) => {
const env = adsEnvironment(t)
const host = env.host()
Object.defineProperty(host.art.template, '$ads', { configurable: true, enumerable: true, writable: true, value: undefined })
const before = Object.getOwnPropertyDescriptor(host.art.template, '$ads')
factory({ html: 'ad' })(host.art)
host.art.destroy()
assert.deepEqual(Object.getOwnPropertyDescriptor(host.art.template, '$ads'), before)
assert.equal(env.timers.size, 0)
})
test(`${name}: repeated play owns one timer and pause clears all countdown work`, (t) => {
const env = adsEnvironment(t)
const host = env.host()
+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
}
}