Executive summary
SCTE-35 is a thirty-year-old, forty-byte message embedded in a video stream that marks when something happens — typically an ad break — and how long it lasts. SCTE 301, published in 2025, is a framework that standardizes everything the marker leaves unsaid: what plays, for whom, decided by what authority, and how the final stream each viewer receives is put together.
The practical consequences: per-viewer (addressable) advertising, channels assembled from files with no encoder running (FAST channels), and rights actions — substitution, blackout — executed as policies in the delivery path rather than as operational emergencies. The marker is necessary for all of this; it is not sufficient for any of it.
The 30-second version
| SCTE-35 (1990s–today) | SCTE 301 (2025) | |
|---|---|---|
| What it is | A binary in-band cue message | A recommended practice for media assembly workflows |
| Answers | “When, and for how long?” | “What, for whom, decided by whom, assembled how?” |
| Size of the idea | ~40 bytes in the stream | An architecture spanning schedule, decision, and assembly |
| Relationship | 301 does not replace 35 — it coordinates and still emits it | |
SCTE-35 in depth
What it is
SCTE-35 (“Digital Program Insertion Cueing Message”) defines a splice_info_section carried in MPEG transport streams and, in HLS/DASH, surfaced in manifests. The most common command, splice_insert, contains: an event ID, an out-of-network flag (break starts vs. ends), an optional precise splice time on the 90 kHz clock (pts_time), an optional duration with auto-return, and a CRC. The time_signal command plus segmentation descriptors extend this to program boundaries, chapter marks and blackout signaling.
In an HLS manifest the marker appears as EXT-X-DATERANGE with a SCTE35-OUT/SCTE35-IN attribute carrying the raw bytes, conventionally accompanied by EXT-X-CUE-OUT/EXT-X-CUE-IN and EXT-X-DISCONTINUITY tags at content boundaries.
What it deliberately does not do
The marker has no field for the ad to play, the replacement content, the affected audience, or the business rule that caused it. Every real deployment therefore surrounds SCTE-35 with proprietary glue — exactly the glue the rest of the SCTE family was written to standardize.
The family around the marker: SCTE 224 and SCTE 250 (ESAM)
SCTE 224 — Event Scheduling and Notification Interface (ESNI)
An out-of-band, XML-based way to express editorial intent and policy: what may air, where, when, to which audiences, and what to do instead (alternate content, slate, blackout). Think of it as the rights-and-schedule brain. It never touches the video; it describes rules about it.
SCTE 250 — Event Signaling and Management (ESAM)
A request/response interface between the streaming pipeline and a decisioning system: “I have hit this signal / this point — what should I do?” The answer can be: replace with this pod, delete the signal, pass it through, switch content. This is where ad decisioning plugs in.
What SCTE 301 actually is
SCTE 301 is the 2025 recommended practice that ties these pieces into a coherent media assembly architecture, written for the world where a “channel” is increasingly assembled in the cloud rather than emitted by an encoder. Its core moves:
1 Schedule — channel composition and policies come from out-of-band sources (224-style schedules, playlists of VoD assets, live sources), not from whatever an encoder happens to be playing.
2 Decision — at each break or policy point, a decisioning step (250/ESAM-style) determines what fills it, potentially differently per session, region, or device.
3 Assembly — the final stream each viewer receives is constructed at the manifest level, per session: program segments, ad segments, slates, with correct discontinuity and timing bookkeeping — and with SCTE-35 still emitted so downstream systems see a normal stream.
The result is that linear television becomes a computed artifact: deterministic, per-viewer, cacheable, and cheap to multiply.
Capability comparison
| Capability | SCTE-35 alone | SCTE 301 workflow |
|---|---|---|
| Mark an ad break in-band | ✓ — its core purpose | ✓ — still uses 35 for this |
| Say which ads fill the break | ✕ — no such field | ✓ — decision step (250/ESAM) |
| Different ads per viewer | ✕ — one splice for all | ✓ — per-session assembly |
| Replace program content in flight | ✕ — cannot express “play this instead” | ✓ — policy + assembly |
| Blackout with enforcement | △ — can signal only | ✓ — scoped and executed in assembly |
| Channel without an encoder | ✕ — needs a live transport stream | ✓ — assembled from VoD files |
| Out-of-band schedule of events | ✕ — in-band only | ✓ — 224-style schedule layer |
| Downstream compatibility | ✓ — universal | ✓ — emits standard 35 + HLS tags |
How adgate implements it
adgate is a single static Go binary (stdlib only) with ffmpeg as its media-conditioning tool. It implements both pipelines side by side so the difference can be demonstrated rather than described:
The SCTE-35 side (streams)
A poller pulls a real HLS origin, holds the live window in RAM, and tracks media sequence and program-date-time. Cues are genuine splice_insert sections encoded natively (event ID, out-of-network, pts_time on the 90 kHz clock, break duration with auto-return, CRC-32/MPEG). At the marked points the splicer rewrites the manifest: EXT-X-DATERANGE with SCTE35-OUT hex, CUE-OUT/CUE-IN, per-ad DISCONTINUITY + PDT, ad segments served from RAM. One shared pod per cue — deliberately, because that is the 35-level model. Automatic breaks can fire on a timer; with no ads assigned it degrades to marker-only passthrough.
The SCTE 301 side (channels)
A channel is a lineup of conditioned VoD assets plus break rules (“30 s of ads after every 60 s of program”). The runtime computes a deterministic program cycle and, for every viewer session, assembles a private manifest: per-session ad pods (a deterministic function of channel + session + break instance), per-session discontinuity-sequence bookkeeping, monotonic media sequence, synthesized SCTE-35 per break, and policy overrides (replace / blackout) resolved at render time. Schedule and decision stages surface as live events (esni_schedule, esam_decision) so the workflow is visible.
Shared machinery
All media is conditioned once by ffmpeg to a uniform profile — 1280×720@30, H.264 High@3.1 ≈2.3 Mb/s, AAC 44.1 kHz, exact 5 s GOP-aligned segments — so any segment can follow any other across a discontinuity. Segments live in RAM; manifests are computed per request. Ads and programs can be uploaded (any format; ffmpeg conditions them) or generated from nothing by ffmpeg. A supervised ffmpeg test source provides a live origin when no real one is available: it auto-restarts if it dies, is killed on shutdown, and resumes on the next start. Events stream to every page over SSE. State persists in adgate.json.
Booth runbook — for the sales team
1 Make sure adgate is running (it starts on boot via systemd). Open http://<host>:<port>/versus on the booth machine.
2 Log in once: admin / (booth password). The cookie lasts 30 days — you only do this once per machine.
3 If the screen says “Nothing on air yet”, press Generate full demo with ffmpeg and wait ~2 minutes.
4 For the self-running show, open /present, pick the time per screen (45 s is right for booth traffic) and press Start. It loops forever. ←/→ to jump, space to pause when someone asks a question, esc back to the start screen.
5 When someone technical stops by, switch to /versus and drive the six buttons yourself, or open /show/<id> to display the live manifest and decoded cue bytes.
The six scenarios — what happens, what to say
What happens: a real splice_insert is injected on the left stream; on the right, a schedule entry → decision → assembly sequence runs. ~10 s later both players hit a break with a countdown.
“One click, two philosophies. The left stream got a forty-byte marker — every viewer crosses the same splice into the same ads. The right channel ran a whole workflow: scheduled, decided, assembled — and every viewer gets their own ads. Same break, different machinery.”
What happens: the right channel swaps the next 30 s of programming for a different program; the left panel shakes “not expressible”.
“Try to say ‘play THIS instead’ in SCTE-35 — there is no field for it. On the assembly side it's a policy: takes effect in seconds, per manifest, no encoder touched. That's your sponsor swap, your regional feed, your legal takedown.”
What happens: the right channel covers content with slate for 30 s, then lifts automatically.
“Sports rights live and die on blackout enforcement. The marker can signal a blackout; it can't fill it or scope it. Here it's a policy executed in the delivery path itself — per region, per device, per viewer if needed.”
What happens: two live sessions are asked what fills the next break; their pods are shown side by side — different on the 301 side, identical by construction on the 35 side.
“This is where streaming out-earns broadcast: the same 30 seconds sold many times over. That requires the decision layer — the marker alone has nothing to differ on.” (If the pods happen to match: “one-in-five coincidence — refire it.”)
What happens: the actual bytes of the last cue open up — hex plus decoded fields.
“Forty bytes: when, how long, which direction. Note that the 301 channel carries the exact same message — adoption is additive, nothing downstream breaks. Your ad partners and measurement keep working.”
What happens: each side reveals what it's made of — a live encoder URL on the left, a list of VoD files on the right.
“Stop the encoder and the left channel is gone. The right channel is a playlist plus files — launching channel two hundred costs what channel two cost. That's the FAST business case in one screen.”
Troubleshooting
- Left player black / “origin problem”
- The origin feed is down. Use the source selector to switch to “Sample live source”, or in /admin → Generate, press Start under Sample live source.
- “Nothing on air yet” after a restart
- State persists; if it shows anyway, run the bootstrap button again — it skips whatever already exists.
- Breaks fire but show slate instead of ads
- No ad assets assigned/available. Generate ads in /admin → Generate (takes seconds each).
- Buttons do nothing
- You're not logged in on this browser. /login, then retry. Playback never requires login; actions do.
- Player stutters on the booth Wi-Fi
- Everything is served from local RAM — check the machine's own load; the sample source encode plus two players is light, but ad generation spikes CPU briefly.
- Hard reset
- Stop adgate, delete
adgate.jsonanddata/assets, start, run bootstrap. Two minutes to a clean demo.
Glossary
- SCTE-35
- In-band binary cue message marking splice points (ad breaks, program boundaries) in a stream.
- splice_insert / time_signal
- The two main SCTE-35 commands: explicit break in/out vs. a generic timestamp carrying segmentation descriptors.
- SCTE 224 / ESNI
- Out-of-band XML schema for schedules, audiences and policies — “what may air, where, and what to do instead”.
- SCTE 250 / ESAM
- Request/response interface for signal processing decisions — “I hit a cue, what do I do?”.
- SCTE 301
- 2025 recommended practice tying schedule + decision + assembly into a standardized media-assembly workflow.
- SSAI / DAI
- Server-side (dynamic) ad insertion: ads stitched into the stream server-side, indistinguishable from content to the player.
- FAST
- Free Ad-supported Streaming TV — linear channels, usually assembled from VoD libraries, monetized with ads.
- Pod
- The sequence of ads filling one break.
- Manifest / playlist
- The HLS index file listing segments; assembly-level SSAI rewrites this rather than the video itself.
- Discontinuity
- HLS tag warning the player that timestamps/codecs reset at this boundary — required at every splice.
- PDT
- EXT-X-PROGRAM-DATE-TIME — wall-clock anchor for segments.
- MSN
- Media Sequence Number — monotonically increasing segment counter in a live playlist.
FAQ
- Does SCTE 301 replace SCTE-35?
- No. 301-assembled streams still generate and carry standard SCTE-35 so every downstream system keeps working. 301 standardizes the layers around the marker.
- Is this demo using real standards messages?
- The SCTE-35 is real and verifiable byte-for-byte on screen. The 224-schedule and 250-decision stages are faithfully modeled inside adgate (visible as events) rather than spoken as external XML/SOAP protocols — see the limitations section.
- Why do both players sometimes show the same ad?
- With a small demo ad pool, two independent per-session decisions occasionally coincide. The decisions are still independent — refire the break and they diverge.
- Could this run our real channels?
- adgate is a demonstration system. The architecture — manifest assembly, RAM serving, per-session decisioning — is exactly how Tulix builds production SSAI/FAST infrastructure on its own CDN; the production stack adds the integrations listed under limitations.
- What about DASH?
- The demo is HLS-only; the assembly model maps to DASH (periods/SCTE-35 in MPD) identically in principle.
- Latency and scale?
- Manifests are computed per request from in-RAM state (microseconds); segments are RAM-served. Per-session output is cheap by construction because pods are deterministic functions, not stored state.
What this demo does NOT implement (honest list)
① No external SCTE 224/ESNI XML interface. Schedules are internal (lineup + break rules); adgate does not ingest or emit 224 XML documents.
② No external SCTE 250/ESAM transactions. Decisions are made by adgate's internal deterministic decisioner; there is no SOAP/XML exchange with an outside ADS.
③ No VAST/VMAP ad-server integration. Ads come from the local asset library, not from a programmatic ad call.
④ HLS only. No DASH/MPD output.
⑤ Single rendition. The best origin variant is selected; the full ABR ladder is not passed through, and assembled channels emit one 720p rendition.
⑥ Origin SCTE-35 is not forwarded. adgate injects its own cues; markers already present in an origin are ignored rather than detected/passed through.
⑦ No DRM, encryption, or playback tokens. Demo playback is open; only control actions require login.
⑧ Impressions are internal events only — no beaconing to external measurement.
⑨ Blackout/substitution scoping is time-based, not audience-based. Policies apply to all sessions; geo/device scoping is described, not enforced (the per-session hook exists in the assembler).
⑩ Editing a channel lineup restarts its timeline (media sequence jumps forward by design; players resync).
⑪ Presentation auto-actions require the booth browser to be logged in; otherwise the show runs with auto-breaks only.
⑫ Player UI is native browser controls via hls.js — no custom skin.