mirror of
https://github.com/zhw2590582/ArtPlayer.git
synced 2026-10-22 05:11:20 -08:00
refactor(ads): [PKG-ADS-03] split TypeScript lifecycle and preserve legacy behavior
This commit is contained in:
1 parent
069ad4aae1
commit
39e2d4cdfb
37 files changed
+3083
-788
No files matched your search
@@ -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.
|
||||
@@ -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)
|
||||
|
||||
@@ -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
@@ -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()
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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"]
|
||||
}
|
||||
Reference in new issue
Block a user