1Overview
Server-side ad insertion (SSAI) for ChannelForge-generated linear FAST channels — no client SDK, no VPAID, no second player. The stream the viewer receives already contains the ads.
FAST World SSAI is the ad-insertion tier of a three-part ecosystem. Each part owns one job:
- ChannelForge (
forge.ventunotech.com) builds the linear channels and emits SCTE-35 ad-break markers in the HLS origin. - FAST World SSAI (this product,
ssai.ventunotech.com) detects those breaks, decides the ads, and stitches them into a per-viewer manifest. - FAST World TV (
fastworldtv.ventunotech.com) plays the stitched manifest in Shaka Player and fires ad-quartile beacons back.
SSAI is not a ChannelForge component — ChannelForge integrates it across a versioned adapter seam, so the two systems evolve independently.
Control plane vs. data plane
The server is split into two planes with very different jobs, so heavy work never touches playback:
GET /v1/hls/{session}/{channel}/index.m3u8. Minimal dependencies, predictable latency. It reads an already-decided, cached ad pod and rewrites segment references — no DB analytics, transcoding, or third-party calls.2The pipeline, end to end
One flow, six stages, from the channel origin to analytics.
- Detect the avail. SSAI reads the ChannelForge origin, finds each
CUE-OUT…CUE-INbreak from its SCTE markers, and reads the declared duration — producing a stable, per-channel opportunity. - Decide the pod. Eligible creatives are selected by priority and weight, looped to fill a longer break, with any remainder padded by slate so the break is always complete. The decision is deterministic: same seed and inventory → the same pod every time.
- Stitch & serve. The pod's real HLS segments replace the break inside a per-viewer manifest, wrapped in the correct discontinuities and held a little behind the live edge so they reliably reach the player.
- Play. The viewer receives a normal continuous HLS stream — program → ad → ad → program — with no separate ad player and no interruption.
- Track & measure. Impression and quartile beacons flow back, third-party VAST beacons fire server-side, and everything reconciles per break into the analytics.
3Core concepts
The objects you work with, top to bottom.
external_break_id so deliveries attach to the right break.4The admin console
Everything below is available in the browser at /admin — no API calls required.
Sign in with your console credentials. The console gives you:
- Channels, Campaigns, Creatives, Slates — full create, edit and archive, plus a source-upload form for creatives.
- VAST config — attach a third-party VAST tag to a channel and let the worker turn it into airable creatives.
- Analytics — fill, slate and completion rates in real time.
- Audit — a log of every configuration change.
- Debug / observability trace — reconstruct any opportunity or session end-to-end: eligible and rejected creatives with reasons, the chosen pod, and the full impression → quartile → complete timeline.
5Getting started
From an empty channel to ads on air, in order.
- Add the channel. Create a channel pointing at its ChannelForge origin. ChannelForge must be emitting SCTE-35 avails for that channel (it does this natively for managed FAST channels).
- Add creatives. Either upload an MP4 on the creative (it's stored and marked uploaded), or ingest a VAST tag on the channel (SSAI fetches it, follows wrapper redirects, and creates per-channel creatives). Either way the offline worker conditions the media to HLS.
- Group creatives into a campaign. Set priority and weight — these drive selection when a break opens.
- Add a slate. Prepare at least one slate so any unsold time in a break is always filled and the break completes cleanly.
- Wait for READY. A creative can only air once conditioning finishes (see the state list in §6). The console shows its status.
- Verify. Open the channel in FAST World TV, hit a break, then use the console's debug trace to confirm the decision, the pod and the beacons.
6Creatives & VAST
Two ways in, one offline conditioning pipeline — media is never transcoded on the playback path.
Getting media in
- Source upload —
POST /v1/creatives/{id}/sourcestores an MP4 and marks the creative uploaded. The worker transcodes it offline. - VAST-tag ingestion —
POST /v1/channels/{id}/vast-tagfetches a third-party VAST tag, follows wrapper redirects, and creates per-channel creatives the worker conditions the same way.
Offline conditioning
The creative worker prepares every asset the same way, driven by a state machine over the creative's status. It validates → transcodes → segments → stores HLS → marks READY, and prepares slates identically. ffprobe/ffmpeg run here, in the worker, never on the manifest hot path.
7Ad decisioning
Deterministic, always-complete break filling.
When a break opens, /v1/ad-decision reads eligible inventory from Postgres, builds the pod, persists the opportunity/decision/pod, and caches the pod in Redis for the data plane to serve. Selection is:
- Eligibility-filtered — only creatives that qualify for this channel/break are considered; the debug trace shows the ones that were rejected and why.
- Priority- and weight-based — higher priority wins; weight distributes within a tier.
- Seeded / deterministic — the same seed and inventory always produce the same pod, which makes runs reproducible and debuggable.
- Loop-to-fill — ads loop to fill a longer break (on by default), so a 90s break isn't left with dead air after a 30s ad.
Failure is always safe
Breaks never go dark: unsold time falls back to slate, and a decisioning failure falls back to the origin. Playback survives any advertising failure.
8Playback & stitching
The data-plane hot path — minimal, cached, signed.
A viewer starts a session and receives a signed, expiring playback token. The data plane verifies that token, then serves the per-viewer manifest at GET /v1/hls/{session}/{channel}/index.m3u8. For each break it reads the already-cached pod and rewrites segment references so the ad segments sit inline, wrapped in the right discontinuities. The stitched break is held a little behind the live edge so the ad segments reliably reach the player before playback reaches them.
9Telemetry & beacons
Two beacon paths: the player's own quartiles, and server-side third-party VAST beacons.
Player telemetry
The player posts playback events to POST /v1/events — quartiles (start / first / mid / third / complete) are mapped to a canonical set and de-duplicated. The endpoint accepts text/plain bodies specifically so navigator.sendBeacon() calls aren't dropped by the browser at unload.
Server-side VAST beacon relay
For creatives that carry third-party tracking, SSAI fires the vendor's impression / quartile / complete beacons server-side, out of band, and records each attempt in beacon_deliveries. Delivery health is visible at GET /v1/beacons/stats. This keeps third-party tracking off the viewer's device and makes it auditable.
10Reconciliation
Every delivery attaches to the right break.
GET /v1/reconciliation/breaks reconciles playback deliveries per break, keyed on ChannelForge's external_break_id. Because the break id is shared across the two systems, SSAI can line up "what we decided" against "what actually played" for each individual break — the foundation for trustworthy fill and completion numbers.
11Analytics
Fill, slate and completion — from real playback, not projections.
GET /v1/analytics (and the console's Analytics screen) report the metrics that matter for a FAST break: fill rate (how much of each break was sold ad vs slate), slate rate, and completion rate (how far into each ad viewers actually watched, from the quartile beacons). Everything is derived from persisted opportunities, decisions and events, reconciled per break.
12Audit log
Who changed what.
Every configuration mutation — create/edit/archive on channels, campaigns, creatives and slates, and VAST config — is written to an audit log, readable at GET /v1/audit and on the console's Audit page. Use it to trace a change back to its source when a break behaves unexpectedly.
13API reference
The live endpoints. Control-plane APIs are under /v1; the data plane serves the manifest hot path.
| Method | Endpoint | What it does |
|---|---|---|
| POST | /v1/ad-decision | Decide and persist the pod for an opportunity; cache it for the data plane. |
| GET | /v1/hls/{session}/{channel}/index.m3u8 | Data plane. The per-viewer stitched manifest (token-verified). |
| POST | /v1/sessions · GET /v1/sessions/:id | Start a playback session / read one. |
| GETPOST | /v1/channels · /v1/channels/:id | Channel CRUD. |
| POST | /v1/channels/:id/vast-tag | Ingest a VAST tag → per-channel creatives (wrappers followed). |
| GETPOST | /v1/campaigns | Campaign CRUD. |
| GETPOST | /v1/creatives · /v1/creatives/:id | Creative CRUD. |
| POST | /v1/creatives/:id/source | Multipart MP4 upload → creative marked uploaded. |
| GETPOST | /v1/slates | Slate CRUD. |
| POST | /v1/events | Player telemetry (quartiles; accepts text/plain for sendBeacon). |
| GET | /v1/beacons/stats | Server-side VAST beacon-relay health. |
| GET | /v1/reconciliation/breaks | Per-break reconciliation on external_break_id. |
| GET | /v1/analytics | Fill / slate / completion metrics. |
| GET | /v1/audit | Configuration-change audit log. |
| GET | /v1/debug/opportunity/:id · /v1/debug/session/:id | Reconstruct a decision / a session end-to-end. |
| GET | /health | Liveness. |
14ChannelForge integration
A clean, versioned seam — not a shared codebase.
ChannelForge owns the channel and emits the SCTE-35 avail markers in its HLS origin; SSAI reads that origin and does the insertion. The two systems talk across a versioned adapter, and each keeps its own ids — SSAI reconciles back to ChannelForge's external_break_id so the boundary stays clean. Practically, that means you can point SSAI at any ChannelForge channel that emits avails, and you can upgrade either system without breaking the other.