@dolby-optiview/ads-sdk 1.0.0 → 1.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +73 -0
- package/ai/reference/error-codes.md +30 -2
- package/ai/reference/knowledge-base.md +315 -12
- package/dist/NativeVideoAdapter.d.ts +23 -0
- package/dist/NativeVideoAdapter.js +1 -1
- package/dist/RoutingAdAdapter.d.ts +7 -0
- package/dist/RoutingAdAdapter.js +1 -1
- package/package.json +3 -3
package/README.md
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
# @dolby-optiview/ads-sdk
|
|
2
|
+
|
|
3
|
+
Web entry package of the OptiView Ads SDK: server-guided and manifest-driven
|
|
4
|
+
dynamic ad insertion for HLS.js, Shaka Player and THEOplayer, with a bundled
|
|
5
|
+
default ad player (HLS.js, native `<video>` fallback for Safari and iOS).
|
|
6
|
+
|
|
7
|
+
## Installation
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
npm install @dolby-optiview/ads-sdk hls.js
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
`hls.js` 1.x is a peer dependency. The package depends on
|
|
14
|
+
`@dolby-optiview/ads-sdk-core` and `@dolby-optiview/ads-sdk-adapter-hlsjs`
|
|
15
|
+
and pulls them in automatically.
|
|
16
|
+
|
|
17
|
+
## Quick start
|
|
18
|
+
|
|
19
|
+
```typescript
|
|
20
|
+
import Hls from 'hls.js';
|
|
21
|
+
import { OptiViewAds, HlsJsAdapter } from '@dolby-optiview/ads-sdk';
|
|
22
|
+
|
|
23
|
+
const video = document.getElementById('video') as HTMLVideoElement;
|
|
24
|
+
const hls = new Hls();
|
|
25
|
+
hls.attachMedia(video);
|
|
26
|
+
|
|
27
|
+
const sdk = new OptiViewAds({
|
|
28
|
+
player: new HlsJsAdapter(hls, video),
|
|
29
|
+
container: document.getElementById('container')!,
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
sdk.addEventListener('adbreakbegin', (e) => console.log('Ad break started', e.break.id));
|
|
33
|
+
sdk.addEventListener('adbreakend', (e) => console.log('Ad break ended', e.break.id));
|
|
34
|
+
|
|
35
|
+
await sdk.startSession({
|
|
36
|
+
manifestUrl: 'https://manifest.example.com/v1/your-org/channels/your-channel',
|
|
37
|
+
});
|
|
38
|
+
hls.loadSource('https://example.com/content.m3u8');
|
|
39
|
+
video.play();
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
`container` is the element that anchors the ad overlay; your player UI sits
|
|
43
|
+
inside it. Start one session per channel or piece of content and call
|
|
44
|
+
`sdk.endSession()` on channel change or stop; call `sdk.destroy()` when the
|
|
45
|
+
player is torn down.
|
|
46
|
+
|
|
47
|
+
## Other content players
|
|
48
|
+
|
|
49
|
+
`OptiViewAds` from this package uses the default ad player. Pass your own
|
|
50
|
+
`createAdAdapter` factory to drive the ads with Shaka Player or THEOplayer
|
|
51
|
+
instead, using `@dolby-optiview/ads-sdk-adapter-shaka` or
|
|
52
|
+
`@dolby-optiview/ads-sdk-adapter-theoplayer`.
|
|
53
|
+
|
|
54
|
+
## AI assistance
|
|
55
|
+
|
|
56
|
+
The package ships knowledge files and skills for IDE assistants:
|
|
57
|
+
|
|
58
|
+
```bash
|
|
59
|
+
npx dolby-ads-init-ai # writes ./.dolby-ads/ai
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
## Documentation
|
|
63
|
+
|
|
64
|
+
- [OptiView Ads SDK documentation](https://optiview.dolby.com/docs/ads/player-integration/optiview-ads-sdk/)
|
|
65
|
+
- Related packages: `@dolby-optiview/ads-sdk-core`,
|
|
66
|
+
`@dolby-optiview/ads-sdk-adapter-hlsjs`,
|
|
67
|
+
`@dolby-optiview/ads-sdk-adapter-shaka`,
|
|
68
|
+
`@dolby-optiview/ads-sdk-adapter-theoplayer`,
|
|
69
|
+
`@dolby-optiview/ads-sdk-react-native`
|
|
70
|
+
|
|
71
|
+
## License
|
|
72
|
+
|
|
73
|
+
See <https://optiview.dolby.com/policies/terms-of-service/>.
|
|
@@ -23,6 +23,10 @@ of the public contract and are never renamed.
|
|
|
23
23
|
| `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. |
|
|
24
24
|
| `DA-ANVATO-CUE-ENTERED` | break | info | ptsSource: 'anvatoCue' — a break was started by the cue reaching the playhead rather than by the scheduler's 250ms tick; context carries the breakId, the cue pts and the media time it fired at, so the two start paths are distinguishable in the field. |
|
|
25
25
|
| `DA-ANVATO-CUE-OBSERVED` | break | info | ptsSource: 'anvatoCue' — an Anvato break-signaling (type=cue) frame was observed in the stream; context carries the cue's pts, the media time it landed at, whether the tracker recorded it, and the raw payload — so a manifest-PTS vs in-stream-PTS misalignment is observable in the field. |
|
|
26
|
+
| `DA-ANVATO-CUE-SEGMENT-ANCHORED` | break | warn | ptsSource: 'anvatoCue' — an Anvato emsg cue arrived without a ptsOffset from the adapter, so its break is scheduled at the cue's carrying segment start, which can be up to one segment early. Emitted once per session; context: pts, mediaTime. |
|
|
27
|
+
| `DA-ANVATO-CUE-OFFSET-REJECTED` | break | warn | ptsSource: 'anvatoCue' — the adapter's ptsOffset placed an Anvato cue outside its carrying segment's window (more than 0.1s before or 15s after the cue startTime), so the cue's startTime is used instead. Emitted once per cue pts; context: pts, startTime, ptsOffset, mappedTime. |
|
|
28
|
+
| `DA-ANVATO-CUE-END-MATCHED` | break | info | ptsSource: 'anvatoCue' — the active break's Anvato type=cue_end was observed and its length on the stream clock (cue_end pts minus the break start pts) is within 2s of the break duration. Emitted once per break; context: breakId, mediaTime (where the player reported the cue_end), lengthSec, durationSec. |
|
|
29
|
+
| `DA-ANVATO-CUE-END-REJECTED` | break | warn | ptsSource: 'anvatoCue' — an Anvato type=cue_end was observed for the active break but its stream-clock length differs from the break duration by more than 2s, so it is ignored and the resume keeps its computed target. Emitted once per break; context: breakId, mediaTime, lengthSec, durationSec. |
|
|
26
30
|
| `DA-PRELOAD-FAILED` | preload | warn | Ad preload failed; the asset will be loaded on demand at break start. |
|
|
27
31
|
| `DA-PRELOAD-MODE-RESOLVED` | preload | info | The configured ad preload mode was resolved for this session; context carries both the configured and the resolved mode. |
|
|
28
32
|
| `DA-PRELOAD-PATH` | preload | info | The preload path actually taken for a break's ad (parallel buffer / detached prefetch / none) and the creative container (stream vs progressive). |
|
|
@@ -49,6 +53,7 @@ of the public contract and are never renamed.
|
|
|
49
53
|
| `DA-GAM-CONFIG-RESOLVED` | gam | info | The GAM stream identity for this session was resolved from the manifest's vendorConfiguration. |
|
|
50
54
|
| `DA-GAM-CONFIG-MISSING` | gam | error | The break manifest carries no usable "gam" vendorConfiguration entry; GAM assets are skipped. |
|
|
51
55
|
| `DA-GAM-POD-IDENTITY-MISMATCH` | gam | error | A GAM pod asset's vendorParameters name a different network code or custom asset key than the stream session was created with; the asset is skipped because the pod request cannot serve. |
|
|
56
|
+
| `DA-GAM-POD-MID-BREAK-END` | gam | warn | IMA reported the ad break ended before the pod's last ad while pod stream remains; per-ad reporting continues until the pod ends. |
|
|
52
57
|
| `DA-VENDOR-CONFIG-UNKNOWN-TYPE` | manifest | info | One or more vendorConfiguration entries declare a type this SDK version does not recognise and were ignored. |
|
|
53
58
|
| `DA-CONTENT-PLAYBACK-ERROR` | playback | error | The content player reported a playback error. |
|
|
54
59
|
| `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. |
|
|
@@ -56,7 +61,13 @@ of the public contract and are never renamed.
|
|
|
56
61
|
| `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. |
|
|
57
62
|
| `DA-CONTENT-PIPELINE-RECREATE-FAILED` | playback | warn | Rebuilding the content media pipeline at break end failed or timed out — or the adapter does not implement recreateMediaPipeline() (reason 'unsupported-adapter', e.g. a wrapper adapter that does not forward it); the SDK fell back to resuming in place with seek + play; context carries breakId, startTime, reason. |
|
|
58
63
|
| `DA-AD-PLAYBACK-ERROR` | playback | error | The ad player reported a playback error during a break. |
|
|
64
|
+
| `DA-AD-LOAD-RETRIED` | playback | warn | The ad player's load() had not settled within the retry window of the ad-start budget and the ad element showed no progress (readyState 0, nothing buffered), so a second load() was issued for the same URI; a slow-but-progressing load is not retried. Context carries breakId, assetId, uri, elapsedMs, remainingBudgetMs. A still-stalled retry ends as DA-AD-PLAYBACK-ERROR "Ad load timeout". |
|
|
65
|
+
| `DA-AD-URI-MISSING` | playback | error | The ad asset had no usable media URI, VAST tag URL or render surface; reported as CMCD adec. |
|
|
66
|
+
| `DA-AD-MEDIA-LOAD-FAILED` | playback | error | The ad player could not fetch the ad media (network or HTTP failure); reported as CMCD adec. |
|
|
67
|
+
| `DA-AD-MEDIA-UNSUPPORTED` | playback | error | The ad player could not decode the ad media or does not support its format; reported as CMCD adec. |
|
|
68
|
+
| `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. |
|
|
59
69
|
| `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. |
|
|
70
|
+
| `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. |
|
|
60
71
|
| `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). |
|
|
61
72
|
| `DA-DELIVERY-MODE-UNKNOWN` | session | warn | The matching delivery rule carried an unrecognized mode value; sgai is used (forward compatibility). |
|
|
62
73
|
| `DA-DELIVERY-MODE-OVERRIDDEN` | session | info | The manifest's delivery rules resolved a different mode, but the explicit client config takes precedence. |
|
|
@@ -72,6 +83,11 @@ of the public contract and are never renamed.
|
|
|
72
83
|
| `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` only the `&`-separated pair containing the macro was dropped. Raised once per parameter key and macro (per dropped pair for GAM `cust_params`). context: key, macro, pair (GAM `cust_params` only), breakId, assetId. |
|
|
73
84
|
| `DA-VAST-SSAI-UNSUPPORTED` | session | warn | VAST assets are not supported in SSAI mode (VAST is SGAI-only); the asset was skipped. |
|
|
74
85
|
| `DA-VAST-IMA-ERROR` | gam | error | Google IMA reported an error while loading or playing a VAST (client-side / CSAI) ad. |
|
|
86
|
+
| `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. |
|
|
87
|
+
| `DA-VAST-TIMEOUT` | gam | error | The VAST response or its media file did not load in time; reported as CMCD adec. |
|
|
88
|
+
| `DA-VAST-MALFORMED` | gam | error | The VAST response could not be parsed or uses an unsupported VAST version; reported as CMCD adec. |
|
|
89
|
+
| `DA-VAST-REQUEST-FAILED` | gam | error | The VAST ad request failed (network error, invalid tag URL, wrapper error or too many redirects); reported as CMCD adec. |
|
|
90
|
+
| `DA-VAST-CREATIVE-FAILED` | gam | error | The VAST creative could not be played (no compatible media file, playback error or trafficking error); reported as CMCD adec. |
|
|
75
91
|
| `DA-VAST-IMA-SDK-MISSING` | gam | error | The Google IMA client-side SDK (ima3.js) is unavailable; VAST (CSAI) ads cannot be played. |
|
|
76
92
|
| `DA-PAUSE-AD-SHOWN` | break | info | A pause ad (full-screen image or video) was shown while content is paused. |
|
|
77
93
|
| `DA-PAUSE-AD-DISMISSED` | break | info | A pause ad was dismissed (content resumed, or the duration cap elapsed). |
|
|
@@ -84,11 +100,23 @@ of the public contract and are never renamed.
|
|
|
84
100
|
| `DA-AD-SKIPPED` | playback | info | The viewer skipped the active ad break (controls.skipOffset honored). |
|
|
85
101
|
| `DA-AD-SKIP-SUPPRESSED` | playback | info | A skip request was suppressed (no skipOffset declared, or requested before the offset). |
|
|
86
102
|
| `DA-AD-CLICKED` | playback | info | The viewer clicked the ad on screen (adclick emitted). |
|
|
103
|
+
| `DA-LIVE-START-PUSHBACK` | playback | info | Initial live playback requested a backward seek to the configured target-duration distance. Context includes enabledBy (config or ptsSource), targetDurationSec, targetDurationSource, initialOffsetSec, initialOffsetTargetDurations, requestedOffsetSec, requestedOffsetTargetDurations, targetOffsetSec, targetOffsetTargetDurations, rewindSec, rewindTargetDurations, target and windowClamped. This is a request, not proof of the landing. |
|
|
104
|
+
| `DA-LIVE-START-PUSHBACK-SKIPPED` | playback | info | Initial live playback was left unchanged. The reason identifies a disabled adjustment (enabledBy says whether ptsSource or an explicit enabled decided it), 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. |
|
|
105
|
+
| `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. |
|
|
87
106
|
| `DA-RESUME-SEEK` | playback | info | Content was resume-seeked to the resolved post-break position (insertion type policy). |
|
|
88
|
-
| `DA-RESUME-
|
|
107
|
+
| `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. |
|
|
108
|
+
| `DA-RESUME-LANDED` | playback | info | Where the post-break resume-seek actually landed, measured on the seeked event rather than inferred: context carries breakId, target (the position requested), landed and errorSec (landed - target; negative means the player settled EARLIER than asked). errorSec only says whether the SEEK did what it was asked: when the live-window clamp moved the seek, policyTarget is the resume point before the clamp and policyErrorSec (landed - policyTarget) is how far the viewer really is from it. landed is null with a reason when no seeked event arrived, which is normal for a seek to the position the playhead already holds. Report-only: the SDK does not correct the position. |
|
|
109
|
+
| `DA-RESUME-PREPARED` | playback | info | Android, live break that resumes at its end — the paused content was parked exactly on the resume point before the hand-back, so the hand-back only plays (DA-RESUME-SEEK then reports prepared=true and issues no seek). Context carries breakId, target, source (cue_end when the stream's matched type=cue_end was known, else cue+duration) leadMs (how long before the expected hand-back it was parked) and refresh (true when content parked long before the hand-back was seeked onto the same point again, 4.5s before it). A later cue_end parks content again. |
|
|
89
110
|
| `DA-RESUME-PREPOSITIONED` | playback | info | The break resumed without a seek: content was moved onto its resume position DURING the break, so the transition needed no jump. Context carries breakId, target, prepositionedTo, landed, shortfallSec and exact. exact=true means it reached the policy target. exact=false means the live window never published those frames, so content was moved as close as exists — the viewer lands short, but arrives already in place rather than via a visible seek at the cut. |
|
|
111
|
+
| `DA-RESUME-PTS-MAP` | playback | info | ptsSource: 'anvatoCue', iOS/tvOS and web — the live resume point was mapped onto currentTime through the stream's own clock: the Anvato type=beacon tags delivered since the run-up's last seek give the currentTime ↔ stream-clock offset, and the resume point (the matched cue_end, else the cue plus the break duration) is converted with it. Reported when a mapping is first available after a seek and whenever the mapped target moves by 0.1s or more. Context carries breakId, target, landingStreamSec, landingSource (cue_end or cue+duration), offsetSec, beacons (beacons averaged), generation (how often the mapping started over since the session began), currentTime, and computedTarget with deltaSec (target - computedTarget) when a currentTime-based target exists. |
|
|
112
|
+
| `DA-RESUME-PTS-REMAP` | playback | warn | ptsSource: 'anvatoCue', iOS/tvOS and web — a stream beacon disagreed with the currentTime ↔ stream-clock mapping by more than 0.2s, so the player re-anchored currentTime (a variant switch or a buffer restart); the mapping starts over from that beacon and the resume point is mapped again. Reported during the resume run-up and after the hand-back. Context carries breakId, fromOffsetSec, toOffsetSec, deltaSec, currentTime and stage (run-up or after-hand-back). |
|
|
113
|
+
| `DA-RESUME-PTS-CORRECTED` | playback | info | ptsSource: 'anvatoCue', iOS/tvOS and web — once per break at the hand-back, on a stream that carries Anvato beacons: whether the stream clock placed the live resume point, and by how much it moved it. applied=true: streamClockTarget is the resume point mapped through the beacons, correctionSec is streamClockTarget - computedTarget (the currentTime target, cue plus duration), and currentTimeShiftSec is how far the same stream instant moved in currentTime between the pause and the hand-back (negative: it now sits earlier), null when no beacon mapped the stream before the pause. Context also carries breakId, landingSource (cue_end or cue+duration), landingStreamSec, offsetSec, beacons, remaps (DA-RESUME-PTS-REMAP during the break) and cueEndTarget when a cue_end was matched. applied=false with a reason means no beacon arrived after the run-up's last seek, so the cue_end or currentTime target applied. |
|
|
114
|
+
| `DA-RESUME-PTS-LANDED` | playback | info | ptsSource: 'anvatoCue', iOS/tvOS and web — where a stream-clock resume landed, measured on the stream's own clock from the beacons delivered after the hand-back: landedStreamSec is the position content resumed from (resumedAt, the seek landing or the playhead at a seek-free hand-back) converted with that mapping, and errorSec is landedStreamSec - landingStreamSec (negative: content resumed before the stream's resume point, so slate is shown). Context carries breakId, landingStreamSec, landingSource, resumedAt, sinceHandBackSec and, when measured, landedStreamSec, errorSec, offsetSec and beacons; reason when no beacon arrived within 15s. Report-only: the SDK does not correct the position from it. |
|
|
115
|
+
| `DA-RESUME-CUE-END` | playback | info | ptsSource: 'anvatoCue', iOS/tvOS, web and Android — the stream's matched type=cue_end replaced the computed resume target: the slate under the ad ends there, not at cue plus the advertised duration (150.206s for a 150s break on NFL Channel), and the player can remap its timeline while content is held, so the cue_end is reported in the current mapping. Context carries breakId, computedTarget, cueEndTarget, deltaSec (cueEndTarget - computedTarget), stage (hold, playing or hand-back; Android: always hand-back) and corrected (whether an in-buffer correction seek ran). Only a policy that resumes at the break end is replaced; otherwise applied=false and only the delta is reported. |
|
|
116
|
+
| `DA-RESUME-CUE-END-LATE` | playback | info | ptsSource: 'anvatoCue', iOS/tvOS and Android — the break's type=cue_end was matched only after the hand-back. When it is more than 1s ahead of the playhead and buffered, content is seeked forward to it once (corrected=true); content is never seeked backward. Context carries breakId, cueEndTarget, currentTime, deltaSec (cueEndTarget - currentTime) and corrected. |
|
|
90
117
|
| `DA-RESUME-CATCHUP` | playback | info | Content began playing behind the ad to reach its resume point. Context carries breakId, startPos, target, remainingAdSec, publishedEnd and clampedStart — clampedStart true means the stream had not published the ideal start position, so playback began late and the alignment loop must make up the difference (the zero-live-headroom case). Its ABSENCE on a live break that resumed short means catch-up never ran at all, which is a different fault from it running and falling short. |
|
|
91
|
-
| `DA-RESUME-TARGET-CLAMPED` | playback | warn | The computed resume position sat
|
|
118
|
+
| `DA-RESUME-TARGET-CLAMPED` | playback | warn | The computed resume position sat outside the held live window and was clamped to the nearest end of it; context carries breakId, requested, clamped, shortfallSec and bound — 'end' when the target is ahead of the published edge, 'start' when it is behind the live window start (already evicted) — applied only when the gap is at most half the held window, otherwise DA-RESUME-START-CLAMP-SKIPPED is emitted instead. Distinct from DA-RESUME-OFFSET-CLAMPED, which shortens the OFFSET to the ad seconds actually played. |
|
|
119
|
+
| `DA-RESUME-START-CLAMP-SKIPPED` | playback | info | The resume target sat behind the element's live seekable start by more than half the held window, which cannot be DVR eviction (the element's seekable range disagrees with the adapter timeline); the target was left unchanged and the engine resolved the seek itself. Context carries breakId, requested, seekableStart, behindSec, windowSec and maxBehindSec. |
|
|
92
120
|
| `DA-RESUME-ALIGN-BLOCKED` | playback | warn | Resume alignment wanted to move content past the live edge and stood down, because no seek can reach media the stream has not published yet; content is left riding the edge and the break-end clamp places it. Context carries target, requested, safeEnd, shortfallSec and remainingSec. Emitted once per break. A persistent shortfall here means the resume target is further ahead than the feed's published edge — typically the player's view of the edge is stale, not that the seek is wrong. |
|
|
93
121
|
| `DA-CONTINUE-PLAYOUT-BEGIN` | playback | info | continueContentDuringBreak engaged: content keeps PLAYING through the break, hidden and muted, so no resume seek is needed. Context carries breakId, anchor, breakDurationSec and edgeDistanceSec. |
|
|
94
122
|
| `DA-CONTINUE-PLAYOUT-END` | playback | info | continueContentDuringBreak reached the cut. Context carries driftSec (content position minus where the break says it should be), maxDriftSec, the corrections applied, rateHonoured (false if the engine drove playbackRate itself) and driftUnrecoverable. |
|
|
@@ -158,6 +158,18 @@ difference — expected for a viewer riding the live edge. The end-of-break code
|
|
|
158
158
|
then carries `alignCorrections` and `alignCorrectionsMs`: shrinking values mean
|
|
159
159
|
the loop converged; similar-magnitude ones mean it chased without settling.
|
|
160
160
|
|
|
161
|
+
## DA-RESUME-PREPARED
|
|
162
|
+
|
|
163
|
+
**Cause:** Informational (Android, a live break that resumes at its end). The
|
|
164
|
+
paused content was parked exactly on its resume point before the hand-back, so
|
|
165
|
+
the hand-back only plays (`context`: `breakId`, `target`, `source` — `cue_end`
|
|
166
|
+
or `cue+duration` — `leadMs` before the expected hand-back, and `refresh`: content
|
|
167
|
+
parked long before the hand-back is seeked onto the same point again 4.5 s before
|
|
168
|
+
it); the following
|
|
169
|
+
`DA-RESUME-SEEK` reports `prepared: true` and issues no seek. **Fix:** None. A
|
|
170
|
+
`DA-RESUME-SEEK` with `prepared: false` on such a break means the resume point
|
|
171
|
+
was not known or published early enough, so the hand-back paid for a seek.
|
|
172
|
+
|
|
161
173
|
## DA-RESUME-PREPOSITIONED
|
|
162
174
|
|
|
163
175
|
**Cause:** Informational. Content was moved onto its resume position _while the
|
|
@@ -179,17 +191,48 @@ has no headroom and cannot be returned to the exact frame they left.
|
|
|
179
191
|
|
|
180
192
|
**Cause:** Informational. Reports where the post-break resume-seek actually
|
|
181
193
|
landed, measured on the `seeked` event rather than inferred from a later
|
|
182
|
-
sample (`context` carries `breakId`, `target`, `landed`, `errorSec
|
|
183
|
-
`errorSec` is `landed - target
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
194
|
+
sample (`context` carries `breakId`, `target`, `landed`, `errorSec`, and
|
|
195
|
+
`policyTarget` / `policyErrorSec`). `errorSec` is `landed - target`: it only
|
|
196
|
+
says whether the **seek** did what it was asked, so a **negative** value means
|
|
197
|
+
the player settled closer to the live edge than the SDK asked for.
|
|
198
|
+
`policyTarget` is the resume point before the live-window clamp, and
|
|
199
|
+
`policyErrorSec` (`landed - policyTarget`) is how far the viewer really is from
|
|
200
|
+
it. **Fix:** None on its own — it is a measurement. A consistent negative
|
|
201
|
+
`errorSec` on live means the engine is overriding the seek with its own minimum
|
|
202
|
+
live offset. An `errorSec` near zero alongside `DA-RESUME-TARGET-CLAMPED` does
|
|
203
|
+
**not** mean the resume was on target: read `policyErrorSec`, which is the
|
|
204
|
+
seconds of the ad window (slate, on an SGAI feed) the viewer still sees —
|
|
205
|
+
typically a viewer riding the live edge with no headroom. Both values are in
|
|
206
|
+
the player's `currentTime`, so they cannot reveal a timeline remap. `landed:
|
|
207
|
+
null` with a `reason` is normal rather than a fault — a seek to the position
|
|
208
|
+
the playhead already holds fires no `seeked` event on several engines. The SDK
|
|
209
|
+
never corrects the landing position.
|
|
210
|
+
|
|
211
|
+
## DA-LIVE-START-PUSHBACK
|
|
212
|
+
|
|
213
|
+
Initial live playback requests a backward seek to five declared target durations behind the
|
|
214
|
+
reported live edge (on by default with `ptsSource: 'anvatoCue'`; an explicit `enabled` wins).
|
|
215
|
+
`enabledBy` (`config` or `ptsSource`) identifies which setting decided it. `targetDurationSec` and `targetDurationSource` identify
|
|
216
|
+
its input. `initialOffsetSec` / `initialOffsetTargetDurations`, `requestedOffsetSec` /
|
|
217
|
+
`requestedOffsetTargetDurations`, and `rewindSec` / `rewindTargetDurations` explain the arithmetic.
|
|
218
|
+
At 3×TD it rewinds 2×TD; at 4×TD it rewinds 1×TD. `windowClamped` indicates insufficient DVR depth.
|
|
219
|
+
This request is not evidence of a successful landing and does not change later break-resume policy.
|
|
220
|
+
|
|
221
|
+
## DA-LIVE-START-PUSHBACK-SKIPPED
|
|
222
|
+
|
|
223
|
+
`disabled` means the adjustment is off: `enabledBy: ptsSource` for a stream without Anvato cue
|
|
224
|
+
signaling, `config` for an explicit `enabled: false`. `already-behind` means the viewer starts at or
|
|
225
|
+
beyond the minimum distance. `no-target-duration`
|
|
226
|
+
requires a configured `liveStartPushback.targetDurationSeconds` when the adapter cannot provide
|
|
227
|
+
actual declared HLS metadata. `no-range` and `no-backward-target` mean there is no safe backward
|
|
228
|
+
seek. Do not infer TD from average segment length or default player holdback.
|
|
229
|
+
|
|
230
|
+
## DA-LIVE-START-PUSHBACK-LANDED
|
|
231
|
+
|
|
232
|
+
`landed`, `errorSec`, `landedOffsetSec` and `landedOffsetTargetDurations` measure the seeked event
|
|
233
|
+
against the requested target and current reported live edge. Timeout or failed seeks have
|
|
234
|
+
`landed: null`, not a guessed success. The edge may advance during settlement. Automatic player
|
|
235
|
+
latency correction can later reduce the headroom; this adjustment only runs at initial playback.
|
|
193
236
|
|
|
194
237
|
## DA-RESUME-TARGET-CLAMPED
|
|
195
238
|
|
|
@@ -209,6 +252,41 @@ resume _offset_ was shortened to the ad seconds actually played. On VOD, and on
|
|
|
209
252
|
any engine that does not report a minimum live offset, no clamp is applied at
|
|
210
253
|
all.
|
|
211
254
|
|
|
255
|
+
## DA-RESUME-UNADDRESSABLE
|
|
256
|
+
|
|
257
|
+
**Cause:** iOS/tvOS, picture-in-picture only. The window presents the content
|
|
258
|
+
player's own layer and nothing else, so the ad has to play **through** that
|
|
259
|
+
player, and content is reloaded onto its own source at the hand-back. A
|
|
260
|
+
reloaded live item starts a **new timeline with its own origin**, so the resume
|
|
261
|
+
position the policy computed — a position on the timeline the break began on —
|
|
262
|
+
addresses nothing on it, and seeking there would leave the window buffering on
|
|
263
|
+
a point the stream does not contain. The SDK therefore needs an address that
|
|
264
|
+
survives the reload: `EXT-X-PROGRAM-DATE-TIME` (exact), or failing that the
|
|
265
|
+
published `seekable.end` plus the lead the pause had over it (approximate to
|
|
266
|
+
about one segment). This code means the reloaded source offered **neither**
|
|
267
|
+
within two seconds, so content was left where the reload put it — near the live
|
|
268
|
+
edge — rather than seeked somewhere that does not exist (`context` carries
|
|
269
|
+
`breakId`, `target` and `offset`). **Fix:** Serve `EXT-X-PROGRAM-DATE-TIME` on
|
|
270
|
+
the content manifest; it removes this case and makes the picture-in-picture
|
|
271
|
+
resume exact instead of approximate. If the stream does carry PDT, the adapter
|
|
272
|
+
is not surfacing it: check that `PlayerAdapter.programDateTime` and
|
|
273
|
+
`PlayerAdapter.seekableEnd` are implemented and answer for the **restored**
|
|
274
|
+
source rather than the one the player was carrying during the break.
|
|
275
|
+
|
|
276
|
+
## DA-RESUME-START-CLAMP-SKIPPED
|
|
277
|
+
|
|
278
|
+
**Cause:** The resume target sat behind the element's live `seekable.start` by
|
|
279
|
+
more than half the held window (`context` carries `requested`, `seekableStart`,
|
|
280
|
+
`behindSec`, `windowSec`, `maxBehindSec`). A gap that large cannot be DVR
|
|
281
|
+
eviction — it means the element's seekable range disagrees with the adapter's
|
|
282
|
+
timeline, which some engines report on a different epoch. **Fix:** None — the
|
|
283
|
+
SDK left the target unchanged and let the engine resolve the seek itself rather
|
|
284
|
+
than pinning it to a start that is not on the same timeline. If the resume then
|
|
285
|
+
lands wrong, inspect `seekable` versus the adapter's reported position for that
|
|
286
|
+
platform; the engine's timeline mapping is the suspect, not the resume
|
|
287
|
+
arithmetic. The start clamp in `DA-RESUME-TARGET-CLAMPED` only applies inside
|
|
288
|
+
this half-window bound.
|
|
289
|
+
|
|
212
290
|
## DA-RESUME-ALIGN-BLOCKED
|
|
213
291
|
|
|
214
292
|
**Cause:** The alignment pass that places content before the cut wanted a
|
|
@@ -321,6 +399,26 @@ DA-AD-PLAYOUT-SYNC and probe its final segment's start times to confirm. A
|
|
|
321
399
|
single event near the end of a pod is the accumulated total, not an isolated
|
|
322
400
|
glitch.
|
|
323
401
|
|
|
402
|
+
## DA-AD-DURATION-PREPARED
|
|
403
|
+
|
|
404
|
+
**Cause:** Informational. The ad media for a break is loaded and its real
|
|
405
|
+
playable duration is now known, BEFORE the first ad frame. `context` carries
|
|
406
|
+
`breakId`, `assetId`, `index`, `total`, `preparedDurationSec` (the loaded
|
|
407
|
+
media's own duration — on a GAM pod, the stitched pod's length),
|
|
408
|
+
`declaredDurationSec` (what the manifest said about this asset, when it said
|
|
409
|
+
anything) and `breakDurationSec` (the slot the break declares). Emitted per
|
|
410
|
+
asset, so a multi-asset break reports one per asset and the break total is
|
|
411
|
+
their sum.
|
|
412
|
+
|
|
413
|
+
**Fix:** None — it is the number to plan against. Compare the three: a
|
|
414
|
+
`preparedDurationSec` well under `breakDurationSec` on a GAM pod means the POD
|
|
415
|
+
came up short of the slot, which is an ad-ops question; a short break with a
|
|
416
|
+
full pod is the SDK's or a late join's. Absent on a live or still-loading ad
|
|
417
|
+
source, where the adapter reports no finite duration — deliberately, because a
|
|
418
|
+
guessed number would be read as a measurement. Everything else that names a
|
|
419
|
+
break's real length arrives too late to act on: `adtimeupdate`'s duration comes
|
|
420
|
+
with the first tick, the resume offset and DA-AD-PLAYOUT-SYNC at the end.
|
|
421
|
+
|
|
324
422
|
## DA-AD-PLAYOUT-SYNC
|
|
325
423
|
|
|
326
424
|
**Cause:** Informational. Per-break summary of ad playout health, emitted at
|
|
@@ -406,6 +504,127 @@ break `start` when a PTS break stays `DA-ANVATO-CUE-PENDING`: a cue that was
|
|
|
406
504
|
observed but never matched points at a manifest-PTS vs in-stream-PTS
|
|
407
505
|
misalignment (must agree within 5 ms).
|
|
408
506
|
|
|
507
|
+
## DA-ANVATO-CUE-SEGMENT-ANCHORED
|
|
508
|
+
|
|
509
|
+
**Cause:** Warning (`ptsSource: 'anvatoCue'`). An Anvato DASH emsg cue
|
|
510
|
+
(`urn:anvato:es1:052016`) arrived without a `ptsOffset` from the content
|
|
511
|
+
adapter. Anvato carries the cue in an emsg v0 with a zero
|
|
512
|
+
`presentation_time_delta`, so players report it at the start of the segment
|
|
513
|
+
that carries it, and the break is scheduled there: up to one segment (e.g. 6s)
|
|
514
|
+
early. Content then resumes the same amount early, showing the end of the slate.
|
|
515
|
+
Emitted once per session; `context` carries `pts` and the `mediaTime` used.
|
|
516
|
+
**Fix:** Use an adapter that supplies `ptsOffset` (THEOplayer web and Android,
|
|
517
|
+
Shaka). A custom adapter sets `ptsOffset` on emsg cues to
|
|
518
|
+
`Period@start - presentationTimeOffset / timescale` from the MPD, or, as the
|
|
519
|
+
THEOplayer Android adapter does, measures it from the stream's Anvato beacons
|
|
520
|
+
(`type=beacon&pts=…`): the median of `startTime - pts` over the beacons that
|
|
521
|
+
agree. On Android the first few seconds of a source have no beacon clock yet.
|
|
522
|
+
|
|
523
|
+
## DA-ANVATO-CUE-OFFSET-REJECTED
|
|
524
|
+
|
|
525
|
+
**Cause:** Warning (`ptsSource: 'anvatoCue'`). The adapter's `ptsOffset` placed
|
|
526
|
+
an Anvato cue more than 0.1s before or 15s after the cue's `startTime`, so the
|
|
527
|
+
SDK ignored it and used the segment-start `startTime`. `context` carries `pts`,
|
|
528
|
+
`startTime`, `ptsOffset` and `mappedTime`. Emitted once per cue `pts`.
|
|
529
|
+
**Fix:** Check that the adapter's offset uses the player's own timeline: the
|
|
530
|
+
player time for media time `t` must be `t + ptsOffset`. A multi-Period MPD or a
|
|
531
|
+
player that counts time from `availabilityStartTime` can produce a wrong offset.
|
|
532
|
+
|
|
533
|
+
## DA-ANVATO-CUE-END-MATCHED
|
|
534
|
+
|
|
535
|
+
**Cause:** Informational (`ptsSource: 'anvatoCue'`). The stream's own end of the
|
|
536
|
+
active break (Anvato `type=cue_end`) was observed, and its length on the stream
|
|
537
|
+
clock is within 2s of the break duration. `context` carries `breakId`,
|
|
538
|
+
`mediaTime` (where the player reported it), `lengthSec` and `durationSec`.
|
|
539
|
+
**Fix:** None needed. On iOS/tvOS and Android this marker is the live resume point.
|
|
540
|
+
|
|
541
|
+
## DA-ANVATO-CUE-END-REJECTED
|
|
542
|
+
|
|
543
|
+
**Cause:** Warning (`ptsSource: 'anvatoCue'`). A `type=cue_end` was observed for
|
|
544
|
+
the active break, but its stream-clock length differs from the break duration by
|
|
545
|
+
more than 2s, so it is ignored and the computed resume point is kept.
|
|
546
|
+
**Fix:** Compare `context.lengthSec` with `context.durationSec` and check that
|
|
547
|
+
the manifest duration matches what the broadcaster signals for that break.
|
|
548
|
+
|
|
549
|
+
## DA-RESUME-CUE-END
|
|
550
|
+
|
|
551
|
+
**Cause:** Informational (iOS/tvOS, web and Android, `ptsSource: 'anvatoCue'`).
|
|
552
|
+
The matched `cue_end` replaced the computed live resume target. The slate under
|
|
553
|
+
the ad ends there, not at the cue plus the advertised duration (150.206s for a
|
|
554
|
+
150s break on NFL Channel), and the player can move its timeline while content
|
|
555
|
+
is held behind the ad, so the `cue_end` is reported in the current mapping.
|
|
556
|
+
`context` carries `breakId`, `computedTarget`, `cueEndTarget`, `deltaSec`,
|
|
557
|
+
`stage` (`hold`, `playing` or `hand-back`; Android always `hand-back`), `applied`
|
|
558
|
+
and `corrected`. **Fix:** None needed. A `deltaSec` of a fraction of a second is
|
|
559
|
+
the splice segment at the break end; a large one means the marker prevented a
|
|
560
|
+
resume inside the slate. `applied: false` means the resume policy (early
|
|
561
|
+
return, `insertion`, or a `resumeOffset` different from the duration) keeps its
|
|
562
|
+
own target.
|
|
563
|
+
|
|
564
|
+
## DA-RESUME-CUE-END-LATE
|
|
565
|
+
|
|
566
|
+
**Cause:** Informational (iOS/tvOS and Android, `ptsSource: 'anvatoCue'`). The
|
|
567
|
+
`cue_end` arrived only after content was handed back. `corrected: true` means
|
|
568
|
+
content was more than 1s short and buffered, and was moved forward onto it;
|
|
569
|
+
content is never moved backward. `context` carries `breakId`, `cueEndTarget`,
|
|
570
|
+
`currentTime`, `deltaSec` and `corrected`. **Fix:** None for a single break.
|
|
571
|
+
Frequent late arrivals with `AVPlayer` suggest the metadata look-ahead is not
|
|
572
|
+
delivering; on Android, the Media3 adapter delivers markers at presentation only,
|
|
573
|
+
so it always arrives late there.
|
|
574
|
+
|
|
575
|
+
## DA-RESUME-PTS-MAP
|
|
576
|
+
|
|
577
|
+
**Cause:** Informational (iOS/tvOS and web, `ptsSource: 'anvatoCue'`). The live resume
|
|
578
|
+
point was placed through the stream's own clock: the Anvato `type=beacon` tags
|
|
579
|
+
delivered since the run-up's last seek give the currentTime offset, and the
|
|
580
|
+
resume point (`landingSource`: the matched `cue_end`, else `cue+duration`) is
|
|
581
|
+
converted with it. `context` carries `breakId`, `target`, `landingStreamSec`,
|
|
582
|
+
`landingSource`, `offsetSec`, `beacons`, `generation` (how often the mapping
|
|
583
|
+
started over), `currentTime`, and
|
|
584
|
+
`computedTarget` with `deltaSec`. **Fix:** None needed. A large `deltaSec` is
|
|
585
|
+
the player's timeline having moved during the break, which this mapping
|
|
586
|
+
corrects. A stream without beacons never reports it and keeps the currentTime
|
|
587
|
+
resume.
|
|
588
|
+
|
|
589
|
+
## DA-RESUME-PTS-REMAP
|
|
590
|
+
|
|
591
|
+
**Cause:** Warning (iOS/tvOS and web, `ptsSource: 'anvatoCue'`). A beacon showed the
|
|
592
|
+
player re-anchored currentTime against the stream clock by more than 0.2s,
|
|
593
|
+
typically at a variant switch or a buffer restart. The mapping starts over from
|
|
594
|
+
that beacon. `context` carries `breakId`, `fromOffsetSec`, `toOffsetSec`,
|
|
595
|
+
`deltaSec`, `currentTime` and `stage` (`run-up` or `after-hand-back`).
|
|
596
|
+
**Fix:** None during the run-up: the resume point is mapped again from the next
|
|
597
|
+
beacons. Frequent remaps after the hand-back point to the engine re-anchoring
|
|
598
|
+
content on its own.
|
|
599
|
+
|
|
600
|
+
## DA-RESUME-PTS-CORRECTED
|
|
601
|
+
|
|
602
|
+
**Cause:** Informational (iOS/tvOS and web, `ptsSource: 'anvatoCue'`). Once per break
|
|
603
|
+
at the hand-back, on a stream with Anvato beacons: whether the stream clock
|
|
604
|
+
placed the resume point (`applied`) and by how much. `correctionSec` is the
|
|
605
|
+
stream-clock target minus the currentTime target (`computedTarget`, cue plus
|
|
606
|
+
duration); `currentTimeShiftSec` is how far the same stream instant moved in
|
|
607
|
+
currentTime during the break (negative: it now sits earlier). `context` also
|
|
608
|
+
carries `breakId`, `streamClockTarget`, `landingSource`, `landingStreamSec`,
|
|
609
|
+
`offsetSec`, `beacons`, `remaps` and `cueEndTarget`. **Fix:** None needed. A
|
|
610
|
+
large `currentTimeShiftSec` is a player timeline re-anchor the stream clock
|
|
611
|
+
corrected. `applied: false` with a `reason` means no beacon arrived after the
|
|
612
|
+
run-up's last seek; frequent occurrences point at beacons not reaching the
|
|
613
|
+
player.
|
|
614
|
+
|
|
615
|
+
## DA-RESUME-PTS-LANDED
|
|
616
|
+
|
|
617
|
+
**Cause:** Informational (iOS/tvOS and web, `ptsSource: 'anvatoCue'`). Where a
|
|
618
|
+
stream-clock resume landed, on the stream's own clock, measured from the beacons
|
|
619
|
+
after the hand-back. `errorSec` is `landedStreamSec - landingStreamSec`:
|
|
620
|
+
negative means content resumed before the stream's resume point (that much
|
|
621
|
+
slate shown), positive means content was skipped. `context` carries `breakId`,
|
|
622
|
+
`landingStreamSec`, `landingSource`, `resumedAt`, `sinceHandBackSec` and, when
|
|
623
|
+
measured, `landedStreamSec`, `errorSec`, `offsetSec` and `beacons`; `reason`
|
|
624
|
+
when no beacon arrived within 15s. **Fix:** None for a single break. Unlike
|
|
625
|
+
`DA-RESUME-LANDED` it is not measured in currentTime, so a timeline remap
|
|
626
|
+
cannot hide in it.
|
|
627
|
+
|
|
409
628
|
## DA-PRELOAD-FAILED
|
|
410
629
|
|
|
411
630
|
**Cause:** Preloading the next ad failed; the asset will load on demand at break
|
|
@@ -874,6 +1093,20 @@ requesting it would only waste the break while the viewer sees nothing.
|
|
|
874
1093
|
`context.assetValue` with `context.sessionValue` for the named `context.field` and fix
|
|
875
1094
|
the provisioning that produced the disagreement.
|
|
876
1095
|
|
|
1096
|
+
## DA-GAM-POD-MID-BREAK-END
|
|
1097
|
+
|
|
1098
|
+
**Cause:** IMA reported `AD_BREAK_ENDED` part-way through a GAM pod, before the pod's last
|
|
1099
|
+
ad, while the pod stream still had time left. IMA typically follows it with a new
|
|
1100
|
+
`AD_BREAK_STARTED` and keeps playing the remaining ads of the same pod.
|
|
1101
|
+
|
|
1102
|
+
**Effect:** None for the viewer. The SDK keeps the break open, closes the ad that was
|
|
1103
|
+
playing, and keeps reporting every following ad with its own `adbegin`/`adend` until the pod
|
|
1104
|
+
stream ends; `adbreakend` fires once, at the real end of the pod.
|
|
1105
|
+
|
|
1106
|
+
**What to check:** `context.adPosition` and `context.totalAds` name the last ad reported
|
|
1107
|
+
before the early end, and `context.remainingSec` the pod left. A recurring hit on one asset
|
|
1108
|
+
points at the pod's ad decisioning or IMA rather than the integration.
|
|
1109
|
+
|
|
877
1110
|
## DA-VENDOR-CONFIG-UNKNOWN-TYPE
|
|
878
1111
|
|
|
879
1112
|
**Cause:** Informational. The manifest carried `vendorConfiguration` entries whose
|
|
@@ -1015,7 +1248,9 @@ idempotent and emits no duplicate diagnostic.
|
|
|
1015
1248
|
## DA-AD-PLAYBACK-ERROR
|
|
1016
1249
|
|
|
1017
1250
|
**Cause:** The AD player reported a playback error during a break (`aderror` with
|
|
1018
|
-
`source: 'ad'`). The ad asset failed to load/decode.
|
|
1251
|
+
`source: 'ad'`). The ad asset failed to load/decode. `context.adErrorCode` carries
|
|
1252
|
+
the specific code reported as CMCD `adec` (for example `DA-AD-MEDIA-LOAD-FAILED`);
|
|
1253
|
+
it stays `DA-AD-PLAYBACK-ERROR` only when the cause is unknown. **Fix:** Verify the ad asset
|
|
1019
1254
|
URL, format (HLS vs progressive MP4), codecs, and CORS. Check the
|
|
1020
1255
|
`context.breakId`/`assetId` against the manifest. The SDK recovers and resumes
|
|
1021
1256
|
content; repeated failures indicate a bad creative or pod URL. Note: with the
|
|
@@ -1024,6 +1259,45 @@ creative (URL extension, then `Content-Type`) and routed to native `<video>` or
|
|
|
1024
1259
|
HLS.js accordingly — a `manifestParsingError` on an MP4 means a custom
|
|
1025
1260
|
`createAdAdapter` is force-routing it through HLS.js.
|
|
1026
1261
|
|
|
1262
|
+
## DA-AD-LOAD-RETRIED
|
|
1263
|
+
|
|
1264
|
+
**Cause:** The ad player's `load()` had not settled within the retry window of
|
|
1265
|
+
the ad-start budget (`context.elapsedMs`) and the ad element showed no progress
|
|
1266
|
+
(readyState 0, nothing buffered), so the SDK issued a second `load()` for the
|
|
1267
|
+
same URI. The first attempt was stalling in the browser's media pipeline — the
|
|
1268
|
+
fetch stays open but delivers no data (seen on Safari). A slow-but-progressing
|
|
1269
|
+
load is not retried. **Fix:**
|
|
1270
|
+
Usually none: the retry transparently recovers. If the retried load also
|
|
1271
|
+
stalls, the break ends as `DA-AD-PLAYBACK-ERROR` with "Ad load timeout" —
|
|
1272
|
+
verify the asset URL (see `context.uri`) and CDN reachability from the device.
|
|
1273
|
+
`context.breakId`/`assetId` identify the asset, `remainingBudgetMs` how much
|
|
1274
|
+
ad-start budget was left when the retry fired.
|
|
1275
|
+
|
|
1276
|
+
## DA-AD-URI-MISSING
|
|
1277
|
+
|
|
1278
|
+
**Cause:** An ad asset had no usable media URI, VAST tag URL, or ad container,
|
|
1279
|
+
so the SDK could not start it. Reported as CMCD `adec` on the `aderror` event.
|
|
1280
|
+
**Fix:** Check the asset in the break manifest (`context.breakId`/`assetId`);
|
|
1281
|
+
for GAM assets, check `vendorParameters`.
|
|
1282
|
+
|
|
1283
|
+
## DA-AD-MEDIA-LOAD-FAILED
|
|
1284
|
+
|
|
1285
|
+
**Cause:** The ad player could not fetch the ad media (network or HTTP
|
|
1286
|
+
failure). Reported as CMCD `adec`. **Fix:** Check that the ad URL is reachable
|
|
1287
|
+
from the device, returns HTTP 200, and allows CORS. The SDK skips the ad.
|
|
1288
|
+
|
|
1289
|
+
## DA-AD-MEDIA-UNSUPPORTED
|
|
1290
|
+
|
|
1291
|
+
**Cause:** The ad player could not decode the ad media or does not support its
|
|
1292
|
+
format. Reported as CMCD `adec`. **Fix:** Check the container (HLS vs
|
|
1293
|
+
progressive MP4) and codecs against the target device. The SDK skips the ad.
|
|
1294
|
+
|
|
1295
|
+
## DA-AD-START-TIMEOUT
|
|
1296
|
+
|
|
1297
|
+
**Cause:** The ad did not load, render a first frame, or start within its start
|
|
1298
|
+
budget. Reported as CMCD `adec`. **Fix:** Check ad media size, CDN latency, and
|
|
1299
|
+
device load; slow first segments are the usual cause.
|
|
1300
|
+
|
|
1027
1301
|
## DA-AD-START-INTERRUPTED
|
|
1028
1302
|
|
|
1029
1303
|
**Cause:** A browser-initiated pause interrupted the ad element's pending `play()`
|
|
@@ -1090,6 +1364,35 @@ Verify the VAST tag URL is reachable with correct CORS and returns a valid VAST
|
|
|
1090
1364
|
document, and confirm the IMA client-side SDK (`ima3.js`) is loaded. Inspect
|
|
1091
1365
|
`context.message` for the IMA error.
|
|
1092
1366
|
|
|
1367
|
+
## DA-VAST-NO-FILL
|
|
1368
|
+
|
|
1369
|
+
**Cause:** The VAST ad server returned no ad (empty response, no ads after
|
|
1370
|
+
wrappers, or no assets). Reported as CMCD `adec`; `adevc` carries the raw IMA
|
|
1371
|
+
code. **Fix:** This is an ad-server decision: check campaign targeting,
|
|
1372
|
+
inventory, and the ad tag parameters.
|
|
1373
|
+
|
|
1374
|
+
## DA-VAST-TIMEOUT
|
|
1375
|
+
|
|
1376
|
+
**Cause:** The VAST response or its media file did not load in time. **Fix:**
|
|
1377
|
+
Check ad-server and wrapper latency and the media file size.
|
|
1378
|
+
|
|
1379
|
+
## DA-VAST-MALFORMED
|
|
1380
|
+
|
|
1381
|
+
**Cause:** The VAST response could not be parsed or uses an unsupported VAST
|
|
1382
|
+
version. **Fix:** Fetch the ad tag URL and validate the returned XML.
|
|
1383
|
+
|
|
1384
|
+
## DA-VAST-REQUEST-FAILED
|
|
1385
|
+
|
|
1386
|
+
**Cause:** The VAST ad request failed: network error, invalid tag URL, wrapper
|
|
1387
|
+
error, or too many redirects. **Fix:** Check the ad tag URL, CORS, and the
|
|
1388
|
+
wrapper chain.
|
|
1389
|
+
|
|
1390
|
+
## DA-VAST-CREATIVE-FAILED
|
|
1391
|
+
|
|
1392
|
+
**Cause:** The VAST creative could not be played: no media file matches the
|
|
1393
|
+
player, playback failed, or a trafficking error. **Fix:** Check the MediaFile
|
|
1394
|
+
types, codecs, and dimensions in the VAST response.
|
|
1395
|
+
|
|
1093
1396
|
## DA-VAST-IMA-SDK-MISSING
|
|
1094
1397
|
|
|
1095
1398
|
**Cause:** The Google IMA client-side SDK (`ima3.js`) is not available on the
|
|
@@ -19,6 +19,8 @@ export declare class NativeVideoAdapter implements PlayerAdapter {
|
|
|
19
19
|
private cueScanHandler?;
|
|
20
20
|
/** Cues already emitted, so the cuechange and scan paths never double-emit. */
|
|
21
21
|
private emittedCues;
|
|
22
|
+
/** Target duration supplied by the SDK for the live startup distance; null when unknown. */
|
|
23
|
+
private liveStartTargetDuration;
|
|
22
24
|
constructor(videoElement: HTMLVideoElement);
|
|
23
25
|
get currentTime(): number;
|
|
24
26
|
get duration(): number;
|
|
@@ -28,6 +30,8 @@ export declare class NativeVideoAdapter implements PlayerAdapter {
|
|
|
28
30
|
get volume(): number;
|
|
29
31
|
set volume(value: number);
|
|
30
32
|
get videoElement(): HTMLVideoElement;
|
|
33
|
+
/** Content is ready and its initial engine seek has settled. */
|
|
34
|
+
get playbackReady(): boolean;
|
|
31
35
|
/**
|
|
32
36
|
* Program Date Time for native HLS playback.
|
|
33
37
|
*
|
|
@@ -43,6 +47,25 @@ export declare class NativeVideoAdapter implements PlayerAdapter {
|
|
|
43
47
|
* treats one valid sample as proof the stream carries PDT.
|
|
44
48
|
*/
|
|
45
49
|
get programDateTime(): Date | null;
|
|
50
|
+
/**
|
|
51
|
+
* The published live edge on WebKit native HLS. WebKit ends `seekable` one hold-back before the
|
|
52
|
+
* newest segment and starts live playback there (measured in Safari: seekable `[0,12]` for a
|
|
53
|
+
* 24s window at TD=4, with media buffered 12s past the seekable end), and exposes no hold-back
|
|
54
|
+
* value, so the HLS minimum of three target durations is added back. A larger `HOLD-BACK` starts
|
|
55
|
+
* further behind than reported; only a low-latency `PART-HOLD-BACK` below 3×TD would start closer.
|
|
56
|
+
*
|
|
57
|
+
* Null off WebKit (`getStartDate()` absent), on VOD, before a seekable range exists, or without
|
|
58
|
+
* a target duration from {@link requestLiveStartOffset}; the SDK then uses the seekable end.
|
|
59
|
+
*/
|
|
60
|
+
get liveEdge(): number | null;
|
|
61
|
+
/**
|
|
62
|
+
* Keeps `fallbackTargetDuration` for {@link liveEdge} and returns false: the element can only
|
|
63
|
+
* be placed before its first frame by an absolute start position, which needs the playlist
|
|
64
|
+
* window, so the SDK seeks once playback starts. `null` withdraws the target duration.
|
|
65
|
+
*/
|
|
66
|
+
requestLiveStartOffset(targetDurations: number | null, fallbackTargetDuration: number | null): boolean;
|
|
67
|
+
/** WebKit's non-standard `getStartDate()` (unbound); undefined on other engines. */
|
|
68
|
+
private webKitStartDate;
|
|
46
69
|
/**
|
|
47
70
|
* The native pipeline is a single decoder per element; buffering the ad
|
|
48
71
|
* element does not contend with the (separate) content element decoder in the
|
|
@@ -1 +1 @@
|
|
|
1
|
-
export class NativeVideoAdapter{constructor(e){this.boundHandlers=new Map,this.eventListeners=new Map,this.cueTracks=[],this.emittedCues=new WeakSet,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 programDateTime(){const e=this.
|
|
1
|
+
export class NativeVideoAdapter{constructor(e){this.boundHandlers=new Map,this.eventListeners=new Map,this.cueTracks=[],this.emittedCues=new WeakSet,this.liveStartTargetDuration=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(e){return new Promise((t,i)=>{const r=()=>{n(),t()},s=()=>{n(),i(new Error("Native video load error"))},n=()=>{this.video.removeEventListener("loadedmetadata",r),this.video.removeEventListener("error",s)};this.video.addEventListener("loadedmetadata",r),this.video.addEventListener("error",s),this.video.preload="auto",this.video.src=e,this.video.load()})}unload(){this.video.removeAttribute("src"),this.video.load()}recreateMediaPipeline(e){const t=this.sourceUrl;return t?new Promise((i,r)=>{const s=()=>{a(),this.video.currentTime=e.startTime,i()},n=()=>{a(),r(new Error("Native pipeline recreate error"))},a=()=>{this.video.removeEventListener("loadedmetadata",s),this.video.removeEventListener("error",n)};this.video.addEventListener("loadedmetadata",s),this.video.addEventListener("error",n),this.video.removeAttribute("src"),this.video.load(),this.video.src=t,this.video.load()}):Promise.reject(new Error("Native pipeline recreate: no source URL"))}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 s=()=>{r||(r=!0,clearTimeout(n),i.removeAttribute("src"),i.load(),t())},n=setTimeout(s,8e3);i.addEventListener("loadedmetadata",s,{once:!0}),i.addEventListener("error",s,{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)},s=e=>{const t=e.track;t&&r(t)};this.addTrackHandler=s,e.addEventListener("addtrack",s);const n=()=>{for(const{track:e}of this.cueTracks)i(e)};this.cueScanHandler=n,this.video.addEventListener("timeupdate",n);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}setupVideoListeners(){const e=["timeupdate","ended","error","seeked","volumechange","waiting","playing","pause"];for(const t of e){const e=()=>{this.emit(t,"error"===t?this.video.error:void 0)};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)}}}
|
|
@@ -57,6 +57,13 @@ export declare class RoutingAdAdapter implements PlayerAdapter {
|
|
|
57
57
|
* `PlayerAdapter.unload`).
|
|
58
58
|
*/
|
|
59
59
|
unload(): void;
|
|
60
|
+
/**
|
|
61
|
+
* Hand a borrowed content element back (shared-element insertion) without emptying it: the
|
|
62
|
+
* content engine's own `load()` replaces the ad media in one step, so the element never sits
|
|
63
|
+
* without a resource — which is when Safari closes the picture-in-picture window. Registered
|
|
64
|
+
* event handlers are kept, as with `unload()`.
|
|
65
|
+
*/
|
|
66
|
+
releaseMediaElement(): void;
|
|
60
67
|
destroy(): void;
|
|
61
68
|
/**
|
|
62
69
|
* Return the inner adapter for `tech`, (re)building it if absent or if a
|
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
|
|
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,i=e.isSupported()){this.inner=null,this.currentTech=null,this.ownedSrc=null,this.hls=null,this.eventListeners=new Map,this.video=t,this.hlsSupported=i}get currentTime(){return this.inner?this.inner.currentTime:this.video.currentTime}get duration(){return this.inner?this.inner.duration:this.video.duration}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.video.currentTime=e}load(e){const t=r(e),i=t=>{const i=this.ensureInner(t).load(e);return this.ownedSrc=this.video.getAttribute("src"),i};return"string"==typeof t?i(t):t.then(i)}preload(e){const t=r(e),i=t=>{const i=this.ensureInner(t);return i.preload?i.preload(e):Promise.resolve()};return"string"==typeof t?i(t):t.then(i)}on(e,t){let i=this.eventListeners.get(e);i||(i=new Set,this.eventListeners.set(e,i)),i.add(t),this.inner?.on(e,t)}off(e,t){this.eventListeners.get(e)?.delete(t),this.inner?.off(e,t)}unload(){this.teardownInner()}releaseMediaElement(){if(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.teardownInner(),this.eventListeners.clear()}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 i of t)this.inner.on(e,i);return this.inner}buildInner(r){return"native"===r?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}}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dolby-optiview/ads-sdk",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.1.1",
|
|
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.1.1",
|
|
48
|
+
"@dolby-optiview/ads-sdk-core": "^1.1.1"
|
|
49
49
|
},
|
|
50
50
|
"peerDependencies": {
|
|
51
51
|
"hls.js": "^1.0.0"
|