eb-player 2.0.20 → 2.0.21

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.
Files changed (151) hide show
  1. package/dist/build/ebplayer.bundle.js +586 -138
  2. package/dist/build/ebplayer.bundle.js.map +1 -1
  3. package/dist/build/types/src/core/command-handler.d.ts +50 -0
  4. package/dist/build/types/src/core/command-handler.d.ts.map +1 -0
  5. package/dist/build/types/src/core/config.d.ts +187 -0
  6. package/dist/build/types/src/core/config.d.ts.map +1 -0
  7. package/dist/build/types/src/core/engine-state-sync.d.ts +38 -0
  8. package/dist/build/types/src/core/engine-state-sync.d.ts.map +1 -0
  9. package/dist/build/types/src/core/event-bus.d.ts +91 -0
  10. package/dist/build/types/src/core/event-bus.d.ts.map +1 -0
  11. package/dist/build/types/src/core/fsm.d.ts +30 -0
  12. package/dist/build/types/src/core/fsm.d.ts.map +1 -0
  13. package/dist/build/types/src/core/i18n.d.ts +51 -0
  14. package/dist/build/types/src/core/i18n.d.ts.map +1 -0
  15. package/dist/build/types/src/core/index.d.ts +23 -0
  16. package/dist/build/types/src/core/index.d.ts.map +1 -0
  17. package/dist/build/types/src/core/lifecycle.d.ts +96 -0
  18. package/dist/build/types/src/core/lifecycle.d.ts.map +1 -0
  19. package/dist/build/types/src/core/player-state.d.ts +15 -0
  20. package/dist/build/types/src/core/player-state.d.ts.map +1 -0
  21. package/dist/build/types/src/core/types.d.ts +85 -0
  22. package/dist/build/types/src/core/types.d.ts.map +1 -0
  23. package/dist/build/types/src/eb-player-standalone.d.ts +23 -0
  24. package/dist/build/types/src/eb-player-standalone.d.ts.map +1 -0
  25. package/dist/build/types/src/eb-player.d.ts +56 -0
  26. package/dist/build/types/src/eb-player.d.ts.map +1 -0
  27. package/dist/build/types/src/engines/abr/dash.d.ts +56 -0
  28. package/dist/build/types/src/engines/abr/dash.d.ts.map +1 -0
  29. package/dist/build/types/src/engines/abr/hls.d.ts +105 -0
  30. package/dist/build/types/src/engines/abr/hls.d.ts.map +1 -0
  31. package/dist/build/types/src/engines/base-engine.d.ts +72 -0
  32. package/dist/build/types/src/engines/base-engine.d.ts.map +1 -0
  33. package/dist/build/types/src/engines/cdn-loader.d.ts +20 -0
  34. package/dist/build/types/src/engines/cdn-loader.d.ts.map +1 -0
  35. package/dist/build/types/src/engines/cdn-token-manager.d.ts +137 -0
  36. package/dist/build/types/src/engines/cdn-token-manager.d.ts.map +1 -0
  37. package/dist/build/types/src/engines/dash.d.ts +95 -0
  38. package/dist/build/types/src/engines/dash.d.ts.map +1 -0
  39. package/dist/build/types/src/engines/drm.d.ts +54 -0
  40. package/dist/build/types/src/engines/drm.d.ts.map +1 -0
  41. package/dist/build/types/src/engines/hls-discontinuity-patch.d.ts +43 -0
  42. package/dist/build/types/src/engines/hls-discontinuity-patch.d.ts.map +1 -0
  43. package/dist/build/types/src/engines/hls.d.ts +53 -0
  44. package/dist/build/types/src/engines/hls.d.ts.map +1 -0
  45. package/dist/build/types/src/engines/index.d.ts +26 -0
  46. package/dist/build/types/src/engines/index.d.ts.map +1 -0
  47. package/dist/build/types/src/engines/ios/hls.d.ts +20 -0
  48. package/dist/build/types/src/engines/ios/hls.d.ts.map +1 -0
  49. package/dist/build/types/src/engines/poster/hls.d.ts +35 -0
  50. package/dist/build/types/src/engines/poster/hls.d.ts.map +1 -0
  51. package/dist/build/types/src/engines/retry/dash.d.ts +51 -0
  52. package/dist/build/types/src/engines/retry/dash.d.ts.map +1 -0
  53. package/dist/build/types/src/engines/retry/hls.d.ts +47 -0
  54. package/dist/build/types/src/engines/retry/hls.d.ts.map +1 -0
  55. package/dist/build/types/src/engines/snapshot/dash.d.ts +55 -0
  56. package/dist/build/types/src/engines/snapshot/dash.d.ts.map +1 -0
  57. package/dist/build/types/src/engines/snapshot/hls.d.ts +118 -0
  58. package/dist/build/types/src/engines/snapshot/hls.d.ts.map +1 -0
  59. package/dist/build/types/src/engines/stall-watchdog.d.ts +41 -0
  60. package/dist/build/types/src/engines/stall-watchdog.d.ts.map +1 -0
  61. package/dist/build/types/src/integrations/ads-manager.d.ts +79 -0
  62. package/dist/build/types/src/integrations/ads-manager.d.ts.map +1 -0
  63. package/dist/build/types/src/integrations/chromecast-manager.d.ts +50 -0
  64. package/dist/build/types/src/integrations/chromecast-manager.d.ts.map +1 -0
  65. package/dist/build/types/src/integrations/epg-manager.d.ts +22 -0
  66. package/dist/build/types/src/integrations/epg-manager.d.ts.map +1 -0
  67. package/dist/build/types/src/integrations/index.d.ts +7 -0
  68. package/dist/build/types/src/integrations/index.d.ts.map +1 -0
  69. package/dist/build/types/src/integrations/intro-stream-manager.d.ts +62 -0
  70. package/dist/build/types/src/integrations/intro-stream-manager.d.ts.map +1 -0
  71. package/dist/build/types/src/integrations/p2p-manager.d.ts +42 -0
  72. package/dist/build/types/src/integrations/p2p-manager.d.ts.map +1 -0
  73. package/dist/build/types/src/integrations/playlist-manager.d.ts +21 -0
  74. package/dist/build/types/src/integrations/playlist-manager.d.ts.map +1 -0
  75. package/dist/build/types/src/skin/bars/bottom-bar.d.ts +16 -0
  76. package/dist/build/types/src/skin/bars/bottom-bar.d.ts.map +1 -0
  77. package/dist/build/types/src/skin/bars/middle-bar.d.ts +16 -0
  78. package/dist/build/types/src/skin/bars/middle-bar.d.ts.map +1 -0
  79. package/dist/build/types/src/skin/bars/top-bar.d.ts +15 -0
  80. package/dist/build/types/src/skin/bars/top-bar.d.ts.map +1 -0
  81. package/dist/build/types/src/skin/base-component.d.ts +69 -0
  82. package/dist/build/types/src/skin/base-component.d.ts.map +1 -0
  83. package/dist/build/types/src/skin/brand/forja-playlist-bar.d.ts +15 -0
  84. package/dist/build/types/src/skin/brand/forja-playlist-bar.d.ts.map +1 -0
  85. package/dist/build/types/src/skin/brand/snrt-radio-carousel.d.ts +16 -0
  86. package/dist/build/types/src/skin/brand/snrt-radio-carousel.d.ts.map +1 -0
  87. package/dist/build/types/src/skin/component-registry.d.ts +16 -0
  88. package/dist/build/types/src/skin/component-registry.d.ts.map +1 -0
  89. package/dist/build/types/src/skin/controllers/auto-hide.d.ts +27 -0
  90. package/dist/build/types/src/skin/controllers/auto-hide.d.ts.map +1 -0
  91. package/dist/build/types/src/skin/controllers/keyboard.d.ts +29 -0
  92. package/dist/build/types/src/skin/controllers/keyboard.d.ts.map +1 -0
  93. package/dist/build/types/src/skin/controls/cast-button.d.ts +15 -0
  94. package/dist/build/types/src/skin/controls/cast-button.d.ts.map +1 -0
  95. package/dist/build/types/src/skin/controls/channel-name.d.ts +16 -0
  96. package/dist/build/types/src/skin/controls/channel-name.d.ts.map +1 -0
  97. package/dist/build/types/src/skin/controls/forward-button.d.ts +15 -0
  98. package/dist/build/types/src/skin/controls/forward-button.d.ts.map +1 -0
  99. package/dist/build/types/src/skin/controls/fullscreen-button.d.ts +30 -0
  100. package/dist/build/types/src/skin/controls/fullscreen-button.d.ts.map +1 -0
  101. package/dist/build/types/src/skin/controls/info-button.d.ts +10 -0
  102. package/dist/build/types/src/skin/controls/info-button.d.ts.map +1 -0
  103. package/dist/build/types/src/skin/controls/live-sync-button.d.ts +20 -0
  104. package/dist/build/types/src/skin/controls/live-sync-button.d.ts.map +1 -0
  105. package/dist/build/types/src/skin/controls/pip-button.d.ts +21 -0
  106. package/dist/build/types/src/skin/controls/pip-button.d.ts.map +1 -0
  107. package/dist/build/types/src/skin/controls/play-pause-button.d.ts +14 -0
  108. package/dist/build/types/src/skin/controls/play-pause-button.d.ts.map +1 -0
  109. package/dist/build/types/src/skin/controls/rewind-button.d.ts +15 -0
  110. package/dist/build/types/src/skin/controls/rewind-button.d.ts.map +1 -0
  111. package/dist/build/types/src/skin/controls/seekbar.d.ts +48 -0
  112. package/dist/build/types/src/skin/controls/seekbar.d.ts.map +1 -0
  113. package/dist/build/types/src/skin/controls/settings-panel.d.ts +53 -0
  114. package/dist/build/types/src/skin/controls/settings-panel.d.ts.map +1 -0
  115. package/dist/build/types/src/skin/controls/share-button.d.ts +11 -0
  116. package/dist/build/types/src/skin/controls/share-button.d.ts.map +1 -0
  117. package/dist/build/types/src/skin/controls/time-display.d.ts +19 -0
  118. package/dist/build/types/src/skin/controls/time-display.d.ts.map +1 -0
  119. package/dist/build/types/src/skin/controls/volume-control.d.ts +21 -0
  120. package/dist/build/types/src/skin/controls/volume-control.d.ts.map +1 -0
  121. package/dist/build/types/src/skin/icons/icons-modern.d.ts +10 -0
  122. package/dist/build/types/src/skin/icons/icons-modern.d.ts.map +1 -0
  123. package/dist/build/types/src/skin/icons/icons.d.ts +10 -0
  124. package/dist/build/types/src/skin/icons/icons.d.ts.map +1 -0
  125. package/dist/build/types/src/skin/icons/sprite.d.ts +39 -0
  126. package/dist/build/types/src/skin/icons/sprite.d.ts.map +1 -0
  127. package/dist/build/types/src/skin/index.d.ts +24 -0
  128. package/dist/build/types/src/skin/index.d.ts.map +1 -0
  129. package/dist/build/types/src/skin/overlays/error-message.d.ts +14 -0
  130. package/dist/build/types/src/skin/overlays/error-message.d.ts.map +1 -0
  131. package/dist/build/types/src/skin/overlays/info-overlay.d.ts +15 -0
  132. package/dist/build/types/src/skin/overlays/info-overlay.d.ts.map +1 -0
  133. package/dist/build/types/src/skin/overlays/loading-spinner.d.ts +17 -0
  134. package/dist/build/types/src/skin/overlays/loading-spinner.d.ts.map +1 -0
  135. package/dist/build/types/src/skin/overlays/preroll-overlay.d.ts +39 -0
  136. package/dist/build/types/src/skin/overlays/preroll-overlay.d.ts.map +1 -0
  137. package/dist/build/types/src/skin/overlays/socials-overlay.d.ts +17 -0
  138. package/dist/build/types/src/skin/overlays/socials-overlay.d.ts.map +1 -0
  139. package/dist/build/types/src/skin/overlays/toast-notification.d.ts +18 -0
  140. package/dist/build/types/src/skin/overlays/toast-notification.d.ts.map +1 -0
  141. package/dist/build/types/src/skin/skin-root.d.ts +88 -0
  142. package/dist/build/types/src/skin/skin-root.d.ts.map +1 -0
  143. package/dist/build/types/src/utils/chapters.d.ts +16 -0
  144. package/dist/build/types/src/utils/chapters.d.ts.map +1 -0
  145. package/dist/build/types/src/utils/format-duration.d.ts +9 -0
  146. package/dist/build/types/src/utils/format-duration.d.ts.map +1 -0
  147. package/dist/build/types/src/utils/format-wall-clock.d.ts +6 -0
  148. package/dist/build/types/src/utils/format-wall-clock.d.ts.map +1 -0
  149. package/dist/build/types/src/utils/settings-helpers.d.ts +41 -0
  150. package/dist/build/types/src/utils/settings-helpers.d.ts.map +1 -0
  151. 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.20";
