@openplayerjs/ads 3.2.0 → 3.4.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 +528 -156
- package/dist/.tsbuildinfo +1 -1
- package/dist/index.js +3552 -2162
- package/dist/index.js.map +1 -1
- package/dist/openplayer-ads.js +1 -1
- package/dist/openplayer-ads.js.map +1 -1
- package/dist/types/ad-dom.d.ts.map +1 -1
- package/dist/types/ads.d.ts.map +1 -1
- package/dist/types/schedule.d.ts +1 -1
- package/dist/types/simid.d.ts +126 -9
- package/dist/types/simid.d.ts.map +1 -1
- package/dist/types/vast-parser.d.ts +1 -1
- package/dist/types/vast-parser.d.ts.map +1 -1
- package/package.json +6 -6
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
# @
|
|
1
|
+
# @openplayerjs/ads
|
|
2
2
|
|
|
3
|
-
> VAST / VMAP ad plugin for [OpenPlayerJS](https://openplayerjs.com).
|
|
3
|
+
> VAST / VMAP / SSAI / Hybrid ad plugin for [OpenPlayerJS](https://openplayerjs.com).
|
|
4
4
|
|
|
5
5
|
[](https://www.npmjs.com/package/@openplayerjs/ads)
|
|
6
6
|
[](https://www.npmjs.com/package/@openplayerjs/ads)
|
|
@@ -10,15 +10,23 @@
|
|
|
10
10
|
|
|
11
11
|
---
|
|
12
12
|
|
|
13
|
-
This package
|
|
13
|
+
This package provides ad insertion for OpenPlayerJS. It supports three delivery modes selectable via `adDelivery`:
|
|
14
|
+
|
|
15
|
+
| Mode | Strategy class | Description |
|
|
16
|
+
| ------------------ | ------------------ | ------------------------------------------------------------------------------------------------------------ |
|
|
17
|
+
| `'csai'` (default) | `CsaiAdStrategy` | Client-side ad insertion — fetches VAST/VMAP, renders an ad `<video>` |
|
|
18
|
+
| `'ssai'` | `SsaiAdStrategy` | Server-side ad stitching — ads are baked into the HLS stream; boundaries detected via SCTE-35 TextTrack cues |
|
|
19
|
+
| `'hybrid'` | `HybridAdStrategy` | CSAI triggered by SCTE-35 OUT cues from the HLS engine |
|
|
20
|
+
|
|
21
|
+
---
|
|
14
22
|
|
|
15
23
|
## Installation
|
|
16
24
|
|
|
17
25
|
```bash
|
|
18
|
-
npm install @
|
|
26
|
+
npm install @openplayerjs/ads @openplayerjs/core
|
|
19
27
|
```
|
|
20
28
|
|
|
21
|
-
`@dailymotion/vast-client` and `@dailymotion/vmap` are bundled automatically —
|
|
29
|
+
`@dailymotion/vast-client` and `@dailymotion/vmap` are bundled automatically — no separate install needed.
|
|
22
30
|
|
|
23
31
|
---
|
|
24
32
|
|
|
@@ -32,30 +40,292 @@ npm install @openplayer/ads @openplayer/core
|
|
|
32
40
|
- Click-through tracking
|
|
33
41
|
- Waterfall and playlist ad source modes
|
|
34
42
|
- Preload-aware VMAP fetching (respects `preload="none"`)
|
|
43
|
+
- **SSAI** — server-side ad stitching via SCTE-35 TextTrack cues
|
|
44
|
+
- **Hybrid** — CSAI triggered by SCTE-35 OUT cues from the HLS engine
|
|
35
45
|
- **SIMID 1.2** — Secure Interactive Media Interface Definition (interactive overlays)
|
|
36
46
|
- **OMID** — Open Measurement Interface Definition (third-party viewability / verification)
|
|
37
47
|
|
|
38
48
|
---
|
|
39
49
|
|
|
40
|
-
##
|
|
50
|
+
## Quick start
|
|
51
|
+
|
|
52
|
+
### CSAI — client-side ad insertion (default)
|
|
41
53
|
|
|
42
54
|
```ts
|
|
43
|
-
import { Core } from '@
|
|
44
|
-
import { AdsPlugin } from '@
|
|
55
|
+
import { Core } from '@openplayerjs/core';
|
|
56
|
+
import { AdsPlugin } from '@openplayerjs/ads';
|
|
45
57
|
|
|
46
58
|
const core = new Core(video, {
|
|
47
59
|
plugins: [
|
|
48
60
|
new AdsPlugin({
|
|
61
|
+
// adDelivery: 'csai' is the default — no need to set it explicitly
|
|
49
62
|
breaks: [
|
|
50
|
-
{ at: 'preroll',
|
|
51
|
-
{ at:
|
|
52
|
-
{ at: 'postroll',
|
|
63
|
+
{ at: 'preroll', source: { type: 'VAST', src: 'https://example.com/preroll.xml' } },
|
|
64
|
+
{ at: 30, source: { type: 'VAST', src: 'https://example.com/midroll.xml' } },
|
|
65
|
+
{ at: 'postroll', source: { type: 'VAST', src: 'https://example.com/postroll.xml' } },
|
|
53
66
|
],
|
|
54
67
|
}),
|
|
55
68
|
],
|
|
56
69
|
});
|
|
57
70
|
```
|
|
58
71
|
|
|
72
|
+
### VMAP (break schedule from server)
|
|
73
|
+
|
|
74
|
+
```ts
|
|
75
|
+
new AdsPlugin({
|
|
76
|
+
sources: [{ type: 'VMAP', src: 'https://example.com/vmap.xml' }],
|
|
77
|
+
});
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
### SSAI — server-side ad stitching
|
|
81
|
+
|
|
82
|
+
SSAI mode reads SCTE-35 splice commands directly from the HLS metadata TextTrack — no VAST requests are
|
|
83
|
+
made. Ads are baked into the stitched stream; the plugin tracks break boundaries and fires lifecycle
|
|
84
|
+
events. Both ID3/DataCue (`enableID3MetadataCues`) and EXT-X-DATERANGE/VTTCue (`enableDateRangeMetadataCues`)
|
|
85
|
+
formats are supported automatically.
|
|
86
|
+
|
|
87
|
+
#### Minimal setup
|
|
88
|
+
|
|
89
|
+
`eventSink` is optional — `ads:break:start`, `ads:break:end`, and `ads:quartile` still fire on the EventBus.
|
|
90
|
+
|
|
91
|
+
```ts
|
|
92
|
+
import { Core } from '@openplayerjs/core';
|
|
93
|
+
import { AdsPlugin } from '@openplayerjs/ads';
|
|
94
|
+
|
|
95
|
+
const core = new Core(video, {
|
|
96
|
+
plugins: [new AdsPlugin({ adDelivery: 'ssai' })],
|
|
97
|
+
});
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
#### Full lifecycle tracking via `eventSink`
|
|
101
|
+
|
|
102
|
+
`eventSink` receives `impression` (break started), `quartile` (25/50/75/100%), and `complete` (break ended).
|
|
103
|
+
|
|
104
|
+
```ts
|
|
105
|
+
import { Core } from '@openplayerjs/core';
|
|
106
|
+
import { AdsPlugin, type AdLifecycleEvent } from '@openplayerjs/ads';
|
|
107
|
+
|
|
108
|
+
const core = new Core(video, {
|
|
109
|
+
plugins: [
|
|
110
|
+
new AdsPlugin({
|
|
111
|
+
adDelivery: 'ssai',
|
|
112
|
+
ssai: {
|
|
113
|
+
eventSink: (event: AdLifecycleEvent) => {
|
|
114
|
+
switch (event.type) {
|
|
115
|
+
case 'impression':
|
|
116
|
+
sendBeacon('/ads/impression', { breakId: event.breakId, src: event.contentSrc });
|
|
117
|
+
break;
|
|
118
|
+
case 'quartile':
|
|
119
|
+
// event.metadata.quartile: 25 | 50 | 75 | 100
|
|
120
|
+
sendBeacon('/ads/quartile', { breakId: event.breakId, q: event.metadata?.quartile });
|
|
121
|
+
break;
|
|
122
|
+
case 'complete':
|
|
123
|
+
sendBeacon('/ads/complete', { breakId: event.breakId });
|
|
124
|
+
break;
|
|
125
|
+
case 'error':
|
|
126
|
+
console.error('SSAI break error', event.breakId, event.metadata);
|
|
127
|
+
break;
|
|
128
|
+
}
|
|
129
|
+
},
|
|
130
|
+
},
|
|
131
|
+
}),
|
|
132
|
+
],
|
|
133
|
+
});
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
#### Listening via EventBus
|
|
137
|
+
|
|
138
|
+
Use `core.on()` instead of (or alongside) `eventSink` — e.g. to show/hide an "Ad" badge in the UI:
|
|
139
|
+
|
|
140
|
+
```ts
|
|
141
|
+
core.on('ads:break:start', ({ id }) => showAdBadge());
|
|
142
|
+
core.on('ads:break:end', ({ id }) => hideAdBadge());
|
|
143
|
+
core.on('ads:quartile', ({ breakId, quartile }) => {
|
|
144
|
+
if (quartile === 50) analytics.track('ssai_midpoint', { breakId });
|
|
145
|
+
});
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
#### Parsing raw SCTE-35 data
|
|
149
|
+
|
|
150
|
+
Use the exported `decodeSplice` helper to inspect splice commands directly in your own TextTrack listener
|
|
151
|
+
(e.g. to read custom descriptors or drive logic outside of SSAI mode):
|
|
152
|
+
|
|
153
|
+
```ts
|
|
154
|
+
import { decodeSplice, type SpliceCommand } from '@openplayerjs/ads';
|
|
155
|
+
|
|
156
|
+
video.textTracks.addEventListener('addtrack', (e) => {
|
|
157
|
+
const track = e.track;
|
|
158
|
+
if (track?.kind !== 'metadata') return;
|
|
159
|
+
track.mode = 'hidden';
|
|
160
|
+
track.addEventListener('cuechange', () => {
|
|
161
|
+
for (const cue of track.activeCues ?? []) {
|
|
162
|
+
const raw = cue as any;
|
|
163
|
+
// ID3 / DataCue path (enableID3MetadataCues)
|
|
164
|
+
if (raw.data instanceof ArrayBuffer) {
|
|
165
|
+
const cmd: SpliceCommand | null = decodeSplice(raw.data);
|
|
166
|
+
if (cmd?.type === 'splice_insert') {
|
|
167
|
+
console.log('splice_insert out=%s dur=%ss', cmd.outOfNetwork, cmd.durationSecs);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
// EXT-X-DATERANGE base64 path (enableDateRangeMetadataCues)
|
|
171
|
+
if (typeof raw.attr?.['X-SCTE35'] === 'string') {
|
|
172
|
+
const cmd = decodeSplice(raw.attr['X-SCTE35']);
|
|
173
|
+
console.log('daterange splice:', cmd);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
});
|
|
177
|
+
});
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
> **Note:** No CodePen is provided for SSAI — it requires an HLS stream with embedded SCTE-35 markers
|
|
181
|
+
> (e.g. from AWS MediaTailor or Mux). No stable public test stream is available.
|
|
182
|
+
|
|
183
|
+
### Hybrid — CSAI triggered by SCTE-35
|
|
184
|
+
|
|
185
|
+
Hybrid mode combines CSAI ad rendering with SCTE-35 cue detection. When the HLS engine fires a splice-out
|
|
186
|
+
cue, `resolveScteUrl` maps it to a VAST tag URL and the plugin runs a standard CSAI break inline.
|
|
187
|
+
`HybridAdStrategy` extends `CsaiAdStrategy`, so every CSAI option (`breaks`, `requestInterceptor`,
|
|
188
|
+
`labels`, `omid`, etc.) is available alongside the two required hybrid fields.
|
|
189
|
+
|
|
190
|
+
#### Minimal setup
|
|
191
|
+
|
|
192
|
+
```ts
|
|
193
|
+
import { Core } from '@openplayerjs/core';
|
|
194
|
+
import { HlsMediaEngine } from '@openplayerjs/hls';
|
|
195
|
+
import { AdsPlugin, type ScteOutCue } from '@openplayerjs/ads';
|
|
196
|
+
|
|
197
|
+
const hlsEngine = new HlsMediaEngine();
|
|
198
|
+
|
|
199
|
+
const core = new Core(video, {
|
|
200
|
+
plugins: [
|
|
201
|
+
hlsEngine,
|
|
202
|
+
new AdsPlugin({
|
|
203
|
+
adDelivery: 'hybrid',
|
|
204
|
+
hybrid: {
|
|
205
|
+
scteSource: hlsEngine,
|
|
206
|
+
resolveScteUrl: ({ id, plannedDuration }: ScteOutCue) =>
|
|
207
|
+
`https://ads.example.com/vast?id=${id}&dur=${plannedDuration ?? 30}`,
|
|
208
|
+
},
|
|
209
|
+
}),
|
|
210
|
+
],
|
|
211
|
+
});
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
#### Async URL resolution — call your ad decision server
|
|
215
|
+
|
|
216
|
+
`resolveScteUrl` may return a `Promise`. Return `null` or `undefined` to skip a cue entirely.
|
|
217
|
+
|
|
218
|
+
```ts
|
|
219
|
+
new AdsPlugin({
|
|
220
|
+
adDelivery: 'hybrid',
|
|
221
|
+
hybrid: {
|
|
222
|
+
scteSource: hlsEngine,
|
|
223
|
+
resolveScteUrl: async ({ id, plannedDuration }: ScteOutCue) => {
|
|
224
|
+
// Skip bumpers shorter than 5 s
|
|
225
|
+
if (plannedDuration != null && plannedDuration < 5) return null;
|
|
226
|
+
|
|
227
|
+
const params = new URLSearchParams({ breakId: id, dur: String(plannedDuration ?? 30) });
|
|
228
|
+
const { vastUrl } = await fetch(`/ads/decision?${params}`).then((r) => r.json());
|
|
229
|
+
return vastUrl ?? null; // null → skip this break
|
|
230
|
+
},
|
|
231
|
+
},
|
|
232
|
+
});
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
#### Preroll + SCTE-triggered midrolls
|
|
236
|
+
|
|
237
|
+
Add static `breaks` to schedule a CSAI preroll before the stream starts, while SCTE-35 cues drive
|
|
238
|
+
midrolls during playback:
|
|
239
|
+
|
|
240
|
+
```ts
|
|
241
|
+
new AdsPlugin({
|
|
242
|
+
adDelivery: 'hybrid',
|
|
243
|
+
hybrid: {
|
|
244
|
+
scteSource: hlsEngine,
|
|
245
|
+
resolveScteUrl: ({ id, plannedDuration }: ScteOutCue) =>
|
|
246
|
+
`https://ads.example.com/vast?id=${id}&dur=${plannedDuration ?? 30}`,
|
|
247
|
+
breaks: [{ at: 'preroll', source: { type: 'VAST', src: 'https://ads.example.com/preroll.xml' } }],
|
|
248
|
+
},
|
|
249
|
+
});
|
|
250
|
+
```
|
|
251
|
+
|
|
252
|
+
#### Full configuration — waterfall sources, request interceptor, OMID, UI labels, companions
|
|
253
|
+
|
|
254
|
+
```ts
|
|
255
|
+
import { Core } from '@openplayerjs/core';
|
|
256
|
+
import { HlsMediaEngine } from '@openplayerjs/hls';
|
|
257
|
+
import { AdsPlugin, type AdLifecycleEvent, type AdTagRequest, type ScteOutCue } from '@openplayerjs/ads';
|
|
258
|
+
|
|
259
|
+
const hlsEngine = new HlsMediaEngine();
|
|
260
|
+
|
|
261
|
+
const core = new Core(video, {
|
|
262
|
+
plugins: [
|
|
263
|
+
hlsEngine,
|
|
264
|
+
new AdsPlugin({
|
|
265
|
+
adDelivery: 'hybrid',
|
|
266
|
+
debug: true, // verbose console output
|
|
267
|
+
|
|
268
|
+
hybrid: {
|
|
269
|
+
// ── Required ──────────────────────────────────────────────────────────
|
|
270
|
+
scteSource: hlsEngine,
|
|
271
|
+
resolveScteUrl: async ({ id, plannedDuration }: ScteOutCue) => {
|
|
272
|
+
const params = new URLSearchParams({ breakId: id, dur: String(plannedDuration ?? 30) });
|
|
273
|
+
const { vastUrl } = await fetch(`/ads/decision?${params}`).then((r) => r.json());
|
|
274
|
+
return vastUrl ?? null;
|
|
275
|
+
},
|
|
276
|
+
|
|
277
|
+
// ── Static breaks (run alongside SCTE-triggered midrolls) ─────────────
|
|
278
|
+
breaks: [
|
|
279
|
+
{
|
|
280
|
+
id: 'bumper-pre', // IDs containing "bumper" are treated as bumpers
|
|
281
|
+
at: 'preroll',
|
|
282
|
+
source: { type: 'VAST', src: 'https://ads.example.com/bumper.xml' },
|
|
283
|
+
once: true, // play only once per page load
|
|
284
|
+
},
|
|
285
|
+
{
|
|
286
|
+
at: 'preroll',
|
|
287
|
+
// Waterfall: try primary ad server, fall back to house ad
|
|
288
|
+
sources: [
|
|
289
|
+
{ type: 'VAST', src: 'https://primary-ads.example.com/preroll.xml' },
|
|
290
|
+
{ type: 'VAST', src: 'https://house-ads.example.com/preroll.xml' },
|
|
291
|
+
],
|
|
292
|
+
},
|
|
293
|
+
],
|
|
294
|
+
adSourcesMode: 'waterfall', // stop at first successful source per break
|
|
295
|
+
|
|
296
|
+
// ── Network ───────────────────────────────────────────────────────────
|
|
297
|
+
requestInterceptor: (req: AdTagRequest) => {
|
|
298
|
+
const url = new URL(req.url);
|
|
299
|
+
url.searchParams.set('cust_params', 'env=prod&uid=abc123');
|
|
300
|
+
return { ...req, url: url.toString() };
|
|
301
|
+
},
|
|
302
|
+
|
|
303
|
+
// ── UI ────────────────────────────────────────────────────────────────
|
|
304
|
+
mountSelector: '#ad-container',
|
|
305
|
+
companionSelector: '#companion-banner',
|
|
306
|
+
nonLinearSelector: '#nonlinear-overlay',
|
|
307
|
+
labels: {
|
|
308
|
+
skip: 'Skip ad',
|
|
309
|
+
advertisement: 'Advertisement',
|
|
310
|
+
adEnded: 'Ad finished',
|
|
311
|
+
},
|
|
312
|
+
resumeContent: true, // resume content after every non-postroll break (default)
|
|
313
|
+
breakTolerance: 0.5, // seconds of tolerance for midroll cue matching
|
|
314
|
+
|
|
315
|
+
// ── Measurement ───────────────────────────────────────────────────────
|
|
316
|
+
omid: { accessMode: 'domain' }, // 'limited' (default) | 'domain'
|
|
317
|
+
eventSink: (event: AdLifecycleEvent) => {
|
|
318
|
+
fetch('/analytics/ads', { method: 'POST', body: JSON.stringify(event) });
|
|
319
|
+
},
|
|
320
|
+
},
|
|
321
|
+
}),
|
|
322
|
+
],
|
|
323
|
+
});
|
|
324
|
+
```
|
|
325
|
+
|
|
326
|
+
> **Note:** No CodePen is provided for Hybrid — it requires an HLS stream carrying SCTE-35 OUT cues.
|
|
327
|
+
> Use the existing [CodePen collection](https://codepen.io/collection/kkwgWj) for CSAI examples.
|
|
328
|
+
|
|
59
329
|
---
|
|
60
330
|
|
|
61
331
|
## UMD / CDN usage
|
|
@@ -67,7 +337,7 @@ const core = new Core(video, {
|
|
|
67
337
|
<script>
|
|
68
338
|
const player = new OpenPlayerJS('player', {
|
|
69
339
|
ads: {
|
|
70
|
-
breaks: [{ at: 'preroll',
|
|
340
|
+
breaks: [{ at: 'preroll', source: { type: 'VAST', src: 'https://example.com/vast.xml' } }],
|
|
71
341
|
},
|
|
72
342
|
});
|
|
73
343
|
player.init();
|
|
@@ -82,27 +352,75 @@ The ads bundle self-registers as `window.OpenPlayerPlugins.ads` and is discovere
|
|
|
82
352
|
|
|
83
353
|
### `AdsPluginConfig`
|
|
84
354
|
|
|
85
|
-
| Option
|
|
86
|
-
|
|
|
87
|
-
| `
|
|
88
|
-
| `
|
|
89
|
-
| `debug`
|
|
355
|
+
| Option | Type | Default | Description |
|
|
356
|
+
| ------------ | ------------------------------ | -------- | ------------------------------------------------------------------ |
|
|
357
|
+
| `adDelivery` | `'csai' \| 'ssai' \| 'hybrid'` | `'csai'` | Selects the delivery strategy |
|
|
358
|
+
| `sources` | `AdsSource \| AdsSource[]` | — | Top-level VAST/VMAP/NONLINEAR sources (shorthand for CSAI) |
|
|
359
|
+
| `debug` | `boolean` | `false` | Enable verbose ads logging |
|
|
360
|
+
| `csai` | `CsaiAdConfig` | — | CSAI-specific options (takes precedence over flat fields) |
|
|
361
|
+
| `ssai` | `SsaiAdConfig` | — | SSAI-specific options |
|
|
362
|
+
| `hybrid` | `HybridAdConfig` | — | Hybrid-specific options (`scteSource` + `resolveScteUrl` required) |
|
|
363
|
+
|
|
364
|
+
> **Backward compatibility:** All flat CSAI fields (`breaks`, `interceptPlayForPreroll`, `mountEl`, etc.) are still accepted at the top level. The `csai` sub-object takes precedence when both are present.
|
|
365
|
+
|
|
366
|
+
### `CsaiAdConfig`
|
|
367
|
+
|
|
368
|
+
All fields are optional. When provided under `csai`, these override the flat equivalents.
|
|
369
|
+
|
|
370
|
+
| Option | Type | Default | Description |
|
|
371
|
+
| ------------------------- | ------------------------------------------------------------- | ---------------------------------- | ------------------------------------------------------------------------------------------------------------------------ |
|
|
372
|
+
| `breaks` | `AdsBreakConfig[]` | `[]` | Ad breaks to schedule |
|
|
373
|
+
| `interceptPlayForPreroll` | `boolean` | auto | Intercept the first `play()` to run a preroll. Defaults to `true` when a preroll break or VAST/VMAP source is configured |
|
|
374
|
+
| `autoPlayOnReady` | `boolean` | `false` | Play ads immediately when the plugin is ready, without waiting for a play gesture |
|
|
375
|
+
| `mountEl` | `HTMLElement` | — | Container element for the ad overlay |
|
|
376
|
+
| `mountSelector` | `string` | — | CSS selector for the ad overlay container (used when `mountEl` is not provided) |
|
|
377
|
+
| `resumeContent` | `boolean` | `true` | Resume content playback after a non-postroll break ends |
|
|
378
|
+
| `preferredMediaTypes` | `string[]` | `['video/mp4', 'video/webm', ...]` | Ordered list of media MIME types for selecting the best ad media file |
|
|
379
|
+
| `breakTolerance` | `number` | `0.25` | Seconds of tolerance when matching a midroll cue point to the current time |
|
|
380
|
+
| `adSourcesMode` | `'waterfall' \| 'playlist'` | `'waterfall'` | How multiple sources in a break are handled. See below |
|
|
381
|
+
| `omid` | `OmidConfig` | `{}` | OMID access mode (`'limited'` or `'domain'`) |
|
|
382
|
+
| `labels` | `{ skip?, advertisement?, adEnded? }` | — | Custom text for the skip button and ad labels |
|
|
383
|
+
| `requestInterceptor` | `(req: AdTagRequest) => AdTagRequest \| null \| Promise<...>` | — | Called before every ad tag fetch; return `null` to suppress the request |
|
|
384
|
+
| `eventSink` | `(event: AdLifecycleEvent) => void` | — | Receives structured lifecycle events (`request`, `impression`, `quartile`, `complete`, `skip`, `error`) |
|
|
385
|
+
|
|
386
|
+
### `SsaiAdConfig`
|
|
387
|
+
|
|
388
|
+
| Option | Type | Description |
|
|
389
|
+
| ----------- | ----------------------------------- | ------------------------------------ |
|
|
390
|
+
| `eventSink` | `(event: AdLifecycleEvent) => void` | Receives SSAI break lifecycle events |
|
|
391
|
+
|
|
392
|
+
### `HybridAdConfig`
|
|
393
|
+
|
|
394
|
+
Extends `CsaiAdConfig` with two required fields:
|
|
395
|
+
|
|
396
|
+
| Option | Type | Description |
|
|
397
|
+
| ---------------- | ------------------------------------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------- |
|
|
398
|
+
| `scteSource` | `ScteSource` | Duck-typed reference to an engine that fires SCTE-35 cues (e.g. `HlsMediaEngine`). Must expose `onCue?: (cue: ScteOutCue) => void` |
|
|
399
|
+
| `resolveScteUrl` | `(cue: ScteOutCue) => string \| null \| undefined \| Promise<...>` | Maps a SCTE-35 OUT cue to a VAST tag URL. Return `null` or `undefined` to skip the cue |
|
|
90
400
|
|
|
91
401
|
### `AdsBreakConfig`
|
|
92
402
|
|
|
93
|
-
|
|
403
|
+
| Field | Type | Description |
|
|
404
|
+
| --------- | ----------------------------------- | -------------------------------------------------------------------------------------------------- |
|
|
405
|
+
| `at` | `'preroll' \| 'postroll' \| number` | When to play the break. Numbers are seconds from content start |
|
|
406
|
+
| `source` | `AdsSource` | Single VAST/VMAP/NONLINEAR source |
|
|
407
|
+
| `sources` | `AdsSource[]` | Multiple sources (used with `adSourcesMode`) |
|
|
408
|
+
| `id` | `string` | Optional break ID. Breaks whose `id` contains `"bumper"` (case-insensitive) are treated as bumpers |
|
|
409
|
+
| `once` | `boolean` | Play this break only once per page load |
|
|
410
|
+
|
|
411
|
+
### `AdsSource`
|
|
94
412
|
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
413
|
+
```ts
|
|
414
|
+
type AdsSource = {
|
|
415
|
+
type: 'VAST' | 'VMAP' | 'NONLINEAR';
|
|
416
|
+
src: string; // URL or inline XML
|
|
417
|
+
};
|
|
418
|
+
```
|
|
101
419
|
|
|
102
420
|
### `adSourcesMode` explained
|
|
103
421
|
|
|
104
|
-
- **`'waterfall'`** (default): A single
|
|
105
|
-
- **`'playlist'`**:
|
|
422
|
+
- **`'waterfall'`** (default): A single `AdsBreakConfig` has a `sources` array. The plugin tries each source in order and stops at the first success. Use this for ad tag fallbacks.
|
|
423
|
+
- **`'playlist'`**: One `AdsBreakConfig` per source; each plays as a separate sequential break. Use this for pre-defined ad pods.
|
|
106
424
|
|
|
107
425
|
---
|
|
108
426
|
|
|
@@ -110,80 +428,81 @@ Each object in the `breaks` array describes one ad break:
|
|
|
110
428
|
|
|
111
429
|
### `AdsPlugin` methods
|
|
112
430
|
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
|
116
|
-
|
|
|
117
|
-
| `
|
|
118
|
-
| `
|
|
119
|
-
| `
|
|
120
|
-
| `
|
|
431
|
+
| Method | Signature | Description |
|
|
432
|
+
| --------------------- | ------------------------------------------------------------ | ------------------------------------------------------------------------------------ |
|
|
433
|
+
| `setup` | `setup(ctx: PluginContext): void` | Called by the plugin registry — not for direct use |
|
|
434
|
+
| `playAds` | `playAds(url: string): Promise<boolean>` | Trigger a one-off ad break from a VAST URL. Returns `true` if at least one ad played |
|
|
435
|
+
| `playAdsFromXml` | `playAdsFromXml(xml: string): Promise<boolean>` | Trigger a one-off break from inline VAST XML |
|
|
436
|
+
| `requestSkip` | `requestSkip(reason?: 'button' \| 'close' \| 'api'): void` | Skip the currently playing ad |
|
|
437
|
+
| `getDueMidrollBreaks` | `getDueMidrollBreaks(t: number): AdsBreakConfig[]` | Return all midroll breaks at or before `t` seconds |
|
|
438
|
+
| `getDueMidrollBreak` | `getDueMidrollBreak(t: number): AdsBreakConfig \| undefined` | Return the earliest unplayed midroll break at or before `t` |
|
|
439
|
+
| `destroy` | `destroy(): void` | Release all subscriptions and DOM state |
|
|
121
440
|
|
|
122
441
|
### `installAds(Core)` and `extendAds(core, plugin)`
|
|
123
442
|
|
|
124
|
-
|
|
443
|
+
Two helpers expose `core.ads` and `core.playAds()` for UMD / imperative usage:
|
|
125
444
|
|
|
126
445
|
```ts
|
|
127
|
-
import { installAds, extendAds } from '@
|
|
128
|
-
import { Core } from '@
|
|
446
|
+
import { installAds, extendAds } from '@openplayerjs/ads';
|
|
447
|
+
import { Core } from '@openplayerjs/core';
|
|
129
448
|
|
|
130
|
-
//
|
|
449
|
+
// Adds Core.prototype.ads getter and Core.prototype.playAds() once.
|
|
131
450
|
installAds(Core);
|
|
132
451
|
|
|
133
|
-
//
|
|
134
|
-
extendAds(core,
|
|
452
|
+
// Wires a specific plugin instance to a player instance.
|
|
453
|
+
extendAds(core, adsPlugin);
|
|
135
454
|
|
|
136
|
-
//
|
|
137
|
-
core.
|
|
138
|
-
core.
|
|
139
|
-
core.ads.resume();
|
|
140
|
-
core.ads.playAds('https://example.com/vast.xml');
|
|
455
|
+
// Now available on the instance:
|
|
456
|
+
core.playAds('https://example.com/vast.xml');
|
|
457
|
+
core.playAds('<VAST version="3.0">...</VAST>'); // XML is detected automatically
|
|
141
458
|
```
|
|
142
459
|
|
|
143
460
|
### Manual ad playback
|
|
144
461
|
|
|
145
462
|
```ts
|
|
146
|
-
//
|
|
147
|
-
|
|
463
|
+
// From a VAST URL:
|
|
464
|
+
const played = await adsPlugin.playAds('https://example.com/vast.xml');
|
|
465
|
+
|
|
466
|
+
// From inline VAST XML:
|
|
467
|
+
const played = await adsPlugin.playAdsFromXml('<VAST version="3.0">...</VAST>');
|
|
148
468
|
|
|
149
|
-
//
|
|
150
|
-
|
|
469
|
+
// Skip the current ad programmatically:
|
|
470
|
+
adsPlugin.requestSkip('api');
|
|
151
471
|
```
|
|
152
472
|
|
|
153
473
|
---
|
|
154
474
|
|
|
155
475
|
## Events
|
|
156
476
|
|
|
157
|
-
All
|
|
158
|
-
|
|
159
|
-
| Event | Payload | When it fires
|
|
160
|
-
| --------------------- | ---------------------------------- |
|
|
161
|
-
| `ads:requested` | `{ url, at, id }` | An ad tag
|
|
162
|
-
| `ads:loaded` | `{ break, count }` |
|
|
163
|
-
| `ads:break:start` | `{ id, kind, at }` | An ad break is
|
|
164
|
-
| `ads:break:end` | `{ id, kind, at }` | An ad break finished; content
|
|
165
|
-
| `ads:ad:start` | `{ break, index }` | An individual ad
|
|
166
|
-
| `ads:ad:end` | `{ break, index }` | An individual ad
|
|
167
|
-
| `ads:impression` | `{ break, index }` |
|
|
168
|
-
| `ads:quartile` | `{ break, quartile }` | Playback reached
|
|
169
|
-
| `ads:timeupdate` | `{ break, currentTime, duration }` |
|
|
170
|
-
| `ads:duration` | `{ break, duration }` |
|
|
171
|
-
| `ads:skip` | `{ break, reason }` |
|
|
172
|
-
| `ads:clickthrough` | `{ break, url }` |
|
|
173
|
-
| `ads:pause` | `{ break }` |
|
|
174
|
-
| `ads:resume` | `{ break }` |
|
|
175
|
-
| `ads:mute` | `{ break }` |
|
|
176
|
-
| `ads:unmute` | `{ break }` |
|
|
177
|
-
| `ads:volumeChange` | `{ break, volume, muted }` | Volume changed during
|
|
178
|
-
| `ads:allAdsCompleted` | `{ break }` | All scheduled
|
|
179
|
-
| `ads:error` | `{ reason, error?, url? }` |
|
|
180
|
-
|
|
181
|
-
###
|
|
477
|
+
All events are prefixed with `ads:`. Listen with `ctx.events.on(...)` or via `core.on(...)`.
|
|
478
|
+
|
|
479
|
+
| Event | Payload | When it fires |
|
|
480
|
+
| --------------------- | ---------------------------------- | ---------------------------------------- |
|
|
481
|
+
| `ads:requested` | `{ url, at, id }` | An ad tag request was sent |
|
|
482
|
+
| `ads:loaded` | `{ break, count }` | VAST/VMAP was parsed and ads are ready |
|
|
483
|
+
| `ads:break:start` | `{ id, kind, at }` | An ad break is starting; content pauses |
|
|
484
|
+
| `ads:break:end` | `{ id, kind, at }` | An ad break finished; content resumes |
|
|
485
|
+
| `ads:ad:start` | `{ break, index }` | An individual ad started |
|
|
486
|
+
| `ads:ad:end` | `{ break, index }` | An individual ad finished |
|
|
487
|
+
| `ads:impression` | `{ break, index }` | Ad impression recorded (once per ad) |
|
|
488
|
+
| `ads:quartile` | `{ break, quartile }` | Playback reached 25 / 50 / 75 / 100 % |
|
|
489
|
+
| `ads:timeupdate` | `{ break, currentTime, duration }` | Ad time updated |
|
|
490
|
+
| `ads:duration` | `{ break, duration }` | Ad total duration became known |
|
|
491
|
+
| `ads:skip` | `{ break, reason }` | Ad was skipped |
|
|
492
|
+
| `ads:clickthrough` | `{ break, url }` | User clicked the ad |
|
|
493
|
+
| `ads:pause` | `{ break }` | Ad was paused |
|
|
494
|
+
| `ads:resume` | `{ break }` | Paused ad was resumed |
|
|
495
|
+
| `ads:mute` | `{ break }` | Ad was muted |
|
|
496
|
+
| `ads:unmute` | `{ break }` | Ad was unmuted |
|
|
497
|
+
| `ads:volumeChange` | `{ break, volume, muted }` | Volume changed during ad |
|
|
498
|
+
| `ads:allAdsCompleted` | `{ break }` | All scheduled breaks have finished |
|
|
499
|
+
| `ads:error` | `{ reason, error?, url? }` | Error during request, parse, or playback |
|
|
500
|
+
|
|
501
|
+
### Listening to events
|
|
182
502
|
|
|
183
503
|
```ts
|
|
184
|
-
core.on('ads:break:start', ({
|
|
185
|
-
console.log(`Ad break "${kind}"
|
|
186
|
-
// Hide any custom overlays or pause your UI here
|
|
504
|
+
core.on('ads:break:start', ({ kind, at }) => {
|
|
505
|
+
console.log(`Ad break "${kind}" at ${at}s starting`);
|
|
187
506
|
});
|
|
188
507
|
|
|
189
508
|
core.on('ads:break:end', () => {
|
|
@@ -191,38 +510,112 @@ core.on('ads:break:end', () => {
|
|
|
191
510
|
});
|
|
192
511
|
|
|
193
512
|
core.on('ads:quartile', ({ quartile }) => {
|
|
194
|
-
if (quartile === 50) console.log('
|
|
513
|
+
if (quartile === 50) console.log('Reached ad midpoint');
|
|
195
514
|
});
|
|
196
515
|
|
|
197
516
|
core.on('ads:error', ({ reason, error }) => {
|
|
198
|
-
console.warn('Ad
|
|
199
|
-
// Content playback resumes automatically
|
|
517
|
+
console.warn('Ad error:', reason, error);
|
|
518
|
+
// Content playback resumes automatically
|
|
519
|
+
});
|
|
520
|
+
```
|
|
521
|
+
|
|
522
|
+
### `AdLifecycleEvent` (structured sink)
|
|
523
|
+
|
|
524
|
+
For server-side or analytics use, provide `eventSink` in the config:
|
|
525
|
+
|
|
526
|
+
```ts
|
|
527
|
+
new AdsPlugin({
|
|
528
|
+
csai: {
|
|
529
|
+
eventSink: (event) => {
|
|
530
|
+
// event.type: 'request' | 'impression' | 'quartile' | 'complete' | 'skip' | 'error'
|
|
531
|
+
// event.adId, event.breakId, event.contentSrc, event.elapsedSec, event.metadata
|
|
532
|
+
fetch('/analytics', { method: 'POST', body: JSON.stringify(event) });
|
|
533
|
+
},
|
|
534
|
+
},
|
|
200
535
|
});
|
|
201
536
|
```
|
|
202
537
|
|
|
203
538
|
---
|
|
204
539
|
|
|
205
|
-
##
|
|
540
|
+
## Ad source modes — waterfall vs playlist
|
|
206
541
|
|
|
207
|
-
|
|
542
|
+
### Waterfall (default)
|
|
208
543
|
|
|
209
|
-
|
|
544
|
+
Try sources in order, stop at the first success:
|
|
210
545
|
|
|
211
|
-
|
|
546
|
+
```ts
|
|
547
|
+
new AdsPlugin({
|
|
548
|
+
adSourcesMode: 'waterfall',
|
|
549
|
+
breaks: [
|
|
550
|
+
{
|
|
551
|
+
at: 'preroll',
|
|
552
|
+
sources: [
|
|
553
|
+
{ type: 'VAST', src: 'https://primary-ad-server.com/vast.xml' },
|
|
554
|
+
{ type: 'VAST', src: 'https://backup-ad-server.com/vast.xml' },
|
|
555
|
+
{ type: 'VAST', src: 'https://house-ad.com/vast.xml' },
|
|
556
|
+
],
|
|
557
|
+
},
|
|
558
|
+
],
|
|
559
|
+
});
|
|
560
|
+
```
|
|
212
561
|
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
562
|
+
### Playlist
|
|
563
|
+
|
|
564
|
+
Play each source as its own sequential break:
|
|
565
|
+
|
|
566
|
+
```ts
|
|
567
|
+
new AdsPlugin({
|
|
568
|
+
adSourcesMode: 'playlist',
|
|
569
|
+
breaks: [
|
|
570
|
+
{ at: 'preroll', source: { type: 'VAST', src: 'https://ads.example.com/ad1.xml' } },
|
|
571
|
+
{ at: 'preroll', source: { type: 'VAST', src: 'https://ads.example.com/ad2.xml' } },
|
|
572
|
+
],
|
|
573
|
+
});
|
|
574
|
+
```
|
|
575
|
+
|
|
576
|
+
---
|
|
577
|
+
|
|
578
|
+
## `requestInterceptor` — modify or suppress ad tag requests
|
|
579
|
+
|
|
580
|
+
```ts
|
|
581
|
+
new AdsPlugin({
|
|
582
|
+
csai: {
|
|
583
|
+
requestInterceptor: async (req) => {
|
|
584
|
+
// Add targeting parameters to every ad request.
|
|
585
|
+
const url = new URL(req.url);
|
|
586
|
+
url.searchParams.set('cust_params', 'section=sports&user_id=abc123');
|
|
587
|
+
return { ...req, url: url.toString() };
|
|
588
|
+
},
|
|
589
|
+
},
|
|
590
|
+
});
|
|
591
|
+
|
|
592
|
+
// Return null to suppress a specific request:
|
|
593
|
+
new AdsPlugin({
|
|
594
|
+
csai: {
|
|
595
|
+
requestInterceptor: (req) => {
|
|
596
|
+
if (req.adType === 'vmap') return null; // Skip VMAP fetches
|
|
597
|
+
return req;
|
|
598
|
+
},
|
|
599
|
+
},
|
|
600
|
+
});
|
|
601
|
+
```
|
|
602
|
+
|
|
603
|
+
---
|
|
604
|
+
|
|
605
|
+
## SIMID 1.2 — Interactive Ad Creatives
|
|
606
|
+
|
|
607
|
+
[SIMID](https://iabtechlab.com/standards/simid/) allows VAST creatives to render interactive overlays in an iframe alongside the ad video.
|
|
608
|
+
|
|
609
|
+
When a VAST response includes `<InteractiveCreativeFile apiFramework="SIMID">`, the plugin automatically:
|
|
220
610
|
|
|
221
|
-
|
|
611
|
+
1. Mounts the creative URL in a sandboxed `<iframe>` over the ad video.
|
|
612
|
+
2. Completes the SIMID 1.2 handshake (`createSession` → `SIMID:Player:init` → `SIMID:Player:startCreative`).
|
|
613
|
+
3. Keeps the creative in sync with playback (progress, pause, resume, volume, skip, stop).
|
|
614
|
+
4. Handles creative-initiated actions: skip, stop, click-through, fullscreen, tracking events.
|
|
222
615
|
|
|
223
|
-
|
|
616
|
+
No configuration is required — SIMID is detected and managed automatically.
|
|
224
617
|
|
|
225
|
-
The iframe is sandboxed with `allow-scripts allow-same-origin allow-forms allow-popups`.
|
|
618
|
+
The iframe is sandboxed with `allow-scripts allow-same-origin allow-forms allow-popups`.
|
|
226
619
|
|
|
227
620
|
### Testing SIMID ads
|
|
228
621
|
|
|
@@ -231,8 +624,7 @@ new AdsPlugin({
|
|
|
231
624
|
sources: [
|
|
232
625
|
{
|
|
233
626
|
type: 'VAST',
|
|
234
|
-
|
|
235
|
-
src: `https://pubads.g.doubleclick.net/gampad/ads?iu=/21775744923/external/simid&description_url=https%3A%2F%2Fdevelopers.google.com%2Finteractive-media-ads&sz=640x480&gdfp_req=1&output=vast&unviewed_position_start=1&env=vp&correlator=${Date.now()}`,
|
|
627
|
+
src: `https://pubads.g.doubleclick.net/gampad/ads?iu=/21775744923/external/simid&sz=640x480&gdfp_req=1&output=vast&unviewed_position_start=1&env=vp&correlator=${Date.now()}`,
|
|
236
628
|
},
|
|
237
629
|
],
|
|
238
630
|
});
|
|
@@ -242,70 +634,69 @@ new AdsPlugin({
|
|
|
242
634
|
|
|
243
635
|
## OMID — Open Measurement
|
|
244
636
|
|
|
245
|
-
[OMID
|
|
637
|
+
[OMID](https://iabtechlab.com/standards/open-measurement-sdk/) enables third-party viewability and verification measurement.
|
|
246
638
|
|
|
247
|
-
###
|
|
639
|
+
### Setup
|
|
248
640
|
|
|
249
|
-
|
|
641
|
+
Load the IAB OMID Session Client SDK before the player:
|
|
250
642
|
|
|
251
643
|
```html
|
|
252
|
-
<!-- Load the IAB OMID Session Client SDK before the player -->
|
|
253
644
|
<script src="https://iab-mm-omid.com/omweb-v1.js"></script>
|
|
254
645
|
```
|
|
255
646
|
|
|
256
|
-
The plugin detects `window.OmidSessionClient` at runtime. If
|
|
647
|
+
The plugin detects `window.OmidSessionClient` at runtime. If absent, OMID silently no-ops.
|
|
257
648
|
|
|
258
649
|
### How it works
|
|
259
650
|
|
|
260
|
-
When the VAST response contains `<AdVerifications
|
|
261
|
-
|
|
262
|
-
1. Injects each `<JavaScriptResource>` verification script as a `<script>` tag into the page.
|
|
263
|
-
2. Instantiates an `OmidSession` with the ad video element and all verification resources.
|
|
264
|
-
3. Fires the required OMID lifecycle events:
|
|
265
|
-
- `impression()` on ad impression
|
|
266
|
-
- `loaded()` with skip/autoplay/position metadata
|
|
267
|
-
- `start(duration, volume)` when the ad begins playing
|
|
268
|
-
- `firstQuartile()`, `midpoint()`, `thirdQuartile()`, `complete()` at the correct percentages
|
|
269
|
-
- `pause()` / `resume()` on pause/play
|
|
270
|
-
- `skipped()` when the ad is skipped
|
|
271
|
-
- `volumeChange(volume)` on volume changes
|
|
272
|
-
- `playerStateChange('fullscreen')` on fullscreen toggle
|
|
273
|
-
4. Calls `destroy()` when the break ends.
|
|
274
|
-
|
|
275
|
-
No extra configuration is needed — OMID runs automatically when the SDK and `<AdVerifications>` are both present.
|
|
651
|
+
When the VAST response contains `<AdVerifications>`, the plugin injects the verification scripts and fires the required OMID lifecycle events automatically (`impression`, `loaded`, `start`, `firstQuartile`, `midpoint`, `thirdQuartile`, `complete`, `pause`, `resume`, `skipped`, `volumeChange`, `playerStateChange`).
|
|
276
652
|
|
|
277
653
|
### Access mode
|
|
278
654
|
|
|
279
|
-
By default, verification scripts run in `limited` access mode (no cross-origin DOM access). You can override this in the plugin config:
|
|
280
|
-
|
|
281
655
|
```ts
|
|
282
656
|
new AdsPlugin({
|
|
283
|
-
|
|
657
|
+
csai: {
|
|
658
|
+
omid: { accessMode: 'domain' }, // 'limited' (default) | 'domain'
|
|
659
|
+
},
|
|
284
660
|
sources: [{ type: 'VAST', src: '...' }],
|
|
285
661
|
});
|
|
286
662
|
```
|
|
287
663
|
|
|
288
|
-
|
|
664
|
+
---
|
|
665
|
+
|
|
666
|
+
## Architecture
|
|
667
|
+
|
|
668
|
+
`AdsPlugin` is a thin dispatcher. On `setup()` it selects the appropriate strategy:
|
|
669
|
+
|
|
670
|
+
```
|
|
671
|
+
AdsPlugin.setup()
|
|
672
|
+
├── adDelivery === 'ssai' → new SsaiAdStrategy()
|
|
673
|
+
├── adDelivery === 'hybrid' → new HybridAdStrategy() (extends CsaiAdStrategy)
|
|
674
|
+
└── default ('csai') → new CsaiAdStrategy()
|
|
675
|
+
```
|
|
676
|
+
|
|
677
|
+
Each strategy implements `AdSessionStrategy`:
|
|
289
678
|
|
|
290
679
|
```ts
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
}
|
|
680
|
+
type AdSessionStrategy = {
|
|
681
|
+
readonly mode: AdDeliveryMode;
|
|
682
|
+
init(ctx: PluginContext, config: AdsPluginConfig): void;
|
|
683
|
+
destroy(): void;
|
|
684
|
+
playAds?(url: string): Promise<boolean>;
|
|
685
|
+
getDueMidrollBreaks?(t: number): AdsBreakConfig[];
|
|
686
|
+
getDueMidrollBreak?(t: number): AdsBreakConfig | undefined;
|
|
687
|
+
requestSkip?(reason?: 'button' | 'close' | 'api'): void;
|
|
688
|
+
};
|
|
300
689
|
```
|
|
301
690
|
|
|
691
|
+
All public methods on `AdsPlugin` delegate to the active strategy, so you can swap delivery modes without changing call sites.
|
|
692
|
+
|
|
302
693
|
---
|
|
303
694
|
|
|
304
695
|
## Dependencies
|
|
305
696
|
|
|
306
697
|
| Package | Type | Required version |
|
|
307
698
|
| -------------------------- | ------- | ---------------- |
|
|
308
|
-
| `@
|
|
699
|
+
| `@openplayerjs/core` | peer | `>=3.0.0` |
|
|
309
700
|
| `@dailymotion/vast-client` | bundled | `>=6.0.0` |
|
|
310
701
|
| `@dailymotion/vmap` | bundled | `>=3.0.0` |
|
|
311
702
|
|
|
@@ -313,37 +704,18 @@ new AdsPlugin({
|
|
|
313
704
|
|
|
314
705
|
## Compatibility with iframe engines (YouTube, Vimeo, etc.)
|
|
315
706
|
|
|
316
|
-
`AdsPlugin` is designed for native `<video>`/`<audio>` content
|
|
317
|
-
|
|
318
|
-
### 1. `media.duration` read from the native element
|
|
319
|
-
|
|
320
|
-
`getDueMidrollBreaks()` and related percentage-based midroll logic read `this.ctx.core.media.duration` directly from the native `<video>` element. When a YouTube (or other iframe) engine is active, that element is hidden and has nothing loaded — `media.duration` will be `NaN`, so percentage-based midrolls will never trigger.
|
|
321
|
-
|
|
322
|
-
**Fix needed:** replace `this.ctx.core.media.duration` with `this.ctx.core.duration` (which is synced from the active surface via `bindSurfaceSync()`).
|
|
323
|
-
|
|
324
|
-
### 2. `media.currentTime` read from the native element
|
|
325
|
-
|
|
326
|
-
`shouldInterceptPreroll()` checks `media?.currentTime` to skip a preroll if playback has already advanced past 0.25 s. For iframe engines, `media.currentTime` is always `0` (the native element is unloaded), so this guard never fires correctly.
|
|
327
|
-
|
|
328
|
-
**Fix needed:** replace `media?.currentTime` with `this.ctx.core.currentTime`.
|
|
329
|
-
|
|
330
|
-
### 3. DOM `timeupdate` listener on the native element
|
|
331
|
-
|
|
332
|
-
`bindBreakScheduler()` listens to the native `'timeupdate'` DOM event on `core.media` to fire midroll checks. Iframe engines never trigger this event on the native element — they emit `timeupdate` through the player EventBus instead.
|
|
333
|
-
|
|
334
|
-
**Fix needed:** replace the native DOM listener with an EventBus listener (`ctx.events.on('timeupdate', ...)`) so it fires for all engine types.
|
|
335
|
-
|
|
336
|
-
### 4. Native `'play'` capture listener in preroll interceptors
|
|
337
|
-
|
|
338
|
-
`bindPrerollInterceptors()` attaches a capture-phase `'play'` listener to `core.media` to intercept prerolls before the browser starts rendering. For iframe engines the native `<video>` never emits `'play'` (it is hidden and unloaded), so the preroll interceptor is never triggered.
|
|
707
|
+
`AdsPlugin` is designed for native `<video>`/`<audio>` content. Several gaps exist when used alongside iframe-based engines — ads + YouTube is not a supported combination yet:
|
|
339
708
|
|
|
340
|
-
|
|
709
|
+
- **`media.duration`**: midroll cue-point logic reads the native `<video>` duration directly, which is `NaN` for iframe engines.
|
|
710
|
+
- **`media.currentTime`**: preroll guards check native `currentTime`, which is always `0` for iframe engines.
|
|
711
|
+
- **`timeupdate` listener**: `bindBreakScheduler()` listens on the native DOM element, which never fires for iframe engines.
|
|
712
|
+
- **`play` capture listener**: `bindPrerollInterceptors()` uses a capture-phase listener on the native element that is never triggered for iframe engines.
|
|
341
713
|
|
|
342
714
|
---
|
|
343
715
|
|
|
344
716
|
## Code samples
|
|
345
717
|
|
|
346
|
-
|
|
718
|
+
Ready-to-run examples covering preroll, midroll, postroll, waterfall sources, VMAP, SSAI, and non-linear ads are available in the CodePen collection:
|
|
347
719
|
|
|
348
720
|
CodePen Collection: [https://codepen.io/collection/kkwgWj](https://codepen.io/collection/kkwgWj)
|
|
349
721
|
|