Skip to main content

HlsOrigin

Struct HlsOrigin 

Source
pub struct HlsOrigin { /* private fields */ }
Available on crate feature std only.
Expand description

The LL-HLS origin ServedEgress: renders playlists and resolves blocking-reload/part-availability requests for one stream, backed by a shared Trunk. See this module’s own doc for exactly what comes straight from the Trunk and what needs the small synced Window.

Implementations§

Source§

impl HlsOrigin

Source

pub fn builder(trunk: Arc<Trunk>) -> HlsOriginBuilder

Start building an HlsOrigin over trunk — see HlsOriginBuilder for the required fields (target_duration_secs/window_segments), the container choice, and how to opt into LL-HLS.

Examples found in repository?
examples/client_stepping.rs (line 41)
36fn canned_playlist() -> String {
37    let trunk = Trunk::new(TrunkConfig::new(nz(16), nz(4), nz(8), nz(4), nz(16)));
38    let writer = trunk
39        .segment_writer()
40        .expect("first (and only) segment writer");
41    let origin = HlsOrigin::builder(std::sync::Arc::clone(&trunk))
42        .target_duration_secs(1.0)
43        .window_segments(nz(4))
44        .low_latency(500)
45        .build()
46        .expect("both required fields set");
47    origin.set_init(vec![0xAA; 32]);
48
49    writer.publish_part(PartEntry::new(
50        vec![0x01; 16],
51        1,
52        0,
53        Duration::from_millis(500),
54        true,
55    ));
56    writer.publish_segment(SegmentEntry::new(
57        vec![0x02; 32],
58        1,
59        Duration::from_secs(1),
60        Timestamp::from_nanos(0),
61        SegmentMeta {
62            discontinuous: false,
63        },
64    ));
65    writer.publish_part(PartEntry::new(
66        vec![0x03; 16],
67        2,
68        0,
69        Duration::from_millis(500),
70        true,
71    ));
72
73    match origin.resolve(
74        HlsRequest::Playlist {
75            track_id: DEFAULT_TRACK_ID,
76            query: BlockingQuery::default(),
77        },
78        Timestamp::from_nanos(0),
79        AwaitPolicy::new(Timestamp::from_nanos(0)),
80    ) {
81        EgressResponse::Ready {
82            body: HlsBody::Playlist(m),
83            ..
84        } => m,
85        other => panic!("expected Ready(Playlist), got {other:?}"),
86    }
87}
More examples
Hide additional examples
examples/origin_playlist.rs (line 57)
52fn main() {
53    let trunk = Trunk::new(TrunkConfig::new(nz(16), nz(4), nz(8), nz(4), nz(16)));
54    let writer = trunk
55        .segment_writer()
56        .expect("first (and only) segment writer");
57    let origin = HlsOrigin::builder(std::sync::Arc::clone(&trunk))
58        .target_duration_secs(TARGET_DURATION_SECS)
59        .window_segments(nz(WINDOW_SEGMENTS))
60        .low_latency(PART_TARGET_MS)
61        .build()
62        .expect("both required fields set");
63    origin.set_init(vec![0xAA; 32]);
64
65    // Segment 1 closes with two parts.
66    writer.publish_part(PartEntry::new(
67        vec![0x01; 16],
68        1,
69        0,
70        Duration::from_millis(500),
71        true,
72    ));
73    writer.publish_part(PartEntry::new(
74        vec![0x02; 16],
75        1,
76        1,
77        Duration::from_millis(500),
78        false,
79    ));
80    writer.publish_segment(SegmentEntry::new(
81        vec![0x03; 32],
82        1,
83        Duration::from_secs(1),
84        Timestamp::from_nanos(0),
85        SegmentMeta {
86            discontinuous: false,
87        },
88    ));
89
90    // Segment 2 is still open, with only its first part landed so far.
91    writer.publish_part(PartEntry::new(
92        vec![0x04; 16],
93        2,
94        0,
95        Duration::from_millis(500),
96        true,
97    ));
98
99    println!("--- master.m3u8 ---");
100    println!("{}", master_playlist_m3u8("media.m3u8"));
101
102    println!("--- media.m3u8 ---");
103    match resolve(
104        &origin,
105        HlsRequest::Playlist {
106            track_id: DEFAULT_TRACK_ID,
107            query: BlockingQuery::default(),
108        },
109    ) {
110        EgressResponse::Ready {
111            body: HlsBody::Playlist(m),
112            ..
113        } => println!("{m}"),
114        other => panic!("expected Ready(Playlist), got {other:?}"),
115    }
116
117    // A plain (non-blocking) request is Ready immediately.
118    let outcome = resolve(
119        &origin,
120        HlsRequest::Playlist {
121            track_id: DEFAULT_TRACK_ID,
122            query: BlockingQuery::default(),
123        },
124    );
125    assert!(matches!(
126        outcome,
127        EgressResponse::Ready {
128            body: HlsBody::Playlist(_),
129            ..
130        }
131    ));
132    println!("resolve(Playlist, no query)     -> Ready");
133
134    // A blocking-reload request for a segment that hasn't closed yet: with
135    // `await_policy`'s deadline already at `now`, this immediately reports
136    // the awaited condition has run out of patience rather than serving a
137    // fabricated Ready.
138    let outcome = resolve(
139        &origin,
140        HlsRequest::Playlist {
141            track_id: DEFAULT_TRACK_ID,
142            // Segment 2 is the in-progress one, so msn=3 is the next segment
143            // that has not closed. Keep this within
144            // `ABUSE_MSN_FUTURE_BOUND` (live edge + 2) — a larger value is
145            // rejected outright as abuse and never reaches the Await path
146            // this block is demonstrating.
147            query: BlockingQuery {
148                hls_msn: Some(3),
149                hls_part: None,
150            },
151        },
152    );
153    assert_eq!(outcome, EgressResponse::NotFound);
154    println!("resolve(Playlist, _HLS_msn=3)   -> NotFound (Await's patience already expired)");
155
156    // A `_HLS_msn` unreasonably far beyond the live edge is rejected outright
157    // (RFC 8216bis §6.2.5.2 abuse prevention) rather than ever Await-ing.
158    let outcome = resolve(
159        &origin,
160        HlsRequest::Playlist {
161            track_id: DEFAULT_TRACK_ID,
162            query: BlockingQuery {
163                hls_msn: Some(999),
164                hls_part: None,
165            },
166        },
167    );
168    assert!(matches!(outcome, EgressResponse::BadRequest { .. }));
169    println!("resolve(Playlist, _HLS_msn=999) -> BadRequest (abuse bound)");
170
171    // `Resource`: the init segment and the closed segment are Ready...
172    match resolve(
173        &origin,
174        HlsRequest::Resource {
175            name: "init-1.mp4".to_string(),
176        },
177    ) {
178        EgressResponse::Ready { .. } => println!("resolve(Resource, init-1.mp4)     -> Ready"),
179        other => panic!("expected Ready, got {other:?}"),
180    }
181    match resolve(
182        &origin,
183        HlsRequest::Resource {
184            name: "seg-1-1.m4s".to_string(),
185        },
186    ) {
187        EgressResponse::Ready { .. } => println!("resolve(Resource, seg-1-1.m4s)    -> Ready"),
188        other => panic!("expected Ready, got {other:?}"),
189    }
190    // ...a live part of the still-open segment is Ready too...
191    match resolve(
192        &origin,
193        HlsRequest::Resource {
194            name: "part-1-2.0.m4s".to_string(),
195        },
196    ) {
197        EgressResponse::Ready { .. } => println!("resolve(Resource, part-1-2.0.m4s) -> Ready"),
198        other => panic!("expected Ready, got {other:?}"),
199    }
200    // ...a preload-hinted part not yet produced reports NotFound once this
201    // call's `await_policy` has already expired (a real HTTP adapter would
202    // instead give it a real deadline and block on `Trunk::listen()`)...
203    match resolve(
204        &origin,
205        HlsRequest::Resource {
206            name: "part-1-2.1.m4s".to_string(),
207        },
208    ) {
209        EgressResponse::NotFound => {
210            println!(
211                "resolve(Resource, part-1-2.1.m4s) -> NotFound (Await's patience already expired)"
212            )
213        }
214        other => panic!("expected NotFound, got {other:?}"),
215    }
216    // ...and an unrecognised filename is a plain 404.
217    match resolve(
218        &origin,
219        HlsRequest::Resource {
220            name: "nope.txt".to_string(),
221        },
222    ) {
223        EgressResponse::NotFound => println!("resolve(Resource, nope.txt)       -> NotFound"),
224        other => panic!("expected NotFound, got {other:?}"),
225    }
226}
Source