7
+ var __EB_PLAYER_VERSION__ = "2.0.21";
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.2");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};
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
- * - Loads the IMA SDK from CDN via loadScript
3888
- * - Gates displayContainer.initialize() on a user gesture when config.autoplay is false
3889
- * (browser autoplay policy requirement — see Pitfall 2 in plan context)
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 when all ads are done
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,270 @@
3899
3921
  /**
3900
3922
  * Initialize the IMA SDK preroll ad system.
3901
3923
  * - Skips if config.ad is undefined
3902
- * - If !config.autoplay: waits for user click on video element before initializing
3903
- * (required by browser autoplay policies to avoid muted/blocked display contexts)
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
- // CR-01: D-10 intro → ad → main precedence. If an intro stream is currently
3911
- // playing, defer IMA initialization until the bus emits 'intro-stream-complete'.
3912
- // Without this gate, IMA fires CONTENT_PAUSE_REQUESTED and tries to play the ad
3913
- // in the same <video> element the intro is using — the two streams race for
3914
- // the element with order determined by network/SDK timing.
3915
- if (state.introStreamPlaying) {
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
+ const gesturePromise = config.autoplay ? null : this.waitForUserGesture(video, signal);
3996
+ // G-07-9: the decision is derived from configuration, not from the
3997
+ // PlayerState intro-playing flag. An async function body runs
3998
+ // synchronously up to its first await, so reading that flag here would
3999
+ // always observe its createPlayerState() default — IntroStreamManager
4000
+ // has not run yet at this point in initIntegrations(). config.preroll is
4001
+ // immutable and readable regardless of integration init order.
4002
+ if (config.preroll) {
3916
4003
  await new Promise((resolve) => {
3917
- bus.on('intro-stream-complete', () => resolve(), { signal });
3918
- signal.addEventListener('abort', () => resolve(), { once: true });
4004
+ const settle = () => {
4005
+ clearIntroDeferralTimer();
4006
+ resolve();
4007
+ };
4008
+ bus.on('intro-stream-complete', settle, { signal });
4009
+ signal.addEventListener('abort', settle, { once: true });
4010
+ introDeferralTimer = setTimeout(() => {
4011
+ console.warn('AdsManager: intro-stream-complete never arrived within the bound — falling through to the ad leg (G-07-9)');
4012
+ settle();
4013
+ }, INTRO_DEFERRAL_TIMEOUT_MS);
3919
4014
  });
3920
4015
  if (signal.aborted)
3921
4016
  return;
3922
4017
  }
3923
- // Gate initialization on user gesture when not in autoplay mode
3924
- if (!config.autoplay) {
3925
- await this.waitForUserGesture(video, signal);
3926
- if (signal.aborted)
4018
+ // G-07-11 / D-08-03: the ad-leg watchdog bounds every step from here —
4019
+ // the gesture wait, loadScript, the ad request and the ads response —
4020
+ // until the loader hands back an AdsManager, which disarms it below.
4021
+ // Armed unconditionally whenever config.ad is set (init() already
4022
+ // returned above at the `!config.ad` early return, so this line is only
4023
+ // ever reached with config.ad truthy): as of D-02, main is gated on
4024
+ // 'ad-complete' in BOTH the intro+ad and ad-only configurations, so an
4025
+ // unbounded leg in either shape is a permanent stuck loader — the exact
4026
+ // failure VAST-05 forbids. Invariant: whenever main waits on the ad leg,
4027
+ // the ad leg is bounded. Do not restore an `if (config.preroll)` guard
4028
+ // here — that would leave the ad-only configuration unbounded again.
4029
+ adLegTimer = setTimeout(() => {
4030
+ console.warn('AdsManager: ad leg produced no AdsManager within the bound — falling through to main (G-07-11)');
4031
+ completeAdLeg();
4032
+ }, AD_LEG_TIMEOUT_MS);
4033
+ // Consume the gesture observed above. A click that arrives after the ad
4034
+ // leg has already completed must not resurrect it.
4035
+ if (gesturePromise !== null) {
4036
+ await gesturePromise;
4037
+ }
4038
+ if (signal.aborted || adLegCompleted)
4039
+ return;
4040
+ try {
4041
+ const google = await loadScript(IMA_SDK_URL, 'google');
4042
+ // G-07-11: the release above must be terminal. A watchdog that
4043
+ // expired while loadScript was still in flight must not let a
4044
+ // resolving SDK rebuild the display container it just destroyed.
4045
+ if (adLegCompleted)
3927
4046
  return;
3928
- }
3929
- const google = await loadScript(IMA_SDK_URL, 'google');
3930
- const { ima } = google;
3931
- const displayContainer = new ima.AdDisplayContainer(adsContainer, video);
3932
- this.imaDisplayContainer = displayContainer;
3933
- displayContainer.initialize();
3934
- const adsLoader = new ima.AdsLoader(displayContainer);
3935
- // AD_ERROR fall-through (CR-02): if the VAST tag fails to load (network
3936
- // error, ad-blocker, malformed XML, autoplay rejection), IMA fires
3937
- // AD_ERROR and never ALL_ADS_COMPLETED. Without this handler the
3938
- // orchestration in eb-player.ts would wait forever for 'ad-complete'
3939
- // and the main stream would never open. Treat AD_ERROR as a normal
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();
4047
+ const { ima } = google;
4048
+ const displayContainer = new ima.AdDisplayContainer(adsContainer, video);
4049
+ this.imaDisplayContainer = displayContainer;
4050
+ displayContainer.initialize();
4051
+ const adsLoader = new ima.AdsLoader(displayContainer);
4052
+ // AD_ERROR fall-through (CR-02): if the VAST tag fails to load (network
4053
+ // error, ad-blocker, malformed XML, autoplay rejection), IMA fires
4054
+ // AD_ERROR and never ALL_ADS_COMPLETED. Route it through the shared
4055
+ // completion funnel so the orchestration always advances.
4056
+ adsLoader.addEventListener(ima.AdErrorEvent.Type.AD_ERROR, (errorEvent) => {
4057
+ console.warn('AdsManager: AD_ERROR (loader) — falling through to main', errorEvent);
4058
+ completeAdLeg();
3971
4059
  });
3972
- adsManager.addEventListener(ima.AdEvent.Type.ALL_ADS_COMPLETED, () => {
3973
- state.adPlaying = false;
3974
- bus.emit('ad-complete');
3975
- });
3976
- // Same AD_ERROR fall-through on the manager — covers errors that
3977
- // surface only after the ad starts playing (decode error, mid-roll
3978
- // load failure, etc.).
3979
- adsManager.addEventListener(ima.AdErrorEvent.Type.AD_ERROR, (errorEvent) => {
3980
- console.warn('AdsManager: AD_ERROR (manager) — falling through to main', errorEvent);
3981
- state.adPlaying = false;
3982
- bus.emit('ad-complete');
4060
+ adsLoader.addEventListener(ima.AdsManagerLoadedEvent.Type.ADS_MANAGER_LOADED, (loadedEvent) => {
4061
+ // T-07-22: a late ads response — the request is issued before the
4062
+ // bound can expire, so this is reachable — must not start an ad
4063
+ // over an already-opened main stream, and must not emit a second
4064
+ // 'ad-complete'.
4065
+ if (adLegCompleted)
4066
+ return;
4067
+ const event = loadedEvent;
4068
+ const adsManager = event.getAdsManager(video);
4069
+ this.imaAdsManager = adsManager;
4070
+ // G-07-11: holding the manager is the earliest proof the ad leg is
4071
+ // alive and progressing. Creative fetch, buffering and render are
4072
+ // IMA's to own from here — disarm the watchdog now rather than at
4073
+ // a later, more visible signal, so none of that time is charged
4074
+ // to the bound.
4075
+ clearAdLegTimer();
4076
+ // getBoundingClientRect is always defined on HTMLElement; the prior
4077
+ // truthy check on the method reference was dead code. The real edge
4078
+ // case is a zero-sized element (display:none, before layout) which
4079
+ // would make IMA's ads container collapse — fall back to a 16:9
4080
+ // baseline in that case.
4081
+ const rect = video.getBoundingClientRect();
4082
+ const width = rect.width > 0 ? rect.width : 640;
4083
+ const height = rect.height > 0 ? rect.height : 360;
4084
+ // D-08-08: CONTENT_PAUSE_REQUESTED, CONTENT_RESUME_REQUESTED and
4085
+ // ALL_ADS_COMPLETED emitted nothing at all before this change, so
4086
+ // none of the three could be individually evidenced in the 08-04
4087
+ // live session (G-08-02) — only their side effects were visible.
4088
+ // These three log lines are observability-only and inherit
4089
+ // D-08-06's constraints below verbatim (never complete the leg,
4090
+ // never write state, never interpolate metadata). Each literal is
4091
+ // captured verbatim into a committed evidence file by 08-07.
4092
+ // Wire event handlers for ad lifecycle
4093
+ adsManager.addEventListener(ima.AdEvent.Type.CONTENT_PAUSE_REQUESTED, () => {
4094
+ console.log('AdsManager: CONTENT_PAUSE_REQUESTED — content paused (D-08-08)');
4095
+ state.adPlaying = true;
4096
+ // Pause the content stream while the ad plays. IMA's integration
4097
+ // contract requires the publisher to pause its own content video on
4098
+ // CONTENT_PAUSE_REQUESTED; the symmetric CONTENT_RESUME_REQUESTED
4099
+ // handler below resumes it with video.play(). Without this the main
4100
+ // HLS/DASH stream keeps playing (audio + video) underneath the ad.
4101
+ video.pause();
4102
+ });
4103
+ adsManager.addEventListener(ima.AdEvent.Type.CONTENT_RESUME_REQUESTED, () => {
4104
+ console.log('AdsManager: CONTENT_RESUME_REQUESTED — content resumed (D-08-08)');
4105
+ state.adPlaying = false;
4106
+ void video.play();
4107
+ // D-08-07: CONTENT_RESUME_REQUESTED is IMA's canonical "ad break
4108
+ // over, resume content" signal and is the ONLY completion event
4109
+ // dispatched for a plain VAST response — ALL_ADS_COMPLETED is
4110
+ // reliable only for VMAP/ad-rules responses. Routing it through
4111
+ // completeAdLeg() is what makes VAST-04's "exactly once per
4112
+ // instance" hold on both response shapes; completeAdLeg() is
4113
+ // idempotent via adLegCompleted, so a later ALL_ADS_COMPLETED is
4114
+ // a no-op rather than a second 'ad-complete'.
4115
+ //
4116
+ // Verified live (08-04): without this, a completed Google sample
4117
+ // preroll never emitted 'ad-complete', and main opened only when
4118
+ // the 30s MAIN_OPEN_FALLBACK_MS backstop expired — a 20-29s gap
4119
+ // on every ad-only playback.
4120
+ completeAdLeg();
4121
+ });
4122
+ adsManager.addEventListener(ima.AdEvent.Type.ALL_ADS_COMPLETED, () => {
4123
+ console.log('AdsManager: ALL_ADS_COMPLETED — ad leg complete (D-08-08)');
4124
+ completeAdLeg();
4125
+ });
4126
+ // Same AD_ERROR fall-through on the manager — covers errors that
4127
+ // surface only after the ad starts playing (decode error, mid-roll
4128
+ // load failure, etc.).
4129
+ adsManager.addEventListener(ima.AdErrorEvent.Type.AD_ERROR, (errorEvent) => {
4130
+ console.warn('AdsManager: AD_ERROR (manager) — falling through to main', errorEvent);
4131
+ completeAdLeg();
4132
+ });
4133
+ // D-08-06: IMA renders and owns its own Skip button (D-05 rules out
4134
+ // a custom skip UI), so this log is the only signal that proves the
4135
+ // control became available — observability only. It must never
4136
+ // call completeAdLeg() (this fires mid-ad, while a creative is
4137
+ // still rendering) and must never write to state (nothing consumes
4138
+ // an adSkippable field today, and speculative state violates
4139
+ // Simplicity First). No ad/creative metadata is interpolated: this
4140
+ // literal is captured verbatim into a committed evidence file by
4141
+ // 08-04 (T-08-05).
4142
+ adsManager.addEventListener(ima.AdEvent.Type.SKIPPABLE_STATE_CHANGED, () => {
4143
+ console.log('AdsManager: ad became skippable (D-08-06)');
4144
+ });
4145
+ adsManager.init(width, height);
4146
+ adsManager.start();
3983
4147
  });
3984
- adsManager.init(width, height);
3985
- adsManager.start();
3986
- });
3987
- // Load the ad tag
3988
- const adsRequest = new ima.AdsRequest();
3989
- adsRequest.adTagUrl = config.ad;
3990
- adsLoader.requestAds(adsRequest);
3991
- // Register abort cleanup
3992
- signal.addEventListener('abort', () => {
3993
- if (this.imaAdsManager !== null) {
3994
- this.imaAdsManager.destroy();
3995
- this.imaAdsManager = null;
3996
- }
3997
- if (this.imaDisplayContainer !== null) {
3998
- this.imaDisplayContainer.destroy();
3999
- this.imaDisplayContainer = null;
4000
- }
4001
- }, { once: true });
4148
+ // Load the ad tag
4149
+ const adsRequest = new ima.AdsRequest();
4150
+ adsRequest.adTagUrl = config.ad;
4151
+ adsLoader.requestAds(adsRequest);
4152
+ }
4153
+ catch (error) {
4154
+ // G-07-11: an ad-blocker blocking the CDN, a network error, or
4155
+ // loadScript's own 10s timeout must fall through to main instead of
4156
+ // escaping to the logging-only .catch() in lifecycle.ts.
4157
+ console.warn('AdsManager: failed to load or initialize the IMA SDK — falling through to main (G-07-11)', error);
4158
+ completeAdLeg();
4159
+ }
4002
4160
  }
4003
4161
  /**
4004
- * Wait for a click event on the video element.
4162
+ * Shared teardown for imaAdsManager and imaDisplayContainer, each behind
4163
+ * its own null check and nulled immediately after — idempotent by
4164
+ * construction. Called unconditionally on abort, and from the completion
4165
+ * funnel only when this.imaAdsManager is still null (T-07-23).
4166
+ */
4167
+ releaseImaResources() {
4168
+ if (this.imaAdsManager !== null) {
4169
+ this.imaAdsManager.destroy();
4170
+ this.imaAdsManager = null;
4171
+ }
4172
+ if (this.imaDisplayContainer !== null) {
4173
+ this.imaDisplayContainer.destroy();
4174
+ this.imaDisplayContainer = null;
4175
+ }
4176
+ }
4177
+ /**
4178
+ * Wait for a click event anywhere inside the player root.
4005
4179
  * Used to satisfy browser autoplay policies when autoplay is disabled.
4006
4180
  *
4181
+ * G-07-11: the target widens from the video element to
4182
+ * video.closest('.eb-player') (falling back to video itself for bare
4183
+ * integrations), registered in the capture phase so a descendant that
4184
+ * halts bubbling cannot hide the click from it. Observes only; never
4185
+ * mutates the event, so the skip button and click-through link keep
4186
+ * working exactly as they do today.
4187
+ *
4007
4188
  * WR-05: single-shot settle flag guards against the click-vs-abort race —
4008
4189
  * Promise resolution is already idempotent but settled tightens the
4009
4190
  * contract by ensuring listener cleanup happens exactly once for either
@@ -4011,16 +4192,17 @@
4011
4192
  */
