@dolby-optiview/ads-sdk-adapter-theoplayer 1.0.0 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,55 @@
1
+ # @dolby-optiview/ads-sdk-adapter-theoplayer
2
+
3
+ THEOplayer Web SDK `PlayerAdapter` for the OptiView Ads SDK. It connects a
4
+ `ChromelessPlayer` (or full `Player`) and its mount element to the SDK
5
+ core, either as the content player or as the ad player.
6
+
7
+ ## Installation
8
+
9
+ ```bash
10
+ npm install @dolby-optiview/ads-sdk-core @dolby-optiview/ads-sdk-adapter-theoplayer theoplayer
11
+ ```
12
+
13
+ `theoplayer` 9.0.0 or newer is a peer dependency. The adapter is typed against
14
+ the structural `THEOplayerLike` interface, so it also works with a THEOplayer
15
+ build loaded from a script tag. A THEOplayer license is required.
16
+
17
+ ## Usage
18
+
19
+ ```typescript
20
+ import { ChromelessPlayer } from 'theoplayer';
21
+ import { OptiViewAds } from '@dolby-optiview/ads-sdk-core';
22
+ import { THEOplayerAdapter } from '@dolby-optiview/ads-sdk-adapter-theoplayer';
23
+
24
+ const config = { license: LICENSE, libraryLocation: LIB, mutedAutoplay: 'all' };
25
+
26
+ const playerDiv = document.getElementById('player') as HTMLElement;
27
+ const player = new ChromelessPlayer(playerDiv, config);
28
+
29
+ const sdk = new OptiViewAds({
30
+ player: new THEOplayerAdapter(player, playerDiv),
31
+ container: document.getElementById('container')!,
32
+ createAdAdapter: (adContainer) => {
33
+ const adPlayer = new ChromelessPlayer(adContainer, config);
34
+ return new THEOplayerAdapter(adPlayer, adContainer);
35
+ },
36
+ });
37
+
38
+ await sdk.startSession({
39
+ manifestUrl: 'https://manifest.example.com/v1/your-org/channels/your-channel',
40
+ });
41
+ player.source = { sources: [{ src: 'https://example.com/live.m3u8' }] };
42
+ player.play();
43
+ ```
44
+
45
+ For React Native apps using `react-native-theoplayer`, use
46
+ `@dolby-optiview/ads-sdk-react-native` instead of this package.
47
+
48
+ ## Documentation
49
+
50
+ - [OptiView Ads SDK documentation](https://optiview.dolby.com/docs/ads/player-integration/optiview-ads-sdk/)
51
+ - Entry package: `@dolby-optiview/ads-sdk`
52
+
53
+ ## License
54
+
55
+ See <https://optiview.dolby.com/policies/terms-of-service/>.
@@ -1,4 +1,5 @@
1
1
  import type { PlayerAdapter, PlayerAdapterEvent, PlayerAdapterEventHandler, VideoQuality } from '@dolby-optiview/ads-sdk-core';
2
+ import { type THEONetworkLike } from './dashPtsOffset';
2
3
  /** Minimal surface of a THEOplayer metadata `TextTrackCue`. */
3
4
  export interface THEOTextTrackCueLike {
4
5
  startTime?: number;
@@ -112,10 +113,16 @@ export interface THEOplayerLike {
112
113
  src?: string;
113
114
  /** The MIME type, e.g. `application/x-mpegurl`. */
114
115
  type?: string;
116
+ /** Seconds behind the end of the manifest where THEOplayer places its live point. */
117
+ liveOffset?: number;
115
118
  }>;
119
+ /** Description-wide live offset; a typed source's own value takes precedence. */
120
+ liveOffset?: number;
116
121
  } | null | undefined;
117
122
  /** Text-track list; metadata tracks carry in-stream ID3/emsg cues. Optional. */
118
123
  readonly textTracks?: THEOTextTracksLike;
124
+ /** Network interceptors; the adapter reads DASH manifests to place emsg cues by PTS. Optional. */
125
+ readonly network?: THEONetworkLike;
119
126
  /** Live-latency controller; absent on builds or sources without live playback. */
120
127
  readonly latencyManager?: THEOLatencyManagerLike;
121
128
  /**
@@ -127,6 +134,8 @@ export interface THEOplayerLike {
127
134
  readonly length: number;
128
135
  /** The end of the seekable range at `index`, in seconds. */
129
136
  end(index: number): number;
137
+ /** The start of the seekable range at index, in seconds, when supported. */
138
+ start?(index: number): number;
130
139
  };
131
140
  /** Video rendition ladder; absent on builds or sources without selectable qualities. */
132
141
  readonly videoTracks?: ArrayLike<THEOMediaTrackLike>;
