Learning Track

SSAI & CTV

Understand server-side ad stitching, HLS/DASH manifest structure, VMAP podding, and how to debug SSAI delivery failures.

Intermediate track

What this page helps you understand in stitched video delivery

This page is written for an intermediate practitioner. It assumes you know the basic CTV and VAST vocabulary and want a more operational view: how SSAI stitches HLS/DASH, how podding rules work, and how to systematically debug manifest and beacon failures.

What this page covers

  • How SSAI works vs CSAI — server-side vs client-side ad insertion.
  • HLS manifest structure with SCTE-35 and EXT-X-DATERANGE ad markers.
  • VMAP podding rules, break types, and ad sequencing.

What is inside

  • Annotated HLS manifest with ad break markers.
  • VMAP XML example with pre-roll, mid-roll, and post-roll definitions.
  • Error code reference table and failure pattern troubleshooting guide.

Best way to use it

  • Capture a live stitched manifest from your SSAI vendor and annotate it line by line.
  • Trace the pod sequence: VMAP → VAST request → media stitching → beacon fire.
  • Keep a vendor-specific error cheat sheet as you work through escalations.

SSAI vs CSAI — why server-side matters for CTV

Client-Side Ad Insertion (CSAI) works on web browsers: the player calls an ad server, fetches a VAST tag, downloads the ad creative, and plays it. This works because desktop browsers handle VAST and can accept third-party JS. CTV devices — Smart TVs, Roku, FireTV, Apple TV — do not support third-party JavaScript or VAST redirects reliably. SSAI solves this by stitching ad segments directly into the content stream on the server, delivering a single seamless HLS or DASH feed that alternates between content and ad segments.

SSAI delivery flow — 7 steps

  1. CTV app requests a stream — sends session initialization request to the SSAI vendor (Google DAI, AWS EMC, Yospace, Brightcove, etc.) with content ID and ad parameters.
  2. SSAI resolves the ad schedule — calls the publisher’s ad server (GAM) for a VMAP document that defines break positions, pod sizes, and VAST tag URLs.
  3. VMAP is parsed — SSAI reads break offsets (start, end, timecodes), resolves each VAST tag in the pod, fetches media files for all ads in the break.
  4. Media is transcoded and cached — ad video segments are transcoded to match the content stream’s codec, bitrate, and segment duration. Cached for reuse across sessions.
  5. Stream is stitched — SSAI replaces the content segments at break points with ad segments, inserting HLS discontinuity markers and EXT-X-DATERANGE annotations.
  6. Single manifest served to player — the CTV player sees one seamless stream. It cannot distinguish content from ad segments unless it inspects the manifest markers.
  7. Beacons fire server-side — SSAI fires VAST tracking URLs (impression, quartile, complete) on behalf of the player, since the CTV device may not have direct network access to tracking servers.

Annotated HLS manifest with SSAI ad markers

A stitched HLS manifest shows the transition from content to ad segments. The key markers that signal an ad break are EXT-X-DISCONTINUITY and EXT-X-DATERANGE.

HLS Stitched HLS manifest — content → ad break → content
#EXTM3U
#EXT-X-VERSION:3
#EXT-X-TARGETDURATION:6           # Max segment duration — all segments must be ≤ 6s
#EXT-X-MEDIA-SEQUENCE:0

# === Content segments (pre-break) ===
#EXTINF:6.000,
https://cdn.example.com/content/seg-000.ts

#EXTINF:6.000,
https://cdn.example.com/content/seg-001.ts

#EXTINF:6.000,
https://cdn.example.com/content/seg-002.ts

# === Ad break starts ===

#EXT-X-DISCONTINUITY                              # Signals codec/timeline break — player resets sync

#EXT-X-DATERANGE:\
  ID="ad-break-1",\                               # Unique break identifier
  CLASS="com.apple.hls.interstitial",\
  START-DATE="2024-04-18T12:08:00Z",\             # Absolute wall-clock time of break start
  PLANNED-DURATION=30.0,\                         # Expected ad pod duration in seconds
  SCTE35-OUT=0xFC301100000000FF00FFF00505000000007FEFFE # SCTE-35 splice insert signal

