Skip to main content

ssai_runtime/
lib.rs

1//! Sans-IO SCTE-35 server-side ad insertion (SSAI) session core — issue #929.
2//!
3//! Per-session ad-break state ([`session`]), a pluggable ad-decision trait
4//! ([`decision`]), SCTE-35 splice-point conditioning against real segment or
5//! keyframe boundaries ([`splice`]), and per-session HLS Interstitial
6//! (`EXT-X-DATERANGE CLASS="com.apple.hls.interstitial"`,
7//! draft-pantos-hls-rfc8216bis Appendix D, transcribed at
8//! `broadcast-hls/docs/interstitials.md`) playlist rendering over
9//! [`broadcast_hls::MediaPlaylist`] ([`playlist`]).
10//!
11//! # What it does / what it does not
12//!
13//! **Does:** decides where a break splices (cue aligned to real boundaries, with
14//! the actual drift reported), remembers which break each viewer session is in,
15//! and renders what that viewer's playlist should say.
16//!
17//! **Does not:**
18//!
19//! - **No HTTP client, no VAST/VMAP.** [`decision::AdDecisionProvider`] is the
20//!   only extension point for "which ad to play"; any network I/O is the
21//!   caller's job.
22//! - **No media manipulation.** Splicing CMAF/TS bytes is `transmux`'s job; this
23//!   crate decides *where* and *what the manifest says*.
24//! - **No per-viewer media cursor.** What differs per session is a small
25//!   [`session::BreakState`] record, never a `media_plane::Trunk` cursor.
26//! - **No DASH period insertion, no `multimux` wiring** (future adapter), and no
27//!   SCTE-35 parsing: the core takes plain tick counts.
28//!
29//! # Standards supported
30//!
31//! | Standard | Edition | Scope | Status |
32//! |---|---|---|---|
33//! | 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) |
34//! | 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`) |
35//!
36//! # Features
37//!
38//! | Feature | Default | Effect |
39//! |---|---|---|
40//! | `std` | yes | Links `std`; off gives `#![no_std]` + `alloc` |
41//! | `serde` | no | `serde` derives on the decision and break-state types |
42//!
43//! # Quick start
44//!
45//! ```rust
46//! use ssai_runtime::{
47//!     AdBreakDecision, BreakState, InterstitialDateRange, SessionStore, condition_splice_point,
48//! };
49//!
50//! // A cue targets tick 900_000 (90 kHz); the real segment boundaries nearest it
51//! // are at 897_000 and 904_500. Allow up to 5_000 ticks of drift.
52//! let boundaries = [810_000_u64, 897_000, 904_500];
53//! let point = condition_splice_point(900_000, &boundaries, 5_000).unwrap();
54//! assert_eq!(point.snapped_pts, 897_000);
55//! assert_eq!(point.delta_ticks, 3_000);
56//!
57//! // Whatever ad-decision service the caller uses hands back one decision.
58//! let decision = AdBreakDecision::single_asset("ad-1", "https://ads.example/ad-1.m3u8");
59//!
60//! // Track the break for one viewer session ...
61//! let mut store = SessionStore::new();
62//! store
63//!     .begin_break("viewer-42", BreakState::new(decision.clone(), point))
64//!     .unwrap();
65//! assert_eq!(store.len(), 1);
66//!
67//! // ... and render the interstitial EXT-X-DATERANGE tag line for their playlist.
68//! let tag = InterstitialDateRange::from_decision(&decision, "2026-01-01T00:00:10.000Z", Some(30.0))
69//!     .to_tag_line()
70//!     .unwrap();
71//! assert!(tag.contains("CLASS=\"com.apple.hls.interstitial\""));
72//! assert!(tag.contains("X-ASSET-URI=\"https://ads.example/ad-1.m3u8\""));
73//! ```
74//!
75//! # What is implemented
76//!
77//! - [`session`]: [`SessionStore`] maps a session id to one [`BreakState`].
78//! - [`decision`]: [`AdDecisionProvider`], [`AdBreakDecision`], [`AssetSource`],
79//!   [`SnapMode`], [`RestrictMode`], [`BreakContext`].
80//! - [`splice`]: [`condition_splice_point`] and
81//!   [`condition_splice_point_wrapping`] (33-bit clock, [`PTS_MODULUS_33`]),
82//!   reporting a [`ConditionedSplicePoint`] with a [`SnapDirection`]. Real cues
83//!   are frequently not IDR-aligned; the caller picks the tolerance.
84//! - [`playlist`]: [`InterstitialDateRange`] renders and parses the tag;
85//!   [`render_session_playlist`] and [`SessionPlaylistBase`] (render the base
86//!   once, splice the tag line per viewer) both refuse a base without
87//!   `EXT-X-PROGRAM-DATE-TIME`.
88//!
89//! # Design notes
90//!
91//! Sans-IO, `no_std` + `alloc`, `#![forbid(unsafe_code)]`, structured
92//! [`Error`]s. Ad-decision-supplied strings containing `"`, CR or LF are rejected
93//! (no tag injection). 33-bit arithmetic goes through
94//! `broadcast_common::clock33`.
95//!
96//! # Related crates
97//!
98//! Depends on `broadcast-common` and `broadcast-hls`; media splicing is
99//! `transmux`; the HTTP-facing origin is `multimux`. MSRV 1.95.0;
100//! MIT OR Apache-2.0.
101#![no_std]
102#![forbid(unsafe_code)]
103#![cfg_attr(docsrs, feature(doc_cfg))]
104
105extern crate alloc;
106
107pub mod decision;
108pub mod error;
109pub mod playlist;
110pub mod session;
111pub mod splice;
112
113pub use decision::{
114    AdBreakDecision, AdDecisionProvider, AssetSource, BreakContext, RestrictMode, SnapMode,
115};
116pub use error::{Error, Result};
117pub use playlist::{
118    INTERSTITIAL_CLASS, InterstitialDateRange, SessionPlaylistBase, render_session_playlist,
119};
120pub use session::{BreakState, SessionStore};
121pub use splice::{
122    ConditionedSplicePoint, PTS_MODULUS_33, SnapDirection, condition_splice_point,
123    condition_splice_point_wrapping,
124};