@@ -152,6 +161,12 @@ export interface THEOplayerLike {
152
161
  * GAM pod URLs with query strings — default to HLS.
153
162
  */
154
163
  export declare function inferSourceType(url: string): string;
164
+ type THEOSourceDescription = NonNullable<THEOplayerLike['source']>;
165
+ /**
166
+ * `source` with every typed source carrying `offset` as its `liveOffset`, or null when the sources
167
+ * are not plain objects in an array (a copy could lose fields) or already declare a live offset.
168
+ */
169
+ export declare function withLiveOffset(source: THEOSourceDescription, offset: number): THEOSourceDescription | null;
155
170
  /**
156
171
  * The {@link PlayerAdapter} for THEOplayer, from the `@dolby-optiview/ads-sdk-adapter-theoplayer`
157
172
  * package (peer dependency: `theoplayer` 9 or later).
@@ -190,6 +205,7 @@ export declare class THEOplayerAdapter implements PlayerAdapter {
190
205
  private eventListeners;
191
206
  private boundHandlers;
192
207
  private addTrackHandler?;
208
+ private metadataTrackList?;
193
209
  /**
194
210
  * The track whose `targetQuality` the pin changed, and what it held before — so a restore
195
211
  * puts back an application's own selection rather than assuming "automatic".
@@ -198,9 +214,16 @@ export declare class THEOplayerAdapter implements PlayerAdapter {
198
214
  private pinnedTrack;
199
215
  private targetQualityBeforePin;
200
216
  private cueTracks;
217
+ /** Pending live start distance in seconds; null once withdrawn. */
218
+ private liveStartOffset;
219
+ /** `loadeddata` fired for the current source, so a frame may already be on screen. */
220
+ private frameLoaded;
221
+ /** The source this adapter last re-set with a live offset, so its own `sourcechange` is not rewritten again. */
222
+ private rewrittenSource;
201
223
  /** Per-cue `enter` listeners, released in teardown — a live stream adds one per cue. */
202
224
  private cueEnters;
203
225
  private seenCues;
226
+ private readonly dashPtsOffsets;
204
227
  /**
205
228
  * @param player The THEOplayer instance (a `ChromelessPlayer` or anything satisfying {@link THEOplayerLike}).
206
229
  * @param container The element the player was created in; the SDK locates the `<video>` inside it.
@@ -229,6 +252,21 @@ export declare class THEOplayerAdapter implements PlayerAdapter {
229
252
  * it does not know rather than a position. `player.seekable` carries the real window.
230
253
  */
231
254
  get seekableEnd(): number | null;
255
+ get seekableStart(): number | null;
256
+ /**
257
+ * The published live edge. THEOplayer ends `seekable` at its live point, documented as the end
258
+ * of the manifest minus the source's `liveOffset`; without a declared offset the seekable end
259
+ * is reported.
260
+ */
261
+ get liveEdge(): number | null;
262
+ /**
263
+ * Makes THEOplayer itself begin a live source `targetDurations` × `fallbackTargetDuration` behind
264
+ * live, by re-setting the host's source with that `liveOffset` before its first frame loads.
265
+ * THEOplayer exposes no playlist target duration, so the fallback is required. Returns false once
266
+ * a frame loaded; a source that cannot be copied losslessly is left alone and the SDK seeks.
267
+ */
268
+ requestLiveStartOffset(targetDurations: number | null, fallbackTargetDuration: number | null): boolean;
269
+ private rewriteSourceForLiveStart;
232
270
  /**
233
271
  * Playback rate, `1` being normal speed.
234
272
  *
@@ -259,6 +297,8 @@ export declare class THEOplayerAdapter implements PlayerAdapter {
259
297
  */
260
298
  get isLive(): boolean | null;
261
299
  get paused(): boolean;
300
+ /** Content is ready and its initial engine seek has settled. */
301
+ get playbackReady(): boolean;
262
302
  get muted(): boolean;
263
303
  set muted(value: boolean);
264
304
  get volume(): number;
@@ -367,3 +407,4 @@ export declare class THEOplayerAdapter implements PlayerAdapter {
367
407
  private setupNativeListeners;
368
408
  private teardownNativeListeners;
369
409
  }
410
+ export {};
@@ -1 +1 @@
1
- export function inferSourceType(e){const t=e.split(/[?#]/,1)[0].toLowerCase();return t.endsWith(".mp4")||t.endsWith(".m4v")?"video/mp4":t.endsWith(".webm")?"video/webm":"application/x-mpegurl"}function e(e){return e instanceof ArrayBuffer||"[object ArrayBuffer]"===Object.prototype.toString.call(e)}export class THEOplayerAdapter{constructor(e,t){this.endedSuppressedUntilStart=!1,this.pendingSeekTime=null,this.liveRemapCleanup=null,this.eventListeners=new Map,this.boundHandlers=new Map,this.pinnedTrack=null,this.cueTracks=[],this.cueEnters=[],this.seenCues=new WeakSet,this.player=e,this.container=t,this.setupNativeListeners(),this.setupMetadataListener()}get seekableEnd(){const e=this.player.seekable;if(!e||0===e.length)return null;const t=e.end(e.length-1);return"number"==typeof t&&Number.isFinite(t)?t:null}get playbackRate(){return this.player.playbackRate??1}set playbackRate(e){this.player.playbackRate=e}setVideoQuality(e){if(null===e){const e=this.pinnedTrack;if(!e)return!1;const t=this.targetQualityBeforePin;try{return e.targetQuality=t,e.targetQuality!==t?!1:(this.pinnedTrack=null,this.targetQualityBeforePin=void 0,!0)}catch{return!1}}const t=this.player.videoTracks;if(!t||0===t.length)return!1;const r=t[0],i=r?.qualities;if(!r||!i||0===i.length)return!1;const n=[];for(let e=0;e<i.length;e++){const t=i[e];if(!t||!1===t.available||!1===t.enabled)continue;const r=t.bandwidth;"number"==typeof r&&Number.isFinite(r)&&r>0&&n.push(t)}if(n.length<2)return!1;let s=n[0];for(let e=1;e<n.length;e++)n[e].bandwidth<s.bandwidth&&(s=n[e]);const a=!this.pinnedTrack;let l;a&&(this.pinnedTrack=r,this.targetQualityBeforePin=r.targetQuality);try{r.targetQuality=s,l=r.targetQuality===s}catch{l=!1}return!!l||(a&&(this.pinnedTrack=null,this.targetQualityBeforePin=void 0),!1)}get maxLiveSeekPosition(){const e=this.player.latencyManager;if(!e?.monitoringLivePlayback)return null;const t=e.currentLatency,r=e.currentConfiguration?.minimumOffset;if("number"!=typeof t||!Number.isFinite(t))return null;if("number"!=typeof r||!Number.isFinite(r))return null;const i=this.player.currentTime+t-r;return Number.isFinite(i)?i:null}get currentTime(){return this.player.currentTime}set currentTime(e){this.player.currentTime=e}get duration(){return this.player.duration}get isLive(){const e=this.player.duration;return"number"==typeof e?!Number.isFinite(e):null}get paused(){return this.player.paused}get muted(){return this.player.muted}set muted(e){this.player.muted=e}get volume(){return this.player.volume}set volume(e){this.player.volume=e}get videoElement(){return this.container.querySelector("video")}get programDateTime(){return this.player.currentProgramDateTime}pause(){this.player.pause()}play(){return this.player.play(),Promise.resolve()}seek(e){!0!==this.player.seeking?(this.pendingSeekTime=null,this.player.currentTime=e):this.pendingSeekTime=e}get sourceUrl(){return this.player.source?.sources?.[0]?.src??null}load(e){return this.pendingSeekTime=null,this.player.source={sources:[{src:e,type:inferSourceType(e)}]},this.endedSuppressedUntilStart=!0,Promise.resolve()}async releaseMediaElement(){this.player.pause(),this.player.source=null;const e=this.videoElement;e&&await new Promise(t=>{let r=!1;const i=()=>{r||(r=!0,e.removeEventListener("emptied",i),clearTimeout(n),t())},n=setTimeout(i,250);e.addEventListener("emptied",i,{once:!0})})}async unload(){this.pendingSeekTime=null,await this.releaseMediaElement()}async recreateMediaPipeline(e){const{startTime:t}=e,r=this.player.source;if(!r?.sources?.length)throw new Error("THEOplayer pipeline recreate: no source description");const i=!0===this.isLive,n=this.player.currentProgramDateTime,s=this.player.currentTime,a=n&&Number.isFinite(n.getTime())?n.getTime()+1e3*(t-s):null,l=this.seekableEnd,u=i&&null!==l?Math.max(0,l-t):null;if(await this.releaseMediaElement(),this.endedSuppressedUntilStart=!0,this.player.source=r,this.player.currentTime=t,i){const e=()=>{this.player.removeEventListener("loadedmetadata",t),this.player.removeEventListener("error",e),this.liveRemapCleanup=null},t=()=>{e();const t=this.player.currentProgramDateTime;let r=null;null!==a&&t&&Number.isFinite(t.getTime())?r=this.player.currentTime+(a-t.getTime())/1e3:null!==u&&null!==this.seekableEnd&&(r=this.seekableEnd-u),null!==r&&Math.abs(r-this.player.currentTime)>.5&&(this.player.currentTime=r)};this.liveRemapCleanup=e,this.player.addEventListener("loadedmetadata",t),this.player.addEventListener("error",e)}}on(e,t){this.eventListeners.has(e)||this.eventListeners.set(e,new Set),this.eventListeners.get(e).add(t)}off(e,t){this.eventListeners.get(e)?.delete(t)}destroy(){this.pendingSeekTime=null,this.liveRemapCleanup?.(),this.liveRemapCleanup=null,this.teardownNativeListeners(),this.teardownMetadataListener(),this.eventListeners.clear(),this.player.destroy()}emit(e,t){const r=this.eventListeners.get(e);if(r)for(const e of r)e(t)}setupMetadataListener(){const e=this.player.textTracks;if(!e)return;const t=e=>{e.track&&this.subscribeMetadataTrack(e.track)};this.addTrackHandler=t,e.addEventListener("addtrack",t);for(let t=0;t<(e.length??0);t++){const r=e[t];r&&this.subscribeMetadataTrack(r)}}deferMetadataCue(e){"function"==typeof queueMicrotask?queueMicrotask(e):Promise.resolve().then(e)}subscribeMetadataTrack(e){if("metadata"!==(e.kind??e.type))return;if(this.cueTracks.some(({track:t})=>t===e))return;let t=!1;const r=i=>{if(!this.cueTracks.some(({track:t})=>t===e))return;if(t)return void this.deferMetadataCue(()=>r(i));const n=i.cue;if(n&&!this.seenCues.has(n)&&(this.seenCues.add(n),this.emit("timedmetadata",{...this.toCue(n),phase:"parsed"}),"function"==typeof n.addEventListener)){const e=()=>{this.emit("timedmetadata",{...this.toCue(n),phase:"presented"})};n.addEventListener("enter",e),this.cueEnters.push({cue:n,handler:e})}},i="disabled"===e.mode;this.cueTracks.push({track:e,handler:r,enabledByAdapter:i}),e.addEventListener("addcue",r),i&&(t=!0,e.mode="hidden",t=!1);const n=e.cues;if(n&&n.length>0){const t=()=>{if(this.cueTracks.some(({track:t})=>t===e))for(let e=0;e<n.length;e++){const t=n[e];t&&r({cue:t})}};this.deferMetadataCue(t)}}teardownMetadataListener(){this.addTrackHandler&&this.player.textTracks&&(this.player.textTracks.removeEventListener("addtrack",this.addTrackHandler),this.addTrackHandler=void 0);for(const{track:e,handler:t,enabledByAdapter:r}of this.cueTracks)e.removeEventListener("addcue",t),r&&"hidden"===e.mode&&(e.mode="disabled");this.cueTracks=[];for(const{cue:e,handler:t}of this.cueEnters)e.removeEventListener?.("enter",t);this.cueEnters=[],this.seenCues=new WeakSet}toCue(t){if("string"==typeof t.schemeIDURI){const i={type:"emsg",startTime:t.startTime??0,endTime:t.endTime,schemeIdUri:t.schemeIDURI},n=e(r=t.content)?new Uint8Array(r):ArrayBuffer.isView(r)?new Uint8Array(r.buffer,r.byteOffset,r.byteLength):void 0;return n?(i.raw=n,i.messageData=new TextDecoder("utf-8").decode(n)):"string"==typeof t.content&&(i.messageData=t.content),i}var r;const i={type:"id3",startTime:t.startTime??0,endTime:t.endTime},n=t.content;if(n&&"object"==typeof n){const t=n.id;if("GEOB"===t||"GEO"===t){"string"==typeof n.description&&(i.description=n.description),"string"==typeof n.mimeType&&(i.mimeType=n.mimeType);const t=function(t){return"string"==typeof t?t:e(t)?new TextDecoder("utf-8").decode(t):ArrayBuffer.isView(t)?new TextDecoder("utf-8").decode(new Uint8Array(t.buffer,t.byteOffset,t.byteLength)):void 0}(n.data);return void 0!==t&&(i.messageData=t),i}const r=n.text??n.data??n.description;t&&void 0!==r?i.frames={[t]:String(r)}:i.messageData=JSON.stringify(n)}else"string"==typeof n&&(i.messageData=n);return i}setupNativeListeners(){const e=["timeupdate","ended","error","seeked","volumechange","waiting","playing","pause"];for(const t of e){const e=()=>{if("ended"===t&&this.endedSuppressedUntilStart)return;"playing"===t&&!1===this.player.paused&&(this.endedSuppressedUntilStart=!1);const e=this.eventListeners.get(t);if(e)for(const t of e)t();if("seeked"===t&&null!==this.pendingSeekTime){const e=this.pendingSeekTime;this.pendingSeekTime=null,this.player.currentTime=e}};this.boundHandlers.set(t,e),this.player.addEventListener(t,e)}}teardownNativeListeners(){for(const[e,t]of this.boundHandlers)this.player.removeEventListener(e,t);this.boundHandlers.clear()}}
1
+ import{toMediaErrorPayload as e}from"./theoErrorToMediaError";import{decodeGeobData as t,toUint8Array as r}from"./cueBytes";import{DashPtsOffsetTracker as s}from"./dashPtsOffset";export function inferSourceType(e){const t=e.split(/[?#]/,1)[0].toLowerCase();return t.endsWith(".mp4")||t.endsWith(".m4v")?"video/mp4":t.endsWith(".webm")?"video/webm":"application/x-mpegurl"}export function withLiveOffset(e,t){const r=e.sources,s=e=>"object"==typeof e&&null!==e&&Object.getPrototypeOf(e)===Object.prototype;return s(e)&&Array.isArray(r)&&0!==r.length&&void 0===e.liveOffset&&r.every(e=>s(e)&&void 0===e.liveOffset)?{...e,sources:r.map(e=>({...e,liveOffset:t}))}:null}export class THEOplayerAdapter{constructor(e,t){this.endedSuppressedUntilStart=!1,this.pendingSeekTime=null,this.liveRemapCleanup=null,this.eventListeners=new Map,this.boundHandlers=new Map,this.pinnedTrack=null,this.cueTracks=[],this.liveStartOffset=null,this.frameLoaded=!1,this.rewrittenSource=null,this.cueEnters=[],this.seenCues=new WeakSet,this.player=e,this.container=t,this.dashPtsOffsets=new s(e.network),this.setupNativeListeners(),this.setupMetadataListener()}get seekableEnd(){const e=this.player.seekable;if(!e||0===e.length)return null;const t=e.end(e.length-1);return"number"==typeof t&&Number.isFinite(t)?t:null}get seekableStart(){const e=this.player.seekable;if(!e?.length)return null;const t=e.start?.(e.length-1);return"number"==typeof t&&Number.isFinite(t)?t:null}get liveEdge(){const e=this.seekableEnd;if(null===e)return null;const t=this.player.source,r=t?.sources?.[0]?.liveOffset??t?.liveOffset;return"number"==typeof r&&Number.isFinite(r)&&r>=0?e+r:e}requestLiveStartOffset(e,t){const r=null!==e&&null!==t?e*t:NaN;return!Number.isFinite(r)||r<=0?(this.liveStartOffset=null,!1):(this.liveStartOffset=r,this.rewriteSourceForLiveStart(),!this.frameLoaded)}rewriteSourceForLiveStart(){const e=this.liveStartOffset,t=this.player.source;if(null===e||this.frameLoaded||!t||t===this.rewrittenSource)return;const r=withLiveOffset(t,e);if(!r)return;this.rewrittenSource=r;const s=!this.player.paused;this.player.source=r,s&&this.player.play()}get playbackRate(){return this.player.playbackRate??1}set playbackRate(e){this.player.playbackRate=e}setVideoQuality(e){if(null===e){const e=this.pinnedTrack;if(!e)return!1;const t=this.targetQualityBeforePin;try{return e.targetQuality=t,e.targetQuality!==t?!1:(this.pinnedTrack=null,this.targetQualityBeforePin=void 0,!0)}catch{return!1}}const t=this.player.videoTracks;if(!t||0===t.length)return!1;const r=t[0],s=r?.qualities;if(!r||!s||0===s.length)return!1;const i=[];for(let e=0;e<s.length;e++){const t=s[e];if(!t||!1===t.available||!1===t.enabled)continue;const r=t.bandwidth;"number"==typeof r&&Number.isFinite(r)&&r>0&&i.push(t)}if(i.length<2)return!1;let n=i[0];for(let e=1;e<i.length;e++)i[e].bandwidth<n.bandwidth&&(n=i[e]);const a=!this.pinnedTrack;let l;a&&(this.pinnedTrack=r,this.targetQualityBeforePin=r.targetQuality);try{r.targetQuality=n,l=r.targetQuality===n}catch{l=!1}return!!l||(a&&(this.pinnedTrack=null,this.targetQualityBeforePin=void 0),!1)}get maxLiveSeekPosition(){const e=this.player.latencyManager;if(!e?.monitoringLivePlayback)return null;const t=e.currentLatency,r=e.currentConfiguration?.minimumOffset;if("number"!=typeof t||!Number.isFinite(t))return null;if("number"!=typeof r||!Number.isFinite(r))return null;const s=this.player.currentTime+t-r;return Number.isFinite(s)?s:null}get currentTime(){return this.player.currentTime}set currentTime(e){this.player.currentTime=e}get duration(){return this.player.duration}get isLive(){const e=this.player.duration;return"number"==typeof e?!Number.isFinite(e):null}get paused(){return this.player.paused}get playbackReady(){const e=this.videoElement;return!0!==this.player.seeking&&!!e&&e.readyState>=HTMLMediaElement.HAVE_CURRENT_DATA&&!e.seeking}get muted(){return this.player.muted}set muted(e){this.player.muted=e}get volume(){return this.player.volume}set volume(e){this.player.volume=e}get videoElement(){return this.container.querySelector("video")}get programDateTime(){return this.player.currentProgramDateTime}pause(){this.player.pause()}play(){return this.player.play(),Promise.resolve()}seek(e){!0!==this.player.seeking?(this.pendingSeekTime=null,this.player.currentTime=e):this.pendingSeekTime=e}get sourceUrl(){return this.player.source?.sources?.[0]?.src??null}load(e){return this.pendingSeekTime=null,this.player.source={sources:[{src:e,type:inferSourceType(e)}]},this.endedSuppressedUntilStart=!0,Promise.resolve()}async releaseMediaElement(){this.player.pause(),this.player.source=null;const e=this.videoElement;e&&await new Promise(t=>{let r=!1;const s=()=>{r||(r=!0,e.removeEventListener("emptied",s),clearTimeout(i),t())},i=setTimeout(s,250);e.addEventListener("emptied",s,{once:!0})})}async unload(){this.pendingSeekTime=null,await this.releaseMediaElement()}async recreateMediaPipeline(e){const{startTime:t}=e,r=this.player.source;if(!r?.sources?.length)throw new Error("THEOplayer pipeline recreate: no source description");const s=!0===this.isLive,i=this.player.currentProgramDateTime,n=this.player.currentTime,a=i&&Number.isFinite(i.getTime())?i.getTime()+1e3*(t-n):null,l=this.seekableEnd,o=s&&null!==l?Math.max(0,l-t):null;if(await this.releaseMediaElement(),this.endedSuppressedUntilStart=!0,this.player.source=r,this.player.currentTime=t,s){const e=()=>{this.player.removeEventListener("loadedmetadata",t),this.player.removeEventListener("error",e),this.liveRemapCleanup=null},t=()=>{e();const t=this.player.currentProgramDateTime;let r=null;null!==a&&t&&Number.isFinite(t.getTime())?r=this.player.currentTime+(a-t.getTime())/1e3:null!==o&&null!==this.seekableEnd&&(r=this.seekableEnd-o),null!==r&&Math.abs(r-this.player.currentTime)>.5&&(this.player.currentTime=r)};this.liveRemapCleanup=e,this.player.addEventListener("loadedmetadata",t),this.player.addEventListener("error",e)}}on(e,t){this.eventListeners.has(e)||this.eventListeners.set(e,new Set),this.eventListeners.get(e).add(t)}off(e,t){this.eventListeners.get(e)?.delete(t)}destroy(){this.pendingSeekTime=null,this.liveRemapCleanup?.(),this.liveRemapCleanup=null,this.teardownNativeListeners(),this.teardownMetadataListener(),this.dashPtsOffsets.destroy(),this.eventListeners.clear(),this.player.destroy()}emit(e,t){const r=this.eventListeners.get(e);if(r)for(const e of r)e(t)}setupMetadataListener(){const e=this.player.textTracks;if(!e)return;const t=e=>{e.track&&this.subscribeMetadataTrack(e.track)};this.addTrackHandler=t,this.metadataTrackList=e,e.addEventListener("addtrack",t);for(let t=0;t<(e.length??0);t++){const r=e[t];r&&this.subscribeMetadataTrack(r)}}deferMetadataCue(e){"function"==typeof queueMicrotask?queueMicrotask(e):Promise.resolve().then(e)}subscribeMetadataTrack(e){if("metadata"!==(e.kind??e.type))return;if(this.cueTracks.some(({track:t})=>t===e))return;let t=!1;const r=s=>{if(!this.cueTracks.some(({track:t})=>t===e))return;if(t)return void this.deferMetadataCue(()=>r(s));const i=s.cue;if(i&&!this.seenCues.has(i)&&(this.seenCues.add(i),this.emit("timedmetadata",{...this.toCue(i),phase:"parsed"}),"function"==typeof i.addEventListener)){const e=()=>{this.emit("timedmetadata",{...this.toCue(i),phase:"presented"})};i.addEventListener("enter",e),this.cueEnters.push({cue:i,handler:e})}},s="disabled"===e.mode;this.cueTracks.push({track:e,handler:r,enabledByAdapter:s}),e.addEventListener("addcue",r),s&&(t=!0,e.mode="hidden",t=!1);const i=e.cues;if(i&&i.length>0){const t=()=>{if(this.cueTracks.some(({track:t})=>t===e))for(let e=0;e<i.length;e++){const t=i[e];t&&r({cue:t})}};this.deferMetadataCue(t)}}teardownMetadataListener(){this.addTrackHandler&&this.metadataTrackList&&this.metadataTrackList.removeEventListener("addtrack",this.addTrackHandler),this.addTrackHandler=void 0,this.metadataTrackList=void 0;for(const{track:e,handler:t,enabledByAdapter:r}of this.cueTracks)e.removeEventListener("addcue",t),r&&"hidden"===e.mode&&(e.mode="disabled");this.cueTracks=[];for(const{cue:e,handler:t}of this.cueEnters)e.removeEventListener?.("enter",t);this.cueEnters=[],this.seenCues=new WeakSet}toCue(e){if("string"==typeof e.schemeIDURI){const t={type:"emsg",startTime:e.startTime??0,endTime:e.endTime,schemeIdUri:e.schemeIDURI},s=this.dashPtsOffsets.offsetAt(t.startTime);void 0!==s&&(t.ptsOffset=s);const i=r(e.content);return i?(t.raw=i,t.messageData=new TextDecoder("utf-8").decode(i)):"string"==typeof e.content&&(t.messageData=e.content),t}const s={type:"id3",startTime:e.startTime??0,endTime:e.endTime},i=e.content;if(i&&"object"==typeof i){const e=i.id;if("GEOB"===e||"GEO"===e){"string"==typeof i.description&&(s.description=i.description),"string"==typeof i.mimeType&&(s.mimeType=i.mimeType);const e=t(i.data);return void 0!==e&&(s.messageData=e),s}const r=i.text??i.data??i.description;e&&void 0!==r?s.frames={[e]:String(r)}:s.messageData=JSON.stringify(i)}else"string"==typeof i&&(s.messageData=i);return s}setupNativeListeners(){const t=["timeupdate","ended","error","seeked","volumechange","waiting","playing","pause"];for(const r of t){const t=t=>{if("ended"===r&&this.endedSuppressedUntilStart)return;"playing"===r&&!1===this.player.paused&&(this.endedSuppressedUntilStart=!1);const s=this.eventListeners.get(r);if(s)for(const i of s)"error"===r?i(e(t,this.videoElement)):i();if("seeked"===r&&null!==this.pendingSeekTime){const e=this.pendingSeekTime;this.pendingSeekTime=null,this.player.currentTime=e}};this.boundHandlers.set(r,t),this.player.addEventListener(r,t)}const r=()=>{this.frameLoaded=!1,setTimeout(()=>this.rewriteSourceForLiveStart(),0)},s=()=>{this.frameLoaded=!0};this.boundHandlers.set("sourcechange",r),this.player.addEventListener("sourcechange",r),this.boundHandlers.set("loadeddata",s),this.player.addEventListener("loadeddata",s)}teardownNativeListeners(){for(const[e,t]of this.boundHandlers)this.player.removeEventListener(e,t);this.boundHandlers.clear()}}
@@ -0,0 +1,10 @@
1
+ /** Byte helpers for THEOplayer cue and response payloads. */
2
+ /**
3
+ * Decode a GEOB frame's object data as UTF-8. THEOplayer types it as an
4
+ * `ArrayBuffer`; typed-array views and strings are accepted defensively.
5
+ */
6
+ export declare function decodeGeobData(data: unknown): string | undefined;
7
+ /** Normalize an emsg cue's content to bytes; THEOplayer types it `Uint8Array`. */
8
+ export declare function toUint8Array(data: unknown): Uint8Array | undefined;
9
+ /** Realm-safe ArrayBuffer check (`instanceof` fails across window/worker realms). */
10
+ export declare function isArrayBuffer(value: unknown): value is ArrayBuffer;
@@ -0,0 +1 @@
1
+ export function decodeGeobData(e){return"string"==typeof e?e:isArrayBuffer(e)?new TextDecoder("utf-8").decode(e):ArrayBuffer.isView(e)?new TextDecoder("utf-8").decode(new Uint8Array(e.buffer,e.byteOffset,e.byteLength)):void 0}export function toUint8Array(e){return isArrayBuffer(e)?new Uint8Array(e):ArrayBuffer.isView(e)?new Uint8Array(e.buffer,e.byteOffset,e.byteLength):void 0}export function isArrayBuffer(e){return e instanceof ArrayBuffer||"[object ArrayBuffer]"===Object.prototype.toString.call(e)}
@@ -0,0 +1,47 @@
1
+ /**
2
+ * PTS-clock offsets of a DASH presentation, read from its MPD.
3
+ *
4
+ * A DASH emsg whose `presentation_time_delta` is 0 surfaces as a cue at the start of its carrying
5
+ * segment, not at the PTS its payload names (Anvato `type=cue&pts=...`). Player time for a media
6
+ * time `t` in a Period is `Period@start + (t - presentationTimeOffset) / timescale`, so
7
+ * `ptsOffset = Period@start - presentationTimeOffset / timescale` lets the core place the cue exactly.
8
+ */
9
+ /** One Period's start on the player timeline and its PTS-clock offset, in seconds. */
10
+ export interface DashPeriodOffset {
11
+ start: number;
12
+ ptsOffset: number;
13
+ }
14
+ /** Seconds in an `xs:duration` such as `PT1791467290S` or `PT1H2.5S`; null when unparseable. */
15
+ export declare function parseIsoDuration(value: string): number | null;
16
+ /**
17
+ * Period offsets of an MPD, in document order. A Period without `start` follows the previous one
18
+ * when that one declares a `duration`; otherwise it is skipped rather than guessed.
19
+ */
20
+ export declare function parseDashPeriodOffsets(mpd: string): DashPeriodOffset[];
21
+ /** Offset of the last Period starting at or before `time`; undefined when none does. */
22
+ export declare function ptsOffsetAt(periods: readonly DashPeriodOffset[], time: number): number | undefined;
23
+ /** The slice of THEOplayer's `player.network` this module needs. */
24
+ export interface THEONetworkLike {
25
+ addResponseInterceptor(interceptor: (response: THEOResponseLike) => void): void;
26
+ removeResponseInterceptor(interceptor: (response: THEOResponseLike) => void): void;
27
+ }
28
+ /** The slice of a THEOplayer `InterceptableResponse` this module reads. */
29
+ export interface THEOResponseLike {
30
+ readonly request?: {
31
+ readonly type?: string;
32
+ };
33
+ readonly body?: unknown;
34
+ }
35
+ /**
36
+ * Keeps the current MPD's Period offsets by watching manifest responses. A non-DASH manifest
37
+ * clears them, so an HLS source never inherits a previous DASH source's offsets.
38
+ */
39
+ export declare class DashPtsOffsetTracker {
40
+ private readonly network;
41
+ private periods;
42
+ private readonly interceptor;
43
+ constructor(network: THEONetworkLike | undefined);
44
+ /** The PTS-clock offset for a cue at player time `time`, when the current MPD gives one. */
45
+ offsetAt(time: number): number | undefined;
46
+ destroy(): void;
47
+ }
@@ -0,0 +1 @@
1
+ import{isArrayBuffer as t}from"./cueBytes";const e=/^P(?:(\d+(?:\.\d+)?)D)?(?:T(?:(\d+(?:\.\d+)?)H)?(?:(\d+(?:\.\d+)?)M)?(?:(\d+(?:\.\d+)?)S)?)?$/;export function parseIsoDuration(t){const r=e.exec(t.trim());if(!r||"P"===t.trim()||"PT"===t.trim())return null;const[,s,o,n,i]=r;return 86400*Number(s??0)+3600*Number(o??0)+60*Number(n??0)+Number(i??0)}function r(t,e){return new RegExp(`\\b${e}="([^"]*)"`).exec(t)?.[1]}function s(t){const e=/<Segment(?:Template|List|Base)\b([^>]*)>/.exec(t)?.[1]??"";return{pto:Number(r(e,"presentationTimeOffset")??0),timescale:Number(r(e,"timescale")??1)}}export function parseDashPeriodOffsets(t){const e=[];let o=0;for(const n of t.matchAll(/<Period\b([^>]*)>([\s\S]*?)<\/Period>/g)){const[,t="",i=""]=n,u=r(t,"start"),c=void 0!==u?parseIsoDuration(u):o,f=r(t,"duration"),p=void 0!==f?parseIsoDuration(f):null;if(o=null!==c&&null!==p?c+p:null,null===c)continue;const{pto:a,timescale:d}=s(i);!Number.isFinite(a)||!Number.isFinite(d)||d<=0||e.push({start:c,ptsOffset:c-a/d})}return e}export function ptsOffsetAt(t,e){let r;for(const s of t)s.start<=e&&(!r||s.start>=r.start)&&(r=s);return r?.ptsOffset}export class DashPtsOffsetTracker{constructor(e){this.network=e,this.periods=[],this.interceptor=e=>{if("manifest"!==e.request?.type)return;const r="string"==typeof(s=e.body)?s:t(s)?new TextDecoder("utf-8").decode(s):null;var s;null!==r&&(this.periods=r.includes("<MPD")?parseDashPeriodOffsets(r):[])},e?.addResponseInterceptor(this.interceptor)}offsetAt(t){return ptsOffsetAt(this.periods,t)}destroy(){this.network?.removeResponseInterceptor(this.interceptor),this.periods=[]}}
@@ -0,0 +1,6 @@
1
+ export declare function toMediaErrorPayload(event: unknown, video: HTMLVideoElement | null): {
2
+ code?: number;
3
+ message: string;
4
+ name?: string;
5
+ cause?: unknown;
6
+ };
@@ -0,0 +1 @@
1
+ const e={3001:4,4e3:2,4001:2,4002:4,5e3:4,5001:2,5002:3,5004:1,6e3:2,6001:2},o=e=>"object"==typeof e&&null!==e;export function toMediaErrorPayload(r,t){const n=o(r)?r:void 0,d=n?.errorObject,i=o(d)?d:void 0,a=i?.code,c=t?.error?.code,s=("number"==typeof a?e[a]:void 0)??(6===i?.category?2:void 0)??("number"==typeof c&&c>=1&&c<=4?c:void 0),y=i?.message,g=n?.error,p={message:"string"==typeof y&&y.length>0?y:"string"==typeof g&&g.length>0?g:"THEOplayer error"};return void 0!==s&&(p.code=s),"string"==typeof i?.name&&(p.name=i.name),void 0!==d&&(p.cause=d),p}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dolby-optiview/ads-sdk-adapter-theoplayer",
3
- "version": "1.0.0",
3
+ "version": "1.1.0",
4
4
  "description": "THEOplayer adapter for OptiView Ads SDK",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -37,7 +37,7 @@
37
37
  },
38
38
  "sideEffects": false,
39
39
  "dependencies": {
40
- "@dolby-optiview/ads-sdk-core": "^1.0.0"
40
+ "@dolby-optiview/ads-sdk-core": "^1.1.0"
41
41
  },
42
42
  "peerDependencies": {
43
43
  "theoplayer": ">=9.0.0"