refactor(ads): [PKG-ADS-03] split TypeScript lifecycle and preserve legacy behavior

This commit is contained in:
Harvey Zhao committed 2026-09-12 07:55:55 +08:00
1 parent 069ad4aae1
commit 39e2d4cdfb
37 files changed
+3083 -788

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,l){"object"===typeof exports&&"undefined"!==typeof module?module.exports=l():"function"==typeof define&&define.amd?(t.artplayerPluginAds=l(),define(function(){return t.artplayerPluginAds})):(e="undefined"!==typeof globalThis?globalThis:e||self).artplayerPluginAds=l()}(this,function(){"use strict";function e(l){return n=>{!function(l){const{version:n,utils:{errorHandle:i}}=l.constructor,a=n.split(".").map(Number);i(a[0]+a[1]/100>=5,`Artplayer.js@${n} is not compatible the artplayerPluginAds@${e.version}. Please update it to version Artplayer.js@5.x.x`)}(n);const{template:{$player:i},icons:{volume:a,volumeClose:t,fullscreenOn:r,fullscreenOff:o,loading:d},constructor:{validator:s,utils:{query:p,append:u,setStyle:c}}}=n;l=s({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"},...l},{html:"?string",video:"?string",url:"?string",playDuration:"number",totalDuration:"number",muted:"?boolean",i18n:{close:"string",countdown:"string",detail:"string",canBeClosed:"string"}});let y=null,g=null,f=null,m=null,v=null,x=null,h=0,b=null,w=!1,$=!1,k=!1;function D(e,l){return l.replace("%s",e)}function A(){w=!0,n.play(),l.video&&y.pause(),c(n.template.$ads,"display","none"),n.emit("artplayerPluginAds:skip",l)}function C(){w||(b=setTimeout(()=>{h+=1;const e=l.playDuration-h;e>=1?f.innerHTML=D(e,l.i18n.canBeClosed):(f.innerHTML=l.i18n.close,k||(k=!0)),m.innerHTML=D(l.totalDuration-h,l.i18n.countdown),h>=l.totalDuration?A():C()},1e3))}function P(){w||clearTimeout(b)}function T(){$||($=!0,function(){n.template.$ads=u(i,'<div class="artplayer-plugin-ads"></div>'),y=u(n.template.$ads,l.video?`<video class="artplayer-plugin-ads-video" src="${l.video}" loop playsInline></video>`:`<div class="artplayer-plugin-ads-html">${l.html}</div>`),x=u(n.template.$ads,'<div class="artplayer-plugin-ads-loading"></div>'),u(x,d),g=u(n.template.$ads,`<div class="artplayer-plugin-ads-timer">\n <div class="artplayer-plugin-ads-close">${l.playDuration<=0?l.i18n.close:D(l.playDuration,l.i18n.canBeClosed)}</div>\n <div class="artplayer-plugin-ads-countdown">${D(l.totalDuration,l.i18n.countdown)}</div>\n </div>`),f=p(".artplayer-plugin-ads-close",g),m=p(".artplayer-plugin-ads-countdown",g),l.playDuration>=l.totalDuration&&c(f,"display","none"),n.proxy(f,"click",()=>{k&&A()}),v=u(n.template.$ads,`<div class="artplayer-plugin-ads-control">\n <div class="artplayer-plugin-ads-detail">${l.i18n.detail}</div>\n <div class="artplayer-plugin-ads-muted"></div>\n <div class="artplayer-plugin-ads-fullscreen"></div>\n </div>`);const e=p(".artplayer-plugin-ads-detail",v),s=p(".artplayer-plugin-ads-muted",v),h=p(".artplayer-plugin-ads-fullscreen",v);if(l.url?n.proxy(e,"click",()=>{window.open(l.url),n.emit("artplayerPluginAds:click",l)}):c(e,"display","none"),l.video){const e=u(s,a),i=u(s,t);c(i,"display","none"),l.muted&&(y.muted=!0,c(e,"display","none"),c(i,"display","inline-flex")),n.proxy(s,"click",()=>{y.muted=!y.muted,y.muted?(c(e,"display","none"),c(i,"display","inline-flex")):(c(e,"display","inline-flex"),c(i,"display","none"))})}else c(s,"display","none");const b=u(h,r),w=u(h,o);c(w,"display","none"),n.proxy(h,"click",()=>{n.fullscreen=!n.fullscreen,n.fullscreen?(c(b,"display","inline-flex"),c(w,"display","none")):(c(b,"display","none"),c(w,"display","inline-flex"))}),n.proxy(y,"click",()=>{l.url&&window.open(l.url),n.emit("artplayerPluginAds:click",l)})}(),n.pause(),l.video?(n.proxy(y,"error",A),n.proxy(y,"loadedmetadata",()=>{C(),y.play(),c(g,"display","flex"),c(v,"display","flex"),c(x,"display","none")})):(C(),c(g,"display","flex"),c(v,"display","flex"),c(x,"display","none")),n.on("document:visibilitychange",()=>{document.hidden?P():C()}))}return n.on("ready",()=>{n.once("play",T),n.once("video:playing",T)}),{name:"artplayerPluginAds",skip:A,pause:P,play:C}}}if("undefined"!==typeof document&&!document.getElementById("artplayer-plugin-ads")){const e=document.createElement("style");e.id="artplayer-plugin-ads",e.textContent=".artplayer-plugin-ads{position:absolute;z-index:150;inset:0;width:100%;height:100%;overflow:hidden;font-size:13px;line-height:1;color:#fff;background-color:#000}.artplayer-plugin-ads .artplayer-plugin-ads-html{display:flex;align-items:center;justify-content:center;width:100%;height:100%}.artplayer-plugin-ads .artplayer-plugin-ads-video{width:100%;height:100%}.artplayer-plugin-ads .artplayer-plugin-ads-timer{display:none;position:absolute;top:10px;right:10px}.artplayer-plugin-ads .artplayer-plugin-ads-timer>div{display:flex;align-items:center;background-color:#00000080;border-radius:15px;margin-left:5px;padding:5px 10px;cursor:pointer}.artplayer-plugin-ads .artplayer-plugin-ads-control{display:none;position:absolute;bottom:10px;right:10px}.artplLine 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),b),m=()=>"active"===c&&!i.isDestroy,h=()=>"destroyed"===c||i.isDestroy;function x(n){try{n?.pause()}catch(t){e(t)}}function w(n,t,i){try{Promise.resolve(n()).then(()=>{m()||i?.()},t).catch(e)}catch(l){t(l)}}function b(){if("ended"!==c&&"destroyed"!==c&&!i.isDestroy){if(c="ended",v.stop(),r.dispose(),y){if(w(()=>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 A(){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 w=i.playDuration<=0;function b(){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;w=0===e?i.playDuration<=0:n<1||Number.isNaN(n),f.innerHTML=w?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),b()):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",()=>{w&&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,b()}),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:b,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;w(()=>t.play(),n=>{m()&&(e(n),b())},()=>x(t))}m()&&n.ready()}};n.video?(r.dom(n.video,"error",b),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",A),r.on("video:playing",A))}),i.isDestroy&&k()}catch(C){throw k(),C}return{name:"artplayerPluginAds",skip:b,pause:v.pause,play:v.play}}if("undefined"!==typeof document&&!document.getElementById("artplayer-plugin-ads")){const e=document.createElement("style");e.id="artplayer-plugin-ads",e.textContent=".artplayeLine truncated
+1 -1
View File
@@ -4,4 +4,4 @@
* (c) 2017-2026 Harvey Zhao
* Released under the MIT License.
*/
!function(e,l){"object"===typeof exports&&"undefined"!==typeof module?module.exports=l():"function"==typeof define&&define.amd?(t.artplayerPluginAds=l(),define(function(){return t.artplayerPluginAds})):(e="undefined"!==typeof globalThis?globalThis:e||self).artplayerPluginAds=l()}(this,function(){"use strict";var e=Object.defineProperty,l=Object.getOwnPropertySymbols,n=Object.prototype.hasOwnProperty,a=Object.prototype.propertyIsEnumerable,i=(l,n,a)=>n in l?e(l,n,{enumerable:!0,configurable:!0,writable:!0,value:a}):l[n]=a;function t(e){return r=>{!function(e){const{version:l,utils:{errorHandle:n}}=e.constructor,a=l.split(".").map(Number);n(a[0]+a[1]/100>=5,`Artplayer.js@${l} is not compatible the artplayerPluginAds@${t.version}. Please update it to version Artplayer.js@5.x.x`)}(r);const{template:{$player:o},icons:{volume:d,volumeClose:p,fullscreenOn:s,fullscreenOff:u,loading:c},constructor:{validator:y,utils:{query:g,append:f,setStyle:m}}}=r;e=y(((e,t)=>{for(var r in t||(t={}))n.call(t,r)&&i(e,r,t[r]);if(l)for(var r of l(t))a.call(t,r)&&i(e,r,t[r]);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"}});let v=null,x=null,h=null,b=null,w=null,$=null,k=0,D=null,P=!1,j=!1,A=!1;function O(e,l){return l.replace("%s",e)}function C(){P=!0,r.play(),e.video&&v.pause(),m(r.template.$ads,"display","none"),r.emit("artplayerPluginAds:skip",e)}function T(){P||(D=setTimeout(()=>{k+=1;const l=e.playDuration-k;l>=1?h.innerHTML=O(l,e.i18n.canBeClosed):(h.innerHTML=e.i18n.close,A||(A=!0)),b.innerHTML=O(e.totalDuration-k,e.i18n.countdown),k>=e.totalDuration?C():T()},1e3))}function B(){P||clearTimeout(D)}function H(){j||(j=!0,function(){r.template.$ads=f(o,'<div class="artplayer-plugin-ads"></div>'),v=f(r.template.$ads,e.video?`<video class="artplayer-plugin-ads-video" src="${e.video}" loop playsInline></video>`:`<div class="artplayer-plugin-ads-html">${e.html}</div>`),$=f(r.template.$ads,'<div class="artplayer-plugin-ads-loading"></div>'),f($,c),x=f(r.template.$ads,`<div class="artplayer-plugin-ads-timer">\n <div class="artplayer-plugin-ads-close">${e.playDuration<=0?e.i18n.close:O(e.playDuration,e.i18n.canBeClosed)}</div>\n <div class="artplayer-plugin-ads-countdown">${O(e.totalDuration,e.i18n.countdown)}</div>\n </div>`),h=g(".artplayer-plugin-ads-close",x),b=g(".artplayer-plugin-ads-countdown",x),e.playDuration>=e.totalDuration&&m(h,"display","none"),r.proxy(h,"click",()=>{A&&C()}),w=f(r.template.$ads,`<div class="artplayer-plugin-ads-control">\n <div class="artplayer-plugin-ads-detail">${e.i18n.detail}</div>\n <div class="artplayer-plugin-ads-muted"></div>\n <div class="artplayer-plugin-ads-fullscreen"></div>\n </div>`);const l=g(".artplayer-plugin-ads-detail",w),n=g(".artplayer-plugin-ads-muted",w),a=g(".artplayer-plugin-ads-fullscreen",w);if(e.url?r.proxy(l,"click",()=>{window.open(e.url),r.emit("artplayerPluginAds:click",e)}):m(l,"display","none"),e.video){const l=f(n,d),a=f(n,p);m(a,"display","none"),e.muted&&(v.muted=!0,m(l,"display","none"),m(a,"display","inline-flex")),r.proxy(n,"click",()=>{v.muted=!v.muted,v.muted?(m(l,"display","none"),m(a,"display","inline-flex")):(m(l,"display","inline-flex"),m(a,"display","none"))})}else m(n,"display","none");const i=f(a,s),t=f(a,u);m(t,"display","none"),r.proxy(a,"click",()=>{r.fullscreen=!r.fullscreen,r.fullscreen?(m(i,"display","inline-flex"),m(t,"display","none")):(m(i,"display","none"),m(t,"display","inline-flex"))}),r.proxy(v,"click",()=>{e.url&&window.open(e.url),r.emit("artplayerPluginAds:click",e)})}(),r.pause(),e.video?(r.proxy(v,"error",C),r.proxy(v,"loadedmetadata",()=>{T(),v.play(),m(x,"display","flex"),m(w,"display","flex"),m($,"display","none")})):(T(),m(x,"display","flex"),m(w,"display","flex"),m($,"display","none")),r.on("document:visibilitychange",()=>{document.hidden?B():T()}))}return r.on("ready",()=>{r.once("play",H),r.once("video:playing",H)}),{name:"artplayerPluginAds",skip:C,pause:B,play:T}}}if("undefined"!==typeof document&&!document.getElementById("artplayer-plugin-ads")){const e=document.createElement("style");e.id="artplayer-plugin-ads",e.textContent=".artplayer-plugin-ads{position:absolute;z-index:150;inset:0;width:100%;height:100%;overflow:hidden;font-size:13px;line-height:1;color:#fff;background-color:#000}.artplayer-plugin-ads .artplayer-plugin-ads-html{display:flex;align-items:center;justify-content:center;width:100%;height:100%}.artplayer-plugin-ads .artplayer-plugin-ads-video{width:100%;height:100%}.artplayer-plugin-ads .artplayer-plugin-ads-timer{display:noneLine 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,l=Object.prototype.hasOwnProperty,t=Object.prototype.propertyIsEnumerable,i=(n,l,t)=>l in n?e(n,l,{enumerable:!0,configurable:!0,writable:!0,value:t}):n[l]=t;function r(e,r){return r(((e,r)=>{for(var o in r||(r={}))l.call(r,o)&&i(e,o,r[o]);if(n)for(var o of n(r))t.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 l=!1;function t(e,t){if(!l){n.push(t);try{e()}finally{l&&t()}}}return{on(n,i){const r=()=>{l||i()};t(()=>{e.on(n,r)},()=>{e.off(n,r)})},dom(e,n,i){const r=()=>{l||i()};t(()=>e.addEventListener(n,r),()=>e.removeEventListener(n,r))},dispose(){if(!l){l=!0;for(const l of n.splice(0).reverse())try{l()}catch(e){o(e)}}}}}const s=(e,n)=>n.replace("%s",String(e));function d(e,n,l,t){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,l){let t,i=0,r=!1,o=!1,a=!1;function s(){r&&o&&!a&&void 0===t&&(t=setTimeout(()=>{t=void 0,!a&&o&&(i+=1,n(i),a||(i>=e()?l():s()))},1e3))}function d(){o=!1,void 0!==t&&clearTimeout(t),t=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,l){try{Promise.resolve(e()).then(()=>{m()||null==l||l()},n).catch(o)}catch(t){n(t)}}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,l,t,i){const{append:r,query:o,setStyle:a}=t,d=r(e,'<div class="artplayer-plugin-ads"></div>');i(d);const u=r(d,l.video?'<video class="artplayer-plugin-ads-video" loop playsInline></video>':`<div class="artplayer-plugin-ads-html">${l.html}</div>`),p=l.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">${l.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=l.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=l.playDuration-e;x=0===e?l.playDuration<=0:n<1||Number.isNaN(n),f.innerHTML=x?l.i18n.close:s(n,l.i18n.canBeClosed),g.innerHTML=s(l.totalDuration-e,l.i18n.countdown)}return l.playDuration>=l.totalDuration&&a(f,"display","none"),l.url||a(m,"display","none"),p?(r(h,n.volume),r(h,n.volumeClose),p.muted=Boolean(l.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),l.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,l,n,t,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
+382 -232
View File
@@ -4,247 +4,397 @@
* (c) 2017-2026 Harvey Zhao
* Released under the MIT License.
*/
const style = ".artplayer-plugin-ads {\n position: absolute;\n z-index: 150;\n left: 0;\n top: 0;\n right: 0;\n bottom: 0;\n width: 100%;\n height: 100%;\n overflow: hidden;\n font-size: 13px;\n line-height: 1;\n color: #fff;\n background-color: #000;\n}\n.artplayer-plugin-ads .artplayer-plugin-ads-html {\n display: flex;\n align-items: center;\n justify-content: center;\n width: 100%;\n height: 100%;\n}\n.artplayer-plugin-ads .artplayer-plugin-ads-video {\n width: 100%;\n height: 100%;\n}\n.artplayer-plugin-ads .artplayer-plugin-ads-timer {\n display: none;\n position: absolute;\n top: 10px;\n right: 10px;\n}\n.artplayer-plugin-ads .artplayer-plugin-ads-timer > div {\n display: flex;\n align-items: center;\n background-color: rgba(0, 0, 0, 0.5);\n border-radius: 15px;\n margin-left: 5px;\n padding: 5px 10px;\n cursor: pointer;\n}\n.artplayer-plugin-ads .artplayer-plugin-ads-control {\n display: none;\n position: absolute;\n bottom: 10px;\n right: 10px;\n}\n.artplayer-plugin-ads .artplayer-plugin-ads-control > div {\n display: flex;\n align-items: center;\n background-color: rgba(0, 0, 0, 0.5);\n border-radius: 15px;\n margin-left: 5px;\n padding: 5px 10px;\n cursor: pointer;\n}\n.artplayer-plugin-ads .artplayer-plugin-ads-control .art-icon svg {\n width: 20px;\n height: 20px;\n}\n.artplayer-plugin-ads .artplayer-plugin-ads-loading {\n display: flex;\n align-items: center;\n justify-content: center;\n position: absolute;\n left: 0;\n top: 0;\n right: 0;\n bottom: 0;\n width: 100%;\n height: 100%;\n}\n";
function checkVersion(art) {
const {
version,
utils: { errorHandle }
} = art.constructor;
const arr = version.split(".").map(Number);
const major = arr[0];
const minor = arr[1] / 100;
errorHandle(
major + minor >= 5,
`Artplayer.js@${version} is not compatible the artplayerPluginAds@${artplayerPluginAds.version}. Please update it to version Artplayer.js@5.x.x`
);
function normalizeOptions(input, validate) {
return validate({
html: "",
video: "",
url: "",
playDuration: 5,
totalDuration: 10,
muted: false,
i18n: {
close: "关闭广告",
countdown: "%s秒",
detail: "查看详情",
canBeClosed: "%s秒后可关闭广告"
},
...input
}, {
html: "?string",
video: "?string",
url: "?string",
playDuration: "number",
totalDuration: "number",
muted: "?boolean",
i18n: { close: "string", countdown: "string", detail: "string", canBeClosed: "string" }
});
}
function artplayerPluginAds(option) {
return (art) => {
checkVersion(art);
const {
template: { $player },
icons: { volume, volumeClose, fullscreenOn, fullscreenOff, loading },
constructor: {
validator,
utils: { query, append, setStyle }
}
} = art;
option = validator(
{
html: "",
video: "",
url: "",
playDuration: 5,
totalDuration: 10,
muted: false,
i18n: {
close: "关闭广告",
countdown: "%s秒",
detail: "查看详情",
canBeClosed: "%s秒后可关闭广告"
},
...option
},
{
html: "?string",
video: "?string",
url: "?string",
playDuration: "number",
totalDuration: "number",
muted: "?boolean",
i18n: {
close: "string",
countdown: "string",
detail: "string",
canBeClosed: "string"
}
}
);
let $ads = null;
let $timer = null;
let $close = null;
let $countdown = null;
let $control = null;
let $loading = null;
let time = 0;
let timer = null;
let isEnd = false;
let isInit = false;
let isCanClose = false;
function getI18n(val, str) {
return str.replace("%s", val);
}
function skip() {
isEnd = true;
art.play();
if (option.video)
$ads.pause();
setStyle(art.template.$ads, "display", "none");
art.emit("artplayerPluginAds:skip", option);
}
function play() {
if (isEnd)
function createCountdown(total, render, complete) {
let elapsed = 0;
let ready = false;
let running = false;
let closed = false;
let timer;
function schedule() {
if (!ready || !running || closed || timer !== void 0)
return;
timer = setTimeout(() => {
timer = void 0;
if (closed || !running)
return;
timer = setTimeout(() => {
time += 1;
const playDuration = option.playDuration - time;
if (playDuration >= 1) {
$close.innerHTML = getI18n(playDuration, option.i18n.canBeClosed);
} else {
$close.innerHTML = option.i18n.close;
if (!isCanClose) {
isCanClose = true;
}
}
$countdown.innerHTML = getI18n(option.totalDuration - time, option.i18n.countdown);
if (time >= option.totalDuration) {
skip();
} else {
play();
}
}, 1e3);
}
function pause() {
if (isEnd)
elapsed += 1;
render(elapsed);
if (closed)
return;
if (elapsed >= total())
complete();
else
schedule();
}, 1e3);
}
function pause() {
running = false;
if (timer !== void 0)
clearTimeout(timer);
}
function show() {
art.template.$ads = append($player, '<div class="artplayer-plugin-ads"></div>');
$ads = append(
art.template.$ads,
option.video ? `<video class="artplayer-plugin-ads-video" src="${option.video}" loop playsInline></video>` : `<div class="artplayer-plugin-ads-html">${option.html}</div>`
);
$loading = append(art.template.$ads, '<div class="artplayer-plugin-ads-loading"></div>');
append($loading, loading);
$timer = append(
art.template.$ads,
`<div class="artplayer-plugin-ads-timer">
<div class="artplayer-plugin-ads-close">${option.playDuration <= 0 ? option.i18n.close : getI18n(option.playDuration, option.i18n.canBeClosed)}</div>
<div class="artplayer-plugin-ads-countdown">${getI18n(
option.totalDuration,
option.i18n.countdown
)}</div>
</div>`
);
$close = query(".artplayer-plugin-ads-close", $timer);
$countdown = query(".artplayer-plugin-ads-countdown", $timer);
if (option.playDuration >= option.totalDuration) {
setStyle($close, "display", "none");
}
art.proxy($close, "click", () => {
if (isCanClose) {
skip();
}
});
$control = append(
art.template.$ads,
`<div class="artplayer-plugin-ads-control">
<div class="artplayer-plugin-ads-detail">${option.i18n.detail}</div>
<div class="artplayer-plugin-ads-muted"></div>
<div class="artplayer-plugin-ads-fullscreen"></div>
</div>`
);
const $detail = query(".artplayer-plugin-ads-detail", $control);
const $muted = query(".artplayer-plugin-ads-muted", $control);
const $fullscreen = query(".artplayer-plugin-ads-fullscreen", $control);
if (option.url) {
art.proxy($detail, "click", () => {
window.open(option.url);
art.emit("artplayerPluginAds:click", option);
});
} else {
setStyle($detail, "display", "none");
}
if (option.video) {
const $volume = append($muted, volume);
const $volumeClose = append($muted, volumeClose);
setStyle($volumeClose, "display", "none");
if (option.muted) {
$ads.muted = true;
setStyle($volume, "display", "none");
setStyle($volumeClose, "display", "inline-flex");
}
art.proxy($muted, "click", () => {
$ads.muted = !$ads.muted;
if ($ads.muted) {
setStyle($volume, "display", "none");
setStyle($volumeClose, "display", "inline-flex");
} else {
setStyle($volume, "display", "inline-flex");
setStyle($volumeClose, "display", "none");
}
});
} else {
setStyle($muted, "display", "none");
}
const $fullscreenOn = append($fullscreen, fullscreenOn);
const $fullscreenOff = append($fullscreen, fullscreenOff);
setStyle($fullscreenOff, "display", "none");
art.proxy($fullscreen, "click", () => {
art.fullscreen = !art.fullscreen;
if (art.fullscreen) {
setStyle($fullscreenOn, "display", "inline-flex");
setStyle($fullscreenOff, "display", "none");
} else {
setStyle($fullscreenOn, "display", "none");
setStyle($fullscreenOff, "display", "inline-flex");
}
});
art.proxy($ads, "click", () => {
if (option.url)
window.open(option.url);
art.emit("artplayerPluginAds:click", option);
});
}
function init() {
if (isInit)
timer = void 0;
}
return {
start() {
if (ready || closed)
return;
isInit = true;
show();
art.pause();
if (option.video) {
art.proxy($ads, "error", skip);
art.proxy($ads, "loadedmetadata", () => {
play();
$ads.play();
setStyle($timer, "display", "flex");
setStyle($control, "display", "flex");
setStyle($loading, "display", "none");
});
} else {
play();
setStyle($timer, "display", "flex");
setStyle($control, "display", "flex");
setStyle($loading, "display", "none");
}
art.on("document:visibilitychange", () => {
if (document.hidden) {
pause();
} else {
play();
}
});
ready = true;
running = true;
schedule();
},
play() {
if (!ready || closed)
return;
running = true;
schedule();
},
pause,
stop() {
closed = true;
pause();
}
art.on("ready", () => {
art.once("play", init);
art.once("video:playing", init);
});
return {
name: "artplayerPluginAds",
skip,
pause,
play
};
};
}
if (typeof document !== "undefined") {
if (!document.getElementById("artplayer-plugin-ads")) {
const $style = document.createElement("style");
$style.id = "artplayer-plugin-ads";
$style.textContent = style;
document.head.appendChild($style);
function report(error) {
console.warn("Artplayer Ads:", error);
}
function createResources(host) {
const cleaners = [];
let closed = false;
function install(add, remove) {
if (closed)
return;
cleaners.push(remove);
try {
add();
} finally {
if (closed)
remove();
}
}
return {
on(name, callback) {
const listener = () => {
if (!closed)
callback();
};
install(() => {
host.on(name, listener);
}, () => {
host.off(name, listener);
});
},
dom(target, name, callback) {
const listener = () => {
if (!closed)
callback();
};
install(() => target.addEventListener(name, listener), () => target.removeEventListener(name, listener));
},
dispose() {
if (closed)
return;
closed = true;
for (const clean of cleaners.splice(0).reverse()) {
try {
clean();
} catch (error) {
report(error);
}
}
}
};
}
const translate = (value, text) => text.replace("%s", String(value));
function createView(parent, icons, option, utils, own) {
const { append, query, setStyle } = utils;
const root = append(parent, '<div class="artplayer-plugin-ads"></div>');
own(root);
const content = append(root, option.video ? '<video class="artplayer-plugin-ads-video" loop playsInline></video>' : `<div class="artplayer-plugin-ads-html">${option.html}</div>`);
const video = option.video ? content : null;
const loading = append(root, '<div class="artplayer-plugin-ads-loading"></div>');
append(loading, icons.loading);
const timer = append(root, `<div class="artplayer-plugin-ads-timer">
<div class="artplayer-plugin-ads-close"></div>
<div class="artplayer-plugin-ads-countdown"></div>
</div>`);
const close = query(".artplayer-plugin-ads-close", timer);
const countdown = query(".artplayer-plugin-ads-countdown", timer);
const control = append(root, `<div class="artplayer-plugin-ads-control">
<div class="artplayer-plugin-ads-detail">${option.i18n.detail}</div>
<div class="artplayer-plugin-ads-muted"></div>
<div class="artplayer-plugin-ads-fullscreen"></div>
</div>`);
const detail = query(".artplayer-plugin-ads-detail", control);
const muted = query(".artplayer-plugin-ads-muted", control);
const fullscreen = query(".artplayer-plugin-ads-fullscreen", control);
let canClose = option.playDuration <= 0;
if (option.playDuration >= option.totalDuration)
setStyle(close, "display", "none");
if (!option.url)
setStyle(detail, "display", "none");
if (video) {
append(muted, icons.volume);
append(muted, icons.volumeClose);
video.muted = Boolean(option.muted);
syncMuted();
} else {
setStyle(muted, "display", "none");
}
append(fullscreen, icons.fullscreenOn);
append(fullscreen, icons.fullscreenOff);
function syncMuted() {
setStyle(icons.volume, "display", video?.muted ? "none" : "inline-flex");
setStyle(icons.volumeClose, "display", video?.muted ? "inline-flex" : "none");
}
function render(elapsed) {
const remaining = option.playDuration - elapsed;
canClose = elapsed === 0 ? option.playDuration <= 0 : remaining < 1 || Number.isNaN(remaining);
close.innerHTML = canClose ? option.i18n.close : translate(remaining, option.i18n.canBeClosed);
countdown.innerHTML = translate(option.totalDuration - elapsed, option.i18n.countdown);
}
render(0);
return {
root,
video,
render,
ready() {
setStyle(timer, "display", "flex");
setStyle(control, "display", "flex");
setStyle(loading, "display", "none");
},
hide() {
setStyle(root, "display", "none");
},
fullscreen(active) {
setStyle(icons.fullscreenOn, "display", active ? "none" : "inline-flex");
setStyle(icons.fullscreenOff, "display", active ? "inline-flex" : "none");
},
bind(events, callbacks) {
events.dom(close, "click", () => {
if (canClose)
callbacks.skip();
});
events.dom(content, "click", callbacks.click);
if (option.url)
events.dom(detail, "click", callbacks.click);
if (video) {
events.dom(muted, "click", () => {
video.muted = !video.muted;
syncMuted();
});
}
events.dom(fullscreen, "click", callbacks.fullscreen);
}
};
}
function createSession(art, option, icons, utils) {
const events = createResources(art);
const lifetime = createResources(art);
const template = art.template;
let state = "waiting";
let initialized = false;
let armed = false;
let mediaReady = false;
let root;
let view;
const clock = createCountdown(() => option.totalDuration, (time) => view?.render(time), skip);
const active = () => state === "active" && !art.isDestroy;
const destroyed = () => state === "destroyed" || art.isDestroy;
function pauseVideo(video) {
try {
video?.pause();
} catch (error) {
report(error);
}
}
function requestPlay(start, rejected, late) {
try {
Promise.resolve(start()).then(() => {
if (!active())
late?.();
}, rejected).catch(report);
} catch (error) {
rejected(error);
}
}
function skip() {
if (state === "ended" || state === "destroyed" || art.isDestroy)
return;
state = "ended";
clock.stop();
events.dispose();
if (initialized) {
requestPlay(() => art.play(), (error) => {
if (state !== "destroyed")
report(error);
});
if (destroyed())
return;
pauseVideo(view?.video);
if (destroyed())
return;
view?.hide();
}
if (!destroyed())
art.emit("artplayerPluginAds:skip", option);
}
function disposeView() {
const video = view?.video;
pauseVideo(video);
if (video) {
try {
video.removeAttribute("src");
video.load();
} catch (error) {
report(error);
}
}
try {
root?.remove();
} catch (error) {
report(error);
} finally {
if (template.$ads === root)
delete template.$ads;
}
}
function destroy() {
if (state === "destroyed")
return;
state = "destroyed";
clock.stop();
events.dispose();
lifetime.dispose();
disposeView();
}
function init() {
if (state !== "waiting" || art.isDestroy)
return;
state = "active";
initialized = true;
try {
view = createView(template.$player, icons, option, utils, (node) => {
root = node;
template.$ads = node;
});
if (!active()) {
disposeView();
return;
}
art.pause();
if (!active())
return;
const current = view;
const document2 = root.ownerDocument;
current.fullscreen(art.fullscreen);
current.bind(events, {
skip,
click() {
if (option.url)
(document2.defaultView || window).open(option.url);
if (active())
art.emit("artplayerPluginAds:click", option);
},
fullscreen() {
art.fullscreen = !art.fullscreen;
if (active())
current.fullscreen(art.fullscreen);
}
});
events.on("fullscreen", () => current.fullscreen(art.fullscreen));
const visibility = () => document2.hidden ? clock.pause() : clock.play();
events.dom(document2, "visibilitychange", visibility);
events.on("document:visibilitychange", visibility);
const ready = () => {
if (!active() || mediaReady)
return;
mediaReady = true;
clock.start();
if (document2.hidden)
clock.pause();
if (current.video) {
const video = current.video;
requestPlay(() => video.play(), (error) => {
if (active()) {
report(error);
skip();
}
}, () => pauseVideo(video));
}
if (active())
current.ready();
};
if (current.video) {
events.dom(current.video, "error", skip);
events.dom(current.video, "loadedmetadata", ready);
current.video.src = option.video;
} else {
ready();
}
} catch (error) {
destroy();
throw error;
}
}
try {
lifetime.on("destroy", destroy);
events.on("ready", () => {
if (armed || state !== "waiting")
return;
armed = true;
events.on("play", init);
events.on("video:playing", init);
});
if (art.isDestroy)
destroy();
} catch (error) {
destroy();
throw error;
}
return { name: "artplayerPluginAds", skip, pause: clock.pause, play: clock.play };
}
const style = ".artplayer-plugin-ads {\n position: absolute;\n z-index: 150;\n left: 0;\n top: 0;\n right: 0;\n bottom: 0;\n width: 100%;\n height: 100%;\n overflow: hidden;\n font-size: 13px;\n line-height: 1;\n color: #fff;\n background-color: #000;\n}\n.artplayer-plugin-ads .artplayer-plugin-ads-html {\n display: flex;\n align-items: center;\n justify-content: center;\n width: 100%;\n height: 100%;\n}\n.artplayer-plugin-ads .artplayer-plugin-ads-video {\n width: 100%;\n height: 100%;\n}\n.artplayer-plugin-ads .artplayer-plugin-ads-timer {\n display: none;\n position: absolute;\n top: 10px;\n right: 10px;\n}\n.artplayer-plugin-ads .artplayer-plugin-ads-timer > div {\n display: flex;\n align-items: center;\n background-color: rgba(0, 0, 0, 0.5);\n border-radius: 15px;\n margin-left: 5px;\n padding: 5px 10px;\n cursor: pointer;\n}\n.artplayer-plugin-ads .artplayer-plugin-ads-control {\n display: none;\n position: absolute;\n bottom: 10px;\n right: 10px;\n}\n.artplayer-plugin-ads .artplayer-plugin-ads-control > div {\n display: flex;\n align-items: center;\n background-color: rgba(0, 0, 0, 0.5);\n border-radius: 15px;\n margin-left: 5px;\n padding: 5px 10px;\n cursor: pointer;\n}\n.artplayer-plugin-ads .artplayer-plugin-ads-control .art-icon svg {\n width: 20px;\n height: 20px;\n}\n.artplayer-plugin-ads .artplayer-plugin-ads-loading {\n display: flex;\n align-items: center;\n justify-content: center;\n position: absolute;\n left: 0;\n top: 0;\n right: 0;\n bottom: 0;\n width: 100%;\n height: 100%;\n}\n";
function artplayerPluginAds(input) {
return (art) => {
const constructor = art.constructor;
if (typeof constructor.validator !== "function" || !["append", "query", "setStyle"].every((name) => typeof Reflect.get(constructor.utils || {}, name) === "function")) {
throw new Error("Artplayer Ads requires the core validator and DOM utilities");
}
const option = normalizeOptions(input, constructor.validator);
const { volume, volumeClose, fullscreenOn, fullscreenOff, loading } = art.icons;
return createSession(art, option, { volume, volumeClose, fullscreenOn, fullscreenOff, loading }, constructor.utils);
};
}
if (typeof document !== "undefined" && !document.getElementById("artplayer-plugin-ads")) {
const element = document.createElement("style");
element.id = "artplayer-plugin-ads";
element.textContent = style;
document.head.appendChild(element);
}
export {
artplayerPluginAds as default
+2 -2
View File
@@ -34,7 +34,7 @@
"build:i18n": "node ./scripts/build-i18n.js",
"build:test": "node ./scripts/build-test.js",
"test:playback": "node --test test/playback.test.js",
"test:ads": "node --test test/ads.test.js",
"test:ads": "node --test test/ads.test.js test/ads-lifecycle.test.js",
"trans:docs": "node ./scripts/trans-docs.js",
"create:plugin": "node ./scripts/plugin/create.js",
"test:dash-control": "node --test test/dash-control.test.js test/dash-contract.test.js test/dash-lifecycle.test.js test/dash-events.test.js",
@@ -51,7 +51,7 @@
"ci:build": "yarn build:types && yarn build all && yarn build:i18n && yarn build:ts && yarn build:docs && yarn test:imports",
"test:imports": "node --test test/esm.test.js test/i18n.test.js test/ssr.test.js",
"typecheck": "node scripts/typecheck.mjs",
"test:unit": "node --test test/ads.test.js test/playback.test.js test/dash-control.test.js test/dash-contract.test.js test/dash-lifecycle.test.js test/dash-events.test.js test/hls-control.test.js test/audio-track.test.js test/public-behavior.test.js test/helpers.test.js test/chapter.test.js test/utils.test.js test/resource-scope.test.js test/instance-lifecycle.test.js test/options.test.js test/media-hosts.test.js test/plugins.test.js test/source.test.js test/playback-properties.test.js test/media-events.test.js test/template-resources.test.js test/core-vendor.test.js test/component-resources.test.js test/setting-model.test.js test/setting-layout.test.js test/setting-resources.test.js test/subtitle.test.js test/display-native.test.js test/display-video-fullscreen.test.js test/display-pip.test.js test/display-mini.test.js test/display-sizing.test.js test/display-orientation.test.js test/hotkey.test.js test/listener-registry.test.js test/global-events.test.js test/pointer-events.test.js test/gesture.test.js test/event-scheduling.test.js test/notice.test.js test/fast-forward.test.js test/auto-playback.test.js test/builtin-layers.test.js test/prompt-components.test.js test/screenshot.test.js test/thumbnails.test.js test/progress.test.js test/environment.test.js test/storage.test.js test/facade-properties.test.js test/dom-boundaries.test.js test/initialization.test.js test/entry.test.js test/accessibility-button.test.js test/accessibility-focus.test.js test/accessibility-slider.test.js",
"test:unit": "node --test test/ads.test.js test/ads-lifecycle.test.js test/playback.test.js test/dash-control.test.js test/dash-contract.test.js test/dash-lifecycle.test.js test/dash-events.test.js test/hls-control.test.js test/audio-track.test.js test/public-behavior.test.js test/helpers.test.js test/chapter.test.js test/utils.test.js test/resource-scope.test.js test/instance-lifecycle.test.js test/options.test.js test/media-hosts.test.js test/plugins.test.js test/source.test.js test/playback-properties.test.js test/media-events.test.js test/template-resources.test.js test/core-vendor.test.js test/component-resources.test.js test/setting-model.test.js test/setting-layout.test.js test/setting-resources.test.js test/subtitle.test.js test/display-native.test.js test/display-video-fullscreen.test.js test/display-pip.test.js test/display-mini.test.js test/display-sizing.test.js test/display-orientation.test.js test/hotkey.test.js test/listener-registry.test.js test/global-events.test.js test/pointer-events.test.js test/gesture.test.js test/event-scheduling.test.js test/notice.test.js test/fast-forward.test.js test/auto-playback.test.js test/builtin-layers.test.js test/prompt-components.test.js test/screenshot.test.js test/thumbnails.test.js test/progress.test.js test/environment.test.js test/storage.test.js test/facade-properties.test.js test/dom-boundaries.test.js test/initialization.test.js test/entry.test.js test/accessibility-button.test.js test/accessibility-focus.test.js test/accessibility-slider.test.js",
"test:coverage": "node --test test/coverage.test.js && node scripts/coverage.mjs",
"test": "yarn test:node && yarn test:baseline",
"test:browser": "playwright test",
+2 -1
View File
@@ -2,4 +2,5 @@ node_modules
.DS_Store
yarn.lock
package-lock.json
src
src
tsconfig.json
@@ -0,0 +1,83 @@
# Ads implementation and maintenance
Ads is a single preroll attached by `artplayerPluginAds(option)(art)`. The factory and
returned `{ name, skip, pause, play }` are synchronous. Package public declarations are
still scheduled for PKG-ADS-04; do not infer that their current source/type fields are
implemented runtime aliases. Runtime input remains html/video/url plus timing, mute and i18n.
## Module boundaries
| File | Responsibility |
| --- | --- |
| src/index.ts | Public factory, checked legacy constructor capabilities, import-time style injection |
| src/options.ts | Per-attachment defaults and the existing shallow validator schema |
| src/types.ts | Internal options, result and narrow core/DOM dependencies; public types remain separate for now |
| src/countdown.ts | One owned timeout, whole-second counting, pause/resume and terminal cancellation; no core or DOM dependency |
| src/view.ts | Existing HTML/CSS hooks, labels, ad element, mute and fullscreen icon rendering |
| src/resources.ts | Instance-owned core/DOM subscriptions, guarded callbacks and disposal that continues after cleanup failures |
| src/session.ts | Ready/first-play lifecycle, ad readiness, playback requests, completion, destruction and cross-module ownership |
The entry calls options/session. Session composes view/countdown/resources. View receives
only a parent, icons, options, three DOM utilities and callbacks, rather than the whole player.
Style remains in src/style.less. There are no runtime imports from the ArtPlayer core package;
all imports of its declarations are erased. Old cores do not need new lifecycle helpers.
## State and ordering
`waiting -> active -> ended`, with any state able to become `destroyed`. Ready arms the two
existing first-play signals; only one can initialize. Repeated ready/play/metadata cannot
create another view or timer. The normalized option belongs to this attachment even when a
caller reuses the factory. Emitted options retain their identity and remain live: for example,
changing totalDuration in an ad-click listener affects subsequent ticks.
For video, the element is created without a source. Error and loadedmetadata listeners are
installed before assigning src. Metadata starts the timer and requests ad playback. HTML
starts immediately. The ad video loops independently from the totalDuration countdown.
Public pause/play still control only the countdown; they do not pause/resume the ad video.
Native document visibility and the newer core visibility bridge both control that same timer;
idempotent resume prevents duplicate timer chains. Initially hidden documents pause counting.
Skip first closes the timer and active subscriptions. For an initialized ad it requests main
content playback, pauses the ad, hides the overlay and synchronously emits
artplayerPluginAds:skip with the normalized option. This keeps the historical successful
ordering. Reentrant destruction prevents subsequent dispatch. Repeated completion is inert.
Before initialization, play/pause are inert; skip cancels the pending preroll and emits once
without starting main playback or creating DOM. These early-call semantics repair historical
exceptions and are covered separately from normal old behavior.
Internally requested play Promises are observed. An ad play rejection warns with the original
error and completes the ad like a media load failure. Rejected main-content restoration also
warns, while completion remains visible and synchronous. Direct consumer calls to art.play
are not wrapped or changed. If an ad play settles after its owner ended/died, its own video is
paused again; stale rejections cannot restart content.
## Resource ownership
The session owns one countdown, active core/native listeners, a separate destroy subscription,
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
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
or depend on new resource APIs. Cleanup guards stale callbacks and handles partial view
construction/registration failure. A failure while cleaning one subscription does not stop
the remaining cleanup. Root ownership is recorded as soon as it is appended.
Keep all artplayer-plugin-ads class names and the template.$ads hook. The full screen button
uses the core fullscreen property; its icons reflect both the initial value and the core
fullscreen event. Actual device/native fullscreen acceptance remains part of PKG-ADS-05.
## Verification and remaining work
Use pinned Yarn/Node. `yarn test:ads` covers source/frozen releases and candidate lifecycle
regressions; `yarn test:browser test/browser/ads.spec.js` covers real local media and old/new
cores. `yarn typecheck` checks the strict package configuration. `yarn build artplayer-plugin-ads`
generates main/legacy/ESM through the repository build. Do not hand-edit dist or copied docs assets.
Tests distinguish actual media decoding from injected errors/controlled clocks. Baseline and
open release gates live in [Ads validation](../../refactor/ads-validation.md). Public declaration
reconciliation, old CommonJS default compatibility, isolated package consumption and demo
validation remain PKG-ADS-04/06. Real hidden-page/device behavior and complete media-resource
acceptance remain PKG-ADS-05. No claim of compatibility with every historical 4.x core is made.
+3
View File
@@ -2,6 +2,9 @@
Ads plugin for ArtPlayer
Implementation, resource ownership and regression commands:
[maintenance guide](ARCHITECTURE.md).
## Demo
[https://artplayer.org](https://artplayer.org/?libs=./uncompiled/artplayer-plugin-ads/index.js&example=ads)
+1 -1
View File
@@ -4,4 +4,4 @@
* (c) 2017-2026 Harvey Zhao
* Released under the MIT License.
*/
!function(e,l){"object"===typeof exports&&"undefined"!==typeof module?module.exports=l():"function"==typeof define&&define.amd?(t.artplayerPluginAds=l(),define(function(){return t.artplayerPluginAds})):(e="undefined"!==typeof globalThis?globalThis:e||self).artplayerPluginAds=l()}(this,function(){"use strict";function e(l){return n=>{!function(l){const{version:n,utils:{errorHandle:i}}=l.constructor,a=n.split(".").map(Number);i(a[0]+a[1]/100>=5,`Artplayer.js@${n} is not compatible the artplayerPluginAds@${e.version}. Please update it to version Artplayer.js@5.x.x`)}(n);const{template:{$player:i},icons:{volume:a,volumeClose:t,fullscreenOn:r,fullscreenOff:o,loading:d},constructor:{validator:s,utils:{query:p,append:u,setStyle:c}}}=n;l=s({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"},...l},{html:"?string",video:"?string",url:"?string",playDuration:"number",totalDuration:"number",muted:"?boolean",i18n:{close:"string",countdown:"string",detail:"string",canBeClosed:"string"}});let y=null,g=null,f=null,m=null,v=null,x=null,h=0,b=null,w=!1,$=!1,k=!1;function D(e,l){return l.replace("%s",e)}function A(){w=!0,n.play(),l.video&&y.pause(),c(n.template.$ads,"display","none"),n.emit("artplayerPluginAds:skip",l)}function C(){w||(b=setTimeout(()=>{h+=1;const e=l.playDuration-h;e>=1?f.innerHTML=D(e,l.i18n.canBeClosed):(f.innerHTML=l.i18n.close,k||(k=!0)),m.innerHTML=D(l.totalDuration-h,l.i18n.countdown),h>=l.totalDuration?A():C()},1e3))}function P(){w||clearTimeout(b)}function T(){$||($=!0,function(){n.template.$ads=u(i,'<div class="artplayer-plugin-ads"></div>'),y=u(n.template.$ads,l.video?`<video class="artplayer-plugin-ads-video" src="${l.video}" loop playsInline></video>`:`<div class="artplayer-plugin-ads-html">${l.html}</div>`),x=u(n.template.$ads,'<div class="artplayer-plugin-ads-loading"></div>'),u(x,d),g=u(n.template.$ads,`<div class="artplayer-plugin-ads-timer">\n <div class="artplayer-plugin-ads-close">${l.playDuration<=0?l.i18n.close:D(l.playDuration,l.i18n.canBeClosed)}</div>\n <div class="artplayer-plugin-ads-countdown">${D(l.totalDuration,l.i18n.countdown)}</div>\n </div>`),f=p(".artplayer-plugin-ads-close",g),m=p(".artplayer-plugin-ads-countdown",g),l.playDuration>=l.totalDuration&&c(f,"display","none"),n.proxy(f,"click",()=>{k&&A()}),v=u(n.template.$ads,`<div class="artplayer-plugin-ads-control">\n <div class="artplayer-plugin-ads-detail">${l.i18n.detail}</div>\n <div class="artplayer-plugin-ads-muted"></div>\n <div class="artplayer-plugin-ads-fullscreen"></div>\n </div>`);const e=p(".artplayer-plugin-ads-detail",v),s=p(".artplayer-plugin-ads-muted",v),h=p(".artplayer-plugin-ads-fullscreen",v);if(l.url?n.proxy(e,"click",()=>{window.open(l.url),n.emit("artplayerPluginAds:click",l)}):c(e,"display","none"),l.video){const e=u(s,a),i=u(s,t);c(i,"display","none"),l.muted&&(y.muted=!0,c(e,"display","none"),c(i,"display","inline-flex")),n.proxy(s,"click",()=>{y.muted=!y.muted,y.muted?(c(e,"display","none"),c(i,"display","inline-flex")):(c(e,"display","inline-flex"),c(i,"display","none"))})}else c(s,"display","none");const b=u(h,r),w=u(h,o);c(w,"display","none"),n.proxy(h,"click",()=>{n.fullscreen=!n.fullscreen,n.fullscreen?(c(b,"display","inline-flex"),c(w,"display","none")):(c(b,"display","none"),c(w,"display","inline-flex"))}),n.proxy(y,"click",()=>{l.url&&window.open(l.url),n.emit("artplayerPluginAds:click",l)})}(),n.pause(),l.video?(n.proxy(y,"error",A),n.proxy(y,"loadedmetadata",()=>{C(),y.play(),c(g,"display","flex"),c(v,"display","flex"),c(x,"display","none")})):(C(),c(g,"display","flex"),c(v,"display","flex"),c(x,"display","none")),n.on("document:visibilitychange",()=>{document.hidden?P():C()}))}return n.on("ready",()=>{n.once("play",T),n.once("video:playing",T)}),{name:"artplayerPluginAds",skip:A,pause:P,play:C}}}if("undefined"!==typeof document&&!document.getElementById("artplayer-plugin-ads")){const e=document.createElement("style");e.id="artplayer-plugin-ads",e.textContent=".artplayer-plugin-ads{position:absolute;z-index:150;inset:0;width:100%;height:100%;overflow:hidden;font-size:13px;line-height:1;color:#fff;background-color:#000}.artplayer-plugin-ads .artplayer-plugin-ads-html{display:flex;align-items:center;justify-content:center;width:100%;height:100%}.artplayer-plugin-ads .artplayer-plugin-ads-video{width:100%;height:100%}.artplayer-plugin-ads .artplayer-plugin-ads-timer{display:none;position:absolute;top:10px;right:10px}.artplayer-plugin-ads .artplayer-plugin-ads-timer>div{display:flex;align-items:center;background-color:#00000080;border-radius:15px;margin-left:5px;padding:5px 10px;cursor:pointer}.artplayer-plugin-ads .artplayer-plugin-ads-control{display:none;position:absolute;bottom:10px;right:10px}.artplLine 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),b),m=()=>"active"===c&&!i.isDestroy,h=()=>"destroyed"===c||i.isDestroy;function x(n){try{n?.pause()}catch(t){e(t)}}function w(n,t,i){try{Promise.resolve(n()).then(()=>{m()||i?.()},t).catch(e)}catch(l){t(l)}}function b(){if("ended"!==c&&"destroyed"!==c&&!i.isDestroy){if(c="ended",v.stop(),r.dispose(),y){if(w(()=>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 A(){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 w=i.playDuration<=0;function b(){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;w=0===e?i.playDuration<=0:n<1||Number.isNaN(n),f.innerHTML=w?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),b()):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",()=>{w&&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,b()}),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:b,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;w(()=>t.play(),n=>{m()&&(e(n),b())},()=>x(t))}m()&&n.ready()}};n.video?(r.dom(n.video,"error",b),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",A),r.on("video:playing",A))}),i.isDestroy&&k()}catch(C){throw k(),C}return{name:"artplayerPluginAds",skip:b,pause:v.pause,play:v.play}}if("undefined"!==typeof document&&!document.getElementById("artplayer-plugin-ads")){const e=document.createElement("style");e.id="artplayer-plugin-ads",e.textContent=".artplayeLine truncated
@@ -4,4 +4,4 @@
* (c) 2017-2026 Harvey Zhao
* Released under the MIT License.
*/
!function(e,l){"object"===typeof exports&&"undefined"!==typeof module?module.exports=l():"function"==typeof define&&define.amd?(t.artplayerPluginAds=l(),define(function(){return t.artplayerPluginAds})):(e="undefined"!==typeof globalThis?globalThis:e||self).artplayerPluginAds=l()}(this,function(){"use strict";var e=Object.defineProperty,l=Object.getOwnPropertySymbols,n=Object.prototype.hasOwnProperty,a=Object.prototype.propertyIsEnumerable,i=(l,n,a)=>n in l?e(l,n,{enumerable:!0,configurable:!0,writable:!0,value:a}):l[n]=a;function t(e){return r=>{!function(e){const{version:l,utils:{errorHandle:n}}=e.constructor,a=l.split(".").map(Number);n(a[0]+a[1]/100>=5,`Artplayer.js@${l} is not compatible the artplayerPluginAds@${t.version}. Please update it to version Artplayer.js@5.x.x`)}(r);const{template:{$player:o},icons:{volume:d,volumeClose:p,fullscreenOn:s,fullscreenOff:u,loading:c},constructor:{validator:y,utils:{query:g,append:f,setStyle:m}}}=r;e=y(((e,t)=>{for(var r in t||(t={}))n.call(t,r)&&i(e,r,t[r]);if(l)for(var r of l(t))a.call(t,r)&&i(e,r,t[r]);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"}});let v=null,x=null,h=null,b=null,w=null,$=null,k=0,D=null,P=!1,j=!1,A=!1;function O(e,l){return l.replace("%s",e)}function C(){P=!0,r.play(),e.video&&v.pause(),m(r.template.$ads,"display","none"),r.emit("artplayerPluginAds:skip",e)}function T(){P||(D=setTimeout(()=>{k+=1;const l=e.playDuration-k;l>=1?h.innerHTML=O(l,e.i18n.canBeClosed):(h.innerHTML=e.i18n.close,A||(A=!0)),b.innerHTML=O(e.totalDuration-k,e.i18n.countdown),k>=e.totalDuration?C():T()},1e3))}function B(){P||clearTimeout(D)}function H(){j||(j=!0,function(){r.template.$ads=f(o,'<div class="artplayer-plugin-ads"></div>'),v=f(r.template.$ads,e.video?`<video class="artplayer-plugin-ads-video" src="${e.video}" loop playsInline></video>`:`<div class="artplayer-plugin-ads-html">${e.html}</div>`),$=f(r.template.$ads,'<div class="artplayer-plugin-ads-loading"></div>'),f($,c),x=f(r.template.$ads,`<div class="artplayer-plugin-ads-timer">\n <div class="artplayer-plugin-ads-close">${e.playDuration<=0?e.i18n.close:O(e.playDuration,e.i18n.canBeClosed)}</div>\n <div class="artplayer-plugin-ads-countdown">${O(e.totalDuration,e.i18n.countdown)}</div>\n </div>`),h=g(".artplayer-plugin-ads-close",x),b=g(".artplayer-plugin-ads-countdown",x),e.playDuration>=e.totalDuration&&m(h,"display","none"),r.proxy(h,"click",()=>{A&&C()}),w=f(r.template.$ads,`<div class="artplayer-plugin-ads-control">\n <div class="artplayer-plugin-ads-detail">${e.i18n.detail}</div>\n <div class="artplayer-plugin-ads-muted"></div>\n <div class="artplayer-plugin-ads-fullscreen"></div>\n </div>`);const l=g(".artplayer-plugin-ads-detail",w),n=g(".artplayer-plugin-ads-muted",w),a=g(".artplayer-plugin-ads-fullscreen",w);if(e.url?r.proxy(l,"click",()=>{window.open(e.url),r.emit("artplayerPluginAds:click",e)}):m(l,"display","none"),e.video){const l=f(n,d),a=f(n,p);m(a,"display","none"),e.muted&&(v.muted=!0,m(l,"display","none"),m(a,"display","inline-flex")),r.proxy(n,"click",()=>{v.muted=!v.muted,v.muted?(m(l,"display","none"),m(a,"display","inline-flex")):(m(l,"display","inline-flex"),m(a,"display","none"))})}else m(n,"display","none");const i=f(a,s),t=f(a,u);m(t,"display","none"),r.proxy(a,"click",()=>{r.fullscreen=!r.fullscreen,r.fullscreen?(m(i,"display","inline-flex"),m(t,"display","none")):(m(i,"display","none"),m(t,"display","inline-flex"))}),r.proxy(v,"click",()=>{e.url&&window.open(e.url),r.emit("artplayerPluginAds:click",e)})}(),r.pause(),e.video?(r.proxy(v,"error",C),r.proxy(v,"loadedmetadata",()=>{T(),v.play(),m(x,"display","flex"),m(w,"display","flex"),m($,"display","none")})):(T(),m(x,"display","flex"),m(w,"display","flex"),m($,"display","none")),r.on("document:visibilitychange",()=>{document.hidden?B():T()}))}return r.on("ready",()=>{r.once("play",H),r.once("video:playing",H)}),{name:"artplayerPluginAds",skip:C,pause:B,play:T}}}if("undefined"!==typeof document&&!document.getElementById("artplayer-plugin-ads")){const e=document.createElement("style");e.id="artplayer-plugin-ads",e.textContent=".artplayer-plugin-ads{position:absolute;z-index:150;inset:0;width:100%;height:100%;overflow:hidden;font-size:13px;line-height:1;color:#fff;background-color:#000}.artplayer-plugin-ads .artplayer-plugin-ads-html{display:flex;align-items:center;justify-content:center;width:100%;height:100%}.artplayer-plugin-ads .artplayer-plugin-ads-video{width:100%;height:100%}.artplayer-plugin-ads .artplayer-plugin-ads-timer{display:noneLine 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,l=Object.prototype.hasOwnProperty,t=Object.prototype.propertyIsEnumerable,i=(n,l,t)=>l in n?e(n,l,{enumerable:!0,configurable:!0,writable:!0,value:t}):n[l]=t;function r(e,r){return r(((e,r)=>{for(var o in r||(r={}))l.call(r,o)&&i(e,o,r[o]);if(n)for(var o of n(r))t.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 l=!1;function t(e,t){if(!l){n.push(t);try{e()}finally{l&&t()}}}return{on(n,i){const r=()=>{l||i()};t(()=>{e.on(n,r)},()=>{e.off(n,r)})},dom(e,n,i){const r=()=>{l||i()};t(()=>e.addEventListener(n,r),()=>e.removeEventListener(n,r))},dispose(){if(!l){l=!0;for(const l of n.splice(0).reverse())try{l()}catch(e){o(e)}}}}}const s=(e,n)=>n.replace("%s",String(e));function d(e,n,l,t){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,l){let t,i=0,r=!1,o=!1,a=!1;function s(){r&&o&&!a&&void 0===t&&(t=setTimeout(()=>{t=void 0,!a&&o&&(i+=1,n(i),a||(i>=e()?l():s()))},1e3))}function d(){o=!1,void 0!==t&&clearTimeout(t),t=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,l){try{Promise.resolve(e()).then(()=>{m()||null==l||l()},n).catch(o)}catch(t){n(t)}}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,l,t,i){const{append:r,query:o,setStyle:a}=t,d=r(e,'<div class="artplayer-plugin-ads"></div>');i(d);const u=r(d,l.video?'<video class="artplayer-plugin-ads-video" loop playsInline></video>':`<div class="artplayer-plugin-ads-html">${l.html}</div>`),p=l.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">${l.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=l.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=l.playDuration-e;x=0===e?l.playDuration<=0:n<1||Number.isNaN(n),f.innerHTML=x?l.i18n.close:s(n,l.i18n.canBeClosed),g.innerHTML=s(l.totalDuration-e,l.i18n.countdown)}return l.playDuration>=l.totalDuration&&a(f,"display","none"),l.url||a(m,"display","none"),p?(r(h,n.volume),r(h,n.volumeClose),p.muted=Boolean(l.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),l.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,l,n,t,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
+382 -232
View File
@@ -4,247 +4,397 @@
* (c) 2017-2026 Harvey Zhao
* Released under the MIT License.
*/
const style = ".artplayer-plugin-ads {\n position: absolute;\n z-index: 150;\n left: 0;\n top: 0;\n right: 0;\n bottom: 0;\n width: 100%;\n height: 100%;\n overflow: hidden;\n font-size: 13px;\n line-height: 1;\n color: #fff;\n background-color: #000;\n}\n.artplayer-plugin-ads .artplayer-plugin-ads-html {\n display: flex;\n align-items: center;\n justify-content: center;\n width: 100%;\n height: 100%;\n}\n.artplayer-plugin-ads .artplayer-plugin-ads-video {\n width: 100%;\n height: 100%;\n}\n.artplayer-plugin-ads .artplayer-plugin-ads-timer {\n display: none;\n position: absolute;\n top: 10px;\n right: 10px;\n}\n.artplayer-plugin-ads .artplayer-plugin-ads-timer > div {\n display: flex;\n align-items: center;\n background-color: rgba(0, 0, 0, 0.5);\n border-radius: 15px;\n margin-left: 5px;\n padding: 5px 10px;\n cursor: pointer;\n}\n.artplayer-plugin-ads .artplayer-plugin-ads-control {\n display: none;\n position: absolute;\n bottom: 10px;\n right: 10px;\n}\n.artplayer-plugin-ads .artplayer-plugin-ads-control > div {\n display: flex;\n align-items: center;\n background-color: rgba(0, 0, 0, 0.5);\n border-radius: 15px;\n margin-left: 5px;\n padding: 5px 10px;\n cursor: pointer;\n}\n.artplayer-plugin-ads .artplayer-plugin-ads-control .art-icon svg {\n width: 20px;\n height: 20px;\n}\n.artplayer-plugin-ads .artplayer-plugin-ads-loading {\n display: flex;\n align-items: center;\n justify-content: center;\n position: absolute;\n left: 0;\n top: 0;\n right: 0;\n bottom: 0;\n width: 100%;\n height: 100%;\n}\n";
function checkVersion(art) {
const {
version,
utils: { errorHandle }
} = art.constructor;
const arr = version.split(".").map(Number);
const major = arr[0];
const minor = arr[1] / 100;
errorHandle(
major + minor >= 5,
`Artplayer.js@${version} is not compatible the artplayerPluginAds@${artplayerPluginAds.version}. Please update it to version Artplayer.js@5.x.x`
);
function normalizeOptions(input, validate) {
return validate({
html: "",
video: "",
url: "",
playDuration: 5,
totalDuration: 10,
muted: false,
i18n: {
close: "关闭广告",
countdown: "%s秒",
detail: "查看详情",
canBeClosed: "%s秒后可关闭广告"
},
...input
}, {
html: "?string",
video: "?string",
url: "?string",
playDuration: "number",
totalDuration: "number",
muted: "?boolean",
i18n: { close: "string", countdown: "string", detail: "string", canBeClosed: "string" }
});
}
function artplayerPluginAds(option) {
return (art) => {
checkVersion(art);
const {
template: { $player },
icons: { volume, volumeClose, fullscreenOn, fullscreenOff, loading },
constructor: {
validator,
utils: { query, append, setStyle }
}
} = art;
option = validator(
{
html: "",
video: "",
url: "",
playDuration: 5,
totalDuration: 10,
muted: false,
i18n: {
close: "关闭广告",
countdown: "%s秒",
detail: "查看详情",
canBeClosed: "%s秒后可关闭广告"
},
...option
},
{
html: "?string",
video: "?string",
url: "?string",
playDuration: "number",
totalDuration: "number",
muted: "?boolean",
i18n: {
close: "string",
countdown: "string",
detail: "string",
canBeClosed: "string"
}
}
);
let $ads = null;
let $timer = null;
let $close = null;
let $countdown = null;
let $control = null;
let $loading = null;
let time = 0;
let timer = null;
let isEnd = false;
let isInit = false;
let isCanClose = false;
function getI18n(val, str) {
return str.replace("%s", val);
}
function skip() {
isEnd = true;
art.play();
if (option.video)
$ads.pause();
setStyle(art.template.$ads, "display", "none");
art.emit("artplayerPluginAds:skip", option);
}
function play() {
if (isEnd)
function createCountdown(total, render, complete) {
let elapsed = 0;
let ready = false;
let running = false;
let closed = false;
let timer;
function schedule() {
if (!ready || !running || closed || timer !== void 0)
return;
timer = setTimeout(() => {
timer = void 0;
if (closed || !running)
return;
timer = setTimeout(() => {
time += 1;
const playDuration = option.playDuration - time;
if (playDuration >= 1) {
$close.innerHTML = getI18n(playDuration, option.i18n.canBeClosed);
} else {
$close.innerHTML = option.i18n.close;
if (!isCanClose) {
isCanClose = true;
}
}
$countdown.innerHTML = getI18n(option.totalDuration - time, option.i18n.countdown);
if (time >= option.totalDuration) {
skip();
} else {
play();
}
}, 1e3);
}
function pause() {
if (isEnd)
elapsed += 1;
render(elapsed);
if (closed)
return;
if (elapsed >= total())
complete();
else
schedule();
}, 1e3);
}
function pause() {
running = false;
if (timer !== void 0)
clearTimeout(timer);
}
function show() {
art.template.$ads = append($player, '<div class="artplayer-plugin-ads"></div>');
$ads = append(
art.template.$ads,
option.video ? `<video class="artplayer-plugin-ads-video" src="${option.video}" loop playsInline></video>` : `<div class="artplayer-plugin-ads-html">${option.html}</div>`
);
$loading = append(art.template.$ads, '<div class="artplayer-plugin-ads-loading"></div>');
append($loading, loading);
$timer = append(
art.template.$ads,
`<div class="artplayer-plugin-ads-timer">
<div class="artplayer-plugin-ads-close">${option.playDuration <= 0 ? option.i18n.close : getI18n(option.playDuration, option.i18n.canBeClosed)}</div>
<div class="artplayer-plugin-ads-countdown">${getI18n(
option.totalDuration,
option.i18n.countdown
)}</div>
</div>`
);
$close = query(".artplayer-plugin-ads-close", $timer);
$countdown = query(".artplayer-plugin-ads-countdown", $timer);
if (option.playDuration >= option.totalDuration) {
setStyle($close, "display", "none");
}
art.proxy($close, "click", () => {
if (isCanClose) {
skip();
}
});
$control = append(
art.template.$ads,
`<div class="artplayer-plugin-ads-control">
<div class="artplayer-plugin-ads-detail">${option.i18n.detail}</div>
<div class="artplayer-plugin-ads-muted"></div>
<div class="artplayer-plugin-ads-fullscreen"></div>
</div>`
);
const $detail = query(".artplayer-plugin-ads-detail", $control);
const $muted = query(".artplayer-plugin-ads-muted", $control);
const $fullscreen = query(".artplayer-plugin-ads-fullscreen", $control);
if (option.url) {
art.proxy($detail, "click", () => {
window.open(option.url);
art.emit("artplayerPluginAds:click", option);
});
} else {
setStyle($detail, "display", "none");
}
if (option.video) {
const $volume = append($muted, volume);
const $volumeClose = append($muted, volumeClose);
setStyle($volumeClose, "display", "none");
if (option.muted) {
$ads.muted = true;
setStyle($volume, "display", "none");
setStyle($volumeClose, "display", "inline-flex");
}
art.proxy($muted, "click", () => {
$ads.muted = !$ads.muted;
if ($ads.muted) {
setStyle($volume, "display", "none");
setStyle($volumeClose, "display", "inline-flex");
} else {
setStyle($volume, "display", "inline-flex");
setStyle($volumeClose, "display", "none");
}
});
} else {
setStyle($muted, "display", "none");
}
const $fullscreenOn = append($fullscreen, fullscreenOn);
const $fullscreenOff = append($fullscreen, fullscreenOff);
setStyle($fullscreenOff, "display", "none");
art.proxy($fullscreen, "click", () => {
art.fullscreen = !art.fullscreen;
if (art.fullscreen) {
setStyle($fullscreenOn, "display", "inline-flex");
setStyle($fullscreenOff, "display", "none");
} else {
setStyle($fullscreenOn, "display", "none");
setStyle($fullscreenOff, "display", "inline-flex");
}
});
art.proxy($ads, "click", () => {
if (option.url)
window.open(option.url);
art.emit("artplayerPluginAds:click", option);
});
}
function init() {
if (isInit)
timer = void 0;
}
return {
start() {
if (ready || closed)
return;
isInit = true;
show();
art.pause();
if (option.video) {
art.proxy($ads, "error", skip);
art.proxy($ads, "loadedmetadata", () => {
play();
$ads.play();
setStyle($timer, "display", "flex");
setStyle($control, "display", "flex");
setStyle($loading, "display", "none");
});
} else {
play();
setStyle($timer, "display", "flex");
setStyle($control, "display", "flex");
setStyle($loading, "display", "none");
}
art.on("document:visibilitychange", () => {
if (document.hidden) {
pause();
} else {
play();
}
});
ready = true;
running = true;
schedule();
},
play() {
if (!ready || closed)
return;
running = true;
schedule();
},
pause,
stop() {
closed = true;
pause();
}
art.on("ready", () => {
art.once("play", init);
art.once("video:playing", init);
});
return {
name: "artplayerPluginAds",
skip,
pause,
play
};
};
}
if (typeof document !== "undefined") {
if (!document.getElementById("artplayer-plugin-ads")) {
const $style = document.createElement("style");
$style.id = "artplayer-plugin-ads";
$style.textContent = style;
document.head.appendChild($style);
function report(error) {
console.warn("Artplayer Ads:", error);
}
function createResources(host) {
const cleaners = [];
let closed = false;
function install(add, remove) {
if (closed)
return;
cleaners.push(remove);
try {
add();
} finally {
if (closed)
remove();
}
}
return {
on(name, callback) {
const listener = () => {
if (!closed)
callback();
};
install(() => {
host.on(name, listener);
}, () => {
host.off(name, listener);
});
},
dom(target, name, callback) {
const listener = () => {
if (!closed)
callback();
};
install(() => target.addEventListener(name, listener), () => target.removeEventListener(name, listener));
},
dispose() {
if (closed)
return;
closed = true;
for (const clean of cleaners.splice(0).reverse()) {
try {
clean();
} catch (error) {
report(error);
}
}
}
};
}
const translate = (value, text) => text.replace("%s", String(value));
function createView(parent, icons, option, utils, own) {
const { append, query, setStyle } = utils;
const root = append(parent, '<div class="artplayer-plugin-ads"></div>');
own(root);
const content = append(root, option.video ? '<video class="artplayer-plugin-ads-video" loop playsInline></video>' : `<div class="artplayer-plugin-ads-html">${option.html}</div>`);
const video = option.video ? content : null;
const loading = append(root, '<div class="artplayer-plugin-ads-loading"></div>');
append(loading, icons.loading);
const timer = append(root, `<div class="artplayer-plugin-ads-timer">
<div class="artplayer-plugin-ads-close"></div>
<div class="artplayer-plugin-ads-countdown"></div>
</div>`);
const close = query(".artplayer-plugin-ads-close", timer);
const countdown = query(".artplayer-plugin-ads-countdown", timer);
const control = append(root, `<div class="artplayer-plugin-ads-control">
<div class="artplayer-plugin-ads-detail">${option.i18n.detail}</div>
<div class="artplayer-plugin-ads-muted"></div>
<div class="artplayer-plugin-ads-fullscreen"></div>
</div>`);
const detail = query(".artplayer-plugin-ads-detail", control);
const muted = query(".artplayer-plugin-ads-muted", control);
const fullscreen = query(".artplayer-plugin-ads-fullscreen", control);
let canClose = option.playDuration <= 0;
if (option.playDuration >= option.totalDuration)
setStyle(close, "display", "none");
if (!option.url)
setStyle(detail, "display", "none");
if (video) {
append(muted, icons.volume);
append(muted, icons.volumeClose);
video.muted = Boolean(option.muted);
syncMuted();
} else {
setStyle(muted, "display", "none");
}
append(fullscreen, icons.fullscreenOn);
append(fullscreen, icons.fullscreenOff);
function syncMuted() {
setStyle(icons.volume, "display", video?.muted ? "none" : "inline-flex");
setStyle(icons.volumeClose, "display", video?.muted ? "inline-flex" : "none");
}
function render(elapsed) {
const remaining = option.playDuration - elapsed;
canClose = elapsed === 0 ? option.playDuration <= 0 : remaining < 1 || Number.isNaN(remaining);
close.innerHTML = canClose ? option.i18n.close : translate(remaining, option.i18n.canBeClosed);
countdown.innerHTML = translate(option.totalDuration - elapsed, option.i18n.countdown);
}
render(0);
return {
root,
video,
render,
ready() {
setStyle(timer, "display", "flex");
setStyle(control, "display", "flex");
setStyle(loading, "display", "none");
},
hide() {
setStyle(root, "display", "none");
},
fullscreen(active) {
setStyle(icons.fullscreenOn, "display", active ? "none" : "inline-flex");
setStyle(icons.fullscreenOff, "display", active ? "inline-flex" : "none");
},
bind(events, callbacks) {
events.dom(close, "click", () => {
if (canClose)
callbacks.skip();
});
events.dom(content, "click", callbacks.click);
if (option.url)
events.dom(detail, "click", callbacks.click);
if (video) {
events.dom(muted, "click", () => {
video.muted = !video.muted;
syncMuted();
});
}
events.dom(fullscreen, "click", callbacks.fullscreen);
}
};
}
function createSession(art, option, icons, utils) {
const events = createResources(art);
const lifetime = createResources(art);
const template = art.template;
let state = "waiting";
let initialized = false;
let armed = false;
let mediaReady = false;
let root;
let view;
const clock = createCountdown(() => option.totalDuration, (time) => view?.render(time), skip);
const active = () => state === "active" && !art.isDestroy;
const destroyed = () => state === "destroyed" || art.isDestroy;
function pauseVideo(video) {
try {
video?.pause();
} catch (error) {
report(error);
}
}
function requestPlay(start, rejected, late) {
try {
Promise.resolve(start()).then(() => {
if (!active())
late?.();
}, rejected).catch(report);
} catch (error) {
rejected(error);
}
}
function skip() {
if (state === "ended" || state === "destroyed" || art.isDestroy)
return;
state = "ended";
clock.stop();
events.dispose();
if (initialized) {
requestPlay(() => art.play(), (error) => {
if (state !== "destroyed")
report(error);
});
if (destroyed())
return;
pauseVideo(view?.video);
if (destroyed())
return;
view?.hide();
}
if (!destroyed())
art.emit("artplayerPluginAds:skip", option);
}
function disposeView() {
const video = view?.video;
pauseVideo(video);
if (video) {
try {
video.removeAttribute("src");
video.load();
} catch (error) {
report(error);
}
}
try {
root?.remove();
} catch (error) {
report(error);
} finally {
if (template.$ads === root)
delete template.$ads;
}
}
function destroy() {
if (state === "destroyed")
return;
state = "destroyed";
clock.stop();
events.dispose();
lifetime.dispose();
disposeView();
}
function init() {
if (state !== "waiting" || art.isDestroy)
return;
state = "active";
initialized = true;
try {
view = createView(template.$player, icons, option, utils, (node) => {
root = node;
template.$ads = node;
});
if (!active()) {
disposeView();
return;
}
art.pause();
if (!active())
return;
const current = view;
const document2 = root.ownerDocument;
current.fullscreen(art.fullscreen);
current.bind(events, {
skip,
click() {
if (option.url)
(document2.defaultView || window).open(option.url);
if (active())
art.emit("artplayerPluginAds:click", option);
},
fullscreen() {
art.fullscreen = !art.fullscreen;
if (active())
current.fullscreen(art.fullscreen);
}
});
events.on("fullscreen", () => current.fullscreen(art.fullscreen));
const visibility = () => document2.hidden ? clock.pause() : clock.play();
events.dom(document2, "visibilitychange", visibility);
events.on("document:visibilitychange", visibility);
const ready = () => {
if (!active() || mediaReady)
return;
mediaReady = true;
clock.start();
if (document2.hidden)
clock.pause();
if (current.video) {
const video = current.video;
requestPlay(() => video.play(), (error) => {
if (active()) {
report(error);
skip();
}
}, () => pauseVideo(video));
}
if (active())
current.ready();
};
if (current.video) {
events.dom(current.video, "error", skip);
events.dom(current.video, "loadedmetadata", ready);
current.video.src = option.video;
} else {
ready();
}
} catch (error) {
destroy();
throw error;
}
}
try {
lifetime.on("destroy", destroy);
events.on("ready", () => {
if (armed || state !== "waiting")
return;
armed = true;
events.on("play", init);
events.on("video:playing", init);
});
if (art.isDestroy)
destroy();
} catch (error) {
destroy();
throw error;
}
return { name: "artplayerPluginAds", skip, pause: clock.pause, play: clock.play };
}
const style = ".artplayer-plugin-ads {\n position: absolute;\n z-index: 150;\n left: 0;\n top: 0;\n right: 0;\n bottom: 0;\n width: 100%;\n height: 100%;\n overflow: hidden;\n font-size: 13px;\n line-height: 1;\n color: #fff;\n background-color: #000;\n}\n.artplayer-plugin-ads .artplayer-plugin-ads-html {\n display: flex;\n align-items: center;\n justify-content: center;\n width: 100%;\n height: 100%;\n}\n.artplayer-plugin-ads .artplayer-plugin-ads-video {\n width: 100%;\n height: 100%;\n}\n.artplayer-plugin-ads .artplayer-plugin-ads-timer {\n display: none;\n position: absolute;\n top: 10px;\n right: 10px;\n}\n.artplayer-plugin-ads .artplayer-plugin-ads-timer > div {\n display: flex;\n align-items: center;\n background-color: rgba(0, 0, 0, 0.5);\n border-radius: 15px;\n margin-left: 5px;\n padding: 5px 10px;\n cursor: pointer;\n}\n.artplayer-plugin-ads .artplayer-plugin-ads-control {\n display: none;\n position: absolute;\n bottom: 10px;\n right: 10px;\n}\n.artplayer-plugin-ads .artplayer-plugin-ads-control > div {\n display: flex;\n align-items: center;\n background-color: rgba(0, 0, 0, 0.5);\n border-radius: 15px;\n margin-left: 5px;\n padding: 5px 10px;\n cursor: pointer;\n}\n.artplayer-plugin-ads .artplayer-plugin-ads-control .art-icon svg {\n width: 20px;\n height: 20px;\n}\n.artplayer-plugin-ads .artplayer-plugin-ads-loading {\n display: flex;\n align-items: center;\n justify-content: center;\n position: absolute;\n left: 0;\n top: 0;\n right: 0;\n bottom: 0;\n width: 100%;\n height: 100%;\n}\n";
function artplayerPluginAds(input) {
return (art) => {
const constructor = art.constructor;
if (typeof constructor.validator !== "function" || !["append", "query", "setStyle"].every((name) => typeof Reflect.get(constructor.utils || {}, name) === "function")) {
throw new Error("Artplayer Ads requires the core validator and DOM utilities");
}
const option = normalizeOptions(input, constructor.validator);
const { volume, volumeClose, fullscreenOn, fullscreenOff, loading } = art.icons;
return createSession(art, option, { volume, volumeClose, fullscreenOn, fullscreenOff, loading }, constructor.utils);
};
}
if (typeof document !== "undefined" && !document.getElementById("artplayer-plugin-ads")) {
const element = document.createElement("style");
element.id = "artplayer-plugin-ads";
element.textContent = style;
document.head.appendChild(element);
}
export {
artplayerPluginAds as default
@@ -0,0 +1,55 @@
import type { Cleanup } from './types'
export function createCountdown(total: () => number, render: (seconds: number) => void, complete: Cleanup) {
let elapsed = 0
let ready = false
let running = false
let closed = false
let timer: ReturnType<typeof setTimeout> | undefined
function schedule(): void {
if (!ready || !running || closed || timer !== undefined)
return
timer = setTimeout(() => {
timer = undefined
if (closed || !running)
return
elapsed += 1
render(elapsed)
if (closed)
return
if (elapsed >= total())
complete()
else
schedule()
}, 1000)
}
function pause(): void {
running = false
if (timer !== undefined)
clearTimeout(timer)
timer = undefined
}
return {
start() {
if (ready || closed)
return
ready = true
running = true
schedule()
},
play() {
if (!ready || closed)
return
running = true
schedule()
},
pause,
stop() {
closed = true
pause()
},
}
}
-294
View File
@@ -1,294 +0,0 @@
import style from './style.less?inline'
function checkVersion(art) {
const {
version,
utils: { errorHandle },
} = art.constructor
const arr = version.split('.').map(Number)
const major = arr[0]
const minor = arr[1] / 100
errorHandle(
major + minor >= 5,
`Artplayer.js@${version} is not compatible the artplayerPluginAds@${artplayerPluginAds.version}. Please update it to version Artplayer.js@5.x.x`,
)
}
export default function artplayerPluginAds(option) {
return (art) => {
checkVersion(art)
const {
template: { $player },
icons: { volume, volumeClose, fullscreenOn, fullscreenOff, loading },
constructor: {
validator,
utils: { query, append, setStyle },
},
} = art
option = validator(
{
html: '',
video: '',
url: '',
playDuration: 5,
totalDuration: 10,
muted: false,
i18n: {
close: '关闭广告',
countdown: '%s秒',
detail: '查看详情',
canBeClosed: '%s秒后可关闭广告',
},
...option,
},
{
html: '?string',
video: '?string',
url: '?string',
playDuration: 'number',
totalDuration: 'number',
muted: '?boolean',
i18n: {
close: 'string',
countdown: 'string',
detail: 'string',
canBeClosed: 'string',
},
},
)
let $ads = null
let $timer = null
let $close = null
let $countdown = null
let $control = null
let $loading = null
let time = 0
let timer = null
let isEnd = false
let isInit = false
let isCanClose = false
function getI18n(val, str) {
return str.replace('%s', val)
}
function skip() {
isEnd = true
art.play()
if (option.video)
$ads.pause()
setStyle(art.template.$ads, 'display', 'none')
art.emit('artplayerPluginAds:skip', option)
}
function play() {
if (isEnd)
return
timer = setTimeout(() => {
time += 1
const playDuration = option.playDuration - time
if (playDuration >= 1) {
$close.innerHTML = getI18n(playDuration, option.i18n.canBeClosed)
}
else {
$close.innerHTML = option.i18n.close
if (!isCanClose) {
isCanClose = true
}
}
$countdown.innerHTML = getI18n(option.totalDuration - time, option.i18n.countdown)
if (time >= option.totalDuration) {
skip()
}
else {
play()
}
}, 1000)
}
function pause() {
if (isEnd)
return
clearTimeout(timer)
}
function show() {
art.template.$ads = append($player, '<div class="artplayer-plugin-ads"></div>')
$ads = append(
art.template.$ads,
option.video
? `<video class="artplayer-plugin-ads-video" src="${option.video}" loop playsInline></video>`
: `<div class="artplayer-plugin-ads-html">${option.html}</div>`,
)
$loading = append(art.template.$ads, '<div class="artplayer-plugin-ads-loading"></div>')
append($loading, loading)
$timer = append(
art.template.$ads,
`<div class="artplayer-plugin-ads-timer">
<div class="artplayer-plugin-ads-close">${
option.playDuration <= 0
? option.i18n.close
: getI18n(option.playDuration, option.i18n.canBeClosed)
}</div>
<div class="artplayer-plugin-ads-countdown">${getI18n(
option.totalDuration,
option.i18n.countdown,
)}</div>
</div>`,
)
$close = query('.artplayer-plugin-ads-close', $timer)
$countdown = query('.artplayer-plugin-ads-countdown', $timer)
if (option.playDuration >= option.totalDuration) {
setStyle($close, 'display', 'none')
}
art.proxy($close, 'click', () => {
if (isCanClose) {
skip()
}
})
$control = append(
art.template.$ads,
`<div class="artplayer-plugin-ads-control">
<div class="artplayer-plugin-ads-detail">${option.i18n.detail}</div>
<div class="artplayer-plugin-ads-muted"></div>
<div class="artplayer-plugin-ads-fullscreen"></div>
</div>`,
)
const $detail = query('.artplayer-plugin-ads-detail', $control)
const $muted = query('.artplayer-plugin-ads-muted', $control)
const $fullscreen = query('.artplayer-plugin-ads-fullscreen', $control)
if (option.url) {
art.proxy($detail, 'click', () => {
window.open(option.url)
art.emit('artplayerPluginAds:click', option)
})
}
else {
setStyle($detail, 'display', 'none')
}
if (option.video) {
const $volume = append($muted, volume)
const $volumeClose = append($muted, volumeClose)
setStyle($volumeClose, 'display', 'none')
// If the ad was set to muted initially, match the icon
if (option.muted) {
$ads.muted = true
setStyle($volume, 'display', 'none')
setStyle($volumeClose, 'display', 'inline-flex')
}
art.proxy($muted, 'click', () => {
$ads.muted = !$ads.muted
if ($ads.muted) {
setStyle($volume, 'display', 'none')
setStyle($volumeClose, 'display', 'inline-flex')
}
else {
setStyle($volume, 'display', 'inline-flex')
setStyle($volumeClose, 'display', 'none')
}
})
}
else {
setStyle($muted, 'display', 'none')
}
const $fullscreenOn = append($fullscreen, fullscreenOn)
const $fullscreenOff = append($fullscreen, fullscreenOff)
setStyle($fullscreenOff, 'display', 'none')
art.proxy($fullscreen, 'click', () => {
art.fullscreen = !art.fullscreen
if (art.fullscreen) {
setStyle($fullscreenOn, 'display', 'inline-flex')
setStyle($fullscreenOff, 'display', 'none')
}
else {
setStyle($fullscreenOn, 'display', 'none')
setStyle($fullscreenOff, 'display', 'inline-flex')
}
})
art.proxy($ads, 'click', () => {
if (option.url)
window.open(option.url)
art.emit('artplayerPluginAds:click', option)
})
}
function init() {
if (isInit)
return
isInit = true
show()
art.pause()
if (option.video) {
art.proxy($ads, 'error', skip)
art.proxy($ads, 'loadedmetadata', () => {
play()
$ads.play()
setStyle($timer, 'display', 'flex')
setStyle($control, 'display', 'flex')
setStyle($loading, 'display', 'none')
})
}
else {
play()
setStyle($timer, 'display', 'flex')
setStyle($control, 'display', 'flex')
setStyle($loading, 'display', 'none')
}
art.on('document:visibilitychange', () => {
if (document.hidden) {
pause()
}
else {
play()
}
})
}
art.on('ready', () => {
art.once('play', init)
art.once('video:playing', init)
})
return {
name: 'artplayerPluginAds',
skip,
pause,
play,
}
}
}
if (typeof document !== 'undefined') {
if (!document.getElementById('artplayer-plugin-ads')) {
const $style = document.createElement('style')
$style.id = 'artplayer-plugin-ads'
$style.textContent = style
document.head.appendChild($style)
}
}
@@ -0,0 +1,25 @@
import type Artplayer from 'artplayer'
import type { Input, Result } from './types'
import { normalizeOptions } from './options'
import { createSession } from './session'
import style from './style.less?inline'
export default function artplayerPluginAds(input?: Input) {
return (art: Artplayer): Result => {
// Legacy core declarations type instance.constructor as Function. Check the
// actual capabilities before using its existing static validator/utilities.
const constructor = art.constructor as typeof Artplayer
if (typeof constructor.validator !== 'function'
|| !['append', 'query', 'setStyle'].every(name => typeof Reflect.get(constructor.utils || {}, name) === 'function')) { throw new Error('Artplayer Ads requires the core validator and DOM utilities') }
const option = normalizeOptions(input, constructor.validator)
const { volume, volumeClose, fullscreenOn, fullscreenOff, loading } = art.icons
return createSession(art, option, { volume, volumeClose, fullscreenOn, fullscreenOff, loading }, constructor.utils)
}
}
if (typeof document !== 'undefined' && !document.getElementById('artplayer-plugin-ads')) {
const element = document.createElement('style')
element.id = 'artplayer-plugin-ads'
element.textContent = style
document.head.appendChild(element)
}
@@ -0,0 +1,28 @@
import type Artplayer from 'artplayer'
import type { Input, Options } from './types'
export function normalizeOptions(input: Input | undefined, validate: typeof Artplayer.validator): Options {
return validate({
html: '',
video: '',
url: '',
playDuration: 5,
totalDuration: 10,
muted: false,
i18n: {
close: '关闭广告',
countdown: '%s秒',
detail: '查看详情',
canBeClosed: '%s秒后可关闭广告',
},
...input,
}, {
html: '?string',
video: '?string',
url: '?string',
playDuration: 'number',
totalDuration: 'number',
muted: '?boolean',
i18n: { close: 'string', countdown: 'string', detail: 'string', canBeClosed: 'string' },
})
}
@@ -0,0 +1,58 @@
import type { Cleanup, Host } from './types'
export function report(error: unknown): void {
console.warn('Artplayer Ads:', error)
}
export function createResources(host: Pick<Host, 'on' | 'off'>) {
const cleaners: Cleanup[] = []
let closed = false
function install(add: Cleanup, remove: Cleanup): void {
if (closed)
return
cleaners.push(remove)
try {
add()
}
finally {
// A custom host/target can destroy the owner while registration is executing.
if (closed)
remove()
}
}
return {
on(name: string, callback: Cleanup) {
const listener = () => {
if (!closed)
callback()
}
install(() => {
host.on(name, listener)
}, () => {
host.off(name, listener)
})
},
dom(target: EventTarget, name: string, callback: Cleanup) {
const listener = () => {
if (!closed)
callback()
}
install(() => target.addEventListener(name, listener), () => target.removeEventListener(name, listener))
},
dispose() {
if (closed)
return
closed = true
for (const clean of cleaners.splice(0).reverse()) {
try {
clean()
}
catch (error) { report(error) }
}
},
}
}
export type Resources = ReturnType<typeof createResources>
@@ -0,0 +1,182 @@
import type { Host, Icons, Options, Result, Utilities } from './types'
import type { View } from './view'
import { createCountdown } from './countdown'
import { createResources, report } from './resources'
import { createView } from './view'
export function createSession(art: Host, option: Options, icons: Icons, utils: Utilities): Result {
const events = createResources(art)
const lifetime = createResources(art)
const template = art.template as Host['template'] & { $ads?: HTMLElement }
let state: 'waiting' | 'active' | 'ended' | 'destroyed' = 'waiting'
let initialized = false
let armed = false
let mediaReady = false
let root: HTMLElement | undefined
let view: View | undefined
const clock = createCountdown(() => option.totalDuration, time => view?.render(time), skip)
const active = () => state === 'active' && !art.isDestroy
const destroyed = () => state === 'destroyed' || art.isDestroy
function pauseVideo(video: HTMLVideoElement | null | undefined): void {
try {
video?.pause()
}
catch (error) { report(error) }
}
function requestPlay(start: () => unknown, rejected: (error: unknown) => void, late?: () => void): void {
try {
Promise.resolve(start()).then(() => {
if (!active())
late?.()
}, rejected).catch(report)
}
catch (error) { rejected(error) }
}
function skip(): void {
if (state === 'ended' || state === 'destroyed' || art.isDestroy)
return
state = 'ended'
clock.stop()
events.dispose()
if (initialized) {
requestPlay(() => art.play(), (error) => {
if (state !== 'destroyed')
report(error)
})
if (destroyed())
return
pauseVideo(view?.video)
if (destroyed())
return
view?.hide()
}
if (!destroyed())
art.emit('artplayerPluginAds:skip', option)
}
function disposeView(): void {
const video = view?.video
pauseVideo(video)
if (video) {
try {
video.removeAttribute('src')
video.load()
}
catch (error) { report(error) }
}
try {
root?.remove()
}
catch (error) { report(error) }
finally {
if (template.$ads === root)
delete template.$ads
}
}
function destroy(): void {
if (state === 'destroyed')
return
state = 'destroyed'
clock.stop()
events.dispose()
lifetime.dispose()
disposeView()
}
function init(): void {
if (state !== 'waiting' || art.isDestroy)
return
state = 'active'
initialized = true
try {
view = createView(template.$player, icons, option, utils, (node) => {
root = node
template.$ads = node
})
if (!active()) {
disposeView()
return
}
art.pause()
if (!active())
return
const current = view
const document = root!.ownerDocument
current.fullscreen(art.fullscreen)
current.bind(events, {
skip,
click() {
if (option.url)
(document.defaultView || window).open(option.url)
if (active())
art.emit('artplayerPluginAds:click', option)
},
fullscreen() {
art.fullscreen = !art.fullscreen
if (active())
current.fullscreen(art.fullscreen)
},
})
events.on('fullscreen', () => current.fullscreen(art.fullscreen))
const visibility = () => document.hidden ? clock.pause() : clock.play()
events.dom(document, 'visibilitychange', visibility)
// Support the newer core event bridge as well; the clock deduplicates resume.
events.on('document:visibilitychange', visibility)
const ready = () => {
if (!active() || mediaReady)
return
mediaReady = true
clock.start()
if (document.hidden)
clock.pause()
if (current.video) {
const video = current.video
requestPlay(() => video.play(), (error) => {
if (active()) {
report(error)
skip()
}
}, () => pauseVideo(video))
}
if (active())
current.ready()
}
if (current.video) {
events.dom(current.video, 'error', skip)
events.dom(current.video, 'loadedmetadata', ready)
// Observe readiness/failure before initiating a potentially cached request.
current.video.src = option.video!
}
else {
ready()
}
}
catch (error) {
destroy()
throw error
}
}
try {
lifetime.on('destroy', destroy)
events.on('ready', () => {
if (armed || state !== 'waiting')
return
armed = true
events.on('play', init)
events.on('video:playing', init)
})
if (art.isDestroy)
destroy()
}
catch (error) {
destroy()
throw error
}
return { name: 'artplayerPluginAds', skip, pause: clock.pause, play: clock.play }
}
@@ -0,0 +1,31 @@
import type Artplayer from 'artplayer'
export interface Translations {
close: string
countdown: string
detail: string
canBeClosed: string
}
export interface Options {
html?: string
video?: string
url?: string
playDuration: number
totalDuration: number
muted?: boolean
i18n: Translations
}
export type Input = Partial<Options>
export type Host = Pick<Artplayer, 'on' | 'off' | 'emit' | 'play' | 'pause' | 'template' | 'icons' | 'fullscreen' | 'isDestroy'>
export type Utilities = Pick<typeof Artplayer.utils, 'append' | 'query' | 'setStyle'>
export type Icons = Pick<Artplayer['icons'], 'volume' | 'volumeClose' | 'fullscreenOn' | 'fullscreenOff' | 'loading'>
export type Cleanup = () => void
export interface Result {
name: 'artplayerPluginAds'
skip: Cleanup
pause: Cleanup
play: Cleanup
}
+95
View File
@@ -0,0 +1,95 @@
import type { Resources } from './resources'
import type { Cleanup, Icons, Options, Utilities } from './types'
const translate = (value: number, text: string) => text.replace('%s', String(value))
export function createView(parent: HTMLElement, icons: Icons, option: Options, utils: Utilities, own: (root: HTMLElement) => void) {
const { append, query, setStyle } = utils
// The private markup below fixes the element kind; public append/query retain their broad types.
const root = append(parent, '<div class="artplayer-plugin-ads"></div>') as HTMLElement
own(root)
const content = append(root, option.video
? '<video class="artplayer-plugin-ads-video" loop playsInline></video>'
: `<div class="artplayer-plugin-ads-html">${option.html}</div>`) as HTMLElement
const video = option.video ? content as HTMLVideoElement : null
const loading = append(root, '<div class="artplayer-plugin-ads-loading"></div>') as HTMLElement
append(loading, icons.loading)
const timer = append(root, `<div class="artplayer-plugin-ads-timer">
<div class="artplayer-plugin-ads-close"></div>
<div class="artplayer-plugin-ads-countdown"></div>
</div>`) as HTMLElement
const close = query('.artplayer-plugin-ads-close', timer) as HTMLElement
const countdown = query('.artplayer-plugin-ads-countdown', timer) as HTMLElement
const control = append(root, `<div class="artplayer-plugin-ads-control">
<div class="artplayer-plugin-ads-detail">${option.i18n.detail}</div>
<div class="artplayer-plugin-ads-muted"></div>
<div class="artplayer-plugin-ads-fullscreen"></div>
</div>`) as HTMLElement
const detail = query('.artplayer-plugin-ads-detail', control) as HTMLElement
const muted = query('.artplayer-plugin-ads-muted', control) as HTMLElement
const fullscreen = query('.artplayer-plugin-ads-fullscreen', control) as HTMLElement
let canClose = option.playDuration <= 0
if (option.playDuration >= option.totalDuration)
setStyle(close, 'display', 'none')
if (!option.url)
setStyle(detail, 'display', 'none')
if (video) {
append(muted, icons.volume)
append(muted, icons.volumeClose)
video.muted = Boolean(option.muted)
syncMuted()
}
else {
setStyle(muted, 'display', 'none')
}
append(fullscreen, icons.fullscreenOn)
append(fullscreen, icons.fullscreenOff)
function syncMuted(): void {
setStyle(icons.volume, 'display', video?.muted ? 'none' : 'inline-flex')
setStyle(icons.volumeClose, 'display', video?.muted ? 'inline-flex' : 'none')
}
function render(elapsed: number): void {
const remaining = option.playDuration - elapsed
canClose = elapsed === 0 ? option.playDuration <= 0 : remaining < 1 || Number.isNaN(remaining)
close.innerHTML = canClose ? option.i18n.close : translate(remaining, option.i18n.canBeClosed)
countdown.innerHTML = translate(option.totalDuration - elapsed, option.i18n.countdown)
}
render(0)
return {
root,
video,
render,
ready() {
setStyle(timer, 'display', 'flex')
setStyle(control, 'display', 'flex')
setStyle(loading, 'display', 'none')
},
hide() { setStyle(root, 'display', 'none') },
fullscreen(active: boolean) {
setStyle(icons.fullscreenOn, 'display', active ? 'none' : 'inline-flex')
setStyle(icons.fullscreenOff, 'display', active ? 'inline-flex' : 'none')
},
bind(events: Resources, callbacks: { skip: Cleanup, click: Cleanup, fullscreen: Cleanup }) {
events.dom(close, 'click', () => {
if (canClose)
callbacks.skip()
})
events.dom(content, 'click', callbacks.click)
if (option.url)
events.dom(detail, 'click', callbacks.click)
if (video) {
events.dom(muted, 'click', () => {
video.muted = !video.muted
syncMuted()
})
}
events.dom(fullscreen, 'click', callbacks.fullscreen)
},
}
}
export type View = ReturnType<typeof createView>
@@ -0,0 +1,5 @@
{
"extends": "../../tsconfig.base.json",
"compilerOptions": { "allowJs": false },
"include": ["src/**/*.ts", "types/**/*.d.ts", "../../types/assets.d.ts"]
}
+15
View File
@@ -1,5 +1,20 @@
# Ads 回归测试与维护入口
## 03 源码改造补充
当前源码已按 [包内架构](../packages/artplayer-plugin-ads/ARCHITECTURE.md) 拆为 7 个严格 TS
模块。新增 test/ads-lifecycle.test.js,仅候选实现运行修复断言;test/ads.test.js 的旧
行为与冻结缺陷观察保留,新增动态修改事件配置的兼容检查。三个正式格式也可通过
ARTPLAYER_TEST_ADS 加入同一运行。02 的原始输入哈希/结果不覆盖更新。
浏览器加入实际 4.5.5 核心映射;候选用例覆盖提前 skip、零阈值幂等、真实 destroy/src
释放与两类内部播放拒绝。拒绝用例透明记录 console.warn 的实际参数,仍调用原方法,
不依赖 Firefox 等引擎不同的 console 文本渲染。成功播放仍检查真实像素和时钟。
结构/缺陷差异及验证过程见 [03 记录](changes/2026-09-12-PKG-ADS-03-lifecycle.md)。
03 最终正式三格式 Node 186 项通过;main/legacy 三引擎各 144 项通过;完整 CI 830 项
通过、269 生产 TS 严格检查。对应 [新执行证据](baselines/ads-lifecycle-validation.json)
与以下 02 历史结果分别保留,不能把任何一轮当作全部包的发布证明。
Ads 的实际发布行为以 [1.0.6 归档](baselines/ads-release.json) 为基线;工作区 2.1.0
未发布,其旧 bundle 单独从冻结 Git 提交加载。不要将当前声明里的 source/type 当成
发布 JS 支持的输入;正常广告仍使用 html/video/url,图片通过 HTML img 提供。
+213
View File
@@ -0,0 +1,213 @@
{
"schemaVersion": 1,
"task": "PKG-ADS-03",
"sourceCommit": "069ad4aa",
"release": {
"name": "artplayer",
"version": "4.5.5",
"tarball": "https://registry.npmjs.org/artplayer/-/artplayer-4.5.5.tgz",
"integrity": "sha512-HeAKbe3od9ZgFQVSBCg5FlixJsefDEBC8txVG19yZgvzlcJuiurxPlasap+GHuex/ftG+jmSXnTRtuNrTki2Kw==",
"sha256": "cf867dc73b6dfca9a9e2313ea8e13ed35e5e3adb37b7046fd9e0316bf0ebdb35",
"manifest": {
"license": "MIT",
"name": "artplayer",
"version": "4.5.5",
"main": "dist/artplayer.js",
"types": "types/artplayer.d.ts",
"homepage": "https://artplayer.org",
"author": "Harvey Zack <laozhaochaguan@gmail.com>",
"browserslist": "> 0.5%, last 2 versions, not dead",
"description": "ArtPlayer.js is a modern and full featured HTML5 video player",
"repository": {
"type": "git",
"url": "git+https://github.com/zhw2590582/ArtPlayer.git"
},
"bugs": {
"url": "https://github.com/zhw2590582/ArtPlayer/issues"
},
"keywords": [
"html5",
"video",
"player"
],
"dependencies": {
"option-validator": "^2.0.6",
"screenfull": "^6.0.2"
}
},
"files": {
"package/package.json": "d559f1c2a6b308842517f025553b38b29e7948241458a93fc573533856224eeb",
"package/dist/artplayer.js": "3467e626ca29d2f7884e75eb75b8b90de74a25761ffa2e272576c9b5a98fa0a4",
"package/src/index.js": "8593d5bc9e99bcc53ab8cf8300f8e9d7767b765beaa2ef5c0443cabb42595a58",
"package/src/events/index.js": "d0a68560c887f2beb0ac5b3ff4ff8ef68e5564e3b106c33c4819b1b272cd1493",
"package/src/utils/dom.js": "d82466f14e9f70b6e1e8d153505087e97be51c64c9e6d55d6bd65ded19a6ead4"
}
},
"archiveMembers": [
"package/README.md",
"package/dist/artplayer.js",
"package/examples/react/Artplayer.js",
"package/examples/vue/Artplayer.vue",
"package/package.json",
"package/src/config/index.js",
"package/src/contextmenu/aspectRatio.js",
"package/src/contextmenu/close.js",
"package/src/contextmenu/flip.js",
"package/src/contextmenu/index.js",
"package/src/contextmenu/info.js",
"package/src/contextmenu/playbackRate.js",
"package/src/contextmenu/version.js",
"package/src/control/airplay.js",
"package/src/control/fullscreen.js",
"package/src/control/fullscreenWeb.js",
"package/src/control/index.js",
"package/src/control/loop.js",
"package/src/control/pip.js",
"package/src/control/playAndPause.js",
"package/src/control/progress.js",
"package/src/control/quality.js",
"package/src/control/screenshot.js",
"package/src/control/setting.js",
"package/src/control/thumbnails.js",
"package/src/control/time.js",
"package/src/control/volume.js",
"package/src/events/clickInit.js",
"package/src/events/gestureInit.js",
"package/src/events/hoverInit.js",
"package/src/events/index.js",
"package/src/events/mousemoveInit.js",
"package/src/events/resizeInit.js",
"package/src/events/viewInit.js",
"package/src/hotkey.js",
"package/src/i18n/cs.json",
"package/src/i18n/es.json",
"package/src/i18n/index.js",
"package/src/i18n/pl.json",
"package/src/i18n/zh-cn.json",
"package/src/i18n/zh-tw.json",
"package/src/icons/airplay.svg",
"package/src/icons/arrow-left.svg",
"package/src/icons/arrow-right.svg",
"package/src/icons/aspect-ratio.svg",
"package/src/icons/check.svg",
"package/src/icons/close.svg",
"package/src/icons/config.svg",
"package/src/icons/error.svg",
"package/src/icons/fast-forward.svg",
"package/src/icons/flip.svg",
"package/src/icons/fullscreen-off.svg",
"package/src/icons/fullscreen-on.svg",
"package/src/icons/fullscreen-web-off.svg",
"package/src/icons/fullscreen-web-on.svg",
"package/src/icons/index.js",
"package/src/icons/loading.svg",
"package/src/icons/lock.svg",
"package/src/icons/pause.svg",
"package/src/icons/pip.svg",
"package/src/icons/play.svg",
"package/src/icons/playback-rate.svg",
"package/src/icons/screenshot.svg",
"package/src/icons/setting.svg",
"package/src/icons/state.svg",
"package/src/icons/switch-off.svg",
"package/src/icons/switch-on.svg",
"package/src/icons/unlock.svg",
"package/src/icons/volume-close.svg",
"package/src/icons/volume.svg",
"package/src/index.js",
"package/src/info.js",
"package/src/layer.js",
"package/src/libs/screenfull.js",
"package/src/loading.js",
"package/src/mask.js",
"package/src/mobile.js",
"package/src/notice.js",
"package/src/player/airplayMix.js",
"package/src/player/aspectRatioMix.js",
"package/src/player/attrMix.js",
"package/src/player/autoHeightMix.js",
"package/src/player/autoSizeMix.js",
"package/src/player/currentTimeMix.js",
"package/src/player/durationMix.js",
"package/src/player/eventInit.js",
"package/src/player/flipMix.js",
"package/src/player/fullscreenMix.js",
"package/src/player/fullscreenWebMix.js",
"package/src/player/index.js",
"package/src/player/loadedMix.js",
"package/src/player/loopMix.js",
"package/src/player/miniMix.js",
"package/src/player/normalSizeMix.js",
"package/src/player/optionInit.js",
"package/src/player/pauseMix.js",
"package/src/player/pipMix.js",
"package/src/player/playMix.js",
"package/src/player/playbackRateMix.js",
"package/src/player/playedMix.js",
"package/src/player/playingMix.js",
"package/src/player/posterMix.js",
"package/src/player/rectMix.js",
"package/src/player/screenshotMix.js",
"package/src/player/seekMix.js",
"package/src/player/subtitleOffsetMix.js",
"package/src/player/switchMix.js",
"package/src/player/themeMix.js",
"package/src/player/titleMix.js",
"package/src/player/toggleMix.js",
"package/src/player/urlMix.js",
"package/src/player/volumeMix.js",
"package/src/plugins/autoOrientation.js",
"package/src/plugins/autoPlayback.js",
"package/src/plugins/fastForward.js",
"package/src/plugins/index.js",
"package/src/plugins/lock.js",
"package/src/plugins/miniProgressBar.js",
"package/src/scheme/index.js",
"package/src/setting/aspectRatio.js",
"package/src/setting/flip.js",
"package/src/setting/index.js",
"package/src/setting/playbackRate.js",
"package/src/setting/subtitleOffset.js",
"package/src/storage.js",
"package/src/style/artplayer.less",
"package/src/style/aspectRatio.less",
"package/src/style/autoSize.less",
"package/src/style/bottom.less",
"package/src/style/contextmenu.less",
"package/src/style/danmuku.less",
"package/src/style/flip.less",
"package/src/style/fullscreenWeb.less",
"package/src/style/hideCursor.less",
"package/src/style/hint.less",
"package/src/style/index.less",
"package/src/style/info.less",
"package/src/style/layers.less",
"package/src/style/loading.less",
"package/src/style/mask.less",
"package/src/style/mini.less",
"package/src/style/notice.less",
"package/src/style/plugins.less",
"package/src/style/poster.less",
"package/src/style/selector.less",
"package/src/style/setting.less",
"package/src/style/subtitle.less",
"package/src/style/video.less",
"package/src/subtitle.js",
"package/src/template.js",
"package/src/utils/compatibility.js",
"package/src/utils/component.js",
"package/src/utils/dom.js",
"package/src/utils/emitter.js",
"package/src/utils/error.js",
"package/src/utils/file.js",
"package/src/utils/format.js",
"package/src/utils/index.js",
"package/src/utils/property.js",
"package/src/utils/subtitle.js",
"package/src/utils/time.js",
"package/src/whitelist.js",
"package/tsconfig.json",
"package/types/artplayer.d.ts"
],
"note": "Actual core associated with Ads 1.0.6 source. Full archive is integrity-checked; selected runtime/manifest/source members are separately fingerprinted. No inference of all 4.x support."
}
@@ -0,0 +1,893 @@
{
"schemaVersion": 1,
"task": "PKG-ADS-03",
"sourceCommit": "069ad4aa",
"production": [
{
"file": "packages/artplayer-plugin-ads/src/countdown.ts",
"sha256LF": "7dab796a3d8fb757da7f87e4396403f0687af52d0876cf74ebb2b8db7c2e752a"
},
{
"file": "packages/artplayer-plugin-ads/src/index.ts",
"sha256LF": "f4fd3eaa7ee0ba638b4bd324305cc496dbb0a5ecd7bf3fac7745f6fcf2ac2978"
},
{
"file": "packages/artplayer-plugin-ads/src/options.ts",
"sha256LF": "fc49c6a73c1563c05067566e243679b42f8cec3e5365fde9873126b1e16af964"
},
{
"file": "packages/artplayer-plugin-ads/src/resources.ts",
"sha256LF": "ba0d5b5f25d79929a39a9cda6b80b9e60a2b746e20312aa658d84fbb9be6562f"
},
{
"file": "packages/artplayer-plugin-ads/src/session.ts",
"sha256LF": "59540b169b7bb348ddd1d773368717cbd6c4d3b1b28305c1c759fea6aef8d827"
},
{
"file": "packages/artplayer-plugin-ads/src/style.less",
"sha256LF": "0eeed3f29ccafc6836f5bc4338f58f784edd114d2b100fda7fc395329031a406"
},
{
"file": "packages/artplayer-plugin-ads/src/types.ts",
"sha256LF": "87fab15723a3eb596d12a22d737d4127d06dd4c45604f9888efe7557fb64f6e0"
},
{
"file": "packages/artplayer-plugin-ads/src/view.ts",
"sha256LF": "7d2cba7a3e20ce02288627723ebc9256d61cee313c3a7d00a31373b7f250eb91"
}
],
"inputs": [
{
"file": "package.json",
"sha256LF": "135e26a113b36ae6b0144abd969649884dd1e2e171125c5958d25eb8b618764e"
},
{
"file": "packages/artplayer-plugin-ads/tsconfig.json",
"sha256LF": "5b53a2bb158e81c5bfd3e2b50e750f7f6e20d0e9aaa6733facec036c15f28ebe"
},
{
"file": "packages/artplayer-plugin-ads/.npmignore",
"sha256LF": "145c5fa0341195e21cf13df3423a057282f8ea374e9a1b3065e2280e1ce1c5c1"
},
{
"file": "test/ads.test.js",
"sha256LF": "918e2c743142af281adb86f55084a1a6421aa3bdbc60e851e14859e3f8b48f50"
},
{
"file": "test/ads-lifecycle.test.js",
"sha256LF": "992b8b786b68735a9472ae26ad0c27306b00292cba28d8d744de9318279107de"
},
{
"file": "test/helpers/ads.js",
"sha256LF": "89f6a65006810813be7b3d60d35837241c2076470131f29d2291ba0a29fae387"
},
{
"file": "test/browser/ads.spec.js",
"sha256LF": "f0a52956e5d7b8a81f3cfef6b52d6efb01f8fe78f6c63563c596500b2bb5dd80"
},
{
"file": "test/browser/server.mjs",
"sha256LF": "d475bef6e13682f1f2445cdd3b1a01e6e27c469c5c1f21de0286969d38cd1a26"
},
{
"file": "refactor/baselines/ads-core.json",
"sha256LF": "5f678676e2e6307bf829912880ae112eff5e46b8a67b7731f0c5a77929f883a9"
},
{
"file": "refactor/scripts/ads-core.test.mjs",
"sha256LF": "514b446c5dd42dc3c7b7230699fde45915a64bc5b8d2fc4799f1fc6b66f14516"
}
],
"artifacts": [
{
"file": "packages/artplayer-plugin-ads/dist/artplayer-plugin-ads.js",
"sha256": "dd36ba43be92b156f6a0476be7924300151f21542f493b18f4dd1ca7eaa01446",
"bytes": 7236,
"copy": {
"file": "docs/compiled/artplayer-plugin-ads.js",
"sha256": "dd36ba43be92b156f6a0476be7924300151f21542f493b18f4dd1ca7eaa01446",
"bytes": 7236
}
},
{
"file": "packages/artplayer-plugin-ads/dist/artplayer-plugin-ads.legacy.js",
"sha256": "1d27be200f522ab80db9562672e531306fbc9f6de4b4a6f311824cf6c4af4cfe",
"bytes": 7675,
"copy": {
"file": "docs/compiled/artplayer-plugin-ads.legacy.js",
"sha256": "1d27be200f522ab80db9562672e531306fbc9f6de4b4a6f311824cf6c4af4cfe",
"bytes": 7675
}
},
{
"file": "packages/artplayer-plugin-ads/dist/artplayer-plugin-ads.mjs",
"sha256": "0e3cc7f1cd527e268a50a7a2126df37a334d26f180fd6e5561f7f7595cfbd0e7",
"bytes": 12990,
"copy": {
"file": "docs/compiled/artplayer-plugin-ads.mjs",
"sha256": "0e3cc7f1cd527e268a50a7a2126df37a334d26f180fd6e5561f7f7595cfbd0e7",
"bytes": 12990
}
}
],
"checks": [
{
"file": "refactor/.cache/ads03-before.log",
"sha256": "814f35b7ce294893bb1fa0240fbf45cf6ce32b2db9facab20ba7002df2b9134f",
"bytes": 36247,
"passed": 0,
"failed": 10,
"note": "First ten candidate regressions on pre-refactor source."
},
{
"file": "refactor/.cache/ads03-first.log",
"sha256": "b7ead3b69d388ee384baf12c7371caf6ec462761568e13bc0d68fb813929432e",
"bytes": 41374,
"passed": 67,
"failed": 1,
"note": "Initial source: controlled Node fixture lacked native remove; strict TS also required a callback-aware state reader."
},
{
"file": "refactor/.cache/ads03-reentry-before.log",
"sha256": "6587b1ec86b2b6238fb0d30f57703ab382a26948c09cf315fe691644bb587b1f",
"bytes": 2967,
"passed": 19,
"failed": 1,
"note": "New synchronous destroy during ad pause regression; repaired before final production build."
},
{
"file": "refactor/.cache/ads03-artifacts-node-first.log",
"sha256": "c0e310174cabe60ecb1be75ea9804c70dd039927504cc5b96c912d97f9dea66b",
"bytes": 23947,
"passed": 182,
"failed": 4,
"note": "VM cross-realm Promise assimilation took more than two microtasks; changed only the test drain."
},
{
"file": "refactor/.cache/ads03-artifacts-node.log",
"sha256": "a1f1d696a3b6f0c6ee255f02536254ba49786dd6e0a879c037bb6f37634af602",
"bytes": 20903,
"passed": 186,
"failed": 0,
"note": "Source plus all three final artifacts and both frozen implementations; public contracts and candidate lifecycle."
},
{
"file": "refactor/.cache/ads03-build.log",
"sha256": "d036d8139d6d4607994a3adc75a90eb9b3602400db91d661eca5467ee952f00f",
"bytes": 1438
}
],
"browser": [
{
"name": "shared",
"note": "Historical 4.5.5 fixture incorrectly used the newer art.video alias; all reports retained.",
"stats": {
"startTime": "2026-09-11T23:37:45.302Z",
"duration": 146339.709,
"expected": 66,
"skipped": 0,
"unexpected": 33,
"flaky": 0
},
"report": {
"file": "refactor/.cache/ads03-browser-shared-report.json",
"sha256": "4ffd4196b6173f78eaf80625096207f4fc9ad47e321c2adad114dd9422feddfd",
"bytes": 7714476
},
"log": {
"file": "refactor/.cache/ads03-browser-shared.log",
"sha256": "aa0b3989a8887a9c37a732cc704e9be7fbdb8fba18cd19688f3a96b0e3e3200c",
"bytes": 108330
},
"results": [
{
"file": "refactor/.cache/ads03-browser-shared-results/.last-run.json",
"sha256": "90dc2c1cf9cf3f75bf81622bdd7faff122fe2aeb834fa26bced10cae2219ea57",
"bytes": 1664
},
{
"file": "refactor/.cache/ads03-browser-shared-results/ads-published-4-5-5-core-c-1e3a2-acement-does-not-replay-ads-chromium/error-context.md",
"sha256": "ef5244b38edf71430c0b90e6ff1c743e299a54201880682060230d40812aba35",
"bytes": 776
},
{
"file": "refactor/.cache/ads03-browser-shared-results/ads-published-4-5-5-core-c-1e3a2-acement-does-not-replay-ads-chromium/test-failed-1.png",
"sha256": "4b6d3264c2a48cce81c5687dbd0a4171be5e71927f153a29f063f64502a3a39a",
"bytes": 79563
},
{
"file": "refactor/.cache/ads03-browser-shared-results/ads-published-4-5-5-core-c-1e3a2-acement-does-not-replay-ads-chromium/trace.zip",
"sha256": "9be3d8ee1155550d26b65647e8cc32ca47b8cb5611f8bc0d92a014bb0494066b",
"bytes": 996043
},
{
"file": "refactor/.cache/ads03-browser-shared-results/ads-published-4-5-5-core-c-1e3a2-acement-does-not-replay-ads-firefox/error-context.md",
"sha256": "7871833d9a264ef0dcd4ffbbe6078d69d604170240b85590331f555b2159f28e",
"bytes": 736
},
{
"file": "refactor/.cache/ads03-browser-shared-results/ads-published-4-5-5-core-c-1e3a2-acement-does-not-replay-ads-firefox/test-failed-1.png",
"sha256": "5838a1ed3f9ebfc277a128ffd72f94f817c05654f5c9d01fd35e97cacd52339c",
"bytes": 110398
},
{
"file": "refactor/.cache/ads03-browser-shared-results/ads-published-4-5-5-core-c-1e3a2-acement-does-not-replay-ads-firefox/trace.zip",
"sha256": "017fb7b8361fc907916dede7eeaaa0076a94cc54668998e54a46dfb8e2dc061a",
"bytes": 986398
},
{
"file": "refactor/.cache/ads03-browser-shared-results/ads-published-4-5-5-core-c-1e3a2-acement-does-not-replay-ads-webkit/error-context.md",
"sha256": "2dd0ad6dcb2b02fdf0bc7b0c208adffcfc62bf1eaca1bf0a8ed0f8588cdbac9b",
"bytes": 9111
},
{
"file": "refactor/.cache/ads03-browser-shared-results/ads-published-4-5-5-core-c-1e3a2-acement-does-not-replay-ads-webkit/test-failed-1.png",
"sha256": "0dbfa9bf58a269c1b5cbc322ad359afa65f0bdb1846de179a2d7fa5f84ce09fa",
"bytes": 274748
},
{
"file": "refactor/.cache/ads03-browser-shared-results/ads-published-4-5-5-core-c-1e3a2-acement-does-not-replay-ads-webkit/trace.zip",
"sha256": "e2b71a13c82da7ea3f3321c0802366f6173f40305091499ea01b1b08a6b5ef6b",
"bytes": 591985
},
{
"file": "refactor/.cache/ads03-browser-shared-results/ads-published-4-5-5-core-c-2b017-reshold-and-resumed-content-chromium/error-context.md",
"sha256": "d8e09a89765ee3943dd824072fbccb5e92df69f549673b5aa110591730390f94",
"bytes": 1586
},
{
"file": "refactor/.cache/ads03-browser-shared-results/ads-published-4-5-5-core-c-2b017-reshold-and-resumed-content-chromium/test-failed-1.png",
"sha256": "84b9477647c08e7834233e683b9388fabbf12cd8a6bf11429a24e87e46315c82",
"bytes": 39173
},
{
"file": "refactor/.cache/ads03-browser-shared-results/ads-published-4-5-5-core-c-2b017-reshold-and-resumed-content-chromium/trace.zip",
"sha256": "8dfd1f75d6120adf3421203bdf66f182dee685c3d34d060cccc0771c2c4ea4dc",
"bytes": 227009
},
{
"file": "refactor/.cache/ads03-browser-shared-results/ads-published-4-5-5-core-c-2b017-reshold-and-resumed-content-firefox/error-context.md",
"sha256": "559a1b90954d5c99407c33726c353517e0d0955ff179849d4f5b05ffad044f63",
"bytes": 1506
},
{
"file": "refactor/.cache/ads03-browser-shared-results/ads-published-4-5-5-core-c-2b017-reshold-and-resumed-content-firefox/test-failed-1.png",
"sha256": "1305c8f416bdc660c918a6eb2b7e1aa62f9aa3d0571bec5489d417d30c564cec",
"bytes": 44317
},
{
"file": "refactor/.cache/ads03-browser-shared-results/ads-published-4-5-5-core-c-2b017-reshold-and-resumed-content-firefox/trace.zip",
"sha256": "d9e3d9eb69bd5a41bee575803df36ae2879761a31d627cf03ab13d400188e352",
"bytes": 563304
},
{
"file": "refactor/.cache/ads03-browser-shared-results/ads-published-4-5-5-core-c-2b017-reshold-and-resumed-content-webkit/error-context.md",
"sha256": "a5ec00c127ea6ed0e601659c17468a75cf98616e7b02a21eebec569f21b6001e",
"bytes": 9770
},
{
"file": "refactor/.cache/ads03-browser-shared-results/ads-published-4-5-5-core-c-2b017-reshold-and-resumed-content-webkit/test-failed-1.png",
"sha256": "ed9d1018d409dc75f3513bddc1f2156bc05b595fb504204626547a5a7d216900",
"bytes": 102016
},
{
"file": "refactor/.cache/ads03-browser-shared-results/ads-published-4-5-5-core-c-2b017-reshold-and-resumed-content-webkit/trace.zip",
"sha256": "b447f96893c5fadf3ad40851c4a626d6398c34e103d992118e3bb31e62b7b743",
"bytes": 201624
},
{
"file": "refactor/.cache/ads03-browser-shared-results/ads-published-4-5-5-core-c-c88d1-ls-open-to-playable-content-chromium/error-context.md",
"sha256": "e20426126d5cb3b7df68debd78d40cf410031e35498a1174177d53bd1ccbe06a",
"bytes": 1041
},
{
"file": "refactor/.cache/ads03-browser-shared-results/ads-published-4-5-5-core-c-c88d1-ls-open-to-playable-content-chromium/test-failed-1.png",
"sha256": "24e7c007f06bd62468312b3febc7fd03392176d20472de6d693b0648b9c46b1b",
"bytes": 83698
},
{
"file": "refactor/.cache/ads03-browser-shared-results/ads-published-4-5-5-core-c-c88d1-ls-open-to-playable-content-chromium/trace.zip",
"sha256": "4a7ca4e36c068c744f419de1f2f467152b89670348eb92d24f871297a9eaa1b7",
"bytes": 449038
},
{
"file": "refactor/.cache/ads03-browser-shared-results/ads-published-4-5-5-core-c-c88d1-ls-open-to-playable-content-firefox/error-context.md",
"sha256": "ae4199a32dadc92e6a762efe1716302f37dbed76debe803ae0e33ad959b312e1",
"bytes": 1001
},
{
"file": "refactor/.cache/ads03-browser-shared-results/ads-published-4-5-5-core-c-c88d1-ls-open-to-playable-content-firefox/test-failed-1.png",
"sha256": "d483027341c8b3eea0b43f28265f8b6ec7c519af01766e6043f3a75f7b7297b5",
"bytes": 117412
},
{
"file": "refactor/.cache/ads03-browser-shared-results/ads-published-4-5-5-core-c-c88d1-ls-open-to-playable-content-firefox/trace.zip",
"sha256": "e974471baaf967acc8479f1e58c5ef6dc1ebce34e920a5f0bd5511c9ae78a4dc",
"bytes": 730455
},
{
"file": "refactor/.cache/ads03-browser-shared-results/ads-published-4-5-5-core-c-c88d1-ls-open-to-playable-content-webkit/error-context.md",
"sha256": "59951fa0c234973d34933462389273e87ab868c542d445c3f3b98dddc2045207",
"bytes": 9376
},
{
"file": "refactor/.cache/ads03-browser-shared-results/ads-published-4-5-5-core-c-c88d1-ls-open-to-playable-content-webkit/test-failed-1.png",
"sha256": "e9bb3ca915c9305e670be1a4401e661150471985002805fc9f5d0eaad5ed74b5",
"bytes": 296764
},
{
"file": "refactor/.cache/ads03-browser-shared-results/ads-published-4-5-5-core-c-c88d1-ls-open-to-playable-content-webkit/trace.zip",
"sha256": "09689fcea6be8ebfbef20f510a083257f89e77bb5a8acd18b24863b0960c2353",
"bytes": 428187
},
{
"file": "refactor/.cache/ads03-browser-shared-results/ads-published-4-5-5-core-c-da607-completion-restores-content-chromium/error-context.md",
"sha256": "725ae57c92da4c558cd8cb42a555accd2af68c03f1d34178424ec36bb38656a7",
"bytes": 1060
},
{
"file": "refactor/.cache/ads03-browser-shared-results/ads-published-4-5-5-core-c-da607-completion-restores-content-chromium/test-failed-1.png",
"sha256": "7f3e5854a32a1fcf19a6435355021033914b455a81682b11e064ef84e00991ae",
"bytes": 83249
},
{
"file": "refactor/.cache/ads03-browser-shared-results/ads-published-4-5-5-core-c-da607-completion-restores-content-chromium/trace.zip",
"sha256": "c65d0620423417e248f24fa16785e6f566d1514fb8953a58e0e71b2ab9e360fb",
"bytes": 990100
},
{
"file": "refactor/.cache/ads03-browser-shared-results/ads-published-4-5-5-core-c-da607-completion-restores-content-firefox/error-context.md",
"sha256": "c3309d51f1b0e08bf38f54db1c440af62c6940b53e478f9b0098e0ee4bd122e4",
"bytes": 1020
},
{
"file": "refactor/.cache/ads03-browser-shared-results/ads-published-4-5-5-core-c-da607-completion-restores-content-firefox/test-failed-1.png",
"sha256": "863546a34e733534f5b2078d698e246b52bb3133823b20ef75f630a318d0c030",
"bytes": 116926
},
{
"file": "refactor/.cache/ads03-browser-shared-results/ads-published-4-5-5-core-c-da607-completion-restores-content-firefox/trace.zip",
"sha256": "c939ae28160d6827c628cfacc41e6ff3800e4b70337795897a0d8b7281149abf",
"bytes": 1433757
},
{
"file": "refactor/.cache/ads03-browser-shared-results/ads-published-4-5-5-core-c-da607-completion-restores-content-webkit/error-context.md",
"sha256": "7e6c7f3eea4cc00f147bfa708014f131c8ff5c27886643b681d84877109e9da5",
"bytes": 9395
},
{
"file": "refactor/.cache/ads03-browser-shared-results/ads-published-4-5-5-core-c-da607-completion-restores-content-webkit/test-failed-1.png",
"sha256": "53467d0e5479a3079bdb133cc84203070a2700cf3c7a57222fc9243f42ae4724",
"bytes": 292303
},
{
"file": "refactor/.cache/ads03-browser-shared-results/ads-published-4-5-5-core-c-da607-completion-restores-content-webkit/trace.zip",
"sha256": "5f4497f4616d0cc64176428ac08f1f6beb57674199c3a133bc68555c4f1778b0",
"bytes": 1144671
},
{
"file": "refactor/.cache/ads03-browser-shared-results/ads-published-4-5-5-core-p-03f26-reshold-and-resumed-content-chromium/error-context.md",
"sha256": "474373de10ecfe0c612ba4e340975429cb211c7c7496092fc155df9723e0d952",
"bytes": 1586
},
{
"file": "refactor/.cache/ads03-browser-shared-results/ads-published-4-5-5-core-p-03f26-reshold-and-resumed-content-chromium/test-failed-1.png",
"sha256": "84b9477647c08e7834233e683b9388fabbf12cd8a6bf11429a24e87e46315c82",
"bytes": 39173
},
{
"file": "refactor/.cache/ads03-browser-shared-results/ads-published-4-5-5-core-p-03f26-reshold-and-resumed-content-chromium/trace.zip",
"sha256": "f3114ce8c2a303d44eda29ba4bbb95b6b6e47a9c58faea2926ac9cad18df71f2",
"bytes": 261124
},
{
"file": "refactor/.cache/ads03-browser-shared-results/ads-published-4-5-5-core-p-03f26-reshold-and-resumed-content-firefox/error-context.md",
"sha256": "91442b03b1b1c327ead14fa6ba4ede1b79a9a52fa272286e8a0aadc52728560e",
"bytes": 1506
},
{
"file": "refactor/.cache/ads03-browser-shared-results/ads-published-4-5-5-core-p-03f26-reshold-and-resumed-content-firefox/test-failed-1.png",
"sha256": "1305c8f416bdc660c918a6eb2b7e1aa62f9aa3d0571bec5489d417d30c564cec",
"bytes": 44317
},
{
"file": "refactor/.cache/ads03-browser-shared-results/ads-published-4-5-5-core-p-03f26-reshold-and-resumed-content-firefox/trace.zip",
"sha256": "066e77efd29e85eec24517b2a0d4cc6cb514bed4f680e4617c0e87aad12771f4",
"bytes": 632240
},
{
"file": "refactor/.cache/ads03-browser-shared-results/ads-published-4-5-5-core-p-03f26-reshold-and-resumed-content-webkit/error-context.md",
"sha256": "25b31129cf5a72dcad9427021cf836a7c488b7b10cdeeffa002659017ac83367",
"bytes": 9770
},
{
"file": "refactor/.cache/ads03-browser-shared-results/ads-published-4-5-5-core-p-03f26-reshold-and-resumed-content-webkit/test-failed-1.png",
"sha256": "ed9d1018d409dc75f3513bddc1f2156bc05b595fb504204626547a5a7d216900",
"bytes": 102016
},
{
"file": "refactor/.cache/ads03-browser-shared-results/ads-published-4-5-5-core-p-03f26-reshold-and-resumed-content-webkit/trace.zip",
"sha256": "b401ff38930495c734204df145504d703c34e09edd5ce296907321935d59df5c",
"bytes": 218759
},
{
"file": "refactor/.cache/ads03-browser-shared-results/ads-published-4-5-5-core-p-656b5-ides-ad-and-leaks-rejection-chromium/error-context.md",
"sha256": "1381e5cacc8fa22c9284ab08c509fce398d06025f511bdf9728ce72114571dce",
"bytes": 1052
},
{
"file": "refactor/.cache/ads03-browser-shared-results/ads-published-4-5-5-core-p-656b5-ides-ad-and-leaks-rejection-chromium/test-failed-1.png",
"sha256": "735a0cec743607f507b1867a8950b4355911a1bc2f921dc528b7f18bb90d220d",
"bytes": 87860
},
{
"file": "refactor/.cache/ads03-browser-shared-results/ads-published-4-5-5-core-p-656b5-ides-ad-and-leaks-rejection-chromium/trace.zip",
"sha256": "6157ee7bbeb711c8383d6446041f6996a1319f797acd2760c7e48157401d48d4",
"bytes": 1798906
},
{
"file": "refactor/.cache/ads03-browser-shared-results/ads-published-4-5-5-core-p-656b5-ides-ad-and-leaks-rejection-firefox/error-context.md",
"sha256": "75434d727d466dd2039012c6dbbf7911e5b61edd980bd1e37d006cb21d50b7c9",
"bytes": 1012
},
{
"file": "refactor/.cache/ads03-browser-shared-results/ads-published-4-5-5-core-p-656b5-ides-ad-and-leaks-rejection-firefox/test-failed-1.png",
"sha256": "5db8a1e7b06fcbee03f9565bdf47e54e6a947bec8428dc86373e49ef7a08f72b",
"bytes": 120538
},
{
"file": "refactor/.cache/ads03-browser-shared-results/ads-published-4-5-5-core-p-656b5-ides-ad-and-leaks-rejection-firefox/trace.zip",
"sha256": "694f76bd0b69461142e1cec9cded3a15f0104aa2adbeacf44c14a52d8944ee13",
"bytes": 1841850
},
{
"file": "refactor/.cache/ads03-browser-shared-results/ads-published-4-5-5-core-p-656b5-ides-ad-and-leaks-rejection-webkit/error-context.md",
"sha256": "7438e7961c27d09cc8d467ed4de0353438eb516fe1916ae1bc57ba3777cc75eb",
"bytes": 9387
},
{
"file": "refactor/.cache/ads03-browser-shared-results/ads-published-4-5-5-core-p-656b5-ides-ad-and-leaks-rejection-webkit/test-failed-1.png",
"sha256": "95e60e1b5079a2b8d6781739d0a07e6ee2707291afc206f174e070e652e0c4e3",
"bytes": 298847
},
{
"file": "refactor/.cache/ads03-browser-shared-results/ads-published-4-5-5-core-p-656b5-ides-ad-and-leaks-rejection-webkit/trace.zip",
"sha256": "6d8398abafef162b4e7c09995590bfc5b7f0e9b5623d710aa2f1196c00e6bdf0",
"bytes": 1124496
},
{
"file": "refactor/.cache/ads03-browser-shared-results/ads-published-4-5-5-core-p-80f2b-skip-before-accepting-click-chromium/error-context.md",
"sha256": "e6a1ebb88deaa3ec5e8fae581eefd17c1b972c1048bdeda9c517913ed04a801a",
"bytes": 1051
},
{
"file": "refactor/.cache/ads03-browser-shared-results/ads-published-4-5-5-core-p-80f2b-skip-before-accepting-click-chromium/test-failed-1.png",
"sha256": "0f3ddf737ab3f65b9a75eb0cf33f1b284b09b4d139c82b7d184661383c8fce18",
"bytes": 78490
},
{
"file": "refactor/.cache/ads03-browser-shared-results/ads-published-4-5-5-core-p-80f2b-skip-before-accepting-click-chromium/trace.zip",
"sha256": "9f7476df6927de06ff6f8730a47192c4d41b0c2d1fba79659cda6134d9e43d8b",
"bytes": 260402
},
{
"file": "refactor/.cache/ads03-browser-shared-results/ads-published-4-5-5-core-p-80f2b-skip-before-accepting-click-firefox/error-context.md",
"sha256": "071d0912347be755bd6a0c10ecdb9b8427f9bc0d6290fd32c9b3b90d4926972d",
"bytes": 1011
},
{
"file": "refactor/.cache/ads03-browser-shared-results/ads-published-4-5-5-core-p-80f2b-skip-before-accepting-click-firefox/test-failed-1.png",
"sha256": "bfd97ba43d66ef7efb4d668e67d7b32dfbf0a59b81511624bd7bdfa710670850",
"bytes": 113846
},
{
"file": "refactor/.cache/ads03-browser-shared-results/ads-published-4-5-5-core-p-80f2b-skip-before-accepting-click-firefox/trace.zip",
"sha256": "a254caf91c161d477458938a0a55dc9473efd6ed4f61860f0cc55829e34b91b5",
"bytes": 627188
},
{
"file": "refactor/.cache/ads03-browser-shared-results/ads-published-4-5-5-core-p-80f2b-skip-before-accepting-click-webkit/error-context.md",
"sha256": "3753e5223e183391bdd1e64b5eecfa91913137d0fa867d767eec81b96dc0c9fb",
"bytes": 9386
},
{
"file": "refactor/.cache/ads03-browser-shared-results/ads-published-4-5-5-core-p-80f2b-skip-before-accepting-click-webkit/test-failed-1.png",
"sha256": "b683cb6356f72b2da1663ed013314fafeede71567092354bbb813a93733630ec",
"bytes": 285997
},
{
"file": "refactor/.cache/ads03-browser-shared-results/ads-published-4-5-5-core-p-80f2b-skip-before-accepting-click-webkit/trace.zip",
"sha256": "6fc3c47ae13f0d1ff055efc9f4807892ef43b238a4978c7dc518975819344a44",
"bytes": 376820
},
{
"file": "refactor/.cache/ads03-browser-shared-results/ads-published-4-5-5-core-p-acaa7-acement-does-not-replay-ads-chromium/error-context.md",
"sha256": "ca2aaa7a2287486729730e5b3da91a95a2d5f4f052dc384c64ca077a87f97454",
"bytes": 776
},
{
"file": "refactor/.cache/ads03-browser-shared-results/ads-published-4-5-5-core-p-acaa7-acement-does-not-replay-ads-chromium/test-failed-1.png",
"sha256": "c845edd149dd22c95b4b35306d539a5be66814a91d73b095399e2a714ea711dc",
"bytes": 79244
},
{
"file": "refactor/.cache/ads03-browser-shared-results/ads-published-4-5-5-core-p-acaa7-acement-does-not-replay-ads-chromium/trace.zip",
"sha256": "fe1d50163e2e23c793b20ea613d5758810875b50b4e57d98e765acb9a0377dab",
"bytes": 1002871
},
{
"file": "refactor/.cache/ads03-browser-shared-results/ads-published-4-5-5-core-p-acaa7-acement-does-not-replay-ads-firefox/error-context.md",
"sha256": "76f4f46789ab9f41ccd95e98be82b71655dbfe9d8a3c6a7b7b0d3a14fd0c9c16",
"bytes": 736
},
{
"file": "refactor/.cache/ads03-browser-shared-results/ads-published-4-5-5-core-p-acaa7-acement-does-not-replay-ads-firefox/test-failed-1.png",
"sha256": "abb3c79c6c686a9ea02b7ade660e726570f8365e48db2117d1aeaa111858ea5b",
"bytes": 111130
},
{
"file": "refactor/.cache/ads03-browser-shared-results/ads-published-4-5-5-core-p-acaa7-acement-does-not-replay-ads-firefox/trace.zip",
"sha256": "136cc4518b14769b612212a84fb8ba5dbb6316711b27a91877023b1a92fa4dc9",
"bytes": 1017446
},
{
"file": "refactor/.cache/ads03-browser-shared-results/ads-published-4-5-5-core-p-acaa7-acement-does-not-replay-ads-webkit/error-context.md",
"sha256": "a7ebf7c5d9b11a7d4c80578763c70ba98dce0c180f7c0c0f202d0231a79cbce7",
"bytes": 9111
},
{
"file": "refactor/.cache/ads03-browser-shared-results/ads-published-4-5-5-core-p-acaa7-acement-does-not-replay-ads-webkit/test-failed-1.png",
"sha256": "0dbfa9bf58a269c1b5cbc322ad359afa65f0bdb1846de179a2d7fa5f84ce09fa",
"bytes": 274748
},
{
"file": "refactor/.cache/ads03-browser-shared-results/ads-published-4-5-5-core-p-acaa7-acement-does-not-replay-ads-webkit/trace.zip",
"sha256": "da3644df7827d24ef08faf7cde5b3a47419a3cd4ca5184a30f3cd3ce71287b50",
"bytes": 557989
},
{
"file": "refactor/.cache/ads03-browser-shared-results/ads-published-4-5-5-core-p-c07a9-ction-while-timer-continues-chromium/error-context.md",
"sha256": "dfaa242162bd86466001b172ccfba30125394c64096b323430468e1079da3af7",
"bytes": 1054
},
{
"file": "refactor/.cache/ads03-browser-shared-results/ads-published-4-5-5-core-p-c07a9-ction-while-timer-continues-chromium/test-failed-1.png",
"sha256": "6dda9159fcf611dad32d36d218ac7c09386ddf75a808cd7e1c42ebe4070083b6",
"bytes": 83727
},
{
"file": "refactor/.cache/ads03-browser-shared-results/ads-published-4-5-5-core-p-c07a9-ction-while-timer-continues-chromium/trace.zip",
"sha256": "b9df6fa13affc44a7a75de205d35ae91422191c21f5b361f6d2828f41f91bbb6",
"bytes": 311510
},
{
"file": "refactor/.cache/ads03-browser-shared-results/ads-published-4-5-5-core-p-c07a9-ction-while-timer-continues-firefox/error-context.md",
"sha256": "2efe6713bd57b1d014c023451d956241eb9ab99b7f9073b4c6ed95ddf281d448",
"bytes": 1014
},
{
"file": "refactor/.cache/ads03-browser-shared-results/ads-published-4-5-5-core-p-c07a9-ction-while-timer-continues-firefox/test-failed-1.png",
"sha256": "3f206be256985c9c5dab216bd29bb5f756e68150a87519e23040381f8eaf2e94",
"bytes": 117115
},
{
"file": "refactor/.cache/ads03-browser-shared-results/ads-published-4-5-5-core-p-c07a9-ction-while-timer-continues-firefox/trace.zip",
"sha256": "8942b1f772e0ea8b9f02b7ea0b31ff36af301083adff8bdfaa5799289eee9242",
"bytes": 871673
},
{
"file": "refactor/.cache/ads03-browser-shared-results/ads-published-4-5-5-core-p-c07a9-ction-while-timer-continues-webkit/error-context.md",
"sha256": "3359cc703f59649c19828ce59812a2fbe084dacffff1c59df538a6fdbf4f4bf7",
"bytes": 9107
},
{
"file": "refactor/.cache/ads03-browser-shared-results/ads-published-4-5-5-core-p-c07a9-ction-while-timer-continues-webkit/test-failed-1.png",
"sha256": "2b48187e2521ca1a1f9b3a3ca38ed1338f5f29e6a52935dc8aa1724d1304f4d0",
"bytes": 274693
},
{
"file": "refactor/.cache/ads03-browser-shared-results/ads-published-4-5-5-core-p-c07a9-ction-while-timer-continues-webkit/trace.zip",
"sha256": "e26c568b9f5aa3040313ab553a62000d3ccd31e9fa26bdf7a060ca3d4c26c190",
"bytes": 475883
},
{
"file": "refactor/.cache/ads03-browser-shared-results/ads-published-4-5-5-core-p-dcb98-ls-open-to-playable-content-chromium/error-context.md",
"sha256": "dd120a04a6c9d1e57510e100cb430d74ef493e341eb91066e2673ef2b5f13a60",
"bytes": 1041
},
{
"file": "refactor/.cache/ads03-browser-shared-results/ads-published-4-5-5-core-p-dcb98-ls-open-to-playable-content-chromium/test-failed-1.png",
"sha256": "f671f716475a8fca5079a19ba84e00b56cadd47ca753b58994cdd66aa8f24ffa",
"bytes": 84342
},
{
"file": "refactor/.cache/ads03-browser-shared-results/ads-published-4-5-5-core-p-dcb98-ls-open-to-playable-content-chromium/trace.zip",
"sha256": "10b3140dba2ac710b1901dc99c808ebaaa3992c833f51713d427f38f41795cbc",
"bytes": 467915
},
{
"file": "refactor/.cache/ads03-browser-shared-results/ads-published-4-5-5-core-p-dcb98-ls-open-to-playable-content-firefox/error-context.md",
"sha256": "f3c4253b907c8f08626339cbf218e9605d2a504c7413482e77af15788c4d6384",
"bytes": 1001
},
{
"file": "refactor/.cache/ads03-browser-shared-results/ads-published-4-5-5-core-p-dcb98-ls-open-to-playable-content-firefox/test-failed-1.png",
"sha256": "6d30ee0f6c41e920b0b4964e6b7c05f8aac29b97c0089ffd6e0a404369fb45a8",
"bytes": 117412
},
{
"file": "refactor/.cache/ads03-browser-shared-results/ads-published-4-5-5-core-p-dcb98-ls-open-to-playable-content-firefox/trace.zip",
"sha256": "21a815b1452a91c12b2359a7bb3277840b48c0bc8d610b85c482db41c26e2368",
"bytes": 721477
},
{
"file": "refactor/.cache/ads03-browser-shared-results/ads-published-4-5-5-core-p-dcb98-ls-open-to-playable-content-webkit/error-context.md",
"sha256": "14c4d712045f85d4e24f947e851d9d706e3c8519b9b9cc2128b0719f327be2ca",
"bytes": 9376
},
{
"file": "refactor/.cache/ads03-browser-shared-results/ads-published-4-5-5-core-p-dcb98-ls-open-to-playable-content-webkit/test-failed-1.png",
"sha256": "47a7d1cd17b853d1b2188289f3d3246832a8621ee69b640eb0adab2aa7eafca1",
"bytes": 293849
},
{
"file": "refactor/.cache/ads03-browser-shared-results/ads-published-4-5-5-core-p-dcb98-ls-open-to-playable-content-webkit/trace.zip",
"sha256": "052ba810895a6e311543feb5391c0f6d398a44f9eb4638f442e1752ddef1c7ca",
"bytes": 458015
},
{
"file": "refactor/.cache/ads03-browser-shared-results/ads-published-4-5-5-core-p-f9294-completion-restores-content-chromium/error-context.md",
"sha256": "9bbfe13a8e4dc22155a51556b33cd22e2d9964057e70fb82ca6b37458e662db1",
"bytes": 895
},
{
"file": "refactor/.cache/ads03-browser-shared-results/ads-published-4-5-5-core-p-f9294-completion-restores-content-chromium/test-failed-1.png",
"sha256": "28b603ae124d31daeef45e85a281cec9568f90c6ef3434367b179a7e4e3efaa2",
"bytes": 83842
},
{
"file": "refactor/.cache/ads03-browser-shared-results/ads-published-4-5-5-core-p-f9294-completion-restores-content-chromium/trace.zip",
"sha256": "4247eacee77713dd5d9d5d7236406fb82567cd396b16a6692fb42fe529233ee4",
"bytes": 957983
},
{
"file": "refactor/.cache/ads03-browser-shared-results/ads-published-4-5-5-core-p-f9294-completion-restores-content-firefox/error-context.md",
"sha256": "0140e20e5dec56efb58df2b9b6c976286a69cc19ad57d7512c1c6cb41913de85",
"bytes": 1020
},
{
"file": "refactor/.cache/ads03-browser-shared-results/ads-published-4-5-5-core-p-f9294-completion-restores-content-firefox/test-failed-1.png",
"sha256": "4964632cf9d26783dbe72f88b10b9f043fe17cdea51aedb7986f7b5b7341a828",
"bytes": 117251
},
{
"file": "refactor/.cache/ads03-browser-shared-results/ads-published-4-5-5-core-p-f9294-completion-restores-content-firefox/trace.zip",
"sha256": "ace815077dcf9fd7cbc334ec748cc013accfcea33980713c9236b4f6cae02312",
"bytes": 1415954
},
{
"file": "refactor/.cache/ads03-browser-shared-results/ads-published-4-5-5-core-p-f9294-completion-restores-content-webkit/error-context.md",
"sha256": "6b038ca192ca8029d4e0e0215629c051e5276302bdad08ecf11c7f903f4e7382",
"bytes": 9395
},
{
"file": "refactor/.cache/ads03-browser-shared-results/ads-published-4-5-5-core-p-f9294-completion-restores-content-webkit/test-failed-1.png",
"sha256": "9f4cb835fbef26265cab5d12725406f6eeaf01c072b052a22e32b266acee3880",
"bytes": 289469
},
{
"file": "refactor/.cache/ads03-browser-shared-results/ads-published-4-5-5-core-p-f9294-completion-restores-content-webkit/trace.zip",
"sha256": "4c721c1b153e7644a4a27e013f084e8635b16678d639c5fe8ca8f2b7f54274a5",
"bytes": 1152077
}
]
},
{
"name": "lifecycle",
"note": "Firefox console text did not include DOMException.message; replaced text rendering assertion with transparent original argument observation. Intermediate source build, before final artifact acceptance.",
"stats": {
"startTime": "2026-09-11T23:41:40.573Z",
"duration": 159246.112,
"expected": 138,
"skipped": 0,
"unexpected": 6,
"flaky": 0
},
"report": {
"file": "refactor/.cache/ads03-browser-lifecycle-report.json",
"sha256": "300f7747114bad33dada5f1f47e38969249a2d845a4a3b333c6907c5d090eaeb",
"bytes": 9414532
},
"log": {
"file": "refactor/.cache/ads03-browser-lifecycle.log",
"sha256": "02e97aebd97cf0942667b330e118c171028e51ea42f54bc2957f717228b22d3f",
"bytes": 42188
},
"results": [
{
"file": "refactor/.cache/ads03-browser-lifecycle-results/.last-run.json",
"sha256": "f0cb82bbf13a73c2704ac594b6ac6685c0ad6fa4950b8543c054da699242ef0f",
"bytes": 341
},
{
"file": "refactor/.cache/ads03-browser-lifecycle-results/ads-candidate-core-candida-13e45--without-an-unhandled-error-firefox/error-context.md",
"sha256": "abfa031e13a6a9efe4fd4faebfcd479fd5973e53972ba8e722b040f73106bc55",
"bytes": 14686
},
{
"file": "refactor/.cache/ads03-browser-lifecycle-results/ads-candidate-core-candida-13e45--without-an-unhandled-error-firefox/test-failed-1.png",
"sha256": "9d9e3123c4c51b0c3f3b0957fe143e983cb15d68ecb4d5dbb1d6aa2721d95086",
"bytes": 106245
},
{
"file": "refactor/.cache/ads03-browser-lifecycle-results/ads-candidate-core-candida-13e45--without-an-unhandled-error-firefox/trace.zip",
"sha256": "d48cb4a82e6ca24b9eab8875632d219b6e1f85ff96467f187c0673473274dfad",
"bytes": 792087
},
{
"file": "refactor/.cache/ads03-browser-lifecycle-results/ads-candidate-core-candida-ba6b9--without-an-unhandled-error-firefox/error-context.md",
"sha256": "69e9d71857fbcd9e8c0988ed37bd65bf48a09e579fa1e088c78be94238f385e3",
"bytes": 14681
},
{
"file": "refactor/.cache/ads03-browser-lifecycle-results/ads-candidate-core-candida-ba6b9--without-an-unhandled-error-firefox/test-failed-1.png",
"sha256": "3a8afc70cb1a37b2bf6fbade0dd7da15dac966175da8c2a165aea071d1c215ca",
"bytes": 106532
},
{
"file": "refactor/.cache/ads03-browser-lifecycle-results/ads-candidate-core-candida-ba6b9--without-an-unhandled-error-firefox/trace.zip",
"sha256": "5c257128599f2c9a1fe54468ea1ffc42a7e101a872086ef888fcb8e3527e6c65",
"bytes": 457990
},
{
"file": "refactor/.cache/ads03-browser-lifecycle-results/ads-published-4-5-5-core-c-2c63b--without-an-unhandled-error-firefox/error-context.md",
"sha256": "9e5c9f04f6036fab2b4067dbfa208d33ffde1ff851e5bb117e0c8969d78e8de0",
"bytes": 14687
},
{
"file": "refactor/.cache/ads03-browser-lifecycle-results/ads-published-4-5-5-core-c-2c63b--without-an-unhandled-error-firefox/test-failed-1.png",
"sha256": "18df6a31cbbace68ce36d49932d39c80913a5f01d3c2242a4609ddb4ca2ddbe8",
"bytes": 103178
},
{
"file": "refactor/.cache/ads03-browser-lifecycle-results/ads-published-4-5-5-core-c-2c63b--without-an-unhandled-error-firefox/trace.zip",
"sha256": "c4f1e54fd76c14295ee6b9e972325b47729bffd35569515d97b177cf167a581e",
"bytes": 518630
},
{
"file": "refactor/.cache/ads03-browser-lifecycle-results/ads-published-4-5-5-core-c-b921a--without-an-unhandled-error-firefox/error-context.md",
"sha256": "7b8d9f1459e49e794f6959419121cca3ded4219a1b1e1abe55c09062be9ceef8",
"bytes": 14692
},
{
"file": "refactor/.cache/ads03-browser-lifecycle-results/ads-published-4-5-5-core-c-b921a--without-an-unhandled-error-firefox/test-failed-1.png",
"sha256": "b5a1d4125c6c7d3bb8328c16ea8d97385f123756da1fa8c3c521d916383d60e5",
"bytes": 104744
},
{
"file": "refactor/.cache/ads03-browser-lifecycle-results/ads-published-4-5-5-core-c-b921a--without-an-unhandled-error-firefox/trace.zip",
"sha256": "0ba655ceaa0133c9bdf06eb994d53da45b3e6700a7dd16c38209f223bd5cfb8a",
"bytes": 577326
},
{
"file": "refactor/.cache/ads03-browser-lifecycle-results/ads-published-core-candida-35c50--without-an-unhandled-error-firefox/error-context.md",
"sha256": "cf36c7c818d24d0a2d7b04d1bd4a83f467c218c745d5eb9bc3e8329fee174a55",
"bytes": 14686
},
{
"file": "refactor/.cache/ads03-browser-lifecycle-results/ads-published-core-candida-35c50--without-an-unhandled-error-firefox/test-failed-1.png",
"sha256": "9d9e3123c4c51b0c3f3b0957fe143e983cb15d68ecb4d5dbb1d6aa2721d95086",
"bytes": 106245
},
{
"file": "refactor/.cache/ads03-browser-lifecycle-results/ads-published-core-candida-35c50--without-an-unhandled-error-firefox/trace.zip",
"sha256": "6ab10ac7dad026bc26a8a0e991bc6045057ded0cb5847218fc1048acc49105cf",
"bytes": 790965
},
{
"file": "refactor/.cache/ads03-browser-lifecycle-results/ads-published-core-candida-85573--without-an-unhandled-error-firefox/error-context.md",
"sha256": "c210842e0e5a80c8e6aecc16cc34d2f18420a3a46ae0d4644231857807879f5e",
"bytes": 14681
},
{
"file": "refactor/.cache/ads03-browser-lifecycle-results/ads-published-core-candida-85573--without-an-unhandled-error-firefox/test-failed-1.png",
"sha256": "d9b2568d99ae3711c5ede23bf093e81088a0bf320599870024270fa1d7400e18",
"bytes": 106388
},
{
"file": "refactor/.cache/ads03-browser-lifecycle-results/ads-published-core-candida-85573--without-an-unhandled-error-firefox/trace.zip",
"sha256": "3242cd978b05aa458d42aacf40226aa322a3894485ec88addab609bcfb6094dc",
"bytes": 473549
}
]
},
{
"name": "main",
"note": "Actual main core/plugin artifacts; three cores and three engines; normal contracts and candidate lifecycle plus published historical observations.",
"stats": {
"startTime": "2026-09-11T23:45:21.943Z",
"duration": 151353.921,
"expected": 144,
"skipped": 0,
"unexpected": 0,
"flaky": 0
},
"report": {
"file": "refactor/.cache/ads03-browser-main-report.json",
"sha256": "52ac530bc6a2099bcc95de92fea665286d1645e5741967eb48f3e0ce07d12bd4",
"bytes": 9432469
},
"log": {
"file": "refactor/.cache/ads03-browser-main.log",
"sha256": "bca4fdc2e85fd9614ca230f15ffb7ce884b2277f43145fa7bf2d8e4e4943b539",
"bytes": 25043
},
"results": [
{
"file": "refactor/.cache/ads03-browser-main-results/.last-run.json",
"sha256": "91d1c43004802cd49950d78eb11c8fa7d05da8ffffe219a8b13b2f561bc00903",
"bytes": 45
}
]
},
{
"name": "legacy",
"note": "Actual legacy core/plugin artifacts; same complete matrix, no skips or retries.",
"stats": {
"startTime": "2026-09-11T23:48:36.896Z",
"duration": 151422.821,
"expected": 144,
"skipped": 0,
"unexpected": 0,
"flaky": 0
},
"report": {
"file": "refactor/.cache/ads03-browser-legacy-report.json",
"sha256": "206e4355f9f7108e562c7801919b2ef9534b332f1a15eb1e1dc4dc6f929b3a3e",
"bytes": 9435856
},
"log": {
"file": "refactor/.cache/ads03-browser-legacy.log",
"sha256": "ab1b0cd3c60880bbf5216027696bc976c7d8e275f7465291a884a969f13104c6",
"bytes": 25041
},
"results": [
{
"file": "refactor/.cache/ads03-browser-legacy-results/.last-run.json",
"sha256": "91d1c43004802cd49950d78eb11c8fa7d05da8ffffe219a8b13b2f561bc00903",
"bytes": 45
}
]
}
],
"ci": {
"file": "refactor/.cache/ads03-ci.log",
"sha256": "1887d58330da102df5ab43934fa7322440f2600ea62878b8ab064a548bd5453f",
"bytes": 86881,
"status": "passed",
"tests": 830,
"unit": 770,
"engineering": 14,
"baseline": 46,
"productionTypeScript": 269,
"coreDeclarationsUnchanged": 37,
"warning": "Existing generated docs/assets/ts/artplayer.d.ts unused eslint-disable; no new lint errors."
},
"unchanged": [
"Public declarations",
"Package version/exports",
"Style source",
"Runtime/development dependencies and lockfile"
],
"limitations": [
"Public declaration reconciliation and old CommonJS default shape pending 04/06.",
"Real devices, native fullscreen and genuine hidden-page policy pending 05.",
"Existing Ads risk entries stay open until their full closure evidence is complete.",
"No push/tag/publish/merge."
],
"visualReview": {
"file": "refactor/.cache/ads03-main-review.png",
"source": "Main report: published 4.5.5 core / candidate Ads, Chromium",
"observed": "Local decoded pattern, countdown and mute/fullscreen controls visible."
}
}
@@ -0,0 +1,74 @@
# PKG-ADS-03:Ads 职责拆分与生命周期修复
起点 069ad4aa。实际实现地图见包内 [ARCHITECTURE.md](../../packages/artplayer-plugin-ads/ARCHITECTURE.md)。
本步已经进入生产源码,公开声明/格式类型入口仍由 04 处理,完整分发和 demo 由 06 接续。
## 结构改造
原 index.js 拆为 7 个严格 TS 模块:入口、配置、内部类型、计时、资源、视图和 session。
计时模块不依赖核心/DOM;视图仅接收父节点、图标、三个 DOM 工具和回调;session 管理
状态转换及媒体请求。entry 中保留旧声明 constructor 边界的局部断言;私有固定 HTML
对应的节点种类断言集中在 view,未用 any 或全局索引签名掩盖全部类型问题。
配置每次 attach 独立归一化;i18n 仍浅合并;emit 的配置仍为同一活对象,修改事件配置
中的 totalDuration 会影响后续 tick。移除未经验证的 >=5 数字门槛,使用已有 validator/
DOM 工具;DOM 监听自有,兼容原生 visibility 与新核心事件桥,重复恢复只有一个 timer。
核心 4.5.5 的实际 npm 归档、SHA512/SHA256、完整成员清单和选定源码/主入口指纹记录在
[ads-core.json](../baselines/ads-core.json),新增基线测试。浏览器服务器正式映射该
历史核心,manifest 记录实际字节来源;没有先加载新核心再覆盖全局变量/样式。
## 有意修正的缺陷与兼容边界
1. 重复 play/metadata/ready 不再创建多个计时链或视图;终止和暂停真正取消已排队 tick。
2. 跳过幂等;销毁后方法/媒体回调均无效。正常 skip 仍先请求主片播放,再暂停广告、
隐藏 DOM、同步 emit 原配置。同步播放/广告暂停回调触发 destroy 时不再继续通知。
3. 提前 play/pause 无操作;提前 skip 取消尚未开始的 preroll 并 emit 一次,不创建 DOM
或提前自动播放主片。旧路径抛错/留下永不结束遮罩,不作为兼容要求保留。
4. 零阈值关闭按钮立即可用;全屏图标按初始状态及外部 fullscreen 事件同步。
5. 媒体监听早于 src 赋值,覆盖同步/缓存 metadata;广告 play 失败观察原错误并按
媒体失败完成广告。主片恢复失败保持完成状态并记录原错误,外部 art.play 不被改写。
6. 销毁独立释放 timer、core/DOM/document 监听、媒体 src 和 overlay,保留调用方
新的 template.$ads 值及外部监听。部分构建失败也清理已拥有的节点和订阅。
正常公共 play/pause 只控制倒计时,未被改成媒体暂停/播放接口。视频优先于 HTML、
数值/默认时长、完整 i18n、URL/事件参数、同步返回及全部既有 CSS 类名保持。
结束时仍保留隐藏 overlay 与媒体 source/error 状态,到 destroy 才释放媒体。
初始隐藏文档暂停计时;真实设备后台策略仍需后续验收。
## 测试与原始失败
先增加的 10 个候选缺陷断言在旧源码上全部失败;初次实现受控测试的 Node 节点缺少
remove 方法,补足最小 DOM 意图模型,不伪装真实浏览器。之后增加动态事件配置、同步
src metadata、Promise 拒绝/晚到、部分构建失败、外部监听与模板所有权、全屏状态等断言。
广告暂停时同步 destroy 的额外回归先 19 通过/1 失败,补终止状态检查后通过。
正式三格式首次 182/186 通过;VM 与主环境 Promise 的跨 realm 同化多于两次 microtask。
改成让一个事件循环轮次排空已排队微任务,而非固定两次 Promise.resolve;未替换 VM 的
Promise 或修改生产实现来配合测试。最终三格式加源码/历史观察 186 项全部通过。
首次加入实际 4.5.5 的浏览器运行 66 通过/33 失败:测试夹具误用该核心不存在的 art.video。
改为已经存在的 template.$video;生产插件没有该依赖。扩展后 138/144 通过,6 个 Firefox
失败为 console 文本没有序列化 DOMException.message;改用透明 console.warn 参数记录,
仍调用原方法、严格核对原异常名/消息,不屏蔽其他页面异常。原始报告与 trace 保留。
正式 main 和 legacy 三引擎分别 144 项全部通过,无跳过或重试,报告中的候选插件哈希
逐例匹配本次正式文件;4.5.5 核心来源也核对实际 manifest 指纹。已查看其加载候选 main
的视频截图,画面、倒计时、静音/全屏控件均可见。完整本地 CI 830 项通过(770 单元、
14 工程、46 基线),269 个生产 TS 严格检查、核心 37 份声明无漂移。既有生成编辑器
声明的一条 unused eslint-disable warning 保留,没有新增 lint 错误。
最终记录见 [执行证据](../baselines/ads-lifecycle-validation.json)。同步源码/构建产物由
仓库 yarn build 生成,未手工编辑 dist/docs compiled。
本步不增加依赖,新增 tsconfig 从 npm 包排除,Ads 生命周期测试接入 test:ads/test:unit。
计划验证曾拒绝直接指向 refactor 外架构文件的 evidence 项,已改为内部变更记录再链接
包内文档;没有修改校验规则。异步测试新增 import 顺序按现有 lint 修正。
## 后续与回退
ADS 的生命周期、媒体/UI 风险保留到相应关闭证据齐全;DOM/媒体真实销毁、4.5.5 组合
证据也不代表所有历史核心或移动设备已认证。04 需要处理历史 export=、totalDuration
错误 string 声明与工作区 source/type 接受面;05 完成实际设备/隐藏页/全屏;06 隔离
tarball/旧 default namespace/8082 demo。DASH 等其他开放媒体风险仍有效。
本地独立提交包含本步生产拆分、生成产物、回归、架构/执行文档与状态;可整体 revert
回到 02 冻结实现与测试基线。没有 push、tag、publish 或 merge。
+1 -1
View File
@@ -33,7 +33,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 归档已冻结;HTML/视频、本片恢复、跳过、错误/销毁;本地短广告和主片 | 关联历史核心 4.5.5 尚待实际归档/浏览器验证;与 5.4.1/候选核心组合均待。无需借 IMA 结果替代自有逻辑 | PKG-ADS-01/02/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/候选核心 × 发布/候选插件实际组合见 ads-validation.md;设备/隐藏页/全屏与完整分发仍待,不扩大到全部历史核心。无需借 IMA 结果替代自有逻辑 | 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 137 / doing 4 / blocked 0 / done 76 / deferred 0。风险 L/M/H 表示兼容风险,不表示工期。
状态:todo 136 / doing 4 / blocked 0 / done 77 / deferred 0。风险 L/M/H 表示兼容风险,不表示工期。
前置依赖是启动条件;验收是完成条件。任务可以继续拆分,但不能复用或悄悄删除旧 ID。
@@ -258,7 +258,7 @@
| --- | --- | --- | --- | --- | --- | --- |
| PKG-ADS-01 | artplayer-plugin-ads<br>核对包契约与历史用法 | BASE-05 | 核对 npm 1.0.0~1.0.6 与未发布工作区 2.1.0 的真实参数、类型、结果、倒计时、分发和旧核心来源 | 源码/声明/README/demo/发布包差异已登记;公开形状和版本范围冻结 | 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 | todo |
| 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 | todo |
| PKG-ADS-05 | artplayer-plugin-ads<br>验证新旧核心和组合 | PKG-ADS-04, CORE-22 | 本地广告 fixture、多实例、正常结束/跳过/销毁;核验历史关联核心 4.5.5、发布 5.4.1 与候选核心的实际归档/能力/媒体组合 | 最终核心与原支持范围核心分别通过;设备/SDK 缺证据不能标完成 | 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 |
@@ -486,3 +486,4 @@
- PKG-DASH-05: [记录](changes/2026-09-12-PKG-DASH-05-sdk-checkpoint.md) [记录](baselines/dash-sdk.json) [记录](baselines/dash-sdk-checkpoint.json) [记录](dash-validation.md) [记录](changes/2026-09-12-PKG-DASH-05-sdk-types.md) [记录](baselines/dash-sdk-types-validation.json) [记录](changes/2026-09-12-PKG-DASH-05-sdk-events.md) [记录](baselines/dash-sdk-events-validation.json) [记录](changes/2026-09-12-PKG-DASH-05-seek-diagnosis.md) [记录](baselines/dash-seek-diagnosis.json)
- PKG-ADS-01: [记录](baselines/ads-release.json) [记录](baselines/ads-contract.md) [记录](baselines/ads-contract-validation.json) [记录](changes/2026-09-12-PKG-ADS-01-contract.md) [记录](scripts/ads-contract.test.mjs)
- 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)
+20
View File
@@ -1,5 +1,25 @@
# 进度与证据
## 最新完成:Ads-03 源码拆分与生命周期修复
Ads 原入口拆为 7 个严格 TS 模块,职责为配置、类型、计时、资源、视图、session 和入口。
修复重复计时/metadata、工厂复用参数串用、零阈值、过早方法、播放拒绝、销毁/重入后的
回调;保留正常 skip 顺序、同步结果、活配置事件参数、CSS 和 play/pause 只控制倒计时。
实际模块地图及媒体/监听/定时器所有权写入包内 ARCHITECTURE.md。
实际核心 4.5.5 归档与实现能力已核验并接入浏览器映射。正式三格式 Node 186 项通过;
正式 main/legacy 的三引擎矩阵分别 144 项全部通过,无跳过/重试,覆盖 4.5.5/5.4.1/
候选核心、新旧插件、本地真实媒体、提前/重复 skip、销毁资源及内部播放拒绝。
完整本地 CI 830 项通过(770 单元、14 工程、46 基线),269 个生产 TS 严格检查,
37 份核心声明无漂移。原始缺陷/测试夹具失败、报告、trace 和最终输入哈希均保留。
当前 217 项:77 done、4 doing、136 todo。下一步 Ads-04 处理准确公开声明、历史
export=、string 时长误声明与工作区 source/type 类型接受面;尚未称整个 Ads 发布验收
完成。隐藏页/设备/全屏和完整分发仍由 05/06 接续,既有 DASH 等开放风险不变。
见 [本步记录](changes/2026-09-12-PKG-ADS-03-lifecycle.md)、
[执行证据](baselines/ads-lifecycle-validation.json) 和 [测试维护](ads-validation.md)。
以下为历史检查点当时状态。
## 最新完成:Ads-02 行为与真实媒体回归
新增 58 项 Node 测试,覆盖源码、实际 npm 1.0.6 和冻结的未发布工作区 2.1.0;
+29 -9
View File
@@ -2988,7 +2988,7 @@
"confirmation": "source-observed",
"status": "open",
"owners": ["PKG-ADS-02", "PKG-ADS-04"],
"evidence": ["refactor/baselines/ads-release.json", "refactor/baselines/ads-contract.md", "packages/artplayer-plugin-ads/types/artplayer-plugin-ads.d.ts", "packages/artplayer-plugin-ads/src/index.js"],
"evidence": ["refactor/baselines/ads-release.json", "refactor/baselines/ads-contract.md", "packages/artplayer-plugin-ads/types/artplayer-plugin-ads.d.ts", "packages/artplayer-plugin-ads/src/index.ts"],
"compatibleResolution": "同时保护实际 npm 字段/旧声明消费者和当前类型接受面;真实数字时长、i18n 与 source/type 别名的行为差异单独登记,不伪称旧实现支持别名。",
"closureCriteria": "两套旧消费者、可选工厂、合法运行字段和错误参数有严格正反例,声明与真实方法一致。"
},
@@ -3007,8 +3007,19 @@
"title": "Ads 旧发布关联核心 4.5.5,当前源码新增 >=5 门槛和 document 事件桥依赖",
"confirmation": "source-observed",
"status": "open",
"owners": ["PKG-ADS-02", "PKG-ADS-03", "PKG-ADS-05"],
"evidence": ["refactor/baselines/ads-release.json", "refactor/baselines/ads-contract.md", "packages/artplayer-plugin-ads/src/index.js"],
"owners": [
"PKG-ADS-02",
"PKG-ADS-03",
"PKG-ADS-05"
],
"evidence": [
"refactor/baselines/ads-release.json",
"refactor/baselines/ads-contract.md",
"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"
],
"compatibleResolution": "先核验实际核心归档和能力,通过本地 fallback 保留原用户调用;不能用未发布源码门槛强制全部旧消费者升级,也不泛称全部 4.x 通过。",
"closureCriteria": "4.5.5 关联来源和实际可用组合明确,5.4.1/候选核心与原生 visibility/事件桥路径有真实浏览器证据。"
},
@@ -3024,11 +3035,14 @@
],
"evidence": [
"refactor/baselines/ads-contract.md",
"packages/artplayer-plugin-ads/src/index.js",
"packages/artplayer-plugin-ads/src/index.ts",
"test/ads.test.js",
"test/browser/ads.spec.js",
"refactor/ads-validation.md",
"refactor/baselines/ads-validation.json"
"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"
],
"compatibleResolution": "先复现重复 play 的计时链、skip 后计时、destroy 后恢复与复用工厂 options;以实例所有权和幂等清理修正,不更改广告时长/跳过规则。",
"closureCriteria": "时钟/订阅/DOM 与主片恢复的成功、异常、重入、销毁、复用和多实例回归通过。"
@@ -3045,12 +3059,15 @@
],
"evidence": [
"refactor/baselines/ads-contract.md",
"packages/artplayer-plugin-ads/src/index.js",
"packages/artplayer-plugin-ads/src/index.ts",
"docs/assets/example/ads.js",
"test/ads.test.js",
"test/browser/ads.spec.js",
"refactor/ads-validation.md",
"refactor/baselines/ads-validation.json"
"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"
],
"compatibleResolution": "用本地媒体验证事件安装顺序、加载失败与拒绝、倒计时和广告播放的独立语义,恢复主片时不吞公共拒绝或在销毁后播放。",
"closureCriteria": "实际广告/主片的加载、暂停/恢复、失败/换源/销毁和隐藏状态有新旧核心组合证据。"
@@ -3067,12 +3084,15 @@
],
"evidence": [
"refactor/baselines/ads-contract.md",
"packages/artplayer-plugin-ads/src/index.js",
"packages/artplayer-plugin-ads/src/index.ts",
"docs/assets/example/ads.js",
"test/ads.test.js",
"test/browser/ads.spec.js",
"refactor/ads-validation.md",
"refactor/baselines/ads-validation.json"
"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"
],
"compatibleResolution": "复现 playDuration<=0 文案可关闭但 isCanClose 尚为 false;按既有 demo 规则修复,验证全屏按钮和外部退出状态,保留 CSS/DOM 钩子。",
"closureCriteria": "零/正/不可跳阈值、真实点击、倒计时/详情/静音/全屏与外部状态组合均有回归。"
+17
View File
@@ -0,0 +1,17 @@
import assert from 'node:assert/strict'
import fs from 'node:fs'
import test from 'node:test'
import { archiveFiles, ensureArchive, hash, readMember } from './releases.mjs'
test('Ads historical core archive and selected implementation members remain verified', async () => {
const baseline = JSON.parse(fs.readFileSync(new URL('../baselines/ads-core.json', import.meta.url)))
const ads = JSON.parse(fs.readFileSync(new URL('../baselines/ads-release.json', import.meta.url)))
assert.equal(baseline.release.name, 'artplayer')
assert.equal(baseline.release.version, ads.historicalCore.version)
const archive = await ensureArchive(baseline.release)
assert.deepEqual(archiveFiles(archive), baseline.archiveMembers)
for (const [member, expected] of Object.entries(baseline.release.files))
assert.equal(hash(readMember(archive, member)), expected)
assert.deepEqual(JSON.parse(readMember(archive, 'package/package.json')), baseline.release.manifest)
assert(Object.hasOwn(baseline.release.files, `package/${baseline.release.manifest.main}`))
})
+6 -2
View File
@@ -2567,11 +2567,15 @@
"CORE-10",
"CORE-13"
],
"status": "todo",
"status": "done",
"risk": "H",
"deliverable": "配置/广告状态/计时/视图/主视频恢复分离,保留产品规则;资源与版本检查改用经验证的旧核心能力边界",
"acceptance": "结构变化和缺陷修复分开记录;原 API/事件/资源生命周期通过",
"evidence": []
"evidence": [
"changes/2026-09-12-PKG-ADS-03-lifecycle.md",
"ads-validation.md",
"baselines/ads-lifecycle-validation.json"
]
},
{
"id": "PKG-ADS-04",
+5
View File
@@ -80,6 +80,11 @@ allowJs=true、checkJs=false 允许渐进迁移时引用旧 JS;只将迁移的
## 后续接入
Ads 的 7 个自有 TS 模块由 packages/artplayer-plugin-ads/tsconfig.json 严格检查,
无生产 JS 遗留。03 的内部 Input/Options 与旧公开声明暂时分开;公开声明仍有已登记
的历史输入/类型漂移,由 PKG-ADS-04 建立精确消费者后统一,不能把源码严格检查称为
旧/新发布类型矩阵完成。
Audio Track 的自有源码由 packages/artplayer-plugin-audio-track/tsconfig.json 严格检查。
默认/legacy 类型保留历史 update 的完整 Option;新增 /runtime 复用同一运行文件,
提供 UpdateOption/RuntimeResult。test/types/audio-track.ts 同时验证旧 Parameters、
+3
View File
@@ -7,6 +7,9 @@ Ads bundle against controlled clock/host contracts. It is included in `test:unit
The helper uses the real option validator but does not simulate media decoding or layout.
Historical defect assertions apply only to frozen implementations. See
[Ads validation](../refactor/ads-validation.md) for artifact overrides and browser scope.
`ads-lifecycle.test.js` adds candidate-only resource, reentry, early-method and Promise
regressions. Async observations drain one event-loop turn, including VM Promise assimilation,
rather than assuming a fixed number of microtasks. The published defect observations stay intact.
| Command | Scope |
| ------------------- | -------------------------------------------------------------------------------------------------------- |
+331
View File
@@ -0,0 +1,331 @@
import assert from 'node:assert/strict'
import { getEventListeners } from 'node:events'
// eslint-disable-next-line test/no-import-node-test -- Controlled lifecycle contracts use Node's runner.
import test from 'node:test'
import { setImmediate as settle } from 'node:timers/promises'
import { adsEnvironment, adsImplementations } from './helpers/ads.js'
const implementations = (await adsImplementations()).filter(item => !item.historical)
const skips = host => host.calls.filter(call => call[0] === 'emit' && call[1] === 'artplayerPluginAds:skip')
const contentPlays = host => host.calls.filter(call => call[0] === 'content.play')
for (const { name, factory } of implementations) {
test(`${name}: repeated play owns one timer and pause clears all countdown work`, (t) => {
const env = adsEnvironment(t)
const host = env.host()
const plugin = factory({ html: 'ad', totalDuration: 3 })(host.art)
host.start()
plugin.play()
plugin.play()
assert.equal(env.timers.size, 1)
env.tick(1000)
assert.equal(host.node('countdown').innerHTML, '2秒')
plugin.pause()
assert.equal(env.timers.size, 0)
})
test(`${name}: skip clears timers and becomes inert under repeated methods/events`, (t) => {
const env = adsEnvironment(t)
const host = env.host()
const plugin = factory({ html: 'ad', totalDuration: 1 })(host.art)
host.start()
plugin.skip()
plugin.skip()
plugin.play()
host.visibility(false)
env.tick(5000)
assert.equal(env.timers.size, 0)
assert.equal(skips(host).length, 1)
assert.equal(contentPlays(host).length, 1)
})
test(`${name}: destroy cancels countdown and future public calls never resume content`, (t) => {
const env = adsEnvironment(t)
const host = env.host()
const plugin = factory({ html: 'ad', totalDuration: 1 })(host.art)
host.start()
host.art.destroy()
plugin.play()
plugin.skip()
env.tick(5000)
assert.equal(env.timers.size, 0)
assert.equal(contentPlays(host).length, 0)
assert.equal(skips(host).length, 0)
})
test(`${name}: one factory retains per-instance normalized event payloads`, (t) => {
const env = adsEnvironment(t)
const first = env.host()
const second = env.host()
const attach = factory({ html: 'shared' })
const one = attach(first.art)
first.start()
attach(second.art)
second.start()
one.skip()
assert.equal(skips(first)[0][2], first.normalized)
assert.notEqual(skips(first)[0][2], second.normalized)
})
test(`${name}: zero threshold close works before the first timer tick`, (t) => {
const env = adsEnvironment(t)
const host = env.host()
factory({ html: 'ad', playDuration: 0 })(host.art)
host.start()
host.fire('close', 'click')
assert.equal(skips(host).length, 1)
assert.equal(env.timers.size, 0)
})
test(`${name}: play before initialization is inert and skip cancels pending preroll`, (t) => {
const env = adsEnvironment(t)
const host = env.host()
const plugin = factory({ html: 'ad' })(host.art)
plugin.play()
assert.equal(env.timers.size, 0)
plugin.skip()
host.start()
assert.equal(host.node(), undefined)
assert.equal(skips(host).length, 1)
assert.equal(contentPlays(host).length, 0)
})
test(`${name}: duplicate metadata starts the ad once`, (t) => {
const env = adsEnvironment(t)
const host = env.host()
factory({ video: 'ad.mp4' })(host.art)
host.start()
host.fire('video', 'loadedmetadata')
host.fire('video', 'loadedmetadata')
assert.equal(env.timers.size, 1)
assert.equal(host.calls.filter(call => call[0] === 'ad.play').length, 1)
})
test(`${name}: core version numbers do not reject hosts providing the required capabilities`, (t) => {
const env = adsEnvironment(t)
const host = env.host()
host.art.constructor.version = '4.5.5'
factory({ html: 'ad', totalDuration: 2 })(host.art)
host.start()
env.tick(1000)
host.visibility(true)
env.tick(5000)
assert.equal(host.node('countdown').innerHTML, '1秒')
host.visibility(false)
env.tick(1000)
assert.equal(skips(host).length, 1)
})
test(`${name}: synchronous content-play failure still hides ad and completes exactly once`, (t) => {
const env = adsEnvironment(t)
const host = env.host()
const warning = []
t.mock.method(console, 'warn', (...args) => warning.push(args))
const plugin = factory({ html: 'ad' })(host.art)
host.start()
const error = new Error('Content play failed')
host.art.play = () => {
throw error
}
assert.doesNotThrow(() => plugin.skip())
assert.equal(host.node().style.display, 'none')
assert.equal(skips(host).length, 1)
assert(warning.some(args => args.includes(error)))
})
test(`${name}: destroy during content restoration suppresses subsequent skip dispatch`, (t) => {
const env = adsEnvironment(t)
const host = env.host()
const plugin = factory({ html: 'ad' })(host.art)
host.start()
host.art.on('play', () => host.art.destroy())
plugin.skip()
assert.equal(skips(host).length, 0)
assert.equal(env.timers.size, 0)
})
test(`${name}: metadata emitted by the src setter is observed after listeners are installed`, (t) => {
const env = adsEnvironment(t)
const host = env.host()
const append = host.art.constructor.utils.append
host.art.constructor.utils.append = (...args) => {
const node = append(...args)
if (node.tagName === 'VIDEO') {
let source
Object.defineProperty(node, 'src', {
configurable: true,
get: () => source,
set(value) {
source = value
node.dispatchEvent(new Event('loadedmetadata'))
},
})
}
return node
}
factory({ video: 'cached.mp4' })(host.art)
host.start()
assert.equal(env.timers.size, 1)
assert.equal(host.calls.filter(call => call[0] === 'ad.play').length, 1)
})
test(`${name}: rejected ad play restores content once and observes the original error`, async (t) => {
const env = adsEnvironment(t)
const host = env.host()
const warnings = []
t.mock.method(console, 'warn', (...args) => warnings.push(args))
factory({ video: 'ad.mp4' })(host.art)
host.start()
const error = new Error('Ad play rejected')
host.node('video').play = () => Promise.reject(error)
host.fire('video', 'loadedmetadata')
await settle()
assert.equal(skips(host).length, 1)
assert.equal(contentPlays(host).length, 1)
assert.equal(env.timers.size, 0)
assert(warnings.some(args => args.includes(error)))
})
test(`${name}: late ad play fulfillment after destroy pauses owned media without restoring content`, async (t) => {
const env = adsEnvironment(t)
const host = env.host()
factory({ video: 'ad.mp4' })(host.art)
host.start()
let resolve
const video = host.node('video')
video.play = () => new Promise((done) => {
resolve = done
})
host.fire('video', 'loadedmetadata')
host.art.destroy()
const pauses = host.calls.filter(call => call[0] === 'ad.pause').length
video.paused = false
resolve()
await settle()
assert.equal(video.paused, true)
assert.equal(host.calls.filter(call => call[0] === 'ad.pause').length, pauses + 1)
assert.equal(contentPlays(host).length, 0)
assert.equal(video.src, undefined)
assert.equal(host.art.template.$ads, undefined)
})
test(`${name}: destroy releases own DOM/document listeners and leaves foreign listeners intact`, (t) => {
const env = adsEnvironment(t)
const host = env.host()
factory({ video: 'ad.mp4' })(host.art)
host.start()
const video = host.node('video')
const foreign = () => {}
video.addEventListener('loadedmetadata', foreign)
assert.equal(getEventListeners(video, 'loadedmetadata').length, 2)
assert.equal(getEventListeners(env.document, 'visibilitychange').length, 1)
host.art.destroy()
assert.deepEqual(getEventListeners(video, 'loadedmetadata'), [foreign])
assert.equal(getEventListeners(video, 'error').length, 0)
assert.equal(getEventListeners(env.document, 'visibilitychange').length, 0)
assert.equal(host.node().parentNode, null)
assert.equal(host.art.template.$ads, undefined)
host.fire('video', 'loadedmetadata')
assert.equal(env.timers.size, 0)
})
test(`${name}: partial view failure removes owned root and subscriptions before propagating`, (t) => {
const env = adsEnvironment(t)
const host = env.host()
const append = host.art.constructor.utils.append
const error = new Error('View failed')
host.art.constructor.utils.append = (...args) => {
if (host.node('loading'))
throw error
return append(...args)
}
factory({ html: 'ad' })(host.art)
assert.throws(() => host.start(), candidate => candidate === error)
assert.equal(host.node().parentNode, null)
assert.equal(host.art.template.$ads, undefined)
assert.equal([...host.listeners.values()].flatMap(set => [...set]).length, 0)
assert.equal(env.timers.size, 0)
})
test(`${name}: synchronous destroy during root creation leaves no newly appended overlay`, (t) => {
const env = adsEnvironment(t)
const host = env.host()
const append = host.art.constructor.utils.append
host.art.constructor.utils.append = (...args) => {
const node = append(...args)
if (node.className === 'artplayer-plugin-ads')
host.art.destroy()
return node
}
factory({ html: 'ad' })(host.art)
host.start()
assert.equal(host.node().parentNode, null)
assert.equal(host.art.template.$ads, undefined)
assert.equal(env.timers.size, 0)
assert.equal(host.calls.filter(call => call[0] === 'content.pause').length, 0)
})
test(`${name}: fullscreen icons follow initial and externally changed state`, (t) => {
const env = adsEnvironment(t)
const host = env.host()
host.art.fullscreen = true
factory({ html: 'ad' })(host.art)
host.start()
assert.equal(host.nodes.get('fullscreenOn').style.display, 'none')
assert.equal(host.nodes.get('fullscreenOff').style.display, 'inline-flex')
host.art.fullscreen = false
host.art.emit('fullscreen', false)
assert.equal(host.nodes.get('fullscreenOn').style.display, 'inline-flex')
assert.equal(host.nodes.get('fullscreenOff').style.display, 'none')
})
test(`${name}: destroy during ad pause prevents late UI writes and skip notification`, (t) => {
const env = adsEnvironment(t)
const host = env.host()
const plugin = factory({ video: 'ad.mp4' })(host.art)
host.start()
const video = host.node('video')
let once = false
video.pause = () => {
if (!once) {
once = true
host.art.destroy()
}
}
plugin.skip()
assert.equal(skips(host).length, 0)
assert.equal(host.node().style.display, undefined)
})
test(`${name}: destroy preserves a caller replacement of template.$ads`, (t) => {
const env = adsEnvironment(t)
const host = env.host()
factory({ html: 'ad' })(host.art)
host.start()
const foreign = {}
host.art.template.$ads = foreign
host.art.destroy()
assert.equal(host.art.template.$ads, foreign)
assert.equal(host.node().parentNode, null)
})
test(`${name}: rejected play after destroy is observed without restoring or reporting cancellation`, async (t) => {
const env = adsEnvironment(t)
const host = env.host()
const warnings = []
t.mock.method(console, 'warn', (...args) => warnings.push(args))
factory({ video: 'ad.mp4' })(host.art)
host.start()
let reject
host.node('video').play = () => new Promise((_, fail) => {
reject = fail
})
host.fire('video', 'loadedmetadata')
host.art.destroy()
reject(new Error('Cancelled by destruction'))
await settle()
assert.equal(warnings.length, 0)
assert.equal(contentPlays(host).length, 0)
assert.equal(skips(host).length, 0)
})
}
+14
View File
@@ -252,6 +252,20 @@ for (const { name, factory } of implementations) {
assert.equal(skips(host).length, 1)
assert.equal(env.timers.size, 0)
})
test(`${name}: normalized event options remain live for subsequent countdown ticks`, (t) => {
const env = adsEnvironment(t)
const host = env.host()
factory({ html: 'ad', totalDuration: 10 })(host.art)
host.art.on('artplayerPluginAds:click', (option) => {
option.totalDuration = 2
})
host.start()
host.fire('html', 'click')
env.tick(2000)
assert.equal(skips(host).length, 1)
assert.equal(host.node('countdown').innerHTML, '0秒')
})
}
// Freeze legacy defects separately; migrated candidates must not inherit these assertions.
+4 -1
View File
@@ -1,10 +1,13 @@
# Browser regression entry
`yarn test:browser test/browser/ads.spec.js` tests actual Ads 1.0.6 and candidate code
with published 5.4.1 and candidate cores using local image/video media. It covers actual
with published 4.5.5/5.4.1 and candidate cores using local image/video media. It covers actual
decoded pixels, countdown, skipping, content restoration, 404 and source replacement.
Published-only rejection cases inject an exact play failure; they do not certify device
autoplay policies. See [Ads validation](../../refactor/ads-validation.md).
Candidate lifecycle cases check early/duplicate skip, real media release after destroy and
observed internal play rejections. The server exposes the verified 4.5.5 bundle separately
and records its fingerprint; the fixture uses the shared template.$video surface.
Audio Track tests use the source and frozen 1.1.0 plugin on both cores. The local AAC fixture
has a pinned hash and generation command in media/audio-tone.json and uses the existing Range
+94 -7
View File
@@ -26,8 +26,9 @@ test.afterEach(async ({ page }, testInfo) => {
return {
events: window.adsEvents,
rejected: window.adsRejected,
warnings: window.adsWarnings,
ad: ad && { time: ad.currentTime, paused: ad.paused, width: ad.videoWidth, height: ad.videoHeight, error: ad.error?.code },
content: window.art && { time: window.art.currentTime, paused: window.art.video.paused, destroyed: window.art.isDestroy },
content: window.art && { time: window.art.currentTime, paused: window.art.template.$video.paused, destroyed: window.art.isDestroy },
}
})
await testInfo.attach('ads-state', { contentType: 'application/json', body: JSON.stringify(state) })
@@ -40,7 +41,7 @@ test.afterEach(async ({ page }, testInfo) => {
})
})
async function openAds(page, core, plugin, option, testInfo) {
async function openAds(page, core, plugin, option, testInfo, start = true) {
await page.goto(`/test/player.html?core=${core}`)
const code = plugin === 'published' ? published : candidate
await page.addScriptTag({ content: code })
@@ -59,10 +60,96 @@ async function openAds(page, core, plugin, option, testInfo) {
}, option)
await expect.poll(() => page.evaluate(() => window.art.isReady)).toBe(true)
await expect(page.locator('.artplayer-plugin-ads')).toHaveCount(0)
await page.locator('#play').click()
if (start)
await page.locator('#play').click()
}
for (const core of ['published', 'candidate']) {
for (const core of ['published-4.5.5', 'published', 'candidate']) {
test(`${core} core / candidate Ads: skip before play suppresses preroll without autoplay`, async ({ page }, testInfo) => {
await openAds(page, core, 'candidate', { html: 'pending ad' }, testInfo, false)
await page.evaluate(() => window.art.plugins.artplayerPluginAds.skip())
expect(await page.evaluate(() => window.art.template.$video.paused)).toBe(true)
expect(await page.evaluate(() => window.adsEvents.filter(event => event.name === 'skip').length)).toBe(1)
await page.locator('#play').click()
await expect.poll(() => page.evaluate(() => window.art.currentTime)).toBeGreaterThan(0.3)
await expect(page.locator('.artplayer-plugin-ads')).toHaveCount(0)
})
test(`${core} core / candidate Ads: immediate zero-threshold skip completes only once`, async ({ page }, testInfo) => {
await openAds(page, core, 'candidate', { html: 'ad', playDuration: 0, totalDuration: 1 }, testInfo)
await page.locator('.artplayer-plugin-ads-close').click()
await expect(page.locator('.artplayer-plugin-ads')).toBeHidden()
await page.evaluate(() => {
window.art.plugins.artplayerPluginAds.skip()
window.art.plugins.artplayerPluginAds.play()
})
await page.waitForTimeout(1200)
expect(await page.evaluate(() => window.adsEvents.filter(event => event.name === 'skip').length)).toBe(1)
await expect.poll(() => page.evaluate(() => window.art.currentTime)).toBeGreaterThan(0.3)
})
test(`${core} core / candidate Ads: destroy releases actual ad media and prevents delayed playback`, async ({ page }, testInfo) => {
await page.addInitScript(() => {
window.adsPlayCalls = []
const play = HTMLMediaElement.prototype.play
HTMLMediaElement.prototype.play = function () {
window.adsPlayCalls.push(this.className)
return play.call(this)
}
})
await openAds(page, core, 'candidate', { video: '/test/pattern.mp4', muted: true, totalDuration: 2 }, testInfo)
await expect.poll(() => page.locator('.artplayer-plugin-ads-video').evaluate(video => video.currentTime)).toBeGreaterThan(0.2)
const before = await page.evaluate(() => {
window.retainedAd = document.querySelector('.artplayer-plugin-ads-video')
window.art.destroy(false)
window.retainedAd.dispatchEvent(new Event('loadedmetadata'))
window.retainedAd.dispatchEvent(new Event('error'))
return window.adsPlayCalls.length
})
await expect(page.locator('.artplayer-plugin-ads')).toHaveCount(0)
await page.waitForTimeout(2200)
expect(await page.evaluate(() => ({
paused: window.retainedAd.paused,
src: window.retainedAd.getAttribute('src'),
calls: window.adsPlayCalls.length,
skips: window.adsEvents.filter(event => event.name === 'skip').length,
hasTemplate: Boolean(window.art.template.$ads),
}))).toEqual({ paused: true, src: null, calls: before, skips: 0, hasTemplate: false })
})
for (const target of ['ad', 'content']) {
test(`${core} core / candidate Ads: observes ${target} play rejection without an unhandled error`, async ({ page }, testInfo) => {
await page.addInitScript((target) => {
window.adsWarnings = []
const warn = console.warn
console.warn = function (...args) {
window.adsWarnings.push(args.map(value => value?.message ? { name: value.name, message: value.message } : value))
return warn.apply(this, args)
}
const play = HTMLMediaElement.prototype.play
HTMLMediaElement.prototype.play = function () {
const selected = target === 'ad'
? this.classList.contains('artplayer-plugin-ads-video')
: this === window.art?.template.$video && document.querySelector('.artplayer-plugin-ads')
if (selected)
return Promise.reject(new DOMException(`Ads candidate ${target} rejection`, 'NotAllowedError'))
return play.call(this)
}
}, target)
const option = target === 'ad' ? { video: '/test/pattern.mp4', muted: true, totalDuration: 10 } : { html: 'ad', totalDuration: 1 }
await openAds(page, core, 'candidate', option, testInfo)
await expect.poll(() => page.evaluate(() => window.adsEvents.filter(event => event.name === 'skip').length)).toBe(1)
await expect(page.locator('.artplayer-plugin-ads')).toBeHidden()
await expect.poll(() => page.evaluate(() => window.adsWarnings)).toEqual([
['Artplayer Ads:', { name: 'NotAllowedError', message: `Ads candidate ${target} rejection` }],
])
if (target === 'ad')
await expect.poll(() => page.evaluate(() => window.art.currentTime)).toBeGreaterThan(0.3)
else
expect(await page.evaluate(() => window.art.template.$video.paused)).toBe(true)
})
}
for (const plugin of ['published', 'candidate']) {
const label = `${core} core / ${plugin} Ads`
@@ -71,7 +158,7 @@ for (const core of ['published', 'candidate']) {
const root = page.locator('.artplayer-plugin-ads')
await expect(root).toBeVisible()
await expect.poll(() => page.locator('img[alt="Ad artwork"]').evaluate(image => image.complete && image.naturalWidth > 0)).toBe(true)
expect(await page.evaluate(() => window.art.video.paused)).toBe(true)
expect(await page.evaluate(() => window.art.template.$video.paused)).toBe(true)
await page.locator('.artplayer-plugin-ads-close').click()
expect(await page.evaluate(() => window.adsEvents.filter(event => event.name === 'skip').length)).toBe(0)
await expect(page.locator('.artplayer-plugin-ads-close')).toHaveText('关闭广告')
@@ -183,7 +270,7 @@ for (const core of ['published', 'candidate']) {
})
const play = HTMLMediaElement.prototype.play
HTMLMediaElement.prototype.play = function () {
if (this === window.art?.video && document.querySelector('.artplayer-plugin-ads'))
if (this === window.art?.template.$video && document.querySelector('.artplayer-plugin-ads'))
return Promise.reject(new DOMException('Ads test controlled content rejection', 'NotAllowedError'))
return play.call(this)
}
@@ -191,7 +278,7 @@ for (const core of ['published', 'candidate']) {
await openAds(page, core, 'published', { html: 'ad', totalDuration: 1 }, testInfo)
await expect.poll(() => page.evaluate(() => window.adsRejected)).toEqual([{ name: 'NotAllowedError', message: 'Ads test controlled content rejection' }])
await expect(page.locator('.artplayer-plugin-ads')).toBeHidden()
expect(await page.evaluate(() => window.art.video.paused)).toBe(true)
expect(await page.evaluate(() => window.art.template.$video.paused)).toBe(true)
expect(await page.evaluate(() => window.adsEvents.filter(event => event.name === 'skip').length)).toBe(1)
})
+6 -1
View File
@@ -59,6 +59,11 @@ for (const extension of ['mp4', 'webm']) {
const name = `test/browser/media/pattern.${extension}`
add(`/test/pattern.${extension}`, fs.readFileSync(path.join(workspace, name)), { kind: 'generated-media', file: name })
}
const adsCore = JSON.parse(fs.readFileSync(path.join(workspace, 'refactor/baselines/ads-core.json'), 'utf8')).release
const adsCoreMember = `package/${adsCore.manifest.main}`
const adsCoreBytes = readMember(await ensureArchive(adsCore), adsCoreMember)
assert.equal(hash(adsCoreBytes), adsCore.files[adsCoreMember], 'Historical Ads core differs from frozen release')
add('/published-4.5.5/artplayer.js', adsCoreBytes, { kind: 'npm-release', version: adsCore.version, integrity: adsCore.integrity, member: adsCoreMember })
add('/test/audio-tone.m4a', fs.readFileSync(path.join(workspace, 'test/browser/media/audio-tone.m4a')), { kind: 'generated-media', file: 'test/browser/media/audio-tone.m4a' })
const mime = { '.html': 'text/html', '.js': 'text/javascript', '.mjs': 'text/javascript', '.json': 'application/json', '.css': 'text/css', '.mp4': 'video/mp4', '.m4a': 'audio/mp4', '.webm': 'video/webm', '.vtt': 'text/vtt', '.svg': 'image/svg+xml', '.png': 'image/png', '.jpg': 'image/jpeg' }
@@ -123,7 +128,7 @@ const server = http.createServer((req, res) => {
if (url.pathname === '/test/player.html') {
const core = url.searchParams.get('core') || 'candidate'
const chapter = url.searchParams.get('chapter') || 'candidate'
assert(['candidate', 'published'].includes(core) && ['candidate', 'published'].includes(chapter), 'Invalid test combination')
assert(['candidate', 'published', 'published-4.5.5'].includes(core) && ['candidate', 'published'].includes(chapter), 'Invalid test combination')
const html = fs.readFileSync(path.join(workspace, 'test/browser/player.html'), 'utf8')
.replace('__CORE__', core)
.replace('__CHAPTER__', chapter)
+18
View File
@@ -109,11 +109,28 @@ export function adsEnvironment(t) {
calls.push(['ad.pause'])
this.paused = true
}
remove() {
if (this.parentNode)
this.parentNode.children = this.parentNode.children.filter(child => child !== this)
this.parentNode = null
calls.push(['remove', this.className])
}
removeAttribute(name) {
calls.push(['removeAttribute', this.className, name])
delete this[name]
}
load() {
calls.push(['ad.load'])
}
}
const prefix = 'artplayer-plugin-ads'
function append(parent, value) {
if (typeof value !== 'string') {
parent.children.push(value)
value.parentNode = parent
return value
}
const match = value.match(/^<(\w+)\s[^>]*class="([^"]*)"[^>]*>([\s\S]*)<\/\w+>$/)
@@ -121,6 +138,7 @@ export function adsEnvironment(t) {
const [, tag, className, inner] = match
const node = new Node(tag, className, inner)
parent.children.push(node)
node.parentNode = parent
for (const child of inner.matchAll(/<div class="([^"]+)"[^>]*>([\s\S]*?)<\/div>/g))
node.children.push(new Node('div', child[1], child[2]))
return node