4012
4193
  waitForUserGesture(video, signal) {
4013
4194
  return new Promise((resolve) => {
4195
+ const target = video.closest('.eb-player') ?? video;
4014
4196
  let settled = false;
4015
4197
  const settle = () => {
4016
4198
  if (settled)
4017
4199
  return;
4018
4200
  settled = true;
4019
- video.removeEventListener('click', onClick);
4201
+ target.removeEventListener('click', onClick, { capture: true });
4020
4202
  resolve();
4021
4203
  };
4022
4204
  const onClick = () => settle();
4023
- video.addEventListener('click', onClick);
4205
+ target.addEventListener('click', onClick, { capture: true });
4024
4206
  // Clean up if aborted before click. The abort listener auto-removes
4025
4207
  // via `{ once: true }`; the click listener is removed inside settle().
4026
4208
  signal.addEventListener('abort', settle, { once: true });
@@ -4028,6 +4210,21 @@
4028
4210
  }
4029
4211
  }
4030
4212
 
4213
+ /**
4214
+ * G-07-12: absolute backstop for an intro pass where every other completion
4215
+ * trigger is lost — no video `ended`, no `preroll-skip`, no `error-fatal`,
4216
+ * and no `state.error` write. 90s cannot truncate a real brand intro
4217
+ * (single-digit to ~30s assets) but converts an infinite hang into a bounded
4218
+ * one.
4219
+ *
4220
+ * D-10: this value MUST stay strictly below AdsManager's
4221
+ * INTRO_DEFERRAL_TIMEOUT_MS (120_000, src/integrations/ads-manager.ts) so
4222
+ * the intro pass always completes before the ad leg falls through — intro →
4223
+ * ad → main precedence must hold in the failure case too, not only the
4224
+ * happy path. Test 14 in test/integrations/intro-stream-manager.test.ts
4225
+ * asserts this ordering so a future edit to either constant fails loudly.
4226
+ */
4227
+ const INTRO_MAX_DURATION_MS = 90000;
4031
4228
  /**
4032
4229
  * IntroStreamManager owns the intro-stream lifecycle.
4033
4230
  *
@@ -4036,12 +4233,24 @@
4036
4233
  * - the video element's native `ended` event
4037
4234
  * - bus `preroll-skip` (from the PrerollOverlay skip button)
4038
4235
  * - bus `error-fatal` (D-04 silent fall-through — intro must never block main)
4236
+ * - PlayerState `error` becoming non-null (G-07-12: the one signal shared
4237
+ * by two silent failure modes that never reach the bus —
4238
+ * StallWatchdog reload-budget exhaustion,
4239
+ * src/core/command-handler.ts:159-164, and a synchronous engine
4240
+ * init() throw caught in onAttach, src/engines/dash.ts:108-120 and
4241
+ * src/engines/hls.ts:194-207)
4242
+ * - the on-demand bus event that lets eb-player.ts's openMainStream()
4243
+ * guarantee the intro pass is finished before main opens (G-07-13
4244
+ * receiving half — see the bus.on() registration below for its name)
4245
+ * - an absolute INTRO_MAX_DURATION_MS backstop timer, armed in case no
4246
+ * other trigger ever fires
4039
4247
  * - Any one of those triggers calls a closure `complete()` exactly once
4040
4248
  * (idempotent via a `completed` boolean guard), which flips
4041
4249
  * state.introStreamPlaying = false and emits `intro-stream-complete`.
4042
4250
  * - On abort, the video `ended` listener is removed explicitly via
4043
- * removeEventListener. Bus listeners auto-clean via the `{ signal }` option
4044
- * (see src/core/event-bus.ts on()).
4251
+ * removeEventListener and the backstop timer is cleared. Bus and state
4252
+ * listeners auto-clean via the `{ signal }` option (see
4253
+ * src/core/event-bus.ts on() and src/core/player-state.ts on()).
4045
4254
  *
4046
4255
  * Scope: this manager listens and emits — it does NOT itself open the intro
4047
4256
  * stream. The engine open() call is owned by eb-player.ts (single source of
@@ -4052,8 +4261,10 @@
4052
4261
  /**
4053
4262
  * Initialize intro-stream lifecycle tracking.
4054
4263
  * - Skips entirely if config.preroll is falsy (no state flip, no listeners).
4055
- * - Otherwise flips state.introStreamPlaying = true and registers the three
4056
- * completion triggers (video `ended`, bus `preroll-skip`, bus `error-fatal`).
4264
+ * - Otherwise flips state.introStreamPlaying = true and registers the
4265
+ * completion triggers (video `ended`, bus `preroll-skip`, bus
4266
+ * `error-fatal`, PlayerState `error`, the on-demand force-completion bus
4267
+ * event, and the INTRO_MAX_DURATION_MS backstop timer).
4057
4268
  */
4058
4269
  async init(config, state, bus, video, signal) {
4059
4270
  if (!config.preroll)
@@ -4068,14 +4279,18 @@
4068
4279
  // Mirror outer aborts onto the local controller so dispose() still
4069
4280
  // cleans everything up.
4070
4281
  signal.addEventListener('abort', () => local.abort(), { once: true });
4282
+ // Initialized to 0; clearing a zero timer handle is a no-op, so no null
4283
+ // guard is needed around either clear call below.
4284
+ let backstopTimer = 0;
4071
4285
  let completed = false;
4072
4286
  const complete = () => {
4073
4287
  if (completed)
4074
4288
  return;
4075
4289
  completed = true;
4290
+ clearTimeout(backstopTimer);
4076
4291
  state.introStreamPlaying = false;
4077
4292
  bus.emit('intro-stream-complete');
4078
- local.abort(); // tear down bus listeners after the intro completes
4293
+ local.abort(); // tear down bus/state listeners after the intro completes
4079
4294
  };
4080
4295
  const onEnded = () => complete();
4081
4296
  video.addEventListener('ended', onEnded);
@@ -4086,8 +4301,40 @@
4086
4301
  console.warn('IntroStreamManager: intro stream errored — falling through to main');
4087
4302
  complete();
4088
4303
  }, { signal: local.signal });
