@dolby-optiview/ads-sdk-adapter-hlsjs 1.0.0 → 1.1.1

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,59 @@
1
+ # @dolby-optiview/ads-sdk-adapter-hlsjs
2
+
3
+ HLS.js `PlayerAdapter` for the OptiView Ads SDK. It connects an `Hls`
4
+ instance and its `<video>` element to the SDK core, either as the content
5
+ player or as the ad player.
6
+
7
+ `@dolby-optiview/ads-sdk` already depends on this package and uses it for its
8
+ default ad player; install it directly only when you use
9
+ `@dolby-optiview/ads-sdk-core` on its own.
10
+
11
+ ## Installation
12
+
13
+ ```bash
14
+ npm install @dolby-optiview/ads-sdk-core @dolby-optiview/ads-sdk-adapter-hlsjs hls.js
15
+ ```
16
+
17
+ `hls.js` 1.x is a peer dependency.
18
+
19
+ ## Usage
20
+
21
+ ```typescript
22
+ import Hls from 'hls.js';
23
+ import { OptiViewAds } from '@dolby-optiview/ads-sdk-core';
24
+ import { HlsJsAdapter } from '@dolby-optiview/ads-sdk-adapter-hlsjs';
25
+
26
+ const video = document.getElementById('video') as HTMLVideoElement;
27
+ const hls = new Hls();
28
+ hls.attachMedia(video);
29
+
30
+ const sdk = new OptiViewAds({
31
+ player: new HlsJsAdapter(hls, video),
32
+ container: document.getElementById('container')!,
33
+ createAdAdapter: (adContainer) => {
34
+ const adVideo = document.createElement('video');
35
+ adContainer.appendChild(adVideo);
36
+ const adHls = new Hls();
37
+ adHls.attachMedia(adVideo);
38
+ return new HlsJsAdapter(adHls, adVideo);
39
+ },
40
+ });
41
+
42
+ await sdk.startSession({
43
+ manifestUrl: 'https://manifest.example.com/v1/your-org/channels/your-channel',
44
+ });
45
+ hls.loadSource('https://example.com/content.m3u8');
46
+ video.play();
47
+ ```
48
+
49
+ The adapter forwards HLS.js program date time and ID3 metadata to the core, so
50
+ breaks are scheduled against the stream timeline without extra configuration.
51
+
52
+ ## Documentation
53
+
54
+ - [OptiView Ads SDK documentation](https://optiview.dolby.com/docs/ads/player-integration/optiview-ads-sdk/)
55
+ - Entry package: `@dolby-optiview/ads-sdk`
56
+
57
+ ## License
58
+
59
+ See <https://optiview.dolby.com/policies/terms-of-service/>.
@@ -34,6 +34,8 @@ export declare class HlsJsAdapter implements PlayerAdapter {
34
34
  private boundHandlers;
35
35
  private _programDateTime;
36
36
  private deferredSeekTime;
37
+ /** `ended` already emitted for the current playback (element event and hls.js MEDIA_ENDED can both arrive). */
38
+ private endedEmitted;
37
39
  /**
38
40
  * The level selection in force before {@link pinLowestQuality}, or `null` when nothing is
39
41
  * pinned. `-1` is hls.js's own value for automatic, so it is a meaningful snapshot rather
@@ -92,6 +94,8 @@ export declare class HlsJsAdapter implements PlayerAdapter {
92
94
  * Whether the player is currently paused.
93
95
  */
94
96
  get paused(): boolean;
97
+ /** Content is ready and its initial engine seek has settled. */
98
+ get playbackReady(): boolean;
95
99
  /**
96
100
  * Whether the player is muted.
97
101
  */
@@ -192,6 +196,14 @@ export declare class HlsJsAdapter implements PlayerAdapter {
192
196
  * `latestLevelDetails.live` is the authoritative flag and is public API.
193
197
  */
194
198
  get maxLiveSeekPosition(): number | null;
199
+ get targetDuration(): number | null;
200
+ /**
201
+ * Makes hls.js itself start a live stream `targetDurations` declared target durations behind the
202
+ * playlist edge, through its live-sync distance (`liveSyncDurationCount`). hls.js reads that when
203
+ * the first media playlist arrives, so this only works before then; afterwards it returns false
204
+ * and the SDK seeks. The distance stays hls.js's live-sync target for the rest of the session.
205
+ */
206
+ requestLiveStartOffset(targetDurations: number | null, _fallbackTargetDuration: number | null): boolean;
195
207
  /**
196
208
  * Set the requested video quality, or restore the selection in force before the first set.
197
209
  * Returns whether an override was applied or an outstanding override was restored.
@@ -248,6 +260,7 @@ export declare class HlsJsAdapter implements PlayerAdapter {
248
260
  * Set up video element event listeners.
249
261
  */
250
262
  private setupVideoListeners;
263
+ private emitEndedOnce;
251
264
  /**
252
265
  * Update program date time based on current playback position.
253
266
  */
@@ -1 +1 @@
1
- import e from"hls.js";import{decodeUtf8 as t,parseId3GeobFrames as s}from"@dolby-optiview/ads-sdk-core/id3";export class HlsJsAdapter{constructor(e,t){this.eventListeners=new Map,this.boundHandlers=new Map,this._programDateTime=null,this.deferredSeekTime=null,this.levelBeforePin=null,this.parsedCues=new Map,this.presentedPts=new Set,this.id3TrackListener=null,this.hls=e,this.video=t,this.setupHlsListeners(),this.setupVideoListeners()}get currentTime(){return this.video.currentTime}get duration(){return this.video.duration}get isLive(){const e=this.hls?.latestLevelDetails;return"boolean"==typeof e?.live?e.live:null}get paused(){return this.video.paused}get muted(){return this.video.muted}set muted(e){this.video.muted=e}get volume(){return this.video.volume}set volume(e){this.video.volume=e}get playbackRate(){return this.video.playbackRate}set playbackRate(e){this.video.playbackRate=e}get videoElement(){return this.video}get supportsParallelBuffering(){return e.isSupported()}get programDateTime(){return this._programDateTime}pause(){this.video.pause()}async play(){await this.video.play()}seek(e){this.video.seeking?this.deferredSeekTime=e:this.video.currentTime=e}get sourceUrl(){return this.hls.url??null}async load(t){return new Promise((s,i)=>{const r=()=>{this.hls.off(e.Events.MANIFEST_PARSED,r),this.hls.off(e.Events.ERROR,n),s()},n=(t,s)=>{s.fatal&&(this.hls.off(e.Events.MANIFEST_PARSED,r),this.hls.off(e.Events.ERROR,n),i(new Error(`HLS load error: ${s.details}`)))};this.hls.on(e.Events.MANIFEST_PARSED,r),this.hls.on(e.Events.ERROR,n),this.hls.loadSource(t),this.hls.attachMedia(this.video)})}unload(){this.deferredSeekTime=null,this.hls.stopLoad(),this.hls.detachMedia()}recreateMediaPipeline(t){return this.deferredSeekTime=null,new Promise((s,i)=>{const r=()=>{this.video.removeEventListener("loadedmetadata",n),this.hls.off(e.Events.FRAG_BUFFERED,a),this.hls.off(e.Events.ERROR,d)},n=()=>{o()},a=()=>{o()},o=()=>{Math.abs(this.video.currentTime-t.startTime)>.5&&(this.video.currentTime=t.startTime),r(),s()},d=(e,t)=>{t.fatal&&(r(),i(new Error(`HLS pipeline recreate error: ${t.details}`)))};this.video.addEventListener("loadedmetadata",n),this.hls.on(e.Events.FRAG_BUFFERED,a),this.hls.on(e.Events.ERROR,d),this.hls.detachMedia(),this.hls.attachMedia(this.video);try{this.video.currentTime=t.startTime}catch{}this.hls.startLoad(t.startTime)})}async preload(t){if(e.isSupported())return new Promise(s=>{const i=new e({startFragPrefetch:!0,autoStartLoad:!0});let r=!1;const n=()=>{if(!r){r=!0,clearTimeout(d),i.off(e.Events.FRAG_LOADED,a),i.off(e.Events.ERROR,o);try{i.destroy()}catch{}s()}},a=()=>n(),o=(e,t)=>{t.fatal&&n()},d=setTimeout(n,8e3);i.on(e.Events.FRAG_LOADED,a),i.on(e.Events.ERROR,o),i.loadSource(t)})}get maxLiveSeekPosition(){const e=this.hls.latestLevelDetails;if(!e?.live)return null;const t=this.hls.liveSyncPosition;return"number"==typeof t&&Number.isFinite(t)?t:null}setVideoQuality(e){if(null===e){if(null===this.levelBeforePin)return!1;const e=this.levelBeforePin;try{return this.hls.nextLevel=e,this.hls.nextLevel!==e?!1:(this.levelBeforePin=null,!0)}catch{return!1}}const t=this.hls.levels;if(!t||t.length<2)return!1;let s=0;for(let e=1;e<t.length;e++)t[e].bitrate<t[s].bitrate&&(s=e);const i=t[s].bitrate;if("number"!=typeof i||!Number.isFinite(i)||i<=0)return!1;const r=null===this.levelBeforePin;let n;r&&(this.levelBeforePin=this.hls.manualLevel);try{this.hls.nextLevel=s,n=this.hls.nextLevel===s}catch{n=!1}return!!n||(r&&(this.levelBeforePin=null),!1)}on(e,t){this.eventListeners.has(e)||this.eventListeners.set(e,new Set),this.eventListeners.get(e).add(t)}off(e,t){const s=this.eventListeners.get(e);s&&s.delete(t)}destroy(){for(const[e,t]of this.boundHandlers)this.video.removeEventListener(e,t);this.boundHandlers.clear(),this.eventListeners.clear(),this.deferredSeekTime=null,this.id3TrackListener&&(this.id3TrackListener.track.removeEventListener("cuechange",this.id3TrackListener.handler),this.id3TrackListener=null),this.parsedCues.clear(),this.presentedPts.clear()}setupHlsListeners(){this.hls.on(e.Events.FRAG_CHANGED,(e,t)=>{t.frag.programDateTime&&this.anchorProgramDateTime(t.frag.programDateTime,t.frag.start)}),this.hls.on(e.Events.LEVEL_LOADED,(e,t)=>{if(t.details.hasProgramDateTime&&t.details.fragments.length>0){const e=t.details.fragments[0];e.programDateTime&&this.anchorProgramDateTime(e.programDateTime,e.start)}}),this.hls.on(e.Events.FRAG_PARSING_METADATA,(e,i)=>{for(const e of i.samples??[]){const i=s(e.data);if(0===i.length){const t={type:"id3",startTime:e.pts,raw:e.data,phase:"parsed"};this.emit("timedmetadata",t);continue}const r=[];for(const s of i){const i={type:"id3",startTime:e.pts,description:s.description,mimeType:s.mimeType,messageData:t(s.data),raw:e.data,phase:"parsed"};r.push(i),this.emit("timedmetadata",i)}this.parsedCues.set(e.pts,r),this.pruneParsedCues()}this.attachId3CueEnterListener()})}attachId3CueEnterListener(){if(this.id3TrackListener)return;const e=this.video.textTracks;if(!e)return;let t;for(let s=0;s<e.length;s++){const i=e[s];if("metadata"===i.kind&&"id3"===i.label){t=i;break}}if(!t||"function"!=typeof t.addEventListener)return;"disabled"===t.mode&&(t.mode="hidden");const s=()=>{const e=t?.activeCues;if(e)for(let t=0;t<e.length;t++){const s=e[t].startTime;if(this.presentedPts.has(s))continue;const i=this.parsedCues.get(s);if(i){this.presentedPts.add(s);for(const e of i)this.emit("timedmetadata",{...e,phase:"presented"})}}};t.addEventListener("cuechange",s),this.id3TrackListener={track:t,handler:s}}pruneParsedCues(){const e=this.video.currentTime-120;for(const t of this.parsedCues.keys())t<e&&(this.parsedCues.delete(t),this.presentedPts.delete(t))}setupVideoListeners(){const e=()=>{this.updateProgramDateTime(),this.emit("timeupdate")},t=()=>{this.emit("ended")},s=()=>{this.emit("error",this.video.error)},i=()=>{if(this.emit("seeked"),null!==this.deferredSeekTime){const e=this.deferredSeekTime;this.deferredSeekTime=null,this.seek(e)}},r=()=>{this.emit("volumechange")},n=()=>{this.emit("waiting")},a=()=>{this.emit("playing")},o=()=>{this.emit("pause")};this.video.addEventListener("timeupdate",e),this.video.addEventListener("ended",t),this.video.addEventListener("error",s),this.video.addEventListener("seeked",i),this.video.addEventListener("volumechange",r),this.video.addEventListener("waiting",n),this.video.addEventListener("playing",a),this.video.addEventListener("pause",o),this.boundHandlers.set("timeupdate",e),this.boundHandlers.set("ended",t),this.boundHandlers.set("error",s),this.boundHandlers.set("seeked",i),this.boundHandlers.set("volumechange",r),this.boundHandlers.set("waiting",n),this.boundHandlers.set("playing",a),this.boundHandlers.set("pause",o)}updateProgramDateTime(){const e=this.hls.levels,t=this.hls.currentLevel;if(t>=0&&e[t]?.details){const s=e[t].details;if(s?.hasProgramDateTime&&s.fragments.length>0){const e=s.fragments[0];e.programDateTime&&this.anchorProgramDateTime(e.programDateTime,e.start)}}}anchorProgramDateTime(e,t){this._programDateTime=new Date(e+1e3*(this.video.currentTime-t))}emit(e,t){const s=this.eventListeners.get(e);if(s)for(const e of s)try{e(t)}catch(e){console.error("[HlsJsAdapter] Event handler error:",e)}}}
1
+ import e from"hls.js";import{decodeUtf8 as t,parseId3GeobFrames as s}from"@dolby-optiview/ads-sdk-core/id3";export class HlsJsAdapter{constructor(e,t){this.eventListeners=new Map,this.boundHandlers=new Map,this._programDateTime=null,this.deferredSeekTime=null,this.endedEmitted=!1,this.levelBeforePin=null,this.parsedCues=new Map,this.presentedPts=new Set,this.id3TrackListener=null,this.hls=e,this.video=t,this.setupHlsListeners(),this.setupVideoListeners()}get currentTime(){return this.video.currentTime}get duration(){return this.video.duration}get isLive(){const e=this.hls?.latestLevelDetails;return"boolean"==typeof e?.live?e.live:null}get paused(){return this.video.paused}get playbackReady(){return this.video.readyState>=HTMLMediaElement.HAVE_CURRENT_DATA&&!this.video.seeking}get muted(){return this.video.muted}set muted(e){this.video.muted=e}get volume(){return this.video.volume}set volume(e){this.video.volume=e}get playbackRate(){return this.video.playbackRate}set playbackRate(e){this.video.playbackRate=e}get videoElement(){return this.video}get supportsParallelBuffering(){return e.isSupported()}get programDateTime(){return this._programDateTime}pause(){this.video.pause()}async play(){await this.video.play()}seek(e){this.video.seeking?this.deferredSeekTime=e:this.video.currentTime=e}get sourceUrl(){return this.hls.url??null}async load(t){return this.endedEmitted=!1,new Promise((s,i)=>{const r=()=>{this.hls.off(e.Events.MANIFEST_PARSED,r),this.hls.off(e.Events.ERROR,n),s()},n=(t,s)=>{s.fatal&&(this.hls.off(e.Events.MANIFEST_PARSED,r),this.hls.off(e.Events.ERROR,n),i(new Error(`HLS load error: ${s.details}`)))};this.hls.on(e.Events.MANIFEST_PARSED,r),this.hls.on(e.Events.ERROR,n),this.hls.loadSource(t),this.hls.attachMedia(this.video)})}unload(){this.deferredSeekTime=null,this.hls.stopLoad(),this.hls.detachMedia()}recreateMediaPipeline(t){return this.deferredSeekTime=null,new Promise((s,i)=>{const r=()=>{this.video.removeEventListener("loadedmetadata",n),this.hls.off(e.Events.FRAG_BUFFERED,a),this.hls.off(e.Events.ERROR,o)},n=()=>{d()},a=()=>{d()},d=()=>{Math.abs(this.video.currentTime-t.startTime)>.5&&(this.video.currentTime=t.startTime),r(),s()},o=(e,t)=>{t.fatal&&(r(),i(new Error(`HLS pipeline recreate error: ${t.details}`)))};this.video.addEventListener("loadedmetadata",n),this.hls.on(e.Events.FRAG_BUFFERED,a),this.hls.on(e.Events.ERROR,o),this.hls.detachMedia(),this.hls.attachMedia(this.video);try{this.video.currentTime=t.startTime}catch{}this.hls.startLoad(t.startTime)})}async preload(t){if(e.isSupported())return new Promise(s=>{const i=new e({startFragPrefetch:!0,autoStartLoad:!0});let r=!1;const n=()=>{if(!r){r=!0,clearTimeout(o),i.off(e.Events.FRAG_LOADED,a),i.off(e.Events.ERROR,d);try{i.destroy()}catch{}s()}},a=()=>n(),d=(e,t)=>{t.fatal&&n()},o=setTimeout(n,8e3);i.on(e.Events.FRAG_LOADED,a),i.on(e.Events.ERROR,d),i.loadSource(t)})}get maxLiveSeekPosition(){const e=this.hls.latestLevelDetails;if(!e?.live)return null;const t=this.hls.liveSyncPosition;return"number"==typeof t&&Number.isFinite(t)?t:null}get targetDuration(){const e=this.hls.latestLevelDetails,t=e?.targetduration;return e?.live&&"number"==typeof t&&Number.isFinite(t)&&t>0?t:null}requestLiveStartOffset(e,t){const s=e;if(null===s||!Number.isFinite(s)||s<=0)return!1;if(this.hls.latestLevelDetails)return!1;const i=this.hls.config;return!(void 0===i.liveMaxLatencyDuration&&i.liveMaxLatencyDurationCount<=s)&&(delete i.liveSyncDuration,i.liveSyncDurationCount=s,this.hls.userConfig.liveSyncDurationCount=s,!0)}setVideoQuality(e){if(null===e){if(null===this.levelBeforePin)return!1;const e=this.levelBeforePin;try{return this.hls.nextLevel=e,this.hls.nextLevel!==e?!1:(this.levelBeforePin=null,!0)}catch{return!1}}const t=this.hls.levels;if(!t||t.length<2)return!1;let s=0;for(let e=1;e<t.length;e++)t[e].bitrate<t[s].bitrate&&(s=e);const i=t[s].bitrate;if("number"!=typeof i||!Number.isFinite(i)||i<=0)return!1;const r=null===this.levelBeforePin;let n;r&&(this.levelBeforePin=this.hls.manualLevel);try{this.hls.nextLevel=s,n=this.hls.nextLevel===s}catch{n=!1}return!!n||(r&&(this.levelBeforePin=null),!1)}on(e,t){this.eventListeners.has(e)||this.eventListeners.set(e,new Set),this.eventListeners.get(e).add(t)}off(e,t){const s=this.eventListeners.get(e);s&&s.delete(t)}destroy(){for(const[e,t]of this.boundHandlers)this.video.removeEventListener(e,t);this.boundHandlers.clear(),this.eventListeners.clear(),this.deferredSeekTime=null,this.id3TrackListener&&(this.id3TrackListener.track.removeEventListener("cuechange",this.id3TrackListener.handler),this.id3TrackListener=null),this.parsedCues.clear(),this.presentedPts.clear()}setupHlsListeners(){this.hls.on(e.Events.MEDIA_ENDED,()=>{this.emitEndedOnce()}),this.hls.on(e.Events.FRAG_CHANGED,(e,t)=>{t.frag.programDateTime&&this.anchorProgramDateTime(t.frag.programDateTime,t.frag.start)}),this.hls.on(e.Events.LEVEL_LOADED,(e,t)=>{if(t.details.hasProgramDateTime&&t.details.fragments.length>0){const e=t.details.fragments[0];e.programDateTime&&this.anchorProgramDateTime(e.programDateTime,e.start)}}),this.hls.on(e.Events.FRAG_PARSING_METADATA,(e,i)=>{for(const e of i.samples??[]){const i=s(e.data);if(0===i.length){const t={type:"id3",startTime:e.pts,raw:e.data,phase:"parsed"};this.emit("timedmetadata",t);continue}const r=[];for(const s of i){const i={type:"id3",startTime:e.pts,description:s.description,mimeType:s.mimeType,messageData:t(s.data),raw:e.data,phase:"parsed"};r.push(i),this.emit("timedmetadata",i)}this.parsedCues.set(e.pts,r),this.pruneParsedCues()}this.attachId3CueEnterListener()})}attachId3CueEnterListener(){if(this.id3TrackListener)return;const e=this.video.textTracks;if(!e)return;let t;for(let s=0;s<e.length;s++){const i=e[s];if("metadata"===i.kind&&"id3"===i.label){t=i;break}}if(!t||"function"!=typeof t.addEventListener)return;"disabled"===t.mode&&(t.mode="hidden");const s=()=>{const e=t?.activeCues;if(e)for(let t=0;t<e.length;t++){const s=e[t].startTime;if(this.presentedPts.has(s))continue;const i=this.parsedCues.get(s);if(i){this.presentedPts.add(s);for(const e of i)this.emit("timedmetadata",{...e,phase:"presented"})}}};t.addEventListener("cuechange",s),this.id3TrackListener={track:t,handler:s}}pruneParsedCues(){const e=this.video.currentTime-120;for(const t of this.parsedCues.keys())t<e&&(this.parsedCues.delete(t),this.presentedPts.delete(t))}setupVideoListeners(){const e=()=>{this.updateProgramDateTime(),this.emit("timeupdate")},t=()=>{this.emitEndedOnce()},s=()=>{this.emit("error",this.video.error)},i=()=>{if(this.emit("seeked"),null!==this.deferredSeekTime){const e=this.deferredSeekTime;this.deferredSeekTime=null,this.seek(e)}},r=()=>{this.emit("volumechange")},n=()=>{this.emit("waiting")},a=()=>{this.endedEmitted=!1,this.emit("playing")},d=()=>{this.emit("pause")};this.video.addEventListener("timeupdate",e),this.video.addEventListener("ended",t),this.video.addEventListener("error",s),this.video.addEventListener("seeked",i),this.video.addEventListener("volumechange",r),this.video.addEventListener("waiting",n),this.video.addEventListener("playing",a),this.video.addEventListener("pause",d),this.boundHandlers.set("timeupdate",e),this.boundHandlers.set("ended",t),this.boundHandlers.set("error",s),this.boundHandlers.set("seeked",i),this.boundHandlers.set("volumechange",r),this.boundHandlers.set("waiting",n),this.boundHandlers.set("playing",a),this.boundHandlers.set("pause",d)}emitEndedOnce(){this.endedEmitted||(this.endedEmitted=!0,this.emit("ended"))}updateProgramDateTime(){const e=this.hls.levels,t=this.hls.currentLevel;if(t>=0&&e[t]?.details){const s=e[t].details;if(s?.hasProgramDateTime&&s.fragments.length>0){const e=s.fragments[0];e.programDateTime&&this.anchorProgramDateTime(e.programDateTime,e.start)}}}anchorProgramDateTime(e,t){this._programDateTime=new Date(e+1e3*(this.video.currentTime-t))}emit(e,t){const s=this.eventListeners.get(e);if(s)for(const e of s)try{e(t)}catch(e){console.error("[HlsJsAdapter] Event handler error:",e)}}}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dolby-optiview/ads-sdk-adapter-hlsjs",
3
- "version": "1.0.0",
3
+ "version": "1.1.1",
4
4
  "description": "HLS.js adapter for OptiView Ads SDK",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -38,7 +38,7 @@
38
38
  },
39
39
  "sideEffects": false,
40
40
  "dependencies": {
41
- "@dolby-optiview/ads-sdk-core": "^1.0.0"
41
+ "@dolby-optiview/ads-sdk-core": "^1.1.1"
42
42
  },
43
43
  "peerDependencies": {
44
44
  "hls.js": "^1.0.0"