# Ad 1 — 15-second spot (3 segments × 5s each)
#EXTINF:5.000,
https://ssai.example.com/ad/creative-55/seg-000.ts
#EXTINF:5.000,
https://ssai.example.com/ad/creative-55/seg-001.ts
#EXTINF:5.000,
https://ssai.example.com/ad/creative-55/seg-002.ts

# Ad 2 — 15-second spot (3 segments × 5s each)
#EXTINF:5.000,
https://ssai.example.com/ad/creative-88/seg-000.ts
#EXTINF:5.000,
https://ssai.example.com/ad/creative-88/seg-001.ts
#EXTINF:5.000,
https://ssai.example.com/ad/creative-88/seg-002.ts

# === Content resumes ===

#EXT-X-DISCONTINUITY                              # Second discontinuity marks return to content

#EXT-X-DATERANGE:\
  ID="ad-break-1",\
  END-DATE="2024-04-18T12:08:30Z"                 # Closes the break — duration confirms 30s delivered

#EXTINF:6.000,
https://cdn.example.com/content/seg-003.ts

#EXTINF:6.000,
https://cdn.example.com/content/seg-004.ts
  • EXT-X-DISCONTINUITY Tells the player that the following segments have a different timeline, codec, or bitrate than what came before. Essential at every content-to-ad and ad-to-content boundary. A missing discontinuity causes audio/video sync issues or buffering.
  • EXT-X-TARGETDURATION All segments — both content and ad — must be at or below this duration. If an ad creative was transcoded into 8s segments but TARGETDURATION is 6, the player will reject the stream.
  • PLANNED-DURATION The expected total duration of the ad pod. If actual ad segments delivered are shorter (e.g., an ad creative is missing), the content resumes early — causing “content bumper” issues.
  • SCTE35-OUT The binary SCTE-35 splice signal that marks the start of an ad opportunity. Used by SSAI to identify where to insert the ad. If malformed or missing, SSAI may not detect the break and skips ad insertion entirely.
  • Segment URLs Ad segment URLs point to the SSAI vendor’s CDN, not the content CDN. If these 404, the player buffers at the ad break. If the ad CDN is slow, it causes rebuffering without an obvious error code.
Common SSAI manifest bug: ad segment duration doesn’t match content segment duration. If content uses 6-second segments and the ad was transcoded into 2-second segments, the player timeline gets confused. Always confirm that SSAI transcodes ads to match the content stream profile exactly.

VMAP 1.0 — Video Multiple Ad Playlist

VMAP defines the ad break schedule for a video. SSAI calls the ad server for a VMAP document first, then uses it to determine when and how many ads to request. Without VMAP, the SSAI vendor cannot know where breaks occur in live or VOD content.

