@dolby-optiview/ads-sdk-core 1.1.0 → 1.2.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 (97) hide show
  1. package/dist/OptiViewAds.d.ts +8 -4
  2. package/dist/OptiViewAds.js +1 -1
  3. package/dist/OptiViewAdsExternalVendors.d.ts +47 -0
  4. package/dist/OptiViewAdsExternalVendors.js +1 -0
  5. package/dist/generated/diagnosticCodes.d.ts +118 -3
  6. package/dist/generated/diagnosticCodes.js +1 -1
  7. package/dist/generated/version.d.ts +1 -1
  8. package/dist/generated/version.js +1 -1
  9. package/dist/id3/index.d.ts +1 -0
  10. package/dist/id3/index.js +1 -1
  11. package/dist/id3/parseId3TextFrames.d.ts +8 -0
  12. package/dist/id3/parseId3TextFrames.js +1 -0
  13. package/dist/index.d.ts +8 -3
  14. package/dist/index.js +1 -1
  15. package/dist/interfaces/PlayerAdapter.d.ts +5 -0
  16. package/dist/services/AdBreakSequencer.js +1 -1
  17. package/dist/services/AdPlayerController.d.ts +8 -1
  18. package/dist/services/AdPlayerController.js +1 -1
  19. package/dist/services/BreakScheduler.d.ts +31 -56
  20. package/dist/services/BreakScheduler.js +1 -1
  21. package/dist/services/BreakSchedulerTypes.d.ts +55 -0
  22. package/dist/services/BreakSchedulerTypes.js +1 -0
  23. package/dist/services/ExternalVendorConfigurationTracker.d.ts +13 -0
  24. package/dist/services/ExternalVendorConfigurationTracker.js +1 -0
  25. package/dist/services/GamStreamManager.d.ts +6 -0
  26. package/dist/services/GamStreamManager.js +1 -1
  27. package/dist/services/ImaMetadataDeduplicator.d.ts +5 -0
  28. package/dist/services/ImaMetadataDeduplicator.js +1 -0
  29. package/dist/services/MetadataCueTracker.d.ts +27 -1
  30. package/dist/services/MetadataCueTracker.js +1 -1
  31. package/dist/services/SsaiController.d.ts +2 -0
  32. package/dist/services/SsaiController.js +1 -1
  33. package/dist/services/adPlayer/AdPlayerEvents.js +1 -1
  34. package/dist/services/adPlayer/AdPlayerState.d.ts +13 -0
  35. package/dist/services/adPlayer/AdPlayerState.js +1 -1
  36. package/dist/services/adPlayer/AdPreloader.js +1 -1
  37. package/dist/services/adPlayer/AdStage.d.ts +7 -3
  38. package/dist/services/adPlayer/AdStage.js +1 -1
  39. package/dist/services/adPlayer/AssetParameterResolver.d.ts +2 -0
  40. package/dist/services/adPlayer/AssetParameterResolver.js +1 -1
  41. package/dist/services/adPlayer/AssetPlayback.js +1 -1
  42. package/dist/services/adPlayer/ContentRecovery.js +1 -1
  43. package/dist/services/adPlayer/ContentResumeCoordinator.d.ts +50 -3
  44. package/dist/services/adPlayer/ContentResumeCoordinator.js +1 -1
  45. package/dist/services/adPlayer/GamPodBridge.js +1 -1
  46. package/dist/services/adPlayer/SharedElementBreakPlayer.js +1 -1
  47. package/dist/services/adPlayer/VariantSelector.js +1 -1
  48. package/dist/services/adPlayer/pdtClockResume.d.ts +57 -0
  49. package/dist/services/adPlayer/pdtClockResume.js +1 -0
  50. package/dist/services/adPlayer/streamClockMapping.d.ts +65 -0
  51. package/dist/services/adPlayer/streamClockMapping.js +1 -0
  52. package/dist/services/adPlayer/streamClockResume.d.ts +94 -0
  53. package/dist/services/adPlayer/streamClockResume.js +1 -0
  54. package/dist/services/anvatoCueRuntime.d.ts +12 -0
  55. package/dist/services/anvatoCueRuntime.js +1 -1
  56. package/dist/services/assetParameterMacros.d.ts +0 -1
  57. package/dist/services/assetParameterMacros.js +1 -1
  58. package/dist/services/breakTiming.d.ts +9 -0
  59. package/dist/services/breakTiming.js +1 -1
  60. package/dist/services/channelSwitch.d.ts +3 -1
  61. package/dist/services/channelSwitch.js +1 -1
  62. package/dist/services/detectDeviceType.d.ts +7 -2
  63. package/dist/services/detectDeviceType.js +1 -1
  64. package/dist/services/diagnosticReport.d.ts +3 -0
  65. package/dist/services/diagnosticReport.js +1 -0
  66. package/dist/services/externalVendorConfigurationGetter.d.ts +12 -0
  67. package/dist/services/externalVendorConfigurationGetter.js +1 -0
  68. package/dist/services/matchTargeting.d.ts +19 -0
  69. package/dist/services/matchTargeting.js +1 -0
  70. package/dist/services/resolveDeliveryMode.d.ts +3 -1
  71. package/dist/services/resolveDeliveryMode.js +1 -1
  72. package/dist/services/resolveExternalVendorConfigurations.d.ts +20 -0
  73. package/dist/services/resolveExternalVendorConfigurations.js +1 -0
  74. package/dist/services/resolveGamVendorConfiguration.js +1 -1
  75. package/dist/services/resolveResumeClamp.d.ts +42 -0
  76. package/dist/services/resolveResumeClamp.js +1 -0
  77. package/dist/services/resolveTargetedUri.d.ts +2 -1
  78. package/dist/services/resolveTargetedUri.js +1 -1
  79. package/dist/services/selectVariant.d.ts +4 -0
  80. package/dist/services/selectVariant.js +1 -1
  81. package/dist/services/toPlaybackError.d.ts +1 -0
  82. package/dist/services/toPlaybackError.js +1 -0
  83. package/dist/services/wallclockBreakWindow.d.ts +41 -0
  84. package/dist/services/wallclockBreakWindow.js +1 -0
  85. package/dist/session/BreakLifecycle.d.ts +2 -0
  86. package/dist/session/BreakLifecycle.js +1 -1
  87. package/dist/session/SessionStarter.d.ts +6 -1
  88. package/dist/session/SessionStarter.js +1 -1
  89. package/dist/session/SessionState.d.ts +1 -0
  90. package/dist/session/SessionState.js +1 -1
  91. package/dist/types/BreakManifest.d.ts +39 -1
  92. package/dist/types/BreakManifest.js +1 -1
  93. package/dist/types/Events.d.ts +16 -1
  94. package/dist/types/ExternalVendorConfiguration.d.ts +5 -0
  95. package/dist/types/ExternalVendorConfiguration.js +1 -0
  96. package/dist/types/OptiViewAdsConfig.d.ts +2 -1
  97. package/package.json +1 -1
@@ -1 +1 @@
1
- export const OPTIVIEW_MACRO_PREFIX="$OPTIVIEW_";export const OPTIVIEW_ASSET_PARAMETER_MACROS={USER_AGENT:"$OPTIVIEW_USER_AGENT$",PLAYER_WIDTH:"$OPTIVIEW_PLAYER_WIDTH$",PLAYER_HEIGHT:"$OPTIVIEW_PLAYER_HEIGHT$",BREAK_DURATION_MS:"$OPTIVIEW_BREAK_DURATION_MS$",BREAK_DURATION_SECONDS:"$OPTIVIEW_BREAK_DURATION_SECONDS$"};export const OPTIVIEW_ASSET_PARAMETER_MACRO_SCOPES={[OPTIVIEW_ASSET_PARAMETER_MACROS.USER_AGENT]:"session",[OPTIVIEW_ASSET_PARAMETER_MACROS.PLAYER_WIDTH]:"session",[OPTIVIEW_ASSET_PARAMETER_MACROS.PLAYER_HEIGHT]:"session",[OPTIVIEW_ASSET_PARAMETER_MACROS.BREAK_DURATION_MS]:"break",[OPTIVIEW_ASSET_PARAMETER_MACROS.BREAK_DURATION_SECONDS]:"break"};export function omitBreakScopedParameters(e){if(!e)return;const t=Object.fromEntries(Object.entries(e).filter(([,e])=>!Object.entries(OPTIVIEW_ASSET_PARAMETER_MACRO_SCOPES).some(([t,o])=>"break"===o&&e.includes(t))));return Object.keys(t).length>0?t:void 0}export function resolveAssetParameterMacro(e){return"function"==typeof e?e():e}export function applyAssetParameterMacros(o,n,r){const s=o??{},E={},i=[],_=[];for(const o of Object.keys(s).sort()){if("gam"===r&&"cust_params"===o){const e=t(s[o],n);void 0!==e.value&&(E[o]=e.value),_.push(...e.omitted.map(({macro:e,pair:t})=>({key:o,macro:e,pair:t}))),i.push(...e.unresolved.map(e=>({key:o,macro:e})));continue}const A=e(s[o],n);void 0===A.omittedBy?(E[o]=A.value,i.push(...A.unresolved.map(e=>({key:o,macro:e})))):_.push({key:o,macro:A.omittedBy})}return{params:Object.keys(E).length>0?E:void 0,unresolved:i,omitted:_}}function e(e,t){let o="",r=0;const s=[],E=new Set;for(;r<e.length;){const i=e.indexOf("$",r);if(-1===i){o+=e.slice(r);break}o+=e.slice(r,i);const _=n(e,i);if(void 0===_){o+="$",r=i+1;continue}const A=t[_]?.();if(null===A)return{value:"",unresolved:[],omittedBy:_};void 0!==A?o+=A:(o+=_,!_.startsWith("$OPTIVIEW_")&&!(_ in t)||E.has(_)||(s.push(_),E.add(_))),r=i+_.length}return{value:o,unresolved:s}}function t(t,o){const n=[],r=[],s=[];for(const E of t.split("&")){const t=e(E,o);void 0===t.omittedBy?(n.push(t.value),r.push(...t.unresolved)):s.push({macro:t.omittedBy,pair:E})}return{value:n.length>0?n.join("&"):void 0,unresolved:r,omitted:s}}function o(e){return e>="A"&&e<="Z"||e>="a"&&e<="z"||e>="0"&&e<="9"||"_"===e}function n(e,t){let n=t+1;for(;n<e.length&&o(e[n]);)n++;return n>t+1&&"$"===e[n]?e.slice(t,n+1):void 0}export function appendQueryParameters(e,t){const o=t??{},n=Object.keys(o).sort();if(0===n.length)return e;if(/^data:/i.test(e))return e;const r=n.map(e=>`${encodeURIComponent(e)}=${encodeURIComponent(o[e])}`).join("&"),s=e.indexOf("#"),E=-1===s?e:e.slice(0,s),i=-1===s?"":e.slice(s),_=E.includes("?")?E.endsWith("?")||E.endsWith("&")?"":"&":"?";return`${E}${_}${r}${i}`}
1
+ export const OPTIVIEW_MACRO_PREFIX="$OPTIVIEW_";export const OPTIVIEW_ASSET_PARAMETER_MACROS={USER_AGENT:"$OPTIVIEW_USER_AGENT$",PLAYER_WIDTH:"$OPTIVIEW_PLAYER_WIDTH$",PLAYER_HEIGHT:"$OPTIVIEW_PLAYER_HEIGHT$",BREAK_DURATION_MS:"$OPTIVIEW_BREAK_DURATION_MS$",BREAK_DURATION_SECONDS:"$OPTIVIEW_BREAK_DURATION_SECONDS$"};export const OPTIVIEW_ASSET_PARAMETER_MACRO_SCOPES={[OPTIVIEW_ASSET_PARAMETER_MACROS.USER_AGENT]:"session",[OPTIVIEW_ASSET_PARAMETER_MACROS.PLAYER_WIDTH]:"session",[OPTIVIEW_ASSET_PARAMETER_MACROS.PLAYER_HEIGHT]:"session",[OPTIVIEW_ASSET_PARAMETER_MACROS.BREAK_DURATION_MS]:"break",[OPTIVIEW_ASSET_PARAMETER_MACROS.BREAK_DURATION_SECONDS]:"break"};export function resolveAssetParameterMacro(e){return"function"==typeof e?e():e}export function applyAssetParameterMacros(o,n,s){const r=o??{},E={},i=[],_=[];for(const o of Object.keys(r).sort()){if("gam"===s&&"cust_params"===o){const e=t(r[o],n);void 0!==e.value&&(E[o]=e.value),_.push(...e.omitted.map(({macro:e,pair:t})=>({key:o,macro:e,pair:t}))),i.push(...e.unresolved.map(e=>({key:o,macro:e})));continue}const A=e(r[o],n);void 0===A.omittedBy?(E[o]=A.value,i.push(...A.unresolved.map(e=>({key:o,macro:e})))):_.push({key:o,macro:A.omittedBy})}return{params:Object.keys(E).length>0?E:void 0,unresolved:i,omitted:_}}function e(e,t){let o="",s=0;const r=[],E=new Set;for(;s<e.length;){const i=e.indexOf("$",s);if(-1===i){o+=e.slice(s);break}o+=e.slice(s,i);const _=n(e,i);if(void 0===_){o+="$",s=i+1;continue}const A=t[_]?.();if(null===A)return{value:"",unresolved:[],omittedBy:_};void 0!==A?o+=A:(o+=_,!_.startsWith("$OPTIVIEW_")&&!(_ in t)||E.has(_)||(r.push(_),E.add(_))),s=i+_.length}return{value:o,unresolved:r}}function t(t,o){const n=[],s=[],r=[];for(const E of t.split("&")){const t=e(E,o);void 0===t.omittedBy?(n.push(t.value),s.push(...t.unresolved)):r.push({macro:t.omittedBy,pair:E})}return{value:n.length>0?n.join("&"):void 0,unresolved:s,omitted:r}}function o(e){return e>="A"&&e<="Z"||e>="a"&&e<="z"||e>="0"&&e<="9"||"_"===e}function n(e,t){let n=t+1;for(;n<e.length&&o(e[n]);)n++;return n>t+1&&"$"===e[n]?e.slice(t,n+1):void 0}export function appendQueryParameters(e,t){const o=t??{},n=Object.keys(o).sort();if(0===n.length)return e;if(/^data:/i.test(e))return e;const s=n.map(e=>`${encodeURIComponent(e)}=${encodeURIComponent(o[e])}`).join("&"),r=e.indexOf("#"),E=-1===r?e:e.slice(0,r),i=-1===r?"":e.slice(r),_=E.includes("?")?E.endsWith("?")||E.endsWith("&")?"":"&":"?";return`${E}${_}${s}${i}`}
@@ -25,6 +25,8 @@ export interface BreakTimingState {
25
25
  anvatoPendingDiagnosed: Set<string>;
26
26
  /** Break ids for which DA-ANVATO-CUE-MATCHED has been emitted (one-shot). */
27
27
  anvatoMatchedDiagnosed: Set<string>;
28
+ /** Break ids whose `cue_end` was judged (DA-ANVATO-CUE-END-MATCHED or -REJECTED, one-shot). */
29
+ anvatoEndDiagnosed: Set<string>;
28
30
  }
29
31
  export declare function createBreakTimingState(): BreakTimingState;
