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.

@dolby-ads/core @dolby-ads/adapter-hlsjs TypeScript

Architecture

┌─────────────────────────────────────────────────────────────┐ │ Your Application │ │ │ │ ┌──────────────────────────────────────────────────────┐ │ │ │ #container (SDK stage, position: relative) │ │ │ │ ┌──────────────────────┐ ┌──────────────────────┐ │ │ │ │ │ #playerContainer │ │ .dolby-ad-container │ │ │ │ │ │ (content video UI) │ │ (SDK-managed, hidden │ │ │ │ │ │ │ │ until break starts) │ │ │ │ │ └──────────────────────┘ └──────────────────────┘ │ │ │ └──────────────────────────────────────────────────────┘ │ │ ▲ │ │ │ createAdAdapter factory │ │ ┌────────────────────┐ │ PlayerAdapter interface │ │ │ @dolby-ads/core │──────┘ │ │ │ • Manifest poll │ ◄── ContentPlayer (PlayerAdapter) │ │ │ • Break schedule │ │ │ │ • Layout mgmt │ ──► @dolby-ads/adapter-hlsjs │ │ └────────────────────┘ (or your own adapter) │ └─────────────────────────────────────────────────────────────┘

Key concepts

  • Internal ad player — the SDK creates its own <video> element and calls your createAdAdapter factory. No need to manage a second player manually.
  • Two-container DOMcontainer is the outer SDK stage (anchors the ad overlay and companions); playerContainer wraps 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 formatssingle, 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();
The SDK creates a <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 of video.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 setting video.currentTime directly — honours snapback enforcement.
  • Drive your countdown from adbreakbegin — the e.break.duration field gives the total break length in seconds. Update the display with a setInterval or by consuming adtimeupdate.
  • Dismiss the countdown on adbreakendadbreakend fires 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.

Your App └─ DolbyAds (core) ├─ content PlayerAdapter ──► HlsJsAdapter ──► Hls.js instance └─ createAdAdapter ──► (SDK creates <div>) ──► HlsJsAdapter ──► Hls.js instance ──► ShakaAdapter ──► Shaka Player (future) ──► VideoJsAdapter ──► Video.js (future)

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 Hls instance for content. The ad HLS instance is created inside createAdAdapter.
  • 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-TIME tags 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.

If your stream does not include 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 any shaka.Player instance

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.

PDT is only available for live streams where Shaka has parsed the presentation timeline. It returns 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 libraryLocation to serve THEOplayer's worker/WASM files — use a CDN or copy from node_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.

Omitting 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);
    });
  }
}
Key implementation notes:
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

  1. A Google Ad Manager account with DAI Pod Serving enabled.
  2. A Network Code and a Custom Asset Key per channel.
  3. 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:

  1. Uses the IMA StreamManager (initialized at startSession()) to build a pod manifest URL from the stream ID and pod ID.
  2. Loads that URL into the ad player (preloaded 5 seconds before break start).
  3. Pauses content and plays the ad overlay at break time.
  4. 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.)
The full manifest specification — including break variants, asset targeting, forward compatibility rules, and JSON schema — is documented in 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
});