Skip to content

[RFC] Video Ad API#1576

Draft
littlespex wants to merge 2 commits into
videojs:mainfrom
littlespex:rfc/ads-api
Draft

[RFC] Video Ad API#1576
littlespex wants to merge 2 commits into
videojs:mainfrom
littlespex:rfc/ads-api

Conversation

@littlespex

Copy link
Copy Markdown
Collaborator

Summary

Proposes a unified, vendor-agnostic Video Ad API for Video.js 10 modeled after the HTMLMediaElement TextTrack hierarchy (AdTrackList -> AdTrack -> AdCue -> Ad). Targets the seven IAB CTV ad formats (Linear, Pause, Menu, Squeezeback, Overlay, In-Scene, Screensaver) so integrators target one surface instead of bolting on per-vendor SDK glue.

Motivation

V10 currently has no ad abstraction. Every integrator wires Google IMA, FreeWheel, YoSpace, etc. directly into the player, which fragments UX, blocks skin/UI integration, and leaves non-linear CTV formats unsupported. With the IAB 2026 CTV Ad Format Guidelines landing and CTV monetization growing, the gap is becoming a structural adoption blocker.

Documents

  • rfc/ads-api/index.md -- problem statement, options considered, recommendation
  • rfc/ads-api/api.md -- full TypeScript surface (enums, interfaces, events, lifecycle)
  • rfc/ads-api/examples.md -- per-format adapter and player usage patterns
  • rfc/ads-api/decisions.md -- format inventory, rationale, open questions

Recommendation

Option 1: TextTrack-analogous hierarchy. Mirrors existing HTMLMediaElement semantics so integrators reuse a familiar mental model, generalizes across all seven IAB formats with one track per format, and supports concurrent experiences (overlay + companion + squeezeback) without per-format coordination logic.

Alternatives considered and rejected: per-format manager classes (surface fragmentation), IMA-shaped surface (vendor coupling), flat event bus (consumer state-machine cost), primitives-only (abdication).

Status

Draft -- seeking review on the overall shape, the choice between Option 1 and the alternatives, and the open questions in decisions.md (notably numeric-indexing ergonomics and menu-ad coupling to playback).

Review focus

  • Does the TextTrack-analogous hierarchy hold up across all seven IAB formats?
  • Are the format boundaries in decisions.md correct, or do any formats collapse/split?
  • Open questions in decisions.md -- especially around indexable collection ergonomics and ad surfaces that exist outside playback.

@netlify

netlify Bot commented May 20, 2026

Copy link
Copy Markdown

‼️ Deploy request for vjs10-site rejected.

Name Link
🔨 Latest commit 1f6c68d

@vercel

vercel Bot commented May 20, 2026

Copy link
Copy Markdown

@littlespex is attempting to deploy a commit to the Mux Team on Vercel.

A member of the Team first needs to authorize it.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Introduces an RFC proposing a unified, vendor-agnostic Video Ad API for Video.js 10, modeled after the HTMLMediaElement TextTrack hierarchy, to support the seven IAB CTV ad formats with a single integrator-facing surface.

Changes:

  • Adds an RFC overview with problem statement, options, and a recommendation for a TextTrack-analogous hierarchy.
  • Defines the proposed TypeScript API surface (enums, interfaces, events, and lifecycle semantics).
  • Provides format-specific adapter/player usage examples and captures design decisions + open questions.

Reviewed changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated 8 comments.

