1use std::collections::{HashMap, HashSet};
13
14use crate::address::template::match_url;
15use crate::mpd::{Mpd, SegmentTemplate};
16use broadcast_common::impl_spec_display;
17use broadcast_common::units::MICROS_PER_SEC;
18use http::StatusCode;
19
20#[derive(Debug, Clone, Copy, PartialEq, Eq)]
22#[non_exhaustive]
23pub enum IngestMethod {
24 Post,
25 Put,
26}
27
28impl IngestMethod {
29 pub fn name(&self) -> &'static str {
30 match self {
31 IngestMethod::Post => "POST",
32 IngestMethod::Put => "PUT",
33 }
34 }
35}
36
37impl_spec_display!(IngestMethod);
38
39#[derive(Debug, Clone, Copy, PartialEq, Eq)]
41#[non_exhaustive]
42pub enum ManifestFormat {
43 Dash,
44 Hls,
45}
46
47impl ManifestFormat {
48 pub fn name(&self) -> &'static str {
49 match self {
50 ManifestFormat::Dash => "dash",
51 ManifestFormat::Hls => "hls",
52 }
53 }
54}
55
56impl_spec_display!(ManifestFormat);
57
58#[derive(Debug, Clone, Copy, PartialEq, Eq)]
60#[non_exhaustive]
61pub enum Addressing {
62 Number,
63 Time,
64}
65
66impl Addressing {
67 pub fn name(&self) -> &'static str {
68 match self {
69 Addressing::Number => "number",
70 Addressing::Time => "time",
71 }
72 }
73}
74
75impl_spec_display!(Addressing);
76
77#[derive(Debug, Clone, PartialEq, Eq)]
79#[non_exhaustive]
80pub enum ObjectKind {
81 Probe,
82 Manifest(ManifestFormat),
83 Header {
84 representation_id: String,
85 },
86 Segment {
87 representation_id: String,
88 number: Option<u64>,
89 time: Option<u64>,
90 },
91}
92
93impl ObjectKind {
94 pub fn name(&self) -> &'static str {
95 match self {
96 ObjectKind::Probe => "probe",
97 ObjectKind::Manifest(_) => "manifest",
98 ObjectKind::Header { .. } => "header",
99 ObjectKind::Segment { .. } => "segment",
100 }
101 }
102}
103
104impl_spec_display!(ObjectKind);
105
106#[derive(Debug, Clone, Copy, PartialEq, Eq)]
108#[non_exhaustive]
109pub enum SegmentDisposition {
110 Accepted,
111 Duplicate,
112 Regression,
113}
114
115impl SegmentDisposition {
116 pub fn name(&self) -> &'static str {
117 match self {
118 SegmentDisposition::Accepted => "accepted",
119 SegmentDisposition::Duplicate => "duplicate",
120 SegmentDisposition::Regression => "regression",
121 }
122 }
123}
124
125impl_spec_display!(SegmentDisposition);
126
127#[derive(Debug, Clone, Copy)]
129pub struct RequestMeta<'a> {
130 pub method: IngestMethod,
131 pub path: &'a str,
132 pub content_length: Option<u64>,
133 pub chunked: bool,
134 pub body_empty: bool,
135}
136
137#[derive(Debug, Clone, PartialEq, Eq)]
139pub struct ManifestGrouping {
140 pub init_template: String,
141 pub media_template: String,
142 pub addressing: Addressing,
143 pub switching_sets: Vec<Vec<String>>,
144 pub availability_start_time: Option<String>,
146 pub period_start: Option<std::time::Duration>,
148}
149
150#[derive(Debug, Clone, thiserror::Error, PartialEq, Eq)]
152#[non_exhaustive]
153pub enum IngestError {
154 #[error("segment without a usable header (§5.3.5c)")]
155 SegmentWithoutHeader,
156
157 #[error("receiver-side fault (§5.3.5d)")]
158 Internal,
159
160 #[error("malformed request (§5.3.5e)")]
161 BadRequest,
162
163 #[error("no manifest received yet")]
164 NoManifest,
165
166 #[error("MPD has multiple Period elements (§6.2.16h)")]
167 MpdMultiplePeriods,
168
169 #[error("MPD SegmentTemplate {attr} template mismatch (§6.2.16a/b/c)")]
170 MpdTemplateMismatch { attr: &'static str },
171
172 #[error("MPD contains a BaseURL element (§6.2.16d)")]
173 MpdBaseUrlPresent,
174
175 #[error("MPD AdaptationSet/Representation lacks a SegmentTemplate (§6.2.16 intro)")]
176 MpdMissingSegmentTemplate,
177
178 #[error("header changed mid-session (§6.2.19)")]
179 HeaderChanged,
180
181 #[error("segment already in progress for this representation (§5.3 catch-all)")]
182 SegmentInProgress,
183}
184
185impl IngestError {
186 pub fn status(&self) -> StatusCode {
187 match self {
188 IngestError::SegmentWithoutHeader => StatusCode::PRECONDITION_FAILED,
189 IngestError::Internal => StatusCode::INTERNAL_SERVER_ERROR,
190 IngestError::SegmentInProgress => StatusCode::CONFLICT,
191 _ => StatusCode::BAD_REQUEST,
192 }
193 }
194}
195
196impl ManifestGrouping {
197 pub fn from_mpd(bytes: &[u8]) -> Result<Self, IngestError> {
198 let mpd = Mpd::parse(bytes).map_err(|_| IngestError::BadRequest)?;
199
200 if mpd.periods.len() != 1 {
201 return Err(IngestError::MpdMultiplePeriods);
202 }
203 let period = &mpd.periods[0];
204
205 if !mpd.base_urls.is_empty()
207 || !period.base_urls.is_empty()
208 || mpd
209 .periods
210 .iter()
211 .flat_map(|p| &p.adaptation_sets)
212 .any(|a| {
213 !a.base_urls.is_empty()
214 || a.representations.iter().any(|r| !r.base_urls.is_empty())
215 })
216 {
217 return Err(IngestError::MpdBaseUrlPresent);
218 }
219
220 let mut templates: Vec<(String, &SegmentTemplate)> = Vec::new();
222 let mut switching_sets: Vec<Vec<String>> = Vec::new();
223 for aset in &period.adaptation_sets {
224 let mut set_ids: Vec<String> = Vec::new();
225 for rep in &aset.representations {
226 let tmpl = rep
227 .segment
228 .template
229 .as_ref()
230 .or(aset.segment.template.as_ref());
231 let tmpl = match tmpl {
232 Some(t) => t,
233 None => return Err(IngestError::MpdMissingSegmentTemplate),
234 };
235 templates.push((rep.id.clone(), tmpl));
236 set_ids.push(rep.id.clone());
237 }
238 if !set_ids.is_empty() {
239 switching_sets.push(set_ids);
240 }
241 }
243
244 if templates.is_empty() {
245 return Err(IngestError::MpdMissingSegmentTemplate);
246 }
247
248 let first_init = templates[0].1.initialization.clone();
250 let first_media = templates[0].1.media.clone();
251 for (_, t) in &templates {
252 if t.initialization != first_init {
253 return Err(IngestError::MpdTemplateMismatch {
254 attr: "initialization",
255 });
256 }
257 if t.media != first_media {
258 return Err(IngestError::MpdTemplateMismatch { attr: "media" });
259 }
260 }
261
262 let init_template = first_init.unwrap_or_default();
263 let media_template = first_media.unwrap_or_default();
264
265 if init_template.matches("$RepresentationID$").count() != 1 {
267 return Err(IngestError::MpdTemplateMismatch {
268 attr: "initialization",
269 });
270 }
271 if media_template.matches("$RepresentationID$").count() != 1 {
273 return Err(IngestError::MpdTemplateMismatch { attr: "media" });
274 }
275 let number_count = count_ident(&media_template, "Number");
277 let time_count = count_ident(&media_template, "Time");
278 let has_number = number_count > 0;
279 let has_time = time_count > 0;
280 if !(has_number ^ has_time) {
281 return Err(IngestError::MpdTemplateMismatch { attr: "media" });
282 }
283 if has_number && number_count != 1 {
284 return Err(IngestError::MpdTemplateMismatch { attr: "media" });
285 }
286 if has_time && time_count != 1 {
287 return Err(IngestError::MpdTemplateMismatch { attr: "media" });
288 }
289
290 let addressing = if has_number {
291 Addressing::Number
292 } else {
293 Addressing::Time
294 };
295
296 Ok(ManifestGrouping {
297 init_template,
298 media_template,
299 addressing,
300 switching_sets,
301 availability_start_time: mpd
302 .availability_start_time_us
303 .and_then(|us| us.checked_div(MICROS_PER_SEC as i64))
304 .and_then(|s| jiff::Timestamp::from_second(s).ok())
305 .map(|t| t.to_string()),
306 period_start: period.start,
307 })
308 }
309}
310
311fn count_ident(template: &str, name: &str) -> usize {
312 let mut count = 0;
314 let mut rest = template;
315 while let Some(start) = rest.find('$') {
316 let tail = &rest[start + 1..];
317 if let Some(end) = tail.find('$') {
318 let ident = &tail[..end];
319 let id_name = ident.split('%').next().unwrap_or(ident);
320 if id_name == name {
321 count += 1;
322 }
323 rest = &tail[end + 1..];
324 } else {
325 break;
326 }
327 }
328 count
329}
330
331pub fn classify(
332 req: &RequestMeta<'_>,
333 grouping: Option<&ManifestGrouping>,
334) -> Result<ObjectKind, IngestError> {
335 if req.body_empty {
336 return Ok(ObjectKind::Probe);
337 }
338 let lower = req.path.to_ascii_lowercase();
339 if lower.ends_with(".mpd") {
340 return Ok(ObjectKind::Manifest(ManifestFormat::Dash));
341 }
342 if lower.ends_with(".m3u8") {
343 return Ok(ObjectKind::Manifest(ManifestFormat::Hls));
344 }
345
346 let g = grouping.ok_or(IngestError::NoManifest)?;
347
348 let known: Vec<&str> = g
349 .switching_sets
350 .iter()
351 .flatten()
352 .map(|s| s.as_str())
353 .collect();
354
355 if let Some(m) = match_url(&g.init_template, req.path, &known) {
356 return Ok(ObjectKind::Header {
357 representation_id: m.representation_id,
358 });
359 }
360 if let Some(m) = match_url(&g.media_template, req.path, &known) {
361 return Ok(ObjectKind::Segment {
362 representation_id: m.representation_id,
363 number: m.number,
364 time: m.time,
365 });
366 }
367
368 Err(IngestError::BadRequest)
369}
370
371#[derive(Debug, Default)]
373pub struct IngestState {
374 headers: HashSet<String>,
375 last_number: HashMap<String, u64>,
376 last_time: HashMap<String, u64>,
377 last_segments: HashSet<String>,
378}
379
380impl IngestState {
381 pub fn new() -> Self {
382 Self::default()
383 }
384
385 pub fn on_header(&mut self, id: &str) {
386 self.headers.insert(id.to_string());
387 self.last_number.remove(id);
389 self.last_time.remove(id);
390 self.last_segments.remove(id);
391 }
392
393 pub fn has_header(&self, id: &str) -> bool {
394 self.headers.contains(id)
395 }
396
397 pub fn on_segment(
398 &mut self,
399 id: &str,
400 number: Option<u64>,
401 time: Option<u64>,
402 ) -> Result<SegmentDisposition, IngestError> {
403 if !self.headers.contains(id) {
404 return Err(IngestError::SegmentWithoutHeader);
405 }
406
407 let mut regression = false;
408 let mut duplicate = true;
409 let mut has_comparable = false;
410 let mut any = false;
411
412 if let Some(n) = number {
413 any = true;
414 if let Some(ln) = self.last_number.get(id) {
415 has_comparable = true;
416 if n < *ln {
417 regression = true;
418 }
419 if n != *ln {
420 duplicate = false;
421 }
422 } else {
423 duplicate = false;
424 }
425 }
426 if let Some(t) = time {
427 any = true;
428 if let Some(lt) = self.last_time.get(id) {
429 has_comparable = true;
430 if t < *lt {
431 regression = true;
432 }
433 if t != *lt {
434 duplicate = false;
435 }
436 } else {
437 duplicate = false;
438 }
439 }
440 if !any {
441 duplicate = false;
442 }
443
444 let disposition = if regression {
445 SegmentDisposition::Regression
446 } else if has_comparable && duplicate {
447 SegmentDisposition::Duplicate
448 } else {
449 SegmentDisposition::Accepted
450 };
451
452 if let Some(n) = number {
453 self.last_number.insert(id.to_string(), n);
454 }
455 if let Some(t) = time {
456 self.last_time.insert(id.to_string(), t);
457 }
458
459 Ok(disposition)
460 }
461
462 pub fn mark_last(&mut self, id: &str) {
463 self.last_segments.insert(id.to_string());
464 }
465
466 pub fn is_last(&self, id: &str) -> bool {
467 self.last_segments.contains(id)
468 }
469}
470
471#[cfg(test)]
472mod tests {
473 use super::*;
474
475 #[test]
476 fn status_mapping_exhaustive() {
477 fn expected(e: &IngestError) -> u16 {
479 match e {
480 IngestError::SegmentWithoutHeader => 412,
481 IngestError::Internal => 500,
482 IngestError::BadRequest => 400,
483 IngestError::NoManifest => 400,
484 IngestError::MpdMultiplePeriods => 400,
485 IngestError::MpdTemplateMismatch { .. } => 400,
486 IngestError::MpdBaseUrlPresent => 400,
487 IngestError::MpdMissingSegmentTemplate => 400,
488 IngestError::HeaderChanged => 400,
489 IngestError::SegmentInProgress => 409,
490 }
491 }
492 let variants = [
493 IngestError::SegmentWithoutHeader,
494 IngestError::Internal,
495 IngestError::BadRequest,
496 IngestError::NoManifest,
497 IngestError::MpdMultiplePeriods,
498 IngestError::MpdTemplateMismatch { attr: "media" },
499 IngestError::MpdBaseUrlPresent,
500 IngestError::MpdMissingSegmentTemplate,
501 IngestError::HeaderChanged,
502 IngestError::SegmentInProgress,
503 ];
504 for v in &variants {
505 assert_eq!(v.status().as_u16(), expected(v));
506 }
507 }
508}
509
510pub mod session;
511pub use session::{
512 DEFAULT_BUFFERED_BUDGET_BYTES, DEFAULT_MAX_BUFFERED_OBJECT_BYTES, DEFAULT_MAX_OBJECT_BYTES,
513 DashifIngestConfig, DashifIngestSession, IngestObject, SegmentHandle, SessionError,
514};