eb-player 2.0.20 → 2.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/build/ebplayer.bundle.js +767 -140
- package/dist/build/ebplayer.bundle.js.map +1 -1
- package/dist/build/types/src/core/command-handler.d.ts +50 -0
- package/dist/build/types/src/core/command-handler.d.ts.map +1 -0
- package/dist/build/types/src/core/config.d.ts +195 -0
- package/dist/build/types/src/core/config.d.ts.map +1 -0
- package/dist/build/types/src/core/engine-state-sync.d.ts +38 -0
- package/dist/build/types/src/core/engine-state-sync.d.ts.map +1 -0
- package/dist/build/types/src/core/event-bus.d.ts +91 -0
- package/dist/build/types/src/core/event-bus.d.ts.map +1 -0
- package/dist/build/types/src/core/fsm.d.ts +30 -0
- package/dist/build/types/src/core/fsm.d.ts.map +1 -0
- package/dist/build/types/src/core/i18n.d.ts +51 -0
- package/dist/build/types/src/core/i18n.d.ts.map +1 -0
- package/dist/build/types/src/core/index.d.ts +23 -0
- package/dist/build/types/src/core/index.d.ts.map +1 -0
- package/dist/build/types/src/core/lifecycle.d.ts +96 -0
- package/dist/build/types/src/core/lifecycle.d.ts.map +1 -0
- package/dist/build/types/src/core/player-state.d.ts +15 -0
- package/dist/build/types/src/core/player-state.d.ts.map +1 -0
- package/dist/build/types/src/core/types.d.ts +85 -0
- package/dist/build/types/src/core/types.d.ts.map +1 -0
- package/dist/build/types/src/eb-player-standalone.d.ts +23 -0
- package/dist/build/types/src/eb-player-standalone.d.ts.map +1 -0
- package/dist/build/types/src/eb-player.d.ts +56 -0
- package/dist/build/types/src/eb-player.d.ts.map +1 -0
- package/dist/build/types/src/engines/abr/dash.d.ts +56 -0
- package/dist/build/types/src/engines/abr/dash.d.ts.map +1 -0
- package/dist/build/types/src/engines/abr/hls.d.ts +105 -0
- package/dist/build/types/src/engines/abr/hls.d.ts.map +1 -0
- package/dist/build/types/src/engines/base-engine.d.ts +72 -0
- package/dist/build/types/src/engines/base-engine.d.ts.map +1 -0
- package/dist/build/types/src/engines/cdn-loader.d.ts +20 -0
- package/dist/build/types/src/engines/cdn-loader.d.ts.map +1 -0
- package/dist/build/types/src/engines/cdn-token-manager.d.ts +137 -0
- package/dist/build/types/src/engines/cdn-token-manager.d.ts.map +1 -0
- package/dist/build/types/src/engines/dash.d.ts +100 -0
- package/dist/build/types/src/engines/dash.d.ts.map +1 -0
- package/dist/build/types/src/engines/drm.d.ts +54 -0
- package/dist/build/types/src/engines/drm.d.ts.map +1 -0
- package/dist/build/types/src/engines/hls-discontinuity-patch.d.ts +43 -0
- package/dist/build/types/src/engines/hls-discontinuity-patch.d.ts.map +1 -0
- package/dist/build/types/src/engines/hls.d.ts +53 -0
- package/dist/build/types/src/engines/hls.d.ts.map +1 -0
- package/dist/build/types/src/engines/index.d.ts +26 -0
- package/dist/build/types/src/engines/index.d.ts.map +1 -0
- package/dist/build/types/src/engines/ios/hls.d.ts +20 -0
- package/dist/build/types/src/engines/ios/hls.d.ts.map +1 -0
- package/dist/build/types/src/engines/poster/hls.d.ts +35 -0
- package/dist/build/types/src/engines/poster/hls.d.ts.map +1 -0
- package/dist/build/types/src/engines/retry/dash.d.ts +51 -0
- package/dist/build/types/src/engines/retry/dash.d.ts.map +1 -0
- package/dist/build/types/src/engines/retry/hls.d.ts +47 -0
- package/dist/build/types/src/engines/retry/hls.d.ts.map +1 -0
- package/dist/build/types/src/engines/segment-token-loader.d.ts +117 -0
- package/dist/build/types/src/engines/segment-token-loader.d.ts.map +1 -0
- package/dist/build/types/src/engines/snapshot/dash.d.ts +55 -0
- package/dist/build/types/src/engines/snapshot/dash.d.ts.map +1 -0
- package/dist/build/types/src/engines/snapshot/hls.d.ts +106 -0
- package/dist/build/types/src/engines/snapshot/hls.d.ts.map +1 -0
- package/dist/build/types/src/engines/stall-watchdog.d.ts +41 -0
- package/dist/build/types/src/engines/stall-watchdog.d.ts.map +1 -0
- package/dist/build/types/src/integrations/ads-manager.d.ts +79 -0
- package/dist/build/types/src/integrations/ads-manager.d.ts.map +1 -0
- package/dist/build/types/src/integrations/chromecast-manager.d.ts +50 -0
- package/dist/build/types/src/integrations/chromecast-manager.d.ts.map +1 -0
- package/dist/build/types/src/integrations/epg-manager.d.ts +22 -0
- package/dist/build/types/src/integrations/epg-manager.d.ts.map +1 -0
- package/dist/build/types/src/integrations/index.d.ts +7 -0
- package/dist/build/types/src/integrations/index.d.ts.map +1 -0
- package/dist/build/types/src/integrations/intro-stream-manager.d.ts +62 -0
- package/dist/build/types/src/integrations/intro-stream-manager.d.ts.map +1 -0
- package/dist/build/types/src/integrations/p2p-manager.d.ts +42 -0
- package/dist/build/types/src/integrations/p2p-manager.d.ts.map +1 -0
- package/dist/build/types/src/integrations/playlist-manager.d.ts +21 -0
- package/dist/build/types/src/integrations/playlist-manager.d.ts.map +1 -0
- package/dist/build/types/src/skin/bars/bottom-bar.d.ts +16 -0
- package/dist/build/types/src/skin/bars/bottom-bar.d.ts.map +1 -0
- package/dist/build/types/src/skin/bars/middle-bar.d.ts +16 -0
- package/dist/build/types/src/skin/bars/middle-bar.d.ts.map +1 -0
- package/dist/build/types/src/skin/bars/top-bar.d.ts +15 -0
- package/dist/build/types/src/skin/bars/top-bar.d.ts.map +1 -0
- package/dist/build/types/src/skin/base-component.d.ts +69 -0
- package/dist/build/types/src/skin/base-component.d.ts.map +1 -0
- package/dist/build/types/src/skin/brand/forja-playlist-bar.d.ts +15 -0
- package/dist/build/types/src/skin/brand/forja-playlist-bar.d.ts.map +1 -0
- package/dist/build/types/src/skin/brand/snrt-radio-carousel.d.ts +16 -0
- package/dist/build/types/src/skin/brand/snrt-radio-carousel.d.ts.map +1 -0
- package/dist/build/types/src/skin/component-registry.d.ts +16 -0
- package/dist/build/types/src/skin/component-registry.d.ts.map +1 -0
- package/dist/build/types/src/skin/controllers/auto-hide.d.ts +27 -0
- package/dist/build/types/src/skin/controllers/auto-hide.d.ts.map +1 -0
- package/dist/build/types/src/skin/controllers/keyboard.d.ts +29 -0
- package/dist/build/types/src/skin/controllers/keyboard.d.ts.map +1 -0
- package/dist/build/types/src/skin/controls/cast-button.d.ts +15 -0
- package/dist/build/types/src/skin/controls/cast-button.d.ts.map +1 -0
- package/dist/build/types/src/skin/controls/channel-name.d.ts +16 -0
- package/dist/build/types/src/skin/controls/channel-name.d.ts.map +1 -0
- package/dist/build/types/src/skin/controls/forward-button.d.ts +15 -0
- package/dist/build/types/src/skin/controls/forward-button.d.ts.map +1 -0
- package/dist/build/types/src/skin/controls/fullscreen-button.d.ts +30 -0
- package/dist/build/types/src/skin/controls/fullscreen-button.d.ts.map +1 -0
- package/dist/build/types/src/skin/controls/info-button.d.ts +10 -0
- package/dist/build/types/src/skin/controls/info-button.d.ts.map +1 -0
- package/dist/build/types/src/skin/controls/live-sync-button.d.ts +20 -0
- package/dist/build/types/src/skin/controls/live-sync-button.d.ts.map +1 -0
- package/dist/build/types/src/skin/controls/pip-button.d.ts +21 -0
- package/dist/build/types/src/skin/controls/pip-button.d.ts.map +1 -0
- package/dist/build/types/src/skin/controls/play-pause-button.d.ts +14 -0
- package/dist/build/types/src/skin/controls/play-pause-button.d.ts.map +1 -0
- package/dist/build/types/src/skin/controls/rewind-button.d.ts +15 -0
- package/dist/build/types/src/skin/controls/rewind-button.d.ts.map +1 -0
- package/dist/build/types/src/skin/controls/seekbar.d.ts +48 -0
- package/dist/build/types/src/skin/controls/seekbar.d.ts.map +1 -0
- package/dist/build/types/src/skin/controls/settings-panel.d.ts +53 -0
- package/dist/build/types/src/skin/controls/settings-panel.d.ts.map +1 -0
- package/dist/build/types/src/skin/controls/share-button.d.ts +11 -0
- package/dist/build/types/src/skin/controls/share-button.d.ts.map +1 -0
- package/dist/build/types/src/skin/controls/time-display.d.ts +19 -0
- package/dist/build/types/src/skin/controls/time-display.d.ts.map +1 -0
- package/dist/build/types/src/skin/controls/volume-control.d.ts +21 -0
- package/dist/build/types/src/skin/controls/volume-control.d.ts.map +1 -0
- package/dist/build/types/src/skin/icons/icons-modern.d.ts +10 -0
- package/dist/build/types/src/skin/icons/icons-modern.d.ts.map +1 -0
- package/dist/build/types/src/skin/icons/icons.d.ts +10 -0
- package/dist/build/types/src/skin/icons/icons.d.ts.map +1 -0
- package/dist/build/types/src/skin/icons/sprite.d.ts +39 -0
- package/dist/build/types/src/skin/icons/sprite.d.ts.map +1 -0
- package/dist/build/types/src/skin/index.d.ts +24 -0
- package/dist/build/types/src/skin/index.d.ts.map +1 -0
- package/dist/build/types/src/skin/overlays/error-message.d.ts +14 -0
- package/dist/build/types/src/skin/overlays/error-message.d.ts.map +1 -0
- package/dist/build/types/src/skin/overlays/info-overlay.d.ts +15 -0
- package/dist/build/types/src/skin/overlays/info-overlay.d.ts.map +1 -0
- package/dist/build/types/src/skin/overlays/loading-spinner.d.ts +17 -0
- package/dist/build/types/src/skin/overlays/loading-spinner.d.ts.map +1 -0
- package/dist/build/types/src/skin/overlays/preroll-overlay.d.ts +39 -0
- package/dist/build/types/src/skin/overlays/preroll-overlay.d.ts.map +1 -0
- package/dist/build/types/src/skin/overlays/socials-overlay.d.ts +17 -0
- package/dist/build/types/src/skin/overlays/socials-overlay.d.ts.map +1 -0
- package/dist/build/types/src/skin/overlays/toast-notification.d.ts +18 -0
- package/dist/build/types/src/skin/overlays/toast-notification.d.ts.map +1 -0
- package/dist/build/types/src/skin/skin-root.d.ts +88 -0
- package/dist/build/types/src/skin/skin-root.d.ts.map +1 -0
- package/dist/build/types/src/utils/chapters.d.ts +16 -0
- package/dist/build/types/src/utils/chapters.d.ts.map +1 -0
- package/dist/build/types/src/utils/format-duration.d.ts +9 -0
- package/dist/build/types/src/utils/format-duration.d.ts.map +1 -0
- package/dist/build/types/src/utils/format-wall-clock.d.ts +6 -0
- package/dist/build/types/src/utils/format-wall-clock.d.ts.map +1 -0
- package/dist/build/types/src/utils/settings-helpers.d.ts +41 -0
- package/dist/build/types/src/utils/settings-helpers.d.ts.map +1 -0
- package/package.json +34 -33
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
(global = typeof globalThis !== 'undefined' ? globalThis : global || self, factory(global.EBPlayer = {}));
|
|
5
5
|
})(this, (function (exports) { 'use strict';
|
|
6
6
|
|
|
7
|
-
var __EB_PLAYER_VERSION__ = "2.0
|
|
7
|
+
var __EB_PLAYER_VERSION__ = "2.1.0";
|
|
8
8
|
|
|
9
9
|
/**
|
|
10
10
|
* Finite State Machine for player playback state transitions.
|
|
@@ -883,7 +883,7 @@
|
|
|
883
883
|
* Copyright 2017 Google LLC
|
|
884
884
|
* SPDX-License-Identifier: BSD-3-Clause
|
|
885
885
|
*/
|
|
886
|
-
const t=globalThis,i=t=>t,s=t.trustedTypes,e=s?s.createPolicy("lit-html",{createHTML:t=>t}):void 0,h="$lit$",o=`lit$${Math.random().toFixed(9).slice(2)}$`,n="?"+o,r=`<${n}>`,l=document,c=()=>l.createComment(""),a=t=>null===t||"object"!=typeof t&&"function"!=typeof t,u=Array.isArray,d=t=>u(t)||"function"==typeof t?.[Symbol.iterator],f="[ \t\n\f\r]",v=/<(?:(!--|\/[^a-zA-Z])|(\/?[a-zA-Z][^>\s]*)|(\/?$))/g,_=/-->/g,m=/>/g,p=RegExp(`>|${f}(?:([^\\s"'>=/]+)(${f}*=${f}*(?:[^ \t\n\f\r"'\`<>=]|("|')|))|$)`,"g"),g=/'/g,$=/"/g,y=/^(?:script|style|textarea|title)$/i,x=t=>(i,...s)=>({_$litType$:t,strings:i,values:s}),b=x(1),w=x(2),E=Symbol.for("lit-noChange"),A=Symbol.for("lit-nothing"),C=new WeakMap,P=l.createTreeWalker(l,129);function V(t,i){if(!u(t)||!t.hasOwnProperty("raw"))throw Error("invalid template strings array");return void 0!==e?e.createHTML(i):i}const N=(t,i)=>{const s=t.length-1,e=[];let n,l=2===i?"<svg>":3===i?"<math>":"",c=v;for(let i=0;i<s;i++){const s=t[i];let a,u,d=-1,f=0;for(;f<s.length&&(c.lastIndex=f,u=c.exec(s),null!==u);)f=c.lastIndex,c===v?"!--"===u[1]?c=_:void 0!==u[1]?c=m:void 0!==u[2]?(y.test(u[2])&&(n=RegExp("</"+u[2],"g")),c=p):void 0!==u[3]&&(c=p):c===p?">"===u[0]?(c=n??v,d=-1):void 0===u[1]?d=-2:(d=c.lastIndex-u[2].length,a=u[1],c=void 0===u[3]?p:'"'===u[3]?$:g):c===$||c===g?c=p:c===_||c===m?c=v:(c=p,n=void 0);const x=c===p&&t[i+1].startsWith("/>")?" ":"";l+=c===v?s+r:d>=0?(e.push(a),s.slice(0,d)+h+s.slice(d)+o+x):s+o+(-2===d?i:x);}return [V(t,l+(t[s]||"<?>")+(2===i?"</svg>":3===i?"</math>":"")),e]};class S{constructor({strings:t,_$litType$:i},e){let r;this.parts=[];let l=0,a=0;const u=t.length-1,d=this.parts,[f,v]=N(t,i);if(this.el=S.createElement(f,e),P.currentNode=this.el.content,2===i||3===i){const t=this.el.content.firstChild;t.replaceWith(...t.childNodes);}for(;null!==(r=P.nextNode())&&d.length<u;){if(1===r.nodeType){if(r.hasAttributes())for(const t of r.getAttributeNames())if(t.endsWith(h)){const i=v[a++],s=r.getAttribute(t).split(o),e=/([.?@])?(.*)/.exec(i);d.push({type:1,index:l,name:e[2],strings:s,ctor:"."===e[1]?I:"?"===e[1]?L:"@"===e[1]?z:H}),r.removeAttribute(t);}else t.startsWith(o)&&(d.push({type:6,index:l}),r.removeAttribute(t));if(y.test(r.tagName)){const t=r.textContent.split(o),i=t.length-1;if(i>0){r.textContent=s?s.emptyScript:"";for(let s=0;s<i;s++)r.append(t[s],c()),P.nextNode(),d.push({type:2,index:++l});r.append(t[i],c());}}}else if(8===r.nodeType)if(r.data===n)d.push({type:2,index:l});else {let t=-1;for(;-1!==(t=r.data.indexOf(o,t+1));)d.push({type:7,index:l}),t+=o.length-1;}l++;}}static createElement(t,i){const s=l.createElement("template");return s.innerHTML=t,s}}function M(t,i,s=t,e){if(i===E)return i;let h=void 0!==e?s._$Co?.[e]:s._$Cl;const o=a(i)?void 0:i._$litDirective$;return h?.constructor!==o&&(h?._$AO?.(false),void 0===o?h=void 0:(h=new o(t),h._$AT(t,s,e)),void 0!==e?(s._$Co??=[])[e]=h:s._$Cl=h),void 0!==h&&(i=M(t,h._$AS(t,i.values),h,e)),i}class R{constructor(t,i){this._$AV=[],this._$AN=void 0,this._$AD=t,this._$AM=i;}get parentNode(){return this._$AM.parentNode}get _$AU(){return this._$AM._$AU}u(t){const{el:{content:i},parts:s}=this._$AD,e=(t?.creationScope??l).importNode(i,true);P.currentNode=e;let h=P.nextNode(),o=0,n=0,r=s[0];for(;void 0!==r;){if(o===r.index){let i;2===r.type?i=new k(h,h.nextSibling,this,t):1===r.type?i=new r.ctor(h,r.name,r.strings,this,t):6===r.type&&(i=new Z(h,this,t)),this._$AV.push(i),r=s[++n];}o!==r?.index&&(h=P.nextNode(),o++);}return P.currentNode=l,e}p(t){let i=0;for(const s of this._$AV) void 0!==s&&(void 0!==s.strings?(s._$AI(t,s,i),i+=s.strings.length-2):s._$AI(t[i])),i++;}}class k{get _$AU(){return this._$AM?._$AU??this._$Cv}constructor(t,i,s,e){this.type=2,this._$AH=A,this._$AN=void 0,this._$AA=t,this._$AB=i,this._$AM=s,this.options=e,this._$Cv=e?.isConnected??true;}get parentNode(){let t=this._$AA.parentNode;const i=this._$AM;return void 0!==i&&11===t?.nodeType&&(t=i.parentNode),t}get startNode(){return this._$AA}get endNode(){return this._$AB}_$AI(t,i=this){t=M(this,t,i),a(t)?t===A||null==t||""===t?(this._$AH!==A&&this._$AR(),this._$AH=A):t!==this._$AH&&t!==E&&this._(t):void 0!==t._$litType$?this.$(t):void 0!==t.nodeType?this.T(t):d(t)?this.k(t):this._(t);}O(t){return this._$AA.parentNode.insertBefore(t,this._$AB)}T(t){this._$AH!==t&&(this._$AR(),this._$AH=this.O(t));}_(t){this._$AH!==A&&a(this._$AH)?this._$AA.nextSibling.data=t:this.T(l.createTextNode(t)),this._$AH=t;}$(t){const{values:i,_$litType$:s}=t,e="number"==typeof s?this._$AC(t):(void 0===s.el&&(s.el=S.createElement(V(s.h,s.h[0]),this.options)),s);if(this._$AH?._$AD===e)this._$AH.p(i);else {const t=new R(e,this),s=t.u(this.options);t.p(i),this.T(s),this._$AH=t;}}_$AC(t){let i=C.get(t.strings);return void 0===i&&C.set(t.strings,i=new S(t)),i}k(t){u(this._$AH)||(this._$AH=[],this._$AR());const i=this._$AH;let s,e=0;for(const h of t)e===i.length?i.push(s=new k(this.O(c()),this.O(c()),this,this.options)):s=i[e],s._$AI(h),e++;e<i.length&&(this._$AR(s&&s._$AB.nextSibling,e),i.length=e);}_$AR(t=this._$AA.nextSibling,s){for(this._$AP?.(false,true,s);t!==this._$AB;){const s=i(t).nextSibling;i(t).remove(),t=s;}}setConnected(t){ void 0===this._$AM&&(this._$Cv=t,this._$AP?.(t));}}class H{get tagName(){return this.element.tagName}get _$AU(){return this._$AM._$AU}constructor(t,i,s,e,h){this.type=1,this._$AH=A,this._$AN=void 0,this.element=t,this.name=i,this._$AM=e,this.options=h,s.length>2||""!==s[0]||""!==s[1]?(this._$AH=Array(s.length-1).fill(new String),this.strings=s):this._$AH=A;}_$AI(t,i=this,s,e){const h=this.strings;let o=false;if(void 0===h)t=M(this,t,i,0),o=!a(t)||t!==this._$AH&&t!==E,o&&(this._$AH=t);else {const e=t;let n,r;for(t=h[0],n=0;n<h.length-1;n++)r=M(this,e[s+n],i,n),r===E&&(r=this._$AH[n]),o||=!a(r)||r!==this._$AH[n],r===A?t=A:t!==A&&(t+=(r??"")+h[n+1]),this._$AH[n]=r;}o&&!e&&this.j(t);}j(t){t===A?this.element.removeAttribute(this.name):this.element.setAttribute(this.name,t??"");}}class I extends H{constructor(){super(...arguments),this.type=3;}j(t){this.element[this.name]=t===A?void 0:t;}}class L extends H{constructor(){super(...arguments),this.type=4;}j(t){this.element.toggleAttribute(this.name,!!t&&t!==A);}}class z extends H{constructor(t,i,s,e,h){super(t,i,s,e,h),this.type=5;}_$AI(t,i=this){if((t=M(this,t,i,0)??A)===E)return;const s=this._$AH,e=t===A&&s!==A||t.capture!==s.capture||t.once!==s.once||t.passive!==s.passive,h=t!==A&&(s===A||e);e&&this.element.removeEventListener(this.name,this,s),h&&this.element.addEventListener(this.name,this,t),this._$AH=t;}handleEvent(t){"function"==typeof this._$AH?this._$AH.call(this.options?.host??this.element,t):this._$AH.handleEvent(t);}}class Z{constructor(t,i,s){this.element=t,this.type=6,this._$AN=void 0,this._$AM=i,this.options=s;}get _$AU(){return this._$AM._$AU}_$AI(t){M(this,t);}}const B=t.litHtmlPolyfillSupport;B?.(S,k),(t.litHtmlVersions??=[]).push("3.3.
|
|
886
|
+
const t=globalThis,i=t=>t,s=t.trustedTypes,e=s?s.createPolicy("lit-html",{createHTML:t=>t}):void 0,h="$lit$",o=`lit$${Math.random().toFixed(9).slice(2)}$`,n="?"+o,r=`<${n}>`,l=document,c=()=>l.createComment(""),a=t=>null===t||"object"!=typeof t&&"function"!=typeof t,u=Array.isArray,d=t=>u(t)||"function"==typeof t?.[Symbol.iterator],f="[ \t\n\f\r]",v=/<(?:(!--|\/[^a-zA-Z])|(\/?[a-zA-Z][^>\s]*)|(\/?$))/g,_=/-->/g,m=/>/g,p=RegExp(`>|${f}(?:([^\\s"'>=/]+)(${f}*=${f}*(?:[^ \t\n\f\r"'\`<>=]|("|')|))|$)`,"g"),g=/'/g,$=/"/g,y=/^(?:script|style|textarea|title)$/i,x=t=>(i,...s)=>({_$litType$:t,strings:i,values:s}),b=x(1),w=x(2),E=Symbol.for("lit-noChange"),A=Symbol.for("lit-nothing"),C=new WeakMap,P=l.createTreeWalker(l,129);function V(t,i){if(!u(t)||!t.hasOwnProperty("raw"))throw Error("invalid template strings array");return void 0!==e?e.createHTML(i):i}const N=(t,i)=>{const s=t.length-1,e=[];let n,l=2===i?"<svg>":3===i?"<math>":"",c=v;for(let i=0;i<s;i++){const s=t[i];let a,u,d=-1,f=0;for(;f<s.length&&(c.lastIndex=f,u=c.exec(s),null!==u);)f=c.lastIndex,c===v?"!--"===u[1]?c=_:void 0!==u[1]?c=m:void 0!==u[2]?(y.test(u[2])&&(n=RegExp("</"+u[2],"g")),c=p):void 0!==u[3]&&(c=p):c===p?">"===u[0]?(c=n??v,d=-1):void 0===u[1]?d=-2:(d=c.lastIndex-u[2].length,a=u[1],c=void 0===u[3]?p:'"'===u[3]?$:g):c===$||c===g?c=p:c===_||c===m?c=v:(c=p,n=void 0);const x=c===p&&t[i+1].startsWith("/>")?" ":"";l+=c===v?s+r:d>=0?(e.push(a),s.slice(0,d)+h+s.slice(d)+o+x):s+o+(-2===d?i:x);}return [V(t,l+(t[s]||"<?>")+(2===i?"</svg>":3===i?"</math>":"")),e]};class S{constructor({strings:t,_$litType$:i},e){let r;this.parts=[];let l=0,a=0;const u=t.length-1,d=this.parts,[f,v]=N(t,i);if(this.el=S.createElement(f,e),P.currentNode=this.el.content,2===i||3===i){const t=this.el.content.firstChild;t.replaceWith(...t.childNodes);}for(;null!==(r=P.nextNode())&&d.length<u;){if(1===r.nodeType){if(r.hasAttributes())for(const t of r.getAttributeNames())if(t.endsWith(h)){const i=v[a++],s=r.getAttribute(t).split(o),e=/([.?@])?(.*)/.exec(i);d.push({type:1,index:l,name:e[2],strings:s,ctor:"."===e[1]?I:"?"===e[1]?L:"@"===e[1]?z:H}),r.removeAttribute(t);}else t.startsWith(o)&&(d.push({type:6,index:l}),r.removeAttribute(t));if(y.test(r.tagName)){const t=r.textContent.split(o),i=t.length-1;if(i>0){r.textContent=s?s.emptyScript:"";for(let s=0;s<i;s++)r.append(t[s],c()),P.nextNode(),d.push({type:2,index:++l});r.append(t[i],c());}}}else if(8===r.nodeType)if(r.data===n)d.push({type:2,index:l});else {let t=-1;for(;-1!==(t=r.data.indexOf(o,t+1));)d.push({type:7,index:l}),t+=o.length-1;}l++;}}static createElement(t,i){const s=l.createElement("template");return s.innerHTML=t,s}}function M(t,i,s=t,e){if(i===E)return i;let h=void 0!==e?s._$Co?.[e]:s._$Cl;const o=a(i)?void 0:i._$litDirective$;return h?.constructor!==o&&(h?._$AO?.(false),void 0===o?h=void 0:(h=new o(t),h._$AT(t,s,e)),void 0!==e?(s._$Co??=[])[e]=h:s._$Cl=h),void 0!==h&&(i=M(t,h._$AS(t,i.values),h,e)),i}class R{constructor(t,i){this._$AV=[],this._$AN=void 0,this._$AD=t,this._$AM=i;}get parentNode(){return this._$AM.parentNode}get _$AU(){return this._$AM._$AU}u(t){const{el:{content:i},parts:s}=this._$AD,e=(t?.creationScope??l).importNode(i,true);P.currentNode=e;let h=P.nextNode(),o=0,n=0,r=s[0];for(;void 0!==r;){if(o===r.index){let i;2===r.type?i=new k(h,h.nextSibling,this,t):1===r.type?i=new r.ctor(h,r.name,r.strings,this,t):6===r.type&&(i=new Z(h,this,t)),this._$AV.push(i),r=s[++n];}o!==r?.index&&(h=P.nextNode(),o++);}return P.currentNode=l,e}p(t){let i=0;for(const s of this._$AV) void 0!==s&&(void 0!==s.strings?(s._$AI(t,s,i),i+=s.strings.length-2):s._$AI(t[i])),i++;}}class k{get _$AU(){return this._$AM?._$AU??this._$Cv}constructor(t,i,s,e){this.type=2,this._$AH=A,this._$AN=void 0,this._$AA=t,this._$AB=i,this._$AM=s,this.options=e,this._$Cv=e?.isConnected??true;}get parentNode(){let t=this._$AA.parentNode;const i=this._$AM;return void 0!==i&&11===t?.nodeType&&(t=i.parentNode),t}get startNode(){return this._$AA}get endNode(){return this._$AB}_$AI(t,i=this){t=M(this,t,i),a(t)?t===A||null==t||""===t?(this._$AH!==A&&this._$AR(),this._$AH=A):t!==this._$AH&&t!==E&&this._(t):void 0!==t._$litType$?this.$(t):void 0!==t.nodeType?this.T(t):d(t)?this.k(t):this._(t);}O(t){return this._$AA.parentNode.insertBefore(t,this._$AB)}T(t){this._$AH!==t&&(this._$AR(),this._$AH=this.O(t));}_(t){this._$AH!==A&&a(this._$AH)?this._$AA.nextSibling.data=t:this.T(l.createTextNode(t)),this._$AH=t;}$(t){const{values:i,_$litType$:s}=t,e="number"==typeof s?this._$AC(t):(void 0===s.el&&(s.el=S.createElement(V(s.h,s.h[0]),this.options)),s);if(this._$AH?._$AD===e)this._$AH.p(i);else {const t=new R(e,this),s=t.u(this.options);t.p(i),this.T(s),this._$AH=t;}}_$AC(t){let i=C.get(t.strings);return void 0===i&&C.set(t.strings,i=new S(t)),i}k(t){u(this._$AH)||(this._$AH=[],this._$AR());const i=this._$AH;let s,e=0;for(const h of t)e===i.length?i.push(s=new k(this.O(c()),this.O(c()),this,this.options)):s=i[e],s._$AI(h),e++;e<i.length&&(this._$AR(s&&s._$AB.nextSibling,e),i.length=e);}_$AR(t=this._$AA.nextSibling,s){for(this._$AP?.(false,true,s);t!==this._$AB;){const s=i(t).nextSibling;i(t).remove(),t=s;}}setConnected(t){ void 0===this._$AM&&(this._$Cv=t,this._$AP?.(t));}}class H{get tagName(){return this.element.tagName}get _$AU(){return this._$AM._$AU}constructor(t,i,s,e,h){this.type=1,this._$AH=A,this._$AN=void 0,this.element=t,this.name=i,this._$AM=e,this.options=h,s.length>2||""!==s[0]||""!==s[1]?(this._$AH=Array(s.length-1).fill(new String),this.strings=s):this._$AH=A;}_$AI(t,i=this,s,e){const h=this.strings;let o=false;if(void 0===h)t=M(this,t,i,0),o=!a(t)||t!==this._$AH&&t!==E,o&&(this._$AH=t);else {const e=t;let n,r;for(t=h[0],n=0;n<h.length-1;n++)r=M(this,e[s+n],i,n),r===E&&(r=this._$AH[n]),o||=!a(r)||r!==this._$AH[n],r===A?t=A:t!==A&&(t+=(r??"")+h[n+1]),this._$AH[n]=r;}o&&!e&&this.j(t);}j(t){t===A?this.element.removeAttribute(this.name):this.element.setAttribute(this.name,t??"");}}class I extends H{constructor(){super(...arguments),this.type=3;}j(t){this.element[this.name]=t===A?void 0:t;}}class L extends H{constructor(){super(...arguments),this.type=4;}j(t){this.element.toggleAttribute(this.name,!!t&&t!==A);}}class z extends H{constructor(t,i,s,e,h){super(t,i,s,e,h),this.type=5;}_$AI(t,i=this){if((t=M(this,t,i,0)??A)===E)return;const s=this._$AH,e=t===A&&s!==A||t.capture!==s.capture||t.once!==s.once||t.passive!==s.passive,h=t!==A&&(s===A||e);e&&this.element.removeEventListener(this.name,this,s),h&&this.element.addEventListener(this.name,this,t),this._$AH=t;}handleEvent(t){"function"==typeof this._$AH?this._$AH.call(this.options?.host??this.element,t):this._$AH.handleEvent(t);}}class Z{constructor(t,i,s){this.element=t,this.type=6,this._$AN=void 0,this._$AM=i,this.options=s;}get _$AU(){return this._$AM._$AU}_$AI(t){M(this,t);}}const B=t.litHtmlPolyfillSupport;B?.(S,k),(t.litHtmlVersions??=[]).push("3.3.3");const D=(t,i,s)=>{const e=i;let h=e._$litPart$;if(void 0===h){const t=null;e._$litPart$=h=new k(i.insertBefore(c(),t),t,void 0,{});}return h._$AI(t),h};
|
|
887
887
|
|
|
888
888
|
/**
|
|
889
889
|
* Abstract base class for all skin components.
|
|
@@ -3881,15 +3881,37 @@
|
|
|
3881
3881
|
}
|
|
3882
3882
|
|
|
3883
3883
|
const IMA_SDK_URL = 'https://imasdk.googleapis.com/js/sdkloader/ima3.js';
|
|
3884
|
+
/**
|
|
3885
|
+
* G-07-9: backstop for an intro that never signals completion (all three
|
|
3886
|
+
* IntroStreamManager completion triggers lost, e.g. a swallowed throw inside
|
|
3887
|
+
* its init() that lifecycle.ts only logs). Chosen to exceed any plausible
|
|
3888
|
+
* intro asset so it can never truncate a real intro — it converts an
|
|
3889
|
+
* infinite hang into a bounded one.
|
|
3890
|
+
*/
|
|
3891
|
+
const INTRO_DEFERRAL_TIMEOUT_MS = 120000;
|
|
3892
|
+
/**
|
|
3893
|
+
* G-07-11: bound from ad-leg entry to the AdsManager being handed back by
|
|
3894
|
+
* the loader. It spans the gesture wait, loadScript, displayContainer
|
|
3895
|
+
* initialization, requestAds and the ads response, and stops there —
|
|
3896
|
+
* creative fetch, buffering and render are IMA's and are not charged to it.
|
|
3897
|
+
* Must stay below MAIN_OPEN_FALLBACK_MS (eb-player.ts) so this more precise
|
|
3898
|
+
* mechanism resolves first.
|
|
3899
|
+
*/
|
|
3900
|
+
const AD_LEG_TIMEOUT_MS = 20000;
|
|
3884
3901
|
/**
|
|
3885
3902
|
* AdsManager manages the IMA SDK preroll ad lifecycle.
|
|
3886
3903
|
*
|
|
3887
|
-
* -
|
|
3888
|
-
*
|
|
3889
|
-
*
|
|
3904
|
+
* - Defers IMA SDK loading until an intro stream (if configured) completes,
|
|
3905
|
+
* bounded by INTRO_DEFERRAL_TIMEOUT_MS (G-07-9)
|
|
3906
|
+
* - Observes a user gesture on the player root starting before that
|
|
3907
|
+
* deferral, so a click during the intro satisfies the browser autoplay
|
|
3908
|
+
* gesture requirement without a second click after the intro (G-07-11)
|
|
3909
|
+
* - Guarantees the ad leg completes exactly once, bounded by
|
|
3910
|
+
* AD_LEG_TIMEOUT_MS until the loader hands back an AdsManager (G-07-11)
|
|
3890
3911
|
* - Sets state.adPlaying based on IMA lifecycle events
|
|
3891
|
-
* - Emits 'ad-complete' on TypedEventBus
|
|
3892
|
-
* - Cleans up adsManager.destroy() and displayContainer.destroy() on abort
|
|
3912
|
+
* - Emits 'ad-complete' on TypedEventBus exactly once per instance
|
|
3913
|
+
* - Cleans up adsManager.destroy() and displayContainer.destroy() on abort,
|
|
3914
|
+
* and on any completion path that never obtained an AdsManager
|
|
3893
3915
|
*/
|
|
3894
3916
|
class AdsManager {
|
|
3895
3917
|
constructor() {
|
|
@@ -3899,111 +3921,278 @@
|
|
|
3899
3921
|
/**
|
|
3900
3922
|
* Initialize the IMA SDK preroll ad system.
|
|
3901
3923
|
* - Skips if config.ad is undefined
|
|
3902
|
-
* -
|
|
3903
|
-
*
|
|
3924
|
+
* - Defers initialization while config.preroll is set, until
|
|
3925
|
+
* 'intro-stream-complete' fires or INTRO_DEFERRAL_TIMEOUT_MS elapses
|
|
3926
|
+
* (G-07-9)
|
|
3927
|
+
* - If !config.autoplay: observes a click anywhere in the player before
|
|
3928
|
+
* the deferral, and consumes it after (required by browser autoplay
|
|
3929
|
+
* policies to avoid muted/blocked display contexts)
|
|
3930
|
+
* - The ad leg always completes exactly once, bounded by
|
|
3931
|
+
* AD_LEG_TIMEOUT_MS until the loader hands back an AdsManager (G-07-11)
|
|
3904
3932
|
* - Loads IMA SDK, creates AdDisplayContainer, AdsLoader, and AdsRequest
|
|
3905
3933
|
* - Wires ad lifecycle events to PlayerState and TypedEventBus
|
|
3906
3934
|
*/
|
|
3907
3935
|
async init(config, state, bus, video, adsContainer, signal) {
|
|
3908
3936
|
if (!config.ad)
|
|
3909
3937
|
return;
|
|
3910
|
-
//
|
|
3911
|
-
//
|
|
3912
|
-
//
|
|
3913
|
-
//
|
|
3914
|
-
//
|
|
3915
|
-
|
|
3938
|
+
// G-07-11: single idempotent completion funnel. Every path that ends the
|
|
3939
|
+
// ad leg — the watchdog, a rejected/failed SDK load, a loader or manager
|
|
3940
|
+
// AD_ERROR, or a normal ALL_ADS_COMPLETED — routes through this closure,
|
|
3941
|
+
// guarded so 'ad-complete' can never fire twice and main can never open
|
|
3942
|
+
// twice.
|
|
3943
|
+
let introDeferralTimer = null;
|
|
3944
|
+
let adLegTimer = null;
|
|
3945
|
+
let adLegCompleted = false;
|
|
3946
|
+
const localController = new AbortController();
|
|
3947
|
+
signal.addEventListener('abort', () => localController.abort(), { once: true });
|
|
3948
|
+
const clearIntroDeferralTimer = () => {
|
|
3949
|
+
if (introDeferralTimer !== null) {
|
|
3950
|
+
clearTimeout(introDeferralTimer);
|
|
3951
|
+
introDeferralTimer = null;
|
|
3952
|
+
}
|
|
3953
|
+
};
|
|
3954
|
+
const clearAdLegTimer = () => {
|
|
3955
|
+
if (adLegTimer !== null) {
|
|
3956
|
+
clearTimeout(adLegTimer);
|
|
3957
|
+
adLegTimer = null;
|
|
3958
|
+
}
|
|
3959
|
+
};
|
|
3960
|
+
const completeAdLeg = () => {
|
|
3961
|
+
if (adLegCompleted)
|
|
3962
|
+
return;
|
|
3963
|
+
adLegCompleted = true;
|
|
3964
|
+
clearIntroDeferralTimer();
|
|
3965
|
+
clearAdLegTimer();
|
|
3966
|
+
// T-07-23: release only when nothing can be rendering. imaAdsManager
|
|
3967
|
+
// is assigned in exactly one place (below, after getAdsManager()
|
|
3968
|
+
// returns), so null here means the loader never handed one back —
|
|
3969
|
+
// watchdog expiry, an IMA-setup rejection, or a loader-level AD_ERROR
|
|
3970
|
+
// before any manager. ALL_ADS_COMPLETED and a manager-level AD_ERROR
|
|
3971
|
+
// keep dispose-time teardown instead (see releaseImaResources below).
|
|
3972
|
+
if (this.imaAdsManager === null) {
|
|
3973
|
+
this.releaseImaResources();
|
|
3974
|
+
}
|
|
3975
|
+
state.adPlaying = false;
|
|
3976
|
+
bus.emit('ad-complete');
|
|
3977
|
+
localController.abort();
|
|
3978
|
+
};
|
|
3979
|
+
// Abort hygiene (G-07-11): unconditional and idempotent, registered
|
|
3980
|
+
// once so it fires no matter which stage init() was in when the signal
|
|
3981
|
+
// aborted. releaseImaResources() is itself null-guarded, so this is a
|
|
3982
|
+
// no-op on any path that already released or never acquired IMA
|
|
3983
|
+
// resources.
|
|
3984
|
+
signal.addEventListener('abort', () => {
|
|
3985
|
+
clearIntroDeferralTimer();
|
|
3986
|
+
clearAdLegTimer();
|
|
3987
|
+
this.releaseImaResources();
|
|
3988
|
+
}, { once: true });
|
|
3989
|
+
// G-07-11: gesture observation starts here, before the intro-deferral
|
|
3990
|
+
// await below, so a click that lands during the intro is already
|
|
3991
|
+
// captured by the time the post-intro await consumes it (the
|
|
3992
|
+
// sequential-await interaction: fixing G-07-9 alone would otherwise
|
|
3993
|
+
// install this listener only after the intro, behind the very click
|
|
3994
|
+
// that started it).
|
|
3995
|
+
//
|
|
3996
|
+
// WR-01: wired to localController.signal, not the outer signal. The outer
|
|
3997
|
+
// abort already propagates into localController (above), so this still
|
|
3998
|
+
// covers player disposal — but it additionally lets completeAdLeg() settle
|
|
3999
|
+
// a still-pending gesture wait. Without it, an ad leg completed by the
|
|
4000
|
+
// watchdog, an AD_ERROR or ALL_ADS_COMPLETED before any click leaves this
|
|
4001
|
+
// promise unsettled forever: init() never resolves and the capture-phase
|
|
4002
|
+
// click listener stays attached to .eb-player.
|
|
4003
|
+
const gesturePromise = config.autoplay ? null : this.waitForUserGesture(video, localController.signal);
|
|
4004
|
+
// G-07-9: the decision is derived from configuration, not from the
|
|
4005
|
+
// PlayerState intro-playing flag. An async function body runs
|
|
4006
|
+
// synchronously up to its first await, so reading that flag here would
|
|
4007
|
+
// always observe its createPlayerState() default — IntroStreamManager
|
|
4008
|
+
// has not run yet at this point in initIntegrations(). config.preroll is
|
|
4009
|
+
// immutable and readable regardless of integration init order.
|
|
4010
|
+
if (config.preroll) {
|
|
3916
4011
|
await new Promise((resolve) => {
|
|
3917
|
-
|
|
3918
|
-
|
|
4012
|
+
const settle = () => {
|
|
4013
|
+
clearIntroDeferralTimer();
|
|
4014
|
+
resolve();
|
|
4015
|
+
};
|
|
4016
|
+
bus.on('intro-stream-complete', settle, { signal });
|
|
4017
|
+
signal.addEventListener('abort', settle, { once: true });
|
|
4018
|
+
introDeferralTimer = setTimeout(() => {
|
|
4019
|
+
console.warn('AdsManager: intro-stream-complete never arrived within the bound — falling through to the ad leg (G-07-9)');
|
|
4020
|
+
settle();
|
|
4021
|
+
}, INTRO_DEFERRAL_TIMEOUT_MS);
|
|
3919
4022
|
});
|
|
3920
4023
|
if (signal.aborted)
|
|
3921
4024
|
return;
|
|
3922
4025
|
}
|
|
3923
|
-
//
|
|
3924
|
-
|
|
3925
|
-
|
|
3926
|
-
|
|
4026
|
+
// G-07-11 / D-08-03: the ad-leg watchdog bounds every step from here —
|
|
4027
|
+
// the gesture wait, loadScript, the ad request and the ads response —
|
|
4028
|
+
// until the loader hands back an AdsManager, which disarms it below.
|
|
4029
|
+
// Armed unconditionally whenever config.ad is set (init() already
|
|
4030
|
+
// returned above at the `!config.ad` early return, so this line is only
|
|
4031
|
+
// ever reached with config.ad truthy): as of D-02, main is gated on
|
|
4032
|
+
// 'ad-complete' in BOTH the intro+ad and ad-only configurations, so an
|
|
4033
|
+
// unbounded leg in either shape is a permanent stuck loader — the exact
|
|
4034
|
+
// failure VAST-05 forbids. Invariant: whenever main waits on the ad leg,
|
|
4035
|
+
// the ad leg is bounded. Do not restore an `if (config.preroll)` guard
|
|
4036
|
+
// here — that would leave the ad-only configuration unbounded again.
|
|
4037
|
+
adLegTimer = setTimeout(() => {
|
|
4038
|
+
console.warn('AdsManager: ad leg produced no AdsManager within the bound — falling through to main (G-07-11)');
|
|
4039
|
+
completeAdLeg();
|
|
4040
|
+
}, AD_LEG_TIMEOUT_MS);
|
|
4041
|
+
// Consume the gesture observed above. A click that arrives after the ad
|
|
4042
|
+
// leg has already completed must not resurrect it.
|
|
4043
|
+
if (gesturePromise !== null) {
|
|
4044
|
+
await gesturePromise;
|
|
4045
|
+
}
|
|
4046
|
+
if (signal.aborted || adLegCompleted)
|
|
4047
|
+
return;
|
|
4048
|
+
try {
|
|
4049
|
+
const google = await loadScript(IMA_SDK_URL, 'google');
|
|
4050
|
+
// G-07-11: the release above must be terminal. A watchdog that
|
|
4051
|
+
// expired while loadScript was still in flight must not let a
|
|
4052
|
+
// resolving SDK rebuild the display container it just destroyed.
|
|
4053
|
+
if (adLegCompleted)
|
|
3927
4054
|
return;
|
|
3928
|
-
|
|
3929
|
-
|
|
3930
|
-
|
|
3931
|
-
|
|
3932
|
-
|
|
3933
|
-
|
|
3934
|
-
|
|
3935
|
-
|
|
3936
|
-
|
|
3937
|
-
|
|
3938
|
-
|
|
3939
|
-
|
|
3940
|
-
// completion so the orchestration always advances.
|
|
3941
|
-
adsLoader.addEventListener(ima.AdErrorEvent.Type.AD_ERROR, (errorEvent) => {
|
|
3942
|
-
console.warn('AdsManager: AD_ERROR (loader) — falling through to main', errorEvent);
|
|
3943
|
-
state.adPlaying = false;
|
|
3944
|
-
bus.emit('ad-complete');
|
|
3945
|
-
});
|
|
3946
|
-
adsLoader.addEventListener(ima.AdsManagerLoadedEvent.Type.ADS_MANAGER_LOADED, (loadedEvent) => {
|
|
3947
|
-
const event = loadedEvent;
|
|
3948
|
-
const adsManager = event.getAdsManager(video);
|
|
3949
|
-
this.imaAdsManager = adsManager;
|
|
3950
|
-
// getBoundingClientRect is always defined on HTMLElement; the prior
|
|
3951
|
-
// truthy check on the method reference was dead code. The real edge
|
|
3952
|
-
// case is a zero-sized element (display:none, before layout) which
|
|
3953
|
-
// would make IMA's ads container collapse — fall back to a 16:9
|
|
3954
|
-
// baseline in that case.
|
|
3955
|
-
const rect = video.getBoundingClientRect();
|
|
3956
|
-
const width = rect.width > 0 ? rect.width : 640;
|
|
3957
|
-
const height = rect.height > 0 ? rect.height : 360;
|
|
3958
|
-
// Wire event handlers for ad lifecycle
|
|
3959
|
-
adsManager.addEventListener(ima.AdEvent.Type.CONTENT_PAUSE_REQUESTED, () => {
|
|
3960
|
-
state.adPlaying = true;
|
|
3961
|
-
// Pause the content stream while the ad plays. IMA's integration
|
|
3962
|
-
// contract requires the publisher to pause its own content video on
|
|
3963
|
-
// CONTENT_PAUSE_REQUESTED; the symmetric CONTENT_RESUME_REQUESTED
|
|
3964
|
-
// handler below resumes it with video.play(). Without this the main
|
|
3965
|
-
// HLS/DASH stream keeps playing (audio + video) underneath the ad.
|
|
3966
|
-
video.pause();
|
|
3967
|
-
});
|
|
3968
|
-
adsManager.addEventListener(ima.AdEvent.Type.CONTENT_RESUME_REQUESTED, () => {
|
|
3969
|
-
state.adPlaying = false;
|
|
3970
|
-
void video.play();
|
|
4055
|
+
const { ima } = google;
|
|
4056
|
+
const displayContainer = new ima.AdDisplayContainer(adsContainer, video);
|
|
4057
|
+
this.imaDisplayContainer = displayContainer;
|
|
4058
|
+
displayContainer.initialize();
|
|
4059
|
+
const adsLoader = new ima.AdsLoader(displayContainer);
|
|
4060
|
+
// AD_ERROR fall-through (CR-02): if the VAST tag fails to load (network
|
|
4061
|
+
// error, ad-blocker, malformed XML, autoplay rejection), IMA fires
|
|
4062
|
+
// AD_ERROR and never ALL_ADS_COMPLETED. Route it through the shared
|
|
4063
|
+
// completion funnel so the orchestration always advances.
|
|
4064
|
+
adsLoader.addEventListener(ima.AdErrorEvent.Type.AD_ERROR, (errorEvent) => {
|
|
4065
|
+
console.warn('AdsManager: AD_ERROR (loader) — falling through to main', errorEvent);
|
|
4066
|
+
completeAdLeg();
|
|
3971
4067
|
});
|
|
3972
|
-
|
|
3973
|
-
|
|
3974
|
-
|
|
3975
|
-
|
|
3976
|
-
|
|
3977
|
-
|
|
3978
|
-
|
|
3979
|
-
|
|
3980
|
-
|
|
3981
|
-
|
|
3982
|
-
|
|
4068
|
+
adsLoader.addEventListener(ima.AdsManagerLoadedEvent.Type.ADS_MANAGER_LOADED, (loadedEvent) => {
|
|
4069
|
+
// T-07-22: a late ads response — the request is issued before the
|
|
4070
|
+
// bound can expire, so this is reachable — must not start an ad
|
|
4071
|
+
// over an already-opened main stream, and must not emit a second
|
|
4072
|
+
// 'ad-complete'.
|
|
4073
|
+
if (adLegCompleted)
|
|
4074
|
+
return;
|
|
4075
|
+
const event = loadedEvent;
|
|
4076
|
+
const adsManager = event.getAdsManager(video);
|
|
4077
|
+
this.imaAdsManager = adsManager;
|
|
4078
|
+
// G-07-11: holding the manager is the earliest proof the ad leg is
|
|
4079
|
+
// alive and progressing. Creative fetch, buffering and render are
|
|
4080
|
+
// IMA's to own from here — disarm the watchdog now rather than at
|
|
4081
|
+
// a later, more visible signal, so none of that time is charged
|
|
4082
|
+
// to the bound.
|
|
4083
|
+
clearAdLegTimer();
|
|
4084
|
+
// getBoundingClientRect is always defined on HTMLElement; the prior
|
|
4085
|
+
// truthy check on the method reference was dead code. The real edge
|
|
4086
|
+
// case is a zero-sized element (display:none, before layout) which
|
|
4087
|
+
// would make IMA's ads container collapse — fall back to a 16:9
|
|
4088
|
+
// baseline in that case.
|
|
4089
|
+
const rect = video.getBoundingClientRect();
|
|
4090
|
+
const width = rect.width > 0 ? rect.width : 640;
|
|
4091
|
+
const height = rect.height > 0 ? rect.height : 360;
|
|
4092
|
+
// D-08-08: CONTENT_PAUSE_REQUESTED, CONTENT_RESUME_REQUESTED and
|
|
4093
|
+
// ALL_ADS_COMPLETED emitted nothing at all before this change, so
|
|
4094
|
+
// none of the three could be individually evidenced in the 08-04
|
|
4095
|
+
// live session (G-08-02) — only their side effects were visible.
|
|
4096
|
+
// These three log lines are observability-only and inherit
|
|
4097
|
+
// D-08-06's constraints below verbatim (never complete the leg,
|
|
4098
|
+
// never write state, never interpolate metadata). Each literal is
|
|
4099
|
+
// captured verbatim into a committed evidence file by 08-07.
|
|
4100
|
+
// Wire event handlers for ad lifecycle
|
|
4101
|
+
adsManager.addEventListener(ima.AdEvent.Type.CONTENT_PAUSE_REQUESTED, () => {
|
|
4102
|
+
console.log('AdsManager: CONTENT_PAUSE_REQUESTED — content paused (D-08-08)');
|
|
4103
|
+
state.adPlaying = true;
|
|
4104
|
+
// Pause the content stream while the ad plays. IMA's integration
|
|
4105
|
+
// contract requires the publisher to pause its own content video on
|
|
4106
|
+
// CONTENT_PAUSE_REQUESTED; the symmetric CONTENT_RESUME_REQUESTED
|
|
4107
|
+
// handler below resumes it with video.play(). Without this the main
|
|
4108
|
+
// HLS/DASH stream keeps playing (audio + video) underneath the ad.
|
|
4109
|
+
video.pause();
|
|
4110
|
+
});
|
|
4111
|
+
adsManager.addEventListener(ima.AdEvent.Type.CONTENT_RESUME_REQUESTED, () => {
|
|
4112
|
+
console.log('AdsManager: CONTENT_RESUME_REQUESTED — content resumed (D-08-08)');
|
|
4113
|
+
state.adPlaying = false;
|
|
4114
|
+
void video.play();
|
|
4115
|
+
// D-08-07: CONTENT_RESUME_REQUESTED is IMA's canonical "ad break
|
|
4116
|
+
// over, resume content" signal and is the ONLY completion event
|
|
4117
|
+
// dispatched for a plain VAST response — ALL_ADS_COMPLETED is
|
|
4118
|
+
// reliable only for VMAP/ad-rules responses. Routing it through
|
|
4119
|
+
// completeAdLeg() is what makes VAST-04's "exactly once per
|
|
4120
|
+
// instance" hold on both response shapes; completeAdLeg() is
|
|
4121
|
+
// idempotent via adLegCompleted, so a later ALL_ADS_COMPLETED is
|
|
4122
|
+
// a no-op rather than a second 'ad-complete'.
|
|
4123
|
+
//
|
|
4124
|
+
// Verified live (08-04): without this, a completed Google sample
|
|
4125
|
+
// preroll never emitted 'ad-complete', and main opened only when
|
|
4126
|
+
// the 30s MAIN_OPEN_FALLBACK_MS backstop expired — a 20-29s gap
|
|
4127
|
+
// on every ad-only playback.
|
|
4128
|
+
completeAdLeg();
|
|
4129
|
+
});
|
|
4130
|
+
adsManager.addEventListener(ima.AdEvent.Type.ALL_ADS_COMPLETED, () => {
|
|
4131
|
+
console.log('AdsManager: ALL_ADS_COMPLETED — ad leg complete (D-08-08)');
|
|
4132
|
+
completeAdLeg();
|
|
4133
|
+
});
|
|
4134
|
+
// Same AD_ERROR fall-through on the manager — covers errors that
|
|
4135
|
+
// surface only after the ad starts playing (decode error, mid-roll
|
|
4136
|
+
// load failure, etc.).
|
|
4137
|
+
adsManager.addEventListener(ima.AdErrorEvent.Type.AD_ERROR, (errorEvent) => {
|
|
4138
|
+
console.warn('AdsManager: AD_ERROR (manager) — falling through to main', errorEvent);
|
|
4139
|
+
completeAdLeg();
|
|
4140
|
+
});
|
|
4141
|
+
// D-08-06: IMA renders and owns its own Skip button (D-05 rules out
|
|
4142
|
+
// a custom skip UI), so this log is the only signal that proves the
|
|
4143
|
+
// control became available — observability only. It must never
|
|
4144
|
+
// call completeAdLeg() (this fires mid-ad, while a creative is
|
|
4145
|
+
// still rendering) and must never write to state (nothing consumes
|
|
4146
|
+
// an adSkippable field today, and speculative state violates
|
|
4147
|
+
// Simplicity First). No ad/creative metadata is interpolated: this
|
|
4148
|
+
// literal is captured verbatim into a committed evidence file by
|
|
4149
|
+
// 08-04 (T-08-05).
|
|
4150
|
+
adsManager.addEventListener(ima.AdEvent.Type.SKIPPABLE_STATE_CHANGED, () => {
|
|
4151
|
+
console.log('AdsManager: ad became skippable (D-08-06)');
|
|
4152
|
+
});
|
|
4153
|
+
adsManager.init(width, height);
|
|
4154
|
+
adsManager.start();
|
|
3983
4155
|
});
|
|
3984
|
-
|
|
3985
|
-
|
|
3986
|
-
|
|
3987
|
-
|
|
3988
|
-
|
|
3989
|
-
|
|
3990
|
-
|
|
3991
|
-
|
|
3992
|
-
|
|
3993
|
-
|
|
3994
|
-
|
|
3995
|
-
|
|
3996
|
-
|
|
3997
|
-
|
|
3998
|
-
|
|
3999
|
-
|
|
4000
|
-
|
|
4001
|
-
|
|
4156
|
+
// Load the ad tag
|
|
4157
|
+
const adsRequest = new ima.AdsRequest();
|
|
4158
|
+
adsRequest.adTagUrl = config.ad;
|
|
4159
|
+
adsLoader.requestAds(adsRequest);
|
|
4160
|
+
}
|
|
4161
|
+
catch (error) {
|
|
4162
|
+
// G-07-11: an ad-blocker blocking the CDN, a network error, or
|
|
4163
|
+
// loadScript's own 10s timeout must fall through to main instead of
|
|
4164
|
+
// escaping to the logging-only .catch() in lifecycle.ts.
|
|
4165
|
+
console.warn('AdsManager: failed to load or initialize the IMA SDK — falling through to main (G-07-11)', error);
|
|
4166
|
+
completeAdLeg();
|
|
4167
|
+
}
|
|
4168
|
+
}
|
|
4169
|
+
/**
|
|
4170
|
+
* Shared teardown for imaAdsManager and imaDisplayContainer, each behind
|
|
4171
|
+
* its own null check and nulled immediately after — idempotent by
|
|
4172
|
+
* construction. Called unconditionally on abort, and from the completion
|
|
4173
|
+
* funnel only when this.imaAdsManager is still null (T-07-23).
|
|
4174
|
+
*/
|
|
4175
|
+
releaseImaResources() {
|
|
4176
|
+
if (this.imaAdsManager !== null) {
|
|
4177
|
+
this.imaAdsManager.destroy();
|
|
4178
|
+
this.imaAdsManager = null;
|
|
4179
|
+
}
|
|
4180
|
+
if (this.imaDisplayContainer !== null) {
|
|
4181
|
+
this.imaDisplayContainer.destroy();
|
|
4182
|
+
this.imaDisplayContainer = null;
|
|
4183
|
+
}
|
|
4002
4184
|
}
|
|
4003
4185
|
/**
|
|
4004
|
-
* Wait for a click event
|
|
4186
|
+
* Wait for a click event anywhere inside the player root.
|
|
4005
4187
|
* Used to satisfy browser autoplay policies when autoplay is disabled.
|
|
4006
4188
|
*
|
|
4189
|
+
* G-07-11: the target widens from the video element to
|
|
4190
|
+
* video.closest('.eb-player') (falling back to video itself for bare
|
|
4191
|
+
* integrations), registered in the capture phase so a descendant that
|
|
4192
|
+
* halts bubbling cannot hide the click from it. Observes only; never
|
|
4193
|
+
* mutates the event, so the skip button and click-through link keep
|
|
4194
|
+
* working exactly as they do today.
|
|
4195
|
+
*
|
|
4007
4196
|
* WR-05: single-shot settle flag guards against the click-vs-abort race —
|
|
4008
4197
|
* Promise resolution is already idempotent but settled tightens the
|
|
4009
4198
|
* contract by ensuring listener cleanup happens exactly once for either
|
|
@@ -4011,16 +4200,17 @@
|
|
|
4011
4200
|
*/
|
|
4012
4201
|
waitForUserGesture(video, signal) {
|
|
4013
4202
|
return new Promise((resolve) => {
|
|
4203
|
+
const target = video.closest('.eb-player') ?? video;
|
|
4014
4204
|
let settled = false;
|
|
4015
4205
|
const settle = () => {
|
|
4016
4206
|
if (settled)
|
|
4017
4207
|
return;
|
|
4018
4208
|
settled = true;
|
|
4019
|
-
|
|
4209
|
+
target.removeEventListener('click', onClick, { capture: true });
|
|
4020
4210
|
resolve();
|
|
4021
4211
|
};
|
|
4022
4212
|
const onClick = () => settle();
|
|
4023
|
-
|
|
4213
|
+
target.addEventListener('click', onClick, { capture: true });
|
|
4024
4214
|
// Clean up if aborted before click. The abort listener auto-removes
|
|
4025
4215
|
// via `{ once: true }`; the click listener is removed inside settle().
|
|
4026
4216
|
signal.addEventListener('abort', settle, { once: true });
|
|
@@ -4028,6 +4218,21 @@
|
|
|
4028
4218
|
}
|
|
4029
4219
|
}
|
|
4030
4220
|
|
|
4221
|
+
/**
|
|
4222
|
+
* G-07-12: absolute backstop for an intro pass where every other completion
|
|
4223
|
+
* trigger is lost — no video `ended`, no `preroll-skip`, no `error-fatal`,
|
|
4224
|
+
* and no `state.error` write. 90s cannot truncate a real brand intro
|
|
4225
|
+
* (single-digit to ~30s assets) but converts an infinite hang into a bounded
|
|
4226
|
+
* one.
|
|
4227
|
+
*
|
|
4228
|
+
* D-10: this value MUST stay strictly below AdsManager's
|
|
4229
|
+
* INTRO_DEFERRAL_TIMEOUT_MS (120_000, src/integrations/ads-manager.ts) so
|
|
4230
|
+
* the intro pass always completes before the ad leg falls through — intro →
|
|
4231
|
+
* ad → main precedence must hold in the failure case too, not only the
|
|
4232
|
+
* happy path. Test 14 in test/integrations/intro-stream-manager.test.ts
|
|
4233
|
+
* asserts this ordering so a future edit to either constant fails loudly.
|
|
4234
|
+
*/
|
|
4235
|
+
const INTRO_MAX_DURATION_MS = 90000;
|
|
4031
4236
|
/**
|
|
4032
4237
|
* IntroStreamManager owns the intro-stream lifecycle.
|
|
4033
4238
|
*
|
|
@@ -4036,12 +4241,24 @@
|
|
|
4036
4241
|
* - the video element's native `ended` event
|
|
4037
4242
|
* - bus `preroll-skip` (from the PrerollOverlay skip button)
|
|
4038
4243
|
* - bus `error-fatal` (D-04 silent fall-through — intro must never block main)
|
|
4244
|
+
* - PlayerState `error` becoming non-null (G-07-12: the one signal shared
|
|
4245
|
+
* by two silent failure modes that never reach the bus —
|
|
4246
|
+
* StallWatchdog reload-budget exhaustion,
|
|
4247
|
+
* src/core/command-handler.ts:159-164, and a synchronous engine
|
|
4248
|
+
* init() throw caught in onAttach, src/engines/dash.ts:108-120 and
|
|
4249
|
+
* src/engines/hls.ts:194-207)
|
|
4250
|
+
* - the on-demand bus event that lets eb-player.ts's openMainStream()
|
|
4251
|
+
* guarantee the intro pass is finished before main opens (G-07-13
|
|
4252
|
+
* receiving half — see the bus.on() registration below for its name)
|
|
4253
|
+
* - an absolute INTRO_MAX_DURATION_MS backstop timer, armed in case no
|
|
4254
|
+
* other trigger ever fires
|
|
4039
4255
|
* - Any one of those triggers calls a closure `complete()` exactly once
|
|
4040
4256
|
* (idempotent via a `completed` boolean guard), which flips
|
|
4041
4257
|
* state.introStreamPlaying = false and emits `intro-stream-complete`.
|
|
4042
4258
|
* - On abort, the video `ended` listener is removed explicitly via
|
|
4043
|
-
* removeEventListener
|
|
4044
|
-
*
|
|
4259
|
+
* removeEventListener and the backstop timer is cleared. Bus and state
|
|
4260
|
+
* listeners auto-clean via the `{ signal }` option (see
|
|
4261
|
+
* src/core/event-bus.ts on() and src/core/player-state.ts on()).
|
|
4045
4262
|
*
|
|
4046
4263
|
* Scope: this manager listens and emits — it does NOT itself open the intro
|
|
4047
4264
|
* stream. The engine open() call is owned by eb-player.ts (single source of
|
|
@@ -4052,8 +4269,10 @@
|
|
|
4052
4269
|
/**
|
|
4053
4270
|
* Initialize intro-stream lifecycle tracking.
|
|
4054
4271
|
* - Skips entirely if config.preroll is falsy (no state flip, no listeners).
|
|
4055
|
-
* - Otherwise flips state.introStreamPlaying = true and registers the
|
|
4056
|
-
* completion triggers (video `ended`, bus `preroll-skip`, bus
|
|
4272
|
+
* - Otherwise flips state.introStreamPlaying = true and registers the
|
|
4273
|
+
* completion triggers (video `ended`, bus `preroll-skip`, bus
|
|
4274
|
+
* `error-fatal`, PlayerState `error`, the on-demand force-completion bus
|
|
4275
|
+
* event, and the INTRO_MAX_DURATION_MS backstop timer).
|
|
4057
4276
|
*/
|
|
4058
4277
|
async init(config, state, bus, video, signal) {
|
|
4059
4278
|
if (!config.preroll)
|
|
@@ -4068,14 +4287,18 @@
|
|
|
4068
4287
|
// Mirror outer aborts onto the local controller so dispose() still
|
|
4069
4288
|
// cleans everything up.
|
|
4070
4289
|
signal.addEventListener('abort', () => local.abort(), { once: true });
|
|
4290
|
+
// Initialized to 0; clearing a zero timer handle is a no-op, so no null
|
|
4291
|
+
// guard is needed around either clear call below.
|
|
4292
|
+
let backstopTimer = 0;
|
|
4071
4293
|
let completed = false;
|
|
4072
4294
|
const complete = () => {
|
|
4073
4295
|
if (completed)
|
|
4074
4296
|
return;
|
|
4075
4297
|
completed = true;
|
|
4298
|
+
clearTimeout(backstopTimer);
|
|
4076
4299
|
state.introStreamPlaying = false;
|
|
4077
4300
|
bus.emit('intro-stream-complete');
|
|
4078
|
-
local.abort(); // tear down bus listeners after the intro completes
|
|
4301
|
+
local.abort(); // tear down bus/state listeners after the intro completes
|
|
4079
4302
|
};
|
|
4080
4303
|
const onEnded = () => complete();
|
|
4081
4304
|
video.addEventListener('ended', onEnded);
|
|
@@ -4086,8 +4309,40 @@
|
|
|
4086
4309
|
console.warn('IntroStreamManager: intro stream errored — falling through to main');
|
|
4087
4310
|
complete();
|
|
4088
4311
|
}, { signal: local.signal });
|
|
4312
|
+
// G-07-12: state.error is the one signal shared by two silent failure
|
|
4313
|
+
// modes that never reach the bus — StallWatchdog reload-budget
|
|
4314
|
+
// exhaustion (src/core/command-handler.ts:159-164) and a synchronous
|
|
4315
|
+
// engine init() throw caught in onAttach (src/engines/dash.ts:108-120,
|
|
4316
|
+
// src/engines/hls.ts:194-207). Both writers are shared with the MAIN
|
|
4317
|
+
// pass, so the fix lives here (the intro-owning component) rather than
|
|
4318
|
+
// in either shared file. The Proxy `set` trap already no-ops on an
|
|
4319
|
+
// unchanged value, but a `null` write (e.g. openMainStream() clearing a
|
|
4320
|
+
// stale error) must still never be read as "the intro finished".
|
|
4321
|
+
state.on('error', (newValue) => {
|
|
4322
|
+
if (newValue === null)
|
|
4323
|
+
return;
|
|
4324
|
+
console.warn('IntroStreamManager: intro stream errored via state.error — falling through to main');
|
|
4325
|
+
complete();
|
|
4326
|
+
}, { signal: local.signal });
|
|
4327
|
+
// G-07-13 receiving half: openMainStream() (src/eb-player.ts, plan
|
|
4328
|
+
// 07-10) emits this to guarantee the intro pass is finished before main
|
|
4329
|
+
// opens. No `completed` re-check is needed or wanted here: complete()
|
|
4330
|
+
// aborts `local`, which removes this listener, so the handler below is
|
|
4331
|
+
// only ever reachable while the intro is still live — adding a guard
|
|
4332
|
+
// would create an unreachable branch.
|
|
4333
|
+
bus.on('force-intro-complete', () => {
|
|
4334
|
+
console.warn('IntroStreamManager: main is opening — force-completing the intro pass');
|
|
4335
|
+
complete();
|
|
4336
|
+
}, { signal: local.signal });
|
|
4337
|
+
// G-07-12: absolute bound — even if no other trigger ever fires, the
|
|
4338
|
+
// intro pass still completes within INTRO_MAX_DURATION_MS.
|
|
4339
|
+
backstopTimer = setTimeout(() => {
|
|
4340
|
+
console.warn('IntroStreamManager: intro stream exceeded INTRO_MAX_DURATION_MS — falling through to main');
|
|
4341
|
+
complete();
|
|
4342
|
+
}, INTRO_MAX_DURATION_MS);
|
|
4089
4343
|
signal.addEventListener('abort', () => {
|
|
4090
4344
|
video.removeEventListener('ended', onEnded);
|
|
4345
|
+
clearTimeout(backstopTimer);
|
|
4091
4346
|
}, { once: true });
|
|
4092
4347
|
}
|
|
4093
4348
|
}
|
|
@@ -5500,6 +5755,153 @@
|
|
|
5500
5755
|
};
|
|
5501
5756
|
}
|
|
5502
5757
|
|
|
5758
|
+
/**
|
|
5759
|
+
* Segment (fragment) CDN token signing — shared primitive.
|
|
5760
|
+
*
|
|
5761
|
+
* Extends CDN token signing from manifests to segments, behind an opt-in flag
|
|
5762
|
+
* (`EngineSettings.signSegmentsWithCdnToken`). Each segment is signed with the
|
|
5763
|
+
* grant carried by its OWN parent playlist — read from `context.frag.baseurl`,
|
|
5764
|
+
* the object hls.js itself parsed that fragment from — never from any store,
|
|
5765
|
+
* cache, or shared manager state. This makes cross-rendition mis-signing
|
|
5766
|
+
* structurally impossible rather than merely unlikely.
|
|
5767
|
+
*
|
|
5768
|
+
* CRITICAL: this module must never acquire or refresh a token. It only reads
|
|
5769
|
+
* data already present on the fragment context handed to it by hls.js. Any
|
|
5770
|
+
* acquisition call here would reintroduce the single-slot cache reuse in
|
|
5771
|
+
* `CDNTokenManager.lastTokenResponse`, which hands one rendition's grant to
|
|
5772
|
+
* another — the exact failure this design forbids.
|
|
5773
|
+
*
|
|
5774
|
+
* Token expiry model (CDN-04, D-07): a segment carries whatever grant its
|
|
5775
|
+
* parent playlist carried when hls.js parsed it. Live playlists reload on
|
|
5776
|
+
* their own cadence, so their fragments pick up refreshed grants with no
|
|
5777
|
+
* refresh machinery here — reload IS the refresh path. VOD playlists are
|
|
5778
|
+
* parsed exactly once by hls.js 1.6.10 (its level controller only
|
|
5779
|
+
* (re)requests a playlist it has none of, or one that is live), so a VOD
|
|
5780
|
+
* asset's segment grants are frozen at first parse for the asset's entire
|
|
5781
|
+
* duration. If a token's lifetime is shorter than the asset's duration, later
|
|
5782
|
+
* segments will be rejected by the edge and surface through the existing
|
|
5783
|
+
* error/retry path. This is an accepted operational constraint — the token
|
|
5784
|
+
* service's lifetime must exceed the longest VOD asset — not an oversight;
|
|
5785
|
+
* D-07 chose reload-cadence refresh and CONTEXT.md defers a proactive
|
|
5786
|
+
* near-expiry refresh path with a named revisit condition (a real asset shown
|
|
5787
|
+
* to outlive its grant). No timer, interval, or scheduled re-fetch belongs in
|
|
5788
|
+
* this module.
|
|
5789
|
+
*/
|
|
5790
|
+
// ---------------------------------------------------------------------------
|
|
5791
|
+
// Supported token-type family (D-03) — query-param tokens only. Akamai's
|
|
5792
|
+
// path-segment mechanic (`appendAkamaiTokenParams`) is a different shape and
|
|
5793
|
+
// deliberately not extended here.
|
|
5794
|
+
// ---------------------------------------------------------------------------
|
|
5795
|
+
const SEGMENT_SIGNING_TOKEN_TYPES = Object.freeze([
|
|
5796
|
+
TOKEN_TYPES.EASY_B,
|
|
5797
|
+
TOKEN_TYPES.VENOM,
|
|
5798
|
+
TOKEN_TYPES.BUNNY
|
|
5799
|
+
]);
|
|
5800
|
+
/**
|
|
5801
|
+
* Builds the single diagnostic string used for every unsupported-token-type
|
|
5802
|
+
* case. Names the flag and the offending token type (or says none is
|
|
5803
|
+
* configured) and lists the supported ones. Never includes a token value.
|
|
5804
|
+
*/
|
|
5805
|
+
function segmentSigningUnsupportedMessage(tokenType) {
|
|
5806
|
+
const supported = SEGMENT_SIGNING_TOKEN_TYPES.join(', ');
|
|
5807
|
+
const described = tokenType === undefined ? 'no token type configured' : `token type "${tokenType}"`;
|
|
5808
|
+
return `signSegmentsWithCdnToken: segment signing is not supported for ${described}. Supported token types: ${supported}.`;
|
|
5809
|
+
}
|
|
5810
|
+
/**
|
|
5811
|
+
* A fresh, all-zero `LoaderStats`-shaped object for the fail-loud path's
|
|
5812
|
+
* `onError` call. hls.js's own error-handling code reads fields off this
|
|
5813
|
+
* object (e.g. `stats.loaded`); passing `undefined` risks a `TypeError`
|
|
5814
|
+
* inside hls.js itself instead of the clean error this module intends to
|
|
5815
|
+
* produce. Returns a new object per call — never shared/mutated state — so
|
|
5816
|
+
* nothing downstream can observe stale values from a previous failed load.
|
|
5817
|
+
*/
|
|
5818
|
+
function createEmptySegmentLoaderStats() {
|
|
5819
|
+
return {
|
|
5820
|
+
aborted: false,
|
|
5821
|
+
loaded: 0,
|
|
5822
|
+
total: 0,
|
|
5823
|
+
retry: 0,
|
|
5824
|
+
chunkCount: 0,
|
|
5825
|
+
bwEstimate: 0,
|
|
5826
|
+
loading: { start: 0, first: 0, end: 0 },
|
|
5827
|
+
parsing: { start: 0, end: 0 },
|
|
5828
|
+
buffering: { start: 0, first: 0, end: 0 }
|
|
5829
|
+
};
|
|
5830
|
+
}
|
|
5831
|
+
/**
|
|
5832
|
+
* Pure transfer: reads `token`, `expires` and `token_path` off the parent
|
|
5833
|
+
* playlist URL's query string and reapplies them to the segment URL via the
|
|
5834
|
+
* same static primitive that signs manifests, so a signed segment URL is
|
|
5835
|
+
* shaped exactly like a signed manifest URL. Returns the segment URL
|
|
5836
|
+
* unchanged when the playlist carries no token, or when either URL cannot be
|
|
5837
|
+
* parsed (a thrown exception inside a loader is a dead player, not a signing
|
|
5838
|
+
* failure).
|
|
5839
|
+
*/
|
|
5840
|
+
function signSegmentUrlFromPlaylist(segmentUrl, parentPlaylistUrl) {
|
|
5841
|
+
try {
|
|
5842
|
+
const playlistUrl = new URL(parentPlaylistUrl);
|
|
5843
|
+
const token = playlistUrl.searchParams.get('token');
|
|
5844
|
+
if (!token)
|
|
5845
|
+
return segmentUrl;
|
|
5846
|
+
const expires = playlistUrl.searchParams.get('expires');
|
|
5847
|
+
const tokenPath = playlistUrl.searchParams.get('token_path');
|
|
5848
|
+
return CDNTokenManager.appendTokenParams(segmentUrl, token, expires, tokenPath);
|
|
5849
|
+
}
|
|
5850
|
+
catch {
|
|
5851
|
+
return segmentUrl;
|
|
5852
|
+
}
|
|
5853
|
+
}
|
|
5854
|
+
/**
|
|
5855
|
+
* Returns a loader class extending `BaseLoader`, built exactly like the
|
|
5856
|
+
* existing playlist loader: call `super()`, bind the inherited `load`, then
|
|
5857
|
+
* assign an own `load` property that reads `context.frag?.baseurl`.
|
|
5858
|
+
*
|
|
5859
|
+
* `tokenSource` is captured by closure — never stored on `this` and never
|
|
5860
|
+
* used to acquire or refresh a token.
|
|
5861
|
+
*/
|
|
5862
|
+
function createSegmentTokenLoaderClass(BaseLoader, tokenSource) {
|
|
5863
|
+
// WR-02: the "unsupported token type" diagnostic is logged to the console
|
|
5864
|
+
// at most once per loader CLASS — i.e. once per engine session that calls
|
|
5865
|
+
// createSegmentTokenLoaderClass, since hls.js constructs a fresh loader
|
|
5866
|
+
// instance per fragment load and an instance-level flag would reset every
|
|
5867
|
+
// load. `callbacks.onError` still fires on every load below, unguarded —
|
|
5868
|
+
// hls.js needs the error signal each time to drive its own retry/fatal
|
|
5869
|
+
// escalation; only the console spam is deduplicated.
|
|
5870
|
+
let unsupportedTokenTypeLogged = false;
|
|
5871
|
+
class SegmentTokenLoader extends BaseLoader {
|
|
5872
|
+
constructor(loaderConfig) {
|
|
5873
|
+
super(loaderConfig);
|
|
5874
|
+
const originalLoad = this.load.bind(this);
|
|
5875
|
+
this.load = function (context, loadConfig, callbacks) {
|
|
5876
|
+
// D-03/D-04: a token type outside the query-param family (akamai, or
|
|
5877
|
+
// none configured) must fail visibly rather than emit an unsigned
|
|
5878
|
+
// request. A silent passthrough here is the same undiagnosable 403
|
|
5879
|
+
// from the edge that D-04 forbids, so it gets the same treatment as
|
|
5880
|
+
// the akamai case: no delegation to the base loader, and a named
|
|
5881
|
+
// error surfaced through hls.js's own error callback.
|
|
5882
|
+
const tokenType = tokenSource.tokenType;
|
|
5883
|
+
if (!tokenType || !SEGMENT_SIGNING_TOKEN_TYPES.includes(tokenType)) {
|
|
5884
|
+
const message = segmentSigningUnsupportedMessage(tokenType);
|
|
5885
|
+
if (!unsupportedTokenTypeLogged) {
|
|
5886
|
+
unsupportedTokenTypeLogged = true;
|
|
5887
|
+
console.error(message);
|
|
5888
|
+
}
|
|
5889
|
+
callbacks.onError({ code: 0, text: message }, context, null, createEmptySegmentLoaderStats());
|
|
5890
|
+
return;
|
|
5891
|
+
}
|
|
5892
|
+
const baseUrl = context.frag?.baseurl;
|
|
5893
|
+
if (!baseUrl || !context.url) {
|
|
5894
|
+
originalLoad(context, loadConfig, callbacks);
|
|
5895
|
+
return;
|
|
5896
|
+
}
|
|
5897
|
+
context.url = signSegmentUrlFromPlaylist(context.url, baseUrl);
|
|
5898
|
+
originalLoad(context, loadConfig, callbacks);
|
|
5899
|
+
};
|
|
5900
|
+
}
|
|
5901
|
+
}
|
|
5902
|
+
return SegmentTokenLoader;
|
|
5903
|
+
}
|
|
5904
|
+
|
|
5503
5905
|
/**
|
|
5504
5906
|
* HlsEngine — HLS playback engine.
|
|
5505
5907
|
*
|
|
@@ -5683,6 +6085,7 @@
|
|
|
5683
6085
|
const hlsSafeSettings = { ...hlsEngineSettings };
|
|
5684
6086
|
delete hlsSafeSettings['extraParamsCallback'];
|
|
5685
6087
|
delete hlsSafeSettings['onCDNTokenError'];
|
|
6088
|
+
delete hlsSafeSettings['signSegmentsWithCdnToken'];
|
|
5686
6089
|
const driverConfig = {
|
|
5687
6090
|
autoStartLoad: !config.startAt,
|
|
5688
6091
|
enableWorker: true,
|
|
@@ -5726,6 +6129,12 @@
|
|
|
5726
6129
|
}
|
|
5727
6130
|
}
|
|
5728
6131
|
driverConfig.pLoader = PLoader;
|
|
6132
|
+
// Opt-in (D-06): sign segment requests with the grant carried by their
|
|
6133
|
+
// own parent playlist. Off by default so segment request shapes stay
|
|
6134
|
+
// byte-identical to today's when the flag is unset.
|
|
6135
|
+
if (config.engineSettings.signSegmentsWithCdnToken === true) {
|
|
6136
|
+
driverConfig.fLoader = createSegmentTokenLoaderClass(Hls.DefaultConfig.loader, tm);
|
|
6137
|
+
}
|
|
5729
6138
|
}
|
|
5730
6139
|
// Inject custom ABR controller unless disabled
|
|
5731
6140
|
if (!config.disableCustomAbr) {
|
|
@@ -5978,7 +6387,14 @@
|
|
|
5978
6387
|
* transient network failures (segment fetch, manifest re-fetch, etc.)
|
|
5979
6388
|
* never permanently stop playback.
|
|
5980
6389
|
*/
|
|
5981
|
-
|
|
6390
|
+
/**
|
|
6391
|
+
* Bounded retry-attempt count used for the intro pass so an unreachable
|
|
6392
|
+
* intro manifest surrenders quickly instead of retrying forever. With
|
|
6393
|
+
* retryIntervals at 1000ms this surfaces the dash.js ERROR event roughly
|
|
6394
|
+
* two seconds after an unreachable manifest.
|
|
6395
|
+
*/
|
|
6396
|
+
const INTRO_PASS_RETRY_ATTEMPTS = 2;
|
|
6397
|
+
function applyDashRetrySettings(driver, options = {}) {
|
|
5982
6398
|
driver.updateSettings({
|
|
5983
6399
|
streaming: {
|
|
5984
6400
|
retryIntervals: {
|
|
@@ -5993,16 +6409,17 @@
|
|
|
5993
6409
|
}
|
|
5994
6410
|
}
|
|
5995
6411
|
});
|
|
6412
|
+
const retryAttempts = options.introPass === true ? INTRO_PASS_RETRY_ATTEMPTS : Infinity;
|
|
5996
6413
|
driver.updateSettings({
|
|
5997
6414
|
streaming: {
|
|
5998
6415
|
retryAttempts: {
|
|
5999
|
-
MPD:
|
|
6000
|
-
XLinkExpansion:
|
|
6001
|
-
InitializationSegment:
|
|
6002
|
-
IndexSegment:
|
|
6003
|
-
MediaSegment:
|
|
6004
|
-
BitstreamSwitchingSegment:
|
|
6005
|
-
other:
|
|
6416
|
+
MPD: retryAttempts,
|
|
6417
|
+
XLinkExpansion: retryAttempts,
|
|
6418
|
+
InitializationSegment: retryAttempts,
|
|
6419
|
+
IndexSegment: retryAttempts,
|
|
6420
|
+
MediaSegment: retryAttempts,
|
|
6421
|
+
BitstreamSwitchingSegment: retryAttempts,
|
|
6422
|
+
other: retryAttempts,
|
|
6006
6423
|
lowLatencyMultiplyFactor: 1
|
|
6007
6424
|
}
|
|
6008
6425
|
}
|
|
@@ -6171,7 +6588,12 @@
|
|
|
6171
6588
|
* Ported from component/engines/dash.js
|
|
6172
6589
|
*
|
|
6173
6590
|
* Extends BaseEngine to bridge dash.js MediaPlayer events to PlayerState.
|
|
6174
|
-
* Uses CDN loader
|
|
6591
|
+
* Uses the CDN loader and StallWatchdog, plus DASH retry settings and ABR rules.
|
|
6592
|
+
*
|
|
6593
|
+
* CDN token signing (manifests and segments alike) is HLS-only by design —
|
|
6594
|
+
* this engine has no `CDNTokenManager` integration at all (Phase 08.1 / D-05).
|
|
6595
|
+
* DASH segments are `.m4s`, so nothing written for HLS's fragment-loader
|
|
6596
|
+
* token signing should be assumed to generalise here.
|
|
6175
6597
|
*
|
|
6176
6598
|
* CRITICAL: driver instance is a private class field — NEVER stored in PlayerState.
|
|
6177
6599
|
*/
|
|
@@ -6184,6 +6606,11 @@
|
|
|
6184
6606
|
// Private — NEVER in PlayerState
|
|
6185
6607
|
this.driver = null;
|
|
6186
6608
|
this.dvrErrorHandler = null;
|
|
6609
|
+
// True when this engine instance is loading the intro (preroll) stream
|
|
6610
|
+
// rather than the main stream — see D-04 / G-07-10 in init().
|
|
6611
|
+
this.isIntroPass = false;
|
|
6612
|
+
// Guards the one-shot error-fatal emit on the intro pass (T-07-21).
|
|
6613
|
+
this.introFallThroughReported = false;
|
|
6187
6614
|
}
|
|
6188
6615
|
getDriver() {
|
|
6189
6616
|
return this.driver;
|
|
@@ -6290,6 +6717,10 @@
|
|
|
6290
6717
|
const { config, video, state, signal } = this;
|
|
6291
6718
|
if (!config || !video || !state || !signal)
|
|
6292
6719
|
return;
|
|
6720
|
+
// D-04 / G-07-10: identify the intro pass so a permanently-unreachable
|
|
6721
|
+
// intro manifest can fall through to main instead of looping forever.
|
|
6722
|
+
// Same gate shape as src/engines/hls.ts:445.
|
|
6723
|
+
this.isIntroPass = Boolean(config.preroll) && this.loadSourceUrl === config.preroll;
|
|
6293
6724
|
const dashjsUrl = config.dashjs;
|
|
6294
6725
|
if (!dashjsUrl) {
|
|
6295
6726
|
throw new Error('DashEngine: config.dashjs URL is required');
|
|
@@ -6319,7 +6750,7 @@
|
|
|
6319
6750
|
player.initialize(video, this.loadSourceUrl || config.src || '', config.autoplay ?? false);
|
|
6320
6751
|
// Apply retry settings if requested
|
|
6321
6752
|
if (config.retry) {
|
|
6322
|
-
applyDashRetrySettings(player);
|
|
6753
|
+
applyDashRetrySettings(player, { introPass: this.isIntroPass });
|
|
6323
6754
|
}
|
|
6324
6755
|
// Apply custom ABR rule unless disabled
|
|
6325
6756
|
if (!config.disableCustomAbr) {
|
|
@@ -6420,12 +6851,34 @@
|
|
|
6420
6851
|
player.on(events['TRACK_CHANGE_RENDERED'], () => {
|
|
6421
6852
|
state.currentAudioTrack = player.getQualityFor('audio');
|
|
6422
6853
|
});
|
|
6423
|
-
// ERROR: log dash.js errors
|
|
6854
|
+
// ERROR: log dash.js errors, and on the intro pass fall through to main
|
|
6424
6855
|
player.on(events['ERROR'], (eventData) => {
|
|
6425
|
-
|
|
6426
|
-
console.warn('DashEngine error:', typedData?.error?.message ?? typedData?.error ?? typedData);
|
|
6856
|
+
this.handleDriverError(eventData);
|
|
6427
6857
|
});
|
|
6428
6858
|
}
|
|
6859
|
+
/**
|
|
6860
|
+
* Handles a dash.js ERROR event.
|
|
6861
|
+
*
|
|
6862
|
+
* D-04 / G-07-10: always logs, exactly as before (test/engines/dash.test.ts:619
|
|
6863
|
+
* and :1156 assert this unchanged). On the intro pass only — gated by
|
|
6864
|
+
* this.isIntroPass, computed once in init() from
|
|
6865
|
+
* `Boolean(config.preroll) && this.loadSourceUrl === config.preroll` — a fatal
|
|
6866
|
+
* manifest/segment error stops the stall watchdog and emits `error-fatal`
|
|
6867
|
+
* exactly once (guarded by introFallThroughReported) so
|
|
6868
|
+
* IntroStreamManager.complete() can fire and openMainStream() run instead of
|
|
6869
|
+
* the watchdog reopening the (still intro) lastSrc in a loop. On the main
|
|
6870
|
+
* pass this method only warns — main-stream resilience must stay untouched.
|
|
6871
|
+
*/
|
|
6872
|
+
handleDriverError(eventData) {
|
|
6873
|
+
const typedData = eventData;
|
|
6874
|
+
console.warn('DashEngine error:', typedData?.error?.message ?? typedData?.error ?? typedData);
|
|
6875
|
+
if (!this.isIntroPass || this.introFallThroughReported)
|
|
6876
|
+
return;
|
|
6877
|
+
this.introFallThroughReported = true;
|
|
6878
|
+
this.stopWatchdog();
|
|
6879
|
+
const errorKey = typedData?.error?.code ?? typedData?.error?.message ?? 'unknown';
|
|
6880
|
+
this.bus?.emit('error-fatal', { code: 'NETWORK', message: `Intro stream error: ${errorKey}` });
|
|
6881
|
+
}
|
|
6429
6882
|
}
|
|
6430
6883
|
|
|
6431
6884
|
/**
|
|
@@ -6581,18 +7034,30 @@
|
|
|
6581
7034
|
}
|
|
6582
7035
|
PLoader = SnapshotPLoader;
|
|
6583
7036
|
}
|
|
7037
|
+
// Build fLoader class with segment CDN token signing — opt-in (D-06), off by
|
|
7038
|
+
// default so segment request shapes stay byte-identical to today's when the
|
|
7039
|
+
// flag is unset. Reuses the shared factory built for the main engine's
|
|
7040
|
+
// fragment loader (`segment-token-loader.ts`) rather than re-implementing the
|
|
7041
|
+
// signing logic here. tokenManager is the same closure-captured reference
|
|
7042
|
+
// used above for pLoader (Pitfall 6) — never read off `this`.
|
|
7043
|
+
let FLoader;
|
|
7044
|
+
if (tokenManager && this.config.engineSettings?.signSegmentsWithCdnToken === true) {
|
|
7045
|
+
FLoader = createSegmentTokenLoaderClass(HlsConstructor.DefaultConfig.loader, tokenManager);
|
|
7046
|
+
}
|
|
6584
7047
|
// Strip player-specific keys that are NOT hls.js config options — they contain
|
|
6585
7048
|
// non-serializable functions that cause DataCloneError when hls.js posts config to its worker.
|
|
6586
7049
|
const rawSettings = { ...(this.config.engineSettings ?? {}) };
|
|
6587
7050
|
delete rawSettings['extraParamsCallback'];
|
|
6588
7051
|
delete rawSettings['onCDNTokenError'];
|
|
7052
|
+
delete rawSettings['signSegmentsWithCdnToken'];
|
|
6589
7053
|
const driverConfig = {
|
|
6590
7054
|
startLevel: 0,
|
|
6591
7055
|
enableWebVTT: false,
|
|
6592
7056
|
enableWorker: false,
|
|
6593
7057
|
maxBufferLength: 1,
|
|
6594
7058
|
...rawSettings,
|
|
6595
|
-
...(PLoader ? { pLoader: PLoader } : {})
|
|
7059
|
+
...(PLoader ? { pLoader: PLoader } : {}),
|
|
7060
|
+
...(FLoader ? { fLoader: FLoader } : {})
|
|
6596
7061
|
};
|
|
6597
7062
|
const driver = new HlsConstructor(driverConfig);
|
|
6598
7063
|
this.driver = driver;
|
|
@@ -6893,6 +7358,18 @@
|
|
|
6893
7358
|
// observably awaitable.
|
|
6894
7359
|
return new Promise((resolve) => setTimeout(resolve, EME_TEARDOWN_DELAY_MS));
|
|
6895
7360
|
}
|
|
7361
|
+
/**
|
|
7362
|
+
* G-07-11: bounded backstop so main always opens even when the ad leg never
|
|
7363
|
+
* produces 'ad-complete'. Longer than AdsManager's own AD_LEG_TIMEOUT_MS
|
|
7364
|
+
* (20s) so that more precise, better-logged inner mechanism wins first in a
|
|
7365
|
+
* live player — this is the outer safety net, not the primary signal. Covers
|
|
7366
|
+
* two cases the inner mechanism cannot: an AdsManager that was never
|
|
7367
|
+
* constructed at all (noUi:true, or a null ads container — lifecycle.ts
|
|
7368
|
+
* skips the branch that would create one), and an AdsManager that obtained
|
|
7369
|
+
* its manager from the loader (disarming its own bound) whose creative then
|
|
7370
|
+
* never reached the screen.
|
|
7371
|
+
*/
|
|
7372
|
+
const MAIN_OPEN_FALLBACK_MS = 30000;
|
|
6896
7373
|
// ---------------------------------------------------------------------------
|
|
6897
7374
|
// Public API
|
|
6898
7375
|
// ---------------------------------------------------------------------------
|
|
@@ -7103,13 +7580,17 @@
|
|
|
7103
7580
|
controller.bus.on('error-fatal', () => {
|
|
7104
7581
|
inst.p2p?.stop();
|
|
7105
7582
|
}, { signal: controller.signal });
|
|
7106
|
-
// Phase 7 —
|
|
7583
|
+
// Phase 7/8 — pre-main leg orchestration.
|
|
7107
7584
|
// When mergedConfig.preroll is set, open the intro stream first, then on
|
|
7108
7585
|
// bus 'intro-stream-complete' (emitted by IntroStreamManager on video.ended,
|
|
7109
7586
|
// preroll-skip, or error-fatal) close → 500ms EME teardown delay → open main.
|
|
7110
7587
|
// If config.ad is also set, wait additionally for bus 'ad-complete' before
|
|
7111
7588
|
// opening main (D-10: intro → ad → main precedence).
|
|
7112
7589
|
//
|
|
7590
|
+
// D-02 (Phase 8): when mergedConfig.preroll is NOT set but mergedConfig.ad
|
|
7591
|
+
// IS, main is gated on 'ad-complete' alone (no intro leg to precede it) —
|
|
7592
|
+
// see the `hasAd` branch below and tryOpenMainAdOnly().
|
|
7593
|
+
//
|
|
7113
7594
|
// Runtime mute override: the live <video>.muted and PlayerState.muted are
|
|
7114
7595
|
// forced to false for the intro pass (D-13 — intros always audible) and
|
|
7115
7596
|
// restored to `originalMuted` before opening main. We do NOT mutate
|
|
@@ -7119,7 +7600,22 @@
|
|
|
7119
7600
|
// P2P opt-in: `mergedConfig.manager` IS re-read on each reference.open() in
|
|
7120
7601
|
// the P2P branch (eb-player.ts:192), so we config-level swap it to false for
|
|
7121
7602
|
// the intro pass and restore it for main (D-08 — P2P attaches only to main).
|
|
7603
|
+
// G-07-11: two independent paths can now reach openMainStream — the
|
|
7604
|
+
// ad-complete handler and the MAIN_OPEN_FALLBACK_MS backstop — so it must
|
|
7605
|
+
// be idempotent.
|
|
7606
|
+
let mainOpened = false;
|
|
7122
7607
|
const openMainStream = () => {
|
|
7608
|
+
if (mainOpened)
|
|
7609
|
+
return;
|
|
7610
|
+
mainOpened = true;
|
|
7611
|
+
// G-07-13 / D-10: force-complete the intro pass on EVERY path into main —
|
|
7612
|
+
// including ad-complete firing with no prior intro-stream-complete (a hung
|
|
7613
|
+
// intro deferred through AdsManager's INTRO_DEFERRAL_TIMEOUT_MS). This is a
|
|
7614
|
+
// FORCE-complete, never a wait: IntroStreamManager.complete() runs
|
|
7615
|
+
// synchronously, so D-04 liveness is untouched — main still opens in the
|
|
7616
|
+
// same tick. complete() is idempotent, so on the normal ordering (intro
|
|
7617
|
+
// already finished) this is a no-op; the listener is already torn down.
|
|
7618
|
+
controller.bus.emit('force-intro-complete');
|
|
7123
7619
|
if (video !== null) {
|
|
7124
7620
|
video.muted = originalMuted;
|
|
7125
7621
|
}
|
|
@@ -7145,8 +7641,100 @@
|
|
|
7145
7641
|
return;
|
|
7146
7642
|
reference.open(mergedConfig.src);
|
|
7147
7643
|
};
|
|
7148
|
-
|
|
7149
|
-
|
|
7644
|
+
// D-02 (Phase 8): promoted out of the `preroll` branch so both the
|
|
7645
|
+
// intro+ad and ad-only configurations share one gated main-open path — see
|
|
7646
|
+
// the assumption_delta_decision in 08-01-PLAN.md ("pluralization": preroll
|
|
7647
|
+
// and ad are two variants of a single "pre-main leg" concept).
|
|
7648
|
+
const hasIntro = Boolean(mergedConfig.preroll);
|
|
7649
|
+
const hasAd = Boolean(mergedConfig.ad);
|
|
7650
|
+
// G-07-11: bounds the ad leg from this player's side, independent of
|
|
7651
|
+
// AdsManager's own internal watchdog — see MAIN_OPEN_FALLBACK_MS.
|
|
7652
|
+
let mainOpenFallbackTimer = null;
|
|
7653
|
+
const clearMainOpenFallback = () => {
|
|
7654
|
+
if (mainOpenFallbackTimer !== null) {
|
|
7655
|
+
clearTimeout(mainOpenFallbackTimer);
|
|
7656
|
+
mainOpenFallbackTimer = null;
|
|
7657
|
+
}
|
|
7658
|
+
};
|
|
7659
|
+
const armMainOpenFallback = (onExpire) => {
|
|
7660
|
+
mainOpenFallbackTimer = setTimeout(() => {
|
|
7661
|
+
if (controller.signal.aborted)
|
|
7662
|
+
return;
|
|
7663
|
+
// CR-01: an ad still playing when the bound elapses is legitimate — a
|
|
7664
|
+
// 60s creative outlives the 30s window. But simply returning here would
|
|
7665
|
+
// retire the ONLY remaining bound on the ad leg: AdsManager disarms its
|
|
7666
|
+
// own AD_LEG_TIMEOUT_MS watchdog as soon as IMA hands back a manager.
|
|
7667
|
+
// An ad that then stalls (no ad-complete, no AD_ERROR) would strand main
|
|
7668
|
+
// playback for the life of the player. Re-arm instead, so the leg stays
|
|
7669
|
+
// bounded on every path for as long as the ad genuinely progresses.
|
|
7670
|
+
if (controller.state.adPlaying) {
|
|
7671
|
+
armMainOpenFallback(onExpire);
|
|
7672
|
+
return;
|
|
7673
|
+
}
|
|
7674
|
+
console.warn('eb-player: ad leg produced no ad-complete within the bound — falling through to main (G-07-11)');
|
|
7675
|
+
onExpire();
|
|
7676
|
+
}, MAIN_OPEN_FALLBACK_MS);
|
|
7677
|
+
};
|
|
7678
|
+
// WR-03: clear the fallback timer on dispose — otherwise a torn-down
|
|
7679
|
+
// player leaves a pending setTimeout holding a closure reference alive.
|
|
7680
|
+
controller.signal.addEventListener('abort', () => {
|
|
7681
|
+
clearMainOpenFallback();
|
|
7682
|
+
}, { once: true });
|
|
7683
|
+
// D-08-02 (Phase 8): the two-gate compound funnel for the ad-only
|
|
7684
|
+
// configuration. With no intro leg, main can open only once BOTH the ad
|
|
7685
|
+
// leg has settled (adGateOpen) AND the user has requested playback
|
|
7686
|
+
// (playGateOpen) — see the `else if (hasAd)` branch below. With no ad
|
|
7687
|
+
// configured the ad gate starts open. With `autoplay: false`, the ad-leg
|
|
7688
|
+
// bound is now armed unconditionally (D-08-03) and sits BEFORE
|
|
7689
|
+
// AdsManager's `await gesturePromise`, so `ad-complete` can fire ~20s
|
|
7690
|
+
// after page load with zero user interaction. Without this play gate that
|
|
7691
|
+
// alone would fetch the CDN token and load the main manifest, breaking the
|
|
7692
|
+
// documented no-prefetch contract (src/eb-player.ts:598-601 below). The
|
|
7693
|
+
// gate is deliberately kept, not dropped (A-OQ1, 08-01-PLAN.md).
|
|
7694
|
+
let adGateOpen = !hasAd;
|
|
7695
|
+
let playGateOpen = Boolean(mergedConfig.autoplay);
|
|
7696
|
+
let deferredPlaybackStarted = false;
|
|
7697
|
+
const startDeferredPlayback = () => {
|
|
7698
|
+
if (deferredPlaybackStarted)
|
|
7699
|
+
return;
|
|
7700
|
+
deferredPlaybackStarted = true;
|
|
7701
|
+
const engine = inst.engine;
|
|
7702
|
+
if (engine === null || video === null)
|
|
7703
|
+
return;
|
|
7704
|
+
// Honor the user's click intent. CommandHandler's earlier bus.on('play')
|
|
7705
|
+
// listener fired before us with no source attached, so its video.play()
|
|
7706
|
+
// was a silent no-op. Wait for driverReady (attachMedia + loadSource
|
|
7707
|
+
// done) then start playback ourselves.
|
|
7708
|
+
engine.driverReady.then(() => {
|
|
7709
|
+
const playResult = video.play();
|
|
7710
|
+
if (playResult && typeof playResult.catch === 'function') {
|
|
7711
|
+
playResult.catch((error) => {
|
|
7712
|
+
console.warn('EBPlayer: deferred play() rejected', error);
|
|
7713
|
+
});
|
|
7714
|
+
}
|
|
7715
|
+
});
|
|
7716
|
+
};
|
|
7717
|
+
const tryOpenMainAdOnly = () => {
|
|
7718
|
+
if (!adGateOpen || !playGateOpen)
|
|
7719
|
+
return;
|
|
7720
|
+
openMainStream();
|
|
7721
|
+
if (!mergedConfig.autoplay) {
|
|
7722
|
+
startDeferredPlayback();
|
|
7723
|
+
}
|
|
7724
|
+
};
|
|
7725
|
+
if (hasAd) {
|
|
7726
|
+
controller.bus.on('ad-complete', () => {
|
|
7727
|
+
clearMainOpenFallback();
|
|
7728
|
+
adGateOpen = true;
|
|
7729
|
+
if (hasIntro) {
|
|
7730
|
+
openMainStream();
|
|
7731
|
+
}
|
|
7732
|
+
else {
|
|
7733
|
+
tryOpenMainAdOnly();
|
|
7734
|
+
}
|
|
7735
|
+
}, { signal: controller.signal });
|
|
7736
|
+
}
|
|
7737
|
+
if (hasIntro) {
|
|
7150
7738
|
controller.bus.on('intro-stream-complete', () => {
|
|
7151
7739
|
reference.close();
|
|
7152
7740
|
// WR-08: restore PlayerState.muted synchronously — DO NOT wait for the
|
|
@@ -7160,18 +7748,25 @@
|
|
|
7160
7748
|
controller.state.muted = originalMuted;
|
|
7161
7749
|
// EME teardown delay — see waitForEmeTeardown / EME_TEARDOWN_DELAY_MS.
|
|
7162
7750
|
waitForEmeTeardown().then(() => {
|
|
7751
|
+
// G-07-13: main may already be open — the ad-complete path force-
|
|
7752
|
+
// completes the intro (openMainStream() emits force-intro-complete),
|
|
7753
|
+
// which re-enters this handler synchronously before this async
|
|
7754
|
+
// continuation ever runs. Arming the fallback timer here would be an
|
|
7755
|
+
// orphan: main is already open and nothing will ever clear it.
|
|
7756
|
+
if (mainOpened)
|
|
7757
|
+
return;
|
|
7163
7758
|
if (!hasAd) {
|
|
7164
7759
|
openMainStream();
|
|
7760
|
+
return;
|
|
7165
7761
|
}
|
|
7166
|
-
//
|
|
7167
|
-
// after the AdsManager finishes (D-10 precedence).
|
|
7762
|
+
// hasAd: the ad-complete handler above will normally call
|
|
7763
|
+
// openMainStream after the AdsManager finishes (D-10 precedence).
|
|
7764
|
+
// G-07-11: arm the bounded fallback in case it never does — an
|
|
7765
|
+
// AdsManager that was never constructed, or one whose creative
|
|
7766
|
+
// never rendered after disarming its own bound.
|
|
7767
|
+
armMainOpenFallback(openMainStream);
|
|
7168
7768
|
});
|
|
7169
7769
|
}, { signal: controller.signal });
|
|
7170
|
-
if (hasAd) {
|
|
7171
|
-
controller.bus.on('ad-complete', () => {
|
|
7172
|
-
openMainStream();
|
|
7173
|
-
}, { signal: controller.signal });
|
|
7174
|
-
}
|
|
7175
7770
|
// Force runtime mute off for the intro pass — applied to the LIVE <video>
|
|
7176
7771
|
// element and PlayerState, NOT to mergedConfig.muted (no-op for runtime).
|
|
7177
7772
|
if (video !== null) {
|
|
@@ -7184,6 +7779,52 @@
|
|
|
7184
7779
|
controller.config.manager = false;
|
|
7185
7780
|
reference.open(mergedConfig.preroll);
|
|
7186
7781
|
}
|
|
7782
|
+
// D-02/D-08-02 (Phase 8): ad-only configuration — no intro stream, so this
|
|
7783
|
+
// must NOT call reference.open() directly and must NOT copy any
|
|
7784
|
+
// intro-specific step (no intro source open, no force-unmute, no P2P
|
|
7785
|
+
// manager swap, no intro-stream-complete listener). Main opens only via
|
|
7786
|
+
// tryOpenMainAdOnly() once both gates clear.
|
|
7787
|
+
else if (hasAd) {
|
|
7788
|
+
if (mergedConfig.autoplay) {
|
|
7789
|
+
// playGateOpen is already true (set above) — main opens on whichever
|
|
7790
|
+
// of 'ad-complete' or the fallback arrives first. tryOpenMainAdOnly()
|
|
7791
|
+
// alone is NOT a real backstop here: it returns early unless
|
|
7792
|
+
// adGateOpen is already true, and the only place that flips
|
|
7793
|
+
// adGateOpen is the ad-complete handler above, which clears this very
|
|
7794
|
+
// timer before calling it — so an unmodified tryOpenMainAdOnly passed
|
|
7795
|
+
// directly can never observe adGateOpen === false the one time it
|
|
7796
|
+
// would need to. Force the ad gate open in the callback itself so the
|
|
7797
|
+
// bound can still open main when ad-complete never arrives at all
|
|
7798
|
+
// (e.g. AdsManager is never constructed) — see G-07-11.
|
|
7799
|
+
armMainOpenFallback(() => {
|
|
7800
|
+
adGateOpen = true;
|
|
7801
|
+
tryOpenMainAdOnly();
|
|
7802
|
+
});
|
|
7803
|
+
}
|
|
7804
|
+
else {
|
|
7805
|
+
// D-08-02: the 'play' gate — see the rationale comment above
|
|
7806
|
+
// tryOpenMainAdOnly(). A repeat click must be inert.
|
|
7807
|
+
controller.bus.on('play', () => {
|
|
7808
|
+
if (playGateOpen)
|
|
7809
|
+
return;
|
|
7810
|
+
playGateOpen = true;
|
|
7811
|
+
if (adGateOpen) {
|
|
7812
|
+
// The ad leg already settled — main can open now.
|
|
7813
|
+
tryOpenMainAdOnly();
|
|
7814
|
+
startDeferredPlayback();
|
|
7815
|
+
}
|
|
7816
|
+
else {
|
|
7817
|
+
// The ad leg is still in flight — start the outer 30s backstop
|
|
7818
|
+
// from the play request, not from page load, using the same
|
|
7819
|
+
// force-the-gate shape as the autoplay:true arm above.
|
|
7820
|
+
armMainOpenFallback(() => {
|
|
7821
|
+
adGateOpen = true;
|
|
7822
|
+
tryOpenMainAdOnly();
|
|
7823
|
+
});
|
|
7824
|
+
}
|
|
7825
|
+
}, { signal: controller.signal });
|
|
7826
|
+
}
|
|
7827
|
+
}
|
|
7187
7828
|
// Auto-open the stream if src is provided in config (matches legacy player behaviour
|
|
7188
7829
|
// where consumers call start({ src: '...' }) and expect playback to begin immediately).
|
|
7189
7830
|
// When autoplay is false, defer open() until the user requests play — this avoids
|
|
@@ -7199,21 +7840,7 @@
|
|
|
7199
7840
|
return;
|
|
7200
7841
|
deferredOpen = false;
|
|
7201
7842
|
reference.open(mergedConfig.src);
|
|
7202
|
-
|
|
7203
|
-
// listener fired before us with no source attached, so its video.play()
|
|
7204
|
-
// was a silent no-op. Wait for driverReady (attachMedia + loadSource
|
|
7205
|
-
// done) then start playback ourselves.
|
|
7206
|
-
const engine = inst.engine;
|
|
7207
|
-
if (engine !== null && video !== null) {
|
|
7208
|
-
engine.driverReady.then(() => {
|
|
7209
|
-
const playResult = video.play();
|
|
7210
|
-
if (playResult && typeof playResult.catch === 'function') {
|
|
7211
|
-
playResult.catch((error) => {
|
|
7212
|
-
console.warn('EBPlayer: deferred play() rejected', error);
|
|
7213
|
-
});
|
|
7214
|
-
}
|
|
7215
|
-
});
|
|
7216
|
-
}
|
|
7843
|
+
startDeferredPlayback();
|
|
7217
7844
|
}, { signal: controller.signal });
|
|
7218
7845
|
}
|
|
7219
7846
|
}
|