4304
+ // G-07-12: state.error is the one signal shared by two silent failure
4305
+ // modes that never reach the bus — StallWatchdog reload-budget
4306
+ // exhaustion (src/core/command-handler.ts:159-164) and a synchronous
4307
+ // engine init() throw caught in onAttach (src/engines/dash.ts:108-120,
4308
+ // src/engines/hls.ts:194-207). Both writers are shared with the MAIN
4309
+ // pass, so the fix lives here (the intro-owning component) rather than
4310
+ // in either shared file. The Proxy `set` trap already no-ops on an
4311
+ // unchanged value, but a `null` write (e.g. openMainStream() clearing a
4312
+ // stale error) must still never be read as "the intro finished".
4313
+ state.on('error', (newValue) => {
4314
+ if (newValue === null)
4315
+ return;
4316
+ console.warn('IntroStreamManager: intro stream errored via state.error — falling through to main');
4317
+ complete();
4318
+ }, { signal: local.signal });
4319
+ // G-07-13 receiving half: openMainStream() (src/eb-player.ts, plan
4320
+ // 07-10) emits this to guarantee the intro pass is finished before main
4321
+ // opens. No `completed` re-check is needed or wanted here: complete()
4322
+ // aborts `local`, which removes this listener, so the handler below is
4323
+ // only ever reachable while the intro is still live — adding a guard
4324
+ // would create an unreachable branch.
4325
+ bus.on('force-intro-complete', () => {
4326
+ console.warn('IntroStreamManager: main is opening — force-completing the intro pass');
4327
+ complete();
4328
+ }, { signal: local.signal });
4329
+ // G-07-12: absolute bound — even if no other trigger ever fires, the
4330
+ // intro pass still completes within INTRO_MAX_DURATION_MS.
4331
+ backstopTimer = setTimeout(() => {
4332
+ console.warn('IntroStreamManager: intro stream exceeded INTRO_MAX_DURATION_MS — falling through to main');
4333
+ complete();
4334
+ }, INTRO_MAX_DURATION_MS);
4089
4335
  signal.addEventListener('abort', () => {
4090
4336
  video.removeEventListener('ended', onEnded);
4337
+ clearTimeout(backstopTimer);
4091
4338
  }, { once: true });
