eb-player 2.0.20 → 2.1.0

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