Appearance
VAST Tag Generation
VAST (Video Ad Serving Template) tags are generated in AF/src/utils/TagGenerator.ts via the makeVASTtag() function. We generate VAST 4.3 compliant XML.
VAST Structure
xml
<VAST version="4.3">
<Ad id="VAST-{fullID}">
<InLine>
<AdSystem version="3.1">Cavai Cloud</AdSystem>
<AdTitle>{creative name}</AdTitle>
<AdServingId>CAVAI-VAST-{fullID}-{timestamp}</AdServingId>
<Impression> <!-- Cavai stub.js impression pixel -->
<Impression> <!-- 3rd-party impression URLs (if configured) -->
<Error> <!-- Error reporting with [ERRORCODE] macro -->
<Creatives>
<Creative>
<Linear>
<Duration>00:{MM:SS}</Duration>
<MediaFiles>
<Mezzanine> <!-- Original MP4 for ad stitching (never served to users) -->
<MediaFile> <!-- HLS streams (one per H.264 rendition) -->
<MediaFile> <!-- MP4 fallbacks (low/mid/high bitrate) -->
</MediaFiles>
<TrackingEvents>
<Tracking event="start"> <!-- Init + 0% play -->
<Tracking event="firstQuartile"> <!-- 25% -->
<Tracking event="midpoint"> <!-- 50% -->
<Tracking event="thirdQuartile"> <!-- 75% -->
<Tracking event="complete"> <!-- 100% -->
<!-- 3rd-party tracking URLs per event -->
</TrackingEvents>
<VideoClicks>
<ClickThrough> <!-- Landing page URL -->
<ClickTracking> <!-- Click tracking URLs -->
<CustomClick> <!-- Custom click URLs -->
</VideoClicks>
</Linear>
<CompanionAds>
<!-- Responsive companion + format-specific companions -->
</CompanionAds>
</Creative>
</Creatives>
</InLine>
</Ad>
</VAST>MediaFile Strategy
Streaming (HLS)
As of PR #1899 (July 2026), we no longer reference the master playlist.m3u8 in VAST tags. Instead, we list individual H.264 rendition playlists:
xml
<MediaFile delivery="streaming" type="application/x-mpegURL"
width="1280" height="720" bitrate="1500"
id="stream_avc1_1500k_{streamId}">
<![CDATA[https://delivery-6.cavai.com/{streamId}/avc1_1500k/video.m3u8]]>
</MediaFile>Why: Bunny CDN's master playlist.m3u8 includes both H.264 and AV1 variants. Android devices with software-only AV1 decoders would select AV1 (as the "better" codec) but couldn't hardware-decode it, causing CPU spikes that made the ad container kill the creative. This resulted in low view-through rates (VTR) on Android.
How it works: The fallbackMp4s array (already filtered to exclude AV1 via init URI detection) is iterated. For each rendition, the per-rendition video.m3u8 playlist is used instead of the master. These per-rendition playlists contain only H.264 segments, so AV1 is never offered to the player.
Trade-off: This is a pragmatic solution. The ideal approach would be for Bunny to generate a separate master playlist with only H.264 variants, but Bunny generates playlists automatically and doesn't easily support custom playlist files. Players still get adaptive bitrate selection across the H.264 renditions.
MP4 Fallbacks
Three progressive download options for players that don't support HLS streaming:
xml
<MediaFile delivery="progressive" type="video/mp4"
width="640" height="360" bitrate="300"
id="low_{streamId}">
<![CDATA[https://delivery-6.cavai.com/{streamId}/play_avc1_300k.mp4]]>
</MediaFile>Selected as lowest, middle, and highest bitrate from the H.264 rendition list. VAST spec recommends at least 3 bitrate options per MIME type.
Mezzanine
The original uploaded MP4, declared for ad stitching purposes. Never served directly to end users.
Duration Bug Fix (PR #1899)
The secondsToMMSS() helper had a bug where the fallback for missing duration returned the number 30 instead of the string '00:30'. Since the duration is prefixed with 00: in the VAST tag, returning 30 produced 00:30 (parsed as HH:MM = 30 minutes), while '00:30' correctly produces 00:00:30 (HH:MM:SS = 30 seconds). The incorrect duration meant VAST players never fired the complete event, which counted the view as incomplete -- contributing to low VTR.
Error Reporting
Added in PR #1899. The <Error> element uses VAST's [ERRORCODE] macro:
xml
<Error>
<![CDATA[{edgeEncodeUrl}?campaignId=...&messageType="error"&errorCode=[ERRORCODE]&sessionId={bust}]]>
</Error>The VAST player replaces [ERRORCODE] with a numeric code (e.g., 301 = timeout, 401 = no supported media). This data flows into our analytics pipeline for debugging delivery issues.
Tracking Events
All tracking events go through the edge encode URL with specific parameters:
messageType="init"-- sent onstartevent (creative initialization)messageType="flow"withplayPercentage-- sent at 0%, 25%, 50%, 75%, 100%messageType="count"-- viewability countingmessageType="event"withclickedLink=true-- click trackingmessageType="error"witherrorCode-- VAST player errors
Third-party tracking URLs are injected per event type if configured in delivery settings (delivery.vastUrls).
DSP Macros
The tag includes DSP-specific cache-busting and domain macros from the ADSERVERS constant. The DSP is selected by delivery.clickMacroId. If the selected DSP has been deleted, it falls back to ID 0 (placeholder).