Skip to content

VAST

VAST 4.2 client-side video ad support. Pure-function parser, beacon-based tracking, quartile events.

Need Google IMA SDK?

If your ad server requires Google IMA SDK, use the IMA plugin instead. IMA SDK handles ad fetching, playback, and tracking internally.

Usage

ts
import { createPlayer } from "@videts/vide";
import { vast } from "@videts/vide/vast";

const player = createPlayer(document.querySelector("video")!);
player.use(vast({ tagUrl: "https://example.com/vast.xml" }));

Options

OptionTypeDefaultDescription
tagUrlstringVAST tag URL (required)
timeoutnumber5000Request timeout in ms
allowSkipbooleantrueHonor skip offsets from the VAST response
adPlugins(ad: VastAd) => AdPlugin[]Per-ad plugin factory (e.g. OMID, SIMID, UI ad components)

When Ads Trigger

The plugin determines when to load the ad based on the player's current state at the time player.use(vast(...)) is called:

  • ready, playing, paused, or ended — the ad loads immediately. The plugin saves the current content source and time position, plays the ad, then restores the content. When called in ended state, the player returns to ended after the ad (content is not restored).
  • idle, loading, or buffering — the plugin waits for the player to reach the ready state before loading the ad. This is the typical pre-roll pattern.

Manual scheduling

Since player.use() can be called at any time, you can implement ad scheduling without VMAP:

ts
// Mid-roll at 5 minutes
player.on("timeupdate", function onMidroll({ currentTime }) {
  if (currentTime >= 300) {
    player.off("timeupdate", onMidroll);
    player.use(vast({ tagUrl: "https://example.com/midroll.xml" }));
  }
});

See the Ads Setup guide for more patterns including post-roll.

Ad Pods and Waterfall

When a VAST response contains multiple <Ad> elements, the plugin automatically classifies and plays them:

PatternConditionBehavior
PodAds have sequence attributesPlayed in sequence order. Failures/skips advance to the next ad.
WaterfallMultiple ads without sequenceTried in order. First successful ad wins.
SingleOne adStandard single-ad playback.

Pod Behavior

  • Ads are sorted by sequence attribute and played sequentially.
  • If a pod ad fails, a stand-alone ad (without sequence) from the same response is substituted before moving to the next pod ad (per VAST 3.3.1).
  • Individual ad failures or skips do not stop the pod — playback advances to the next ad.

Waterfall Behavior

  • Ads are tried in document order.
  • If an ad fails during loading (before canplay), the next ad is tried.
  • If an ad fails during playback (after canplay), the waterfall stops.
  • If all ads fail, an ad:error event is emitted.

Utility Functions

The pod/waterfall logic is available as standalone functions for advanced use:

ts
import { classifyAds, playPod, playWaterfall, playSingleAd } from "@videts/vide/vast";

const classified = classifyAds(response.ads);
// classified.type: "single" | "pod" | "waterfall"
// classified.ads: PlayableAd[]
// classified.standalonePool: PlayableAd[] (for pod substitution)

Events

Per-Ad Events

EventPayloadDescription
ad:loaded{ adId }VAST response parsed, ad ready
ad:start{ adId, clickThrough?, skipOffset?, duration?, adTitle? }Ad playback started
ad:end{ adId }Ad playback ended
ad:skip{ adId }Ad skipped by user
ad:click{ clickThrough, clickTracking }Ad clicked
ad:error{ error, source, vastErrorCode? }Ad loading or playback error. vastErrorCode is a VAST 4.2 error code when available.
ad:impression{ adId }Impression pixel fired
ad:quartile{ adId, quartile }Quartile reached (start, firstQuartile, midpoint, thirdQuartile, complete)
ad:mute{ adId }Ad muted
ad:unmute{ adId }Ad unmuted
ad:volumeChange{ adId, volume }Ad volume changed
ad:fullscreen{ adId, fullscreen }Fullscreen state changed during ad
ad:companions{ adId, required, companions }Companion ad data available (emitted with ad:start)
ad:nonlinears{ adId, nonLinears, trackingEvents }NonLinear overlay ad data available (emitted with ad:start)

Pod Events

EventPayloadDescription
ad:pod:start{ ads, total }Pod playback started
ad:pod:end{ completed, skipped, failed }Pod playback ended with stats
ad:pod:adstart{ ad, index, total }Individual ad within pod started
ad:pod:adend{ ad, index, total }Individual ad within pod ended

Error Codes

The VAST plugin includes all VAST 4.2 error codes as named constants. When an ad error occurs, the ad:error event payload includes a vastErrorCode field identifying the specific VAST error.

Usage

ts
import { VAST_MEDIA_UNSUPPORTED, VAST_NO_ADS } from "@videts/vide/vast";

player.on("ad:error", ({ error, source, vastErrorCode }) => {
  switch (vastErrorCode) {
    case VAST_NO_ADS:
      console.log("No ads available");
      break;
    case VAST_MEDIA_UNSUPPORTED:
      console.warn("No playable media file in the VAST response");
      break;
  }
});

