eb-player 2.0.19 → 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 (156) hide show
  1. package/dist/build/eb-player.css +9 -0
  2. package/dist/build/ebplayer.bundle.js +604 -132
  3. package/dist/build/ebplayer.bundle.js.map +1 -1
  4. package/dist/build/types/integrations/ads-manager.d.ts.map +1 -1
  5. package/dist/build/types/skin/skin-root.d.ts +8 -0
  6. package/dist/build/types/skin/skin-root.d.ts.map +1 -1
  7. package/dist/build/types/src/core/command-handler.d.ts +50 -0
  8. package/dist/build/types/src/core/command-handler.d.ts.map +1 -0
  9. package/dist/build/types/src/core/config.d.ts +187 -0
  10. package/dist/build/types/src/core/config.d.ts.map +1 -0
  11. package/dist/build/types/src/core/engine-state-sync.d.ts +38 -0
  12. package/dist/build/types/src/core/engine-state-sync.d.ts.map +1 -0
  13. package/dist/build/types/src/core/event-bus.d.ts +91 -0
  14. package/dist/build/types/src/core/event-bus.d.ts.map +1 -0
  15. package/dist/build/types/src/core/fsm.d.ts +30 -0
  16. package/dist/build/types/src/core/fsm.d.ts.map +1 -0
  17. package/dist/build/types/src/core/i18n.d.ts +51 -0
  18. package/dist/build/types/src/core/i18n.d.ts.map +1 -0
  19. package/dist/build/types/src/core/index.d.ts +23 -0
  20. package/dist/build/types/src/core/index.d.ts.map +1 -0
  21. package/dist/build/types/src/core/lifecycle.d.ts +96 -0
  22. package/dist/build/types/src/core/lifecycle.d.ts.map +1 -0
  23. package/dist/build/types/src/core/player-state.d.ts +15 -0
  24. package/dist/build/types/src/core/player-state.d.ts.map +1 -0
  25. package/dist/build/types/src/core/types.d.ts +85 -0
  26. package/dist/build/types/src/core/types.d.ts.map +1 -0
  27. package/dist/build/types/src/eb-player-standalone.d.ts +23 -0
  28. package/dist/build/types/src/eb-player-standalone.d.ts.map +1 -0
  29. package/dist/build/types/src/eb-player.d.ts +56 -0
  30. package/dist/build/types/src/eb-player.d.ts.map +1 -0
  31. package/dist/build/types/src/engines/abr/dash.d.ts +56 -0
  32. package/dist/build/types/src/engines/abr/dash.d.ts.map +1 -0
  33. package/dist/build/types/src/engines/abr/hls.d.ts +105 -0
  34. package/dist/build/types/src/engines/abr/hls.d.ts.map +1 -0
  35. package/dist/build/types/src/engines/base-engine.d.ts +72 -0
  36. package/dist/build/types/src/engines/base-engine.d.ts.map +1 -0
  37. package/dist/build/types/src/engines/cdn-loader.d.ts +20 -0
  38. package/dist/build/types/src/engines/cdn-loader.d.ts.map +1 -0
  39. package/dist/build/types/src/engines/cdn-token-manager.d.ts +137 -0
  40. package/dist/build/types/src/engines/cdn-token-manager.d.ts.map +1 -0
  41. package/dist/build/types/src/engines/dash.d.ts +95 -0
  42. package/dist/build/types/src/engines/dash.d.ts.map +1 -0
  43. package/dist/build/types/src/engines/drm.d.ts +54 -0
  44. package/dist/build/types/src/engines/drm.d.ts.map +1 -0
  45. package/dist/build/types/src/engines/hls-discontinuity-patch.d.ts +43 -0
  46. package/dist/build/types/src/engines/hls-discontinuity-patch.d.ts.map +1 -0
  47. package/dist/build/types/src/engines/hls.d.ts +53 -0
  48. package/dist/build/types/src/engines/hls.d.ts.map +1 -0
  49. package/dist/build/types/src/engines/index.d.ts +26 -0
  50. package/dist/build/types/src/engines/index.d.ts.map +1 -0
  51. package/dist/build/types/src/engines/ios/hls.d.ts +20 -0
  52. package/dist/build/types/src/engines/ios/hls.d.ts.map +1 -0
  53. package/dist/build/types/src/engines/poster/hls.d.ts +35 -0
  54. package/dist/build/types/src/engines/poster/hls.d.ts.map +1 -0
  55. package/dist/build/types/src/engines/retry/dash.d.ts +51 -0
  56. package/dist/build/types/src/engines/retry/dash.d.ts.map +1 -0
  57. package/dist/build/types/src/engines/retry/hls.d.ts +47 -0
  58. package/dist/build/types/src/engines/retry/hls.d.ts.map +1 -0
  59. package/dist/build/types/src/engines/snapshot/dash.d.ts +55 -0
  60. package/dist/build/types/src/engines/snapshot/dash.d.ts.map +1 -0
  61. package/dist/build/types/src/engines/snapshot/hls.d.ts +118 -0
  62. package/dist/build/types/src/engines/snapshot/hls.d.ts.map +1 -0
  63. package/dist/build/types/src/engines/stall-watchdog.d.ts +41 -0
  64. package/dist/build/types/src/engines/stall-watchdog.d.ts.map +1 -0
  65. package/dist/build/types/src/integrations/ads-manager.d.ts +79 -0
  66. package/dist/build/types/src/integrations/ads-manager.d.ts.map +1 -0
  67. package/dist/build/types/src/integrations/chromecast-manager.d.ts +50 -0
  68. package/dist/build/types/src/integrations/chromecast-manager.d.ts.map +1 -0
  69. package/dist/build/types/src/integrations/epg-manager.d.ts +22 -0
  70. package/dist/build/types/src/integrations/epg-manager.d.ts.map +1 -0
  71. package/dist/build/types/src/integrations/index.d.ts +7 -0
  72. package/dist/build/types/src/integrations/index.d.ts.map +1 -0
  73. package/dist/build/types/src/integrations/intro-stream-manager.d.ts +62 -0
  74. package/dist/build/types/src/integrations/intro-stream-manager.d.ts.map +1 -0
  75. package/dist/build/types/src/integrations/p2p-manager.d.ts +42 -0
  76. package/dist/build/types/src/integrations/p2p-manager.d.ts.map +1 -0
  77. package/dist/build/types/src/integrations/playlist-manager.d.ts +21 -0
  78. package/dist/build/types/src/integrations/playlist-manager.d.ts.map +1 -0
  79. package/dist/build/types/src/skin/bars/bottom-bar.d.ts +16 -0
  80. package/dist/build/types/src/skin/bars/bottom-bar.d.ts.map +1 -0
  81. package/dist/build/types/src/skin/bars/middle-bar.d.ts +16 -0
  82. package/dist/build/types/src/skin/bars/middle-bar.d.ts.map +1 -0
  83. package/dist/build/types/src/skin/bars/top-bar.d.ts +15 -0
  84. package/dist/build/types/src/skin/bars/top-bar.d.ts.map +1 -0
  85. package/dist/build/types/src/skin/base-component.d.ts +69 -0
  86. package/dist/build/types/src/skin/base-component.d.ts.map +1 -0
  87. package/dist/build/types/src/skin/brand/forja-playlist-bar.d.ts +15 -0
  88. package/dist/build/types/src/skin/brand/forja-playlist-bar.d.ts.map +1 -0
  89. package/dist/build/types/src/skin/brand/snrt-radio-carousel.d.ts +16 -0
  90. package/dist/build/types/src/skin/brand/snrt-radio-carousel.d.ts.map +1 -0
  91. package/dist/build/types/src/skin/component-registry.d.ts +16 -0
  92. package/dist/build/types/src/skin/component-registry.d.ts.map +1 -0
  93. package/dist/build/types/src/skin/controllers/auto-hide.d.ts +27 -0
  94. package/dist/build/types/src/skin/controllers/auto-hide.d.ts.map +1 -0
  95. package/dist/build/types/src/skin/controllers/keyboard.d.ts +29 -0
  96. package/dist/build/types/src/skin/controllers/keyboard.d.ts.map +1 -0
  97. package/dist/build/types/src/skin/controls/cast-button.d.ts +15 -0
  98. package/dist/build/types/src/skin/controls/cast-button.d.ts.map +1 -0
  99. package/dist/build/types/src/skin/controls/channel-name.d.ts +16 -0
  100. package/dist/build/types/src/skin/controls/channel-name.d.ts.map +1 -0
  101. package/dist/build/types/src/skin/controls/forward-button.d.ts +15 -0
  102. package/dist/build/types/src/skin/controls/forward-button.d.ts.map +1 -0
  103. package/dist/build/types/src/skin/controls/fullscreen-button.d.ts +30 -0
  104. package/dist/build/types/src/skin/controls/fullscreen-button.d.ts.map +1 -0
  105. package/dist/build/types/src/skin/controls/info-button.d.ts +10 -0
  106. package/dist/build/types/src/skin/controls/info-button.d.ts.map +1 -0
  107. package/dist/build/types/src/skin/controls/live-sync-button.d.ts +20 -0
  108. package/dist/build/types/src/skin/controls/live-sync-button.d.ts.map +1 -0
  109. package/dist/build/types/src/skin/controls/pip-button.d.ts +21 -0
  110. package/dist/build/types/src/skin/controls/pip-button.d.ts.map +1 -0
  111. package/dist/build/types/src/skin/controls/play-pause-button.d.ts +14 -0
  112. package/dist/build/types/src/skin/controls/play-pause-button.d.ts.map +1 -0
  113. package/dist/build/types/src/skin/controls/rewind-button.d.ts +15 -0
  114. package/dist/build/types/src/skin/controls/rewind-button.d.ts.map +1 -0
  115. package/dist/build/types/src/skin/controls/seekbar.d.ts +48 -0
  116. package/dist/build/types/src/skin/controls/seekbar.d.ts.map +1 -0
  117. package/dist/build/types/src/skin/controls/settings-panel.d.ts +53 -0
  118. package/dist/build/types/src/skin/controls/settings-panel.d.ts.map +1 -0
  119. package/dist/build/types/src/skin/controls/share-button.d.ts +11 -0
  120. package/dist/build/types/src/skin/controls/share-button.d.ts.map +1 -0
  121. package/dist/build/types/src/skin/controls/time-display.d.ts +19 -0
  122. package/dist/build/types/src/skin/controls/time-display.d.ts.map +1 -0
  123. package/dist/build/types/src/skin/controls/volume-control.d.ts +21 -0
  124. package/dist/build/types/src/skin/controls/volume-control.d.ts.map +1 -0
  125. package/dist/build/types/src/skin/icons/icons-modern.d.ts +10 -0
  126. package/dist/build/types/src/skin/icons/icons-modern.d.ts.map +1 -0
  127. package/dist/build/types/src/skin/icons/icons.d.ts +10 -0
  128. package/dist/build/types/src/skin/icons/icons.d.ts.map +1 -0
  129. package/dist/build/types/src/skin/icons/sprite.d.ts +39 -0
  130. package/dist/build/types/src/skin/icons/sprite.d.ts.map +1 -0
  131. package/dist/build/types/src/skin/index.d.ts +24 -0
  132. package/dist/build/types/src/skin/index.d.ts.map +1 -0
  133. package/dist/build/types/src/skin/overlays/error-message.d.ts +14 -0
  134. package/dist/build/types/src/skin/overlays/error-message.d.ts.map +1 -0
  135. package/dist/build/types/src/skin/overlays/info-overlay.d.ts +15 -0
  136. package/dist/build/types/src/skin/overlays/info-overlay.d.ts.map +1 -0
  137. package/dist/build/types/src/skin/overlays/loading-spinner.d.ts +17 -0
  138. package/dist/build/types/src/skin/overlays/loading-spinner.d.ts.map +1 -0
  139. package/dist/build/types/src/skin/overlays/preroll-overlay.d.ts +39 -0
  140. package/dist/build/types/src/skin/overlays/preroll-overlay.d.ts.map +1 -0
  141. package/dist/build/types/src/skin/overlays/socials-overlay.d.ts +17 -0
  142. package/dist/build/types/src/skin/overlays/socials-overlay.d.ts.map +1 -0
  143. package/dist/build/types/src/skin/overlays/toast-notification.d.ts +18 -0
  144. package/dist/build/types/src/skin/overlays/toast-notification.d.ts.map +1 -0
  145. package/dist/build/types/src/skin/skin-root.d.ts +88 -0
  146. package/dist/build/types/src/skin/skin-root.d.ts.map +1 -0
  147. package/dist/build/types/src/utils/chapters.d.ts +16 -0
  148. package/dist/build/types/src/utils/chapters.d.ts.map +1 -0
  149. package/dist/build/types/src/utils/format-duration.d.ts +9 -0
  150. package/dist/build/types/src/utils/format-duration.d.ts.map +1 -0
  151. package/dist/build/types/src/utils/format-wall-clock.d.ts +6 -0
  152. package/dist/build/types/src/utils/format-wall-clock.d.ts.map +1 -0
  153. package/dist/build/types/src/utils/settings-helpers.d.ts +41 -0
  154. package/dist/build/types/src/utils/settings-helpers.d.ts.map +1 -0
  155. package/dist/eb-player.css +9 -0
  156. package/package.json +15 -15
