@dolby-optiview/ads-sdk 1.2.1 → 1.3.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/ai/reference/error-codes.md +13 -5
- package/ai/reference/knowledge-base.md +85 -15
- package/dist/HlsAdQueue.d.ts +60 -0
- package/dist/HlsAdQueue.js +1 -0
- package/dist/NativeVideoAdapter.d.ts +4 -1
- package/dist/NativeVideoAdapter.js +1 -1
- package/dist/RoutingAdAdapter.d.ts +40 -1
- package/dist/RoutingAdAdapter.js +1 -1
- package/dist/defaultAdAdapter.d.ts +3 -0
- package/dist/defaultAdAdapter.js +1 -1
- package/package.json +3 -3
|
@@ -18,6 +18,7 @@ of the public contract and are never renamed.
|
|
|
18
18
|
| `DA-MANIFEST-INTERCEPT-FAILED` | manifest | warn | The customer manifest-response interceptor threw; the un-modified parsed manifest was used instead. |
|
|
19
19
|
| `DA-MANIFEST-REQUEST-INTERCEPT-FAILED` | manifest | warn | The customer manifest-request interceptor threw; the normal network fetch of the original URL was used instead. |
|
|
20
20
|
| `DA-MANIFEST-SIGNATURE-INVALID` | manifest | error | The break-manifest response failed detached-JWS signature verification (missing/malformed X-Manifest-Signature, unsupported alg, unknown kid, unknown critical param, or signature mismatch); the manifest was rejected and no breaks were scheduled. context: url, reason. |
|
|
21
|
+
| `DA-MANIFEST-EVENT-INVALID` | manifest | warn | A channel event was ignored because its time window or configuration is invalid, or because events are unsupported for the manifest timebase. context: eventId and index when available. |
|
|
21
22
|
| `DA-PDT-MISSING` | playback | warn | No EXT-X-PROGRAM-DATE-TIME once content is playing (or after the startup backstop window); wallclock-timebase breaks fall back to the system clock. |
|
|
22
23
|
| `DA-ANVATO-CUE-PENDING` | break | info | ptsSource: 'anvatoCue' — no Anvato in-stream cue observed yet for a PTS break; the break stays pending until its cue arrives. |
|
|
23
24
|
| `DA-ANVATO-CUE-MATCHED` | break | info | ptsSource: 'anvatoCue' — a PTS break's start was matched to an observed Anvato cue and scheduled at the cue's media time. |
|
|
@@ -34,7 +35,8 @@ of the public contract and are never renamed.
|
|
|
34
35
|
| `DA-BREAK-WINDOW-ESCAPED` | break | warn | An active wallclock break outlived its window on the stream clock (ad stall, bogus trigger position, or a re-fired completed break); the SDK ended it and returned to content. context.overrunSec says by how much. |
|
|
35
36
|
| `DA-BREAK-EVENT-UNSUPPORTED` | break | warn | A break is triggered by a spec event this SDK version does not schedule on this platform (web schedules `start`, `pause` and `end`; Android and iOS do not schedule `end` yet); the break was ignored. context.event names the event. |
|
|
36
37
|
| `DA-BREAK-SUPPRESSED-OVERLAP` | break | warn | A break was suppressed because an ad was already playing. |
|
|
37
|
-
| `DA-BREAK-TRANSITION` | break | info | Measured duration of a playback transition into an ad break, between two ads, between chained breaks, or back to content. |
|
|
38
|
+
| `DA-BREAK-TRANSITION` | break | info | Measured duration of a playback transition into an ad break, between two ads, between chained breaks, or back to content. A break-to-break switch runs from the outgoing ad's end to the incoming ad's first frame and adds path (queued, late-queued or cold) and, when the incoming ad was buffered, leadMs. On web an ad-to-ad switch adds path (queued or cold) and leadMs too. |
|
|
39
|
+
| `DA-BREAK-CHAIN` | chaining | info | Summary of one chain of back-to-back breaks, emitted once when content plays again (or the chain is flushed): chainId, per break its id, scheduledStart, plannedDurationSec, beganAtMs, endedAtMs, playedSec, announcedDuringChain and path, per switch its gapMs and path, contentResumeMs and totalMs. |
|
|
38
40
|
| `DA-AD-SURFACE-REVEALED` | break | info | The ad surface became visible: how long it was held back waiting for the ad's first frame, and whether it was revealed because that frame arrived or because the wait timed out. |
|
|
39
41
|
| `DA-BREAK-CUT-LATE` | break | warn | timebase: 'wallclock', all platforms — content was cut more than 0.5s after the break's manifest start on the stream's Program Date Time, so the viewer saw the stream's own break (slate) instead of the ad until then; typically the ad's first frame was not ready at the cut and content kept playing under the cross-fade. The replaced window is unaffected: the resume anchor is the content time of the manifest start whatever the cut landed on. Context carries breakId, lateSec (cut minus start on the PDT axis), start (the manifest start), cutPdt (the PDT at the cut), cutAt (currentTime at the cut) and anchor; iOS/Android add waitedForAdMs when the cut waited for the ad surface. |
|
|
40
42
|
| `DA-PREROLL-CONTENT-HELD` | break | info | Content playback was held at session start until a pending pre-roll began, so no content is shown before the ad. |
|
|
@@ -54,6 +56,7 @@ of the public contract and are never renamed.
|
|
|
54
56
|
| `DA-CHAIN-RESOLVER-ERROR` | chaining | warn | The chained-successor resolver threw; chaining was skipped for this break. |
|
|
55
57
|
| `DA-GAM-NO-ASSET-KEY` | gam | warn | Retired: the GAM stream identity now comes from the manifest's vendorConfiguration. Kept declared so the code is never reused; see DA-GAM-CONFIG-MISSING. |
|
|
56
58
|
| `DA-GAM-SESSION-FAILED` | gam | error | The GAM/IMA session failed to initialize; GAM breaks will not serve. |
|
|
59
|
+
| `DA-GAM-SESSION-PRELOADED` | gam | info | A GAM/IMA session was preloaded for the next event; context includes the event boundary and GAM pod identity. |
|
|
57
60
|
| `DA-GAM-START-DEFERRED` | gam | info | The SGAI IMA stream request is deferred until the configured ad start delay has been played; context includes adStartDelaySec. |
|
|
58
61
|
| `DA-GAM-CONFIG-RESOLVED` | gam | info | The GAM stream identity for this session was resolved from the manifest's vendorConfiguration. |
|
|
59
62
|
| `DA-GAM-CONFIG-MISSING` | gam | error | The break manifest carries no usable "gam" vendorConfiguration entry; GAM assets are skipped. |
|
|
@@ -64,6 +67,11 @@ of the public contract and are never renamed.
|
|
|
64
67
|
| `DA-EXTERNAL-VENDOR-INVALID` | manifest | warn | External vendorConfiguration entries were ignored because provider was blank or assetParameters was not an object. context: count. |
|
|
65
68
|
| `DA-EXTERNAL-VENDOR-DUPLICATE` | manifest | warn | External vendorConfiguration entries repeated a provider; the first valid entry was used. context: providers. |
|
|
66
69
|
| `DA-CONTENT-PLAYBACK-ERROR` | playback | error | The content player reported a playback error. |
|
|
70
|
+
| `DA-EXTERNAL-PLAYBACK-CHANGED` | playback | info | The content player's external (AirPlay) playback state changed; context carries the new state and the target. |
|
|
71
|
+
| `DA-EXTERNAL-PLAYBACK-BLOCKED` | playback | warn | AirPlay was requested while an ad break is active or about to start; the content player stays on the device and the request is retried when the break ends. |
|
|
72
|
+
| `DA-SSB-SWITCH-FAILED` | playback | warn | Switching the content player to the SSB stream for AirPlay failed (context.reason: no-restore-source, load-error, load-timeout, external-not-active or receiver-paused); the SDK restores the previous content. |
|
|
73
|
+
| `DA-SSB-RESTORE-FAILED` | playback | error | The previous content source could not be reloaded after the SSB stream (AirPlay ended or the switch failed); the session stays in ssb delivery until the app reloads its content. |
|
|
74
|
+
| `DA-EXTERNAL-PLAYBACK-ALLOWED` | playback | info | An AirPlay request passed the ad-break guard; context.trigger says whether it was the request itself or the retry after a break. |
|
|
67
75
|
| `DA-CONTENT-RESUME-STALLED` | playback | warn | Content did not resume within the watchdog window after a break that paused it ended; the SDK re-issued play() once. iOS only for now. |
|
|
68
76
|
| `DA-CONTENT-RECOVERY-MODE-RESOLVED` | playback | info | The resolved post-break content recovery mode for the session (recreate or none); context carries configured and resolved. |
|
|
69
77
|
| `DA-CONTENT-PIPELINE-RECREATED` | playback | info | The content player's media pipeline was rebuilt at break end before content resumed (recovery mode recreate); context carries breakId, startTime and durationMs. |
|
|
@@ -76,7 +84,7 @@ of the public contract and are never renamed.
|
|
|
76
84
|
| `DA-AD-START-TIMEOUT` | playback | error | The ad did not load, render a first frame or start within its start budget; reported as CMCD adec. |
|
|
77
85
|
| `DA-AD-START-INTERRUPTED` | playback | warn | The ad element's play() was interrupted by a browser-initiated pause before the ad started (Chromium AbortError, seen on Samsung Tizen); the SDK re-issued play() once. context: breakId, error. |
|
|
78
86
|
| `DA-AD-DURATION-PREPARED` | playback | info | The ad media for a break was prepared and its real playable duration is now known, BEFORE the first frame. context: breakId, assetId, index, total, preparedDurationSec (the loaded media's own duration — on a GAM pod, the stitched pod's length), declaredDurationSec (what the manifest said about this asset, when it said anything) and breakDurationSec (the slot the break declares). Emitted per asset, so a multi-asset break reports one per asset and the break total is their sum. The three together separate a short break caused by a short POD from one caused by a short request or a late join. |
|
|
79
|
-
| `DA-DELIVERY-MODE-RESOLVED` | session | info | Delivery mode (sgai/ssai) resolved for the session at start and again on an ads-channel switch from the manifest's delivery rules (or the explicit client config), with the detected device profile. |
|
|
87
|
+
| `DA-DELIVERY-MODE-RESOLVED` | session | info | Delivery mode (sgai/ssai/ssb) resolved for the session at start and again on an ads-channel switch from the manifest's delivery rules (or the explicit client config), with the detected device profile; context.source is external-playback or external-playback-ended when AirPlay switched the session to the SSB stream and back. |
|
|
80
88
|
| `DA-TARGETING-VALUE-UNKNOWN` | manifest | warn | A targeting value is not recognized and the targeted entry does not match. |
|
|
81
89
|
| `DA-TARGETING-FIELD-UNKNOWN` | manifest | warn | A targeting field is not recognized and the targeted entry does not match. |
|
|
82
90
|
| `DA-TARGETING-INVALID` | manifest | warn | A targeting constraint is malformed or incomplete and the targeted entry does not match. |
|
|
@@ -91,7 +99,7 @@ of the public contract and are never renamed.
|
|
|
91
99
|
| `DA-INSERTION-TYPE-IGNORED-SSAI` | session | warn | adInsertionType is ignored in SSAI mode; the stitcher controls ad insertion server-side. |
|
|
92
100
|
| `DA-VAST-UNSUPPORTED-FORMAT` | break | warn | A VAST asset appeared in an unsupported break format; VAST is only supported in single, double, and lshape_ad. The break was skipped. |
|
|
93
101
|
| `DA-ASSET-MACRO-UNKNOWN` | break | warn | An asset parameter value contained a macro the SDK has no value for (an unknown or unavailable OptiView macro, or a registered customer macro that returned no value); the parameter was still sent, with the macro kept as its literal token. Raised once per parameter key and macro. context: key, macro, breakId, assetId. |
|
|
94
|
-
| `DA-ASSET-PARAMETER-OMITTED` | break | info | An asset parameter value contained a customer macro that is empty (value `null`), so the parameter was not sent; for GAM `cust_params
|
|
102
|
+
| `DA-ASSET-PARAMETER-OMITTED` | break | info | An asset parameter value contained a customer macro that is empty (value `null`), so the parameter was not sent; for GAM `cust_params`, and for a static-asset value with a literal `&`, only the `&`-separated pair containing the macro was dropped. Raised once per parameter key and macro (per dropped pair when the value is split into pairs). context: key, macro, pair (split values only), breakId, assetId. |
|
|
95
103
|
| `DA-VAST-SSAI-UNSUPPORTED` | session | warn | VAST assets are not supported in SSAI mode (VAST is SGAI-only); the asset was skipped. |
|
|
96
104
|
| `DA-VAST-IMA-ERROR` | gam | error | Google IMA reported an error while loading or playing a VAST (client-side / CSAI) ad. |
|
|
97
105
|
| `DA-VAST-NO-FILL` | gam | error | The VAST ad server returned no ad (empty response, no ads after wrappers or no assets); reported as CMCD adec. |
|
|
@@ -111,8 +119,8 @@ of the public contract and are never renamed.
|
|
|
111
119
|
| `DA-AD-SKIPPED` | playback | info | The viewer skipped the active ad break (controls.skipOffset honored). |
|
|
112
120
|
| `DA-AD-SKIP-SUPPRESSED` | playback | info | A skip request was suppressed (no skipOffset declared, or requested before the offset). |
|
|
113
121
|
| `DA-AD-CLICKED` | playback | info | The viewer clicked the ad on screen (adclick emitted). |
|
|
114
|
-
| `DA-LIVE-START-PUSHBACK` | playback | info | Initial live playback requested a backward seek to the configured target-duration distance. Context includes enabledBy (config
|
|
115
|
-
| `DA-LIVE-START-PUSHBACK-SKIPPED` | playback | info | Initial live playback was left unchanged. The reason identifies a disabled adjustment (
|
|
122
|
+
| `DA-LIVE-START-PUSHBACK` | playback | info | Initial live playback requested a backward seek to the configured target-duration distance. Context includes enabledBy (always config), targetDurationSec, targetDurationSource, initialOffsetSec, initialOffsetTargetDurations, requestedOffsetSec, requestedOffsetTargetDurations, targetOffsetSec, targetOffsetTargetDurations, rewindSec, rewindTargetDurations, target and windowClamped. This is a request, not proof of the landing. |
|
|
123
|
+
| `DA-LIVE-START-PUSHBACK-SKIPPED` | playback | info | Initial live playback was left unchanged. The reason identifies a disabled adjustment (liveStartPushback.enabled is not true; enabledBy is always config), an already sufficient offset, missing target duration or seekable range, invalid configuration, or no safe backward target. Available target-duration and offset measurements are included. |
|
|
116
124
|
| `DA-LIVE-START-PUSHBACK-LANDED` | playback | info | Measured initial-live seek landing. Context includes the requested target and targetDurationSec, landed, errorSec, liveEdgeAtLanding, landedOffsetSec and landedOffsetTargetDurations. A timeout or failed seek reports landed=null, never an inferred success. |
|
|
117
125
|
| `DA-RESUME-SEEK` | playback | info | Content was resume-seeked to the resolved post-break position (insertion type policy). |
|
|
118
126
|
| `DA-RESUME-UNADDRESSABLE` | playback | warn | A break played through the content player (picture-in-picture), so content came back on a reloaded source whose timeline has its own origin, and that source published neither a program-date-time nor a seekable range to re-address the resume point against. Content stays where the reload left it — near the live edge — rather than being seeked to a position that does not exist on the new timeline. Context carries breakId, target (the position on the pre-borrow timeline) and offset. |
|
|
@@ -72,6 +72,11 @@ also restart with the new manifest's ids once the switch is applied.
|
|
|
72
72
|
**Fix:** Nothing when the backend re-pointed the stream on purpose. Repeated
|
|
73
73
|
flips between channels indicate an unstable manifest source.
|
|
74
74
|
|
|
75
|
+
## DA-MANIFEST-EVENT-INVALID
|
|
76
|
+
|
|
77
|
+
**Cause:** A channel event was ignored for an invalid time window or configuration, or because events are unsupported for the manifest timebase.
|
|
78
|
+
**Fix:** Use `context.eventId` or `context.index` to locate the event, validate its window and configuration, and use events only with the wallclock timebase.
|
|
79
|
+
|
|
75
80
|
## External provider parameters
|
|
76
81
|
|
|
77
82
|
Call `getExternalVendorConfiguration()` to retrieve all current provider
|
|
@@ -221,8 +226,8 @@ never corrects the landing position.
|
|
|
221
226
|
## DA-LIVE-START-PUSHBACK
|
|
222
227
|
|
|
223
228
|
Initial live playback requests a backward seek to five declared target durations behind the
|
|
224
|
-
reported live edge (
|
|
225
|
-
`enabledBy`
|
|
229
|
+
reported live edge (opt-in: only `liveStartPushback.enabled: true` turns it on, for every `ptsSource`).
|
|
230
|
+
`enabledBy` is always `config`. `targetDurationSec` and `targetDurationSource` identify
|
|
226
231
|
its input. `initialOffsetSec` / `initialOffsetTargetDurations`, `requestedOffsetSec` /
|
|
227
232
|
`requestedOffsetTargetDurations`, and `rewindSec` / `rewindTargetDurations` explain the arithmetic.
|
|
228
233
|
At 3×TD it rewinds 2×TD; at 4×TD it rewinds 1×TD. `windowClamped` indicates insufficient DVR depth.
|
|
@@ -230,8 +235,8 @@ This request is not evidence of a successful landing and does not change later b
|
|
|
230
235
|
|
|
231
236
|
## DA-LIVE-START-PUSHBACK-SKIPPED
|
|
232
237
|
|
|
233
|
-
`disabled` means the adjustment is off
|
|
234
|
-
|
|
238
|
+
`disabled` means the adjustment is off because `liveStartPushback.enabled` is not `true` (unset or
|
|
239
|
+
`false`); `ptsSource: 'anvatoCue'` does not turn it on. `already-behind` means the viewer starts at or
|
|
235
240
|
beyond the minimum distance. `no-target-duration`
|
|
236
241
|
requires a configured `liveStartPushback.targetDurationSeconds` when the adapter cannot provide
|
|
237
242
|
actual declared HLS metadata. `no-range` and `no-backward-target` mean there is no safe backward
|
|
@@ -741,6 +746,52 @@ position the pipeline was seeded at) and `durationMs` (the rebuild cost).
|
|
|
741
746
|
**Fix:** None — use `durationMs` to measure what the rebuild costs on a panel;
|
|
742
747
|
expect roughly a second of re-buffering instead of an instant resume.
|
|
743
748
|
|
|
749
|
+
## DA-EXTERNAL-PLAYBACK-BLOCKED
|
|
750
|
+
|
|
751
|
+
**Cause:** The manifest carries a server-side beaconing (`ssb`) stream, so the
|
|
752
|
+
SDK keeps the content player off AirPlay around ad breaks, and a viewer asked
|
|
753
|
+
for AirPlay while a break was active (`context.reason: break-active`) or within
|
|
754
|
+
`externalPlayback.blockBeforeBreakSec` seconds of the next one
|
|
755
|
+
(`break-imminent`). `context` also carries `breakId`, `secondsUntilBreak` and
|
|
756
|
+
`trigger`.
|
|
757
|
+
**Fix:** None needed; the video stays on the device and the SDK retries when
|
|
758
|
+
the break ends if the route is still on AirPlay. Shorten the window with
|
|
759
|
+
`externalPlayback.blockBeforeBreakSec` (`0` blocks only during a break).
|
|
760
|
+
|
|
761
|
+
## DA-EXTERNAL-PLAYBACK-ALLOWED
|
|
762
|
+
|
|
763
|
+
**Cause:** An AirPlay request passed the ad-break guard. `context.trigger` is
|
|
764
|
+
`connecting` for the request itself and `break-ended` for the retry after a
|
|
765
|
+
break.
|
|
766
|
+
**Fix:** None needed; informational. The gate stays closed.
|
|
767
|
+
|
|
768
|
+
## DA-SSB-SWITCH-FAILED
|
|
769
|
+
|
|
770
|
+
**Cause:** AirPlay started on a channel with an `ssb` stream and the SDK could
|
|
771
|
+
not play that stream on the receiver. `context.reason` is `no-restore-source`
|
|
772
|
+
(the adapter has no `sourceUrl`), `load-error`, `load-timeout` (the stream was
|
|
773
|
+
not ready within 15 s), `external-not-active` (the route was not active 1.5 s
|
|
774
|
+
after the stream was ready) or `receiver-paused`.
|
|
775
|
+
**Fix:** Implement `PlayerAdapter.sourceUrl` for `no-restore-source`; otherwise
|
|
776
|
+
none needed: the SDK restores the previous content.
|
|
777
|
+
|
|
778
|
+
## DA-SSB-RESTORE-FAILED
|
|
779
|
+
|
|
780
|
+
**Cause:** After the SSB stream the previous content source could not be
|
|
781
|
+
reloaded (two attempts). The session stays in `ssb` delivery and the AirPlay
|
|
782
|
+
gate stays closed.
|
|
783
|
+
**Fix:** Reload your content and start a new session, or end the session.
|
|
784
|
+
|
|
785
|
+
## DA-EXTERNAL-PLAYBACK-CHANGED
|
|
786
|
+
|
|
787
|
+
**Cause:** The content player's external (AirPlay) playback state changed.
|
|
788
|
+
`context` carries `state` (`connecting`, `active` or `inactive`) and `target`
|
|
789
|
+
(`airplay` or `unknown`). `connecting` is a hint (the route moved to an AirPlay
|
|
790
|
+
output, video may not follow, as with screen mirroring); `active` means the
|
|
791
|
+
receiver renders the video.
|
|
792
|
+
**Fix:** None needed; the SDK only observes. To keep the content on the device,
|
|
793
|
+
block external video playback through the adapter's `setExternalPlaybackAllowed`.
|
|
794
|
+
|
|
744
795
|
## DA-CONTENT-PIPELINE-RECREATE-FAILED
|
|
745
796
|
|
|
746
797
|
**Cause:** In `recreate` recovery mode, the adapter's `recreateMediaPipeline`
|
|
@@ -914,10 +965,12 @@ dispatching them would hand you a second copy. Read them in
|
|
|
914
965
|
**Cause:** Informational measurement. Reports how long a playback transition
|
|
915
966
|
actually took, in `context.durationMs`, for each `context.transition`:
|
|
916
967
|
`into-break` (break triggered → the break's first ad renders), `ad-to-ad` (one ad
|
|
917
|
-
ends → the next ad in the same break renders), `break-to-break` (
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
968
|
+
ends → the next ad in the same break renders), `break-to-break` (the outgoing
|
|
969
|
+
break's last ad ends → the chained break's first ad renders, with `context.path`
|
|
970
|
+
`queued` / `late-queued` / `cold` and, when buffered, `context.leadMs`) and
|
|
971
|
+
`out-of-break` (break ends → content is playing again). `context.adPreload` /
|
|
972
|
+
`context.adInsertion` carry the resolved modes so numbers are comparable across
|
|
973
|
+
devices and configurations.
|
|
921
974
|
**Fix:** Nothing to fix per se — use it to size transition cost. A large
|
|
922
975
|
`into-break` value means the ad was not ready at break start: confirm the break
|
|
923
976
|
was known early enough for the preload window and, on Smart TVs, that
|
|
@@ -925,6 +978,18 @@ was known early enough for the preload window and, on Smart TVs, that
|
|
|
925
978
|
a TV decoder entirely). A large `out-of-break` value points at the content player
|
|
926
979
|
re-buffering after the break rather than at ad delivery.
|
|
927
980
|
|
|
981
|
+
## DA-BREAK-CHAIN
|
|
982
|
+
|
|
983
|
+
**Cause:** Informational summary of one chain of back-to-back breaks, emitted once
|
|
984
|
+
when content plays again (or when the chain is flushed by a new chain or the
|
|
985
|
+
session ending, then without `context.contentResumeMs`). `context.breaks` lists
|
|
986
|
+
each break (`id`, `scheduledStart`, `plannedDurationSec`, `beganAtMs`,
|
|
987
|
+
`endedAtMs`, `playedSec`, `announcedDuringChain`, `path`); `context.switches`
|
|
988
|
+
lists each measured switch (`toBreakId`, `gapMs`, `path`, `leadMs`).
|
|
989
|
+
**Fix:** Nothing to fix per se. Many `cold` switches mean the next break was not
|
|
990
|
+
buffered ahead on this platform, or was published too late. A break published
|
|
991
|
+
while the chain plays is only picked up when the manifest has a `polling` object.
|
|
992
|
+
|
|
928
993
|
## DA-AD-SURFACE-REVEALED
|
|
929
994
|
|
|
930
995
|
**Cause:** Informational, Android only. Reports the moment the ad surface became
|
|
@@ -1165,6 +1230,11 @@ Common causes: wrong `networkCode`/`customAssetKey`, IMA SDK not loaded, or no
|
|
|
1165
1230
|
the IMA SDK is loaded, the `networkCode` and `customAssetKey` are correct, and your
|
|
1166
1231
|
content adapter exposes `videoElement`. Inspect `context.message`.
|
|
1167
1232
|
|
|
1233
|
+
## DA-GAM-SESSION-PRELOADED
|
|
1234
|
+
|
|
1235
|
+
**Cause:** Informational. A GAM/IMA session was opened ahead of the next event's boundary.
|
|
1236
|
+
**Fix:** None needed; `context.networkCode` and `context.customAssetKey` identify the prepared pod session.
|
|
1237
|
+
|
|
1168
1238
|
## DA-GAM-START-DEFERRED
|
|
1169
1239
|
|
|
1170
1240
|
**Cause:** Informational. The SGAI IMA stream request was deferred because the
|
|
@@ -1497,14 +1567,14 @@ is sent literally without a diagnostic.
|
|
|
1497
1567
|
|
|
1498
1568
|
## DA-ASSET-PARAMETER-OMITTED
|
|
1499
1569
|
|
|
1500
|
-
**Cause:** An `assetParameters` value contained
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
|
|
1570
|
+
**Cause:** An `assetParameters` value contained an empty customer macro
|
|
1571
|
+
(`""`, `null`, or a registered callback result of `undefined`). The parameter
|
|
1572
|
+
was omitted from the ad request, or only the affected pair when the value
|
|
1573
|
+
contained a literal `&`; this applies to GAM, VAST, and static assets.
|
|
1504
1574
|
|
|
1505
|
-
**Fix:** Return a string when the
|
|
1506
|
-
|
|
1507
|
-
|
|
1575
|
+
**Fix:** Return a non-empty string when the value should be sent. Use an
|
|
1576
|
+
unresolved core-table value when the literal token should remain in the request.
|
|
1577
|
+
Return an empty macro only when the parameter or pair should be omitted.
|
|
1508
1578
|
`DA-ASSET-MACRO-UNKNOWN` reports unresolved macros that remain in the request.
|
|
1509
1579
|
|
|
1510
1580
|
## DA-ASSET-PARAMETERS-UPDATED
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import Hls from 'hls.js';
|
|
2
|
+
/** A hand-over closer than this to the playing ad's end could not append the next ad in time. */
|
|
3
|
+
export declare const MIN_HANDOVER_LEAD_SEC = 0.25;
|
|
4
|
+
/**
|
|
5
|
+
* Whether the element can play from `from` for `ahead` seconds. `buffered` is the intersection of
|
|
6
|
+
* the audio and video SourceBuffers, so a track that never arrived leaves the boundary uncovered.
|
|
7
|
+
*/
|
|
8
|
+
export declare function bufferedFrom(video: HTMLMediaElement, from: number, ahead: number): boolean;
|
|
9
|
+
export interface HlsAdQueueOptions {
|
|
10
|
+
url: string;
|
|
11
|
+
video: HTMLVideoElement;
|
|
12
|
+
/** The instance playing the current ad; it hands its MediaSource over. */
|
|
13
|
+
current: Hls;
|
|
14
|
+
/** Where the next ad starts on the media timeline: the playing ad's end. */
|
|
15
|
+
boundary: number;
|
|
16
|
+
}
|
|
17
|
+
/** SourceBuffer kinds a stream needs according to its manifest, or `null` when it does not say. */
|
|
18
|
+
export declare function trackKinds(hls: Hls): string | null;
|
|
19
|
+
/**
|
|
20
|
+
* The next HLS ad, loaded by its own hls.js instance with `timelineOffset` at the playing ad's end,
|
|
21
|
+
* and handed the playing ad's MediaSource once that ad is buffered to its end: the next ad is then
|
|
22
|
+
* appended behind it, as hls.js does for back-to-back interstitial assets (hls.js 1.6+).
|
|
23
|
+
*/
|
|
24
|
+
export declare class HlsAdQueue {
|
|
25
|
+
private readonly opts;
|
|
26
|
+
readonly url: string;
|
|
27
|
+
readonly boundary: number;
|
|
28
|
+
readonly next: Hls;
|
|
29
|
+
/** The next ad's playlist duration, once known. */
|
|
30
|
+
duration: number;
|
|
31
|
+
/** True once the next ad owns the MediaSource. */
|
|
32
|
+
transferred: boolean;
|
|
33
|
+
private cancelled;
|
|
34
|
+
private mediaSource;
|
|
35
|
+
private onFatal;
|
|
36
|
+
/** Rejecters of the waits in flight, so a cancellation settles `start()` at once. */
|
|
37
|
+
private readonly pendingWaits;
|
|
38
|
+
constructor(opts: HlsAdQueueOptions);
|
|
39
|
+
/**
|
|
40
|
+
* Resolves `true` once the next ad is playable behind the playing one, `false` when it cannot be by
|
|
41
|
+
* the deadline; after the hand-over the caller then drops it.
|
|
42
|
+
*/
|
|
43
|
+
start(): Promise<boolean>;
|
|
44
|
+
/** Whether the element can play the next ad from the boundary on. */
|
|
45
|
+
isPlayable(): boolean;
|
|
46
|
+
/** The hand-over alone appends nothing: wait until the element can play the next ad from the boundary. */
|
|
47
|
+
private untilPlayable;
|
|
48
|
+
/** `handler` runs when the next ad fails fatally. */
|
|
49
|
+
setOnFatal(handler: (() => void) | null): void;
|
|
50
|
+
/**
|
|
51
|
+
* Drops the next ad. After the hand-over its appended range is removed and the stream ends at the
|
|
52
|
+
* boundary, so the playing ad ends at its own end and no frame of the dropped ad is shown.
|
|
53
|
+
*/
|
|
54
|
+
drop(): void;
|
|
55
|
+
destroy(): void;
|
|
56
|
+
private endStream;
|
|
57
|
+
/** Resolves with the event's data, or rejects when the deadline or a cancellation comes first. */
|
|
58
|
+
private waitFor;
|
|
59
|
+
private abortWaits;
|
|
60
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import t from"hls.js";export const MIN_HANDOVER_LEAD_SEC=.25;export function bufferedFrom(t,e,r){const a=t.buffered;for(let t=0;t<a.length;t+=1)if(a.start(t)<=e+.5&&a.end(t)>=e+r)return!0;return!1}export function trackKinds(t){const e=t.levels??[],r=e.some(t=>Boolean(t.videoCodec)),a=e.some(t=>Boolean(t.audioCodec))||(t.audioTracks?.length??0)>0;return r||a?`${r?"video":""}+${a?"audio":""}`:null}export class HlsAdQueue{constructor(e){this.opts=e,this.duration=0,this.transferred=!1,this.cancelled=!1,this.mediaSource=null,this.onFatal=null,this.pendingWaits=new Set,this.url=e.url,this.boundary=e.boundary;const r={timelineOffset:e.boundary,autoStartLoad:!1,startFragPrefetch:!0,backBufferLength:10};Object.assign(r,{interstitialsController:void 0}),this.next=new t(r),this.next.on(t.Events.ERROR,(t,e)=>{e.fatal&&this.onFatal?.()})}async start(){const{next:e,opts:r}=this,a=1e3*(r.boundary-r.video.currentTime-.25)/(r.video.playbackRate||1);if(!(a>0))return!1;const n=Date.now()+a;try{const a=this.waitFor(e,t.Events.MANIFEST_PARSED,n),i=this.waitFor(e,t.Events.LEVEL_LOADED,n),s=this.waitFor(e,t.Events.BUFFER_CODECS,n);if(i.catch(()=>{}),s.catch(()=>{}),e.loadSource(r.url),await a,this.cancelled)return!1;e.startLoad(-1,!0),this.duration=(await i).details.totalduration;const o=trackKinds(e),d=trackKinds(r.current);if(o&&d&&o!==d)return!1;if(await s,r.current.bufferedToEnd||await this.waitFor(r.current,t.Events.BUFFERED_TO_END,n),this.cancelled||r.video.ended||Date.now()>n||r.current.media!==r.video)return!1;const c=r.current.transferMedia();return!!c?.mediaSource&&(this.mediaSource=c.mediaSource,e.attachMedia({...c,overrides:{duration:r.boundary+this.duration,endOfStream:!0,cueRemoval:!1}}),this.transferred=!0,await this.untilPlayable(n))}catch{return!1}}isPlayable(){const t=Math.max(.1,Math.min(1,this.duration));return bufferedFrom(this.opts.video,this.boundary,t)}async untilPlayable(e){for(;!this.isPlayable();)if(await this.waitFor(this.next,t.Events.FRAG_BUFFERED,e),this.cancelled)return!1;return!0}setOnFatal(t){this.onFatal=t}drop(){if(this.cancelled=!0,this.onFatal=null,this.abortWaits(),!this.transferred)return void this.destroy();const{next:e}=this;e.stopLoad(),e.once(t.Events.BUFFER_FLUSHED,()=>this.endStream(10)),e.trigger(t.Events.BUFFER_FLUSHING,{startOffset:this.boundary,endOffset:Number.POSITIVE_INFINITY,type:null})}destroy(){this.cancelled=!0,this.onFatal=null,this.abortWaits();try{this.next.destroy()}catch{}}endStream(t){const e=this.mediaSource;if(e&&"open"===e.readyState)if(Array.from(e.sourceBuffers).some(t=>t.updating))t>1&&setTimeout(()=>this.endStream(t-1),50);else try{e.endOfStream()}catch{}}waitFor(t,e,r){return new Promise((a,n)=>{const i=t.on.bind(t),s=t.off.bind(t),o=()=>{clearTimeout(c),s(e,u),this.pendingWaits.delete(d)},d=t=>{o(),n(t)},c=setTimeout(()=>d(new Error(`timed out waiting for ${e}`)),Math.max(0,r-Date.now())),u=(t,e)=>{o(),this.cancelled?n(new Error("cancelled")):a(e)};this.pendingWaits.add(d),i(e,u)})}abortWaits(){for(const t of[...this.pendingWaits])t(new Error("cancelled"))}}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { PlayerAdapter, PlayerAdapterEvent, PlayerAdapterEventHandler } from '@dolby-optiview/ads-sdk-core';
|
|
1
|
+
import type { ExternalPlaybackState, PlayerAdapter, PlayerAdapterEvent, PlayerAdapterEventHandler } from '@dolby-optiview/ads-sdk-core';
|
|
2
2
|
/**
|
|
3
3
|
* PlayerAdapter backed by a plain HTMLVideoElement.
|
|
4
4
|
*
|
|
@@ -11,6 +11,7 @@ import type { PlayerAdapter, PlayerAdapterEvent, PlayerAdapterEventHandler } fro
|
|
|
11
11
|
*/
|
|
12
12
|
export declare class NativeVideoAdapter implements PlayerAdapter {
|
|
13
13
|
private video;
|
|
14
|
+
private externalPlayback;
|
|
14
15
|
private boundHandlers;
|
|
15
16
|
private eventListeners;
|
|
16
17
|
private addTrackHandler?;
|
|
@@ -108,6 +109,8 @@ export declare class NativeVideoAdapter implements PlayerAdapter {
|
|
|
108
109
|
preload(url: string): Promise<void>;
|
|
109
110
|
on(event: PlayerAdapterEvent, handler: PlayerAdapterEventHandler): void;
|
|
110
111
|
off(event: PlayerAdapterEvent, handler: PlayerAdapterEventHandler): void;
|
|
112
|
+
get externalPlaybackState(): ExternalPlaybackState | undefined;
|
|
113
|
+
setExternalPlaybackAllowed(allowed: boolean): void;
|
|
111
114
|
destroy(): void;
|
|
112
115
|
/**
|
|
113
116
|
* Subscribe to native in-band metadata text tracks (Safari/WebKit surfaces HLS
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{nativeMediaLoadError as e}from"./nativeMediaLoadError";export class NativeVideoAdapter{constructor(e){this.boundHandlers=new Map,this.eventListeners=new Map,this.cueTracks=[],this.emittedCues=new WeakSet,this.liveStartTargetDuration=null,this.activeOperation=null,this.video=e,this.setupVideoListeners(),this.setupMetadataListener()}get currentTime(){return this.video.currentTime}get duration(){return this.video.duration}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 videoElement(){return this.video}get playbackReady(){return this.video.readyState>=HTMLMediaElement.HAVE_CURRENT_DATA&&!this.video.seeking}get programDateTime(){const e=this.webKitStartDate();if(!e)return null;if(this.video.readyState<HTMLMediaElement.HAVE_METADATA)return null;const t=e.call(this.video);return t instanceof Date&&t.getTime()>0?new Date(t.getTime()+1e3*this.video.currentTime):null}get liveEdge(){const e=this.liveStartTargetDuration,t=this.video.seekable;if(null===e||!this.webKitStartDate()||this.video.duration!==1/0||!t?.length)return null;const i=t.end(t.length-1);return Number.isFinite(i)?i+3*e:null}requestLiveStartOffset(e,t){const i=t;return this.liveStartTargetDuration=null!==e&&null!==i&&Number.isFinite(i)&&i>0?i:null,!1}webKitStartDate(){const e=this.video.getStartDate;return"function"==typeof e?e:void 0}get supportsParallelBuffering(){return!0}pause(){this.video.pause()}async play(){await this.video.play()}seek(e){this.video.currentTime=e}get sourceUrl(){return this.video.currentSrc||this.video.src||null}async load(t){const i=this.beginOperation();return new Promise((r,n)=>{const s=()=>{o(),this.endOperation(i),r()},a=()=>{o(),e("Native video load error",t,this.video.error).then(e=>{this.endOperation(i),n(e)})},o=()=>{this.video.removeEventListener("loadedmetadata",s),this.video.removeEventListener("error",a)};this.video.addEventListener("loadedmetadata",s),this.video.addEventListener("error",a),this.video.preload="auto",this.video.src=t,this.video.load()})}unload(){this.activeOperation=null,this.video.removeAttribute("src"),this.video.load()}recreateMediaPipeline(t){const i=this.sourceUrl;if(!i)return Promise.reject(new Error("Native pipeline recreate: no source URL"));const r=this.beginOperation();return new Promise((n,s)=>{const a=()=>{d(),this.endOperation(r),this.video.currentTime=t.startTime,n()},o=()=>{d(),e("Native pipeline recreate error",i,this.video.error).then(e=>{this.endOperation(r),s(e)})},d=()=>{this.video.removeEventListener("loadedmetadata",a),this.video.removeEventListener("error",o)};this.video.addEventListener("loadedmetadata",a),this.video.addEventListener("error",o),this.video.removeAttribute("src"),this.video.load(),this.video.src=i,this.video.load()})}async preload(e){if("undefined"!=typeof document)return new Promise(t=>{const i=document.createElement("video");i.preload="metadata",i.muted=!0;let r=!1;const n=()=>{r||(r=!0,clearTimeout(s),i.removeAttribute("src"),i.load(),t())},s=setTimeout(n,8e3);i.addEventListener("loadedmetadata",n,{once:!0}),i.addEventListener("error",n,{once:!0}),i.src=e})}on(e,t){this.eventListeners.has(e)||this.eventListeners.set(e,new Set),this.eventListeners.get(e).add(t)}off(e,t){this.eventListeners.get(e)?.delete(t)}destroy(){for(const[e,t]of this.boundHandlers)this.video.removeEventListener(e,t);this.boundHandlers.clear(),this.teardownMetadataListener(),this.eventListeners.clear()}setupMetadataListener(){const e=this.video.textTracks;if(!e||"function"!=typeof e.addEventListener)return;const t=e=>{this.emittedCues.has(e)||(this.emittedCues.add(e),this.emit("timedmetadata",this.toCue(e)))},i=e=>{const i=e.cues;if(i)for(let e=0;e<i.length;e++)t(i[e])},r=e=>{if("metadata"!==e.kind)return;e.mode="hidden";const r=()=>{const i=e.activeCues;if(i)for(let e=0;e<i.length;e++)t(i[e])};e.addEventListener("cuechange",r),this.cueTracks.push({track:e,handler:r}),i(e)},n=e=>{const t=e.track;t&&r(t)};this.addTrackHandler=n,e.addEventListener("addtrack",n);const s=()=>{for(const{track:e}of this.cueTracks)i(e)};this.cueScanHandler=s,this.video.addEventListener("timeupdate",s);for(let t=0;t<e.length;t++)r(e[t])}teardownMetadataListener(){const e=this.video.textTracks;this.addTrackHandler&&e?.removeEventListener&&(e.removeEventListener("addtrack",this.addTrackHandler),this.addTrackHandler=void 0),this.cueScanHandler&&(this.video.removeEventListener("timeupdate",this.cueScanHandler),this.cueScanHandler=void 0);for(const{track:e,handler:t}of this.cueTracks)e.removeEventListener("cuechange",t);this.cueTracks=[],this.emittedCues=new WeakSet}toCue(e){const t={type:"id3",startTime:e.startTime,endTime:e.endTime},i=e.value;if(i?.key){const e="string"==typeof(r=i.data)?r:null==r?"":"[object ArrayBuffer]"===Object.prototype.toString.call(r)?(new TextDecoder).decode(new Uint8Array(r)):ArrayBuffer.isView(r)?(new TextDecoder).decode(new Uint8Array(r.buffer,r.byteOffset,r.byteLength)):String(r);t.frames={[i.key]:e},"string"==typeof i.info&&i.info&&(t.description=i.info),e&&(t.messageData=e)}var r;return t}beginOperation(){const e=Symbol("native-operation");return this.activeOperation=e,e}endOperation(e){this.activeOperation===e&&(this.activeOperation=null)}setupVideoListeners(){const e=["timeupdate","ended","error","seeked","volumechange","waiting","playing","pause"];for(const t of e){const e=()=>{"error"!==t?this.emit(t,void 0):null===this.activeOperation&&this.emit(t,this.video.error)};this.video.addEventListener(t,e),this.boundHandlers.set(t,e)}}emit(e,t){const i=this.eventListeners.get(e);if(i)for(const e of i)try{e(t)}catch(e){console.error("[NativeVideoAdapter] Event handler error:",e)}}}
|
|
1
|
+
import{nativeMediaLoadError as e}from"./nativeMediaLoadError";import{observeExternalPlayback as t,setExternalPlaybackAllowed as i}from"@dolby-optiview/ads-sdk-core/external-playback";export class NativeVideoAdapter{constructor(e){this.boundHandlers=new Map,this.eventListeners=new Map,this.cueTracks=[],this.emittedCues=new WeakSet,this.liveStartTargetDuration=null,this.activeOperation=null,this.video=e,this.externalPlayback=t(e,e=>this.emit("externalplaybackchange",e)),this.setupVideoListeners(),this.setupMetadataListener()}get currentTime(){return this.video.currentTime}get duration(){return this.video.duration}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 videoElement(){return this.video}get playbackReady(){return this.video.readyState>=HTMLMediaElement.HAVE_CURRENT_DATA&&!this.video.seeking}get programDateTime(){const e=this.webKitStartDate();if(!e)return null;if(this.video.readyState<HTMLMediaElement.HAVE_METADATA)return null;const t=e.call(this.video);return t instanceof Date&&t.getTime()>0?new Date(t.getTime()+1e3*this.video.currentTime):null}get liveEdge(){const e=this.liveStartTargetDuration,t=this.video.seekable;if(null===e||!this.webKitStartDate()||this.video.duration!==1/0||!t?.length)return null;const i=t.end(t.length-1);return Number.isFinite(i)?i+3*e:null}requestLiveStartOffset(e,t){const i=t;return this.liveStartTargetDuration=null!==e&&null!==i&&Number.isFinite(i)&&i>0?i:null,!1}webKitStartDate(){const e=this.video.getStartDate;return"function"==typeof e?e:void 0}get supportsParallelBuffering(){return!0}pause(){this.video.pause()}async play(){await this.video.play()}seek(e){this.video.currentTime=e}get sourceUrl(){return this.video.currentSrc||this.video.src||null}async load(t){const i=this.beginOperation();return new Promise((r,n)=>{const s=()=>{o(),this.endOperation(i),r()},a=()=>{o(),e("Native video load error",t,this.video.error).then(e=>{this.endOperation(i),n(e)})},o=()=>{this.video.removeEventListener("loadedmetadata",s),this.video.removeEventListener("error",a)};this.video.addEventListener("loadedmetadata",s),this.video.addEventListener("error",a),this.video.preload="auto",this.video.src=t,this.video.load()})}unload(){this.activeOperation=null,this.video.removeAttribute("src"),this.video.load()}recreateMediaPipeline(t){const i=this.sourceUrl;if(!i)return Promise.reject(new Error("Native pipeline recreate: no source URL"));const r=this.beginOperation();return new Promise((n,s)=>{const a=()=>{d(),this.endOperation(r),this.video.currentTime=t.startTime,n()},o=()=>{d(),e("Native pipeline recreate error",i,this.video.error).then(e=>{this.endOperation(r),s(e)})},d=()=>{this.video.removeEventListener("loadedmetadata",a),this.video.removeEventListener("error",o)};this.video.addEventListener("loadedmetadata",a),this.video.addEventListener("error",o),this.video.removeAttribute("src"),this.video.load(),this.video.src=i,this.video.load()})}async preload(e){if("undefined"!=typeof document)return new Promise(t=>{const i=document.createElement("video");i.preload="metadata",i.muted=!0;let r=!1;const n=()=>{r||(r=!0,clearTimeout(s),i.removeAttribute("src"),i.load(),t())},s=setTimeout(n,8e3);i.addEventListener("loadedmetadata",n,{once:!0}),i.addEventListener("error",n,{once:!0}),i.src=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){i(this.video,e)}destroy(){this.externalPlayback?.dispose();for(const[e,t]of this.boundHandlers)this.video.removeEventListener(e,t);this.boundHandlers.clear(),this.teardownMetadataListener(),this.eventListeners.clear()}setupMetadataListener(){const e=this.video.textTracks;if(!e||"function"!=typeof e.addEventListener)return;const t=e=>{this.emittedCues.has(e)||(this.emittedCues.add(e),this.emit("timedmetadata",this.toCue(e)))},i=e=>{const i=e.cues;if(i)for(let e=0;e<i.length;e++)t(i[e])},r=e=>{if("metadata"!==e.kind)return;e.mode="hidden";const r=()=>{const i=e.activeCues;if(i)for(let e=0;e<i.length;e++)t(i[e])};e.addEventListener("cuechange",r),this.cueTracks.push({track:e,handler:r}),i(e)},n=e=>{const t=e.track;t&&r(t)};this.addTrackHandler=n,e.addEventListener("addtrack",n);const s=()=>{for(const{track:e}of this.cueTracks)i(e)};this.cueScanHandler=s,this.video.addEventListener("timeupdate",s);for(let t=0;t<e.length;t++)r(e[t])}teardownMetadataListener(){const e=this.video.textTracks;this.addTrackHandler&&e?.removeEventListener&&(e.removeEventListener("addtrack",this.addTrackHandler),this.addTrackHandler=void 0),this.cueScanHandler&&(this.video.removeEventListener("timeupdate",this.cueScanHandler),this.cueScanHandler=void 0);for(const{track:e,handler:t}of this.cueTracks)e.removeEventListener("cuechange",t);this.cueTracks=[],this.emittedCues=new WeakSet}toCue(e){const t={type:"id3",startTime:e.startTime,endTime:e.endTime},i=e.value;if(i?.key){const e="string"==typeof(r=i.data)?r:null==r?"":"[object ArrayBuffer]"===Object.prototype.toString.call(r)?(new TextDecoder).decode(new Uint8Array(r)):ArrayBuffer.isView(r)?(new TextDecoder).decode(new Uint8Array(r.buffer,r.byteOffset,r.byteLength)):String(r);t.frames={[i.key]:e},"string"==typeof i.info&&i.info&&(t.description=i.info),e&&(t.messageData=e)}var r;return t}beginOperation(){const e=Symbol("native-operation");return this.activeOperation=e,e}endOperation(e){this.activeOperation===e&&(this.activeOperation=null)}setupVideoListeners(){const e=["timeupdate","ended","error","seeked","volumechange","waiting","playing","pause"];for(const t of e){const e=()=>{"error"!==t?this.emit(t,void 0):null===this.activeOperation&&this.emit(t,this.video.error)};this.video.addEventListener(t,e),this.boundHandlers.set(t,e)}}emit(e,t){const i=this.eventListeners.get(e);if(i)for(const e of i)try{e(t)}catch(e){console.error("[NativeVideoAdapter] Event handler error:",e)}}}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { PlayerAdapter, PlayerAdapterEvent, PlayerAdapterEventHandler } from '@dolby-optiview/ads-sdk-core';
|
|
2
|
+
import type { AdSourceQueue } from '@dolby-optiview/ads-sdk-core/internal';
|
|
2
3
|
/**
|
|
3
4
|
* Default ad `PlayerAdapter` that picks its playback technology from the
|
|
4
5
|
* creative rather than from `Hls.isSupported()` alone.
|
|
@@ -14,7 +15,7 @@ import type { PlayerAdapter, PlayerAdapterEvent, PlayerAdapterEventHandler } fro
|
|
|
14
15
|
* different technology than the one currently attached. When HLS.js is not
|
|
15
16
|
* supported the adapter always uses {@link NativeVideoAdapter} (native HLS).
|
|
16
17
|
*/
|
|
17
|
-
export declare class RoutingAdAdapter implements PlayerAdapter {
|
|
18
|
+
export declare class RoutingAdAdapter implements PlayerAdapter, AdSourceQueue {
|
|
18
19
|
private readonly video;
|
|
19
20
|
private readonly hlsSupported;
|
|
20
21
|
private inner;
|
|
@@ -23,6 +24,15 @@ export declare class RoutingAdAdapter implements PlayerAdapter {
|
|
|
23
24
|
private ownedSrc;
|
|
24
25
|
private hls;
|
|
25
26
|
private readonly eventListeners;
|
|
27
|
+
/** The next ad, queued behind the playing one until it is taken or dropped. */
|
|
28
|
+
private queue;
|
|
29
|
+
/** A queue dropped after its hand-over: it owns the element's MediaSource until the next load. */
|
|
30
|
+
private orphan;
|
|
31
|
+
/** The playing ad's start and end on the media timeline, which runs on across queued ads. */
|
|
32
|
+
private segmentStart;
|
|
33
|
+
private segmentEnd;
|
|
34
|
+
private stopBoundaryWatch;
|
|
35
|
+
private boundaryCrossed;
|
|
26
36
|
constructor(video: HTMLVideoElement, hlsSupported?: boolean);
|
|
27
37
|
get currentTime(): number;
|
|
28
38
|
get duration(): number;
|
|
@@ -46,6 +56,22 @@ export declare class RoutingAdAdapter implements PlayerAdapter {
|
|
|
46
56
|
load(url: string): Promise<void>;
|
|
47
57
|
/** Detached prefetch via the routed inner adapter (best-effort). */
|
|
48
58
|
preload(url: string): Promise<void>;
|
|
59
|
+
/**
|
|
60
|
+
* Buffers the next HLS ad behind the playing one (see `AdSourceQueue`): a second hls.js instance
|
|
61
|
+
* takes over the playing ad's MediaSource once that ad is buffered to its end.
|
|
62
|
+
* @internal
|
|
63
|
+
*/
|
|
64
|
+
queueNext(url: string, onLost: () => void): Promise<boolean>;
|
|
65
|
+
/**
|
|
66
|
+
* Makes the queued ad the current source without `load()` (see `AdSourceQueue`).
|
|
67
|
+
* @internal
|
|
68
|
+
*/
|
|
69
|
+
takeQueued(url: string): boolean;
|
|
70
|
+
/**
|
|
71
|
+
* Drops the queued ad; media appended behind the playing ad never reaches the screen.
|
|
72
|
+
* @internal
|
|
73
|
+
*/
|
|
74
|
+
clearQueued(): void;
|
|
49
75
|
on(event: PlayerAdapterEvent, handler: PlayerAdapterEventHandler): void;
|
|
50
76
|
off(event: PlayerAdapterEvent, handler: PlayerAdapterEventHandler): void;
|
|
51
77
|
/**
|
|
@@ -65,6 +91,19 @@ export declare class RoutingAdAdapter implements PlayerAdapter {
|
|
|
65
91
|
*/
|
|
66
92
|
releaseMediaElement(): void;
|
|
67
93
|
destroy(): void;
|
|
94
|
+
/** Teardown: forget any queued ad and the running timeline before the element is reloaded. */
|
|
95
|
+
private resetQueue;
|
|
96
|
+
/** Drops the queued ad: before its hand-over by destroying it, after it by removing its media. */
|
|
97
|
+
private disposeQueue;
|
|
98
|
+
/**
|
|
99
|
+
* The playing ad then ends at its own end (`segmentEnd` stays at the boundary, as the element's duration
|
|
100
|
+
* still includes the dropped ad until the stream ends); the dropped instance keeps the MediaSource until the next load.
|
|
101
|
+
*/
|
|
102
|
+
private dropTransferred;
|
|
103
|
+
/** Emits `event` once the playhead reaches `boundary`, where the element itself fires nothing. */
|
|
104
|
+
private watchBoundary;
|
|
105
|
+
private stopWatch;
|
|
106
|
+
private emit;
|
|
68
107
|
/**
|
|
69
108
|
* Return the inner adapter for `tech`, (re)building it if absent or if a
|
|
70
109
|
* different technology is currently attached. Registered event handlers are
|
package/dist/RoutingAdAdapter.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import e from"hls.js";import{HlsJsAdapter as t}from"@dolby-optiview/ads-sdk-adapter-hlsjs";import{NativeVideoAdapter as i}from"./NativeVideoAdapter";import{selectAdTech as r}from"./selectAdTech";export class RoutingAdAdapter{constructor(t,
|
|
1
|
+
import e from"hls.js";import{HlsJsAdapter as t}from"@dolby-optiview/ads-sdk-adapter-hlsjs";import{HlsAdQueue as s}from"./HlsAdQueue";import{NativeVideoAdapter as i}from"./NativeVideoAdapter";import{selectAdTech as r}from"./selectAdTech";export class RoutingAdAdapter{constructor(t,s=e.isSupported()){this.inner=null,this.currentTech=null,this.ownedSrc=null,this.hls=null,this.eventListeners=new Map,this.queue=null,this.orphan=null,this.segmentStart=0,this.segmentEnd=null,this.stopBoundaryWatch=null,this.boundaryCrossed=!1,this.video=t,this.hlsSupported=s}get currentTime(){const e=this.inner?this.inner.currentTime:this.video.currentTime;return Math.max(0,e-this.segmentStart)}get duration(){return null!==this.segmentEnd?this.segmentEnd-this.segmentStart:(this.inner?this.inner.duration:this.video.duration)-this.segmentStart}get paused(){return this.inner?this.inner.paused: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 programDateTime(){return this.inner?this.inner.programDateTime:null}get videoElement(){return this.video}get supportsParallelBuffering(){return this.inner?.supportsParallelBuffering??this.hlsSupported}pause(){this.inner?this.inner.pause():this.video.pause()}async play(){if(this.inner)return this.inner.play();await this.video.play()}seek(e){this.inner?this.inner.seek(e+this.segmentStart):this.video.currentTime=e+this.segmentStart}load(e){this.resetQueue();const t=r(e),s=t=>{const s=this.ensureInner(t).load(e);return this.ownedSrc=this.video.getAttribute("src"),s};return"string"==typeof t?s(t):t.then(s)}preload(e){const t=r(e),s=t=>{if(this.inner&&this.currentTech!==(this.hlsSupported?t:"native"))return Promise.resolve();const s=this.ensureInner(t);return s.preload?s.preload(e):Promise.resolve()};return"string"==typeof t?s(t):t.then(s)}async queueNext(e,t){this.disposeQueue();const i=this.hls;if(!i||"hls"!==this.currentTech||i.media!==this.video)return!1;if("function"!=typeof i.transferMedia)return!1;const n=r(e);if("hls"!==("string"==typeof n?n:await n)||this.hls!==i||this.queue)return!1;const h=i.latestLevelDetails?.totalduration;if("number"!=typeof h||!Number.isFinite(h)||h<=0)return!1;const o=new s({url:e,video:this.video,current:i,boundary:this.segmentStart+h});this.queue=o;const u=this.segmentEnd;this.segmentEnd=o.boundary;const d=await o.start();return this.queue===o&&(d?(o.setOnFatal(()=>{this.queue===o&&(this.queue=null,this.dropTransferred(o),t())}),this.watchBoundary(o.boundary,"ended"),!0):(this.disposeQueue(),o.transferred||(this.segmentEnd=u),!1))}takeQueued(e){const s=this.queue;if(!s||s.url!==e||!s.transferred||this.video.ended||!s.isPlayable())return this.disposeQueue(),!1;this.queue=null;const i=this.boundaryCrossed;this.stopWatch(),!i&&this.video.currentTime<s.boundary&&(this.video.currentTime=s.boundary),this.inner?.destroy(),this.hls?.destroy(),this.hls=s.next,this.inner=new t(s.next,this.video);for(const[e,t]of this.eventListeners)for(const s of t)this.inner.on(e,s);return this.segmentStart=s.boundary,this.segmentEnd=s.boundary+s.duration,i?queueMicrotask(()=>this.emit("playing")):this.watchBoundary(s.boundary,"playing"),!0}clearQueued(){this.disposeQueue()}on(e,t){let s=this.eventListeners.get(e);s||(s=new Set,this.eventListeners.set(e,s)),s.add(t),this.inner?.on(e,t)}off(e,t){this.eventListeners.get(e)?.delete(t),this.inner?.off(e,t)}unload(){this.resetQueue(),this.teardownInner()}releaseMediaElement(){if(this.resetQueue(),this.inner){if(this.inner.destroy(),this.hls){try{this.hls.stopLoad()}catch{}this.video.getAttribute("src")===this.ownedSrc&&this.video.removeAttribute("src");for(const e of Array.from(this.video.querySelectorAll("source")))e.remove();try{this.hls.destroy()}catch{}this.hls=null}this.ownedSrc=null,this.inner=null,this.currentTech=null}}destroy(){this.resetQueue(),this.teardownInner(),this.eventListeners.clear()}resetQueue(){const e=this.queue;this.queue=null,this.stopWatch(),e?.destroy(),this.orphan?.destroy(),this.orphan=null,this.segmentStart=0,this.segmentEnd=null,this.boundaryCrossed=!1}disposeQueue(){const e=this.queue;this.queue=null,this.stopWatch(),e&&(e.transferred?this.dropTransferred(e):e.destroy())}dropTransferred(e){this.stopWatch(),e.drop(),this.orphan?.destroy(),this.orphan=e}watchBoundary(e,t){this.stopWatch(),this.boundaryCrossed=!1;const s=this.video;let i=null;const r=()=>{s.removeEventListener("timeupdate",h),null!==i&&s.cancelVideoFrameCallback?.(i)},n=()=>{this.stopBoundaryWatch===r&&(this.stopWatch(),this.boundaryCrossed=!0,this.emit(t))},h=()=>{s.currentTime>=e&&n()},o=(t,h)=>{h.mediaTime>=e?n():this.stopBoundaryWatch===r&&(i=s.requestVideoFrameCallback?.(o)??null)};this.stopBoundaryWatch=r,s.addEventListener("timeupdate",h),i=s.requestVideoFrameCallback?.(o)??null}stopWatch(){const e=this.stopBoundaryWatch;this.stopBoundaryWatch=null,e?.()}emit(e){for(const t of this.eventListeners.get(e)??[])t()}ensureInner(e){const t=this.hlsSupported?e:"native";if(this.inner&&this.currentTech===t)return this.inner;this.teardownInner(),this.inner=this.buildInner(t),this.currentTech=t;for(const[e,t]of this.eventListeners)for(const s of t)this.inner.on(e,s);return this.inner}buildInner(s){return"native"===s?new i(this.video):(this.hls=new e,new t(this.hls,this.video))}teardownInner(){if(this.inner?.destroy(),this.hls){const e=this.video.getAttribute("src");if(e&&e!==this.ownedSrc)for(const e of Array.from(this.video.querySelectorAll("source")))e.remove();try{this.hls.destroy()}catch{}this.hls=null}const e=this.video.getAttribute("src");e&&e!==this.ownedSrc||(this.video.removeAttribute("src"),"native"!==this.currentTech||null===this.ownedSrc||this.video.srcObject||this.video.load()),this.ownedSrc=null,this.inner=null,this.currentTech=null}}
|
|
@@ -14,5 +14,8 @@ import type { PlayerAdapter } from '@dolby-optiview/ads-sdk-core';
|
|
|
14
14
|
* When the SDK passes an existing `video` — the shared-element path, where the ad has to
|
|
15
15
|
* play through the content player's own element to reach a picture-in-picture window — that
|
|
16
16
|
* element is wrapped as-is and nothing is created or appended.
|
|
17
|
+
*
|
|
18
|
+
* The created element has remote playback disabled: an ad player allowed to AirPlay would take
|
|
19
|
+
* the receiver on `play()` and AVFoundation-based browsers then pause the content player.
|
|
17
20
|
*/
|
|
18
21
|
export declare function defaultAdAdapter(container: HTMLElement, video?: HTMLVideoElement): PlayerAdapter;
|
package/dist/defaultAdAdapter.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{RoutingAdAdapter as e}from"./RoutingAdAdapter";export function defaultAdAdapter(t,n){if(n)return new e(n);const r=document.createElement("video");return r.setAttribute("playsinline",""),t.appendChild(r),new e(r)}
|
|
1
|
+
import{RoutingAdAdapter as e}from"./RoutingAdAdapter";export function defaultAdAdapter(t,n){if(n)return new e(n);const r=document.createElement("video");return r.setAttribute("playsinline",""),r.disableRemotePlayback=!0,t.appendChild(r),new e(r)}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dolby-optiview/ads-sdk",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.3.0",
|
|
4
4
|
"description": "OptiView Ads SDK entry package - OptiViewAds with a default HLS.js ad player (native fallback)",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
@@ -44,8 +44,8 @@
|
|
|
44
44
|
},
|
|
45
45
|
"sideEffects": false,
|
|
46
46
|
"dependencies": {
|
|
47
|
-
"@dolby-optiview/ads-sdk-adapter-hlsjs": "^1.
|
|
48
|
-
"@dolby-optiview/ads-sdk-core": "^1.
|
|
47
|
+
"@dolby-optiview/ads-sdk-adapter-hlsjs": "^1.3.0",
|
|
48
|
+
"@dolby-optiview/ads-sdk-core": "^1.3.0"
|
|
49
49
|
},
|
|
50
50
|
"peerDependencies": {
|
|
51
51
|
"hls.js": "^1.0.0"
|