Error Tracking URLs

When an error occurs, the plugin automatically fires the <Error> tracking URLs from the VAST response with the [ERRORCODE] macro replaced:

xml
<Error><![CDATA[https://example.com/error?code=[ERRORCODE]]]></Error>

Becomes https://example.com/error?code=403 when the error is "media file not supported".

For manual error tracking:

ts
import { trackError } from "@videts/vide/vast";

trackError(["https://example.com/error?code=[ERRORCODE]"], 403);

Error Code Reference

CodeConstantDescription
100VAST_XML_PARSE_ERRORXML parsing error
101VAST_SCHEMA_ERRORVAST schema validation error
102VAST_VERSION_UNSUPPORTEDVAST version not supported
200VAST_TRAFFICKING_ERRORTrafficking error
201VAST_LINEARITY_ERRORExpecting different linearity
202VAST_DURATION_ERRORExpecting different duration
203VAST_SIZE_ERRORExpecting different size
204VAST_CATEGORY_REQUIREDAd category required but not provided
205VAST_CATEGORY_BLOCKEDInLine category violates Wrapper BlockedAdCategories
206VAST_BREAK_SHORTENEDAd break shortened, ad not served
300VAST_WRAPPER_ERRORGeneral Wrapper error
301VAST_WRAPPER_TIMEOUTTimeout of VAST URI in Wrapper
302VAST_WRAPPER_LIMITWrapper limit reached
303VAST_NO_ADSNo VAST response after one or more Wrappers
304VAST_INLINE_TIMEOUTInLine ad failed to display in time
400VAST_LINEAR_ERRORGeneral Linear error
401VAST_MEDIA_NOT_FOUNDUnable to find Linear/MediaFile from URI
402VAST_MEDIA_TIMEOUTTimeout of MediaFile URI
403VAST_MEDIA_UNSUPPORTEDCould not find supported MediaFile
405VAST_MEDIA_DISPLAY_ERRORProblem displaying MediaFile
406VAST_MEZZANINE_REQUIREDMezzanine required but not provided
407VAST_MEZZANINE_DOWNLOADINGMezzanine download in progress
408VAST_CONDITIONAL_REJECTEDConditional ad rejected (deprecated)
409VAST_INTERACTIVE_NOT_EXECUTEDInteractiveCreativeFile not executed
410VAST_VERIFICATION_NOT_EXECUTEDVerification unit not executed
411VAST_MEZZANINE_INVALIDMezzanine didn't meet spec
500VAST_NONLINEAR_ERRORGeneral NonLinearAds error
501VAST_NONLINEAR_SIZE_ERRORNonLinear dimensions don't fit
502VAST_NONLINEAR_FETCH_ERRORUnable to fetch NonLinear resource
503VAST_NONLINEAR_UNSUPPORTEDNonLinear resource type not supported
600VAST_COMPANION_ERRORGeneral CompanionAds error
601VAST_COMPANION_SIZE_ERRORCompanion dimensions don't fit
602VAST_COMPANION_REQUIRED_ERRORUnable to display required Companion
603VAST_COMPANION_FETCH_ERRORUnable to fetch Companion resource
604VAST_COMPANION_UNSUPPORTEDCompanion resource type not supported
900VAST_UNDEFINED_ERRORUndefined error
901VAST_VPAID_ERRORGeneral VPAID error
902VAST_INTERACTIVE_ERRORGeneral InteractiveCreativeFile error

AdPlugin Lifecycle

The adPlugins option creates per-ad plugins that are set up when an ad starts and cleaned up when it ends. This is how OMID, SIMID, and UI ad components integrate with VAST.

ts
import { vast } from "@videts/vide/vast";
import { omid } from "@videts/vide/omid";
import { simid } from "@videts/vide/simid";

player.use(vast({
  tagUrl: "https://example.com/vast.xml",
  adPlugins: (ad) => [
    omid({ partner: { name: "my-company", version: "1.0.0" } }),
    simid({ container: adContainer }),
  ],
}));

Companion Ads

Companion ads are secondary ads (banners, sidebars) that accompany the video ad. The plugin parses <CompanionAds> from the VAST response and emits data via the ad:companions event. Display is the integrator's responsibility.

Listening for Companions

ts
import { trackCompanionView } from "@videts/vide/vast";

player.on("ad:companions", ({ companions, required }) => {
  const banner = companions.find(c => c.width === 300 && c.height === 250);
  if (banner) {
    const resource = banner.resources.find(r => r.type === "static");
    if (resource) {
      document.getElementById("sidebar-ad")!.innerHTML =
        `<a href="${banner.clickThrough}"><img src="${resource.url}" alt="${banner.altText || ""}"></a>`;
      trackCompanionView(banner); // fires creativeView tracking beacons
    }
  }
});

required Attribute

ValueBehavior
"all"All companions must be displayed, or disregard the ad
"any"At least one companion must be displayed
"none"Companion display is optional (default)

Resource Types

Each companion may contain multiple resources. The integrator picks the most suitable:

  • { type: "static", url, creativeType } — Static image (e.g. image/png)
  • { type: "iframe", url } — URL to load in an iframe
  • { type: "html", content } — Inline HTML snippet

Tracking

Call trackCompanionView(companion) when a companion is actually displayed to fire the creativeView tracking beacons. The plugin does not fire these automatically since it does not control rendering.

NonLinear Ads

NonLinear ads are overlay creatives displayed on top of the video during content playback — the video is not interrupted. The plugin parses <NonLinearAds> from the VAST response and emits data via the ad:nonlinears event. Display and dismissal are the integrator's responsibility.

Listening for NonLinear Ads

ts
import { trackNonLinear } from "@videts/vide/vast";
import type { VastNonLinearAds } from "@videts/vide/vast";

let activeNonLinearAds: VastNonLinearAds | null = null;

player.on("ad:nonlinears", ({ nonLinears, trackingEvents }) => {
  activeNonLinearAds = { nonLinears, trackingEvents };
  const nl = nonLinears[0];
  const resource = nl.resources.find(r => r.type === "static");
  if (!resource) return;

  const overlay = document.createElement("div");
  overlay.innerHTML = `<a href="${nl.clickThrough}"><img src="${resource.url}" width="${nl.width}" height="${nl.height}"></a>`;
  playerContainer.appendChild(overlay);

  // Fire creativeView tracking
  trackNonLinear(activeNonLinearAds, "creativeView");

  // Show close button after minSuggestedDuration
  if (nl.minSuggestedDuration) {
    setTimeout(() => {
      const btn = document.createElement("button");
      btn.textContent = "×";
      btn.onclick = () => {
        overlay.remove();
        trackNonLinear(activeNonLinearAds!, "close");
      };
      overlay.appendChild(btn);
    }, nl.minSuggestedDuration * 1000);
  }
});

NonLinear vs Companion

NonLinearCompanion
Display locationInside the player (overlay)Outside the player (sidebar, etc.)
Content playbackContinues playingContinues playing
minSuggestedDurationYesNo
Tracking eventsMultiple (creativeView, acceptInvitation, close, etc.)creativeView only

Attributes

Each NonLinearAd object contains:

PropertyTypeDescription
widthnumberDisplay width in pixels
heightnumberDisplay height in pixels
minSuggestedDurationnumber | undefinedMinimum display time in seconds. Don't show a close button before this.
scalableboolean | undefinedWhether the creative can be resized
maintainAspectRatioboolean | undefinedWhether to preserve aspect ratio on resize
resourcesCompanionResource[]Same resource types as companions (static, iframe, html)
clickThroughstring | undefinedClick destination URL
clickTrackingstring[]Click tracking URLs

Tracking

NonLinear ads support multiple tracking events (unlike companions which only have creativeView). Call trackNonLinear() with the event name when the corresponding action occurs:

ts
import { trackNonLinear } from "@videts/vide/vast";

trackNonLinear(nonLinearAds, "creativeView");       // overlay is displayed
trackNonLinear(nonLinearAds, "acceptInvitation");    // user clicked/tapped the overlay
trackNonLinear(nonLinearAds, "close");               // user closed the overlay
trackNonLinear(nonLinearAds, "collapse");            // user minimized the overlay
trackNonLinear(nonLinearAds, "adExpand");            // user expanded the overlay
trackNonLinear(nonLinearAds, "adCollapse");          // user collapsed the expanded overlay

Wrapper Chain Resolution

When a VAST response contains a <Wrapper>, the plugin follows the <VASTAdTagURI> redirect chain (up to 5 levels by default) until an InLine ad is found. Data from all Wrapper layers is merged into the final InLine ad per VAST 4.2 spec:

  • Errors, Impressions — concatenated from all layers
  • Linear TrackingEvents — concatenated per event name
  • Linear ClickTracking — concatenated from all layers
  • ClickThrough — InLine only
  • CompanionAds resources — InLine takes precedence; falls back to closest Wrapper
  • CompanionClickTracking, Companion creativeView tracking — prepended to InLine companions
  • NonLinear TrackingEvents, NonLinearClickTracking — concatenated from all layers
  • AdVerifications — concatenated from all layers
  • Extensions — concatenated from all layers
  • ViewableImpression — URLs concatenated per type (Viewable, NotViewable, ViewUndetermined)

Notes

  • The parser is a pure function: takes XML string, returns VastResponse. No I/O inside.
  • Tracking uses navigator.sendBeacon() with Image pixel fallback.
  • Wrapper/chain resolution is supported (follows <VASTAdTagURI>).
  • The plugin saves/restores the content source after ad playback.
  • Ad Pods and Waterfall are automatically detected from the VAST response structure.
  • classifyAds, playSingleAd, playPod, playWaterfall, and selectMediaFile are all exported for advanced use cases.