@dolby-optiview/ads-sdk-adapter-theoplayer 1.4.0 → 1.6.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 CHANGED
@@ -42,6 +42,22 @@ player.source = { sources: [{ src: 'https://example.com/live.m3u8' }] };
42
42
  player.play();
43
43
  ```
44
44
 
45
+ ### THEOlive sources
46
+
47
+ For a THEOlive source the adapter reads the ads manifest URL from the THEOlive
48
+ discovery response, so `manifestUrl` can be omitted. Opt in with
49
+ `manifestUrlDiscovery: true` (off by default) and create the SDK before you set
50
+ the source:
51
+
52
+ ```typescript
53
+ const sdk = new OptiViewAds({ player: new THEOplayerAdapter(player, playerDiv), manifestUrlDiscovery: true /* … */ });
54
+ player.source = { sources: [{ type: 'theolive', src: 'your-distribution-id' }] };
55
+ await sdk.startSession({});
56
+ ```
57
+
58
+ `startSession()` waits up to 10 seconds for discovery and rejects when the
59
+ response has no ads manifest URL. An explicit `manifestUrl` always wins.
60
+
45
61
  For React Native apps using `react-native-theoplayer`, use
46
62
  `@dolby-optiview/ads-sdk-react-native` instead of this package.
47
63
 
@@ -0,0 +1,47 @@
1
+ import type { DiscoveredAdsManifestUrl } from '@dolby-optiview/ads-sdk-core';
2
+ import type { THEONetworkLike } from './dashPtsOffset';
3
+ /** The slice of `player.theoLive` this tracker needs (optional on the player). */
4
+ export interface THEOLiveApiLike {
5
+ addEventListener(type: string, listener: (event: unknown) => void): void;
6
+ removeEventListener(type: string, listener: (event: unknown) => void): void;
7
+ }
8
+ /** The slice of the player this tracker needs beyond `THEODiscoveryNetworkLike`/`THEOLiveApiLike`. */
9
+ export interface THEODiscoveryPlayerLike {
10
+ source?: {
11
+ sources?: ReadonlyArray<{
12
+ type?: string | undefined;
13
+ }> | undefined;
14
+ } | null | undefined;
15
+ addEventListener(type: string, listener: (event: unknown) => void): void;
16
+ removeEventListener(type: string, listener: (event: unknown) => void): void;
17
+ }
18
+ /**
19
+ * Reads `ads.adsManifestUrl` out of a discovery response body: the URL when present and non-blank,
20
+ * `null` when the body parsed but carries none, `undefined` when the body could not be parsed.
21
+ */
22
+ export declare function adsManifestUrlFromBody(body: unknown): string | null | undefined;
23
+ /**
24
+ * Tracks `ads.adsManifestUrl` for the player's current source: a read-only network response
25
+ * interceptor watches the THEOlive discovery endpoints, `distributionloadstart` re-arms the
26
+ * `pending` state, and `sourcechange` to a non-THEOlive source marks the URL `unavailable`.
27
+ * Players without `network`/`theoLive` stay in their initial state forever. The player
28
+ * hooks install lazily on the first `onDiscoveredAdsManifestUrlChange` subscription.
29
+ */
30
+ export declare class THEOliveDiscovery {
31
+ private readonly player;
32
+ private state;
33
+ private listeners;
34
+ private readonly interceptor;
35
+ private readonly onDistributionLoadStart;
36
+ private readonly onSourceChange;
37
+ constructor(player: THEODiscoveryPlayerLike & {
38
+ network?: THEONetworkLike;
39
+ theoLive?: THEOLiveApiLike;
40
+ });
41
+ private installed;
42
+ private install;
43
+ get discoveredAdsManifestUrl(): DiscoveredAdsManifestUrl;
44
+ onDiscoveredAdsManifestUrlChange(listener: (state: DiscoveredAdsManifestUrl) => void): () => void;
45
+ private setState;
46
+ destroy(): void;
47
+ }
@@ -0,0 +1 @@
1
+ import{isArrayBuffer as t}from"./cueBytes";const e=["discovery.sneezysparrow.com","discovery.theo.live","api.sneezysparrow.com"],s=new Set(["theolive","application/vnd.theo.live+channel"]);function i(t){const e=t?.sources?.[0]?.type;return"string"==typeof e&&s.has(e.toLowerCase())}export function adsManifestUrlFromBody(e){let s=e;if("string"==typeof e||t(e)){const t="string"==typeof e?e:new TextDecoder("utf-8").decode(e);try{s=JSON.parse(t)}catch{return}}if("object"!=typeof s||null===s)return;const i=s.ads?.adsManifestUrl;return"string"==typeof i&&""!==i.trim()?i:null}export class THEOliveDiscovery{constructor(t){this.player=t,this.listeners=new Set,this.interceptor=t=>{const s=t.request?.url;if("string"!=typeof s||!e.some(t=>s.includes(t)))return;const i=t.status;if("number"!=typeof i||i<200||i>=300)return;const r=adsManifestUrlFromBody(t.body);void 0!==r&&this.setState(null!==r?{status:"available",url:r}:{status:"unavailable"})},this.onDistributionLoadStart=()=>{this.setState({status:"pending"})},this.onSourceChange=()=>{i(this.player.source)||this.setState({status:"unavailable"})},this.installed=!1,this.state=i(t.source)||!t.source?{status:"pending"}:{status:"unavailable"}}install(){this.installed||(this.installed=!0,this.player.network?.addResponseInterceptor(this.interceptor),this.player.theoLive?.addEventListener("distributionloadstart",this.onDistributionLoadStart),this.player.addEventListener("sourcechange",this.onSourceChange))}get discoveredAdsManifestUrl(){return this.state}onDiscoveredAdsManifestUrlChange(t){return this.install(),this.listeners.add(t),()=>{this.listeners.delete(t)}}setState(t){if(e=this.state,s=t,e.status!==s.status||"available"===e.status&&("available"!==s.status||e.url!==s.url)){var e,s;this.state=t;for(const e of this.listeners)e(t)}}destroy(){this.installed&&(this.player.network?.removeResponseInterceptor(this.interceptor),this.player.theoLive?.removeEventListener("distributionloadstart",this.onDistributionLoadStart),this.player.removeEventListener("sourcechange",this.onSourceChange),this.installed=!1),this.listeners.clear()}}
@@ -1,5 +1,6 @@
1
- import type { ExternalPlaybackState, PlayerAdapter, PlayerAdapterEvent, PlayerAdapterEventHandler, VideoQuality } from '@dolby-optiview/ads-sdk-core';
1
+ import type { DiscoveredAdsManifestUrl, ExternalPlaybackState, PlayerAdapter, PlayerAdapterEvent, PlayerAdapterEventHandler, VideoQuality } from '@dolby-optiview/ads-sdk-core';
2
2
  import { type THEONetworkLike } from './dashPtsOffset';
3
+ import { type THEOLiveApiLike } from './THEOliveDiscovery';
3
4
  import { type THEOCastLike } from './theoExternalPlayback';
4
5
  /** Minimal surface of a THEOplayer metadata `TextTrackCue`. */
5
6
  export interface THEOTextTrackCueLike {
@@ -122,8 +123,10 @@ export interface THEOplayerLike {
122
123
  } | null | undefined;
123
124
  /** Text-track list; metadata tracks carry in-stream ID3/emsg cues. Optional. */
124
125
  readonly textTracks?: THEOTextTracksLike;
125
- /** Network interceptors; the adapter reads DASH manifests to place emsg cues by PTS. Optional. */
126
+ /** Network interceptors; the adapter reads DASH manifests to place emsg cues by PTS and THEOlive discovery for the ads manifest URL. Optional. */
126
127
  readonly network?: THEONetworkLike;
128
+ /** THEOlive API; `distributionloadstart` re-arms ads-manifest-URL discovery. Optional. */
129
+ readonly theoLive?: THEOLiveApiLike;
127
130
  /** Live-latency controller; absent on builds or sources without live playback. */
128
131
  readonly latencyManager?: THEOLatencyManagerLike;
129
132
  /**
@@ -228,6 +231,7 @@ export declare class THEOplayerAdapter implements PlayerAdapter {
228
231
  private seenCues;
229
232
  private readonly dashPtsOffsets;
230
233
  private readonly externalPlayback;
234
+ private readonly theoLiveDiscovery;
231
235
  private readonly externalPlaybackGate;
232
236
  /**
233
237
  * @param player The THEOplayer instance (a `ChromelessPlayer` or anything satisfying {@link THEOplayerLike}).
@@ -379,6 +383,9 @@ export declare class THEOplayerAdapter implements PlayerAdapter {
379
383
  on(event: PlayerAdapterEvent, handler: PlayerAdapterEventHandler): void;
380
384
  off(event: PlayerAdapterEvent, handler: PlayerAdapterEventHandler): void;
381
385
  get externalPlaybackState(): ExternalPlaybackState | undefined;
386
+ /** Ads manifest URL from the THEOlive discovery response, when the player exposes one. */
387
+ get discoveredAdsManifestUrl(): DiscoveredAdsManifestUrl;
388
+ onDiscoveredAdsManifestUrlChange(listener: (state: DiscoveredAdsManifestUrl) => void): () => void;
382
389
  setExternalPlaybackAllowed(allowed: boolean): void;
383
390
  destroy(): void;
384
391
  private emit;
@@ -1 +1 @@
1
- import{toMediaErrorPayload as e}from"./theoErrorToMediaError";import{decodeGeobData as t,toUint8Array as r}from"./cueBytes";import{DashPtsOffsetTracker as s}from"./dashPtsOffset";import{ExternalPlaybackGate as i,observeTheoExternalPlayback as n}from"./theoExternalPlayback";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.externalPlaybackGate=new i(()=>this.videoElement),this.player=e,this.container=t,this.dashPtsOffsets=new s(e.network),this.externalPlayback=n(e.cast?.airplay,this.videoElement,e=>this.emit("externalplaybackchange",e)),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.externalPlaybackGate.reapply(),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)}get externalPlaybackState(){return this.externalPlayback?.state}setExternalPlaybackAllowed(e){this.externalPlaybackGate.set(e)}destroy(){this.externalPlayback?.dispose(),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,this.externalPlaybackGate.reapply(),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()}}
1
+ import{toMediaErrorPayload as e}from"./theoErrorToMediaError";import{decodeGeobData as t,toUint8Array as s}from"./cueBytes";import{DashPtsOffsetTracker as i}from"./dashPtsOffset";import{THEOliveDiscovery as r}from"./THEOliveDiscovery";import{recreateMediaPipeline as n,waitForElementEmptied as a}from"./mediaPipeline";import{ExternalPlaybackGate as l,observeTheoExternalPlayback as o}from"./theoExternalPlayback";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 s=e.sources,i=e=>"object"==typeof e&&null!==e&&Object.getPrototypeOf(e)===Object.prototype;return i(e)&&Array.isArray(s)&&0!==s.length&&void 0===e.liveOffset&&s.every(e=>i(e)&&void 0===e.liveOffset)?{...e,sources:s.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.externalPlaybackGate=new l(()=>this.videoElement),this.player=e,this.container=t,this.dashPtsOffsets=new i(e.network),this.theoLiveDiscovery=new r(e),this.externalPlayback=o(e.cast?.airplay,this.videoElement,e=>this.emit("externalplaybackchange",e)),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,s=t?.sources?.[0]?.liveOffset??t?.liveOffset;return"number"==typeof s&&Number.isFinite(s)&&s>=0?e+s:e}requestLiveStartOffset(e,t){const s=null!==e&&null!==t?e*t:NaN;return!Number.isFinite(s)||s<=0?(this.liveStartOffset=null,!1):(this.liveStartOffset=s,this.rewriteSourceForLiveStart(),!this.frameLoaded)}rewriteSourceForLiveStart(){const e=this.liveStartOffset,t=this.player.source;if(null===e||this.frameLoaded||!t||t===this.rewrittenSource)return;const s=withLiveOffset(t,e);if(!s)return;this.rewrittenSource=s;const i=!this.player.paused;this.player.source=s,i&&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 s=t[0],i=s?.qualities;if(!s||!i||0===i.length)return!1;const r=[];for(let e=0;e<i.length;e++){const t=i[e];if(!t||!1===t.available||!1===t.enabled)continue;const s=t.bandwidth;"number"==typeof s&&Number.isFinite(s)&&s>0&&r.push(t)}if(r.length<2)return!1;let n=r[0];for(let e=1;e<r.length;e++)r[e].bandwidth<n.bandwidth&&(n=r[e]);const a=!this.pinnedTrack;let l;a&&(this.pinnedTrack=s,this.targetQualityBeforePin=s.targetQuality);try{s.targetQuality=n,l=s.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,s=e.currentConfiguration?.minimumOffset;if("number"!=typeof t||!Number.isFinite(t))return null;if("number"!=typeof s||!Number.isFinite(s))return null;const i=this.player.currentTime+t-s;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.isNaN(e)?null:!Number.isFinite(e)}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.externalPlaybackGate.reapply(),this.endedSuppressedUntilStart=!0,Promise.resolve()}async releaseMediaElement(){this.player.pause(),this.player.source=null;const e=this.videoElement;e&&await a(e)}async unload(){this.pendingSeekTime=null,await this.releaseMediaElement()}async recreateMediaPipeline(e){await n(this.player,{isLive:this.isLive,seekableEnd:()=>this.seekableEnd,releaseMediaElement:()=>this.releaseMediaElement(),markEndedSuppressedUntilStart:()=>{this.endedSuppressedUntilStart=!0},setLiveRemapCleanup:e=>{this.liveRemapCleanup=e}},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)}get externalPlaybackState(){return this.externalPlayback?.state}get discoveredAdsManifestUrl(){return this.theoLiveDiscovery.discoveredAdsManifestUrl}onDiscoveredAdsManifestUrlChange(e){return this.theoLiveDiscovery.onDiscoveredAdsManifestUrlChange(e)}setExternalPlaybackAllowed(e){this.externalPlaybackGate.set(e)}destroy(){this.externalPlayback?.dispose(),this.pendingSeekTime=null,this.liveRemapCleanup?.(),this.liveRemapCleanup=null,this.theoLiveDiscovery.destroy(),this.teardownNativeListeners(),this.teardownMetadataListener(),this.dashPtsOffsets.destroy(),this.eventListeners.clear(),this.player.destroy()}emit(e,t){const s=this.eventListeners.get(e);if(s)for(const e of s)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 s=e[t];s&&this.subscribeMetadataTrack(s)}}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 s=i=>{if(!this.cueTracks.some(({track:t})=>t===e))return;if(t)return void this.deferMetadataCue(()=>s(i));const r=i.cue;if(r&&!this.seenCues.has(r)&&(this.seenCues.add(r),this.emit("timedmetadata",{...this.toCue(r),phase:"parsed"}),"function"==typeof r.addEventListener)){const e=()=>{this.emit("timedmetadata",{...this.toCue(r),phase:"presented"})};r.addEventListener("enter",e),this.cueEnters.push({cue:r,handler:e})}},i="disabled"===e.mode;this.cueTracks.push({track:e,handler:s,enabledByAdapter:i}),e.addEventListener("addcue",s),i&&(t=!0,e.mode="hidden",t=!1);const r=e.cues;if(r&&r.length>0){const t=()=>{if(this.cueTracks.some(({track:t})=>t===e))for(let e=0;e<r.length;e++){const t=r[e];t&&s({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:s}of this.cueTracks)e.removeEventListener("addcue",t),s&&"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},i=this.dashPtsOffsets.offsetAt(t.startTime);void 0!==i&&(t.ptsOffset=i);const r=s(e.content);return r?(t.raw=r,t.messageData=new TextDecoder("utf-8").decode(r)):"string"==typeof e.content&&(t.messageData=e.content),t}const i={type:"id3",startTime:e.startTime??0,endTime:e.endTime},r=e.content;if(r&&"object"==typeof r){const e=r.id;if("GEOB"===e||"GEO"===e){"string"==typeof r.description&&(i.description=r.description),"string"==typeof r.mimeType&&(i.mimeType=r.mimeType);const e=t(r.data);return void 0!==e&&(i.messageData=e),i}const s=r.text??r.data??r.description;e&&void 0!==s?i.frames={[e]:String(s)}:i.messageData=JSON.stringify(r)}else"string"==typeof r&&(i.messageData=r);return i}setupNativeListeners(){const t=["timeupdate","ended","error","seeked","volumechange","waiting","playing","pause"];for(const s of t){const t=t=>{if("ended"===s&&this.endedSuppressedUntilStart)return;"playing"===s&&!1===this.player.paused&&(this.endedSuppressedUntilStart=!1);const i=this.eventListeners.get(s);if(i)for(const r of i)"error"===s?r(e(t,this.videoElement)):r();if("seeked"===s&&null!==this.pendingSeekTime){const e=this.pendingSeekTime;this.pendingSeekTime=null,this.player.currentTime=e}};this.boundHandlers.set(s,t),this.player.addEventListener(s,t)}const s=()=>{this.frameLoaded=!1,this.externalPlaybackGate.reapply(),setTimeout(()=>this.rewriteSourceForLiveStart(),0)},i=()=>{this.frameLoaded=!0};this.boundHandlers.set("sourcechange",s),this.player.addEventListener("sourcechange",s),this.boundHandlers.set("loadeddata",i),this.player.addEventListener("loadeddata",i)}teardownNativeListeners(){for(const[e,t]of this.boundHandlers)this.player.removeEventListener(e,t);this.boundHandlers.clear()}}
@@ -29,7 +29,9 @@ export interface THEONetworkLike {
29
29
  export interface THEOResponseLike {
30
30
  readonly request?: {
31
31
  readonly type?: string;
32
+ readonly url?: string;
32
33
  };
34
+ readonly status?: number;
33
35
  readonly body?: unknown;
34
36
  }
35
37
  /**
@@ -0,0 +1,33 @@
1
+ import type { THEOplayerLike } from './THEOplayerAdapter';
2
+ /**
3
+ * Clearing `source` starts THEOplayer's teardown; it does not finish it. The teardown
4
+ * resets the media element asynchronously, and if the SDK's ad engine attaches in the
5
+ * meantime the reset lands ON the new MediaSource: measured on Safari as an element
6
+ * holding hls.js's `blob:` src while `networkState` was back at NETWORK_EMPTY (0) — the
7
+ * manifest was fetched and not one segment was ever requested, because the MediaSource
8
+ * never opened. Waiting for the element's own `emptied` is waiting for the thing that
9
+ * actually has to have happened; the timeout is a floor, not the mechanism, so a build
10
+ * that has already emptied by this point is not delayed by it.
11
+ */
12
+ export declare function waitForElementEmptied(video: HTMLVideoElement): Promise<void>;
13
+ /** The adapter surface {@link recreateMediaPipeline} needs, kept narrow for testing. */
14
+ export interface MediaPipelineHooks {
15
+ readonly isLive: boolean | null;
16
+ seekableEnd(): number | null;
17
+ releaseMediaElement(): Promise<void>;
18
+ markEndedSuppressedUntilStart(): void;
19
+ setLiveRemapCleanup(cleanup: (() => void) | null): void;
20
+ }
21
+ /**
22
+ * Rebuild the pipeline for the CURRENT source positioned at `startTime` — see
23
+ * `PlayerAdapter.recreateMediaPipeline`. Resolves right after the source is
24
+ * re-assigned: THEOplayer defaults to `preload: 'none'`, so waiting for metadata
25
+ * before the SDK's `play()` would deadlock.
26
+ *
27
+ * A live reload rebases the media timeline (measured 31.2s → 11.2s on the same
28
+ * stream), so the position is re-mapped once metadata arrives — by program date
29
+ * time when the stream carries it, else by distance to the live edge.
30
+ */
31
+ export declare function recreateMediaPipeline(player: THEOplayerLike, hooks: MediaPipelineHooks, options: {
32
+ startTime: number;
33
+ }): Promise<void>;
@@ -0,0 +1 @@
1
+ export function waitForElementEmptied(e){return new Promise(t=>{let r=!1;const n=()=>{r||(r=!0,e.removeEventListener("emptied",n),clearTimeout(i),t())},i=setTimeout(n,250);e.addEventListener("emptied",n,{once:!0})})}export async function recreateMediaPipeline(e,t,r){const{startTime:n}=r,i=e.source;if(!i?.sources?.length)throw new Error("THEOplayer pipeline recreate: no source description");const a=!0===t.isLive,l=e.currentProgramDateTime,s=e.currentTime,m=l&&Number.isFinite(l.getTime())?l.getTime()+1e3*(n-s):null,o=t.seekableEnd(),u=a&&null!==o?Math.max(0,o-n):null;if(await t.releaseMediaElement(),t.markEndedSuppressedUntilStart(),e.source=i,e.currentTime=n,a){const r=()=>{e.removeEventListener("loadedmetadata",n),e.removeEventListener("error",r),t.setLiveRemapCleanup(null)},n=()=>{r();const n=e.currentProgramDateTime;let i=null;if(null!==m&&n&&Number.isFinite(n.getTime()))i=e.currentTime+(m-n.getTime())/1e3;else{const e=t.seekableEnd();null!==u&&null!==e&&(i=e-u)}null!==i&&Math.abs(i-e.currentTime)>.5&&(e.currentTime=i)};t.setLiveRemapCleanup(r),e.addEventListener("loadedmetadata",n),e.addEventListener("error",r)}}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dolby-optiview/ads-sdk-adapter-theoplayer",
3
- "version": "1.4.0",
3
+ "version": "1.6.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.4.0"
40
+ "@dolby-optiview/ads-sdk-core": "^1.6.0"
41
41
  },
42
42
  "peerDependencies": {
43
43
  "theoplayer": ">=9.0.0"