Dolby Ads SDK
Player-agnostic SDK for Server-Guided Ad Insertion (SGAI) on HLS live streams. The SDK polls a break manifest from the Optiview Ads backend and inserts ad breaks at the specified times — without modifying the content stream.
Architecture
Key concepts
- Internal ad player — the SDK creates its own
<video>element and calls yourcreateAdAdapterfactory. No need to manage a second player manually. - Two-container DOM —
containeris the outer SDK stage (anchors the ad overlay and companions);playerContainerwraps your content player UI (scaled to a pip corner for L-shape formats). - PlayerAdapter — a thin interface wrapping any HLS-capable player. The SDK never imports player libraries directly.
- Ad formats —
single,double,lshape_ad,lshape_content,overlay. Each format has its own layout rules and content-pause behaviour. - Break manifest — a JSON document served by the Optiview Ads backend that schedules when and what ads to play.
- Session — a per-channel monetization session. Start one per piece of content; end it on channel change or stop.
Getting Started
Installation
npm install @dolby-ads/core @dolby-ads/adapter-hlsjs hls.js
Quick start with HLS.js
import Hls from 'hls.js';
import { DolbyAds } from '@dolby-ads/core';
import { HlsJsAdapter } from '@dolby-ads/adapter-hlsjs';
// 1. Content player
const video = document.getElementById('video');
const hls = new Hls();
hls.attachMedia(video);
// 2. Create the SDK
// playerContainer is optional — SDK auto-wraps container's children if omitted
const sdk = new DolbyAds({
orgId: 'your-org-id',
player: new HlsJsAdapter(hls, video),
container: document.getElementById('container'),
createAdAdapter: (adContainer) => {
const adVideo = document.createElement('video');
adContainer.appendChild(adVideo);
const adHls = new Hls();
adHls.attachMedia(adVideo);
return new HlsJsAdapter(adHls, adVideo);
},
debug: true,
});
// 3. Listen to events
sdk.addEventListener('adbreakbegin', (e) => console.log('Break started:', e.break.id));
sdk.addEventListener('adbreakend', (e) => console.log('Break ended:', e.break.id));
// 4. Start a monetization session for a channel
await sdk.startSession({ channelId: 'your-channel-id' });
// 5. Load and play your content stream
hls.loadSource('https://example.com/stream.m3u8');
video.play();
<div> container and passes it to your createAdAdapter factory. Create whatever media element(s) your player library needs inside that container and return a PlayerAdapter wrapping them. playerContainer is optional — when omitted the SDK automatically wraps the existing children of container.Minimal HTML
<!-- Include IMA SDK if using Google Ad Manager -->
<script src="https://imasdk.googleapis.com/js/sdkloader/ima3_dai.js"></script>
<!-- Minimal: single container, SDK auto-wraps your content -->
<div id="container" style="position:relative;width:100%;aspect-ratio:16/9">
<video id="video" controls muted playsinline></video>
<!-- SDK auto-creates a playerContainer wrapper around the video and appends .dolby-ad-container here -->
</div>
<!-- Optional: provide your own playerContainer for more control (e.g. if you have custom controls inside) -->
<div id="container" style="position:relative;width:100%;aspect-ratio:16/9">
<div id="playerContainer" style="width:100%;height:100%">
<video id="video" controls muted playsinline></video>
</div>
</div>
SDK Configuration
Configuration is split into two levels: SDK-level (fixed for the app lifetime) and session-level (per channel / content piece).
DolbyAdsConfig constructor
| Property | Type | Required | Description |
|---|---|---|---|
| orgId | string | ✅ | Organization ID from the Optiview Ads platform. |
| player | PlayerAdapter | ✅ | Adapter wrapping your content video player. |
| container | HTMLElement | ✅ | Outer SDK stage element. The SDK appends its ad overlay and companion elements here. Must have position: relative (SDK sets it automatically if unset). |
| playerContainer | HTMLElement | ❌ | Wrapper around your content video UI. The SDK animates this to a pip corner for L-shape formats. Optional — when omitted the SDK automatically wraps the existing children of container in a new <div>. |
| createAdAdapter | (container: HTMLElement) => PlayerAdapter |
✅ | Factory called once at initialisation. Receives a <div> container; create your media element(s) inside it and return a PlayerAdapter wrapping them. |
| gam | GamConfig | ❌ | Google Ad Manager configuration. Required for GAM pod serving. |
| manifestBaseUrl | string | ❌ | Override the default manifest base URL. |
| debug | boolean | ❌ | Enable verbose SDK logging to the console. Default: false. |
SessionConfig startSession()
| Property | Type | Required | Description |
|---|---|---|---|
| channelId | string | ✅ | Channel ID from the Optiview Ads platform. Determines the manifest endpoint. |
| customAssetKey | string | ❌ | GAM DAI custom asset key for this channel. Required when gam is configured. |
| adTagParameters | Record<string, string> |
❌ | Per-session ad targeting parameters. Merged with GamConfig.adTagParameters; session values take precedence on key conflicts. |
GamConfig optional
| Property | Type | Required | Description |
|---|---|---|---|
| networkCode | string | ✅ | Google Ad Manager network code. |
| adTagParameters | Record<string, string> |
❌ | Org-level ad tag parameters, merged with per-session parameters. |
| streamActivityMonitorId | string | ❌ | Debug session ID for Google's Stream Activity Monitor tool. |
Example
const sdk = new DolbyAds({
orgId: '0bda787b-b10a-4e9e-a5f2-c0952d1b8a88',
player: new HlsJsAdapter(hls, video),
container: document.getElementById('container'),
playerContainer: document.getElementById('playerContainer'),
createAdAdapter: (adContainer) => {
const adVideo = document.createElement('video');
adContainer.appendChild(adVideo);
const adHls = new Hls();
adHls.attachMedia(adVideo);
return new HlsJsAdapter(adHls, adVideo);
},
gam: {
networkCode: '23285652104',
adTagParameters: { ott_placement: '0' }, // org-level defaults
streamActivityMonitorId: 'my-debug-session',
},
debug: false,
});
Session Management
A session represents the monetization lifecycle for one piece of content. Call startSession() before loading a stream and endSession() when done.
startSession(config: SessionConfig): Promise<void>
Starts manifest polling and (if GAM is configured) initializes the IMA stream session. Resolves when the first manifest has been fetched successfully.
await sdk.startSession({
channelId: 'd7803a87-465a-4e43-b12e-6f479be119a1',
customAssetKey: 'my-asset-key',
adTagParameters: { cust_params: 'genre=sports' },
});
// Now load and play your content
hls.loadSource(contentUrl);
video.play();
endSession(): void
Stops manifest polling and resets the GAM session. Call this when the user navigates away, changes channel, or stops playback.
sdk.endSession();
updateAdTagParameters(params: Record<string, string>): void
Replaces the active GAM session's ad tag parameters in real time. Useful for updating targeting (e.g. sport segment changes) without restarting the session. The new parameters are merged with the base config params.
sdk.updateAdTagParameters({ cust_params: 'sport=basketball' });
destroy(): void
Tears down the entire SDK instance, releasing all resources. Call this when the player is unmounted.
sdk.destroy();
play(): Promise<void>
Proxy for contentPlayer.play(). Suppressed automatically during content-locking ad breaks (e.g. single format) so the ad is not interrupted. Prefer this over calling your player directly so break policies are honoured.
await sdk.play();
pause(): void
Proxy for contentPlayer.pause(). No-op during a content-locking ad break.
sdk.pause();
seek(time: number): void
Seek the content stream to a position in seconds. Blocked when the active break has controls.snapback: true. The SDK also listens to the native seeked event from the PlayerAdapter and will snap back automatically even if the customer seeks directly on the underlying player.
sdk.seek(180); // seek to 3:00 in the content stream
Lifecycle example
// App startup
const sdk = new DolbyAds({ ... });
// User starts watching a channel
await sdk.startSession({ channelId, customAssetKey });
hls.loadSource(channelUrl);
// User switches to another channel
sdk.endSession();
await sdk.startSession({ channelId: newChannelId, customAssetKey: newKey });
hls.loadSource(newChannelUrl);
// App teardown
sdk.destroy();
Events
Subscribe via sdk.addEventListener(type, handler). All events include a type string and a timestamp (ms since epoch).
| Event | When | Key payload fields |
|---|---|---|
| adbreakbegin | Ad break starts (content paused) | break — the full Break object |
| adbreakend | Ad break ends (content resumes) | break |
| adbegin | Individual ad starts playing | break, asset, adIndex, totalAds |
| adend | Individual ad finishes | break, asset, adIndex, totalAds |
| aderror | Ad fails to play (non-fatal, SDK recovers) | break, asset?, error |
| adfirstquartile | Ad reaches 25% completion | break, asset |
| admidpoint | Ad reaches 50% completion | break, asset |
| adthirdquartile | Ad reaches 75% completion | break, asset |
| adtimeupdate | Fires on each timeupdate tick during ad playback (~4 Hz) |
break, asset, currentTime, duration |
TypeScript types
// Base — all events extend this
interface DolbyAdsEvent {
type: string;
timestamp: number;
}
// Break events
sdk.addEventListener('adbreakbegin', (e: AdBreakBeginEvent) => {
// e.break.id, e.break.duration, e.break.start
});
// Ad events
sdk.addEventListener('adbegin', (e: AdBeginEvent) => {
// e.asset.id, e.asset.type, e.adIndex, e.totalAds
});
// Error events
sdk.addEventListener('aderror', (e: AdErrorEvent) => {
console.error(e.error.message);
// SDK automatically recovers — content will resume
});
// Ad progress — track ad position to drive a custom progress bar
sdk.addEventListener('adtimeupdate', (e: AdTimeupdateEvent) => {
const pct = (e.currentTime / e.duration) * 100;
progressBar.style.width = `${pct}%`;
});
Unsubscribing
const handler = (e) => console.log(e);
sdk.addEventListener('adbreakbegin', handler);
// Later:
sdk.removeEventListener('adbreakbegin', handler);
Building a custom player UI
Replace the browser's native controls with your own UI so you can:
- Hide controls during ad breaks and show a countdown toast instead.
- Route play/pause through the SDK so break policies (content-lock, snapback) are honoured.
Key rules
- Use
sdk.play()/sdk.pause()instead ofvideo.play()/video.pause()— the SDK will no-op these during content-locking breaks, preventing the user from accidentally resuming content mid-break. - Use
sdk.seek(time)instead of settingvideo.currentTimedirectly — honourssnapbackenforcement. - Drive your countdown from
adbreakbegin— thee.break.durationfield gives the total break length in seconds. Update the display with asetIntervalor by consumingadtimeupdate. - Dismiss the countdown on
adbreakend—adbreakendfires after all assets in the break have played (or been skipped/errored).
Minimal example
<!-- Remove the native controls attribute -->
<video id="video" muted playsinline></video>
<!-- Custom controls bar (inside the player wrapper) -->
<div id="playerControls" class="player-controls">
<button id="btnPlay">▶</button>
<button id="btnMute">🔇</button>
<button id="btnFullscreen">⤢</button>
</div>
<!-- Break countdown toast (positioned over the player) -->
<div id="breakToast" class="break-toast hidden">
<span class="toast-badge">AD</span>
<span id="toastText">Ad break</span>
</div>
let countdownInterval: ReturnType<typeof setInterval> | null = null;
sdk.addEventListener('adbreakbegin', (e: AdBreakBeginEvent) => {
// 1. Hide player controls
document.getElementById('playerControls')!.classList.remove('visible');
// 2. Show and start the countdown toast
const toast = document.getElementById('breakToast')!;
const text = document.getElementById('toastText')!;
toast.classList.add('visible');
let remaining = Math.round(e.break.duration ?? 0);
text.textContent = remaining > 0 ? `Ad break · ${remaining}s remaining` : 'Ad break';
if (remaining > 0) {
countdownInterval = setInterval(() => {
remaining = Math.max(0, remaining - 1);
text.textContent = remaining > 0 ? `Ad break · ${remaining}s remaining` : 'Ad break ending…';
if (remaining === 0) { clearInterval(countdownInterval!); countdownInterval = null; }
}, 1000);
}
});
sdk.addEventListener('adbreakend', () => {
// Dismiss toast and restore controls
clearInterval(countdownInterval!);
countdownInterval = null;
document.getElementById('breakToast')!.classList.remove('visible');
document.getElementById('playerControls')!.classList.add('visible');
});
// Route play/pause through the SDK
document.getElementById('btnPlay')!.addEventListener('click', () => {
if (video.paused) sdk.play(); else sdk.pause();
});
Player Adapters
The SDK core (@dolby-ads/core) has zero dependencies on any video player library. All player interactions go through the PlayerAdapter interface — a small contract that any player can implement.
This means you can use the SDK with HLS.js today and migrate to Shaka or Video.js later by swapping the adapter, with no changes to your SDK integration code.
PlayerAdapter Interface
Any adapter must implement all properties and methods below. The interface is imported from @dolby-ads/core.
import type { PlayerAdapter, PlayerAdapterEvent, PlayerAdapterEventHandler } from '@dolby-ads/core';
// Events the adapter must support
type PlayerAdapterEvent = 'timeupdate' | 'ended' | 'error' | 'seeked' | 'volumechange';
interface PlayerAdapter {
/** Current playback position in seconds. */
readonly currentTime: number;
/** Total duration in seconds (Infinity for live streams). */
readonly duration: number;
/** True when playback is paused. */
readonly paused: boolean;
/** Whether the player is muted. */
muted: boolean;
/** Volume level in the range [0, 1]. */
volume: number;
/** Current Program Date Time from the HLS manifest.
* Required for wallclock-timebase break matching on live streams.
* Return null if not available. */
readonly programDateTime: Date | null;
/** Pause playback. Called by SDK at break start. */
pause(): void;
/** Resume playback. Called by SDK at break end. */
play(): Promise<void>;
/** Seek to a position in seconds.
* Required for SDK snapback enforcement during locked breaks. */
seek(time: number): void;
/** Load a media URL and resolve when ready to play (manifest parsed).
* Used for preloading ad content 5 seconds before break start. */
load(url: string): Promise<void>;
/** Subscribe to a player event. */
on(event: PlayerAdapterEvent, handler: PlayerAdapterEventHandler): void;
/** Unsubscribe from a player event. */
off(event: PlayerAdapterEvent, handler: PlayerAdapterEventHandler): void;
/** Release all resources. Called when SDK is destroyed. */
destroy(): void;
}
| Member | Notes |
|---|---|
| programDateTime | Critical for live streams with timebase: "wallclock". Track the current EXT-X-PROGRAM-DATE-TIME tag from the HLS manifest. Return null if the stream has no PDT. |
| load(url) | Must resolve only when the player has parsed the manifest and buffered enough to play immediately. Reject on fatal errors. This enables seamless preloading. |
| seek(time) | Set the playback position directly. Used by the SDK to snap back to a break start when controls.snapback: true and a seek is detected. |
| on / off | The SDK listens to 'timeupdate' for break timing, 'ended' to know when an ad asset finishes, 'error' for recovery, 'seeked' for snapback enforcement, and 'volumechange' for mute/volume sync. |
HLS.js Adapter
The official HLS.js adapter is available as a separate package.
Install
npm install @dolby-ads/adapter-hlsjs hls.js
Requirements
- One
Hlsinstance for content. The ad HLS instance is created insidecreateAdAdapter. - One
<video>element for content. The ad<video>is created by your factory inside the SDK-provided container. - HLS streams must contain
EXT-X-PROGRAM-DATE-TIMEtags for wallclock-timebase channels.
Full setup example
import Hls from 'hls.js';
import { DolbyAds } from '@dolby-ads/core';
import { HlsJsAdapter } from '@dolby-ads/adapter-hlsjs';
const video = document.getElementById('video') as HTMLVideoElement;
const hls = new Hls();
hls.attachMedia(video);
const sdk = new DolbyAds({
orgId: 'your-org-id',
player: new HlsJsAdapter(hls, video),
container: document.getElementById('container'),
createAdAdapter: (adContainer) => {
const adVideo = document.createElement('video');
adContainer.appendChild(adVideo);
const adHls = new Hls();
adHls.attachMedia(adVideo);
return new HlsJsAdapter(adHls, adVideo);
},
gam: { networkCode: '23285652104' },
});
sdk.addEventListener('adbreakbegin', (e) => {
console.log('Ad break', e.break.id, 'started —', e.break.duration, 's');
});
sdk.addEventListener('adbreakend', () => {
console.log('Content resuming');
});
await sdk.startSession({
channelId: 'your-channel-id',
customAssetKey: 'your-asset-key',
});
hls.loadSource('https://example.com/live.m3u8');
video.play();
Seek & seeked event
The HLS.js adapter implements seek(time) by setting video.currentTime and forwards the native seeked video event. This enables the SDK to detect and correct unexpected seeks during locked ad breaks.
PDT (Program Date Time)
The HLS.js adapter automatically tracks EXT-X-PROGRAM-DATE-TIME from the manifest and exposes it via programDateTime. This is used by the SDK to match wallclock-timebase breaks against the live stream position.
EXT-X-PROGRAM-DATE-TIME tags, wallclock breaks cannot be matched. Ensure your origin adds these tags to HLS manifests.Cleanup
// When the player is torn down:
sdk.destroy(); // also destroys the internal ad player
hls.destroy();
Shaka Player Adapter
The official Shaka Player adapter is available as a separate package. It supports HLS and DASH streams and extracts programDateTime from Shaka's presentation timeline for live streams.
Install
npm install @dolby-ads/adapter-shaka shaka-player
Requirements
- Shaka Player 4.x or 5.x
- Call
shaka.polyfill.installAll()once before creating anyshaka.Playerinstance
Full setup example
import shaka from 'shaka-player';
import { DolbyAds } from '@dolby-ads/core';
import { ShakaAdapter } from '@dolby-ads/adapter-shaka';
// 1. Install Shaka polyfills (once per page)
shaka.polyfill.installAll();
// 2. Content player
const video = document.getElementById('video') as HTMLVideoElement;
const player = new shaka.Player(video);
// 3. SDK — pass Player WITHOUT video to avoid eager MediaSource init on the ad element
// playerContainer is optional; SDK auto-wraps when omitted
const sdk = new DolbyAds({
orgId: 'your-org-id',
player: new ShakaAdapter(player, video),
container: document.getElementById('container'),
createAdAdapter: (adContainer) => {
const adVideo = document.createElement('video');
adContainer.appendChild(adVideo);
return new ShakaAdapter(new shaka.Player(), adVideo);
},
});
// 5. Start session and load stream
await sdk.startSession({ channelId: 'your-channel-id' });
await player.load('https://example.com/live.m3u8');
video.play();
PDT (Program Date Time)
For live HLS streams with EXT-X-PROGRAM-DATE-TIME, the Shaka adapter derives the wall-clock position from Shaka's PresentationTimeline.getPresentationStartTime() combined with video.currentTime. This is used to match wallclock-timebase breaks.
null for VOD content and live streams without absolute time references.Structural typing — no hard import on shaka-player
The ShakaAdapter constructor accepts any object that satisfies the ShakaPlayerLike interface exported from the package. The real shaka.Player satisfies this interface, but you can also pass a compatible mock in tests without importing the full Shaka library.
Cleanup
// When the player is torn down:
sdk.destroy(); // also destroys the internal ad player
await player.destroy();
THEOplayer Adapter
The official THEOplayer adapter is available as a separate package. Unlike HLS.js and Shaka, THEOplayer manages its own internal <video> element — you pass it a <div> container and it renders into that.
Install
npm install @dolby-ads/adapter-theoplayer theoplayer
Requirements
- THEOplayer v9 or later (peer dependency)
- A valid THEOplayer license key (passed in the player configuration)
- Set
libraryLocationto serve THEOplayer's worker/WASM files — use a CDN or copy fromnode_modules/theoplayer/
Key difference — no raw <video> element
THEOplayer creates and manages its own <video> element inside the container div. The adapter exposes it via the videoElement getter (container.querySelector('video')) for GAM integration.
Full setup example
import { ChromelessPlayer } from 'theoplayer/chromeless';
import { DolbyAds } from '@dolby-ads/core';
import { THEOplayerAdapter } from '@dolby-ads/adapter-theoplayer';
const LICENSE = 'YOUR_THEOPLAYER_LICENSE';
const LIB = 'https://cdn.jsdelivr.net/npm/theoplayer@11.4.0/';
// 1. Content player — THEOplayer mounts into a <div>
const playerDiv = document.getElementById('player') as HTMLElement;
const player = new ChromelessPlayer(playerDiv, {
license: LICENSE,
libraryLocation: LIB,
allowMixedContent: true,
mutedAutoplay: 'all',
});
// 2. SDK — playerContainer is optional; SDK auto-wraps when omitted
const sdk = new DolbyAds({
orgId: 'your-org-id',
player: new THEOplayerAdapter(player, playerDiv),
container: document.getElementById('container'),
createAdAdapter: (adContainer) => {
const adPlayer = new ChromelessPlayer(adContainer, {
license: LICENSE,
libraryLocation: LIB,
allowMixedContent: true,
mutedAutoplay: 'all',
});
return new THEOplayerAdapter(adPlayer, adContainer);
},
});
// 3. Start session and load stream
await sdk.startSession({ channelId: 'your-channel-id' });
player.source = { sources: [{ src: 'https://example.com/live.m3u8' }] };
player.play();
Autoplay configuration
The ad player's play() is triggered by the SDK's break scheduler — always outside a user gesture. Set mutedAutoplay: 'all' in the ChromelessPlayer config for both the content and ad player instances. This tells THEOplayer to permit autoplay regardless of browser policy.
mutedAutoplay: 'all' will cause the ad player to silently refuse to play on browsers with strict autoplay policies (Chrome, Safari, most Smart TV WebViews).PDT (Program Date Time)
The THEOplayer adapter reads player.currentProgramDateTime directly — THEOplayer exposes the wall-clock position as a Date on that property for live HLS/DASH streams. No custom manifest parsing is needed.
Cleanup
sdk.destroy(); // also destroys the internal ad player
player.destroy();
Implementing a Custom Adapter
To use the SDK with Video.js, AVPlayer (iOS), or any other HLS-capable player, implement the PlayerAdapter interface.
TypeScript skeleton
import type {
PlayerAdapter,
PlayerAdapterEvent,
PlayerAdapterEventHandler,
} from '@dolby-ads/core';
export class MyPlayerAdapter implements PlayerAdapter {
private player: MyPlayer; // ← your player instance
private video: HTMLVideoElement;
private listeners = new Map<PlayerAdapterEvent, Set<PlayerAdapterEventHandler>>();
private _programDateTime: Date | null = null;
constructor(player: MyPlayer, videoElement: HTMLVideoElement) {
this.player = player;
this.video = videoElement;
this.setupListeners();
}
// ── Required properties ──────────────────
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(v) { this.video.muted = v; }
get volume() { return this.video.volume; }
set volume(v) { this.video.volume = v; }
get programDateTime(): Date | null {
// Track EXT-X-PROGRAM-DATE-TIME from your player's manifest events
return this._programDateTime;
}
// ── Required methods ─────────────────────
pause(): void {
this.video.pause();
}
async play(): Promise<void> {
await this.video.play();
}
seek(time: number): void {
this.video.currentTime = time; // adapt to your player's seek API if needed
}
/** Load URL — resolve when manifest is parsed and player is ready to play. */
async load(url: string): Promise<void> {
return new Promise((resolve, reject) => {
this.player.once('manifestparsed', resolve); // adapt to your player's event
this.player.once('error', reject);
this.player.load(url);
});
}
on(event: PlayerAdapterEvent, handler: PlayerAdapterEventHandler): void {
if (!this.listeners.has(event)) this.listeners.set(event, new Set());
this.listeners.get(event)!.add(handler);
}
off(event: PlayerAdapterEvent, handler: PlayerAdapterEventHandler): void {
this.listeners.get(event)?.delete(handler);
}
destroy(): void {
this.listeners.clear();
// Remove any event listeners added in setupListeners()
}
// ── Internal helpers ─────────────────────
private emit(event: PlayerAdapterEvent, data?: unknown): void {
this.listeners.get(event)?.forEach((h) => h(data));
}
private setupListeners(): void {
// Forward native video events to SDK listeners
this.video.addEventListener('timeupdate', () => this.emit('timeupdate'));
this.video.addEventListener('ended', () => this.emit('ended'));
this.video.addEventListener('error', () => this.emit('error'));
this.video.addEventListener('seeked', () => this.emit('seeked'));
this.video.addEventListener('volumechange', () => this.emit('volumechange'));
// Track PDT from your player's manifest events
this.player.on('fragmentchanged', (frag) => {
if (frag.programDateTime) this._programDateTime = new Date(frag.programDateTime);
});
}
}
•
load(url) must resolve only when the player is ready to start playback immediately — not just when the request is sent.•
programDateTime must reflect the current stream position, not just the start of the manifest.• The SDK passes a
<div> container to createAdAdapter. Create your own <video> inside it — never share the content player instance or its video element.
Google Ad Manager
The SDK integrates with Google DAI Pod Serving via the IMA DAI SDK. When a GAM vendor asset is scheduled, the SDK requests a pod manifest URL and plays it in the ad player.
Prerequisites
- A Google Ad Manager account with DAI Pod Serving enabled.
- A Network Code and a Custom Asset Key per channel.
- Include the IMA DAI SDK in your HTML before your application script:
<script src="https://imasdk.googleapis.com/js/sdkloader/ima3_dai.js"></script>
Configuration
const sdk = new DolbyAds({
orgId: '...',
player: contentAdapter,
container: document.getElementById('container'),
playerContainer: document.getElementById('playerContainer'),
createAdAdapter: (adVideo) => buildAdAdapter(adVideo),
gam: {
networkCode: '23285652104', // your GAM network code
adTagParameters: {
ott_placement: '0', // 0 = single / full-screen
cust_params: 'genre=sports', // custom targeting
},
},
});
await sdk.startSession({
channelId: 'your-channel-id',
customAssetKey: 'your-asset-key', // per-channel key
adTagParameters: { cust_params: 'region=eu' }, // merged at session level
});
Ad tag parameters
Parameters from GamConfig.adTagParameters and SessionConfig.adTagParameters are merged before each IMA session. Session values override config values on key conflicts.
To update parameters during an active session (e.g. content segment changes):
sdk.updateAdTagParameters({ cust_params: 'genre=basketball' });
This calls IMA's StreamManager.replaceAdTagParameters() under the hood — no session restart needed.
OTT placement values
| Value | Placement |
|---|---|
| 0 | Single (standard full-screen) |
| 1 | Pause screen |
| 3 | Picture-in-Picture / double box |
| 4 | L-banner |
| 5 | Overlay |
How pod serving works
When a GAM break is detected in the manifest, the SDK:
- Uses the IMA
StreamManager(initialized atstartSession()) to build a pod manifest URL from the stream ID and pod ID. - Loads that URL into the ad player (preloaded 5 seconds before break start).
- Pauses content and plays the ad overlay at break time.
- Forwards IMA metadata events for ad tracking (quartile beacons, etc.).
Ad Formats
Each break in the manifest carries a variant that specifies the format. The SDK applies the correct layout automatically and controls whether content playback is paused.
| Format | Content paused? | Companion? | Description |
|---|---|---|---|
| single | ✅ Yes | — | Full-screen ad overlay. Content is hidden and paused. |
| double | ❌ No | ✅ Image (full background) | Content and ad play side-by-side in equal boxes (920×517.5px at 1080p, 20px border). Companion image fills the full background behind both boxes. Animated transition in/out. |
| lshape_ad | ✅ Yes | ✅ Image (full background) | Content player is hidden. Ad video plays in a pip at the top-left quadrant (5% inset). Companion image fills the full background behind the pip. Animated transition in/out. |
| lshape_content | ❌ No | — | Content player shrinks to a pip at the top-left quadrant — content keeps playing. A backdrop image (from assets[0]) fills the full background. No ad video plays. Animated transition in/out. |
| overlay | ❌ No | — | Semi-transparent ad positioned over content using manifest-supplied position and size values (fractional 0.0–1.0, converted to %). Content keeps playing. |
Manifest variant shapes
single / lshape_content / overlay — simple assets array
{
"format": "single",
"assets": [
{ "id": "a1", "type": "static", "mediaType": "video", "uri": "https://cdn.example.com/ad.m3u8" }
]
}
double / lshape_ad — assets with companion (flat)
The companion is a property directly on the asset object (flat intersection, not nested).
{
"format": "double",
"assets": [
{
"id": "a1", "type": "static", "mediaType": "video", "uri": "https://cdn.example.com/ad.m3u8",
"companion": { "id": "c1", "type": "static", "mediaType": "image", "uri": "https://cdn.example.com/companion.jpg" }
}
]
}
overlay — position + size (fractional 0.0–1.0)
{
"format": "overlay",
"assets": [ "..." ],
"position": { "bottom": 0.05, "right": 0.05 },
"size": { "width": 0.30, "height": 0.20 },
"opacity": 0.9
}
Layout DOM structure
At initialize() the SDK permanently sets playerContainer to position:absolute; top:0; left:0; right:0; bottom:0 with a 0.3s ease-in-out transition on all position axes, enabling smooth animated transitions between formats. At break end the container animates back to the base position. On destroy() the original styles are fully restored.
| Format | playerContainer at break start | .dolby-ad-container | .dolby-companion |
|---|---|---|---|
| single | base (full stage) | top:0; left:0; right:0; bottom:0 |
— |
| double | top:26.04%; left:1.04%; right:51.04%; bottom:26.04% (left box) |
top:26.04%; left:51.04%; right:1.04%; bottom:26.04% (right box) |
Full background, z-index 99 |
| lshape_ad | top:5%; left:5%; right:30%; bottom:30% then opacity:0 (content scales to pip before hiding) |
top:5%; left:5%; right:30%; bottom:30% pip, z-index 100 (ad plays here) |
Full background, z-index 99 |
| lshape_content | top:5%; left:5%; right:30%; bottom:30% pip, z-index 101 (content plays here) |
Hidden (display:none) | Backdrop from assets[0], full background, z-index 99 |
| overlay | base (full stage) | Manifest fractional position/size → CSS % | — |
Break Manifest
The break manifest is a JSON document served by the Optiview Ads backend. The SDK fetches it at session start and polls it at the intervals specified in the manifest.
Manifest URL
GET https://optiview-ads.sneezysparrow.com/manifest/v1/{orgId}/channels/{channelId}
Structure overview
{
"version": "1.0.0",
"timebase": "wallclock",
"polling": {
"idle": 30,
"active": 5
},
"breaks": [
{
"id": "break-001",
"start": "2026-06-07T14:00:00Z",
"duration": 30,
"variant": {
"format": "single",
"assets": [
{
"id": "asset-001",
"type": "vendor",
"vendor": "gam",
"mediaType": "video",
"uri": "pod-id-from-eabn",
"vendorParameters": {
"type": "pod",
"eabnVersion": "V2",
"networkCode": "23285652104",
"customAssetKey": "my-asset-key"
}
}
]
}
}
]
}
Timebase
| Value | break.start type | Matching strategy |
|---|---|---|
| wallclock | ISO-8601 string | Compared against the stream's EXT-X-PROGRAM-DATE-TIME via programDateTime. |
| pts | number (seconds) | Compared against player.currentTime. |
Asset types
| type | Description |
|---|---|
| static | A direct HLS URL. SDK loads it into the ad player as-is. |
| vendor (gam) | A GAM pod ID. SDK builds a DAI pod manifest URL using the IMA stream session. |
| vast | A VAST ad tag URL. (Not yet supported in current SDK version.) |
docs/dolby-ads-manifest-spec.md in the SDK repository.Custom Player UI
The SDK is designed to work alongside a fully custom player UI. By replacing the browser's native controls with your own, you get complete control over how playback and ad breaks are presented to users.
Why custom controls?
Native browser controls have no awareness of ad breaks. A user could hit pause, seek, or unmute at exactly the wrong moment during a break. Routing all interactions through the SDK prevents this:
| Action | Native | With SDK |
|---|---|---|
video.pause() during locked break |
Pauses content | No-op (SDK ignores it) |
video.currentTime = x during break |
Seeks content | SDK snaps back if snapback: true |
| Play button during break | Resumes content | SDK blocks until break ends |
Setup
Remove the controls attribute and position your own UI elements inside the player container:
<!-- The SDK stage -->
<div id="container" style="position:relative;">
<!--
Provide an explicit playerContainer so your controls scale correctly
during L-shape/Double pip animations. If you omit it the SDK wraps
the container's children automatically, but controls must live inside
that wrapper to move with the player.
-->
<div id="playerContainer" style="position:absolute;inset:0;">
<video id="video" muted playsinline></video>
<!-- Controls bar: lives inside playerContainer so it scales with PIP -->
<div id="playerControls" class="player-controls">
<button id="btnPlay" aria-label="Play/Pause"></button>
<button id="btnMute" aria-label="Mute/Unmute"></button>
<button id="btnFullscreen" aria-label="Fullscreen"></button>
</div>
</div>
<!-- Break toast: lives on container so it always covers the full stage -->
<div id="breakToast" class="break-toast">
<span class="toast-badge">AD</span>
<span id="toastText">Ad break</span>
</div>
</div>
Routing playback through the SDK
Always call sdk.play(), sdk.pause(), and sdk.seek() — never manipulate the video element directly from your UI:
btnPlay.addEventListener('click', () => {
if (video.paused) sdk.play(); // SDK will block if break is content-locking
else sdk.pause(); // SDK will no-op if break is content-locking
});
// Seek to 3:00 — SDK will snap back if a locked break is active
btnSeek.addEventListener('click', () => sdk.seek(180));
Hiding controls during ad breaks
The SDK fires adbreakbegin and adbreakend when a break starts and ends. Use these to swap your controls for a break indicator:
sdk.addEventListener('adbreakbegin', () => {
document.getElementById('playerControls')!.classList.remove('visible');
document.getElementById('breakToast')!.classList.add('visible');
});
sdk.addEventListener('adbreakend', () => {
document.getElementById('breakToast')!.classList.remove('visible');
document.getElementById('playerControls')!.classList.add('visible');
});
Break countdown timer
adbreakbegin gives you the total break duration via e.break.duration (seconds, may be undefined for dynamic breaks). Drive a countdown with setInterval:
let countdownTimer: ReturnType<typeof setInterval> | null = null;
sdk.addEventListener('adbreakbegin', (e: AdBreakBeginEvent) => {
const toastText = document.getElementById('toastText')!;
let remaining = Math.round(e.break.duration ?? 0);
const fmt = (s: number) => s >= 60 ? `${Math.floor(s/60)}m ${s%60}s` : `${s}s`;
toastText.textContent = remaining > 0 ? `Ad break · ${fmt(remaining)} remaining` : 'Ad break';
if (remaining > 0) {
countdownTimer = setInterval(() => {
remaining = Math.max(0, remaining - 1);
toastText.textContent = remaining > 0
? `Ad break · ${fmt(remaining)} remaining`
: 'Ad break ending…';
if (remaining === 0) { clearInterval(countdownTimer!); countdownTimer = null; }
}, 1000);
}
});
sdk.addEventListener('adbreakend', () => {
clearInterval(countdownTimer!);
countdownTimer = null;
});
For a more accurate countdown that stays in sync with actual ad playback, use adtimeupdate instead:
sdk.addEventListener('adtimeupdate', (e: AdTimeupdateEvent) => {
const remaining = Math.ceil(e.duration - e.currentTime);
document.getElementById('toastText')!.textContent =
`Ad break · ${remaining}s remaining`;
});
Ad progress bar
adtimeupdate fires at ~4 Hz during ad playback and includes both currentTime and duration:
sdk.addEventListener('adtimeupdate', (e: AdTimeupdateEvent) => {
const pct = e.duration > 0 ? (e.currentTime / e.duration) * 100 : 0;
progressBar.style.width = `${pct}%`;
});
Auto-hiding controls
Show controls on mouse interaction and hide them after a timeout during playback:
let hideTimer: ReturnType<typeof setTimeout> | null = null;
function showControls() {
controls.classList.add('visible');
clearTimeout(hideTimer!);
if (!video.paused) {
hideTimer = setTimeout(() => controls.classList.remove('visible'), 3000);
}
}
container.addEventListener('mousemove', showControls);
video.addEventListener('pause', showControls); // keep visible when paused
Fullscreen
Request fullscreen on the SDK container (not the video element) so ad overlays and companion banners are included:
btnFullscreen.addEventListener('click', () => {
if (!document.fullscreenElement) container.requestFullscreen();
else document.exitFullscreen();
});
document.addEventListener('fullscreenchange', () => {
btnFullscreen.setAttribute('aria-label',
document.fullscreenElement ? 'Exit fullscreen' : 'Fullscreen');
});
Mute / volume sync
The SDK syncs mute state between the content player and the ad player automatically. You only need to update the icon:
btnMute.addEventListener('click', () => {
video.muted = !video.muted;
});
video.addEventListener('volumechange', () => {
btnMute.setAttribute('aria-label', video.muted ? 'Unmute' : 'Mute');
// swap icon here
});