pub fn set_init(&self, bytes: impl Into<Bytes>)

Store the init segment bytes — see this module’s doc for why an init segment is not something any Trunk ring holds.

Documented no-op under Container::MpegTs: this origin’s MpegTs grammar has no init resource and never emits #EXT-X-MAP by default (see Container’s own doc), so bytes stored here are never advertised or served in that mode. The method stays callable regardless of container so a caller sharing one code path across both (e.g. a segmenter that always calls set_init once available) does not need to branch on which container it configured.

Examples found in repository?
examples/client_stepping.rs (line 47)
36fn canned_playlist() -> String {
37    let trunk = Trunk::new(TrunkConfig::new(nz(16), nz(4), nz(8), nz(4), nz(16)));
38    let writer = trunk
39        .segment_writer()
40        .expect("first (and only) segment writer");
41    let origin = HlsOrigin::builder(std::sync::Arc::clone(&trunk))
42        .target_duration_secs(1.0)
43        .window_segments(nz(4))
44        .low_latency(500)
45        .build()
46        .expect("both required fields set");
47    origin.set_init(vec![0xAA; 32]);
48
49    writer.publish_part(PartEntry::new(
50        vec![0x01; 16],
51        1,
52        0,
53        Duration::from_millis(500),
54        true,
55    ));
56    writer.publish_segment(SegmentEntry::new(
57        vec![0x02; 32],
58        1,
59        Duration::from_secs(1),
60        Timestamp::from_nanos(0),
61        SegmentMeta {
62            discontinuous: false,
63        },
64    ));
65    writer.publish_part(PartEntry::new(
66        vec![0x03; 16],
67        2,
68        0,
69        Duration::from_millis(500),
70        true,
71    ));
72
73    match origin.resolve(
74        HlsRequest::Playlist {
75            track_id: DEFAULT_TRACK_ID,
76            query: BlockingQuery::default(),
77        },
78        Timestamp::from_nanos(0),
79        AwaitPolicy::new(Timestamp::from_nanos(0)),
80    ) {
81        EgressResponse::Ready {
82            body: HlsBody::Playlist(m),
83            ..
84        } => m,
85        other => panic!("expected Ready(Playlist), got {other:?}"),
86    }
87}
More examples
Hide additional examples
examples/origin_playlist.rs (line 63)
52fn main() {
53    let trunk = Trunk::new(TrunkConfig::new(nz(16), nz(4), nz(8), nz(4), nz(16)));
54    let writer = trunk
55        .segment_writer()
56        .expect("first (and only) segment writer");
57    let origin = HlsOrigin::builder(std::sync::Arc::clone(&trunk))
58        .target_duration_secs(TARGET_DURATION_SECS)
59        .window_segments(nz(WINDOW_SEGMENTS))
60        .low_latency(PART_TARGET_MS)
61        .build()
62        .expect("both required fields set");
63    origin.set_init(vec![0xAA; 32]);
64
65    // Segment 1 closes with two parts.
66    writer.publish_part(PartEntry::new(
67        vec![0x01; 16],
68        1,
69        0,
70        Duration::from_millis(500),
71        true,
72    ));
73    writer.publish_part(PartEntry::new(
74        vec![0x02; 16],
75        1,
76        1,
77        Duration::from_millis(500),
78        false,
79    ));
80    writer.publish_segment(SegmentEntry::new(
81        vec![0x03; 32],
82        1,
83        Duration::from_secs(1),
84        Timestamp::from_nanos(0),
85        SegmentMeta {
86            discontinuous: false,
87        },
88    ));
89
90    // Segment 2 is still open, with only its first part landed so far.
91    writer.publish_part(PartEntry::new(
92        vec![0x04; 16],
93        2,
94        0,
95        Duration::from_millis(500),
96        true,
97    ));
98
99    println!("--- master.m3u8 ---");
100    println!("{}", master_playlist_m3u8("media.m3u8"));
101
102    println!("--- media.m3u8 ---");
103    match resolve(
104        &origin,
105        HlsRequest::Playlist {
106            track_id: DEFAULT_TRACK_ID,
107            query: BlockingQuery::default(),
108        },
109    ) {
110        EgressResponse::Ready {
111            body: HlsBody::Playlist(m),
112            ..
113        } => println!("{m}"),
114        other => panic!("expected Ready(Playlist), got {other:?}"),
115    }
116
117    // A plain (non-blocking) request is Ready immediately.
118    let outcome = resolve(
119        &origin,
120        HlsRequest::Playlist {
121            track_id: DEFAULT_TRACK_ID,
122            query: BlockingQuery::default(),
123        },
124    );
125    assert!(matches!(
126        outcome,
127        EgressResponse::Ready {
128            body: HlsBody::Playlist(_),
129            ..
130        }
131    ));
132    println!("resolve(Playlist, no query)     -> Ready");
133
134    // A blocking-reload request for a segment that hasn't closed yet: with
135    // `await_policy`'s deadline already at `now`, this immediately reports
136    // the awaited condition has run out of patience rather than serving a
137    // fabricated Ready.
138    let outcome = resolve(
139        &origin,
140        HlsRequest::Playlist {
141            track_id: DEFAULT_TRACK_ID,
142            // Segment 2 is the in-progress one, so msn=3 is the next segment
143            // that has not closed. Keep this within
144            // `ABUSE_MSN_FUTURE_BOUND` (live edge + 2) — a larger value is
145            // rejected outright as abuse and never reaches the Await path
146            // this block is demonstrating.
147            query: BlockingQuery {
148                hls_msn: Some(3),
149                hls_part: None,
150            },
151        },
152    );
153    assert_eq!(outcome, EgressResponse::NotFound);
154    println!("resolve(Playlist, _HLS_msn=3)   -> NotFound (Await's patience already expired)");
155
156    // A `_HLS_msn` unreasonably far beyond the live edge is rejected outright
157    // (RFC 8216bis §6.2.5.2 abuse prevention) rather than ever Await-ing.
158    let outcome = resolve(
159        &origin,
160        HlsRequest::Playlist {
161            track_id: DEFAULT_TRACK_ID,
162            query: BlockingQuery {
163                hls_msn: Some(999),
164                hls_part: None,
165            },
166        },
167    );
168    assert!(matches!(outcome, EgressResponse::BadRequest { .. }));
169    println!("resolve(Playlist, _HLS_msn=999) -> BadRequest (abuse bound)");
170
171    // `Resource`: the init segment and the closed segment are Ready...
172    match resolve(
173        &origin,
174        HlsRequest::Resource {
175            name: "init-1.mp4".to_string(),
176        },
177    ) {
178        EgressResponse::Ready { .. } => println!("resolve(Resource, init-1.mp4)     -> Ready"),
179        other => panic!("expected Ready, got {other:?}"),
180    }
181    match resolve(
182        &origin,
183        HlsRequest::Resource {
184            name: "seg-1-1.m4s".to_string(),
185        },
186    ) {
187        EgressResponse::Ready { .. } => println!("resolve(Resource, seg-1-1.m4s)    -> Ready"),
188        other => panic!("expected Ready, got {other:?}"),
189    }
190    // ...a live part of the still-open segment is Ready too...
191    match resolve(
192        &origin,
193        HlsRequest::Resource {
194            name: "part-1-2.0.m4s".to_string(),
195        },
196    ) {
197        EgressResponse::Ready { .. } => println!("resolve(Resource, part-1-2.0.m4s) -> Ready"),
198        other => panic!("expected Ready, got {other:?}"),
199    }
200    // ...a preload-hinted part not yet produced reports NotFound once this
201    // call's `await_policy` has already expired (a real HTTP adapter would
202    // instead give it a real deadline and block on `Trunk::listen()`)...
203    match resolve(
204        &origin,
205        HlsRequest::Resource {
206            name: "part-1-2.1.m4s".to_string(),
207        },
208    ) {
209        EgressResponse::NotFound => {
210            println!(
211                "resolve(Resource, part-1-2.1.m4s) -> NotFound (Await's patience already expired)"
212            )
213        }
214        other => panic!("expected NotFound, got {other:?}"),
215    }
216    // ...and an unrecognised filename is a plain 404.
217    match resolve(
218        &origin,
219        HlsRequest::Resource {
220            name: "nope.txt".to_string(),
221        },
222    ) {
223        EgressResponse::NotFound => println!("resolve(Resource, nope.txt)       -> NotFound"),
224        other => panic!("expected NotFound, got {other:?}"),
225    }
226}
Source