File Description
rfc/ads-api/index.md RFC overview: problem statement, alternatives, and recommendation.
rfc/ads-api/api.md Proposed API surface (types, events, lifecycle contracts).
rfc/ads-api/examples.md Usage patterns per IAB format (adapter-side + player-side examples).
rfc/ads-api/decisions.md Rationale, format inventory, and open design questions/tradeoffs.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread rfc/ads-api/examples.md
Comment on lines +80 to +92
linearTrack.addEventListener('change', () => {
const cue = linearTrack.activeCues[0];
cue?.addEventListener('adchange', () => {
const currentCue = linearTrack.activeCues[0];
if (currentCue == null) {
return;
}
adUI.updateForAd(
currentCue.activeAd,
currentCue.activeAd?.position ?? 0,
currentCue.ads.length,
);
});
Comment thread rfc/ads-api/examples.md Outdated
Comment on lines +246 to +250
const menuManager = adapter.createStandaloneAdTrackList();
const menuTrack = menuManager.getTrackByFormat(AdFormat.MENU);
if (menuTrack == null) {
throw new Error('MENU ad track not available');
}
Comment thread rfc/ads-api/examples.md
Comment on lines +252 to +272
menuTrack.addEventListener('change', () => {
headlineSlot.clear();
tileGrid.clear();

const cue = menuTrack.activeCues[0];
if (cue == null) {
return;
}

for (let i = 0; i < cue.ads.length; i += 1) {
const ad = cue.ads[i];
const slotKey = ad.metadata['slot'];
const slot = slotKey === 'headline' ? headlineSlot : tileGrid;
slot.render(ad);
ad.addEventListener('click', () => {
routeToAdvertiser(ad);
});
ad.addEventListener('impression', () => {
measurement.record(ad);
});
}
Comment thread rfc/ads-api/examples.md
Comment on lines +165 to +186
pauseTrack.addEventListener('change', () => {
const cue = pauseTrack.activeCues[0];
if (cue == null) {
pauseAdLayer.clear();
return;
}

const activeAd = cue.activeAd;
if (activeAd == null) {
pauseAdLayer.clear();
return;
}

pauseAdLayer.render(activeAd);

activeAd.addEventListener('refresh', () => {
const refreshed = cue.activeAd;
if (refreshed == null) {
return;
}
pauseAdLayer.render(refreshed); // swap the creative
});
Comment thread rfc/ads-api/examples.md
Comment on lines +344 to +370
sqTrack.addEventListener('change', () => {
const cue = sqTrack.activeCues[0];

if (cue == null) {
// Animate back to fullscreen and clear ad regions.
contentLayer.animateTo({ top: 0, right: 0, bottom: 0, left: 0 }, 1500);
squeezebackLayer.clear();
return;
}

const meta = cue.metadata as SqueezebackMetadata;

// 'activating' fires when the resize should start.
cue.addEventListener('activating', () => {
contentLayer.animateTo(meta.contentViewport, meta.transitionDuration);
});

// Render each ad in its own adViewport as soon as the cue is ACTIVE.
cue.addEventListener('activated', () => {
for (let i = 0; i < cue.ads.length; i += 1) {
const ad = cue.ads[i];
const adViewport = ad.adViewport;
if (adViewport != null) {
squeezebackLayer.render(ad, adViewport);
}
}
});
Comment thread rfc/ads-api/examples.md
Comment on lines +421 to +441
overlayTrack.addEventListener('change', () => {
overlayLayer.clear();

// Multiple overlay cues can theoretically be active at once.
for (let c = 0; c < overlayTrack.activeCues.length; c += 1) {
const cue = overlayTrack.activeCues[c];
for (let a = 0; a < cue.ads.length; a += 1) {
const ad = cue.ads[a];
const adViewport = ad.adViewport;
if (adViewport != null) {
overlayLayer.render(ad, adViewport);
}
ad.addEventListener('click', () => {
handleClickThrough(ad);
});
}

cue.addEventListener('deactivated', () => {
overlayLayer.remove(cue.id);
});
}
Comment thread rfc/ads-api/examples.md Outdated
Comment on lines +518 to +524
// The API can't control rendering, but it can fire impression tracking
// once the brand has been on-screen for >= 3 seconds.
const minExposureMs = 3000;
const fireImpression = window.setTimeout(() => {
if (ad.state === AdState.ACTIVE) {
measurement.recordImpression(ad);
ad.dispatchEvent(new CustomEvent('impression'));
Comment thread rfc/ads-api/api.md Outdated
Comment on lines +170 to +172
CLIENT_SIDE = 'client-side',
SSAI = 'ssai',
SGAI = 'sgai',
Convert enums to lowercase string-literal unions matching v10's existing
type style. Generalize AdTrackList.servingMode (was linearServingMode)
since the property applies to non-linear formats too. Remove adapter-side
code blocks from examples.md so the doc reads as player-side guidance.
Address Copilot review: share the standalone AdTrackList in the menu
example, drop the player-side dispatchEvent('impression') in the in-scene
example.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants