Ritcher is a high-performance HLS/DASH stitcher built in Rust for ad insertion. It supports both SSAI (Server-Side Ad Insertion) and SGAI (Server-Guided Ad Insertion via HLS Interstitials and DASH Callback EventStreams), sitting between the origin CDN and the player to dynamically insert ads into live and VOD streams.
Ritcher runs as a standalone Docker container deployable anywhere. It integrates well with the Eyevinn Open Source Cloud ecosystem — particularly as a downstream stitcher for Channel Engine — but has no platform dependencies.
- SCTE-35 CUE tag detection — Detects
EXT-X-CUE-OUT,EXT-X-CUE-IN, andEXT-X-CUE-OUT-CONTmarkers in HLS playlists - SSAI: Ad interleaving — Replaces content segments in ad break windows with ad segments, including proper
EXT-X-DISCONTINUITYtags - SGAI: HLS Interstitials — Injects
EXT-X-DATERANGEtags withCLASS="com.apple.hls.interstitial"per RFC 8216bis, enabling client-side ad playback via hls.js 1.6+ and AVPlayer - Asset-list endpoint — JSON endpoint returning ad creatives per ad break for HLS Interstitials players
- Master playlist support — Rewrites variant-stream URLs for multi-quality stitching
- Low-Latency HLS (LL-HLS) — Pass-through support for partial segments (
EXT-X-PART), blocking playlist reload (_HLS_msn/_HLS_part),EXT-X-SERVER-CONTROL,EXT-X-PART-INF,EXT-X-PRELOAD-HINT, andEXT-X-RENDITION-REPORT. All URIs rewritten through the stitcher proxy. Works with SGAI mode for low-latency ad insertion via HLS Interstitials - Demo endpoints — Synthetic HLS playlists with real Mux test segments, CUE markers, and
EXT-X-PROGRAM-DATE-TIMEfor testing, including an LL-HLS variant with partial segments
- DASH MPD parsing — Parse and serialize DASH MPD manifests with hierarchical BaseURL resolution
- SCTE-35 EventStream detection — Detects ad breaks from
urn:scte:scte35:2013:xmlEventStream elements - URL rewriting — Rewrites BaseURL and SegmentTemplate URLs at all MPD hierarchy levels through the stitcher proxy
- SSAI: Period-based ad insertion — Inserts ad Periods with SegmentList after detected ad break signals
- SGAI: Callback EventStreams — Injects
urn:mpeg:dash:event:callback:2015EventStream per ISO 23009-1, enabling client-side ad playback via dash.js and Shaka Player. Reuses the asset-list endpoint for ad creative delivery - Demo endpoint — Synthetic DASH manifest with SCTE-35 EventStream for testing
- Multi-track ad insertion — Handles separate audio/video/subtitle renditions; HLS
trackparam for per-rendition playlists, DASH AdaptationSet mirroring with bandwidth and language preservation - VAST ad provider — Fetches and parses VAST 2.0/3.0/4.0 XML from any ad server, with wrapper chain support
- Static ad provider — Built-in provider for testing with pre-configured ad segments
- Slate management — Fallback filler content when VAST returns no ads or fails
- Segment proxying — High-performance proxying for content, ad, and slate segments with retry logic
- Session management — In-memory (DashMap) or distributed (Valkey/Redis) session store with automatic TTL-based cleanup. Feature-flagged:
cargo build --features valkey - Prometheus metrics —
GET /metricsendpoint with request counts, durations, VAST stats, and session gauges - Ad tracking & beaconing — VAST impression, quartile (start/firstQuartile/midpoint/thirdQuartile/complete), and error beacons fired server-side on segment delivery
- Ad conditioning — Warning-level validation of ad creative compatibility (codec, resolution, MIME type)
- Error recovery — Retry logic (1 retry, 500ms backoff) for VAST, origin, and ad segment fetches
- JSON health check — Structured diagnostics with version, session count, and uptime
- CORS support — Permissive in dev mode, restrictive in production
- Docker ready — Multi-stage Dockerfile for production deployment
graph LR
Player -->|Request| S1[Fetch manifest]
S1 --> S2[Detect ad breaks]
S2 --> Mode{SSAI / SGAI}
Mode -->|SSAI| S3[Fetch ads]
S3 --> S4[Interleave segments]
Mode -->|SGAI| S5[Inject markers]
S4 --> S6[Rewrite URLs]
S5 --> S6
S6 --> S7[Serve manifest]
CDN[Origin CDN] -.-> S1
ADS[Ad Server] -.-> S3
SLATE[Slate Source] -.-> S4
SSAI replaces content segments with ad segments server-side. SGAI injects HLS Interstitial
EXT-X-DATERANGEtags or DASH callbackEventStreamelements — the player fetches ads client-side via the asset-list endpoint.
- Rust stable (edition 2024)
# Start with built-in demo and static ad provider
DEV_MODE=true cargo run
# Demo playlist (raw, no stitching):
# http://localhost:3000/demo/playlist.m3u8
# Stitched demo (with ad insertion):
# http://localhost:3000/stitch/demo/playlist.m3u8?origin=http://localhost:3000/demo/playlist.m3u8# Using Eyevinn test-adserver (or any VAST-compatible ad server)
DEV_MODE=true \
VAST_ENDPOINT="http://localhost:8080/api/v1/vast?dur=[DURATION]" \
cargo run# VAST mode with slate fallback when ads unavailable
DEV_MODE=true \
VAST_ENDPOINT="http://localhost:8080/api/v1/vast?dur=[DURATION]" \
SLATE_URL="https://hls.src.tedm.io/content/ts_h264_480p_1s" \
cargo run# Server-Guided: player fetches ads client-side via HLS Interstitials
DEV_MODE=true \
STITCHING_MODE=sgai \
VAST_ENDPOINT="http://localhost:8080/api/v1/vast?dur=[DURATION]" \
cargo run# Low-Latency HLS: partial segments pass through, ads via HLS Interstitials
DEV_MODE=true \
STITCHING_MODE=sgai \
cargo run
# LL-HLS demo (raw, no stitching):
# http://localhost:3000/demo/ll-hls/playlist.m3u8
# Stitched LL-HLS (with DATERANGE ad markers):
# http://localhost:3000/stitch/demo/playlist.m3u8docker build -t ritcher .
docker run -p 3000:3000 \
-e PORT=3000 \
-e BASE_URL=https://stitcher.example.com \
-e VAST_ENDPOINT=https://ads.example.com/vast \
ritcherPORT=3000 \
BASE_URL=https://stitcher.example.com \
ORIGIN_URL=https://cdn.example.com/stream/playlist.m3u8 \
VAST_ENDPOINT=https://ads.example.com/vast \
SLATE_URL=https://slate.example.com/content \
cargo run --release| Endpoint | Description |
|---|---|
GET /health |
JSON health check ({ status, version, active_sessions, uptime_seconds }) |
GET /metrics |
Prometheus metrics in text exposition format |
GET /demo/playlist.m3u8 |
Demo HLS playlist with CUE markers |
GET /demo/ll-hls/playlist.m3u8 |
Demo LL-HLS playlist with partial segments and CUE markers |
GET /demo/manifest.mpd |
Demo DASH manifest with SCTE-35 EventStream |
GET /stitch/{session_id}/playlist.m3u8?origin={url} |
Stitched HLS playlist with ad insertion |
GET /stitch/{session_id}/manifest.mpd?origin={url} |
Stitched DASH manifest with ad insertion |
GET /stitch/{session_id}/segment/{*path}?origin={base} |
Proxied content segment (HLS/DASH) |
GET /stitch/{session_id}/ad/{ad_name} |
Proxied ad segment |
GET /stitch/{session_id}/asset-list/{break_id}?dur={seconds} |
Asset-list JSON for HLS Interstitials and DASH callback EventStreams (SGAI mode) |
| Variable | Description | Required | Default |
|---|---|---|---|
DEV_MODE |
Enable dev mode with defaults | No | false |
PORT |
Server port | Prod only | 3000 |
BASE_URL |
Stitcher's public URL | Prod only | http://localhost:3000 |
ORIGIN_URL |
Default origin playlist URL | Prod only | — |
AD_PROVIDER_TYPE |
vast, static, or auto |
No | auto |
VAST_ENDPOINT |
VAST ad server URL (supports [DURATION] and [CACHEBUSTING] macros) |
For VAST mode | — |
SLATE_URL |
Slate fallback content URL | No | — |
SLATE_SEGMENT_DURATION |
Slate segment duration (seconds) | No | 1.0 |
AD_SOURCE_URL |
Static ad segment source | For static mode | tedm.io test stream |
AD_SEGMENT_DURATION |
Static ad segment duration (seconds) | No | 1.0 |
SESSION_STORE |
Session backend: memory or valkey |
No | memory |
VALKEY_URL |
Valkey/Redis connection URL | When SESSION_STORE=valkey |
— |
SESSION_TTL_SECS |
Session TTL in seconds | No | 300 |
STITCHING_MODE |
Ad insertion strategy: ssai or sgai |
No | ssai |
Auto-detection: When AD_PROVIDER_TYPE=auto (default), Ritcher uses VAST if VAST_ENDPOINT is set, otherwise falls back to static.
Stitching modes: STITCHING_MODE=ssai (default) replaces content segments with ad segments server-side. STITCHING_MODE=sgai injects HLS Interstitial markers (EXT-X-DATERANGE) for HLS and callback EventStreams (urn:mpeg:dash:event:callback:2015) for DASH, serving an asset-list endpoint — the player fetches and plays ads client-side. Both modes work with any ad provider (VAST or static).
Distributed sessions: To share sessions across multiple Ritcher instances behind a load balancer, build with cargo build --features valkey and set SESSION_STORE=valkey with a VALKEY_URL.
Prometheus metrics available at GET /metrics:
| Metric | Type | Description |
|---|---|---|
ritcher_requests_total |
Counter | Total requests by endpoint and status |
ritcher_request_duration_seconds |
Histogram | Request duration by endpoint |
ritcher_active_sessions |
Gauge | Currently active sessions |
ritcher_ad_breaks_detected |
Counter | Ad breaks detected across all requests |
ritcher_vast_requests_total |
Counter | VAST requests by result (success/error/empty) |
ritcher_slate_fallbacks_total |
Counter | Slate fallback activations |
ritcher_tracking_beacons_total |
Counter | Tracking beacons by event and result |
ritcher_origin_fetch_errors_total |
Counter | Origin fetch errors |
ritcher_interstitials_injected_total |
Counter | HLS Interstitial/DASH callback tags injected (SGAI) |
ritcher_asset_list_requests_total |
Counter | Asset-list endpoint requests by status (SGAI) |
In live SSAI, every concurrent viewer gets a unique manifest on every segment refresh — this work cannot be CDN-cached. The stitcher's manifest pipeline is one of the scalability bottlenecks.
Ritcher's CPU-only manifest pipeline (parse → detect CUE breaks → interleave ads → rewrite URLs → serialize) runs in ~6 µs for a typical live playlist:
| Scenario | Segments | Ad Breaks | Time per manifest | CPU throughput (single core) |
|---|---|---|---|---|
| Typical live | 6 | 1 | ~6 µs | ~156K ops/sec |
| Medium window | 15 | 1 | ~12 µs | ~84K ops/sec |
| DVR/catchup | 60 | 3 | ~44 µs | ~23K ops/sec |
| Pass-through | 12 | 0 | ~7 µs | ~137K ops/sec |
Important: These numbers measure pure CPU time for manifest manipulation — no network I/O is included. In a real deployment, each manifest request also involves fetching the source playlist from the origin CDN, and segment proxying consumes significant bandwidth. Real-world throughput depends heavily on network latency, connection concurrency, and available bandwidth — not just CPU.
VAST XML parsing adds ~18 µs per ad pod (3 ads, 3 media files each), though in production this is cached per ad break rather than per viewer.
See BENCHMARK.md for detailed results, methodology, scaling estimates, and real-world considerations. Run benchmarks yourself:
cargo bench- Rust (Edition 2024) — Zero-cost abstractions for manifest-per-viewer scalability
- Axum 0.8 — Async HTTP server
- Tokio — Async runtime
- m3u8-rs 6.0 — HLS playlist parsing
- dash-mpd — DASH MPD parsing and serialization
- quick-xml — VAST XML parsing
- reqwest — HTTP client with connection pooling
- DashMap — Lock-free concurrent in-memory session storage
- redis 0.29 — Optional Valkey/Redis backend for distributed sessions (feature-flagged)
- metrics + metrics-exporter-prometheus — Prometheus observability
- tower-http — CORS middleware
- tracing — Structured logging
# Run all tests (248 tests: 227 unit + 14 E2E + 7 handler)
cargo test
# Run only unit tests
cargo test --lib
# Run only E2E tests
cargo test --test e2e
# Run with logging
RUST_LOG=debug cargo test
# Run benchmarks (Criterion)
cargo bench
# Clippy
cargo clippy -- -D warnings- HLS playlist parsing and URL rewriting
- SCTE-35 CUE-OUT/CUE-IN/CUE-OUT-CONT detection
- Ad interleaving with DISCONTINUITY tags
- Static ad provider (testing)
- VAST ad provider (VAST 2.0/3.0/4.0, wrapper chains)
- Session management with background cleanup
- Demo endpoint with real test segments
- JSON health check with diagnostics
- CORS middleware (dev/prod)
- Slate management (fallback when no ads available)
- Master playlist support
- Prometheus metrics
- Error recovery with retry logic
- Ad conditioning (warning-level creative validation)
- Docker deployment
- DASH MPD parsing and serialization
- Hierarchical BaseURL resolution (MPD/Period/AdaptationSet/Representation)
- SegmentTemplate URL rewriting through stitcher proxy
- SCTE-35 EventStream ad break detection
- Duration/timing validation with DoS prevention
- Period-based ad insertion (interleaver)
- DASH manifest handler and routes
- DASH demo endpoint
- Multi-track ad insertion (separate audio/video/subtitle renditions)
- Distributed session store (Valkey/Redis for multi-instance consistency)
- Ad tracking and beaconing
-
STITCHING_MODEenv var (ssaidefault,sgaioption) -
EXT-X-DATERANGEinjection withCLASS="com.apple.hls.interstitial" -
EXT-X-PROGRAM-DATE-TIMEsynthesis for origins without PDT - Asset-list JSON endpoint per RFC 8216bis §6.3
- CUE tag removal after DateRange injection (no double-signaling)
- Low-latency HLS (LL-HLS)
- Callback EventStream injection (
urn:mpeg:dash:event:callback:2015) - SCTE-35 EventStream stripping (no double-signaling)
- Asset-list endpoint reuse for DASH SGAI
- DASH manifest handler
StitchingMode::Sgaibranch
- Per-viewer manifest personalization
The SSAI market is growing at 20.3% CAGR toward $14.5B by 2033, yet no production-ready open-source live SSAI stitcher exists. Ritcher fills that gap with Rust performance for the CPU-bound work of generating unique manifests per viewer. It works with any VAST-compatible ad server and any HLS/DASH origin — deploy it on Eyevinn Open Source Cloud for a turnkey setup with Channel Engine, or run it standalone anywhere Docker runs.
Joel del Pilar (@JoeldelPilar)
Built on the shoulders of Eyevinn Technology's open-source streaming ecosystem. Eyevinn Open Source Cloud is the recommended deployment platform for a managed experience, but Ritcher runs anywhere.
MIT License — see LICENSE file for details.
