@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 +16 -0
- package/dist/THEOliveDiscovery.d.ts +47 -0
- package/dist/THEOliveDiscovery.js +1 -0
- package/dist/THEOplayerAdapter.d.ts +9 -2
- package/dist/THEOplayerAdapter.js +1 -1
- package/dist/dashPtsOffset.d.ts +2 -0
- package/dist/mediaPipeline.d.ts +33 -0
- package/dist/mediaPipeline.js +1 -0
- package/package.json +2 -2
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()}}
|
package/dist/dashPtsOffset.d.ts
CHANGED
|
@@ -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.
|
|
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.
|
|
40
|
+
"@dolby-optiview/ads-sdk-core": "^1.6.0"
|
|
41
41
|
},
|
|
42
42
|
"peerDependencies": {
|
|
43
43
|
"theoplayer": ">=9.0.0"
|