@@ -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.19";
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.
@@ -3324,6 +3324,11 @@
3324
3324
  state.on('controlsVisible', () => {
3325
3325
  this.updateControlsVisibility();
3326
3326
  }, { signal });
3327
+ // Toggle ad-playing class so the IMA ads container can receive clicks
3328
+ // (skip button, click-through) and sit above the player chrome during ads.
3329
+ state.on('adPlaying', () => {
3330
+ this.updateAdPlayingState();
3331
+ }, { signal });
3327
3332
  }
3328
3333
  /**
3329
3334
  * Toggles poster image visibility based on playbackState.
@@ -3398,6 +3403,19 @@
3398
3403
  inner.classList.toggle('eb-controls-visible', visible);
3399
3404
  inner.classList.toggle('eb-controls-hidden', !visible);
3400
3405
  }
3406
+ /**
3407
+ * Toggles the eb-ad-playing class on the player container. While an IMA ad
3408
+ * plays, CSS raises the ads container above the player chrome and enables
3409
+ * pointer-events on it so IMA's own ad UI (Skip button, click-through) is
3410
+ * clickable — the container is pointer-events:none at rest to avoid blocking
3411
+ * clicks on the content video.
3412
+ */
3413
+ updateAdPlayingState() {
3414
+ const inner = this.el?.firstElementChild;
3415
+ if (inner == null)
3416
+ return;
3417
+ inner.classList.toggle('eb-ad-playing', this.state.adPlaying);
3418
+ }
3401
3419
  /**
3402
3420
  * Applies or removes the dir=rtl attribute without full re-render.
3403
3421
  * More efficient than re-rendering the entire template for a simple attribute change.
@@ -3863,15 +3881,37 @@
3863
3881
  }
3864
3882
 
3865
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;
3866
3901
  /**
3867
3902
  * AdsManager manages the IMA SDK preroll ad lifecycle.
3868
3903
  *
3869
- * - Loads the IMA SDK from CDN via loadScript
3870
- * - Gates displayContainer.initialize() on a user gesture when config.autoplay is false
3871
- * (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)
3872
3911
  * - Sets state.adPlaying based on IMA lifecycle events
3873
- * - Emits 'ad-complete' on TypedEventBus when all ads are done
3874
- * - 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
3875
3915
  */
