pub struct HlsOrigin { /* private fields */ }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
impl HlsOrigin
Sourcepub fn builder(trunk: Arc<Trunk>) -> HlsOriginBuilder
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?
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
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}Sourcepub fn set_init(&self, bytes: impl Into<Bytes>)
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?
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
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}Sourcepub fn init_bytes(&self) -> Option<Bytes>
pub fn init_bytes(&self) -> Option<Bytes>
The fMP4 init segment bytes, if set.
Sourcepub fn closed_segments(&self) -> Vec<ClosedSegment>
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
impl ServedEgress for HlsOrigin
Source§type Request = HlsRequest
type Request = HlsRequest
Source§type Body = HlsBody
type Body = HlsBody
Source§fn resolve(
&self,
request: HlsRequest,
now: Timestamp,
await_policy: AwaitPolicy,
) -> EgressResponse<HlsBody>
fn resolve( &self, request: HlsRequest, now: Timestamp, await_policy: AwaitPolicy, ) -> EgressResponse<HlsBody>
request against this implementation’s current state at
time now, bounded by await_policy — see
EgressResponse::Await’s contract.