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 query: BlockingQuery {
143 hls_msn: Some(5),
144 hls_part: None,
145 },
146 },
147 );
148 assert_eq!(outcome, EgressResponse::NotFound);
149 println!("resolve(Playlist, _HLS_msn=5) -> NotFound (Await's patience already expired)");
150
151 // A `_HLS_msn` unreasonably far beyond the live edge is rejected outright
152 // (RFC 8216bis §6.2.5.2 abuse prevention) rather than ever Await-ing.
153 let outcome = resolve(
154 &origin,
155 HlsRequest::Playlist {
156 track_id: DEFAULT_TRACK_ID,
157 query: BlockingQuery {
158 hls_msn: Some(999),
159 hls_part: None,
160 },
161 },
162 );
163 assert!(matches!(outcome, EgressResponse::BadRequest { .. }));
164 println!("resolve(Playlist, _HLS_msn=999) -> BadRequest (abuse bound)");
165
166 // `Resource`: the init segment and the closed segment are Ready...
167 match resolve(
168 &origin,
169 HlsRequest::Resource {
170 name: "init-1.mp4".to_string(),
171 },
172 ) {
173 EgressResponse::Ready { .. } => println!("resolve(Resource, init-1.mp4) -> Ready"),
174 other => panic!("expected Ready, got {other:?}"),
175 }
176 match resolve(
177 &origin,
178 HlsRequest::Resource {
179 name: "seg-1-1.m4s".to_string(),
180 },
181 ) {
182 EgressResponse::Ready { .. } => println!("resolve(Resource, seg-1-1.m4s) -> Ready"),
183 other => panic!("expected Ready, got {other:?}"),
184 }
185 // ...a live part of the still-open segment is Ready too...
186 match resolve(
187 &origin,
188 HlsRequest::Resource {
189 name: "part-1-2.0.m4s".to_string(),
190 },
191 ) {
192 EgressResponse::Ready { .. } => println!("resolve(Resource, part-1-2.0.m4s) -> Ready"),
193 other => panic!("expected Ready, got {other:?}"),
194 }
195 // ...a preload-hinted part not yet produced reports NotFound once this
196 // call's `await_policy` has already expired (a real HTTP adapter would
197 // instead give it a real deadline and block on `Trunk::listen()`)...
198 match resolve(
199 &origin,
200 HlsRequest::Resource {
201 name: "part-1-2.1.m4s".to_string(),
202 },
203 ) {
204 EgressResponse::NotFound => {
205 println!(
206 "resolve(Resource, part-1-2.1.m4s) -> NotFound (Await's patience already expired)"
207 )
208 }
209 other => panic!("expected NotFound, got {other:?}"),
210 }
211 // ...and an unrecognised filename is a plain 404.
212 match resolve(
213 &origin,
214 HlsRequest::Resource {
215 name: "nope.txt".to_string(),
216 },
217 ) {
218 EgressResponse::NotFound => println!("resolve(Resource, nope.txt) -> NotFound"),
219 other => panic!("expected NotFound, got {other:?}"),
220 }
221}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 query: BlockingQuery {
143 hls_msn: Some(5),
144 hls_part: None,
145 },
146 },
147 );
148 assert_eq!(outcome, EgressResponse::NotFound);
149 println!("resolve(Playlist, _HLS_msn=5) -> NotFound (Await's patience already expired)");
150
151 // A `_HLS_msn` unreasonably far beyond the live edge is rejected outright
152 // (RFC 8216bis §6.2.5.2 abuse prevention) rather than ever Await-ing.
153 let outcome = resolve(
154 &origin,
155 HlsRequest::Playlist {
156 track_id: DEFAULT_TRACK_ID,
157 query: BlockingQuery {
158 hls_msn: Some(999),
159 hls_part: None,
160 },
161 },
162 );
163 assert!(matches!(outcome, EgressResponse::BadRequest { .. }));
164 println!("resolve(Playlist, _HLS_msn=999) -> BadRequest (abuse bound)");
165
166 // `Resource`: the init segment and the closed segment are Ready...
167 match resolve(
168 &origin,
169 HlsRequest::Resource {
170 name: "init-1.mp4".to_string(),
171 },
172 ) {
173 EgressResponse::Ready { .. } => println!("resolve(Resource, init-1.mp4) -> Ready"),
174 other => panic!("expected Ready, got {other:?}"),
175 }
176 match resolve(
177 &origin,
178 HlsRequest::Resource {
179 name: "seg-1-1.m4s".to_string(),
180 },
181 ) {
182 EgressResponse::Ready { .. } => println!("resolve(Resource, seg-1-1.m4s) -> Ready"),
183 other => panic!("expected Ready, got {other:?}"),
184 }
185 // ...a live part of the still-open segment is Ready too...
186 match resolve(
187 &origin,
188 HlsRequest::Resource {
189 name: "part-1-2.0.m4s".to_string(),
190 },
191 ) {
192 EgressResponse::Ready { .. } => println!("resolve(Resource, part-1-2.0.m4s) -> Ready"),
193 other => panic!("expected Ready, got {other:?}"),
194 }
195 // ...a preload-hinted part not yet produced reports NotFound once this
196 // call's `await_policy` has already expired (a real HTTP adapter would
197 // instead give it a real deadline and block on `Trunk::listen()`)...
198 match resolve(
199 &origin,
200 HlsRequest::Resource {
201 name: "part-1-2.1.m4s".to_string(),
202 },
203 ) {
204 EgressResponse::NotFound => {
205 println!(
206 "resolve(Resource, part-1-2.1.m4s) -> NotFound (Await's patience already expired)"
207 )
208 }
209 other => panic!("expected NotFound, got {other:?}"),
210 }
211 // ...and an unrecognised filename is a plain 404.
212 match resolve(
213 &origin,
214 HlsRequest::Resource {
215 name: "nope.txt".to_string(),
216 },
217 ) {
218 EgressResponse::NotFound => println!("resolve(Resource, nope.txt) -> NotFound"),
219 other => panic!("expected NotFound, got {other:?}"),
220 }
221}Sourcepub fn init_bytes(&self) -> Option<Bytes>
pub fn init_bytes(&self) -> Option<Bytes>
The fMP4 init segment bytes, if set.
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.