[RFC] Video Ad API#1576
Draft
littlespex wants to merge 2 commits into
Draft
Conversation
|
| Name | Link |
|---|---|
| 🔨 Latest commit | 1f6c68d |
|
@littlespex is attempting to deploy a commit to the Mux Team on Vercel. A member of the Team first needs to authorize it. |
Contributor
There was a problem hiding this comment.
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 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 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 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 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 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 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 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 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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Proposes a unified, vendor-agnostic Video Ad API for Video.js 10 modeled after the
HTMLMediaElementTextTrack 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, recommendationrfc/ads-api/api.md-- full TypeScript surface (enums, interfaces, events, lifecycle)rfc/ads-api/examples.md-- per-format adapter and player usage patternsrfc/ads-api/decisions.md-- format inventory, rationale, open questionsRecommendation
Option 1: TextTrack-analogous hierarchy. Mirrors existing
HTMLMediaElementsemantics 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
decisions.mdcorrect, or do any formats collapse/split?decisions.md-- especially around indexable collection ergonomics and ad surfaces that exist outside playback.