3876
3916
  class AdsManager {
3877
3917
  constructor() {
@@ -3881,105 +3921,270 @@
3881
3921
  /**
3882
3922
  * Initialize the IMA SDK preroll ad system.
3883
3923
  * - Skips if config.ad is undefined
3884
- * - If !config.autoplay: waits for user click on video element before initializing
3885
- * (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)
3886
3932
  * - Loads IMA SDK, creates AdDisplayContainer, AdsLoader, and AdsRequest
3887
3933
  * - Wires ad lifecycle events to PlayerState and TypedEventBus
3888
3934
  */
3889
3935
  async init(config, state, bus, video, adsContainer, signal) {
3890
3936
  if (!config.ad)
3891
3937
  return;
3892
- // CR-01: D-10 intro → ad → main precedence. If an intro stream is currently
3893
- // playing, defer IMA initialization until the bus emits 'intro-stream-complete'.
3894
- // Without this gate, IMA fires CONTENT_PAUSE_REQUESTED and tries to play the ad
3895
- // in the same <video> element the intro is using — the two streams race for
3896
- // the element with order determined by network/SDK timing.
3897
- 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) {
3898
4003
  await new Promise((resolve) => {
3899
- bus.on('intro-stream-complete', () => resolve(), { signal });
3900
- 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);
3901
4014
  });
3902
4015
  if (signal.aborted)
3903
4016
  return;
3904
4017
  }
3905
- // Gate initialization on user gesture when not in autoplay mode
3906
- if (!config.autoplay) {
3907
- await this.waitForUserGesture(video, signal);
3908
- 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)
3909
4046
  return;
3910
- }
3911
- const google = await loadScript(IMA_SDK_URL, 'google');
3912
- const { ima } = google;
3913
- const displayContainer = new ima.AdDisplayContainer(adsContainer, video);
3914
- this.imaDisplayContainer = displayContainer;
3915
- displayContainer.initialize();
3916
- const adsLoader = new ima.AdsLoader(displayContainer);
3917
- // AD_ERROR fall-through (CR-02): if the VAST tag fails to load (network
3918
- // error, ad-blocker, malformed XML, autoplay rejection), IMA fires
3919
- // AD_ERROR and never ALL_ADS_COMPLETED. Without this handler the
3920
- // orchestration in eb-player.ts would wait forever for 'ad-complete'
3921
- // and the main stream would never open. Treat AD_ERROR as a normal
3922
- // completion so the orchestration always advances.
3923
- adsLoader.addEventListener(ima.AdErrorEvent.Type.AD_ERROR, (errorEvent) => {
3924
- console.warn('AdsManager: AD_ERROR (loader) — falling through to main', errorEvent);
3925
- state.adPlaying = false;
3926
- bus.emit('ad-complete');
3927
- });
3928
- adsLoader.addEventListener(ima.AdsManagerLoadedEvent.Type.ADS_MANAGER_LOADED, (loadedEvent) => {
3929
- const event = loadedEvent;
3930
- const adsManager = event.getAdsManager(video);
3931
- this.imaAdsManager = adsManager;
3932
- // getBoundingClientRect is always defined on HTMLElement; the prior
3933
- // truthy check on the method reference was dead code. The real edge
3934
- // case is a zero-sized element (display:none, before layout) which
3935
- // would make IMA's ads container collapse — fall back to a 16:9
3936
- // baseline in that case.
3937
- const rect = video.getBoundingClientRect();
3938
- const width = rect.width > 0 ? rect.width : 640;
3939
- const height = rect.height > 0 ? rect.height : 360;
3940
- // Wire event handlers for ad lifecycle
3941
- adsManager.addEventListener(ima.AdEvent.Type.CONTENT_PAUSE_REQUESTED, () => {
3942
- state.adPlaying = true;
3943
- });
3944
- adsManager.addEventListener(ima.AdEvent.Type.CONTENT_RESUME_REQUESTED, () => {
3945
- state.adPlaying = false;
3946
- 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();
3947
4059
  });
3948
- adsManager.addEventListener(ima.AdEvent.Type.ALL_ADS_COMPLETED, () => {
3949
- state.adPlaying = false;
3950
- bus.emit('ad-complete');
3951
- });
3952
- // Same AD_ERROR fall-through on the manager — covers errors that
3953
- // surface only after the ad starts playing (decode error, mid-roll
3954
- // load failure, etc.).
3955
- adsManager.addEventListener(ima.AdErrorEvent.Type.AD_ERROR, (errorEvent) => {
3956
- console.warn('AdsManager: AD_ERROR (manager) — falling through to main', errorEvent);
3957
- state.adPlaying = false;
3958
- 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();
3959
4147
  });
3960
- adsManager.init(width, height);
3961
- adsManager.start();
3962
- });
3963
- // Load the ad tag
3964
- const adsRequest = new ima.AdsRequest();
3965
- adsRequest.adTagUrl = config.ad;
3966
- adsLoader.requestAds(adsRequest);
3967
- // Register abort cleanup
3968
- signal.addEventListener('abort', () => {
3969
- if (this.imaAdsManager !== null) {
3970
- this.imaAdsManager.destroy();
3971
- this.imaAdsManager = null;
3972
- }
3973
- if (this.imaDisplayContainer !== null) {
3974
- this.imaDisplayContainer.destroy();
3975
- this.imaDisplayContainer = null;
3976
- }
3977
- }, { 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
+ }
4160
+ }
4161
+ /**
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
+ }
3978
4176
  }
3979
4177
  /**
3980
- * Wait for a click event on the video element.
4178
+ * Wait for a click event anywhere inside the player root.
3981
4179
  * Used to satisfy browser autoplay policies when autoplay is disabled.
3982
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
+ *
3983
4188
  * WR-05: single-shot settle flag guards against the click-vs-abort race —
3984
4189
  * Promise resolution is already idempotent but settled tightens the
3985
4190
  * contract by ensuring listener cleanup happens exactly once for either
@@ -3987,16 +4192,17 @@
3987
4192
  */