XML VMAP 1.0 — pre-roll, mid-roll, post-roll with tracking
<?xml version="1.0" encoding="UTF-8"?>
<vmap:VMAP xmlns:vmap="http://www.iab.net/vmap-1.0" version="1.0">

  <!-- Pre-roll: fires at content start (timeOffset="start") -->
  <vmap:AdBreak
    timeOffset="start"          <!-- Other options: "end", "00:08:00" (HH:MM:SS), or percentage "25%" -->
    breakType="linear"           <!-- "linear" = full-screen video; "nonLinear" = overlay; "display" = banner -->
    breakId="preroll-1">

    <vmap:AdSource
      id="pre-source-1"
      allowMultipleAds="false"  <!-- false = only first ad in response serves (no pod) -->
      followRedirects="true">  <!-- true = SSAI follows VAST Wrapper chains -->
      <vmap:VASTAdData>
        <!-- Inline VAST — embed the VAST XML directly in VMAP -->
        <VAST version="4.1"><!-- ...full VAST here... --></VAST>
      </vmap:VASTAdData>
    </vmap:AdSource>

    <vmap:TrackingEvents>
      <vmap:Tracking event="breakStart">
        <![CDATA[https://track.example.com/vmap/break/pre/start?cb=__random__]]>
      </vmap:Tracking>
      <vmap:Tracking event="breakEnd">
        <![CDATA[https://track.example.com/vmap/break/pre/end?cb=__random__]]>
      </vmap:Tracking>
    </vmap:TrackingEvents>
  </vmap:AdBreak>

  <!-- Mid-roll: fires at 8 minutes into content -->
  <vmap:AdBreak
    timeOffset="00:08:00"
    breakType="linear"
    breakId="midroll-1">
    <vmap:AdSource
      id="mid-source-1"
      allowMultipleAds="true"   <!-- true = pod: multiple ads served sequentially in one break -->
      followRedirects="true">
      <vmap:AdTagURI templateType="vast4">
        <!-- Dynamic VAST tag — SSAI calls this URL to get ad(s) for this break -->
        <![CDATA[https://adserver.example.com/vast?pos=mid&break=1&dur=90&cb=__random__]]>
      </vmap:AdTagURI>
    </vmap:AdSource>
  </vmap:AdBreak>

  <!-- Second mid-roll: fires at 16 minutes -->
  <vmap:AdBreak
    timeOffset="00:16:00"
    breakType="linear"
    breakId="midroll-2">
    <vmap:AdSource id="mid-source-2" allowMultipleAds="true" followRedirects="true">
      <vmap:AdTagURI templateType="vast4">
        <![CDATA[https://adserver.example.com/vast?pos=mid&break=2&dur=60&cb=__random__]]>
      </vmap:AdTagURI>
    </vmap:AdSource>
  </vmap:AdBreak>

  <!-- Post-roll: fires when content ends -->
  <vmap:AdBreak
    timeOffset="end"
    breakType="linear"
    breakId="postroll-1">
    <vmap:AdSource id="post-source-1" allowMultipleAds="false" followRedirects="true">
      <vmap:AdTagURI templateType="vast4">
        <![CDATA[https://adserver.example.com/vast?pos=post&cb=__random__]]>
      </vmap:AdTagURI>
    </vmap:AdSource>
  </vmap:AdBreak>

</vmap:VMAP>
  • timeOffset Defines when the break fires. start = pre-roll, end = post-roll, 00:08:00 = absolute timecode, 25% = percent of content duration. For live streams, timecodes should match expected SCTE-35 signal positions.
  • allowMultipleAds Controls pod behavior. true = the SSAI fills the break with multiple ads sequentially (pod). false = only one ad serves regardless of how many the VAST response contains. Setting this incorrectly causes the break to be either too short or overly long.
  • followRedirects Controls VAST Wrapper chain resolution. true = SSAI follows wrapper redirects to reach the InLine creative. false = SSAI only uses first-level VAST, causing empty breaks when ads use wrapper chains (most do).
  • AdTagURI vs VASTAdData Use AdTagURI for dynamic ad serving (ad server decides which creative at request time). Use VASTAdData to embed static VAST — useful for fallback or house ads that never change.
  • __random__ Cache-busting macro. SSAI should replace this with a unique value on each request, preventing cached VAST responses from serving the same ad twice in the same session.

Common SSAI error codes and causes

ErrorWhere it appearsMeaningHow to fix
VAST 400 VAST error URL, SSAI logs XML parse error — malformed VAST tag Validate the VAST tag with the IAB validator. Check for unescaped &amp; in tracking URLs.
VAST 401 VAST error URL VAST schema validation error Confirm VAST version matches schema. VAST 4.1 tags must not use deprecated VAST 2.0 elements.
VAST 301 Wrapper chain Wrapper redirect returned no ads Confirm the wrapped VAST URL is accessible from the SSAI server IP. Check for geo or IP allow-list blocking.
VAST 303 Wrapper chain Too many wrapper redirects (> 5 is common limit) Flatten the VAST wrapper chain. Each hop adds latency — more than 3 hops frequently causes timeouts on CTV devices.
VAST 101 SSAI logs / debug tools VAST schema error — missing required element Verify <Impression>, <MediaFile>, and <Duration> are all present in the InLine element.
503 from ad server SSAI network logs Ad server overloaded or no fill Check ad server latency. If timeout, increase SSAI ad request timeout. Add backfill VAST fallback in VMAP.
Black screen at break Player / viewer report Ad segment 404, codec mismatch, or missing discontinuity marker Inspect manifest in a text editor — confirm segment URLs resolve. Verify TARGETDURATION matches. Check for missing EXT-X-DISCONTINUITY.
Beacons not firing DSP reporting, VAST tracking SSAI server-side tracking failed or was not configured Confirm SSAI vendor has server-side tracking enabled for the creative. Check the tracking URL format — CDATA wrapping is required. Test with curl from the SSAI server IP.

Common SSAI failure patterns

Ad break is shorter than expected (pod truncation)

The ad break ends earlier than the VMAP planned-duration. One or more ads in the pod failed to transcode, returned 404, or the VAST response was empty. SSAI fills what it can and returns to content.

▸ Pull SSAI session logs for the break ID. Check how many VAST responses returned creative vs. empty. Confirm each ad creative URL is accessible from the SSAI server’s IP — geo or IP blocks are a common cause.

Audio/video out of sync after ad break

After returning from an ad break, the audio and video tracks in the content stream are offset. Caused by a discontinuity marker being missing or misplaced at the ad-to-content boundary.

▸ Download the manifest and inspect the second EXT-X-DISCONTINUITY marker. Confirm it is on its own line immediately before the first content segment after the break. If missing, escalate to SSAI vendor with the session ID.

Same ad serving every break

Ad frequency is not capped — same creative appears in multiple ad pods in the same session. SSAI is not passing session ID or user ID to the ad server, so frequency capping cannot function.

▸ Confirm the SSAI session initialization passes a unique user or device ID to the ad server via the VAST request parameters. In GAM, configure frequency capping per session. Use cache-busting macros in AdTagURI.

VAST impressions fire but DSP reports no delivery

SSAI fires tracking beacons server-side, but the DSP’s impression tracker is not recording them. The DSP’s impression pixel expects a client-side fire (from the device), not a server-side fire from the SSAI server IP.

▸ Confirm the DSP supports server-side impression tracking. Some DSPs add IP allow-lists for SSAI servers. Share your SSAI vendor’s server IP range with the DSP and ask them to whitelist it.

Live stream ad break timing is off

In live streams, SCTE-35 signals trigger ad breaks. If the live encoder sends a SCTE-35 signal early or late, the ad break starts at the wrong point in the content — cutting off dialogue or starting in the middle of an action sequence.

▸ Capture the SCTE-35 signal from the live encoder using a tool like SCTE-35 Parser. Compare the signal timestamp against the actual program event time. Work with the encoder team to calibrate signal lead time.

CTV player buffers only during ad breaks

Content plays smoothly but the player buffers every time an ad serves. Ad segments are coming from a different CDN than content, with higher latency or lower cache hit rate.

▸ Compare the content segment CDN with the ad segment CDN in the manifest. Confirm the ad CDN has POPs in the same regions as your viewer base. Ask the SSAI vendor to pre-warm the ad CDN cache for high-frequency creatives.

Practice drills and outputs

Drill 1 — Annotate a stitched manifest

  • Use a video player with a test SSAI stream (Google DAI has a public sample stream).
  • Download the .m3u8 manifest using cURL or a browser extension.
  • Annotate each section: content segments, EXT-X-DISCONTINUITY, EXT-X-DATERANGE, ad segments, return to content.
  • Output: manifest-annotated.txt with line-by-line comments on what each marker does.

Drill 2 — Build a VMAP from scratch

  • Write a VMAP with one pre-roll, two mid-rolls (at 5:00 and 12:00), and one post-roll.
  • Mid-rolls should use allowMultipleAds="true" with a max pod duration of 90 seconds.
  • Include breakStart/breakEnd tracking events for each break.
  • Validate against the IAB VMAP 1.0 schema. Fix any validation errors.
  • Output: vmap-pod.xml

Drill 3 — SSAI error audit

  • Pull SSAI error logs from your vendor for a 7-day window.
  • Group errors by type: VAST parse errors, 404 creatives, timeout, codec mismatch, beacon failures.
  • Estimate revenue impact per error category (breaks affected × average CPM).
  • Output: ssai-error-audit.md with ranked error list and recommended fixes.

References

  • IAB VMAP 1.0 specification — iab.com guidelines
  • IAB VAST 4.1 — error codes appendix
  • Google DAI documentation — session initialization and manifest debugging
  • SCTE-35 specification — splice signal format for live streams
  • HLS spec (RFC 8216) — EXT-X-DATERANGE and EXT-X-DISCONTINUITY

AdTech Toolkit

Enter any two values
to calculate the third

More tools coming soon