1use std::collections::{HashMap, HashSet, VecDeque};
25
26use broadcast_common::{Demand, Stage, Timestamp, Unpackage};
27use media_plane::ingress::{IngestSession, ProgramId, SessionEvent};
28use media_plane::trunk::RetentionClass;
29use transmux::TrackId;
30use transmux::media::Fmp4Demux;
31use transmux::pipeline::TrackSpec;
32
33use super::{IngestError, IngestState, ManifestGrouping, ObjectKind};
34
35use tracing::{debug, warn};
36
37const MAX_TS_READ: usize = 65_536;
38
39#[derive(Debug)]
42pub struct IngestObject<'a> {
43 pub kind: ObjectKind,
45 pub bytes: &'a [u8],
47}
48
49#[derive(Debug, Clone, thiserror::Error, PartialEq, Eq)]
54#[non_exhaustive]
55pub enum SessionError {
56 #[error("ingest: {0}")]
58 Ingest(#[from] IngestError),
59 #[error("demux: {0}")]
61 Demux(String),
62}
63
64#[derive(Default)]
65struct RepData {
66 header_bytes: Option<Vec<u8>>,
67 local_tracks: Option<Vec<TrackSpec>>,
68 init_bytes: Option<Vec<u8>>,
69 local_to_global: Option<HashMap<TrackId, TrackId>>,
70 buffered: Vec<(TrackId, transmux::ir::Sample)>,
71}
72
73#[derive(Debug, Clone)]
77pub struct SegmentHandle {
78 rep_id: String,
79 skip: bool,
80 buf: Vec<u8>,
81}
82
83pub const DEFAULT_MAX_OBJECT_BYTES: usize = 64 * 1024 * 1024;
85
86pub const DEFAULT_MAX_BUFFERED_OBJECT_BYTES: usize = 8 * 1024 * 1024;
88
89pub const DEFAULT_BUFFERED_BUDGET_BYTES: usize = 64 * 1024 * 1024;
91
92#[derive(Debug, Clone, Copy, PartialEq, Eq)]
95#[non_exhaustive]
96pub struct DashifIngestConfig {
97 pub max_object_bytes: usize,
99 pub max_buffered_object_bytes: usize,
104 pub buffered_budget_bytes: usize,
107}
108
109impl Default for DashifIngestConfig {
110 fn default() -> Self {
111 DashifIngestConfig {
112 max_object_bytes: DEFAULT_MAX_OBJECT_BYTES,
113 max_buffered_object_bytes: DEFAULT_MAX_BUFFERED_OBJECT_BYTES,
114 buffered_budget_bytes: DEFAULT_BUFFERED_BUDGET_BYTES,
115 }
116 }
117}
118
119impl DashifIngestConfig {
120 #[must_use]
122 pub fn with_max_object_bytes(mut self, max_object_bytes: usize) -> Self {
123 self.max_object_bytes = max_object_bytes;
124 self
125 }
126
127 #[must_use]
129 pub fn with_max_buffered_object_bytes(mut self, max_buffered_object_bytes: usize) -> Self {
130 self.max_buffered_object_bytes = max_buffered_object_bytes;
131 self
132 }
133
134 #[must_use]
136 pub fn with_buffered_budget_bytes(mut self, buffered_budget_bytes: usize) -> Self {
137 self.buffered_budget_bytes = buffered_budget_bytes;
138 self
139 }
140}
141
142pub struct DashifIngestSession {
144 config: DashifIngestConfig,
145 grouping: Option<ManifestGrouping>,
146 rep_order: Vec<String>,
147 reps: HashMap<String, RepData>,
148 ingest_state: IngestState,
149 established_emitted: bool,
150 new_program_emitted: bool,
151 pending_events: VecDeque<SessionEvent>,
152 active_segments: HashSet<String>,
153}
154
155impl DashifIngestSession {
156 pub fn grouping(&self) -> Option<ManifestGrouping> {
158 self.grouping.clone()
159 }
160
161 pub fn config(&self) -> DashifIngestConfig {
163 self.config
164 }
165
166 pub fn new(config: DashifIngestConfig) -> Self {
169 Self {
170 config,
171 grouping: None,
172 rep_order: Vec::new(),
173 reps: HashMap::new(),
174 ingest_state: IngestState::new(),
175 established_emitted: false,
176 new_program_emitted: false,
177 pending_events: VecDeque::new(),
178 active_segments: HashSet::new(),
179 }
180 }
181
182 fn on_header(&mut self, rep_id: &str, bytes: &[u8]) -> Result<(), SessionError> {
183 if self.grouping.is_none() {
184 return Err(SessionError::Ingest(IngestError::NoManifest));
185 }
186 if !self.reps.contains_key(rep_id) {
187 return Err(SessionError::Ingest(IngestError::BadRequest));
188 }
189
190 let is_identical = self
192 .reps
193 .get(rep_id)
194 .and_then(|r| r.header_bytes.as_ref())
195 .map(|prev| prev == bytes)
196 .unwrap_or(false);
197 if is_identical {
198 debug!("dashif-ingest: identical header repeat ignored for {rep_id}");
199 self.ingest_state.on_header(rep_id);
200 return Ok(());
201 }
202
203 if self.new_program_emitted
205 && self
206 .reps
207 .get(rep_id)
208 .is_some_and(|r| r.header_bytes.is_some())
209 {
210 return Err(SessionError::Ingest(IngestError::HeaderChanged));
211 }
212
213 let media = Fmp4Demux::new()
215 .unpackage(bytes)
216 .map_err(|e| SessionError::Demux(e.to_string()))?;
217 if media.tracks.is_empty() {
218 return Err(SessionError::Ingest(IngestError::BadRequest));
219 }
220 let local_tracks: Vec<TrackSpec> = media.tracks.into_iter().map(|t| t.spec).collect();
221
222 let rep = self
223 .reps
224 .get_mut(rep_id)
225 .ok_or(SessionError::Ingest(IngestError::Internal))?;
226 rep.header_bytes = Some(bytes.to_vec());
227 rep.init_bytes = Some(bytes.to_vec());
228 rep.local_tracks = Some(local_tracks);
229
230 self.ingest_state.on_header(rep_id);
231
232 if !self.new_program_emitted
234 && self.rep_order.iter().all(|id| {
235 self.reps
236 .get(id)
237 .map(|r| r.header_bytes.is_some())
238 .unwrap_or(false)
239 })
240 {
241 self.emit_new_program();
242 self.flush_buffered();
243 }
244
245 Ok(())
246 }
247
248 fn emit_new_program(&mut self) {
249 let mut next_track_id = TrackId::new(1);
250 let mut specs = Vec::new();
251 for rep_id in self.rep_order.clone() {
252 let Some(rep) = self.reps.get_mut(&rep_id) else {
253 continue;
254 };
255 let local_tracks = rep.local_tracks.clone().unwrap_or_default();
256 let mut l2g = HashMap::new();
257 for spec in &local_tracks {
258 let global_id = next_track_id;
259 next_track_id = next_track_id.next();
260 l2g.insert(spec.track_id, global_id);
261 let mut remapped = spec.clone();
262 remapped.track_id = global_id;
263 specs.push(remapped);
264 }
265 rep.local_to_global = Some(l2g);
266 }
267 self.pending_events.push_back(SessionEvent::NewProgram {
268 program: ProgramId(0),
269 tracks: specs,
270 });
271 self.new_program_emitted = true;
272 }
273
274 fn flush_buffered(&mut self) {
275 for rep_id in self.rep_order.clone() {
276 let Some(rep) = self.reps.get_mut(&rep_id) else {
277 continue;
278 };
279 let l2g = match &rep.local_to_global {
280 Some(m) => m.clone(),
281 None => continue,
282 };
283 let buffered = std::mem::take(&mut rep.buffered);
284 for (local_id, sample) in buffered {
285 if let Some(&global_id) = l2g.get(&local_id) {
286 self.pending_events.push_back(SessionEvent::Sample {
287 program: ProgramId(0),
288 track_id: global_id,
289 retention: RetentionClass::Timed,
290 sample,
291 });
292 }
293 }
294 }
295 }
296
297 fn on_segment(
298 &mut self,
299 rep_id: &str,
300 number: Option<u64>,
301 time: Option<u64>,
302 bytes: &[u8],
303 ) -> Result<(), SessionError> {
304 let mut handle = self.begin_segment(rep_id, number, time)?;
305 if let Err(e) = self.push_segment_bytes(&mut handle, bytes) {
306 self.active_segments.remove(rep_id);
307 let _ = handle;
309 return Err(e);
310 }
311 self.finish_segment(handle)
312 }
313
314 pub fn begin_segment(
315 &mut self,
316 rep: impl AsRef<str>,
317 number: Option<u64>,
318 time: Option<u64>,
319 ) -> Result<SegmentHandle, SessionError> {
320 let rep_id = rep.as_ref();
321 if self.grouping.is_none() {
322 return Err(SessionError::Ingest(IngestError::NoManifest));
323 }
324 if !self.reps.contains_key(rep_id) {
325 return Err(SessionError::Ingest(IngestError::BadRequest));
326 }
327 if !self.ingest_state.has_header(rep_id) {
328 return Err(SessionError::Ingest(IngestError::SegmentWithoutHeader));
329 }
330 if self.active_segments.contains(rep_id) {
331 return Err(SessionError::Ingest(IngestError::SegmentInProgress));
332 }
333 let disp = self
334 .ingest_state
335 .on_segment(rep_id, number, time)
336 .map_err(SessionError::Ingest)?;
337
338 if disp == super::SegmentDisposition::Regression {
339 warn!("dashif-ingest: segment regression for {rep_id}");
340 }
341
342 self.active_segments.insert(rep_id.to_string());
343
344 Ok(SegmentHandle {
345 rep_id: rep_id.to_string(),
346 skip: disp == super::SegmentDisposition::Duplicate,
347 buf: Vec::new(),
348 })
349 }
350
351 pub fn push_segment_bytes(
352 &mut self,
353 handle: &mut SegmentHandle,
354 bytes: &[u8],
355 ) -> Result<(), SessionError> {
356 handle.buf.extend_from_slice(bytes);
357
358 if handle.skip {
359 loop {
362 let mut i = 0usize;
363 let mut end = None;
364 while i < handle.buf.len() {
365 match transmux::parse_box(&handle.buf[i..]) {
366 Ok((bx, consumed)) => {
367 i += consumed;
368 if bx.header.box_type.is(b"mdat") {
369 end = Some(i);
370 break;
371 }
372 if consumed == 0 {
373 break;
374 }
375 }
376 Err(_) => break,
377 }
378 }
379 match end {
380 Some(e) => {
381 self.detect_styp_lmsg(&handle.rep_id, &handle.buf[..e]);
382 handle.buf.drain(..e);
383 }
384 None => break,
385 }
386 }
387 return Ok(());
388 }
389
390 loop {
391 let mut i = 0usize;
392 let mut end = None;
393 while i < handle.buf.len() {
394 match transmux::parse_box(&handle.buf[i..]) {
395 Ok((bx, consumed)) => {
396 i += consumed;
397 if bx.header.box_type.is(b"mdat") {
398 end = Some(i);
399 break;
400 }
401 if consumed == 0 {
402 break;
403 }
404 }
405 Err(_) => break,
406 }
407 }
408 let Some(e) = end else {
409 break;
410 };
411
412 let group = handle.buf[..e].to_vec();
413 self.detect_styp_lmsg(&handle.rep_id, &group);
414
415 let init = self
417 .reps
418 .get(&handle.rep_id)
419 .and_then(|r| r.init_bytes.clone())
420 .ok_or(SessionError::Ingest(IngestError::SegmentWithoutHeader))?;
421 let mut combined = Vec::with_capacity(init.len() + group.len());
422 combined.extend_from_slice(&init);
423 combined.extend_from_slice(&group);
424 let media = Fmp4Demux::new()
425 .unpackage(&combined)
426 .map_err(|e| SessionError::Demux(e.to_string()))?;
427
428 if self.new_program_emitted {
429 let l2g = self
430 .reps
431 .get(&handle.rep_id)
432 .and_then(|r| r.local_to_global.clone())
433 .unwrap_or_default();
434 for track in media.tracks {
435 if let Some(&global_id) = l2g.get(&track.spec.track_id) {
436 for sample in track.samples {
437 self.pending_events.push_back(SessionEvent::Sample {
438 program: ProgramId(0),
439 track_id: global_id,
440 retention: RetentionClass::Timed,
441 sample,
442 });
443 }
444 }
445 }
446 } else {
447 let new_count: usize = media.tracks.iter().map(|t| t.samples.len()).sum();
448 let rep = self
449 .reps
450 .get_mut(&handle.rep_id)
451 .ok_or(SessionError::Ingest(IngestError::Internal))?;
452 if rep.buffered.len() + new_count > 64 {
453 return Err(SessionError::Ingest(IngestError::Internal));
454 }
455 for track in media.tracks {
456 for sample in track.samples {
457 rep.buffered.push((track.spec.track_id, sample));
458 }
459 }
460 }
461
462 handle.buf.drain(..e);
463 }
464
465 Ok(())
466 }
467
468 pub fn finish_segment(&mut self, mut handle: SegmentHandle) -> Result<(), SessionError> {
469 let push_res = self.push_segment_bytes(&mut handle, &[]);
470 let rep_id = handle.rep_id.clone();
471 if let Err(e) = push_res {
472 self.active_segments.remove(&rep_id);
473 return Err(e);
474 }
475
476 if handle.skip {
477 self.active_segments.remove(&rep_id);
478 return Ok(());
479 }
480
481 if !handle.buf.is_empty() {
482 self.active_segments.remove(&rep_id);
483 return Err(SessionError::Ingest(IngestError::BadRequest));
484 }
485 self.active_segments.remove(&rep_id);
486 Ok(())
487 }
488
489 pub fn abort_segment(&mut self, handle: SegmentHandle) {
492 self.active_segments.remove(&handle.rep_id);
493 }
495
496 fn detect_styp_lmsg(&mut self, rep_id: &str, bytes: &[u8]) {
497 if let Ok((boxref, _)) = transmux::parse_box(bytes)
498 && boxref.header.box_type.is(b"styp")
499 {
500 let body = boxref.body;
501 if body.len() >= 8 {
502 let mut i = 8usize;
503 let mut slat_count = 0usize;
504 while i + 4 <= body.len() {
505 let brand = &body[i..i + 4];
506 if brand == b"lmsg" {
507 self.ingest_state.mark_last(rep_id);
508 } else if brand == b"slat" {
509 slat_count += 1;
510 }
511 i += 4;
512 }
513 if slat_count > 0 {
514 debug!("dashif-ingest: slat brand count {slat_count} for {rep_id}");
515 }
516 }
517 }
518 }
519}
520
521impl Stage for DashifIngestSession {
522 type In<'a> = IngestObject<'a>;
523 type Out = SessionEvent;
524 type Error = SessionError;
525
526 fn feed(&mut self, input: Self::In<'_>, _now: Timestamp) -> Result<(), Self::Error> {
527 match input.kind {
528 ObjectKind::Probe => Ok(()),
529 ObjectKind::Manifest(super::ManifestFormat::Dash) => {
530 if self.grouping.is_some() {
531 debug!("dashif-ingest: re-sent manifest ignored");
532 return Ok(());
533 }
534 let g = ManifestGrouping::from_mpd(input.bytes).map_err(SessionError::Ingest)?;
535 self.rep_order = g.switching_sets.iter().flatten().cloned().collect();
536 for id in &self.rep_order {
537 self.reps.entry(id.clone()).or_default();
538 }
539 self.grouping = Some(g);
540 if !self.established_emitted {
541 self.pending_events.push_back(SessionEvent::Established);
542 self.established_emitted = true;
543 }
544 Ok(())
545 }
546 ObjectKind::Manifest(super::ManifestFormat::Hls) => {
547 debug!("dashif-ingest: HLS manifest ignored (§6.2.17e)");
548 Ok(())
549 }
550 ObjectKind::Header { representation_id } => {
551 self.on_header(&representation_id, input.bytes)
552 }
553 ObjectKind::Segment {
554 representation_id,
555 number,
556 time,
557 } => self.on_segment(&representation_id, number, time, input.bytes),
558 }
559 }
560
561 fn poll(&mut self) -> Option<Self::Out> {
562 self.pending_events.pop_front()
563 }
564
565 fn finish(&mut self) -> Result<(), Self::Error> {
566 Ok(())
567 }
568
569 fn demand(&self) -> Demand {
570 Demand::new(MAX_TS_READ)
571 }
572}
573
574impl IngestSession for DashifIngestSession {
575 type Request = std::convert::Infallible;
576
577 fn poll_transmit(&mut self) -> Option<Self::Request> {
578 None
579 }
580}
581
582