3988
4193
  waitForUserGesture(video, signal) {
3989
4194
  return new Promise((resolve) => {
4195
+ const target = video.closest('.eb-player') ?? video;
3990
4196
  let settled = false;
3991
4197
  const settle = () => {
3992
4198
  if (settled)
3993
4199
  return;
3994
4200
  settled = true;
3995
- video.removeEventListener('click', onClick);
4201
+ target.removeEventListener('click', onClick, { capture: true });
3996
4202
  resolve();
3997
4203
  };
3998
4204
  const onClick = () => settle();
3999
- video.addEventListener('click', onClick);
4205
+ target.addEventListener('click', onClick, { capture: true });
4000
4206
  // Clean up if aborted before click. The abort listener auto-removes
4001
4207
  // via `{ once: true }`; the click listener is removed inside settle().
4002
4208
  signal.addEventListener('abort', settle, { once: true });
@@ -4004,6 +4210,21 @@
4004
4210
  }
4005
4211
  }
4006
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;
4007
4228
  /**
4008
4229
  * IntroStreamManager owns the intro-stream lifecycle.
4009
4230
  *
@@ -4012,12 +4233,24 @@
4012
4233
  * - the video element's native `ended` event
4013
4234
  * - bus `preroll-skip` (from the PrerollOverlay skip button)
4014
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
4015
4247
  * - Any one of those triggers calls a closure `complete()` exactly once
4016
4248
  * (idempotent via a `completed` boolean guard), which flips
4017
4249
  * state.introStreamPlaying = false and emits `intro-stream-complete`.
4018
4250
  * - On abort, the video `ended` listener is removed explicitly via
4019
- * removeEventListener. Bus listeners auto-clean via the `{ signal }` option
4020
- * (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()).
4021
4254
  *
4022
4255
  * Scope: this manager listens and emits — it does NOT itself open the intro
4023
4256
  * stream. The engine open() call is owned by eb-player.ts (single source of
@@ -4028,8 +4261,10 @@
4028
4261
  /**
4029
4262
  * Initialize intro-stream lifecycle tracking.
4030
4263
  * - Skips entirely if config.preroll is falsy (no state flip, no listeners).
4031
- * - Otherwise flips state.introStreamPlaying = true and registers the three
4032
- * 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).
4033
4268
  */
4034
4269
  async init(config, state, bus, video, signal) {
4035
4270
  if (!config.preroll)
@@ -4044,14 +4279,18 @@
4044
4279
  // Mirror outer aborts onto the local controller so dispose() still
4045
4280
  // cleans everything up.
4046
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;
4047
4285
  let completed = false;
4048
4286
  const complete = () => {
4049
4287
  if (completed)
4050
4288
  return;
4051
4289
  completed = true;
4290
+ clearTimeout(backstopTimer);
4052
4291
  state.introStreamPlaying = false;
4053
4292
  bus.emit('intro-stream-complete');
4054
- local.abort(); // tear down bus listeners after the intro completes
4293
+ local.abort(); // tear down bus/state listeners after the intro completes
4055
4294
  };
4056
4295
  const onEnded = () => complete();
4057
4296
  video.addEventListener('ended', onEnded);
@@ -4062,8 +4301,40 @@
4062
4301
  console.warn('IntroStreamManager: intro stream errored — falling through to main');
4063
4302
  complete();
4064
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);
4065
4335
  signal.addEventListener('abort', () => {
4066
4336
  video.removeEventListener('ended', onEnded);
4337
+ clearTimeout(backstopTimer);
4067
4338
  }, { once: true });
4068
4339
  }
