Expand description
Sans-IO SCTE-35 server-side ad insertion (SSAI) session core — issue #929.
Per-session ad-break state (session), a pluggable ad-decision trait
(decision), SCTE-35 splice-point conditioning against real segment or
keyframe boundaries (splice), and per-session HLS Interstitial
(EXT-X-DATERANGE CLASS="com.apple.hls.interstitial",
draft-pantos-hls-rfc8216bis Appendix D, transcribed at
broadcast-hls/docs/interstitials.md) playlist rendering over
broadcast_hls::MediaPlaylist (playlist).
§What it does / what it does not
Does: decides where a break splices (cue aligned to real boundaries, with the actual drift reported), remembers which break each viewer session is in, and renders what that viewer’s playlist should say.
Does not:
- No HTTP client, no VAST/VMAP.
decision::AdDecisionProvideris the only extension point for “which ad to play”; any network I/O is the caller’s job. - No media manipulation. Splicing CMAF/TS bytes is
transmux’s job; this crate decides where and what the manifest says. - No per-viewer media cursor. What differs per session is a small
session::BreakStaterecord, never amedia_plane::Trunkcursor. - No DASH period insertion, no
multimuxwiring (future adapter), and no SCTE-35 parsing: the core takes plain tick counts.
§Standards supported
| Standard | Edition | Scope | Status |
|---|---|---|---|
| IETF draft-pantos-hls-rfc8216bis Appendix D (Interstitials) | draft dated 1 May 2026 (vendored); an Internet-Draft | EXT-X-DATERANGE Interstitial schema: CLASS, X-ASSET-URI/X-ASSET-LIST (URI only), X-RESUME-OFFSET, X-PLAYOUT-LIMIT, X-SNAP, X-RESTRICT | Partial (no skip-button control D.3, no asset-list JSON schema) |
| ANSI/SCTE 35 2023r1 | 2023r1 | cue semantics as inputs (decision::BreakContext); 33-bit pts_time wrap handling | Partial (ticks in, no wire decoding; see scte35-splice) |
§Features
| Feature | Default | Effect |
|---|---|---|
std | yes | Links std; off gives #![no_std] + alloc |
serde | no | serde derives on the decision and break-state types |
§Quick start
use ssai_runtime::{
AdBreakDecision, BreakState, InterstitialDateRange, SessionStore, condition_splice_point,
};
// A cue targets tick 900_000 (90 kHz); the real segment boundaries nearest it
// are at 897_000 and 904_500. Allow up to 5_000 ticks of drift.
let boundaries = [810_000_u64, 897_000, 904_500];
let point = condition_splice_point(900_000, &boundaries, 5_000).unwrap();
assert_eq!(point.snapped_pts, 897_000);
assert_eq!(point.delta_ticks, 3_000);
// Whatever ad-decision service the caller uses hands back one decision.
let decision = AdBreakDecision::single_asset("ad-1", "https://ads.example/ad-1.m3u8");
// Track the break for one viewer session ...
let mut store = SessionStore::new();
store
.begin_break("viewer-42", BreakState::new(decision.clone(), point))
.unwrap();
assert_eq!(store.len(), 1);
// ... and render the interstitial EXT-X-DATERANGE tag line for their playlist.
let tag = InterstitialDateRange::from_decision(&decision, "2026-01-01T00:00:10.000Z", Some(30.0))
.to_tag_line()
.unwrap();
assert!(tag.contains("CLASS=\"com.apple.hls.interstitial\""));
assert!(tag.contains("X-ASSET-URI=\"https://ads.example/ad-1.m3u8\""));§What is implemented
session:SessionStoremaps a session id to oneBreakState.decision:AdDecisionProvider,AdBreakDecision,AssetSource,SnapMode,RestrictMode,BreakContext.splice:condition_splice_pointandcondition_splice_point_wrapping(33-bit clock,PTS_MODULUS_33), reporting aConditionedSplicePointwith aSnapDirection. Real cues are frequently not IDR-aligned; the caller picks the tolerance.playlist:InterstitialDateRangerenders and parses the tag;render_session_playlistandSessionPlaylistBase(render the base once, splice the tag line per viewer) both refuse a base withoutEXT-X-PROGRAM-DATE-TIME.
§Design notes
Sans-IO, no_std + alloc, #![forbid(unsafe_code)], structured
Errors. Ad-decision-supplied strings containing ", CR or LF are rejected
(no tag injection). 33-bit arithmetic goes through
broadcast_common::clock33.
§Related crates
Depends on broadcast-common and broadcast-hls; media splicing is
transmux; the HTTP-facing origin is multimux. MSRV 1.95.0;
MIT OR Apache-2.0.
Re-exports§
pub use decision::AdBreakDecision;pub use decision::AdDecisionProvider;pub use decision::AssetSource;pub use decision::BreakContext;pub use decision::RestrictMode;pub use decision::SnapMode;pub use error::Error;pub use error::Result;pub use playlist::INTERSTITIAL_CLASS;pub use playlist::InterstitialDateRange;pub use playlist::SessionPlaylistBase;pub use playlist::render_session_playlist;pub use session::BreakState;pub use session::SessionStore;pub use splice::ConditionedSplicePoint;pub use splice::PTS_MODULUS_33;pub use splice::SnapDirection;pub use splice::condition_splice_point;pub use splice::condition_splice_point_wrapping;
Modules§
- decision
- Ad-break decision types.
- error
- Crate error type.
- playlist
- Per-session HLS Interstitial playlist rendering.
- session
- Per-session ad-break state.
- splice
- Splice-point conditioning: aligning a SCTE-35 cue’s target presentation
time (
splice_time().pts_time, ANSI/SCTE 35 2023r1 §9.8.1, or a DASHemsg’spresentation_time) against the primary content’s actual segment/keyframe boundaries.