pub fn init_bytes(&self) -> Option<Bytes>

The fMP4 init segment bytes, if set.

Source

pub fn closed_segments(&self) -> Vec<ClosedSegment>

A snapshot of this origin’s currently-advertised closed segments (drains the cursor first, same as render_playlist) — ascending by sequence number.

Exists for a caller that needs to merge this origin’s live window with a different source of segments over the same numbering (multimux’s DVR archive, issue #900: catch-up serving must present one continuous playlist spanning the archive and the still-live tail that hasn’t been archived yet). Reuses the one cursor drain already maintains rather than making the caller open a second cursor on the same Trunk just to learn the same window render_playlist itself renders — media_plane’s own module doc: writer cost is O(N) in cursor count, so a cursor is per distinct consumer, never per peer, and never duplicated for data another cursor already tracks.

Trait Implementations§

Source§

impl ServedEgress for HlsOrigin

Source§

type Request = HlsRequest

The protocol-specific request shape (e.g. LL-HLS’s Media Sequence Number + part index; a DASH segment/manifest request; a catch-up time range) — owned by the implementing crate, not this one.
Source§

type Body = HlsBody

The protocol-specific resolved body (a playlist string, manifest XML, segment bytes, …).
Source§

fn resolve( &self, request: HlsRequest, now: Timestamp, await_policy: AwaitPolicy, ) -> EgressResponse<HlsBody>

Resolve request against this implementation’s current state at time now, bounded by await_policy — see EgressResponse::Await’s contract.

Auto Trait Implementations§

Blanket Implementations§

Source§

impl<T> Any for T
where T: 'static + ?Sized,

Source§

fn type_id(&self) -> TypeId

Gets the TypeId of self. Read more
Source§

impl<T> Borrow<T> for T
where T: ?Sized,

Source§

fn borrow(&self) -> &T

Immutably borrows from an owned value. Read more
Source§

impl<T> BorrowMut<T> for T
where T: ?Sized,

Source§

fn borrow_mut(&mut self) -> &mut T

Mutably borrows from an owned value. Read more
Source§

impl<ST, DT> CastableFrom<ST, Initialized, Initialized> for DT
where ST: ?Sized, DT: ?Sized,

Source§

impl<ST, DT> CastableFrom<ST, Uninit, Uninit> for DT
where ST: ?Sized, DT: ?Sized,

Source§

impl<T> From<T> for T

Source§

fn from(t: T) -> T

Returns the argument unchanged.

Source§

impl<T> Instrument for T

Source§

fn instrument(self, span: Span) -> Instrumented<Self>

Instruments this type with the provided Span, returning an Instrumented wrapper. Read more
Source§

fn in_current_span(self) -> Instrumented<Self>

Instruments this type with the current Span, returning an Instrumented wrapper. Read more
Source§

impl<T, U> Into<U> for T
where U: From<T>,

Source§

fn into(self) -> U

Calls U::from(self).

That is, this conversion is whatever the implementation of From<T> for U chooses to do.

Source§

impl<T> PolicyExt for T
where T: ?Sized,

Source§

fn and<P, B, E>(self, other: P) -> And<T, P>
where T: Sized + Policy<B, E>, P: Policy<B, E>,

Create a new Policy that returns Action::Follow only if self and other return Action::Follow. Read more
Source§

fn or<P, B, E>(self, other: P) -> Or<T, P>
where T: Sized + Policy<B, E>, P: Policy<B, E>,

Create a new Policy that returns Action::Follow if either self or other returns Action::Follow. Read more
Source§

impl<T> Read<Exclusive, BecauseExclusive> for T
where T: ?Sized,

Source§

impl<T> Same for T

Source§

type Output = T

Should always be Self
Source§

impl<T, U> TryFrom<U> for T
where U: Into<T>,

Source§

type Error = Infallible

The type returned in the event of a conversion error.
Source§

fn try_from(value: U) -> Result<T, <T as TryFrom<U>>::Error>

Performs the conversion.
Source§

impl<T, U> TryInto<U> for T
where U: TryFrom<T>,

Source§

type Error = <U as TryFrom<T>>::Error

The type returned in the event of a conversion error.
Source§

fn try_into(self) -> Result<U, <U as TryFrom<T>>::Error>

Performs the conversion.
Source§

impl<V, T> VZip<V> for T
where V: MultiLane<T>,

Source§

fn vzip(self) -> V

Source§

impl<T> WithSubscriber for T

Source§

fn with_subscriber<S>(self, subscriber: S) -> WithDispatch<Self>
where S: Into<Dispatch>,

Attaches the provided Subscriber to this type, returning a WithDispatch wrapper. Read more
Source§

fn with_current_subscriber(self) -> WithDispatch<Self>

Attaches the current default Subscriber to this type, returning a WithDispatch wrapper. Read more