4069
4340
  }
@@ -5954,7 +6225,14 @@
5954
6225
  * transient network failures (segment fetch, manifest re-fetch, etc.)
5955
6226
  * never permanently stop playback.
5956
6227
  */
5957
- 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 = {}) {
5958
6236
  driver.updateSettings({
5959
6237
  streaming: {
5960
6238
  retryIntervals: {
@@ -5969,16 +6247,17 @@
5969
6247
  }
5970
6248
  }
5971
6249
  });
6250
+ const retryAttempts = options.introPass === true ? INTRO_PASS_RETRY_ATTEMPTS : Infinity;
5972
6251
  driver.updateSettings({
5973
6252
  streaming: {
5974
6253
  retryAttempts: {
5975
- MPD: Infinity,
5976
- XLinkExpansion: Infinity,
5977
- InitializationSegment: Infinity,
5978
- IndexSegment: Infinity,
5979
- MediaSegment: Infinity,
5980
- BitstreamSwitchingSegment: Infinity,
5981
- other: Infinity,
6254
+ MPD: retryAttempts,
6255
+ XLinkExpansion: retryAttempts,
6256
+ InitializationSegment: retryAttempts,
6257
+ IndexSegment: retryAttempts,
6258
+ MediaSegment: retryAttempts,
6259
+ BitstreamSwitchingSegment: retryAttempts,
6260
+ other: retryAttempts,
5982
6261
  lowLatencyMultiplyFactor: 1
5983
6262
  }
5984
6263
  }
@@ -6160,6 +6439,11 @@
6160
6439
  // Private — NEVER in PlayerState
6161
6440
  this.driver = null;
6162
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;
6163
6447
  }
6164
6448
  getDriver() {
6165
6449
  return this.driver;
@@ -6266,6 +6550,10 @@
6266
6550
  const { config, video, state, signal } = this;
6267
6551
  if (!config || !video || !state || !signal)
6268
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;
6269
6557
  const dashjsUrl = config.dashjs;
6270
6558
  if (!dashjsUrl) {
6271
6559
  throw new Error('DashEngine: config.dashjs URL is required');
@@ -6295,7 +6583,7 @@
6295
6583
  player.initialize(video, this.loadSourceUrl || config.src || '', config.autoplay ?? false);
6296
6584
  // Apply retry settings if requested
6297
6585
  if (config.retry) {
6298
- applyDashRetrySettings(player);
6586
+ applyDashRetrySettings(player, { introPass: this.isIntroPass });
6299
6587
  }
6300
6588
  // Apply custom ABR rule unless disabled
6301
6589
  if (!config.disableCustomAbr) {
@@ -6396,12 +6684,34 @@
6396
6684
  player.on(events['TRACK_CHANGE_RENDERED'], () => {
6397
6685
  state.currentAudioTrack = player.getQualityFor('audio');
6398
6686
  });
6399
- // ERROR: log dash.js errors
6687
+ // ERROR: log dash.js errors, and on the intro pass fall through to main
6400
6688
  player.on(events['ERROR'], (eventData) => {
6401
- const typedData = eventData;
6402
- console.warn('DashEngine error:', typedData?.error?.message ?? typedData?.error ?? typedData);
6689
+ this.handleDriverError(eventData);
6403
6690
  });
6404
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
+ }
6405
6715
  }
6406
6716
 
6407
6717
  /**
@@ -6869,6 +7179,18 @@
6869
7179
  // observably awaitable.
6870
7180
  return new Promise((resolve) => setTimeout(resolve, EME_TEARDOWN_DELAY_MS));
6871
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;
6872
7194
  // ---------------------------------------------------------------------------
6873
7195
  // Public API
6874
7196
  // ---------------------------------------------------------------------------
@@ -7079,13 +7401,17 @@
7079
7401
  controller.bus.on('error-fatal', () => {
7080
7402
  inst.p2p?.stop();
7081
7403
  }, { signal: controller.signal });
7082
- // Phase 7 — intro→main orchestration.
7404
+ // Phase 7/8 — pre-main leg orchestration.
7083
7405
  // When mergedConfig.preroll is set, open the intro stream first, then on
7084
7406
  // bus 'intro-stream-complete' (emitted by IntroStreamManager on video.ended,
7085
7407
  // preroll-skip, or error-fatal) close → 500ms EME teardown delay → open main.
7086
7408
  // If config.ad is also set, wait additionally for bus 'ad-complete' before
7087
7409
  // opening main (D-10: intro → ad → main precedence).
7088
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
+ //
7089
7415
  // Runtime mute override: the live <video>.muted and PlayerState.muted are
7090
7416
  // forced to false for the intro pass (D-13 — intros always audible) and
7091
7417
  // restored to `originalMuted` before opening main. We do NOT mutate
@@ -7095,7 +7421,22 @@
7095
7421
  // P2P opt-in: `mergedConfig.manager` IS re-read on each reference.open() in
7096
7422
  // the P2P branch (eb-player.ts:192), so we config-level swap it to false for
7097
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;
7098
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');
7099
7440
  if (video !== null) {
7100
7441
  video.muted = originalMuted;
7101
7442
  }
@@ -7121,8 +7462,100 @@
7121
7462
  return;
7122
7463
  reference.open(mergedConfig.src);
7123
7464
  };
7124
- if (mergedConfig.preroll) {
7125
- 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) {
7126
7559
  controller.bus.on('intro-stream-complete', () => {
7127
7560
  reference.close();
7128
7561
  // WR-08: restore PlayerState.muted synchronously — DO NOT wait for the
@@ -7136,18 +7569,25 @@
7136
7569
  controller.state.muted = originalMuted;
7137
7570
  // EME teardown delay — see waitForEmeTeardown / EME_TEARDOWN_DELAY_MS.
7138
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;
7139
7579
  if (!hasAd) {
7140
7580
  openMainStream();
7581
+ return;
7141
7582
  }
7142
- // If hasAd, the ad-complete handler below will call openMainStream
7143
- // 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);
7144
7589
  });
7145
7590
  }, { signal: controller.signal });
7146
- if (hasAd) {
7147
- controller.bus.on('ad-complete', () => {
7148
- openMainStream();
7149
- }, { signal: controller.signal });
7150
- }
7151
7591
  // Force runtime mute off for the intro pass — applied to the LIVE <video>
7152
7592
  // element and PlayerState, NOT to mergedConfig.muted (no-op for runtime).
7153
7593
  if (video !== null) {
@@ -7160,6 +7600,52 @@
7160
7600
  controller.config.manager = false;
7161
7601
  reference.open(mergedConfig.preroll);
7162
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
+ }
7163
7649
  // Auto-open the stream if src is provided in config (matches legacy player behaviour
7164
7650
  // where consumers call start({ src: '...' }) and expect playback to begin immediately).
7165
7651
  // When autoplay is false, defer open() until the user requests play — this avoids
@@ -7175,21 +7661,7 @@
7175
7661
  return;
7176
7662
  deferredOpen = false;
7177
7663
  reference.open(mergedConfig.src);
7178
- // Honor the user's click intent. CommandHandler's earlier bus.on('play')
7179
- // listener fired before us with no source attached, so its video.play()
7180
- // was a silent no-op. Wait for driverReady (attachMedia + loadSource
7181
- // done) then start playback ourselves.
7182
- const engine = inst.engine;
7183
- if (engine !== null && video !== null) {
7184
- engine.driverReady.then(() => {
7185
- const playResult = video.play();
7186
- if (playResult && typeof playResult.catch === 'function') {
7187
- playResult.catch((error) => {
7188
- console.warn('EBPlayer: deferred play() rejected', error);
7189
- });
7190
- }
7191
- });
7192
- }
7664
+ startDeferredPlayback();
7193
7665
  }, { signal: controller.signal });
7194
7666
  }
7195
7667
  }