1pub mod active;
27pub mod ladder;
28
29mod catalog;
30mod config;
31mod error;
32mod feed;
33mod pipeline;
34mod rung;
35
36pub use config::Config;
37pub use ladder::{Ladder, Rung};
38
39pub use error::Error;
40
41pub async fn run(
46 source: moq_net::broadcast::Consumer,
47 output: moq_net::broadcast::Producer,
48 config: Config,
49) -> Result<(), Error> {
50 Transcoder::new(source, output, config)?.run().await
51}
52
53pub struct Transcoder {
65 source: moq_net::broadcast::Consumer,
66 output: moq_net::broadcast::Producer,
67 config: Config,
68 derived: moq_mux::catalog::Producer,
69 dynamic: moq_net::broadcast::Dynamic,
71 active: active::Producer,
72}
73
74impl Transcoder {
75 pub fn new(
80 source: moq_net::broadcast::Consumer,
81 mut output: moq_net::broadcast::Producer,
82 config: Config,
83 ) -> Result<Self, Error> {
84 let derived = moq_mux::catalog::Producer::new(&mut output, moq_mux::catalog::Config::default())?;
87 let dynamic = output.dynamic();
88
89 Ok(Self {
90 source,
91 output,
92 config,
93 derived,
94 dynamic,
95 active: active::Producer::default(),
96 })
97 }
98
99 pub fn active(&self) -> active::Consumer {
105 self.active.consume()
106 }
107
108 pub async fn run(self) -> Result<(), Error> {
110 let Self {
111 source,
112 output,
113 config,
114 mut derived,
115 mut dynamic,
116 active,
117 } = self;
118
119 let track = source
122 .track(hang::Catalog::DEFAULT_NAME)?
123 .subscribe(hang::Catalog::default_subscription())
124 .await?;
125 let mut catalogs = moq_mux::catalog::hang::Consumer::<()>::new(track);
126 let mut decoders = catalog::Decoders::new(config.feed_decoder());
127 let (source_name, source_config, snapshot) = loop {
128 let Some(snapshot) = catalogs.next().await? else {
129 return Err(Error::NoSource);
130 };
131 match catalog::choose_source(&snapshot.video, &mut decoders).await {
132 Ok((name, config)) => break (name, config, snapshot),
133 Err(Error::NoSource) => tracing::debug!("no transcodable rendition yet; waiting for a catalog update"),
134 Err(err) => return Err(err),
135 }
136 };
137 let mut ladder = pipeline::Pipeline::new(
141 source.clone(),
142 config.clone(),
143 active,
144 decoders,
145 source_name,
146 source_config,
147 )
148 .await?;
149
150 {
154 let mut guard = derived.modify()?;
155 catalog::populate(&mut guard, &snapshot, ladder.rungs(), config.source.as_ref())?;
156 guard.commit()?;
157 }
158
159 let mut tasks = tokio::task::JoinSet::new();
161 loop {
162 tokio::select! {
163 request = dynamic.requested_track() => {
164 let Ok(request) = request else { break };
166 match ladder.rung(request.name())? {
167 Some(rung) => { tasks.spawn(rung::serve(rung, request)); }
168 None => request.reject(moq_net::Error::NotFound),
169 }
170 },
171 update = catalogs.next() => match update {
172 Ok(Some(snapshot)) => {
173 ladder.follow(&snapshot.video).await?;
174 let mut guard = derived.modify()?;
175 catalog::populate(&mut guard, &snapshot, ladder.rungs(), config.source.as_ref())?;
176 guard.commit()?;
177 }
178 Ok(None) => break,
180 Err(err) => {
181 tracing::debug!(%err, "source catalog ended");
182 break;
183 }
184 },
185 Some(result) = tasks.join_next() => match result {
186 Ok(Ok(())) => {}
187 Ok(Err(err)) => tracing::warn!(%err, "rung failed"),
188 Err(err) => tracing::warn!(%err, "rung panicked"),
189 }
190 }
191 }
192
193 tasks.shutdown().await;
200
201 derived.finish()?;
202 output.finish();
203 Ok(())
204 }
205}
206
207#[cfg(test)]
208mod tests {
209
210 use super::*;
211
212 struct Source {
215 broadcast: moq_net::broadcast::Producer,
216 catalog: moq_mux::catalog::Producer,
217 _track: moq_net::track::Producer,
218 size: (u32, u32),
221 }
222
223 impl Source {
224 fn resize(&mut self, width: u32, height: u32) {
228 self.publish(width, height, None);
229 }
230
231 fn describe(&mut self, description: Option<bytes::Bytes>) {
234 let (width, height) = self.size;
235 self.publish(width, height, description);
236 }
237
238 fn publish(&mut self, width: u32, height: u32, description: Option<bytes::Bytes>) {
239 let mut video = hang::catalog::VideoConfig::new(hang::catalog::H264 {
240 inline: true,
241 profile: 0x42,
242 constraints: 0,
243 level: 30,
244 });
245 video.coded_width = Some(width);
246 video.coded_height = Some(height);
247 video.bitrate = Some(1_000_000);
248 video.framerate = Some(30.0);
249 video.description = description;
250 self.size = (width, height);
251
252 let mut guard = self.catalog.modify().unwrap();
253 guard.video = hang::catalog::Video::default();
254 guard.video.insert("video", video).unwrap();
255 }
256 }
257
258 fn source_catalog(width: u32, height: u32) -> Source {
261 let mut broadcast = moq_net::broadcast::Info::default().produce();
262 let catalog = moq_mux::catalog::Producer::new(&mut broadcast, moq_mux::catalog::Config::default()).unwrap();
263 let track = broadcast
264 .create_track("video", hang::container::track_info(hang::catalog::PRIORITY.video))
265 .unwrap();
266
267 let mut source = Source {
268 broadcast,
269 catalog,
270 _track: track,
271 size: (width, height),
272 };
273 source.resize(width, height);
274 source
275 }
276
277 async fn await_catalog(
280 catalogs: &mut moq_mux::catalog::hang::Consumer<()>,
281 ready: impl Fn(&moq_mux::catalog::hang::Catalog) -> bool,
282 ) -> moq_mux::catalog::hang::Catalog {
283 loop {
284 let snapshot = catalogs.next().await.unwrap().unwrap();
285 if ready(&snapshot) {
286 return snapshot;
287 }
288 }
289 }
290
291 async fn subscribe(consumer: &moq_net::broadcast::Consumer, name: &str) -> moq_net::track::Subscriber {
293 let track = loop {
294 match consumer.track(name) {
295 Ok(track) => break track,
296 Err(moq_net::Error::NotFound) => tokio::task::yield_now().await,
297 Err(err) => panic!("track {name}: {err}"),
298 }
299 };
300 track.subscribe(None).await.unwrap()
301 }
302
303 fn nal_types(annexb: &[u8]) -> Vec<u8> {
306 let mut types = Vec::new();
307 let mut i = 0;
308 while i + 3 < annexb.len() {
309 if annexb[i..i + 3] == [0, 0, 1] {
310 types.push(annexb[i + 3] & 0x1f);
311 i += 3;
312 } else {
313 i += 1;
314 }
315 }
316 types
317 }
318
319 fn write_keyframe(group: &mut moq_net::group::Producer) {
322 let mut encoder = moq_video::encode::Encoder::new(&{
323 let mut config = moq_video::encode::Config::new(320, 240, moq_video::Rate::new(30, 1).unwrap());
324 config.kind = moq_video::encode::Kind::Software;
325 config
326 })
327 .unwrap();
328 encoder.cut().unwrap();
329 let gray = vec![0x80u8; 320 * 240 * 4];
330 for encoded in encoder.encode(&gray_frame(&gray, 0)).unwrap() {
331 hang::container::Frame {
332 timestamp: encoded.timestamp,
333 payload: encoded.payload,
334 }
335 .write_to(group)
336 .unwrap();
337 }
338 }
339
340 fn gray_frame(rgba: &[u8], timestamp: u64) -> moq_video::Frame {
342 let surface = moq_video::Surface::rgba(rgba, moq_video::Size::new(320, 240)).unwrap();
343 moq_video::Frame::new(surface, moq_net::Timestamp::from_micros(timestamp).unwrap())
344 }
345
346 fn source_broadcast(groups: u64, frames: u64) -> Source {
349 let mut broadcast = moq_net::broadcast::Info::default().produce();
350 let mut catalog = moq_mux::catalog::Producer::new(&mut broadcast, moq_mux::catalog::Config::default()).unwrap();
351
352 let mut video = hang::catalog::VideoConfig::new(hang::catalog::H264 {
353 inline: true,
354 profile: 0x42,
355 constraints: 0,
356 level: 30,
357 });
358 video.coded_width = Some(320);
359 video.coded_height = Some(240);
360 video.bitrate = Some(1_000_000);
361 video.framerate = Some(30.0);
362 catalog.modify().unwrap().video.insert("video", video).unwrap();
363
364 let info = hang::container::track_info(hang::catalog::PRIORITY.video);
365 let track = broadcast.create_track("video", info).unwrap();
366
367 let mut encoder = moq_video::encode::Encoder::new(&{
368 let mut config = moq_video::encode::Config::new(320, 240, moq_video::Rate::new(30, 1).unwrap());
369 config.kind = moq_video::encode::Kind::Software;
370 config
371 })
372 .unwrap();
373 let gray = vec![0x80u8; 320 * 240 * 4];
374
375 for sequence in 0..groups {
376 let mut group = track.create_group(sequence.into()).unwrap();
377 for index in 0..frames {
378 let timestamp = (sequence * frames + index) * 33_333;
379 if index == 0 {
380 encoder.cut().unwrap();
381 }
382 for encoded in encoder.encode(&gray_frame(&gray, timestamp)).unwrap() {
383 let frame = hang::container::Frame {
384 timestamp: encoded.timestamp,
385 payload: encoded.payload,
386 };
387 frame.write_to(&mut group).unwrap();
388 }
389 }
390 group.finish().unwrap();
391 }
392
393 Source {
394 broadcast,
395 catalog,
396 _track: track,
397 size: (320, 240),
398 }
399 }
400
401 fn source_broadcast_live(groups: u64, frames: u64) -> (Source, tokio::task::JoinHandle<()>) {
406 let mut broadcast = moq_net::broadcast::Info::default().produce();
407 let mut catalog = moq_mux::catalog::Producer::new(&mut broadcast, moq_mux::catalog::Config::default()).unwrap();
408
409 let mut video = hang::catalog::VideoConfig::new(hang::catalog::H264 {
410 inline: true,
411 profile: 0x42,
412 constraints: 0,
413 level: 30,
414 });
415 video.coded_width = Some(320);
416 video.coded_height = Some(240);
417 video.bitrate = Some(1_000_000);
418 video.framerate = Some(30.0);
419 catalog.modify().unwrap().video.insert("video", video).unwrap();
420
421 let info = hang::container::track_info(hang::catalog::PRIORITY.video);
422 let track = broadcast.create_track("video", info).unwrap();
423
424 let source = Source {
425 broadcast,
426 catalog,
427 _track: track.clone(),
430 size: (320, 240),
431 };
432
433 let task = tokio::spawn(async move {
434 let mut encoder = moq_video::encode::Sink::open(&{
435 let mut config = moq_video::encode::Config::new(320, 240, moq_video::Rate::new(30, 1).unwrap());
436 config.kind = moq_video::encode::Kind::Software;
437 config
438 })
439 .await
440 .unwrap();
441 let gray = vec![0x80u8; 320 * 240 * 4];
442
443 for sequence in 0..groups {
444 tokio::time::sleep(std::time::Duration::from_millis(100)).await;
448 let mut group = track.create_group(sequence.into()).unwrap();
449 for index in 0..frames {
450 let timestamp = (sequence * frames + index) * 33_333;
451 if index == 0 {
452 encoder.cut().await.unwrap();
453 }
454 for encoded in encoder.encode(gray_frame(&gray, timestamp)).await.unwrap() {
455 let frame = hang::container::Frame {
456 timestamp: encoded.timestamp,
457 payload: encoded.payload,
458 };
459 frame.write_to(&mut group).unwrap();
460 }
461 }
462 group.finish().unwrap();
463 }
464 std::future::pending::<()>().await;
466 });
467
468 (source, task)
469 }
470
471 #[tokio::test]
474 async fn live_multi_rung() {
475 let (source, producer_task) = source_broadcast_live(3, 5);
482 let config = Config {
483 ladder: Ladder::new([
484 Rung::new(120, moq_net::bandwidth::Rate::from_bps(100_000)),
485 Rung::new(60, moq_net::bandwidth::Rate::from_bps(50_000)),
486 ])
487 .unwrap(),
488 encoder: moq_video::encode::Kind::Software,
489 decoder: moq_video::decode::Kind::Software,
490 source: None,
491 ..Default::default()
492 };
493
494 let output = moq_net::broadcast::Info::default().produce();
495 let consumer = output.consume();
496 let transcoder = tokio::spawn(run(source.broadcast.consume(), output, config));
497
498 let mut subscribers = Vec::new();
501 for name in ["video/120p", "video/60p"] {
502 let track = loop {
503 match consumer.track(name) {
504 Ok(track) => break track,
505 Err(moq_net::Error::NotFound) => tokio::task::yield_now().await,
506 Err(err) => panic!("rung track {name}: {err}"),
507 }
508 };
509 subscribers.push((name, track.subscribe(None).await.unwrap().ordered()));
510 }
511
512 for (name, subscriber) in &mut subscribers {
514 let mut group = subscriber.next_group().await.unwrap().unwrap();
515 let payload = group.read_frame().await.unwrap().unwrap();
516 let frame = hang::container::Frame::decode(payload.payload).unwrap();
517 assert!(
518 frame.payload.starts_with(&[0, 0, 0, 1]) || frame.payload.starts_with(&[0, 0, 1]),
519 "{name} output is not Annex-B"
520 );
521 while group.read_frame().await.unwrap().is_some() {}
522 assert_eq!(group.frame_count(), 5, "{name} dropped frames");
523 }
524
525 producer_task.abort();
526 transcoder.abort();
527 }
528
529 #[cfg_attr(
533 target_os = "windows",
534 ignore = "explicit live-DXVA GPU probe; VideoProcessorBlt can hang on affected drivers"
535 )]
536 #[tokio::test]
537 async fn live_multi_rung_hardware() {
538 if !hardware_available() {
539 eprintln!("skipping: no hardware decoder + encoder available");
540 return;
541 }
542 let (source, producer_task) = source_broadcast_live(3, 5);
549 let config = Config {
553 ladder: Ladder::new([
554 Rung::new(180, moq_net::bandwidth::Rate::from_bps(200_000)),
555 Rung::new(120, moq_net::bandwidth::Rate::from_bps(100_000)),
556 ])
557 .unwrap(),
558 encoder: moq_video::encode::Kind::Hardware,
559 decoder: moq_video::decode::Kind::Hardware,
560 source: None,
561 ..Default::default()
562 };
563
564 let output = moq_net::broadcast::Info::default().produce();
565 let consumer = output.consume();
566 let transcoder = tokio::spawn(run(source.broadcast.consume(), output, config));
567
568 let mut subscribers = Vec::new();
569 for name in ["video/180p", "video/120p"] {
570 let track = loop {
571 match consumer.track(name) {
572 Ok(track) => break track,
573 Err(moq_net::Error::NotFound) => tokio::task::yield_now().await,
574 Err(err) => panic!("rung track {name}: {err}"),
575 }
576 };
577 subscribers.push((name, track.subscribe(None).await.unwrap().ordered()));
578 }
579
580 for (name, subscriber) in &mut subscribers {
581 let mut group = subscriber.next_group().await.unwrap().unwrap();
582 let payload = group.read_frame().await.unwrap().unwrap();
583 let frame = hang::container::Frame::decode(payload.payload).unwrap();
584 assert!(
585 frame.payload.starts_with(&[0, 0, 0, 1]) || frame.payload.starts_with(&[0, 0, 1]),
586 "{name} output is not Annex-B"
587 );
588 while group.read_frame().await.unwrap().is_some() {}
589 assert_eq!(group.frame_count(), 5, "{name} dropped frames");
590 }
591
592 producer_task.abort();
593 transcoder.abort();
594 }
595
596 fn hardware_available() -> bool {
600 let mut encode = moq_video::encode::Config::new(160, 120, moq_video::Rate::new(30, 1).unwrap());
601 encode.kind = moq_video::encode::Kind::Hardware;
602 if moq_video::encode::Encoder::new(&encode).is_err() {
603 return false;
604 }
605
606 let video = hang::catalog::VideoConfig::new(hang::catalog::H264 {
607 inline: true,
608 profile: 0x42,
609 constraints: 0,
610 level: 30,
611 });
612 let mut decode = moq_video::decode::Config::new();
613 decode.kind = moq_video::decode::Kind::Hardware;
614 moq_video::decode::Decoder::new(&video, &decode).is_ok()
615 }
616
617 #[cfg(feature = "vaapi")]
618 fn vaapi_decoder_available() -> bool {
619 let video = hang::catalog::VideoConfig::new(hang::catalog::H264 {
620 inline: true,
621 profile: 0x42,
622 constraints: 0,
623 level: 30,
624 });
625 let mut decode = moq_video::decode::Config::new();
626 decode.kind = moq_video::decode::Kind::Named("vaapi".to_string());
627 moq_video::decode::Decoder::new(&video, &decode).is_ok()
628 }
629
630 #[cfg(feature = "vaapi")]
633 #[tokio::test]
634 async fn vaapi_fetch_keeps_the_buffered_tail() {
635 let source = source_broadcast(1, 5);
636 let config = Config {
637 ladder: Ladder::new([Rung::new(120, moq_net::bandwidth::Rate::from_bps(100_000))]).unwrap(),
638 encoder: moq_video::encode::Kind::Software,
639 decoder: moq_video::decode::Kind::Named("vaapi".to_string()),
640 source: None,
641 ..Default::default()
642 };
643
644 if !vaapi_decoder_available() {
645 eprintln!("skipping: no VA-API H.264 decoder");
646 return;
647 }
648 let output = moq_net::broadcast::Info::default().produce();
649 let consumer = output.consume();
650 let transcoder = tokio::spawn(run(source.broadcast.consume(), output, config));
651
652 let track = loop {
653 match consumer.track("video/120p") {
654 Ok(track) => break track,
655 Err(moq_net::Error::NotFound) => tokio::task::yield_now().await,
656 Err(err) => panic!("rung track: {err}"),
657 }
658 };
659 let mut fetched = track.fetch_group(0, None).await.unwrap();
660 while fetched.read_frame().await.unwrap().is_some() {}
661 assert_eq!(
662 fetched.frame_count(),
663 5,
664 "VAAPI dropped the fetched group's buffered tail"
665 );
666
667 transcoder.abort();
668 }
669
670 #[cfg(feature = "vaapi")]
673 #[tokio::test]
674 async fn vaapi_live_keeps_the_buffered_tail_in_its_group() {
675 if !vaapi_decoder_available() {
676 eprintln!("skipping: no VA-API H.264 decoder");
677 return;
678 }
679
680 let (source, producer_task) = source_broadcast_live(1, 5);
681 let config = Config {
682 ladder: Ladder::new([Rung::new(120, moq_net::bandwidth::Rate::from_bps(100_000))]).unwrap(),
683 encoder: moq_video::encode::Kind::Software,
684 decoder: moq_video::decode::Kind::Named("vaapi".to_string()),
685 source: None,
686 ..Default::default()
687 };
688
689 let output = moq_net::broadcast::Info::default().produce();
690 let consumer = output.consume();
691 let transcoder = tokio::spawn(run(source.broadcast.consume(), output, config));
692
693 let track = loop {
694 match consumer.track("video/120p") {
695 Ok(track) => break track,
696 Err(moq_net::Error::NotFound) => tokio::task::yield_now().await,
697 Err(err) => panic!("rung track: {err}"),
698 }
699 };
700 let mut subscriber = track.subscribe(None).await.unwrap();
701 let mut group = subscriber.recv_group().await.unwrap().unwrap();
702 while group.read_frame().await.unwrap().is_some() {}
703 assert_eq!(
704 group.frame_count(),
705 5,
706 "VAAPI moved the live group's buffered tail past its end"
707 );
708
709 producer_task.abort();
710 transcoder.abort();
711 }
712
713 #[cfg_attr(
718 target_os = "windows",
719 ignore = "explicit live-DXVA GPU probe; VideoProcessorBlt can hang on affected drivers"
720 )]
721 #[tokio::test]
722 async fn end_to_end_hardware() {
723 if !hardware_available() {
724 eprintln!("skipping: no hardware decoder + encoder available");
725 return;
726 }
727
728 let source = source_broadcast(2, 5);
729 let config = Config {
730 ladder: Ladder::new([Rung::new(120, moq_net::bandwidth::Rate::from_bps(100_000))]).unwrap(),
731 encoder: moq_video::encode::Kind::Hardware,
732 decoder: moq_video::decode::Kind::Hardware,
733 source: None,
734 ..Default::default()
735 };
736
737 let output = moq_net::broadcast::Info::default().produce();
738 let consumer = output.consume();
739 let transcoder = tokio::spawn(run(source.broadcast.consume(), output, config));
740
741 let track = loop {
744 match consumer.track("video/120p") {
745 Ok(track) => break track,
746 Err(moq_net::Error::NotFound) => tokio::task::yield_now().await,
747 Err(err) => panic!("rung track: {err}"),
748 }
749 };
750 let mut fetched = track.fetch_group(0, None).await.unwrap();
751 let payload = fetched.read_frame().await.unwrap().unwrap();
752 let frame = hang::container::Frame::decode(payload.payload).unwrap();
753 assert!(
754 frame.payload.starts_with(&[0, 0, 0, 1]) || frame.payload.starts_with(&[0, 0, 1]),
755 "hardware rung output is not Annex-B"
756 );
757 while fetched.read_frame().await.unwrap().is_some() {}
758 assert_eq!(fetched.frame_count(), 5, "hardware transcode dropped frames");
759
760 transcoder.abort();
761 }
762
763 #[tokio::test]
764 async fn end_to_end() {
765 let source = source_broadcast(2, 5);
766
767 let config = Config {
768 ladder: Ladder::new([Rung::new(120, moq_net::bandwidth::Rate::from_bps(100_000))]).unwrap(),
769 encoder: moq_video::encode::Kind::Software,
770 decoder: moq_video::decode::Kind::Software,
771 source: Some(moq_net::path::RelativeOwned::from(".".to_string())),
772 ..Default::default()
773 };
774
775 let origin = moq_tokio::origin::spawn();
779 let output = origin.create_broadcast("room/transcode").unwrap();
780 let consumer = output.consume();
781 let transcoder = tokio::spawn(run(source.broadcast.consume(), output, config));
782
783 let track = loop {
788 match consumer.track(hang::Catalog::DEFAULT_NAME) {
789 Ok(track) => break track,
790 Err(moq_net::Error::NotFound) => tokio::task::yield_now().await,
791 Err(err) => panic!("catalog track: {err}"),
792 }
793 };
794 let track = track.subscribe(None).await.unwrap();
795 let mut catalogs = moq_mux::catalog::hang::Consumer::<()>::new(track);
796 let derived = loop {
799 let snapshot = catalogs.next().await.unwrap().unwrap();
800 if snapshot.video.renditions.contains_key("video/120p") {
801 break snapshot;
802 }
803 };
804
805 let rung = derived.video.renditions.get("video/120p").expect("rung missing");
806 assert_eq!(rung.coded_width, Some(160));
807 assert_eq!(rung.coded_height, Some(120));
808 assert_eq!(rung.bitrate, Some(100_000));
809 assert!(rung.codec.to_string().starts_with("avc3."));
810
811 let passthrough = derived.video.renditions.get("video").expect("passthrough missing");
812 assert_eq!(passthrough.broadcast.as_ref().map(|b| b.as_ref()), Some("."));
813
814 let mut subscriber = consumer
817 .track("video/120p")
818 .unwrap()
819 .subscribe(None)
820 .await
821 .unwrap()
822 .ordered();
823 let mut group = subscriber.next_group().await.unwrap().unwrap();
824 assert!(group.sequence <= 1, "unexpected sequence {}", group.sequence);
825 let payload = group.read_frame().await.unwrap().unwrap();
826 let frame = hang::container::Frame::decode(payload.payload).unwrap();
827 assert!(
828 frame.payload.starts_with(&[0, 0, 0, 1]) || frame.payload.starts_with(&[0, 0, 1]),
829 "rung output is not Annex-B"
830 );
831
832 let mut fetched = consumer
834 .track("video/120p")
835 .unwrap()
836 .fetch_group(0, None)
837 .await
838 .unwrap();
839 let mut timestamps = Vec::new();
840 let mut first_payload = None;
841 while let Some(payload) = fetched.read_frame().await.unwrap() {
842 let frame = hang::container::Frame::decode(payload.payload).unwrap();
843 assert!(!frame.payload.is_empty());
844 timestamps.push(frame.timestamp.as_micros());
845 first_payload = first_payload.or(Some(frame.payload));
846 }
847
848 let types = nal_types(&first_payload.expect("the group had no frames"));
854 assert!(types.contains(&7), "group does not open with an SPS: {types:?}");
855 assert!(types.contains(&8), "group does not open with a PPS: {types:?}");
856 assert!(types.contains(&5), "group does not open with an IDR: {types:?}");
857 assert_eq!(timestamps, (0..5).map(|i| i * 33_333).collect::<Vec<u128>>());
861 let total = fetched.finished().await.unwrap();
864 assert_eq!(total, 5);
865
866 transcoder.abort();
867 }
868
869 #[tokio::test]
874 async fn reports_active_rungs() {
875 let source = source_broadcast(2, 5);
876
877 let config = Config {
878 ladder: Ladder::new([Rung::new(120, moq_net::bandwidth::Rate::from_bps(100_000))]).unwrap(),
879 encoder: moq_video::encode::Kind::Software,
880 decoder: moq_video::decode::Kind::Software,
881 source: None,
882 ..Default::default()
883 };
884
885 let output = moq_net::broadcast::Info::default().produce();
886 let consumer = output.consume();
887 let transcoder = Transcoder::new(source.broadcast.consume(), output, config).unwrap();
888 let mut active = transcoder.active();
889 let driver = tokio::spawn(transcoder.run());
890
891 let update = active.next().await.unwrap();
893 let rendition = update.rendition;
894 assert_eq!(rendition.name(), "video/120p");
895 assert_eq!(rendition.size().height, 120);
896 assert_eq!(rendition.bitrate(), moq_net::bandwidth::Rate::from_bps(100_000));
897 assert!(!update.encoding, "encoding before anyone asked");
898 assert_eq!(rendition.frames(), 0);
899
900 let mut subscriber = consumer
901 .track("video/120p")
902 .unwrap()
903 .subscribe(None)
904 .await
905 .unwrap()
906 .ordered();
907 let update = active.next().await.unwrap();
908 assert_eq!(update.rendition.name(), "video/120p");
909 assert!(update.encoding);
910
911 let mut group = subscriber.next_group().await.unwrap().unwrap();
913 group.read_frame().await.unwrap().unwrap();
914 assert!(rendition.frames() > 0);
915 assert!(rendition.bytes() > 0);
916
917 drop(group);
919 drop(subscriber);
920 let update = active.next().await.unwrap();
921 assert_eq!(update.rendition.name(), "video/120p");
922 assert!(!update.encoding);
923
924 assert!(rendition.frames() > 0);
926 assert!(rendition.bytes() > 0);
927
928 driver.abort();
929 }
930
931 #[tokio::test]
942 async fn a_resized_rung_takes_a_fresh_name() {
943 let mut source = source_catalog(640, 360);
944
945 let config = Config {
946 ladder: Ladder::new([Rung::new(120, moq_net::bandwidth::Rate::from_bps(100_000))]).unwrap(),
947 encoder: moq_video::encode::Kind::Software,
948 decoder: moq_video::decode::Kind::Software,
949 source: None,
950 ..Default::default()
951 };
952
953 let output = moq_net::broadcast::Info::default().produce();
954 let consumer = output.consume();
955 let transcoder = tokio::spawn(run(source.broadcast.consume(), output, config));
956
957 let track = loop {
958 match consumer.track(hang::Catalog::DEFAULT_NAME) {
959 Ok(track) => break track,
960 Err(moq_net::Error::NotFound) => tokio::task::yield_now().await,
961 Err(err) => panic!("catalog track: {err}"),
962 }
963 };
964 let mut catalogs = moq_mux::catalog::hang::Consumer::<()>::new(track.subscribe(None).await.unwrap());
965
966 let derived = await_catalog(&mut catalogs, |snapshot| {
967 snapshot.video.renditions.contains_key("video/120p")
968 })
969 .await;
970 assert_eq!(
971 derived.video.renditions.get("video/120p").and_then(|v| v.coded_width),
972 Some(212),
973 "640x360 should give 120p a 212 wide picture"
974 );
975 let mut retired = subscribe(&consumer, "video/120p").await;
976
977 source.resize(480, 360);
979
980 let derived = await_catalog(&mut catalogs, |snapshot| {
981 !snapshot.video.renditions.contains_key("video/120p")
982 })
983 .await;
984 let replacement = derived
985 .video
986 .renditions
987 .get("video/120p.2")
988 .expect("the resized rung was not republished under a fresh name");
989 assert_eq!(replacement.coded_width, Some(160));
990 assert_eq!(replacement.coded_height, Some(120));
991
992 let ended = tokio::time::timeout(std::time::Duration::from_secs(5), retired.recv_group())
995 .await
996 .expect("the retired rung never ended its track")
997 .expect("the retired rung aborted instead of finishing");
998 assert!(ended.is_none(), "expected a clean end, got a group");
999
1000 subscribe(&consumer, "video/120p.2").await;
1002
1003 transcoder.abort();
1004 }
1005
1006 #[tokio::test]
1011 async fn ladder_follows_a_source_resize() {
1012 let mut source = source_catalog(640, 360);
1013
1014 let config = Config {
1015 ladder: Ladder::new([
1018 Rung::new(360, moq_net::bandwidth::Rate::from_bps(900_000)),
1019 Rung::new(240, moq_net::bandwidth::Rate::from_bps(300_000)),
1020 Rung::new(120, moq_net::bandwidth::Rate::from_bps(100_000)),
1021 ])
1022 .unwrap(),
1023 encoder: moq_video::encode::Kind::Software,
1024 decoder: moq_video::decode::Kind::Software,
1025 source: Some(moq_net::path::RelativeOwned::from(".".to_string())),
1026 ..Default::default()
1027 };
1028
1029 let output = moq_net::broadcast::Info::default().produce();
1030 let consumer = output.consume();
1031 let transcoder = tokio::spawn(run(source.broadcast.consume(), output, config));
1032
1033 let track = loop {
1034 match consumer.track(hang::Catalog::DEFAULT_NAME) {
1035 Ok(track) => break track,
1036 Err(moq_net::Error::NotFound) => tokio::task::yield_now().await,
1037 Err(err) => panic!("catalog track: {err}"),
1038 }
1039 };
1040 let mut catalogs = moq_mux::catalog::hang::Consumer::<()>::new(track.subscribe(None).await.unwrap());
1041
1042 let derived = await_catalog(&mut catalogs, |snapshot| {
1043 snapshot.video.renditions.contains_key("video/360p")
1044 })
1045 .await;
1046 assert!(derived.video.renditions.contains_key("video/240p"));
1047 assert!(derived.video.renditions.contains_key("video/120p"));
1048 assert_eq!(
1049 derived.video.renditions.get("video").and_then(|v| v.coded_width),
1050 Some(640),
1051 "the passthrough entry should describe the source"
1052 );
1053
1054 let mut retired = subscribe(&consumer, "video/240p").await;
1057 let mut kept = subscribe(&consumer, "video/120p").await;
1058
1059 source.resize(320, 180);
1062
1063 let derived = await_catalog(&mut catalogs, |snapshot| {
1064 !snapshot.video.renditions.contains_key("video/360p")
1065 })
1066 .await;
1067 assert!(
1068 !derived.video.renditions.contains_key("video/240p"),
1069 "240p outlived the resize"
1070 );
1071 let rung = derived
1072 .video
1073 .renditions
1074 .get("video/120p")
1075 .expect("120p was retired too");
1076 assert_eq!(rung.coded_width, Some(212));
1077 assert_eq!(rung.coded_height, Some(120));
1078 assert_eq!(
1079 derived.video.renditions.get("video").and_then(|v| v.coded_width),
1080 Some(320),
1081 "the passthrough entry should follow the source"
1082 );
1083
1084 let ended = tokio::time::timeout(std::time::Duration::from_secs(5), retired.recv_group())
1088 .await
1089 .expect("the retired rung never ended its track")
1090 .expect("the retired rung aborted instead of finishing");
1091 assert!(ended.is_none(), "expected a clean end, got a group");
1092
1093 assert!(
1096 tokio::time::timeout(std::time::Duration::from_millis(100), kept.recv_group())
1097 .await
1098 .is_err(),
1099 "a rung that still fits was retired anyway"
1100 );
1101
1102 transcoder.abort();
1103 }
1104
1105 #[tokio::test]
1117 async fn retirement_rides_out_an_open_live_group() {
1118 let mut source = source_catalog(320, 240);
1119 let mut group = source._track.create_group(0u64.into()).unwrap();
1120 write_keyframe(&mut group);
1121
1122 let config = Config {
1123 ladder: Ladder::new([Rung::new(120, moq_net::bandwidth::Rate::from_bps(100_000))]).unwrap(),
1124 encoder: moq_video::encode::Kind::Software,
1125 decoder: moq_video::decode::Kind::Software,
1126 source: None,
1127 ..Default::default()
1128 };
1129 let output = moq_net::broadcast::Info::default().produce();
1130 let consumer = output.consume();
1131 let transcoder = tokio::spawn(run(source.broadcast.consume(), output, config));
1132
1133 let catalog = loop {
1134 match consumer.track(hang::Catalog::DEFAULT_NAME) {
1135 Ok(track) => break track,
1136 Err(moq_net::Error::NotFound) => tokio::task::yield_now().await,
1137 Err(err) => panic!("catalog track: {err}"),
1138 }
1139 };
1140 let mut catalogs = moq_mux::catalog::hang::Consumer::<()>::new(catalog.subscribe(None).await.unwrap());
1141 await_catalog(&mut catalogs, |snapshot| {
1142 snapshot.video.renditions.contains_key("video/120p")
1143 })
1144 .await;
1145
1146 let rung = consumer.track("video/120p").unwrap();
1150 rung.query().await.unwrap();
1151 while rung.latest() != Some(0) {
1152 tokio::task::yield_now().await;
1153 }
1154 let mut fetched = rung.fetch_group(0, None).await.unwrap();
1155
1156 source.resize(160, 90);
1159 tokio::time::timeout(
1160 std::time::Duration::from_secs(5),
1161 await_catalog(&mut catalogs, |snapshot| {
1162 !snapshot.video.renditions.contains_key("video/120p")
1163 }),
1164 )
1165 .await
1166 .expect("the ladder never retired the rung");
1167 group.finish().unwrap();
1168
1169 let finished = tokio::time::timeout(std::time::Duration::from_secs(5), async {
1170 while fetched.read_frame().await?.is_some() {}
1171 fetched.finished().await
1172 })
1173 .await
1174 .expect("the accepted fetch never finished");
1175 assert!(finished.is_ok(), "retirement aborted the accepted group: {finished:?}");
1176
1177 transcoder.abort();
1178 }
1179
1180 #[tokio::test]
1194 async fn retirement_waits_for_an_unclaimed_fetch() {
1195 let mut source = source_catalog(320, 240);
1196 let source_fetches = source._track.dynamic();
1199
1200 let config = Config {
1201 ladder: Ladder::new([Rung::new(120, moq_net::bandwidth::Rate::from_bps(100_000))]).unwrap(),
1202 encoder: moq_video::encode::Kind::Software,
1203 decoder: moq_video::decode::Kind::Software,
1204 source: None,
1205 ..Default::default()
1206 };
1207 let output = moq_net::broadcast::Info::default().produce();
1208 let consumer = output.consume();
1209 let transcoder = tokio::spawn(run(source.broadcast.consume(), output, config));
1210
1211 let catalog = loop {
1212 match consumer.track(hang::Catalog::DEFAULT_NAME) {
1213 Ok(track) => break track,
1214 Err(moq_net::Error::NotFound) => tokio::task::yield_now().await,
1215 Err(err) => panic!("catalog track: {err}"),
1216 }
1217 };
1218 let mut catalogs = moq_mux::catalog::hang::Consumer::<()>::new(catalog.subscribe(None).await.unwrap());
1219 await_catalog(&mut catalogs, |snapshot| {
1220 snapshot.video.renditions.contains_key("video/120p")
1221 })
1222 .await;
1223
1224 let rung = consumer.track("video/120p").unwrap();
1227 rung.query().await.unwrap();
1228 assert!(
1229 source._track.subscription_changed().await.unwrap().is_some(),
1230 "the rung never subscribed to the live source",
1231 );
1232
1233 let fetching = rung.fetch_group(7, None);
1236
1237 let request = source_fetches.requested_group().await.expect("the source track closed");
1240 assert_eq!(request.sequence(), 7);
1241
1242 source.resize(160, 90);
1245 await_catalog(&mut catalogs, |snapshot| {
1246 !snapshot.video.renditions.contains_key("video/120p")
1247 })
1248 .await;
1249 assert!(
1250 source._track.subscription_changed().await.unwrap().is_none(),
1251 "the rung kept its live source subscription after retirement",
1252 );
1253
1254 let mut group = request.accept(None).unwrap();
1257 write_keyframe(&mut group);
1258 group.finish().unwrap();
1259
1260 let mut fetched = fetching
1261 .await
1262 .expect("retirement finished the track before the fetch claimed its group");
1263 let frames = async {
1264 while fetched.read_frame().await?.is_some() {}
1265 fetched.finished().await
1266 }
1267 .await
1268 .expect("retirement aborted the accepted group");
1269 assert!(frames > 0, "the fetch claimed its group but produced no frames");
1270
1271 transcoder.abort();
1272 }
1273
1274 #[tokio::test]
1279 async fn a_rebuilt_decode_renames_every_rung() {
1280 let mut source = source_catalog(320, 240);
1281
1282 let config = Config {
1283 ladder: Ladder::new([Rung::new(120, moq_net::bandwidth::Rate::from_bps(100_000))]).unwrap(),
1284 encoder: moq_video::encode::Kind::Software,
1285 decoder: moq_video::decode::Kind::Software,
1286 source: None,
1287 ..Default::default()
1288 };
1289
1290 let output = moq_net::broadcast::Info::default().produce();
1291 let consumer = output.consume();
1292 let transcoder = tokio::spawn(run(source.broadcast.consume(), output, config));
1293
1294 let track = loop {
1295 match consumer.track(hang::Catalog::DEFAULT_NAME) {
1296 Ok(track) => break track,
1297 Err(moq_net::Error::NotFound) => tokio::task::yield_now().await,
1298 Err(err) => panic!("catalog track: {err}"),
1299 }
1300 };
1301 let mut catalogs = moq_mux::catalog::hang::Consumer::<()>::new(track.subscribe(None).await.unwrap());
1302 await_catalog(&mut catalogs, |snapshot| {
1303 snapshot.video.renditions.contains_key("video/120p")
1304 })
1305 .await;
1306 let mut retired = subscribe(&consumer, "video/120p").await;
1307
1308 source.describe(Some(bytes::Bytes::from_static(&[0x01, 0x42, 0x00, 0x1e])));
1311
1312 let derived = tokio::time::timeout(
1313 std::time::Duration::from_secs(5),
1314 await_catalog(&mut catalogs, |snapshot| {
1315 snapshot.video.renditions.contains_key("video/120p.2")
1316 }),
1317 )
1318 .await
1319 .expect("the rebuilt decode kept the retired rung name");
1320 assert!(
1321 !derived.video.renditions.contains_key("video/120p"),
1322 "the retired name is still advertised"
1323 );
1324 assert_eq!(
1325 derived.video.renditions.get("video/120p.2").and_then(|v| v.coded_width),
1326 Some(160),
1327 "the replacement should serve the same picture under a new name"
1328 );
1329
1330 let ended = tokio::time::timeout(std::time::Duration::from_secs(5), retired.recv_group())
1331 .await
1332 .expect("the retired rung never ended its track")
1333 .expect("the retired rung aborted instead of finishing");
1334 assert!(ended.is_none(), "expected a clean end, got a group");
1335 subscribe(&consumer, "video/120p.2").await;
1336
1337 transcoder.abort();
1338 }
1339
1340 fn mixed_codec_source() -> (
1344 moq_net::broadcast::Producer,
1345 moq_mux::catalog::Producer,
1346 [moq_net::track::Producer; 2],
1347 ) {
1348 let mut broadcast = moq_net::broadcast::Info::default().produce();
1349 let mut catalog = moq_mux::catalog::Producer::new(&mut broadcast, moq_mux::catalog::Config::default()).unwrap();
1350
1351 let mut hevc = hang::catalog::VideoConfig::new(hang::catalog::H265 {
1352 in_band: true,
1353 profile_space: 0,
1354 profile_idc: 1,
1355 profile_compatibility_flags: [0x60, 0, 0, 0],
1356 tier_flag: false,
1357 level_idc: 120,
1358 constraint_flags: [0x90, 0, 0, 0, 0, 0],
1359 });
1360 hevc.coded_width = Some(1920);
1361 hevc.coded_height = Some(1080);
1362 hevc.bitrate = Some(6_000_000);
1363
1364 let mut avc = hang::catalog::VideoConfig::new(hang::catalog::H264 {
1365 inline: true,
1366 profile: 0x42,
1367 constraints: 0,
1368 level: 30,
1369 });
1370 avc.coded_width = Some(640);
1371 avc.coded_height = Some(360);
1372 avc.bitrate = Some(1_000_000);
1373
1374 let mut guard = catalog.modify().unwrap();
1375 guard.video.insert("hevc", hevc).unwrap();
1376 guard.video.insert("avc", avc).unwrap();
1377 guard.commit().unwrap();
1378
1379 let info = hang::container::track_info(hang::catalog::PRIORITY.video);
1380 let tracks = [
1381 broadcast.create_track("hevc", info.clone()).unwrap(),
1382 broadcast.create_track("avc", info).unwrap(),
1383 ];
1384 (broadcast, catalog, tracks)
1385 }
1386
1387 #[tokio::test]
1391 async fn an_undecodable_larger_rendition_is_not_the_source() {
1392 let (broadcast, _catalog, _tracks) = mixed_codec_source();
1393
1394 let config = Config {
1395 ladder: Ladder::new([
1396 Rung::new(720, moq_net::bandwidth::Rate::from_bps(2_500_000)),
1397 Rung::new(120, moq_net::bandwidth::Rate::from_bps(100_000)),
1398 ])
1399 .unwrap(),
1400 encoder: moq_video::encode::Kind::Software,
1401 decoder: moq_video::decode::Kind::Software,
1402 source: None,
1403 ..Default::default()
1404 };
1405
1406 let output = moq_net::broadcast::Info::default().produce();
1407 let consumer = output.consume();
1408 let transcoder = tokio::spawn(run(broadcast.consume(), output, config));
1409
1410 let track = loop {
1411 match consumer.track(hang::Catalog::DEFAULT_NAME) {
1412 Ok(track) => break track,
1413 Err(moq_net::Error::NotFound) => tokio::task::yield_now().await,
1414 Err(err) => panic!("catalog track: {err}"),
1415 }
1416 };
1417 let mut catalogs = moq_mux::catalog::hang::Consumer::<()>::new(track.subscribe(None).await.unwrap());
1418 let derived = await_catalog(&mut catalogs, |snapshot| !snapshot.video.renditions.is_empty()).await;
1419
1420 let names: Vec<_> = derived.video.renditions.keys().map(String::as_str).collect();
1423 assert_eq!(
1424 names,
1425 ["video/120p"],
1426 "the ladder was sized against the H.265 rendition"
1427 );
1428
1429 transcoder.abort();
1430 }
1431
1432 #[tokio::test]
1435 async fn a_missing_decoder_is_refused() {
1436 let (broadcast, _catalog, _tracks) = mixed_codec_source();
1437
1438 let config = Config {
1439 ladder: Ladder::new([Rung::new(120, moq_net::bandwidth::Rate::from_bps(100_000))]).unwrap(),
1440 encoder: moq_video::encode::Kind::Software,
1441 decoder: moq_video::decode::Kind::Named("missing".to_string()),
1442 source: None,
1443 ..Default::default()
1444 };
1445
1446 let output = moq_net::broadcast::Info::default().produce();
1447 let result = tokio::time::timeout(
1448 std::time::Duration::from_secs(5),
1449 run(broadcast.consume(), output, config),
1450 )
1451 .await
1452 .expect("run kept waiting for a source it can never decode");
1453
1454 match result {
1455 Err(Error::Video(moq_video::Error::UnknownDecoder { name, codec, .. })) => {
1457 assert_eq!(name, "missing");
1458 assert_eq!(codec, moq_video::decode::Codec::H265);
1459 }
1460 other => panic!("expected the decoder's refusal, got {other:?}"),
1461 }
1462 }
1463
1464 #[tokio::test]
1467 async fn shuts_down_on_source_end() {
1468 let source = source_broadcast(1, 3);
1469
1470 let config = Config {
1471 ladder: Ladder::new([Rung::new(120, moq_net::bandwidth::Rate::from_bps(100_000))]).unwrap(),
1472 encoder: moq_video::encode::Kind::Software,
1473 decoder: moq_video::decode::Kind::Software,
1474 source: None,
1475 ..Default::default()
1476 };
1477
1478 let output = moq_net::broadcast::Info::default().produce();
1479 let consumer = output.consume();
1480 let transcoder = tokio::spawn(run(source.broadcast.consume(), output, config));
1481
1482 let track = loop {
1485 match consumer.track(hang::Catalog::DEFAULT_NAME) {
1486 Ok(track) => break track,
1487 Err(moq_net::Error::NotFound) => tokio::task::yield_now().await,
1488 Err(err) => panic!("catalog track: {err}"),
1489 }
1490 };
1491 let mut catalogs = moq_mux::catalog::hang::Consumer::<()>::new(track.subscribe(None).await.unwrap());
1492 catalogs.next().await.unwrap().unwrap();
1493
1494 drop(source);
1497
1498 let result = tokio::time::timeout(std::time::Duration::from_secs(5), transcoder).await;
1499 result.expect("run did not shut down within 5s").unwrap().unwrap();
1500 }
1501}