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

+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"]
}