4092
4339
  }
4093
4340
  }
@@ -5978,7 +6225,14 @@
5978
6225
  * transient network failures (segment fetch, manifest re-fetch, etc.)
5979
6226
  * never permanently stop playback.
5980
6227
  */
5981
- function applyDashRetrySettings(driver) {
6228
+ /**
6229
+ * Bounded retry-attempt count used for the intro pass so an unreachable
6230
+ * intro manifest surrenders quickly instead of retrying forever. With
6231
+ * retryIntervals at 1000ms this surfaces the dash.js ERROR event roughly
6232
+ * two seconds after an unreachable manifest.
6233
+ */
6234
+ const INTRO_PASS_RETRY_ATTEMPTS = 2;
6235
+ function applyDashRetrySettings(driver, options = {}) {
5982
6236
  driver.updateSettings({
5983
6237
  streaming: {
5984
6238
  retryIntervals: {
@@ -5993,16 +6247,17 @@
5993
6247
  }
5994
6248
  }
5995
6249
  });
6250
+ const retryAttempts = options.introPass === true ? INTRO_PASS_RETRY_ATTEMPTS : Infinity;
5996
6251
  driver.updateSettings({
5997
6252
  streaming: {
5998
6253
  retryAttempts: {
5999
- MPD: Infinity,
6000
- XLinkExpansion: Infinity,
6001
- InitializationSegment: Infinity,
6002
- IndexSegment: Infinity,
6003
- MediaSegment: Infinity,
6004
- BitstreamSwitchingSegment: Infinity,
6005
- other: Infinity,
6254
+ MPD: retryAttempts,
6255
+ XLinkExpansion: retryAttempts,
6256
+ InitializationSegment: retryAttempts,
6257
+ IndexSegment: retryAttempts,
6258
+ MediaSegment: retryAttempts,
6259
+ BitstreamSwitchingSegment: retryAttempts,
6260
+ other: retryAttempts,
6006
6261
  lowLatencyMultiplyFactor: 1
6007
6262
  }
6008
6263
  }
@@ -6184,6 +6439,11 @@
6184
6439
  // Private — NEVER in PlayerState
6185
6440
  this.driver = null;
6186
6441
  this.dvrErrorHandler = null;
6442
+ // True when this engine instance is loading the intro (preroll) stream
6443
+ // rather than the main stream — see D-04 / G-07-10 in init().
6444
+ this.isIntroPass = false;
6445
+ // Guards the one-shot error-fatal emit on the intro pass (T-07-21).
6446
+ this.introFallThroughReported = false;
6187
6447
  }
6188
6448
  getDriver() {
6189
6449
  return this.driver;
@@ -6290,6 +6550,10 @@
6290
6550
  const { config, video, state, signal } = this;
6291
6551
  if (!config || !video || !state || !signal)
6292
6552
  return;
6553
+ // D-04 / G-07-10: identify the intro pass so a permanently-unreachable
6554
+ // intro manifest can fall through to main instead of looping forever.
6555
+ // Same gate shape as src/engines/hls.ts:445.
6556
+ this.isIntroPass = Boolean(config.preroll) && this.loadSourceUrl === config.preroll;
6293
6557
  const dashjsUrl = config.dashjs;
6294
6558
  if (!dashjsUrl) {
6295
6559
  throw new Error('DashEngine: config.dashjs URL is required');
@@ -6319,7 +6583,7 @@
6319
6583
  player.initialize(video, this.loadSourceUrl || config.src || '', config.autoplay ?? false);
6320
6584
  // Apply retry settings if requested
6321
6585
  if (config.retry) {
6322
- applyDashRetrySettings(player);
6586
+ applyDashRetrySettings(player, { introPass: this.isIntroPass });
6323
6587
  }
6324
6588
  // Apply custom ABR rule unless disabled
6325
6589
  if (!config.disableCustomAbr) {
@@ -6420,12 +6684,34 @@
6420
6684
  player.on(events['TRACK_CHANGE_RENDERED'], () => {
6421
6685
  state.currentAudioTrack = player.getQualityFor('audio');
6422
6686
  });
6423
- // ERROR: log dash.js errors
6687
+ // ERROR: log dash.js errors, and on the intro pass fall through to main
6424
6688
  player.on(events['ERROR'], (eventData) => {
6425
- const typedData = eventData;
6426
- console.warn('DashEngine error:', typedData?.error?.message ?? typedData?.error ?? typedData);
6689
+ this.handleDriverError(eventData);
6427
6690
  });
6428
6691
  }
6692
+ /**
6693
+ * Handles a dash.js ERROR event.
6694
+ *
6695
+ * D-04 / G-07-10: always logs, exactly as before (test/engines/dash.test.ts:619
6696
+ * and :1156 assert this unchanged). On the intro pass only — gated by
6697
+ * this.isIntroPass, computed once in init() from
6698
+ * `Boolean(config.preroll) && this.loadSourceUrl === config.preroll` — a fatal
6699
+ * manifest/segment error stops the stall watchdog and emits `error-fatal`
6700
+ * exactly once (guarded by introFallThroughReported) so
6701
+ * IntroStreamManager.complete() can fire and openMainStream() run instead of
6702
+ * the watchdog reopening the (still intro) lastSrc in a loop. On the main
6703
+ * pass this method only warns — main-stream resilience must stay untouched.
6704
+ */
6705
+ handleDriverError(eventData) {
6706
+ const typedData = eventData;
6707
+ console.warn('DashEngine error:', typedData?.error?.message ?? typedData?.error ?? typedData);
6708
+ if (!this.isIntroPass || this.introFallThroughReported)
6709
+ return;
6710
+ this.introFallThroughReported = true;
6711
+ this.stopWatchdog();
6712
+ const errorKey = typedData?.error?.code ?? typedData?.error?.message ?? 'unknown';
6713
+ this.bus?.emit('error-fatal', { code: 'NETWORK', message: `Intro stream error: ${errorKey}` });
6714
+ }
6429
6715
  }
6430
6716
 
6431
6717
  /**
@@ -6893,6 +7179,18 @@
6893
7179
  // observably awaitable.
6894
7180
  return new Promise((resolve) => setTimeout(resolve, EME_TEARDOWN_DELAY_MS));
6895
7181
  }
7182
+ /**
7183
+ * G-07-11: bounded backstop so main always opens even when the ad leg never
7184
+ * produces 'ad-complete'. Longer than AdsManager's own AD_LEG_TIMEOUT_MS
7185
+ * (20s) so that more precise, better-logged inner mechanism wins first in a
7186
+ * live player — this is the outer safety net, not the primary signal. Covers
7187
+ * two cases the inner mechanism cannot: an AdsManager that was never
7188
+ * constructed at all (noUi:true, or a null ads container — lifecycle.ts
7189
+ * skips the branch that would create one), and an AdsManager that obtained
7190
+ * its manager from the loader (disarming its own bound) whose creative then
7191
+ * never reached the screen.
7192
+ */
7193
+ const MAIN_OPEN_FALLBACK_MS = 30000;
6896
7194
  // ---------------------------------------------------------------------------
6897
7195
  // Public API
6898
7196
  // ---------------------------------------------------------------------------
@@ -7103,13 +7401,17 @@
7103
7401
  controller.bus.on('error-fatal', () => {
7104
7402
  inst.p2p?.stop();
7105
7403
  }, { signal: controller.signal });
7106
- // Phase 7 — intro→main orchestration.
7404
+ // Phase 7/8 — pre-main leg orchestration.
7107
7405
  // When mergedConfig.preroll is set, open the intro stream first, then on
7108
7406
  // bus 'intro-stream-complete' (emitted by IntroStreamManager on video.ended,
7109
7407
  // preroll-skip, or error-fatal) close → 500ms EME teardown delay → open main.
7110
7408
  // If config.ad is also set, wait additionally for bus 'ad-complete' before
7111
7409
  // opening main (D-10: intro → ad → main precedence).
7112
7410
  //
7411
+ // D-02 (Phase 8): when mergedConfig.preroll is NOT set but mergedConfig.ad
7412
+ // IS, main is gated on 'ad-complete' alone (no intro leg to precede it) —
7413
+ // see the `hasAd` branch below and tryOpenMainAdOnly().
7414
+ //
7113
7415
  // Runtime mute override: the live <video>.muted and PlayerState.muted are
7114
7416
  // forced to false for the intro pass (D-13 — intros always audible) and
7115
7417
  // restored to `originalMuted` before opening main. We do NOT mutate
@@ -7119,7 +7421,22 @@
7119
7421
  // P2P opt-in: `mergedConfig.manager` IS re-read on each reference.open() in
7120
7422
  // the P2P branch (eb-player.ts:192), so we config-level swap it to false for
7121
7423
  // the intro pass and restore it for main (D-08 — P2P attaches only to main).
7424
+ // G-07-11: two independent paths can now reach openMainStream — the
7425
+ // ad-complete handler and the MAIN_OPEN_FALLBACK_MS backstop — so it must
7426
+ // be idempotent.
7427
+ let mainOpened = false;
7122
7428
  const openMainStream = () => {
7429
+ if (mainOpened)
7430
+ return;
7431
+ mainOpened = true;
7432
+ // G-07-13 / D-10: force-complete the intro pass on EVERY path into main —
7433
+ // including ad-complete firing with no prior intro-stream-complete (a hung
7434
+ // intro deferred through AdsManager's INTRO_DEFERRAL_TIMEOUT_MS). This is a
7435
+ // FORCE-complete, never a wait: IntroStreamManager.complete() runs
7436
+ // synchronously, so D-04 liveness is untouched — main still opens in the
7437
+ // same tick. complete() is idempotent, so on the normal ordering (intro
7438
+ // already finished) this is a no-op; the listener is already torn down.
7439
+ controller.bus.emit('force-intro-complete');
7123
7440
  if (video !== null) {
7124
7441
  video.muted = originalMuted;
7125
7442
  }
@@ -7145,8 +7462,100 @@
7145
7462
  return;
7146
7463
  reference.open(mergedConfig.src);
7147
7464
  };
7148
- if (mergedConfig.preroll) {
7149
- const hasAd = Boolean(mergedConfig.ad);
7465
+ // D-02 (Phase 8): promoted out of the `preroll` branch so both the
7466
+ // intro+ad and ad-only configurations share one gated main-open path — see
7467
+ // the assumption_delta_decision in 08-01-PLAN.md ("pluralization": preroll
7468
+ // and ad are two variants of a single "pre-main leg" concept).
7469
+ const hasIntro = Boolean(mergedConfig.preroll);
7470
+ const hasAd = Boolean(mergedConfig.ad);
7471
+ // G-07-11: bounds the ad leg from this player's side, independent of
7472
+ // AdsManager's own internal watchdog — see MAIN_OPEN_FALLBACK_MS.
7473
+ let mainOpenFallbackTimer = null;
7474
+ const clearMainOpenFallback = () => {
7475
+ if (mainOpenFallbackTimer !== null) {
7476
+ clearTimeout(mainOpenFallbackTimer);
7477
+ mainOpenFallbackTimer = null;
7478
+ }
7479
+ };
7480
+ const armMainOpenFallback = (onExpire) => {
7481
+ mainOpenFallbackTimer = setTimeout(() => {
7482
+ if (controller.signal.aborted)
7483
+ return;
7484
+ // CR-01: an ad still playing when the bound elapses is legitimate — a
7485
+ // 60s creative outlives the 30s window. But simply returning here would
7486
+ // retire the ONLY remaining bound on the ad leg: AdsManager disarms its
7487
+ // own AD_LEG_TIMEOUT_MS watchdog as soon as IMA hands back a manager.
7488
+ // An ad that then stalls (no ad-complete, no AD_ERROR) would strand main
7489
+ // playback for the life of the player. Re-arm instead, so the leg stays
7490
+ // bounded on every path for as long as the ad genuinely progresses.
7491
+ if (controller.state.adPlaying) {
7492
+ armMainOpenFallback(onExpire);
7493
+ return;
7494
+ }
7495
+ console.warn('eb-player: ad leg produced no ad-complete within the bound — falling through to main (G-07-11)');
7496
+ onExpire();
7497
+ }, MAIN_OPEN_FALLBACK_MS);
7498
+ };
7499
+ // WR-03: clear the fallback timer on dispose — otherwise a torn-down
7500
+ // player leaves a pending setTimeout holding a closure reference alive.
7501
+ controller.signal.addEventListener('abort', () => {
7502
+ clearMainOpenFallback();
7503
+ }, { once: true });
7504
+ // D-08-02 (Phase 8): the two-gate compound funnel for the ad-only
7505
+ // configuration. With no intro leg, main can open only once BOTH the ad
7506
+ // leg has settled (adGateOpen) AND the user has requested playback
7507
+ // (playGateOpen) — see the `else if (hasAd)` branch below. With no ad
7508
+ // configured the ad gate starts open. With `autoplay: false`, the ad-leg
7509
+ // bound is now armed unconditionally (D-08-03) and sits BEFORE
7510
+ // AdsManager's `await gesturePromise`, so `ad-complete` can fire ~20s
7511
+ // after page load with zero user interaction. Without this play gate that
7512
+ // alone would fetch the CDN token and load the main manifest, breaking the
7513
+ // documented no-prefetch contract (src/eb-player.ts:598-601 below). The
7514
+ // gate is deliberately kept, not dropped (A-OQ1, 08-01-PLAN.md).
7515
+ let adGateOpen = !hasAd;
7516
+ let playGateOpen = Boolean(mergedConfig.autoplay);
7517
+ let deferredPlaybackStarted = false;
7518
+ const startDeferredPlayback = () => {
7519
+ if (deferredPlaybackStarted)
7520
+ return;
7521
+ deferredPlaybackStarted = true;
7522
+ const engine = inst.engine;
7523
+ if (engine === null || video === null)
7524
+ return;
7525
+ // Honor the user's click intent. CommandHandler's earlier bus.on('play')
7526
+ // listener fired before us with no source attached, so its video.play()
7527
+ // was a silent no-op. Wait for driverReady (attachMedia + loadSource
7528
+ // done) then start playback ourselves.
7529
+ engine.driverReady.then(() => {
7530
+ const playResult = video.play();
7531
+ if (playResult && typeof playResult.catch === 'function') {
7532
+ playResult.catch((error) => {
7533
+ console.warn('EBPlayer: deferred play() rejected', error);
7534
+ });
7535
+ }
7536
+ });
7537
+ };
7538
+ const tryOpenMainAdOnly = () => {
7539
+ if (!adGateOpen || !playGateOpen)
7540
+ return;
7541
+ openMainStream();
7542
+ if (!mergedConfig.autoplay) {
7543
+ startDeferredPlayback();
7544
+ }
7545
+ };
7546
+ if (hasAd) {
7547
+ controller.bus.on('ad-complete', () => {
7548
+ clearMainOpenFallback();
7549
+ adGateOpen = true;
7550
+ if (hasIntro) {
7551
+ openMainStream();
7552
+ }
7553
+ else {
7554
+ tryOpenMainAdOnly();
7555
+ }
7556
+ }, { signal: controller.signal });
7557
+ }
7558
+ if (hasIntro) {
7150
7559
  controller.bus.on('intro-stream-complete', () => {
7151
7560
  reference.close();
7152
7561
  // WR-08: restore PlayerState.muted synchronously — DO NOT wait for the
@@ -7160,18 +7569,25 @@
7160
7569
  controller.state.muted = originalMuted;
7161
7570
  // EME teardown delay — see waitForEmeTeardown / EME_TEARDOWN_DELAY_MS.
7162
7571
  waitForEmeTeardown().then(() => {
7572
+ // G-07-13: main may already be open — the ad-complete path force-
7573
+ // completes the intro (openMainStream() emits force-intro-complete),
7574
+ // which re-enters this handler synchronously before this async
7575
+ // continuation ever runs. Arming the fallback timer here would be an
7576
+ // orphan: main is already open and nothing will ever clear it.
7577
+ if (mainOpened)
7578
+ return;
7163
7579
  if (!hasAd) {
7164
7580
  openMainStream();
7581
+ return;
7165
7582
  }
7166
- // If hasAd, the ad-complete handler below will call openMainStream
7167
- // after the AdsManager finishes (D-10 precedence).
7583
+ // hasAd: the ad-complete handler above will normally call
7584
+ // openMainStream after the AdsManager finishes (D-10 precedence).
7585
+ // G-07-11: arm the bounded fallback in case it never does — an
7586
+ // AdsManager that was never constructed, or one whose creative
7587
+ // never rendered after disarming its own bound.
7588
+ armMainOpenFallback(openMainStream);
7168
7589
  });
7169
7590
  }, { signal: controller.signal });
7170
- if (hasAd) {
7171
- controller.bus.on('ad-complete', () => {
7172
- openMainStream();
7173
- }, { signal: controller.signal });
7174
- }
7175
7591
  // Force runtime mute off for the intro pass — applied to the LIVE <video>
7176
7592
  // element and PlayerState, NOT to mergedConfig.muted (no-op for runtime).
7177
7593
  if (video !== null) {
@@ -7184,6 +7600,52 @@
7184
7600
  controller.config.manager = false;
7185
7601
  reference.open(mergedConfig.preroll);
7186
7602
  }
7603
+ // D-02/D-08-02 (Phase 8): ad-only configuration — no intro stream, so this
7604
+ // must NOT call reference.open() directly and must NOT copy any
7605
+ // intro-specific step (no intro source open, no force-unmute, no P2P
7606
+ // manager swap, no intro-stream-complete listener). Main opens only via
7607
+ // tryOpenMainAdOnly() once both gates clear.
7608
+ else if (hasAd) {
7609
+ if (mergedConfig.autoplay) {
7610
+ // playGateOpen is already true (set above) — main opens on whichever
7611
+ // of 'ad-complete' or the fallback arrives first. tryOpenMainAdOnly()
7612
+ // alone is NOT a real backstop here: it returns early unless
7613
+ // adGateOpen is already true, and the only place that flips
7614
+ // adGateOpen is the ad-complete handler above, which clears this very
7615
+ // timer before calling it — so an unmodified tryOpenMainAdOnly passed
7616
+ // directly can never observe adGateOpen === false the one time it
7617
+ // would need to. Force the ad gate open in the callback itself so the
7618
+ // bound can still open main when ad-complete never arrives at all
7619
+ // (e.g. AdsManager is never constructed) — see G-07-11.
7620
+ armMainOpenFallback(() => {
7621
+ adGateOpen = true;
7622
+ tryOpenMainAdOnly();
7623
+ });
7624
+ }
7625
+ else {
7626
+ // D-08-02: the 'play' gate — see the rationale comment above
7627
+ // tryOpenMainAdOnly(). A repeat click must be inert.
7628
+ controller.bus.on('play', () => {
7629
+ if (playGateOpen)
7630
+ return;
7631
+ playGateOpen = true;
7632
+ if (adGateOpen) {
7633
+ // The ad leg already settled — main can open now.
7634
+ tryOpenMainAdOnly();
7635
+ startDeferredPlayback();
7636
+ }
7637
+ else {
7638
+ // The ad leg is still in flight — start the outer 30s backstop
7639
+ // from the play request, not from page load, using the same
7640
+ // force-the-gate shape as the autoplay:true arm above.
7641
+ armMainOpenFallback(() => {
7642
+ adGateOpen = true;
7643
+ tryOpenMainAdOnly();
7644
+ });
7645
+ }
7646
+ }, { signal: controller.signal });
7647
+ }
7648
+ }
7187
7649
  // Auto-open the stream if src is provided in config (matches legacy player behaviour
7188
7650
  // where consumers call start({ src: '...' }) and expect playback to begin immediately).
7189
7651
  // When autoplay is false, defer open() until the user requests play — this avoids
@@ -7199,21 +7661,7 @@
7199
7661
  return;
7200
7662
  deferredOpen = false;
7201
7663
  reference.open(mergedConfig.src);
7202
- // Honor the user's click intent. CommandHandler's earlier bus.on('play')
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
- }
7664
+ startDeferredPlayback();
7217
7665
  }, { signal: controller.signal });
7218
7666
  }
7219
7667
  }