30
32
  export interface BreakTimingDeps {
@@ -57,4 +59,11 @@ export declare function getBreakStartTime(state: BreakTimingState, deps: BreakTi
57
59
  * Null while no matching cue was observed; pending and first match are each diagnosed once.
58
60
  */
59
61
  export declare function resolveAnvatoStartTime(state: BreakTimingState, deps: BreakTimingDeps, breakInfo: Break, startPts: number): number | null;
62
+ /**
63
+ * Judge the active PTS break's `type=cue_end` once: matched when its stream-clock length is
64
+ * within tolerance of the duration, rejected otherwise. Nothing until a candidate is observed.
65
+ */
66
+ export declare function diagnoseAnvatoBreakEnd(state: BreakTimingState, deps: BreakTimingDeps, breakInfo: Break): void;
60
67
  export declare function secondsUntilBreak(breakStartTime: number, currentTime: number): number;
68
+ /** Human-readable start of a break for scheduler logs. */
69
+ export declare function describeBreakStart(breakInfo: Break): string;
@@ -1 +1 @@
1
- import{Timebase as t}from"../types/BreakManifest";export const PDT_PLAYBACK_SETTLE_MS=1e3;export function createBreakTimingState(){return{timebase:t.WALLCLOCK,pdtMissingWarned:!1,pdtSeen:!1,firstPdtAttemptMs:null,playbackStartedAtMs:null,anvatoPendingDiagnosed:new Set,anvatoMatchedDiagnosed:new Set}}export function getCurrentTime(e,a){if(e.timebase!==t.WALLCLOCK)return 1e3*a.player.currentTime;const n=a.player.programDateTime;if(n){const t=n.getTime();if(!isNaN(t))return e.pdtSeen=!0,t}if(e.pdtSeen)return null;const r=a.clock.now();null===e.firstPdtAttemptMs&&(e.firstPdtAttemptMs=r),null===e.playbackStartedAtMs&&a.player.currentTime>0&&(e.playbackStartedAtMs=r);const s=null!==e.playbackStartedAtMs&&r-e.playbackStartedAtMs>=1e3,i=r-e.firstPdtAttemptMs>=a.pdtGraceMs;return s||i?(warnPdtMissing(e,a,s),r):null}export function warnPdtMissing(t,e,a){if(t.pdtMissingWarned)return;t.pdtMissingWarned=!0;const n=a?"content is playing and exposes none":`none appeared within ${e.pdtGraceSeconds}s`;e.log(`No Program Date Time on wallclock stream (${n}); falling back to the system clock`),e.diagnose?.("DA-PDT-MISSING",`No EXT-X-PROGRAM-DATE-TIME available (${n}); wallclock-timebase breaks are matched against the system clock instead of the stream position.`,{graceSeconds:e.pdtGraceSeconds,playbackStarted:a})}export function getBreakStartTime(e,a,n){if(e.timebase===t.WALLCLOCK){if("string"!=typeof n.start)return a.log("Invalid break start for wallclock timebase:",n.start),null;const t=new Date(n.start);return isNaN(t.getTime())?(a.log("Invalid break start date:",n.start),null):t.getTime()}return"number"!=typeof n.start?(a.log(`Invalid break start for ${e.timebase} timebase:`,n.start),null):a.cueTracker&&e.timebase===t.PTS?resolveAnvatoStartTime(e,a,n,n.start):1e3*n.start}export function resolveAnvatoStartTime(t,e,a,n){const r=e.cueTracker.mediaTimeForPts(n);return null===r?(t.anvatoPendingDiagnosed.has(a.id)||(t.anvatoPendingDiagnosed.add(a.id),e.log(`⌛ Break "${a.id}" awaiting Anvato cue for PTS ${n}`),e.diagnose?.("DA-ANVATO-CUE-PENDING",`No Anvato cue observed for break "${a.id}" (pts ${n}); break stays pending.`,{breakId:a.id,pts:n})),null):(t.anvatoMatchedDiagnosed.has(a.id)||(t.anvatoMatchedDiagnosed.add(a.id),e.log(`🎯 Break "${a.id}" matched Anvato cue: pts ${n} → ${r}s`),e.diagnose?.("DA-ANVATO-CUE-MATCHED",`Anvato cue matched for break "${a.id}": pts ${n} plays at media time ${r}s.`,{breakId:a.id,pts:n,mediaTime:r})),1e3*r)}export function secondsUntilBreak(t,e){const a=Math.round((t-e)/1e3);return a>0?a:0}
1
+ import{Timebase as e,getPostRollDelay as t,getPreRollDelay as a}from"../types/BreakManifest";export const PDT_PLAYBACK_SETTLE_MS=1e3;export function createBreakTimingState(){return{timebase:e.WALLCLOCK,pdtMissingWarned:!1,pdtSeen:!1,firstPdtAttemptMs:null,playbackStartedAtMs:null,anvatoPendingDiagnosed:new Set,anvatoMatchedDiagnosed:new Set,anvatoEndDiagnosed:new Set}}export function getCurrentTime(t,a){if(t.timebase!==e.WALLCLOCK)return 1e3*a.player.currentTime;const n=a.player.programDateTime;if(n){const e=n.getTime();if(!isNaN(e))return t.pdtSeen=!0,e}if(t.pdtSeen)return null;const r=a.clock.now();null===t.firstPdtAttemptMs&&(t.firstPdtAttemptMs=r),null===t.playbackStartedAtMs&&a.player.currentTime>0&&(t.playbackStartedAtMs=r);const i=null!==t.playbackStartedAtMs&&r-t.playbackStartedAtMs>=1e3,s=r-t.firstPdtAttemptMs>=a.pdtGraceMs;return i||s?(warnPdtMissing(t,a,i),r):null}export function warnPdtMissing(e,t,a){if(e.pdtMissingWarned)return;e.pdtMissingWarned=!0;const n=a?"content is playing and exposes none":`none appeared within ${t.pdtGraceSeconds}s`;t.log(`No Program Date Time on wallclock stream (${n}); falling back to the system clock`),t.diagnose?.("DA-PDT-MISSING",`No EXT-X-PROGRAM-DATE-TIME available (${n}); wallclock-timebase breaks are matched against the system clock instead of the stream position.`,{graceSeconds:t.pdtGraceSeconds,playbackStarted:a})}export function getBreakStartTime(t,a,n){if(t.timebase===e.WALLCLOCK){if("string"!=typeof n.start)return a.log("Invalid break start for wallclock timebase:",n.start),null;const e=new Date(n.start);return isNaN(e.getTime())?(a.log("Invalid break start date:",n.start),null):e.getTime()}return"number"!=typeof n.start?(a.log(`Invalid break start for ${t.timebase} timebase:`,n.start),null):a.cueTracker&&t.timebase===e.PTS?resolveAnvatoStartTime(t,a,n,n.start):1e3*n.start}export function resolveAnvatoStartTime(e,t,a,n){const r=t.cueTracker.mediaTimeForPts(n);return null===r?(e.anvatoPendingDiagnosed.has(a.id)||(e.anvatoPendingDiagnosed.add(a.id),t.log(`⌛ Break "${a.id}" awaiting Anvato cue for PTS ${n}`),t.diagnose?.("DA-ANVATO-CUE-PENDING",`No Anvato cue observed for break "${a.id}" (pts ${n}); break stays pending.`,{breakId:a.id,pts:n})),null):(e.anvatoMatchedDiagnosed.has(a.id)||(e.anvatoMatchedDiagnosed.add(a.id),t.log(`🎯 Break "${a.id}" matched Anvato cue: pts ${n} → ${r}s`),t.diagnose?.("DA-ANVATO-CUE-MATCHED",`Anvato cue matched for break "${a.id}": pts ${n} plays at media time ${r}s.`,{breakId:a.id,pts:n,mediaTime:r})),1e3*r)}export function diagnoseAnvatoBreakEnd(e,t,a){if(null===t.cueTracker||"number"!=typeof a.start)return;if(e.anvatoEndDiagnosed.has(a.id))return;const n=t.cueTracker.breakEnd(a.start,a.duration);if(null===n)return;e.anvatoEndDiagnosed.add(a.id);const r={breakId:a.id,mediaTime:n.mediaTime,lengthSec:n.lengthSec,durationSec:a.duration};n.accepted?(t.log(`Break "${a.id}" end signalled at media time ${n.mediaTime}s`),t.diagnose?.("DA-ANVATO-CUE-END-MATCHED",`Anvato cue_end matched for break "${a.id}": the stream ends it at media time ${n.mediaTime}s (${n.lengthSec}s on the stream clock).`,r)):t.diagnose?.("DA-ANVATO-CUE-END-REJECTED",`Anvato cue_end for break "${a.id}" signals ${n.lengthSec}s, not the ${a.duration}s duration; it is ignored.`,r)}export function secondsUntilBreak(e,t){const a=Math.round((e-t)/1e3);return a>0?a:0}export function describeBreakStart(e){const n=a(e);if(null!==n)return n>0?`playback start + ${n}s`:"playback start";const r=t(e);return null!==r?r>0?`playback end + ${r}s`:"playback end":"string"==typeof e.start?new Date(e.start).toLocaleTimeString():String(e.start)}
@@ -1,5 +1,6 @@
1
1
  import type { BreakManifest } from '../types/BreakManifest';
2
2
  import { DeviceType } from '../types/BreakManifest';
3
+ import type { DeviceProfile, TargetingIssue } from './matchTargeting';
3
4
  import type { AdChannelChangeReason } from '../types/Events';
4
5
  import type { OptiViewAdsMode } from '../types/OptiViewAdsConfig';
5
6
  export interface ChannelSwitch {
@@ -16,9 +17,10 @@ export interface DeliveryModeDecision {
16
17
  mode: OptiViewAdsMode;
17
18
  ruleIndex: number | null;
18
19
  unknownMode: string | null;
20
+ targetingIssues: TargetingIssue[];
19
21
  source: DeliveryModeSource;
20
22
  }
21
- export declare function decideDeliveryMode(manifest: BreakManifest, deviceType: DeviceType, explicitMode: OptiViewAdsMode | null, source: 'manifest' | 'channel-switch'): DeliveryModeDecision;
23
+ export declare function decideDeliveryMode(manifest: BreakManifest, deviceProfile: DeviceType | DeviceProfile, explicitMode: OptiViewAdsMode | null, source: 'manifest' | 'channel-switch'): DeliveryModeDecision;
22
24
  /** Blank ids count as absent: the backend omits the field, a proxy may emit `""`. */
23
25
  export declare function normalizeChannelId(channelId: string | undefined): string | null;
24
26
  /** True when the new manifest needs a new IMA session (different network or custom asset key). */
@@ -1 +1 @@
1
- import{resolveGamVendorConfiguration as e}from"./resolveGamVendorConfiguration";import{resolveDeliveryMode as n,resolveStitchedStreamVendor as o}from"./resolveDeliveryMode";export function decideDeliveryMode(e,o,r,t){const d=n(e.delivery,o);return{manifestMode:d.mode,mode:r??d.mode,ruleIndex:d.ruleIndex,unknownMode:d.unknownMode,source:r?"config":t}}export function normalizeChannelId(e){return"string"==typeof e&&""!==e.trim()?e:null}function r(n){const o=e(n).identity;return o?`${o.networkCode}|${o.customAssetKey}`:""}export function gamPodIdentityChanged(e,n){return r(e)!==r(n)}function t(e){const n=o(e.vendorConfiguration);return n?`${n.networkCode}|${n.assetKey}`:""}export function detectChannelSwitch(e,n){const o=normalizeChannelId(e.channelId),r=normalizeChannelId(n.channelId),d={previousChannelId:o,channelId:r};return o!==r?null===o?{...d,reason:"channel-id-added"}:null===r?{...d,reason:"channel-id-removed"}:{...d,reason:"channel-id-changed"}:gamPodIdentityChanged(e,n)||t(e)!==t(n)?{...d,reason:"vendor-configuration-changed"}:e.timebase!==n.timebase?{...d,reason:"timebase-changed"}:null}
1
+ import{resolveGamVendorConfiguration as e}from"./resolveGamVendorConfiguration";import{resolveDeliveryMode as n,resolveStitchedStreamVendor as o}from"./resolveDeliveryMode";export function decideDeliveryMode(e,o,r,t){const d=n(e.delivery,o);return{manifestMode:d.mode,mode:r??d.mode,ruleIndex:d.ruleIndex,unknownMode:d.unknownMode,targetingIssues:d.targetingIssues,source:r?"config":t}}export function normalizeChannelId(e){return"string"==typeof e&&""!==e.trim()?e:null}function r(n){const o=e(n).identity;return o?`${o.networkCode}|${o.customAssetKey}`:""}export function gamPodIdentityChanged(e,n){return r(e)!==r(n)}function t(e){const n=o(e.vendorConfiguration);return n?`${n.networkCode}|${n.assetKey}`:""}export function detectChannelSwitch(e,n){const o=normalizeChannelId(e.channelId),r=normalizeChannelId(n.channelId),d={previousChannelId:o,channelId:r};return o!==r?null===o?{...d,reason:"channel-id-added"}:null===r?{...d,reason:"channel-id-removed"}:{...d,reason:"channel-id-changed"}:gamPodIdentityChanged(e,n)||t(e)!==t(n)?{...d,reason:"vendor-configuration-changed"}:e.timebase!==n.timebase?{...d,reason:"timebase-changed"}:null}
@@ -1,4 +1,5 @@
1
1
  import { DeviceType } from '../types/BreakManifest';
2
+ import type { DeviceProfile } from './matchTargeting';
2
3
  /**
3
4
  * Heuristic: classify a User-Agent string into a {@link DeviceType} for
4
5
  * per-URI asset targeting (see {@link resolveTargetedUri}).
@@ -9,7 +10,11 @@ import { DeviceType } from '../types/BreakManifest';
9
10
  * conformance fixture (mirrors `isSharedElementUserAgent` /
10
11
  * `isSingleDecoderUserAgent`, which are likewise UA-only web glue).
11
12
  *
12
- * Checked in order TV → tablet → mobile → desktop (default), since a tablet
13
- * or TV UA can otherwise coincidentally match a broader mobile pattern.
13
+ * Checked in order console → TV → tablet → mobile → desktop (default), since a tablet
14
+ * or TV UA can otherwise coincidentally match a broader mobile pattern. Console and TV
15
+ * devices are recognised by their User-Agent brand tokens;
16
+ * a bare `CrKey` is not enough, as it also marks Chromecast-style desktops.
14
17
  */
15
18
  export declare function detectDeviceType(ua?: string): DeviceType;
19
+ /** Derives the browser's targeting profile synchronously from its User-Agent. */
20
+ export declare function detectDeviceProfile(ua?: string): DeviceProfile;
@@ -1 +1 @@
1
- import{DeviceType as e}from"../types/BreakManifest";export function detectDeviceType(t=("undefined"!=typeof navigator?navigator.userAgent:"")){return t?/SMART-TV|SmartTV|Tizen|Web0S|WebOS|GoogleTV|AppleTV|HbbTV|NetCast|VIDAA|BRAVIA/i.test(t)?e.TV:/iPad/i.test(t)||/Android/i.test(t)&&!/Mobile/i.test(t)?e.TABLET:/iPhone|iPod|Mobi|Android.*Mobile/i.test(t)?e.MOBILE:e.DESKTOP:e.DESKTOP}
1
+ import{DeviceType as e}from"../types/BreakManifest";import{Os as t,Platform as i}from"../types/BreakManifest";export function detectDeviceType(t=("undefined"!=typeof navigator?navigator.userAgent:"")){return t?/Xbox/i.test(t)?e.CONSOLE:/SMART-TV|SmartTV|Tizen|Web0S|WebOS|GoogleTV|AppleTV|HbbTV|NetCast|VIDAA|BRAVIA|VIZIO|SmartCast/i.test(t)?e.TV:/iPad/i.test(t)||/Android/i.test(t)&&!/Mobile/i.test(t)?e.TABLET:/iPhone|iPod|Mobi|Android.*Mobile/i.test(t)?e.MOBILE:e.DESKTOP:e.DESKTOP}const o={38:"3",53:"4",68:"5",79:"6",87:"22",94:"23",108:"24",120:"25"},s=Object.keys(o).reduce((e,t)=>Number(t)>Number(e)?t:e),r=o[s];export function detectDeviceProfile(e=("undefined"!=typeof navigator?navigator.userAgent:"")){const n={deviceType:detectDeviceType(e),platform:i.WEB};let a;if(/Xbox/i.test(e))return{...n,os:t.XBOX};if(/VIZIO/i.test(e))return{...n,os:t.VIZIO};if(/Tizen/i.test(e)){const i=e.match(/Tizen\s+([\d.]+)/i)?.[1];return{...n,os:t.TIZEN,...i?{osVersion:i}:{}}}if(/Web0S|WebOS/i.test(e)){const i=e.match(/Chrome\/(\d+)/)?.[1],a=i?o[i]??(Number(i)>Number(s)?r:void 0):void 0;return{...n,os:t.WEBOS,...a?{osVersion:a}:{}}}return(a=e.match(/CrOS\s+\S+\s+([\d.]+)/i))?{...n,os:t.CHROMEOS,osVersion:a[1]}:(a=e.match(/Android\s+([\d.]+)/i))?{...n,os:t.ANDROID,osVersion:a[1]}:(a=e.match(/(?:iPhone|iPad|iPod).*?OS\s+([\d_]+)/i))?{...n,os:/iPad/i.test(e)?t.IPADOS:t.IOS,osVersion:a[1].replace(/_/g,".")}:/Macintosh|Mac OS X/i.test(e)?{...n,os:t.MACOS}:/Windows/i.test(e)?{...n,os:t.WINDOWS}:/Linux/i.test(e)?{...n,os:t.LINUX}:n}
@@ -0,0 +1,3 @@
1
+ import type { DiagnosticEvent, DiagnosticReport } from '../diagnostics/DiagnosticEvent';
2
+ import type { SessionState } from '../session/SessionState';
3
+ export declare function buildDiagnosticReport(state: Pick<SessionState, 'configSummary' | 'manifestOrigin' | 'stitcherOrigin'>, diagnostics: DiagnosticEvent[]): DiagnosticReport;
@@ -0,0 +1 @@
1
+ import{SDK_VERSION as i}from"../generated/version";export function buildDiagnosticReport(n,r){const t={...n.configSummary,...n.manifestOrigin?{manifestOrigin:n.manifestOrigin}:{},...n.configSummary.ssai&&n.stitcherOrigin?{ssai:{...n.configSummary.ssai,stitcherOrigin:n.stitcherOrigin}}:{}};return{sdkVersion:i,generatedAt:Date.now(),userAgent:"undefined"!=typeof navigator?navigator.userAgent:null,config:t,diagnostics:r}}
@@ -0,0 +1,12 @@
1
+ import type { ExternalVendorConfiguration } from '../types/ExternalVendorConfiguration';
2
+ import type { ExternalVendorConfiguration as ExternalVendorConfigurationTemplate } from './resolveExternalVendorConfigurations';
3
+ interface ExternalVendorConfigurationController {
4
+ resolveExternalVendorConfiguration(entry: ExternalVendorConfigurationTemplate): ExternalVendorConfiguration | undefined;
5
+ }
6
+ interface ExternalVendorConfigurationState {
7
+ sessionActive: boolean;
8
+ steeredSsaiActive: boolean;
9
+ ssaiController?: ExternalVendorConfigurationController | null;
10
+ }
11
+ export declare function readExternalVendorConfiguration(state: ExternalVendorConfigurationState, entries: ExternalVendorConfigurationTemplate[], mode: string, adPlayerController: ExternalVendorConfigurationController | null | undefined, provider?: string): ExternalVendorConfiguration[] | ExternalVendorConfiguration | undefined;
12
+ export {};
@@ -0,0 +1 @@
1
+ export function readExternalVendorConfiguration(r,e,o,i,t){if(!r.sessionActive)return void 0===t?[]:void 0;const s="ssai"===o||r.steeredSsaiActive?r.ssaiController:i,n=r=>{const e=s?.resolveExternalVendorConfiguration(r)??{provider:r.provider,assetParameters:{}};return{provider:e.provider,assetParameters:{...e.assetParameters}}};if(void 0!==t){const r=e.find(r=>r.provider===t);return r?n(r):void 0}return e.map(n)}
@@ -0,0 +1,19 @@
1
+ import { DeviceType, Os, Platform } from '../types/BreakManifest';
2
+ import type { Targeting } from '../types/BreakManifest';
3
+ export interface DeviceProfile {
4
+ deviceType?: DeviceType | string;
5
+ platform?: Platform | string;
6
+ os?: Os | string;
7
+ osVersion?: string;
8
+ }
9
+ export interface TargetingIssue {
10
+ code: 'DA-TARGETING-VALUE-UNKNOWN' | 'DA-TARGETING-FIELD-UNKNOWN' | 'DA-TARGETING-INVALID';
11
+ field: string;
12
+ value?: string;
13
+ }
14
+ export interface TargetingMatch {
15
+ matches: boolean;
16
+ issues: TargetingIssue[];
17
+ }
18
+ /** Matches every specified targeting field; empty targeting remains a default. */
19
+ export declare function matchTargeting(targeting: Targeting | undefined, profile: DeviceProfile): TargetingMatch;
@@ -0,0 +1 @@
1
+ import{DeviceType as e,Os as s,Platform as i}from"../types/BreakManifest";const o=["deviceType","platform","os","osVersion"],n=Object.keys(e).map(s=>e[s]),t=Object.keys(i).map(e=>i[e]),r=Object.keys(s).map(e=>s[e]),f=/^\d+$/;function u(e){const s=e.split(".");return s.every(e=>f.test(e))?s:null}function c(e,s){const i=e.replace(/^0+(?=\d)/,""),o=s.replace(/^0+(?=\d)/,"");return i.length!==o.length?i.length<o.length?-1:1:i===o?0:i<o?-1:1}function m(e,s){for(let i=0;i<s.length;i++){const o=c(e[i]??"0",s[i]);if(0!==o)return o}return 0}function a(e,s,i){if("object"!=typeof(o=e)||null===o||Array.isArray(o))return i.push({code:"DA-TARGETING-INVALID",field:"osVersion"}),!1;var o;for(const s of Object.keys(e))if("min"!==s&&"max"!==s)return i.push({code:"DA-TARGETING-FIELD-UNKNOWN",field:`osVersion.${s}`}),!1;const n=e;if(void 0===n.min&&void 0===n.max)return i.push({code:"DA-TARGETING-INVALID",field:"osVersion"}),!1;if("string"==typeof n.min&&!u(n.min))return i.push({code:"DA-TARGETING-INVALID",field:"osVersion.min",value:n.min}),!1;if("string"==typeof n.max&&!u(n.max))return i.push({code:"DA-TARGETING-INVALID",field:"osVersion.max",value:n.max}),!1;if(void 0!==n.min&&"string"!=typeof n.min||void 0!==n.max&&"string"!=typeof n.max)return i.push({code:"DA-TARGETING-INVALID",field:"osVersion"}),!1;if(!s.os||!s.osVersion)return!1;const t=u(s.osVersion);return!!t&&(!(void 0!==n.min&&m(t,u(n.min))<0)&&!(void 0!==n.max&&m(t,u(n.max))>0))}export function matchTargeting(e,s){if(void 0===e)return{matches:!0,issues:[]};const i=e,f=[];for(const e of Object.keys(i))o.includes(e)||f.push({code:"DA-TARGETING-FIELD-UNKNOWN",field:e});if(f.length>0)return{matches:!1,issues:f};for(const[e,o,u]of[["deviceType",n,s.deviceType],["platform",t,s.platform],["os",r,s.os]]){const s=i[e];if(void 0===s)continue;if("string"!=typeof s||!o.includes(s))return f.push({code:"DA-TARGETING-VALUE-UNKNOWN",field:e,value:String(s)}),{matches:!1,issues:f};if(s!==u)return{matches:!1,issues:f}}if(void 0!==i.osVersion){if(!i.os)return f.push({code:"DA-TARGETING-INVALID",field:"osVersion"}),{matches:!1,issues:f};if(!a(i.osVersion,s,f)||f.length>0)return{matches:!1,issues:f}}return{matches:!0,issues:f}}
@@ -1,4 +1,5 @@
1
1
  import { BreakManifest, DeliveryMode, DeliveryRule, DeviceType, VendorConfiguration } from '../types/BreakManifest';
2
+ import type { DeviceProfile, TargetingIssue } from './matchTargeting';
2
3
  import { type StitchedStreamVendor } from './resolveGamVendorConfiguration';
3
4
  export type { StitchedStreamVendor } from './resolveGamVendorConfiguration';
4
5
  /**
@@ -22,6 +23,7 @@ export interface DeliveryModeDecision {
22
23
  * value, which the resolver replaced with sgai (forward compatibility).
23
24
  */
24
25
  unknownMode: string | null;
26
+ targetingIssues: TargetingIssue[];
25
27
  }
26
28
  /**
27
29
  * Resolve the delivery mode from a manifest's ordered `delivery` rules for the
@@ -38,7 +40,7 @@ export interface DeliveryModeDecision {
38
40
  * 3. A matching rule with an unrecognized mode value → sgai, with the raw value
39
41
  * reported via `unknownMode` so the caller can log it (forward compatibility).
40
42
  */
41
- export declare function resolveDeliveryMode(delivery: DeliveryRule[] | undefined, deviceType: DeviceType): DeliveryModeDecision;
43
+ export declare function resolveDeliveryMode(delivery: DeliveryRule[] | undefined, deviceProfile: DeviceType | DeviceProfile): DeliveryModeDecision;
42
44
  /**
43
45
  * Find the first vendor configuration entry able to provide a server-stitched
44
46
  * stream — currently a `gam` entry carrying a full-service `assetKey`. Returns
@@ -1 +1 @@
1
- import{DeliveryMode as e,getBreakEvent as r}from"../types/BreakManifest";import{resolveGamVendorConfiguration as n}from"./resolveGamVendorConfiguration";export class SsaiUnavailableError extends Error{constructor(e){super(e),this.name="SsaiUnavailableError"}}export function resolveDeliveryMode(r,n){if(!Array.isArray(r))return{mode:e.SGAI,ruleIndex:null,unknownMode:null};for(let o=0;o<r.length;o++){const t=r[o],i=t?.targeting?.deviceType;if(void 0===i||i===n)return t.mode===e.SGAI||t.mode===e.SSAI?{mode:t.mode,ruleIndex:o,unknownMode:null}:{mode:e.SGAI,ruleIndex:o,unknownMode:String(t.mode)}}return{mode:e.SGAI,ruleIndex:null,unknownMode:null}}export function resolveStitchedStreamVendor(e){return n(void 0===e?{}:{vendorConfiguration:e}).ssai}export function filterSsaiClientBreaks(e){return{...e,breaks:e.breaks.filter(e=>null!==r(e))}}
1
+ import{DeliveryMode as e,getBreakEvent as r}from"../types/BreakManifest";import{matchTargeting as n}from"./matchTargeting";import{resolveGamVendorConfiguration as o}from"./resolveGamVendorConfiguration";export class SsaiUnavailableError extends Error{constructor(e){super(e),this.name="SsaiUnavailableError"}}export function resolveDeliveryMode(r,o){const t="string"==typeof o?{deviceType:o}:o;if(!Array.isArray(r))return{mode:e.SGAI,ruleIndex:null,unknownMode:null,targetingIssues:[]};const s=[];for(let o=0;o<r.length;o++){const i=r[o],u=n(i?.targeting,t);if(s.push(...u.issues),u.matches)return i.mode===e.SGAI||i.mode===e.SSAI?{mode:i.mode,ruleIndex:o,unknownMode:null,targetingIssues:s}:{mode:e.SGAI,ruleIndex:o,unknownMode:String(i.mode),targetingIssues:s}}return{mode:e.SGAI,ruleIndex:null,unknownMode:null,targetingIssues:s}}export function resolveStitchedStreamVendor(e){return o(void 0===e?{}:{vendorConfiguration:e}).ssai}export function filterSsaiClientBreaks(e){return{...e,breaks:e.breaks.filter(e=>null!==r(e))}}
@@ -0,0 +1,20 @@
1
+ import type { BreakManifest } from '../types/BreakManifest';
2
+ import { type AssetParameterMacroTable, type OmittedAssetParameter, type UnresolvedAssetParameterMacro } from './assetParameterMacros';
3
+ export interface ExternalVendorConfiguration {
4
+ provider: string;
5
+ assetParameters: Record<string, string>;
6
+ }
7
+ export interface ExternalVendorConfigurationsResult {
8
+ entries: ExternalVendorConfiguration[];
9
+ duplicateProviders: string[];
10
+ invalidCount: number;
11
+ }
12
+ export interface ResolvedExternalVendorParameters {
13
+ configuration: ExternalVendorConfiguration;
14
+ unresolved: UnresolvedAssetParameterMacro[];
15
+ omitted: OmittedAssetParameter[];
16
+ }
17
+ /** Return providers whose manifest templates were added, changed, or removed. */
18
+ export declare function changedExternalVendorProviders(previous: ExternalVendorConfiguration[], next: ExternalVendorConfiguration[]): string[];
19
+ export declare function resolveExternalVendorConfigurations(manifest: Pick<BreakManifest, 'vendorConfiguration'>): ExternalVendorConfigurationsResult;
20
+ export declare function resolveExternalVendorParameters(entry: ExternalVendorConfiguration, applicationParameters: Record<string, string> | undefined, macros: AssetParameterMacroTable): ResolvedExternalVendorParameters;
@@ -0,0 +1 @@
1
+ import{applyAssetParameterMacros as r}from"./assetParameterMacros";import{objectEntries as e,objectFromEntries as t}from"./runtimeObject";export function changedExternalVendorProviders(r,e){const t=new Map(r.map(r=>[r.provider,r])),n=new Set(e.map(r=>r.provider)),o=e.filter(r=>{const e=t.get(r.provider);return void 0===e||!function(r,e){const t=Object.keys(r);return t.length===Object.keys(e).length&&t.every(t=>Object.prototype.hasOwnProperty.call(e,t)&&r[t]===e[t])}(e.assetParameters,r.assetParameters)}).map(r=>r.provider);for(const e of r)n.has(e.provider)||o.push(e.provider);return o}function n(r){if("object"!=typeof r||null===r||Array.isArray(r))return!1;const e=Object.getPrototypeOf(r);return e===Object.prototype||null===e}function o(r){return"string"==typeof r&&""!==r.trim()}function s(r){return n(r)?t(e(r).filter(r=>"string"==typeof r[1])):null}export function resolveExternalVendorConfigurations(r){const e=r.vendorConfiguration,t=[],i=[],a=new Set;let u=0;if(!Array.isArray(e))return{entries:t,duplicateProviders:i,invalidCount:u};for(const r of e){if(!n(r)||"external"!==r.type)continue;const e=s(r.assetParameters);if(!o(r.provider)||null===e){u+=1;continue}const p=r.provider;a.has(p)?i.includes(p)||i.push(p):(a.add(p),t.push({provider:p,assetParameters:e}))}return{entries:t,duplicateProviders:i,invalidCount:u}}export function resolveExternalVendorParameters(e,t,n){const o={...t??{},...e.assetParameters},s=r(o,n,"gam");return{configuration:{provider:e.provider,assetParameters:s.params??{}},unresolved:s.unresolved,omitted:s.omitted}}
@@ -1 +1 @@
1
- import{objectEntries as e,objectFromEntries as s}from"./runtimeObject";function t(e){return"string"==typeof e&&""!==e.trim()?e:null}export function resolveGamVendorConfiguration(e){const s=e.vendorConfiguration,o={identity:null,unknownTypes:[],duplicateGam:!1,sgaiAssetParameters:void 0,ssai:null};if(!Array.isArray(s))return{...o,reason:"absent"};const r=[];let a=null,i=0;for(const e of s){if("object"!=typeof e||null===e)continue;const s=e,n=t(s.type);null!==n&&("gam"!==n?r.includes(n)||r.push(n):(i+=1,null===a&&(a=s)))}const u=i>1;if(null===a)return{...o,reason:"absent",unknownTypes:r,duplicateGam:u};const l=t(a.networkCode);if(null===l)return{...o,reason:"missing-network-code",unknownTypes:r,duplicateGam:u};const c=Array.isArray(a.ssai)?a.ssai[0]:void 0,y=("object"==typeof c&&null!==c?t(c.assetKey):null)??t(a.assetKey),f=y?{networkCode:l,assetKey:y,assetParameters:n("object"==typeof c&&null!==c?c.assetParameters:void 0)}:null,m=Array.isArray(a.sgai)?a.sgai[0]:void 0,d=("object"==typeof m&&null!==m?t(m.customAssetKey):null)??t(a.customAssetKey);if(null===d)return{...o,reason:"missing-custom-asset-key",unknownTypes:r,duplicateGam:u,ssai:f};return{identity:{networkCode:l,customAssetKey:d},reason:"ok",unknownTypes:r,duplicateGam:u,sgaiAssetParameters:n("object"==typeof m&&null!==m?m.assetParameters:void 0),ssai:f}}function n(t){if("object"!=typeof t||null===t||Array.isArray(t))return;const n=s(e(t).filter(e=>"string"==typeof e[1]));return Object.keys(n).length>0?n:void 0}export function gamPodIdentityMismatches(e,s){const t=[];return s.networkCode!==e.networkCode&&t.push({field:"networkCode",assetValue:s.networkCode,sessionValue:e.networkCode}),s.customAssetKey!==e.customAssetKey&&t.push({field:"customAssetKey",assetValue:s.customAssetKey,sessionValue:e.customAssetKey}),t}export function manifestHasGamAssets(e){for(const s of e.breaks??[]){const e=[...s.variants??[],...s.variant?[s.variant]:[]];for(const s of e)for(const e of s?.assets??[])if("gam"===e?.vendor)return!0}return!1}
1
+ import{objectEntries as e,objectFromEntries as s}from"./runtimeObject";function t(e){return"string"==typeof e&&""!==e.trim()?e:null}export function resolveGamVendorConfiguration(e){const s=e.vendorConfiguration,o={identity:null,unknownTypes:[],duplicateGam:!1,sgaiAssetParameters:void 0,ssai:null};if(!Array.isArray(s))return{...o,reason:"absent"};const r=[];let a=null,i=0;for(const e of s){if("object"!=typeof e||null===e)continue;const s=e,n=t(s.type);null!==n&&("gam"!==n?"external"!==n&&(r.includes(n)||r.push(n)):(i+=1,null===a&&(a=s)))}const u=i>1;if(null===a)return{...o,reason:"absent",unknownTypes:r,duplicateGam:u};const l=t(a.networkCode);if(null===l)return{...o,reason:"missing-network-code",unknownTypes:r,duplicateGam:u};const c=Array.isArray(a.ssai)?a.ssai[0]:void 0,y=("object"==typeof c&&null!==c?t(c.assetKey):null)??t(a.assetKey),f=y?{networkCode:l,assetKey:y,assetParameters:n("object"==typeof c&&null!==c?c.assetParameters:void 0)}:null,m=Array.isArray(a.sgai)?a.sgai[0]:void 0,d=("object"==typeof m&&null!==m?t(m.customAssetKey):null)??t(a.customAssetKey);if(null===d)return{...o,reason:"missing-custom-asset-key",unknownTypes:r,duplicateGam:u,ssai:f};return{identity:{networkCode:l,customAssetKey:d},reason:"ok",unknownTypes:r,duplicateGam:u,sgaiAssetParameters:n("object"==typeof m&&null!==m?m.assetParameters:void 0),ssai:f}}function n(t){if("object"!=typeof t||null===t||Array.isArray(t))return;const n=s(e(t).filter(e=>"string"==typeof e[1]));return Object.keys(n).length>0?n:void 0}export function gamPodIdentityMismatches(e,s){const t=[];return s.networkCode!==e.networkCode&&t.push({field:"networkCode",assetValue:s.networkCode,sessionValue:e.networkCode}),s.customAssetKey!==e.customAssetKey&&t.push({field:"customAssetKey",assetValue:s.customAssetKey,sessionValue:e.customAssetKey}),t}export function manifestHasGamAssets(e){for(const s of e.breaks??[]){const e=[...s.variants??[],...s.variant?[s.variant]:[]];for(const s of e)for(const e of s?.assets??[])if("gam"===e?.vendor)return!0}return!1}
@@ -0,0 +1,42 @@
1
+ /**
2
+ * The live-window clamp on a resume target: a delivery constraint applied AFTER the
3
+ * conformance-locked resume policy, pulling the target back inside what the engine holds.
4
+ * Portable brain rule, locked by the conformance harness (`resume-clamp`).
5
+ *
6
+ * The end clamp answers which frames exist: a target beyond the published end is pulled to it.
7
+ * The start clamp answers which frames are still held: the live window slides during a break,
8
+ * and a target behind `seekable.start` is already evicted, so asking for it lands on the window
9
+ * start anyway and measures as an error. A gap larger than half the window cannot be eviction
10
+ * (the engine's seekable range does not match the adapter's timeline) and is left to the engine.
11
+ */
12
+ export declare const START_CLAMP_MAX_WINDOW_FRACTION = 0.5;
13
+ export interface ResumeClampInput {
14
+ /** The policy's resume target, seconds of currentTime. */
15
+ target: number;
16
+ /** Earliest position the engine still holds; null on VOD or when unreadable (no start clamp). */
17
+ safeStart: number | null;
18
+ /** Latest position a seek will stick; null on VOD or when unreadable (no end clamp). */
19
+ safeEnd: number | null;
20
+ /** Length of the held window; null when unreadable (the start clamp is then skipped). */
21
+ windowSec: number | null;
22
+ /**
23
+ * Seconds kept clear of the window start on a start clamp, so the aim is not evicted while the
24
+ * seek resolves (AVFoundation cancels a seek outside its seekable range outright). Default 0.
25
+ */
26
+ startMarginSec?: number;
27
+ }
28
+ export interface ResumeClamp {
29
+ /** The target to seek, after the clamp. */
30
+ target: number;
31
+ /** Which bound pulled the target, or null when it was left unchanged. */
32
+ bound: 'start' | 'end' | null;
33
+ /** How far the clamp moved the target, seconds (0 when unchanged). */
34
+ shortfallSec: number;
35
+ /** Set when the target was behind the window start by more than the clamp allows. */
36
+ startClampSkipped: {
37
+ behindSec: number;
38
+ windowSec: number | null;
39
+ maxBehindSec: number | null;
40
+ } | null;
41
+ }
42
+ export declare function resolveResumeClamp(input: ResumeClampInput): ResumeClamp;
@@ -0,0 +1 @@
1
+ export const START_CLAMP_MAX_WINDOW_FRACTION=.5;const e=e=>Math.round(1e3*e)/1e3;export function resolveResumeClamp(t){const{safeStart:n,safeEnd:l,windowSec:a}=t,o=Math.max(0,t.startMarginSec??0);let r=t.target,s=0,u=null;null!==l&&r>l&&(s=e(r-l),r=l,u="end");let c=null;if(null!==n&&r<n){const t=e(n-r),i=null===a?null:.5*a;if(null!==i&&t<=i){const t=null===l?n+o:Math.min(n+o,l);s=e(t-r),r=t,u="start"}else c={behindSec:t,windowSec:a,maxBehindSec:i}}return{target:r,bound:u,shortfallSec:s,startClampSkipped:c}}
@@ -1,4 +1,5 @@
1
1
  import { AssetUri, DeviceType } from '../types/BreakManifest';
2
+ import type { DeviceProfile, TargetingIssue } from './matchTargeting';
2
3
  /**
3
4
  * Resolve a `static`/`vast` asset's `uri` field (a plain string, or an
4
5
  * `AssetUri[]` where each entry may carry `targeting.deviceType`) to the single
@@ -19,4 +20,4 @@ import { AssetUri, DeviceType } from '../types/BreakManifest';
19
20
  * This is the single source of truth for `AdPlayerController.resolveUri` and
20
21
  * the native asset-URI resolvers.
21
22
  */
22
- export declare function resolveTargetedUri(uri: string | AssetUri[], deviceType?: DeviceType): string | null;
23
+ export declare function resolveTargetedUri(uri: string | AssetUri[], deviceType?: DeviceType | DeviceProfile, onTargetingIssue?: (issue: TargetingIssue) => void): string | null;
@@ -1 +1 @@
1
- export function resolveTargetedUri(e,r){if("string"==typeof e)return e;if(!Array.isArray(e)||0===e.length)return null;if(r){const t=e.find(e=>e.targeting?.deviceType===r);if(t)return t.value}const t=e.find(e=>void 0===e.targeting?.deviceType);return t?t.value:e[0].value}
1
+ import{matchTargeting as t}from"./matchTargeting";export function resolveTargetedUri(e,r,n){if("string"==typeof e)return e;if(!Array.isArray(e)||0===e.length)return null;const i="string"==typeof r?{deviceType:r}:r??{};for(const r of e){if(!r.targeting||0===Object.keys(r.targeting).length)continue;const e=t(r.targeting,i);if(e.issues.forEach(t=>n?.(t)),e.matches)return r.value}const g=e.find(t=>void 0===t.targeting||0===Object.keys(t.targeting).length);return g?g.value:e[0].value}
@@ -1,5 +1,6 @@
1
1
  import { BreakFormat } from '../types/BreakManifest';
2
2
  import type { Break, BreakVariant } from '../types/BreakManifest';
3
+ import type { DeviceProfile, TargetingIssue } from './matchTargeting';
3
4
  /**
4
5
  * Break-variant selection per the manifest spec's `targeting`, locked across the three cores by
5
6
  * the `variant-selection` conformance fixture. One ordered walk with two filters:
@@ -20,6 +21,8 @@ export declare const KNOWN_BREAK_FORMATS: readonly string[];
20
21
  export interface VariantSelectionInputs {
21
22
  /** Detected device class. Undefined = unknown: targeted variants never match, defaults still qualify. */
22
23
  deviceType?: string | undefined;
24
+ /** Full platform/OS profile; when omitted, selection remains device-type-only. */
25
+ deviceProfile?: DeviceProfile | undefined;
23
26
  /** Formats renderable in the CURRENT context (PiP narrows it, see {@link playableFormatsFor}). Omitted = all renderable. */
24
27
  playableFormats?: readonly string[] | undefined;
25
28
  }
@@ -35,6 +38,7 @@ export interface VariantSelection {
35
38
  * Carried on the result so the selector stays pure; each core diagnoses from its own layer.
36
39
  */
37
40
  skippedUnknownFormats: string[];
41
+ targetingIssues: TargetingIssue[];
38
42
  }
39
43
  /** The playable-format set for the web renderer's current context; native renderers mirror this table. */
40
44
  export declare function playableFormatsFor(context: {
@@ -1 +1 @@
1
- import{BreakFormat as e}from"../types/BreakManifest";export const RENDERABLE_BREAK_FORMATS=[e.SINGLE,e.DOUBLE,e.LSHAPE_AD,e.LSHAPE_CONTENT,e.OVERLAY];export const KNOWN_BREAK_FORMATS=[...RENDERABLE_BREAK_FORMATS];export function playableFormatsFor(t){return t.pictureInPicture?[e.SINGLE,e.LSHAPE_CONTENT]:t.singleSurfaceDevice?RENDERABLE_BREAK_FORMATS.filter(t=>t!==e.DOUBLE):RENDERABLE_BREAK_FORMATS}export function variantsOf(e){const t=e.variant;return Array.isArray(t)?t:t?[t]:[]}function t(e,t){const n=e.targeting?.deviceType;return void 0===n||void 0!==t&&n===t}export function selectVariantDetailed(e,n={}){const r=n.playableFormats??RENDERABLE_BREAK_FORMATS;let i=null,o=null;const E=[];for(const A of variantsOf(e)){if(!t(A,n.deviceType))continue;const e=A.format,a=void 0!==e&&KNOWN_BREAK_FORMATS.includes(e);if(a||E.push(e??""),null===o&&a&&(o=A),null===i&&void 0!==e&&r.includes(e)&&(i=A),null!==i)break}return{selected:i,deviceQualified:o,skippedUnknownFormats:E}}
1
+ import{BreakFormat as e}from"../types/BreakManifest";import{matchTargeting as t}from"./matchTargeting";export const RENDERABLE_BREAK_FORMATS=[e.SINGLE,e.DOUBLE,e.LSHAPE_AD,e.LSHAPE_CONTENT,e.OVERLAY];export const KNOWN_BREAK_FORMATS=[...RENDERABLE_BREAK_FORMATS];export function playableFormatsFor(t){return t.pictureInPicture?[e.SINGLE,e.LSHAPE_CONTENT]:t.singleSurfaceDevice?RENDERABLE_BREAK_FORMATS.filter(t=>t!==e.DOUBLE):RENDERABLE_BREAK_FORMATS}export function variantsOf(e){const t=e.variant;return Array.isArray(t)?t:t?[t]:[]}export function selectVariantDetailed(e,n={}){const r=n.playableFormats??RENDERABLE_BREAK_FORMATS;let i=null,o=null;const E=[],s=[],a=n.deviceProfile??(void 0===n.deviceType?{}:{deviceType:n.deviceType});for(const n of variantsOf(e)){const e=t(n.targeting,a);if(s.push(...e.issues),!e.matches)continue;const c=n.format,A=void 0!==c&&KNOWN_BREAK_FORMATS.includes(c);if(A||E.push(c??""),null===o&&A&&(o=n),null===i&&void 0!==c&&r.includes(c)&&(i=n),null!==i)break}return{selected:i,deviceQualified:o,skippedUnknownFormats:E,targetingIssues:s}}
@@ -0,0 +1 @@
1
+ export declare function toPlaybackError(mediaError: unknown, fallback: string): Error;
@@ -0,0 +1 @@
1
+ export function toPlaybackError(e,r){const o=e;if(o&&("number"==typeof o.code||o.message)){const e="number"==typeof o.code?` (code ${o.code})`:"",n=o.message?`: ${o.message}`:"";return new Error(`${r}${e}${n}`)}return new Error(r)}
@@ -0,0 +1,41 @@
1
+ /**
2
+ * A `wallclock` break's window on the content timeline, placed through the stream's own
3
+ * Program Date Time. Portable brain rule, locked by the conformance harness (`wallclock-break-window`).
4
+ *
5
+ * The manifest names the break in wallclock (`start`, `duration`); the player names positions in
6
+ * currentTime. One sample of both (`currentTime`, `programDateTime`) is the mapping in force, and
7
+ * under it the break starts at `currentTime - (programDateTime - start)` whenever the sample was
8
+ * taken — at the cut, or seconds later when content was only paused once the ad was ready. The
9
+ * resume point follows from the same mapping, so neither depends on when content stopped.
10
+ */
11
+ /** A content-player sample: a position and the stream's Program Date Time at that position. */
12
+ export interface ProgramDateTimeSample {
13
+ /** Content position, seconds of currentTime. */
14
+ currentTime: number;
15
+ /** The stream's Program Date Time at that position, epoch milliseconds. */
16
+ programDateTimeMs: number;
17
+ }
18
+ export interface WallclockBreakWindowInput {
19
+ /** The manifest `start`, epoch milliseconds. */
20
+ startMs: number;
21
+ /** The manifest `duration`, seconds. */
22
+ durationSec: number;
23
+ sample: ProgramDateTimeSample;
24
+ }
25
+ export interface WallclockBreakWindow {
26
+ /** Content time of the manifest start: the resume anchor. */
27
+ anchor: number;
28
+ /** Content time of `start + duration`: where content resumes. */
29
+ resumeTarget: number;
30
+ /** How far past the start the sample sits on the stream clock, seconds; negative before it. */
31
+ sampleLateSec: number;
32
+ }
33
+ /**
34
+ * Content cut this long after the manifest start on the stream clock is reported
35
+ * (DA-BREAK-CUT-LATE): the viewer saw the stream's own break instead of the ad for that long.
36
+ */
37
+ export declare const BREAK_CUT_LATE_TOLERANCE_SEC = 0.5;
38
+ /** Where a wallclock instant sits in currentTime under `sample`'s mapping, seconds (unrounded). */
39
+ export declare function contentTimeForWallclock(sample: ProgramDateTimeSample, wallclockMs: number): number;
40
+ /** Null when any input is not a finite number; otherwise every field at millisecond precision. */
41
+ export declare function resolveWallclockBreakWindow(input: WallclockBreakWindowInput): WallclockBreakWindow | null;
@@ -0,0 +1 @@
1
+ export const BREAK_CUT_LATE_TOLERANCE_SEC=.5;const e=e=>Math.round(1e3*e)/1e3;export function contentTimeForWallclock(e,r){return e.currentTime+(r-e.programDateTimeMs)/1e3}export function resolveWallclockBreakWindow(r){const{startMs:t,durationSec:o,sample:n}=r;if(![t,o,n.currentTime,n.programDateTimeMs].every(e=>Number.isFinite(e)))return null;const c=contentTimeForWallclock(n,t);return{anchor:e(c),resumeTarget:e(c+o),sampleLateSec:e((n.programDateTimeMs-t)/1e3)}}
@@ -1,4 +1,5 @@
1
1
  import { Break, BreakFormat } from '../types/BreakManifest';
2
+ import type { AdBreakSkipReason } from '../types/Events';
2
3
  import { BreakScheduler, BreakStartContext } from '../services/BreakScheduler';
3
4
  import { ManifestService } from '../services/ManifestService';
4
5
  import { AdPlayerController } from '../services/AdPlayerController';
@@ -36,6 +37,7 @@ export declare class BreakLifecycle {
36
37
  emitAdBreakBegin(brk: Break, context?: BreakStartContext): void;
37
38
  /** Flush a deferred (first-frame-gated) `adbreakbegin`, if one is pending. */
38
39
  flushPendingAdBreakBegin(): void;
40
+ onBreakSkipped(brk: Break, reason: AdBreakSkipReason): void;
39
41
  onBreakStart(breakInfo: Break, context: BreakStartContext): Promise<void>;
40
42
  /** A chained break begins (overlay held, content not resumed): emit `adbreakbegin`, promote it to ACTIVE, keep polling. */
41
43
  onChainedBreakBegin(next: Break): void;
@@ -1 +1 @@
1
- import{BreakFormat as e}from"../types/BreakManifest";import{playbackErrorContext as t}from"../diagnostics/playbackErrorContext";import{adErrorCode as a}from"../types/AdError";import{VendorAdError as i}from"../types/VendorAdError";export class BreakLifecycle{constructor(e,t){this.state=e,this.deps=t,this.breakBeginEmittedId=null,this.pendingAdBreakBegin=null}emitAdBreakBegin(e,t){this.breakBeginEmittedId!==e.id&&(this.breakBeginEmittedId=e.id,this.pendingAdBreakBegin=null,this.deps.emit("adbreakbegin",{type:"adbreakbegin",timestamp:Date.now(),break:e,format:this.deps.effectiveFormatOf(e),elapsedSec:t?.elapsedSec??0,remainingSec:t?.remainingSec??e.duration??0,...t?.tuneIn?{tuneIn:{elapsedSec:t.elapsedSec,remainingSec:t.remainingSec}}:{}}))}flushPendingAdBreakBegin(){this.pendingAdBreakBegin?.()}async onBreakStart(d,r){if(this.state.snapbackSeekInFlight=!1,this.deps.adPlayerController.isAdPlaying())return this.deps.diagnose("DA-BREAK-SUPPRESSED-OVERLAP",`Break suppressed (ad already playing): ${d.id}`,{breakId:d.id}),this.deps.breakScheduler.completeBreak(d.id),void this.deps.onBreakSettled?.();this.deps.adPlayerController.diagnoseUnknownVariantFormats(d);const s=this.deps.adPlayerController.breakNothingToPlayReason(d);if(s)return this.deps.log("Break not begun (no playable variant):",d.id,s),this.deps.diagnose("DA-BREAK-NO-PLAYABLE-ASSET",`break ${d.id} has no playable variant (${s}); it will not begin.`,{breakId:d.id,reason:s}),this.deps.emit("aderror",{type:"aderror",timestamp:Date.now(),source:"ad",break:d,format:this.deps.effectiveFormatOf(d),error:new Error(`Break ${d.id} has no playable variant (${s})`)}),this.deps.breakScheduler.completeBreak(d.id),void this.deps.onBreakSettled?.();this.deps.log("Break starting:",d.id,r.tuneIn?"(tune-in)":""),this.deps.breakTransitionTimer?.mark("into-break",d.id),this.breakBeginEmittedId=null,this.pendingAdBreakBegin=null;const n=this.state.heldImmediatePreRollId===d.id,o=this.deps.effectiveFormatOf(d),k=o===e.SINGLE||o===e.LSHAPE_AD,p=n&&k;n&&(this.state.heldImmediatePreRollId=null),this.deps.adBreakStatusTracker.setAdBreakStatusActive(d,void 0!==r.remainingSec?r.remainingSec:d.duration),p||this.emitAdBreakBegin(d,r),this.deps.manifestService.setActive(!0),this.deps.breakScheduler.claimChain(d.id);const h={onAdStart:(e,t,a,i,d)=>{this.deps.adBreakStatusTracker.onAdStarted(e,t,a,i),this.deps.emit("adbegin",{type:"adbegin",timestamp:Date.now(),break:e,format:this.deps.effectiveFormatOf(e),asset:t,adIndex:a,totalAds:i,...d?.adId?{adId:d.adId}:{},...d?.creativeId?{creativeId:d.creativeId}:{},...void 0!==d?.podPosition?{podPosition:d.podPosition}:{}})},onAdEnd:(e,t,a,i,d)=>{this.deps.adBreakStatusTracker.onAdEnded(e,a,i),a+1<i&&this.deps.breakTransitionTimer?.mark("ad-to-ad",e.id),this.deps.emit("adend",{type:"adend",timestamp:Date.now(),break:e,format:this.deps.effectiveFormatOf(e),asset:t,adIndex:a,totalAds:i,...d?.adId?{adId:d.adId}:{},...d?.creativeId?{creativeId:d.creativeId}:{}})},onAdError:(e,d,r)=>{this.flushPendingAdBreakBegin(),this.deps.diagnose("DA-AD-PLAYBACK-ERROR",r.message,{breakId:e.id,assetId:d?.id,adErrorCode:a(r),...r instanceof i?{vendorCode:r.vendorCode}:{},...t(r)}),this.deps.emit("aderror",{type:"aderror",timestamp:Date.now(),source:"ad",break:e,format:this.deps.effectiveFormatOf(e),...d?{asset:d}:{},error:r})},onAdTimeUpdate:(e,t,a,i)=>{this.deps.adBreakStatusTracker.onAdTimeUpdated(e,t,a,i),this.deps.emit("adtimeupdate",{type:"adtimeupdate",timestamp:Date.now(),break:e,format:this.deps.effectiveFormatOf(e),asset:t,currentTime:a,duration:i})},onAdWaiting:(e,t)=>{this.deps.adBreakStatusTracker.onAdPlaybackWaiting(),this.deps.emit("waiting",{type:"waiting",timestamp:Date.now(),source:"ad",break:e,asset:t})},onAdPlaying:(e,t)=>{this.deps.adBreakStatusTracker.onAdPlaybackPlaying(),this.deps.emit("playing",{type:"playing",timestamp:Date.now(),source:"ad",break:e,asset:t})},onBreakEnd:e=>{this.onBreakEnd(e)},onBreakCut:e=>{this.deps.cmcdAdReporter.onBreakCut(e.id)},onBreakEndedWithoutContentPause:()=>{this.onBreakEndedWithoutContentPause()},onChainedBreakBegin:e=>{this.onChainedBreakBegin(e)},onChainedBreakSkipped:e=>{this.deps.log("Chained break skipped (no playable variant):",e.id),this.deps.breakScheduler.completeBreak(e.id)},onContentResumeSeek:(e,t)=>{this.deps.breakScheduler.notifyResumeSeek(e,t)},onContentReloading:e=>{this.deps.breakScheduler.setContentReloading(e)}};p&&(this.pendingAdBreakBegin=()=>this.emitAdBreakBegin(d,r),h.onBreakVisible=e=>{this.deps.adBreakStatusTracker.setAdBreakVisible(e),this.flushPendingAdBreakBegin()}),await this.deps.adPlayerController.playBreak(d,h,{effectiveDurationSec:r.remainingSec,firstFrameGate:p})}onChainedBreakBegin(e){this.deps.log("Chained break starting:",e.id),this.deps.breakTransitionTimer?.mark("break-to-break",e.id),this.deps.breakScheduler.activateChainedBreak(e.id),this.deps.manifestService.setActive(!0),this.deps.adBreakStatusTracker.setAdBreakStatusActive(e,e.duration),this.emitAdBreakBegin(e)}onBreakEnd(e){this.deps.log("Break ended:",e.id),this.deps.breakTransitionTimer?.mark("out-of-break",e.id),this.deps.breakScheduler.completeBreak(e.id),this.deps.manifestService.setActive(!1),this.deps.adBreakStatusTracker.resetAdBreakStatus(),this.flushPendingAdBreakBegin(),this.deps.emit("adbreakend",{type:"adbreakend",timestamp:Date.now(),break:e,format:this.deps.effectiveFormatOf(e)}),this.deps.onBreakSettled?.()}onBreakEndedWithoutContentPause(){this.deps.breakTransitionTimer?.settle("out-of-break"),this.deps.emit("playing",{type:"playing",timestamp:Date.now(),source:"content"})}onBreakAborted(e){if(this.deps.adPlayerController.isAdPlaying()){const t=this.deps.adBreakStatusTracker.currentBreakElapsedSec();this.deps.log(`Early return — ending break '${e.id}' at ${t.toFixed(2)}s`),this.deps.cmcdAdReporter.onBreakAborted(e.id),this.deps.adPlayerController.abortBreak(t)}else this.deps.breakScheduler.completeBreak(e.id),this.deps.onBreakSettled?.()}reportAbandonedBreak(e){const t=this.deps.adBreakStatusTracker.activeBreak;if(!t)return;const a=this.deps.adBreakStatusTracker.currentAdIndex;this.deps.diagnose("DA-BREAK-ABANDONED",`Session torn down (${e}) while break ${t.id} was still in flight`+(null!==a?"; an ad began and will never end.":"."),{breakId:t.id,cause:e,phase:this.deps.adBreakStatusTracker.adBreakStatus.phase,unbalancedAdBegin:null!==a,...null!==a?{adIndex:a}:{},...null!==this.deps.adBreakStatusTracker.currentAdTotal?{totalAds:this.deps.adBreakStatusTracker.currentAdTotal}:{}})}}
1
+ import{BreakFormat as e}from"../types/BreakManifest";import{playbackErrorContext as t}from"../diagnostics/playbackErrorContext";import{adErrorCode as a}from"../types/AdError";import{VendorAdError as i}from"../types/VendorAdError";export class BreakLifecycle{constructor(e,t){this.state=e,this.deps=t,this.breakBeginEmittedId=null,this.pendingAdBreakBegin=null}emitAdBreakBegin(e,t){this.breakBeginEmittedId!==e.id&&(this.breakBeginEmittedId=e.id,this.pendingAdBreakBegin=null,this.deps.emit("adbreakbegin",{type:"adbreakbegin",timestamp:Date.now(),break:e,format:this.deps.effectiveFormatOf(e),elapsedSec:t?.elapsedSec??0,remainingSec:t?.remainingSec??e.duration??0,...t?.tuneIn?{tuneIn:{elapsedSec:t.elapsedSec,remainingSec:t.remainingSec}}:{}}))}flushPendingAdBreakBegin(){this.pendingAdBreakBegin?.()}onBreakSkipped(e,t){this.deps.emit("adbreakskipped",{type:"adbreakskipped",timestamp:Date.now(),break:e,reason:t,format:this.deps.effectiveFormatOf(e)})}async onBreakStart(r,d){const s=this.state.sessionGeneration;if(this.state.snapbackSeekInFlight=!1,this.deps.adPlayerController.isAdPlaying()){if(this.deps.diagnose("DA-BREAK-SUPPRESSED-OVERLAP",`Break suppressed (ad already playing): ${r.id}`,{breakId:r.id}),s!==this.state.sessionGeneration)return;return this.deps.breakScheduler.completeBreak(r.id),this.onBreakSkipped(r,"overlap"),void this.deps.onBreakSettled?.()}this.deps.adPlayerController.diagnoseUnknownVariantFormats(r);const n=this.deps.adPlayerController.breakNothingToPlayReason(r);if(n){if(this.deps.log("Break not begun (no playable variant):",r.id,n),this.deps.diagnose("DA-BREAK-NO-PLAYABLE-ASSET",`break ${r.id} has no playable variant (${n}); it will not begin.`,{breakId:r.id,reason:n}),this.deps.emit("aderror",{type:"aderror",timestamp:Date.now(),source:"ad",break:r,format:this.deps.effectiveFormatOf(r),error:new Error(`Break ${r.id} has no playable variant (${n})`)}),s!==this.state.sessionGeneration)return;return this.deps.breakScheduler.completeBreak(r.id),this.onBreakSkipped(r,"unsupported"),void this.deps.onBreakSettled?.()}this.deps.log("Break starting:",r.id,d.tuneIn?"(tune-in)":""),this.deps.breakTransitionTimer?.mark("into-break",r.id),this.breakBeginEmittedId=null,this.pendingAdBreakBegin=null;const o=this.state.heldImmediatePreRollId===r.id,p=this.deps.effectiveFormatOf(r),k=p===e.SINGLE||p===e.LSHAPE_AD,h=o&&k;o&&(this.state.heldImmediatePreRollId=null),this.deps.adBreakStatusTracker.setAdBreakStatusActive(r,void 0!==d.remainingSec?d.remainingSec:r.duration),h||this.emitAdBreakBegin(r,d),this.deps.manifestService.setActive(!0),this.deps.breakScheduler.claimChain(r.id);const l={onAdStart:(e,t,a,i,r)=>{this.deps.adBreakStatusTracker.onAdStarted(e,t,a,i),this.deps.emit("adbegin",{type:"adbegin",timestamp:Date.now(),break:e,format:this.deps.effectiveFormatOf(e),asset:t,adIndex:a,totalAds:i,...r?.adId?{adId:r.adId}:{},...r?.creativeId?{creativeId:r.creativeId}:{},...void 0!==r?.podPosition?{podPosition:r.podPosition}:{}})},onAdEnd:(e,t,a,i,r)=>{this.deps.adBreakStatusTracker.onAdEnded(e,a,i),a+1<i&&this.deps.breakTransitionTimer?.mark("ad-to-ad",e.id),this.deps.emit("adend",{type:"adend",timestamp:Date.now(),break:e,format:this.deps.effectiveFormatOf(e),asset:t,adIndex:a,totalAds:i,...r?.adId?{adId:r.adId}:{},...r?.creativeId?{creativeId:r.creativeId}:{}})},onAdError:(e,r,d)=>{this.flushPendingAdBreakBegin(),this.deps.diagnose("DA-AD-PLAYBACK-ERROR",d.message,{breakId:e.id,assetId:r?.id,adErrorCode:a(d),...d instanceof i?{vendorCode:d.vendorCode}:{},...t(d)}),this.deps.emit("aderror",{type:"aderror",timestamp:Date.now(),source:"ad",break:e,format:this.deps.effectiveFormatOf(e),...r?{asset:r}:{},error:d})},onAdTimeUpdate:(e,t,a,i)=>{this.deps.adBreakStatusTracker.onAdTimeUpdated(e,t,a,i),this.deps.emit("adtimeupdate",{type:"adtimeupdate",timestamp:Date.now(),break:e,format:this.deps.effectiveFormatOf(e),asset:t,currentTime:a,duration:i})},onAdWaiting:(e,t)=>{this.deps.adBreakStatusTracker.onAdPlaybackWaiting(),this.deps.emit("waiting",{type:"waiting",timestamp:Date.now(),source:"ad",break:e,asset:t})},onAdPlaying:(e,t)=>{this.deps.adBreakStatusTracker.onAdPlaybackPlaying(),this.deps.emit("playing",{type:"playing",timestamp:Date.now(),source:"ad",break:e,asset:t})},onBreakEnd:e=>{this.onBreakEnd(e)},onBreakCut:e=>{this.deps.cmcdAdReporter.onBreakCut(e.id)},onBreakEndedWithoutContentPause:()=>{this.onBreakEndedWithoutContentPause()},onChainedBreakBegin:e=>{this.onChainedBreakBegin(e)},onChainedBreakSkipped:e=>{s===this.state.sessionGeneration&&(this.deps.log("Chained break skipped (no playable variant):",e.id),this.deps.breakScheduler.completeBreak(e.id),this.onBreakSkipped(e,"unsupported"))},onContentResumeSeek:(e,t)=>{this.deps.breakScheduler.notifyResumeSeek(e,t)},onContentReloading:e=>{this.deps.breakScheduler.setContentReloading(e)}};h&&(this.pendingAdBreakBegin=()=>this.emitAdBreakBegin(r,d),l.onBreakVisible=e=>{this.deps.adBreakStatusTracker.setAdBreakVisible(e),this.flushPendingAdBreakBegin()}),await this.deps.adPlayerController.playBreak(r,l,{effectiveDurationSec:d.remainingSec,firstFrameGate:h})}onChainedBreakBegin(e){this.deps.log("Chained break starting:",e.id),this.deps.breakTransitionTimer?.mark("break-to-break",e.id),this.deps.breakScheduler.activateChainedBreak(e.id),this.deps.manifestService.setActive(!0),this.deps.adBreakStatusTracker.setAdBreakStatusActive(e,e.duration),this.emitAdBreakBegin(e)}onBreakEnd(e){this.deps.log("Break ended:",e.id),this.deps.breakTransitionTimer?.mark("out-of-break",e.id),this.deps.breakScheduler.completeBreak(e.id),this.deps.manifestService.setActive(!1),this.deps.adBreakStatusTracker.resetAdBreakStatus(),this.flushPendingAdBreakBegin(),this.deps.emit("adbreakend",{type:"adbreakend",timestamp:Date.now(),break:e,format:this.deps.effectiveFormatOf(e)}),this.deps.onBreakSettled?.()}onBreakEndedWithoutContentPause(){this.deps.breakTransitionTimer?.settle("out-of-break"),this.deps.emit("playing",{type:"playing",timestamp:Date.now(),source:"content"})}onBreakAborted(e){if(this.deps.adPlayerController.isAdPlaying()){const t=this.deps.adBreakStatusTracker.currentBreakElapsedSec();this.deps.log(`Early return — ending break '${e.id}' at ${t.toFixed(2)}s`),this.deps.cmcdAdReporter.onBreakAborted(e.id),this.deps.adPlayerController.abortBreak(t)}else this.deps.breakScheduler.completeBreak(e.id),this.deps.onBreakSettled?.()}reportAbandonedBreak(e){const t=this.deps.adBreakStatusTracker.activeBreak;if(!t)return;const a=this.deps.adBreakStatusTracker.currentAdIndex;this.deps.diagnose("DA-BREAK-ABANDONED",`Session torn down (${e}) while break ${t.id} was still in flight`+(null!==a?"; an ad began and will never end.":"."),{breakId:t.id,cause:e,phase:this.deps.adBreakStatusTracker.adBreakStatus.phase,unbalancedAdBegin:null!==a,...null!==a?{adIndex:a}:{},...null!==this.deps.adBreakStatusTracker.currentAdTotal?{totalAds:this.deps.adBreakStatusTracker.currentAdTotal}:{}})}}
@@ -1,5 +1,6 @@
1
1
  import { PlayerAdapter } from '../interfaces/PlayerAdapter';
2
2
  import { BreakManifest } from '../types/BreakManifest';
3
+ import { ExternalVendorConfigurationTracker } from '../services/ExternalVendorConfigurationTracker';
3
4
  import { OptiViewAdsMode, SessionConfig, SsaiSessionConfig, StartSessionConfig } from '../types/OptiViewAdsConfig';
4
5
  import { ManifestService } from '../services/ManifestService';
5
6
  import { BreakScheduler } from '../services/BreakScheduler';
@@ -25,6 +26,7 @@ export interface SessionStarterDeps {
25
26
  cmcdAdReporter: CmcdAdReporter;
26
27
  lensReporter: LensReporter;
27
28
  adBreakStatusTracker: AdBreakStatusTracker;
29
+ externalVendorConfigurationTracker: ExternalVendorConfigurationTracker;
28
30
  emit: EmitEvent;
29
31
  diagnose: Diagnose;
30
32
  log: Log;
@@ -94,6 +96,8 @@ export declare class SessionStarter {
94
96
  */
95
97
  private switchChannel;
96
98
  private emitChannelChange;
99
+ private adoptExternalVendorConfigurations;
100
+ private diagnoseExternalVendorConfigurations;
97
101
  private applySameModeChannelSwitch;
98
102
  private switchToSteeredSsai;
99
103
  private switchToSgai;
@@ -101,7 +105,8 @@ export declare class SessionStarter {
101
105
  * Resolve the GAM stream identity from the manifest and open the IMA/DAI session. GAM failure
102
106
  * is non-fatal: non-GAM breaks still schedule, only the GAM assets are skipped.
103
107
  */
104
- startGamForManifest(manifest: BreakManifest, sessionParams: Record<string, string> | undefined, startStep: (step: string) => void): Promise<void>;
108
+ startGamForManifest(manifest: BreakManifest, sessionParams: Record<string, string> | undefined, startStep: (step: string) => void, generation?: number, initialStart?: boolean): Promise<void>;
109
+ private startResolvedGamSession;
105
110
  /** SSAI session: create the IMA DAI stream, load the stitched master, wire timed-metadata forwarding. Throws on failure. */
106
111
  startSsaiSession(config: SsaiSessionConfig, generation: number): Promise<void>;
107
112
  /** Whether a startSession() begun at {@link generation} was invalidated by a later end/destroy/restart. */
@@ -1 +1 @@
1
- import{getPreRollDelay as e}from"../types/BreakManifest";import{manifestHasGamAssets as s,resolveGamVendorConfiguration as t}from"../services/resolveGamVendorConfiguration";import{SsaiController as i}from"../services/SsaiController";import{resolveUrlOrigin as a}from"../services/urlOrigin";import{filterSsaiClientBreaks as r,resolveStitchedStreamVendor as n,SsaiUnavailableError as o}from"../services/resolveDeliveryMode";import{detectDeviceType as d}from"../services/detectDeviceType";import{decideDeliveryMode as l,detectChannelSwitch as h,gamPodIdentityChanged as c}from"../services/channelSwitch";import{prepareWebRuntime as m}from"../services/webCompatibility";const p=1e4;function S(e){return"stitcherUrl"in e}function u(e){return"string"==typeof e&&""!==e.trim()}export class SessionStarter{constructor(e,s){this.state=e,this.deps=s}async startSession(s){if("ssai"===this.deps.mode){if(!S(s)||!u(s.stitcherUrl))throw new Error('[OptiViewAds] mode "ssai" requires a stitcherUrl in startSession().')}else if(S(s)||!u(s.manifestUrl))throw new Error("[OptiViewAds] startSession requires a manifestUrl (the full break-manifest URL).");this.state.sessionActive&&(this.deps.log("Ending previous session before starting new one"),this.deps.endSessionInternal("session-restart"));const t=this.state.sessionGeneration;this.state.sessionActive=!0;try{const e=m();e&&await e}catch(e){if(this.isStaleStart(t))return;throw this.state.sessionActive=!1,e}if(this.isStaleStart(t))return;if(this.deps.lensReporter.startSession(),S(s))return s.adInsertionType&&this.deps.diagnose("DA-INSERTION-TYPE-IGNORED-SSAI",`adInsertionType "${s.adInsertionType}" ignored in SSAI mode; the stitcher controls ad insertion.`,{adInsertionType:s.adInsertionType}),void await this.startSsaiSession(s,t);const i=s.manifestUrl;this.state.sessionConfig=s,this.state.manifestOrigin=a(i),this.deps.manifestService.setManifestUrl(i),this.deps.log("Session starting, manifest URL:",i);const r=Date.now(),n=e=>{this.deps.diagnose("DA-SESSION-STARTING",`Session start: ${e}`,{step:e,manifestUrl:i,elapsedMs:Date.now()-r})};n("begin");const o=s.assetParameters;let d;this.deps.adPlayerController.setSessionAssetParameters(o),this.deps.adPlayerController.setSessionAssetParameterMacros(s.assetParameterMacros),this.state.configSummary.gam&&(this.state.configSummary.gam={...this.state.configSummary.gam,hasAssetParameters:!!o}),n("manifest-fetch");try{d=await this.deps.manifestService.fetchManifest()}catch(e){if(this.isStaleStart(t))return;throw this.failSessionStart(e)}if(this.deps.cmcdAdReporter.onSessionStart(d),!this.isStaleStart(t))if(this.state.sessionManifest=d,"ssai"!==this.resolveSessionDeliveryMode(d))try{if(await this.startGamForManifest(d,o,n),this.isStaleStart(t))return;const a=s.adInsertionType;this.deps.adPlayerController.setBreakResumePolicy(a,d.timebase,this.deps.continueContentDuringBreak),this.deps.diagnose("DA-INSERTION-TYPE-RESOLVED",void 0===a?`Ad insertion type: unset — the manifest's resumeOffset drives the resume point (timebase: ${d.timebase})`:`Ad insertion type: ${a}, client-set (timebase: ${d.timebase})`,{adInsertionType:a??null,source:void 0===a?"unset":"client",timebase:d.timebase}),n("scheduling"),this.deps.breakScheduler.updateManifest(d),this.deps.pauseAdController.updateManifest(d),this.state.pauseAdTickInterval=setInterval(()=>this.deps.pauseAdController?.tick(),250);const r=(d.adStartDelay??0)>0?void 0:d.breaks.find(s=>0===e(s));r?(this.deps.adPlayerController.holdContentForPreRoll(r),this.state.heldImmediatePreRollId=r.id,this.state.heldPreRollArmed=!1,this.state.heldPreRollWaitStartedAt=Date.now(),this.contentIsReady()?this.armHeldImmediatePreRoll():this.state.heldPreRollArmInterval=setInterval(()=>{this.contentIsReady()?this.armHeldImmediatePreRoll():this.armHeldImmediatePreRollIfTimedOut()},250)):this.markPlaybackStartIfPlaying(),this.deps.startLivePushback(),this.startBreakScheduler(),this.startManifestPolling(),this.deps.diagnose("DA-SESSION-STARTED",`Session active for manifest ${i}`,{manifestUrl:i}),this.deps.log("Session active")}catch(e){if(this.isStaleStart(t))return;throw this.failSessionStart(e)}else await this.startSteeredSsaiSession(d,s,n)}markPlaybackStartIfPlaying(){this.deps.player.paused||this.deps.breakScheduler.markPlaybackStart()}contentIsReady(){return!Number.isNaN(this.deps.player.duration)||!this.deps.player.paused}clearHeldImmediatePreRollArmInterval(){this.state.heldPreRollArmInterval&&(clearInterval(this.state.heldPreRollArmInterval),this.state.heldPreRollArmInterval=null)}armHeldImmediatePreRoll(){this.state.heldImmediatePreRollId&&!this.state.heldPreRollArmed&&(this.clearHeldImmediatePreRollArmInterval(),this.state.heldPreRollArmed=!0,this.deps.breakScheduler.markPlaybackStart())}armHeldImmediatePreRollIfTimedOut(){if(!this.state.heldImmediatePreRollId||this.state.heldPreRollArmed)return;const e=this.state.heldPreRollWaitStartedAt;if(null===e||Date.now()-e<p)return;const s=this.state.heldImmediatePreRollId;this.armHeldImmediatePreRoll(),this.deps.diagnose("DA-PREROLL-CONTENT-WAIT-TIMEOUT","Pending pre-roll began after waiting 10000ms for content readiness.",{breakId:s,waitMs:p})}cancelHeldImmediatePreRollOnContentError(){const e=this.state.heldImmediatePreRollId;return!(!e||this.state.heldPreRollArmed)&&(this.clearHeldImmediatePreRollArmInterval(),this.deps.breakScheduler?.completeBreak(e),this.deps.adPlayerController?.releaseContentHold(),this.state.heldImmediatePreRollId=null,this.state.heldPreRollArmed=!1,this.state.heldPreRollWaitStartedAt=null,this.deps.diagnose("DA-PREROLL-CANCELLED-CONTENT-ERROR","A pending pre-roll was cancelled because the content failed to load before it could begin.",{breakId:e}),!0)}clearHeldImmediatePreRollArm(){this.clearHeldImmediatePreRollArmInterval(),this.state.heldImmediatePreRollId=null,this.state.heldPreRollArmed=!1,this.state.heldPreRollWaitStartedAt=null}failSessionStart(e){const s=e instanceof Error?e:new Error(String(e)),t=this.deps.manifestService?.takeLastFailure();return this.deps.diagnose("DA-MANIFEST-FETCH-FAILED",s.message,t?{...t}:void 0),this.state.sessionActive=!1,this.deps.cmcdAdReporter.onSessionEnd(),this.deps.lensReporter.endSession(),s}resolveSessionDeliveryMode(e,s="manifest"){const t=this.state.sessionDeliveryMode,i=l(e,d(),this.deps.hasExplicitMode?this.deps.mode:null,s);return null!==i.unknownMode&&this.deps.diagnose("DA-DELIVERY-MODE-UNKNOWN",`Delivery rule ${i.ruleIndex} carries unrecognized mode "${i.unknownMode}"; using sgai.`,{unknownMode:i.unknownMode,ruleIndex:i.ruleIndex}),this.deps.hasExplicitMode&&i.manifestMode!==i.mode&&this.deps.diagnose("DA-DELIVERY-MODE-OVERRIDDEN",`Manifest delivery rules resolve "${i.manifestMode}" but the explicit config mode "${i.mode}" takes precedence.`,{manifestMode:i.manifestMode,configuredMode:i.mode}),this.state.sessionDeliveryMode=i.mode,this.deps.diagnose("DA-DELIVERY-MODE-RESOLVED",`Delivery mode: ${i.mode}`,{mode:i.mode,ruleIndex:i.ruleIndex,source:i.source,..."channel-switch"===s?{previousMode:t}:{}}),i.mode}async startSteeredSsaiSession(e,s,t){const i=n(e.vendorConfiguration);if(!i){const e="Manifest selected ssai delivery but no vendor configuration provides a stitched stream (no gam entry with an assetKey).";throw this.deps.diagnose("DA-SSAI-UNAVAILABLE",e),this.deps.manifestService.reset(),this.state.sessionActive=!1,new o(e)}try{t("ssai-live-stream"),this.state.steeredSsaiActive=!0,this.state.ssaiController=this.createSsaiController(i.networkCode),await this.state.ssaiController.startSteered(i,s.assetParameters,s.assetParameterMacros),t("scheduling");const a=r(e);this.deps.adPlayerController.setBreakResumePolicy(s.adInsertionType,e.timebase,!1),this.deps.breakScheduler.updateManifest(a),this.deps.pauseAdController.updateManifest(a),this.state.pauseAdTickInterval=setInterval(()=>this.deps.pauseAdController?.tick(),250),this.markPlaybackStartIfPlaying(),this.deps.startLivePushback(),this.startBreakScheduler(),this.startManifestPolling(),this.deps.diagnose("DA-SESSION-STARTED",`SSAI session active (manifest-steered) for ${s.manifestUrl}`,{manifestUrl:s.manifestUrl}),this.deps.log("SSAI session active (manifest-steered)")}catch(e){const s=e instanceof Error?e:new Error(String(e));throw this.deps.diagnose("DA-SSAI-SESSION-FAILED",s.message),this.state.steeredSsaiActive=!1,this.deps.manifestService.reset(),this.state.sessionActive=!1,s}}createSsaiController(e){return new i(this.deps.player,{gam:this.state.steeredSsaiOptions?.streamActivityMonitorId?{streamActivityMonitorId:this.state.steeredSsaiOptions.streamActivityMonitorId}:{},ssai:{networkCode:e},maxBitrate:this.state.steeredSsaiOptions?.maxBitrate,autoplay:this.state.steeredSsaiOptions?.autoplay,debug:this.deps.debug},{emit:(e,s)=>this.deps.emit(e,s),diagnose:(e,s,t)=>this.deps.diagnose(e,s,t),log:(...e)=>this.deps.log(...e)})}startBreakScheduler(){this.deps.breakScheduler.start((e,s)=>{this.deps.breakLifecycle.onBreakStart(e,s)},e=>!this.deps.adPlayerController.isAdPlaying()&&(this.deps.adPlayerController.preload(e).catch(s=>{this.deps.diagnose("DA-PRELOAD-FAILED",s instanceof Error?s.message:String(s),{breakId:e.id})}),!0),(e,s,t)=>{this.deps.adBreakStatusTracker.onBreakWarning(e,s,t)},e=>{this.deps.breakLifecycle.onBreakAborted(e)})}startManifestPolling(){this.deps.manifestService.startPolling(e=>{if(!this.state.sessionActive)return;const s=this.deps.notedManifestBreakIds(e);this.deps.diagnose("DA-MANIFEST-POLLED","Manifest polled",{breaks:e.breaks.length,newBreakIds:s,pollingIntervalSeconds:this.deps.manifestService.currentPollingIntervalSeconds});const t=this.state.sessionManifest,i=t?h(t,e):null;if(this.state.pendingChannelSwitch||i&&this.deps.breakScheduler.getActiveBreak())return this.state.pendingChannelSwitch=e,void this.deps.log("Channel switch deferred until the active break ends");i?this.switchChannel(e,i):this.applyManifestUpdate(e)},e=>{this.state.sessionActive&&this.deps.diagnose("DA-MANIFEST-POLL-FAILED",e.message)})}applyPendingChannelSwitch(){if(!this.state.sessionActive||!this.state.pendingChannelSwitch)return;const e=this.state.pendingChannelSwitch;this.state.pendingChannelSwitch=null;const s=h(this.state.sessionManifest,e);s?this.switchChannel(e,s):this.applyManifestUpdate(e)}applyManifestUpdate(e){this.diagnoseIgnoredDeliveryModeChange(e);const s=t(e);this.state.steeredSsaiActive?this.state.ssaiController?.setManifestAssetParameters(s.ssai?.assetParameters):this.deps.adPlayerController.setManifestAssetParameters(s.sgaiAssetParameters);const i=this.state.steeredSsaiActive?r(e):e;this.deps.breakScheduler.updateManifest(i),this.deps.pauseAdController.updateManifest(i)}diagnoseIgnoredDeliveryModeChange(e){if(!this.state.sessionDeliveryMode)return;const s=l(e,d(),null,"manifest");s.manifestMode!==this.state.sessionDeliveryMode&&this.deps.diagnose("DA-DELIVERY-MODE-CHANGE-IGNORED",`Polled manifest resolves delivery mode "${s.manifestMode}" but "${this.state.sessionDeliveryMode}" is pinned for this session.`,{polledMode:s.manifestMode,pinnedMode:this.state.sessionDeliveryMode})}async switchChannel(e,s){if(this.state.channelSwitchInFlight)return;const t=this.state.sessionConfig,i=this.state.sessionManifest,a={previousChannelId:s.previousChannelId,channelId:s.channelId,reason:s.reason,manifestUrl:t.manifestUrl};this.deps.diagnose("DA-MANIFEST-CHANNEL-CHANGED",`Manifest channel changed (${s.reason}): ${s.previousChannelId??"none"} -> ${s.channelId??"none"}`,a),this.state.sessionManifest=e,this.state.channelSwitchInFlight=!0;const r=this.state.sessionGeneration,n=this.state.sessionDeliveryMode??"sgai";try{let s=this.resolveSessionDeliveryMode(e,"channel-switch");if(this.deps.cmcdAdReporter.onSessionEnd(),this.deps.cmcdAdReporter.onSessionStart(e),this.deps.lensReporter.endSession(),this.deps.lensReporter.startSession(),"ssai"===s&&"sgai"===n){const a=await this.switchToSteeredSsai(e,t);if(this.isStaleStart(r))return;a||(s="sgai",this.state.sessionDeliveryMode="sgai",await this.applySameModeChannelSwitch(i,e,t,r))}else"sgai"===s&&"ssai"===n?await this.switchToSgai(e,t,r):await this.applySameModeChannelSwitch(i,e,t,r);if(this.isStaleStart(r))return}finally{this.state.channelSwitchInFlight=!1}this.deps.startLivePushback(),this.emitChannelChange({...a,previousDeliveryMode:n,deliveryMode:this.state.sessionDeliveryMode??n})}emitChannelChange(e){this.deps.emit("adchannelchange",{type:"adchannelchange",timestamp:Date.now(),...e})}async applySameModeChannelSwitch(e,s,i,a){const n=this.state.steeredSsaiActive,o=n?r(s):s;if(this.deps.breakScheduler.resetForChannelSwitch(o),this.deps.pauseAdController.updateManifest(o),this.deps.adPlayerController.setBreakResumePolicy(i.adInsertionType,s.timebase,this.deps.continueContentDuringBreak),!n&&c(e,s)){const e=this.deps.adPlayerController.updatedAssetParameters,t=this.deps.adPlayerController.sessionAssetParameterMacros;if(this.deps.adPlayerController.endGamSession(),this.deps.adPlayerController.setSessionAssetParameters(i.assetParameters),this.deps.adPlayerController.setSessionAssetParameterMacros(t),await this.startGamForManifest(s,i.assetParameters,e=>{this.deps.diagnose("DA-SESSION-STARTING",`Channel switch: ${e}`,{step:e,manifestUrl:i.manifestUrl})}),this.isStaleStart(a))return;e&&this.deps.adPlayerController.updateAssetParameters(e)}else if(n){const e=t(s);this.state.ssaiController?.setManifestAssetParameters(e.ssai?.assetParameters)}else{const e=t(s);this.deps.adPlayerController.setManifestAssetParameters(e.sgaiAssetParameters)}}async switchToSteeredSsai(e,s){const t=n(e.vendorConfiguration);if(!t)return this.deps.diagnose("DA-SSAI-UNAVAILABLE","Channel switch resolved ssai delivery but no vendor configuration provides a stitched stream; the session stays in sgai.",{manifestUrl:s.manifestUrl,previousMode:"sgai"}),!1;const i=this.deps.adPlayerController,a=i.updatedAssetParameters,o=i.sessionAssetParameterMacros;i.stopActiveBreak(),i.releaseContentHold(),i.endGamSession(),i.setSessionAssetParameters(s.assetParameters),i.setSessionAssetParameterMacros(o),a&&i.updateAssetParameters(a);const d=this.createSsaiController(t.networkCode);this.state.ssaiController=d,this.state.steeredSsaiActive=!0,this.deps.diagnose("DA-SESSION-STARTING","Channel switch: ssai-live-stream",{step:"ssai-live-stream",manifestUrl:s.manifestUrl});try{await d.startSteered(t,s.assetParameters,o),a&&d.updateAssetParameters(a);const n=r(e);return this.deps.breakScheduler.resetForChannelSwitch(n),this.deps.pauseAdController.updateManifest(n),i.setBreakResumePolicy(s.adInsertionType,e.timebase,!1),!0}catch(e){const s=e instanceof Error?e.message:String(e);return this.deps.diagnose("DA-SSAI-SESSION-FAILED",s),d.stop(),delete this.state.ssaiController,this.state.steeredSsaiActive=!1,!1}}async switchToSgai(e,s,t){const i=this.state.ssaiController?.sessionAssetParameterMacros;this.state.ssaiController?.stop(),delete this.state.ssaiController,this.state.steeredSsaiActive=!1,this.deps.breakScheduler.resetForChannelSwitch(e),this.deps.pauseAdController.updateManifest(e);const a=this.deps.adPlayerController;a.setBreakResumePolicy(s.adInsertionType,e.timebase,this.deps.continueContentDuringBreak);const r=a.updatedAssetParameters;a.endGamSession(),a.setSessionAssetParameters(s.assetParameters),a.setSessionAssetParameterMacros(i??s.assetParameterMacros),await this.startGamForManifest(e,s.assetParameters,e=>{this.deps.diagnose("DA-SESSION-STARTING",`Channel switch: ${e}`,{step:e,manifestUrl:s.manifestUrl})}),this.isStaleStart(t)||r&&a.updateAssetParameters(r)}async startGamForManifest(e,i,a){const r=t(e);if(this.deps.adPlayerController.setManifestAssetParameters(r.sgaiAssetParameters),r.unknownTypes.length>0&&this.deps.diagnose("DA-VENDOR-CONFIG-UNKNOWN-TYPE",`Ignored ${r.unknownTypes.length} vendorConfiguration `+(1===r.unknownTypes.length?"entry":"entries")+" with unrecognised type: "+`${r.unknownTypes.join(", ")}.`,{types:r.unknownTypes}),this.deps.adPlayerController.isGamEnabled())if(r.identity){this.deps.diagnose("DA-GAM-CONFIG-RESOLVED",`GAM session identity: network ${r.identity.networkCode}, custom asset ${r.identity.customAssetKey}`,{networkCode:r.identity.networkCode,customAssetKey:r.identity.customAssetKey,channelId:e.channelId,duplicateEntriesIgnored:r.duplicateGam});try{a("gam-init"),await this.deps.adPlayerController.startGamSession(r.identity,i),this.deps.log("GAM session started")}catch(e){this.deps.diagnose("DA-GAM-SESSION-FAILED",e instanceof Error?e.message:String(e))}}else s(e)&&this.deps.diagnose("DA-GAM-CONFIG-MISSING",'The break manifest carries GAM pod assets but no usable "gam" vendorConfiguration entry; those assets will be skipped.',{reason:r.reason});else s(e)&&this.deps.diagnose("DA-GAM-CONFIG-MISSING","The break manifest carries GAM pod assets but GAM is disabled for this session (gam.enabled === false); those assets will be skipped.",{reason:"gam-disabled"})}async startSsaiSession(e,s){try{if(this.state.stitcherOrigin=a(e.stitcherUrl),this.state.configSummary.gam&&(this.state.configSummary.gam={...this.state.configSummary.gam,hasAssetParameters:!!e.assetParameters}),await this.state.ssaiController.start(e),this.isStaleStart(s))return;this.deps.diagnose("DA-SESSION-STARTED",`SSAI session active for ${e.stitcherUrl}`,{stitcherUrl:e.stitcherUrl}),this.deps.startLivePushback(),this.deps.log("SSAI session active")}catch(e){if(this.isStaleStart(s))return;const t=e instanceof Error?e:new Error(String(e));throw this.deps.diagnose("DA-SSAI-SESSION-FAILED",t.message),this.state.sessionActive=!1,this.deps.cmcdAdReporter.onSessionEnd(),this.deps.lensReporter.endSession(),t}}isStaleStart(e){return e!==this.state.sessionGeneration&&(this.deps.log("Session start aborted — the session ended or was replaced while starting"),!0)}}
1
+ import{getPreRollDelay as e}from"../types/BreakManifest";import{manifestHasGamAssets as s,resolveGamVendorConfiguration as t}from"../services/resolveGamVendorConfiguration";import{resolveExternalVendorConfigurations as i}from"../services/resolveExternalVendorConfigurations";import{SsaiController as a}from"../services/SsaiController";import{resolveUrlOrigin as r}from"../services/urlOrigin";import{filterSsaiClientBreaks as n,resolveStitchedStreamVendor as o,SsaiUnavailableError as d}from"../services/resolveDeliveryMode";import{detectDeviceProfile as l}from"../services/detectDeviceType";import{decideDeliveryMode as h,detectChannelSwitch as c,gamPodIdentityChanged as p}from"../services/channelSwitch";import{prepareWebRuntime as m}from"../services/webCompatibility";const S=1e4;function u(e){return"stitcherUrl"in e}function f(e){return"string"==typeof e&&""!==e.trim()}export class SessionStarter{constructor(e,s){this.state=e,this.deps=s}async startSession(s){if("ssai"===this.deps.mode){if(!u(s)||!f(s.stitcherUrl))throw new Error('[OptiViewAds] mode "ssai" requires a stitcherUrl in startSession().')}else if(u(s)||!f(s.manifestUrl))throw new Error("[OptiViewAds] startSession requires a manifestUrl (the full break-manifest URL).");this.state.sessionActive&&(this.deps.log("Ending previous session before starting new one"),this.deps.endSessionInternal("session-restart"));const t=this.state.sessionGeneration;this.state.sessionActive=!0,this.state.adStartDelayElapsed=!1;try{const e=m();e&&await e}catch(e){if(this.isStaleStart(t))return;throw this.state.sessionActive=!1,e}if(this.isStaleStart(t))return;if(this.deps.lensReporter.startSession(),u(s))return s.adInsertionType&&this.deps.diagnose("DA-INSERTION-TYPE-IGNORED-SSAI",`adInsertionType "${s.adInsertionType}" ignored in SSAI mode; the stitcher controls ad insertion.`,{adInsertionType:s.adInsertionType}),void await this.startSsaiSession(s,t);const i=s.manifestUrl;this.state.sessionConfig=s,this.state.manifestOrigin=r(i),this.deps.manifestService.setManifestUrl(i),this.deps.log("Session starting, manifest URL:",i);const a=Date.now(),n=e=>{this.deps.diagnose("DA-SESSION-STARTING",`Session start: ${e}`,{step:e,manifestUrl:i,elapsedMs:Date.now()-a})};n("begin");const o=s.assetParameters;let d;this.deps.adPlayerController.setSessionAssetParameters(o),this.deps.adPlayerController.setSessionAssetParameterMacros(s.assetParameterMacros),this.state.configSummary.gam&&(this.state.configSummary.gam={...this.state.configSummary.gam,hasAssetParameters:!!o}),n("manifest-fetch");try{d=await this.deps.manifestService.fetchManifest()}catch(e){if(this.isStaleStart(t))return;throw this.failSessionStart(e)}if(this.deps.cmcdAdReporter.onSessionStart(d),!this.isStaleStart(t))if(this.state.sessionManifest=d,this.adoptExternalVendorConfigurations(d),"ssai"!==this.resolveSessionDeliveryMode(d))try{if(await this.startGamForManifest(d,o,n,t,!0),this.isStaleStart(t))return;const a=s.adInsertionType;this.deps.adPlayerController.setBreakResumePolicy(a,d.timebase,this.deps.continueContentDuringBreak),this.deps.diagnose("DA-INSERTION-TYPE-RESOLVED",void 0===a?`Ad insertion type: unset — the manifest's resumeOffset drives the resume point (timebase: ${d.timebase})`:`Ad insertion type: ${a}, client-set (timebase: ${d.timebase})`,{adInsertionType:a??null,source:void 0===a?"unset":"client",timebase:d.timebase}),n("scheduling"),this.deps.breakScheduler.updateManifest(d),this.deps.pauseAdController.updateManifest(d),this.state.pauseAdTickInterval=setInterval(()=>this.deps.pauseAdController?.tick(),250);const r=(d.adStartDelay??0)>0?void 0:d.breaks.find(s=>0===e(s));r?(this.deps.adPlayerController.holdContentForPreRoll(r),this.state.heldImmediatePreRollId=r.id,this.state.heldPreRollArmed=!1,this.state.heldPreRollWaitStartedAt=Date.now(),this.contentIsReady()?this.armHeldImmediatePreRoll():this.state.heldPreRollArmInterval=setInterval(()=>{this.contentIsReady()?this.armHeldImmediatePreRoll():this.armHeldImmediatePreRollIfTimedOut()},250)):this.markPlaybackStartIfPlaying(),this.deps.startLivePushback(),this.startBreakScheduler(),this.startManifestPolling(),this.deps.diagnose("DA-SESSION-STARTED",`Session active for manifest ${i}`,{manifestUrl:i}),this.deps.log("Session active")}catch(e){if(this.isStaleStart(t))return;throw this.failSessionStart(e)}else await this.startSteeredSsaiSession(d,s,n)}markPlaybackStartIfPlaying(){this.deps.player.paused||this.deps.breakScheduler.markPlaybackStart()}contentIsReady(){return!Number.isNaN(this.deps.player.duration)||!this.deps.player.paused}clearHeldImmediatePreRollArmInterval(){this.state.heldPreRollArmInterval&&(clearInterval(this.state.heldPreRollArmInterval),this.state.heldPreRollArmInterval=null)}armHeldImmediatePreRoll(){this.state.heldImmediatePreRollId&&!this.state.heldPreRollArmed&&(this.clearHeldImmediatePreRollArmInterval(),this.state.heldPreRollArmed=!0,this.deps.breakScheduler.markPlaybackStart())}armHeldImmediatePreRollIfTimedOut(){if(!this.state.heldImmediatePreRollId||this.state.heldPreRollArmed)return;const e=this.state.heldPreRollWaitStartedAt;if(null===e||Date.now()-e<S)return;const s=this.state.heldImmediatePreRollId;this.armHeldImmediatePreRoll(),this.deps.diagnose("DA-PREROLL-CONTENT-WAIT-TIMEOUT","Pending pre-roll began after waiting 10000ms for content readiness.",{breakId:s,waitMs:S})}cancelHeldImmediatePreRollOnContentError(){const e=this.state.heldImmediatePreRollId;return!(!e||this.state.heldPreRollArmed)&&(this.clearHeldImmediatePreRollArmInterval(),this.deps.breakScheduler?.completeBreak(e),this.deps.adPlayerController?.releaseContentHold(),this.state.heldImmediatePreRollId=null,this.state.heldPreRollArmed=!1,this.state.heldPreRollWaitStartedAt=null,this.deps.diagnose("DA-PREROLL-CANCELLED-CONTENT-ERROR","A pending pre-roll was cancelled because the content failed to load before it could begin.",{breakId:e}),!0)}clearHeldImmediatePreRollArm(){this.clearHeldImmediatePreRollArmInterval(),this.state.heldImmediatePreRollId=null,this.state.heldPreRollArmed=!1,this.state.heldPreRollWaitStartedAt=null}failSessionStart(e){const s=e instanceof Error?e:new Error(String(e)),t=this.deps.manifestService?.takeLastFailure();return this.deps.diagnose("DA-MANIFEST-FETCH-FAILED",s.message,t?{...t}:void 0),this.state.sessionActive=!1,this.deps.cmcdAdReporter.onSessionEnd(),this.deps.lensReporter.endSession(),s}resolveSessionDeliveryMode(e,s="manifest"){const t=this.state.sessionDeliveryMode,i=h(e,l(),this.deps.hasExplicitMode?this.deps.mode:null,s);null!==i.unknownMode&&this.deps.diagnose("DA-DELIVERY-MODE-UNKNOWN",`Delivery rule ${i.ruleIndex} carries unrecognized mode "${i.unknownMode}"; using sgai.`,{unknownMode:i.unknownMode,ruleIndex:i.ruleIndex});for(const e of i.targetingIssues)this.deps.diagnose(e.code,`Targeting ${e.field} was rejected.`,{field:e.field,...void 0===e.value?{}:{value:e.value}});return this.deps.hasExplicitMode&&i.manifestMode!==i.mode&&this.deps.diagnose("DA-DELIVERY-MODE-OVERRIDDEN",`Manifest delivery rules resolve "${i.manifestMode}" but the explicit config mode "${i.mode}" takes precedence.`,{manifestMode:i.manifestMode,configuredMode:i.mode}),this.state.sessionDeliveryMode=i.mode,this.deps.diagnose("DA-DELIVERY-MODE-RESOLVED",`Delivery mode: ${i.mode}`,{mode:i.mode,ruleIndex:i.ruleIndex,source:i.source,deviceProfile:l(),..."channel-switch"===s?{previousMode:t}:{}}),i.mode}async startSteeredSsaiSession(e,s,t){const i=o(e.vendorConfiguration);if(!i){const e="Manifest selected ssai delivery but no vendor configuration provides a stitched stream (no gam entry with an assetKey).";throw this.deps.diagnose("DA-SSAI-UNAVAILABLE",e),this.deps.manifestService.reset(),this.state.sessionActive=!1,new d(e)}try{t("ssai-live-stream"),this.state.steeredSsaiActive=!0,this.state.ssaiController=this.createSsaiController(i.networkCode),await this.state.ssaiController.startSteered(i,s.assetParameters,s.assetParameterMacros),t("scheduling");const a=n(e);this.deps.adPlayerController.setBreakResumePolicy(s.adInsertionType,e.timebase,!1),this.deps.breakScheduler.updateManifest(a),this.deps.pauseAdController.updateManifest(a),this.state.pauseAdTickInterval=setInterval(()=>this.deps.pauseAdController?.tick(),250),this.markPlaybackStartIfPlaying(),this.deps.startLivePushback(),this.startBreakScheduler(),this.startManifestPolling(),this.deps.diagnose("DA-SESSION-STARTED",`SSAI session active (manifest-steered) for ${s.manifestUrl}`,{manifestUrl:s.manifestUrl}),this.deps.log("SSAI session active (manifest-steered)")}catch(e){const s=e instanceof Error?e:new Error(String(e));throw this.deps.diagnose("DA-SSAI-SESSION-FAILED",s.message),this.state.steeredSsaiActive=!1,this.deps.manifestService.reset(),this.state.sessionActive=!1,s}}createSsaiController(e){return new a(this.deps.player,{gam:this.state.steeredSsaiOptions?.streamActivityMonitorId?{streamActivityMonitorId:this.state.steeredSsaiOptions.streamActivityMonitorId}:{},ssai:{networkCode:e},maxBitrate:this.state.steeredSsaiOptions?.maxBitrate,autoplay:this.state.steeredSsaiOptions?.autoplay,debug:this.deps.debug},{emit:(e,s)=>this.deps.emit(e,s),diagnose:(e,s,t)=>this.deps.diagnose(e,s,t),log:(...e)=>this.deps.log(...e)})}startBreakScheduler(){this.deps.breakScheduler.start((e,s)=>{this.deps.breakLifecycle.onBreakStart(e,s)},e=>!this.deps.adPlayerController.isAdPlaying()&&(this.deps.adPlayerController.preload(e).catch(s=>{this.deps.diagnose("DA-PRELOAD-FAILED",s instanceof Error?s.message:String(s),{breakId:e.id})}),!0),(e,s,t)=>{this.deps.adBreakStatusTracker.onBreakWarning(e,s,t)},e=>{this.deps.breakLifecycle.onBreakAborted(e)},(e,s)=>this.deps.breakLifecycle.onBreakSkipped(e,s))}startManifestPolling(){this.deps.manifestService.startPolling(e=>{if(!this.state.sessionActive)return;const s=this.deps.notedManifestBreakIds(e);this.deps.diagnose("DA-MANIFEST-POLLED","Manifest polled",{breaks:e.breaks.length,newBreakIds:s,pollingIntervalSeconds:this.deps.manifestService.currentPollingIntervalSeconds});const t=this.state.sessionManifest,i=t?c(t,e):null;if(this.state.pendingChannelSwitch||i&&this.deps.breakScheduler.getActiveBreak())return this.state.pendingChannelSwitch=e,void this.deps.log("Channel switch deferred until the active break ends");i?this.switchChannel(e,i):this.applyManifestUpdate(e)},e=>{this.state.sessionActive&&this.deps.diagnose("DA-MANIFEST-POLL-FAILED",e.message)})}applyPendingChannelSwitch(){if(!this.state.sessionActive||!this.state.pendingChannelSwitch)return;const e=this.state.pendingChannelSwitch;this.state.pendingChannelSwitch=null;const s=c(this.state.sessionManifest,e);s?this.switchChannel(e,s):this.applyManifestUpdate(e)}applyManifestUpdate(e){if(!this.state.sessionActive)return;this.adoptExternalVendorConfigurations(e),this.diagnoseIgnoredDeliveryModeChange(e);const s=t(e);this.state.steeredSsaiActive?this.state.ssaiController?.setManifestAssetParameters(s.ssai?.assetParameters):this.deps.adPlayerController.setManifestAssetParameters(s.sgaiAssetParameters);const i=this.state.steeredSsaiActive?n(e):e;this.deps.breakScheduler.updateManifest(i),this.deps.pauseAdController.updateManifest(i)}diagnoseIgnoredDeliveryModeChange(e){if(!this.state.sessionDeliveryMode)return;const s=h(e,l(),null,"manifest");s.manifestMode!==this.state.sessionDeliveryMode&&this.deps.diagnose("DA-DELIVERY-MODE-CHANGE-IGNORED",`Polled manifest resolves delivery mode "${s.manifestMode}" but "${this.state.sessionDeliveryMode}" is pinned for this session.`,{polledMode:s.manifestMode,pinnedMode:this.state.sessionDeliveryMode})}async switchChannel(e,s){if(this.state.channelSwitchInFlight)return;const t=this.state.sessionConfig,i=this.state.sessionManifest,a={previousChannelId:s.previousChannelId,channelId:s.channelId,reason:s.reason,manifestUrl:t.manifestUrl};this.deps.diagnose("DA-MANIFEST-CHANNEL-CHANGED",`Manifest channel changed (${s.reason}): ${s.previousChannelId??"none"} -> ${s.channelId??"none"}`,a),this.state.sessionManifest=e,this.state.channelSwitchInFlight=!0;const r=this.state.sessionGeneration,n=this.state.sessionDeliveryMode??"sgai";try{let s=this.resolveSessionDeliveryMode(e,"channel-switch");if(this.deps.cmcdAdReporter.onSessionEnd(),this.deps.cmcdAdReporter.onSessionStart(e),this.deps.lensReporter.endSession(),this.deps.lensReporter.startSession(),"ssai"===s&&"sgai"===n){const a=await this.switchToSteeredSsai(e,t);if(this.isStaleStart(r))return;a||(s="sgai",this.state.sessionDeliveryMode="sgai",await this.applySameModeChannelSwitch(i,e,t,r))}else"sgai"===s&&"ssai"===n?await this.switchToSgai(e,t,r):await this.applySameModeChannelSwitch(i,e,t,r);if(this.isStaleStart(r))return}finally{this.state.channelSwitchInFlight=!1}this.adoptExternalVendorConfigurations(e),this.deps.startLivePushback(),this.emitChannelChange({...a,previousDeliveryMode:n,deliveryMode:this.state.sessionDeliveryMode??n})}emitChannelChange(e){this.deps.emit("adchannelchange",{type:"adchannelchange",timestamp:Date.now(),...e})}adoptExternalVendorConfigurations(e){this.deps.externalVendorConfigurationTracker.adopt(e)}diagnoseExternalVendorConfigurations(e){const s=i(e);s.invalidCount>0&&this.deps.diagnose("DA-EXTERNAL-VENDOR-INVALID",`Ignored ${s.invalidCount} invalid external vendorConfiguration `+(1===s.invalidCount?"entry":"entries")+".",{count:s.invalidCount}),s.duplicateProviders.length>0&&this.deps.diagnose("DA-EXTERNAL-VENDOR-DUPLICATE",`Ignored duplicate external vendorConfiguration entries for providers: ${s.duplicateProviders.join(", ")}.`,{providers:s.duplicateProviders})}async applySameModeChannelSwitch(e,s,i,a){const r=this.state.steeredSsaiActive,o=r?n(s):s;if(this.deps.breakScheduler.resetForChannelSwitch(o),this.deps.pauseAdController.updateManifest(o),this.deps.adPlayerController.setBreakResumePolicy(i.adInsertionType,s.timebase,this.deps.continueContentDuringBreak),!r&&p(e,s)){const e=this.deps.adPlayerController.updatedAssetParameters,t=this.deps.adPlayerController.sessionAssetParameterMacros;if(this.deps.adPlayerController.endGamSession(),this.deps.adPlayerController.setSessionAssetParameters(i.assetParameters),this.deps.adPlayerController.setSessionAssetParameterMacros(t),await this.startGamForManifest(s,i.assetParameters,e=>{this.deps.diagnose("DA-SESSION-STARTING",`Channel switch: ${e}`,{step:e,manifestUrl:i.manifestUrl})},a),this.isStaleStart(a))return;e&&this.deps.adPlayerController.updateAssetParameters(e)}else if(r){const e=t(s);this.state.ssaiController?.setManifestAssetParameters(e.ssai?.assetParameters)}else{const e=t(s);this.deps.adPlayerController.setManifestAssetParameters(e.sgaiAssetParameters)}}async switchToSteeredSsai(e,s){const t=o(e.vendorConfiguration);if(!t)return this.deps.diagnose("DA-SSAI-UNAVAILABLE","Channel switch resolved ssai delivery but no vendor configuration provides a stitched stream; the session stays in sgai.",{manifestUrl:s.manifestUrl,previousMode:"sgai"}),!1;const i=this.deps.adPlayerController,a=i.updatedAssetParameters,r=i.sessionAssetParameterMacros;i.stopActiveBreak(),i.releaseContentHold(),i.endGamSession(),i.setSessionAssetParameters(s.assetParameters),i.setSessionAssetParameterMacros(r),a&&i.updateAssetParameters(a);const d=this.createSsaiController(t.networkCode);this.state.ssaiController=d,this.state.steeredSsaiActive=!0,this.deps.diagnose("DA-SESSION-STARTING","Channel switch: ssai-live-stream",{step:"ssai-live-stream",manifestUrl:s.manifestUrl});try{await d.startSteered(t,s.assetParameters,r),a&&d.updateAssetParameters(a);const o=n(e);return this.deps.breakScheduler.resetForChannelSwitch(o),this.deps.pauseAdController.updateManifest(o),i.setBreakResumePolicy(s.adInsertionType,e.timebase,!1),!0}catch(e){const s=e instanceof Error?e.message:String(e);return this.deps.diagnose("DA-SSAI-SESSION-FAILED",s),d.stop(),delete this.state.ssaiController,this.state.steeredSsaiActive=!1,!1}}async switchToSgai(e,s,t){const i=this.state.ssaiController?.sessionAssetParameterMacros;this.state.ssaiController?.stop(),delete this.state.ssaiController,this.state.steeredSsaiActive=!1,this.deps.breakScheduler.resetForChannelSwitch(e),this.deps.pauseAdController.updateManifest(e);const a=this.deps.adPlayerController;a.setBreakResumePolicy(s.adInsertionType,e.timebase,this.deps.continueContentDuringBreak);const r=a.updatedAssetParameters;a.endGamSession(),a.setSessionAssetParameters(s.assetParameters),a.setSessionAssetParameterMacros(i??s.assetParameterMacros),await this.startGamForManifest(e,s.assetParameters,e=>{this.deps.diagnose("DA-SESSION-STARTING",`Channel switch: ${e}`,{step:e,manifestUrl:s.manifestUrl})},t),this.isStaleStart(t)||r&&a.updateAssetParameters(r)}async startGamForManifest(e,i,a,r=this.state.sessionGeneration,n=!1){const o=t(e);if(this.deps.adPlayerController.setManifestAssetParameters(o.sgaiAssetParameters),o.unknownTypes.length>0&&this.deps.diagnose("DA-VENDOR-CONFIG-UNKNOWN-TYPE",`Ignored ${o.unknownTypes.length} vendorConfiguration `+(1===o.unknownTypes.length?"entry":"entries")+" with unrecognised type: "+`${o.unknownTypes.join(", ")}.`,{types:o.unknownTypes}),this.diagnoseExternalVendorConfigurations(e),!this.deps.adPlayerController.isGamEnabled())return void(s(e)&&this.deps.diagnose("DA-GAM-CONFIG-MISSING","The break manifest carries GAM pod assets but GAM is disabled for this session (gam.enabled === false); those assets will be skipped.",{reason:"gam-disabled"}));if(!o.identity)return void(s(e)&&this.deps.diagnose("DA-GAM-CONFIG-MISSING",'The break manifest carries GAM pod assets but no usable "gam" vendorConfiguration entry; those assets will be skipped.',{reason:o.reason}));this.deps.diagnose("DA-GAM-CONFIG-RESOLVED",`GAM session identity: network ${o.identity.networkCode}, custom asset ${o.identity.customAssetKey}`,{networkCode:o.identity.networkCode,customAssetKey:o.identity.customAssetKey,channelId:e.channelId,duplicateEntriesIgnored:o.duplicateGam});const d=e.adStartDelay??0;if(d>0&&!this.state.adStartDelayElapsed&&(n||this.deps.breakScheduler.isAdStartDelayActive()))return this.deps.breakScheduler.holdBreakStarts(),this.deps.diagnose("DA-GAM-START-DEFERRED",`GAM session start deferred until ${d} seconds of content have played.`,{adStartDelaySec:d}),void this.deps.breakScheduler.setOnAdStartDelayElapsed(()=>{if(this.isStaleStart(r)||!this.state.sessionActive)return;this.state.adStartDelayElapsed=!0;const s=this.state.sessionManifest??e,n=t(s).identity;n?(this.deps.adPlayerController.setManifestAssetParameters(t(s).sgaiAssetParameters),this.startResolvedGamSession(n,i,a,r,!0)):this.deps.breakScheduler.releaseBreakStarts()});this.state.adStartDelayElapsed=!0,await this.startResolvedGamSession(o.identity,i,a,r)}async startResolvedGamSession(e,s,t,i,a=!1){try{if(this.isStaleStart(i)||!this.state.sessionActive)return;t("gam-init"),await this.deps.adPlayerController.startGamSession(e,s),this.deps.log("GAM session started")}catch(e){this.deps.diagnose("DA-GAM-SESSION-FAILED",e instanceof Error?e.message:String(e))}finally{a&&!this.isStaleStart(i)&&this.state.sessionActive&&this.deps.breakScheduler.releaseBreakStarts()}}async startSsaiSession(e,s){try{if(this.state.stitcherOrigin=r(e.stitcherUrl),this.state.configSummary.gam&&(this.state.configSummary.gam={...this.state.configSummary.gam,hasAssetParameters:!!e.assetParameters}),await this.state.ssaiController.start(e),this.isStaleStart(s))return;this.deps.diagnose("DA-SESSION-STARTED",`SSAI session active for ${e.stitcherUrl}`,{stitcherUrl:e.stitcherUrl}),this.deps.startLivePushback(),this.deps.log("SSAI session active")}catch(e){if(this.isStaleStart(s))return;const t=e instanceof Error?e:new Error(String(e));throw this.deps.diagnose("DA-SSAI-SESSION-FAILED",t.message),this.state.sessionActive=!1,this.deps.cmcdAdReporter.onSessionEnd(),this.deps.lensReporter.endSession(),t}}isStaleStart(e){return e!==this.state.sessionGeneration&&(this.deps.log("Session start aborted — the session ended or was replaced while starting"),!0)}}
@@ -11,6 +11,7 @@ export declare class SessionState {
11
11
  * "replaced" — the new session sets it back to true before the stale fetch resolves.
12
12
  */
13
13
  sessionGeneration: number;
14
+ adStartDelayElapsed: boolean;
14
15
  sessionDeliveryMode: OptiViewAdsMode | null;
15
16
  steeredSsaiActive: boolean;
16
17
  steeredSsaiOptions: {