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 (source_name, source_config, snapshot) = loop {
127 let Some(snapshot) = catalogs.next().await? else {
128 return Err(Error::NoSource);
129 };
130 match catalog::choose_source(&snapshot.video) {
131 Ok((name, config)) => break (name, config, snapshot),
132 Err(_) => tracing::debug!("no transcodable rendition yet; waiting for a catalog update"),
133 }
134 };
135 let mut ladder =
139 pipeline::Pipeline::new(source.clone(), config.clone(), active, source_name, source_config).await?;
140
141 {
145 let mut guard = derived.modify()?;
146 catalog::populate(&mut guard, &snapshot, ladder.rungs(), config.source.as_ref())?;
147 guard.commit()?;
148 }
149
150 let mut tasks = tokio::task::JoinSet::new();
152 loop {
153 tokio::select! {
154 request = dynamic.requested_track() => {
155 let Ok(request) = request else { break };
157 match ladder.rung(request.name())? {
158 Some(rung) => { tasks.spawn(rung::serve(rung, request)); }
159 None => request.reject(moq_net::Error::NotFound),
160 }
161 },
162 update = catalogs.next() => match update {
163 Ok(Some(snapshot)) => {
164 ladder.follow(&snapshot.video).await?;
165 let mut guard = derived.modify()?;
166 catalog::populate(&mut guard, &snapshot, ladder.rungs(), config.source.as_ref())?;
167 guard.commit()?;
168 }
169 Ok(None) => break,
171 Err(err) => {
172 tracing::debug!(%err, "source catalog ended");
173 break;
174 }
175 },
176 Some(result) = tasks.join_next() => match result {
177 Ok(Ok(())) => {}
178 Ok(Err(err)) => tracing::warn!(%err, "rung failed"),
179 Err(err) => tracing::warn!(%err, "rung panicked"),
180 }
181 }
182 }
183
184 tasks.shutdown().await;
191
192 derived.finish()?;
193 output.finish();
194 Ok(())
195 }
196}
197
198#[cfg(test)]
199mod tests {
200
201 use super::*;
202
203 struct Source {
206 broadcast: moq_net::broadcast::Producer,
207 catalog: moq_mux::catalog::Producer,
208 _track: moq_net::track::Producer,
209 size: (u32, u32),
212 }
213
214 impl Source {
215 fn resize(&mut self, width: u32, height: u32) {
219 self.publish(width, height, None);
220 }
221
222 fn describe(&mut self, description: Option<bytes::Bytes>) {
225 let (width, height) = self.size;
226 self.publish(width, height, description);
227 }
228
229 fn publish(&mut self, width: u32, height: u32, description: Option<bytes::Bytes>) {
230 let mut video = hang::catalog::VideoConfig::new(hang::catalog::H264 {
231 inline: true,
232 profile: 0x42,
233 constraints: 0,
234 level: 30,
235 });
236 video.coded_width = Some(width);
237 video.coded_height = Some(height);
238 video.bitrate = Some(1_000_000);
239 video.framerate = Some(30.0);
240 video.description = description;
241 self.size = (width, height);
242
243 let mut guard = self.catalog.modify().unwrap();
244 guard.video = hang::catalog::Video::default();
245 guard.video.insert("video", video).unwrap();
246 }
247 }
248
249 fn source_catalog(width: u32, height: u32) -> Source {
252 let mut broadcast = moq_net::broadcast::Info::default().produce();
253 let catalog = moq_mux::catalog::Producer::new(&mut broadcast, moq_mux::catalog::Config::default()).unwrap();
254 let track = broadcast
255 .create_track("video", hang::container::track_info(hang::catalog::PRIORITY.video))
256 .unwrap();
257
258 let mut source = Source {
259 broadcast,
260 catalog,
261 _track: track,
262 size: (width, height),
263 };
264 source.resize(width, height);
265 source
266 }
267
268 async fn await_catalog(
271 catalogs: &mut moq_mux::catalog::hang::Consumer<()>,
272 ready: impl Fn(&moq_mux::catalog::hang::Catalog) -> bool,
273 ) -> moq_mux::catalog::hang::Catalog {
274 loop {
275 let snapshot = catalogs.next().await.unwrap().unwrap();
276 if ready(&snapshot) {
277 return snapshot;
278 }
279 }
280 }
281
282 async fn subscribe(consumer: &moq_net::broadcast::Consumer, name: &str) -> moq_net::track::Subscriber {
284 let track = loop {
285 match consumer.track(name) {
286 Ok(track) => break track,
287 Err(moq_net::Error::NotFound) => tokio::task::yield_now().await,
288 Err(err) => panic!("track {name}: {err}"),
289 }
290 };
291 track.subscribe(None).await.unwrap()
292 }
293
294 fn nal_types(annexb: &[u8]) -> Vec<u8> {
297 let mut types = Vec::new();
298 let mut i = 0;
299 while i + 3 < annexb.len() {
300 if annexb[i..i + 3] == [0, 0, 1] {
301 types.push(annexb[i + 3] & 0x1f);
302 i += 3;
303 } else {
304 i += 1;
305 }
306 }
307 types
308 }
309
310 fn write_keyframe(group: &mut moq_net::group::Producer) {
313 let mut encoder = moq_video::encode::Encoder::new(&{
314 let mut config = moq_video::encode::Config::new(320, 240, moq_video::Rate::new(30, 1).unwrap());
315 config.kind = moq_video::encode::Kind::Software;
316 config
317 })
318 .unwrap();
319 encoder.cut().unwrap();
320 let gray = vec![0x80u8; 320 * 240 * 4];
321 for encoded in encoder.encode(&gray_frame(&gray, 0)).unwrap() {
322 hang::container::Frame {
323 timestamp: encoded.timestamp,
324 payload: encoded.payload,
325 }
326 .write_to(group)
327 .unwrap();
328 }
329 }
330
331 fn gray_frame(rgba: &[u8], timestamp: u64) -> moq_video::Frame {
333 let surface = moq_video::Surface::rgba(rgba, moq_video::Size::new(320, 240)).unwrap();
334 moq_video::Frame::new(surface, moq_net::Timestamp::from_micros(timestamp).unwrap())
335 }
336
337 fn source_broadcast(groups: u64, frames: u64) -> Source {
340 let mut broadcast = moq_net::broadcast::Info::default().produce();
341 let mut catalog = moq_mux::catalog::Producer::new(&mut broadcast, moq_mux::catalog::Config::default()).unwrap();
342
343 let mut video = hang::catalog::VideoConfig::new(hang::catalog::H264 {
344 inline: true,
345 profile: 0x42,
346 constraints: 0,
347 level: 30,
348 });
349 video.coded_width = Some(320);
350 video.coded_height = Some(240);
351 video.bitrate = Some(1_000_000);
352 video.framerate = Some(30.0);
353 catalog.modify().unwrap().video.insert("video", video).unwrap();
354
355 let info = hang::container::track_info(hang::catalog::PRIORITY.video);
356 let track = broadcast.create_track("video", info).unwrap();
357
358 let mut encoder = moq_video::encode::Encoder::new(&{
359 let mut config = moq_video::encode::Config::new(320, 240, moq_video::Rate::new(30, 1).unwrap());
360 config.kind = moq_video::encode::Kind::Software;
361 config
362 })
363 .unwrap();
364 let gray = vec![0x80u8; 320 * 240 * 4];
365
366 for sequence in 0..groups {
367 let mut group = track.create_group(sequence.into()).unwrap();
368 for index in 0..frames {
369 let timestamp = (sequence * frames + index) * 33_333;
370 if index == 0 {
371 encoder.cut().unwrap();
372 }
373 for encoded in encoder.encode(&gray_frame(&gray, timestamp)).unwrap() {
374 let frame = hang::container::Frame {
375 timestamp: encoded.timestamp,
376 payload: encoded.payload,
377 };
378 frame.write_to(&mut group).unwrap();
379 }
380 }
381 group.finish().unwrap();
382 }
383
384 Source {
385 broadcast,
386 catalog,
387 _track: track,
388 size: (320, 240),
389 }
390 }
391
392 fn source_broadcast_live(groups: u64, frames: u64) -> (Source, tokio::task::JoinHandle<()>) {
397 let mut broadcast = moq_net::broadcast::Info::default().produce();
398 let mut catalog = moq_mux::catalog::Producer::new(&mut broadcast, moq_mux::catalog::Config::default()).unwrap();
399
400 let mut video = hang::catalog::VideoConfig::new(hang::catalog::H264 {
401 inline: true,
402 profile: 0x42,
403 constraints: 0,
404 level: 30,
405 });
406 video.coded_width = Some(320);
407 video.coded_height = Some(240);
408 video.bitrate = Some(1_000_000);
409 video.framerate = Some(30.0);
410 catalog.modify().unwrap().video.insert("video", video).unwrap();
411
412 let info = hang::container::track_info(hang::catalog::PRIORITY.video);
413 let track = broadcast.create_track("video", info).unwrap();
414
415 let source = Source {
416 broadcast,
417 catalog,
418 _track: track.clone(),
421 size: (320, 240),
422 };
423
424 let task = tokio::spawn(async move {
425 let mut encoder = moq_video::encode::Sink::open(&{
426 let mut config = moq_video::encode::Config::new(320, 240, moq_video::Rate::new(30, 1).unwrap());
427 config.kind = moq_video::encode::Kind::Software;
428 config
429 })
430 .await
431 .unwrap();
432 let gray = vec![0x80u8; 320 * 240 * 4];
433
434 for sequence in 0..groups {
435 tokio::time::sleep(std::time::Duration::from_millis(100)).await;
439 let mut group = track.create_group(sequence.into()).unwrap();
440 for index in 0..frames {
441 let timestamp = (sequence * frames + index) * 33_333;
442 if index == 0 {
443 encoder.cut().await.unwrap();
444 }
445 for encoded in encoder.encode(gray_frame(&gray, timestamp)).await.unwrap() {
446 let frame = hang::container::Frame {
447 timestamp: encoded.timestamp,
448 payload: encoded.payload,
449 };
450 frame.write_to(&mut group).unwrap();
451 }
452 }
453 group.finish().unwrap();
454 }
455 std::future::pending::<()>().await;
457 });
458
459 (source, task)
460 }
461
462 #[tokio::test]
465 async fn live_multi_rung() {
466 let (source, producer_task) = source_broadcast_live(3, 5);
473 let config = Config {
474 ladder: Ladder::new([
475 Rung::new(120, moq_net::bandwidth::Rate::from_bps(100_000)),
476 Rung::new(60, moq_net::bandwidth::Rate::from_bps(50_000)),
477 ])
478 .unwrap(),
479 encoder: moq_video::encode::Kind::Software,
480 decoder: moq_video::decode::Kind::Software,
481 source: None,
482 ..Default::default()
483 };
484
485 let output = moq_net::broadcast::Info::default().produce();
486 let consumer = output.consume();
487 let transcoder = tokio::spawn(run(source.broadcast.consume(), output, config));
488
489 let mut subscribers = Vec::new();
492 for name in ["video/120p", "video/60p"] {
493 let track = loop {
494 match consumer.track(name) {
495 Ok(track) => break track,
496 Err(moq_net::Error::NotFound) => tokio::task::yield_now().await,
497 Err(err) => panic!("rung track {name}: {err}"),
498 }
499 };
500 subscribers.push((name, track.subscribe(None).await.unwrap().ordered()));
501 }
502
503 for (name, subscriber) in &mut subscribers {
505 let mut group = subscriber.next_group().await.unwrap().unwrap();
506 let payload = group.read_frame().await.unwrap().unwrap();
507 let frame = hang::container::Frame::decode(payload.payload).unwrap();
508 assert!(
509 frame.payload.starts_with(&[0, 0, 0, 1]) || frame.payload.starts_with(&[0, 0, 1]),
510 "{name} output is not Annex-B"
511 );
512 while group.read_frame().await.unwrap().is_some() {}
513 assert_eq!(group.frame_count(), 5, "{name} dropped frames");
514 }
515
516 producer_task.abort();
517 transcoder.abort();
518 }
519
520 #[cfg_attr(
524 target_os = "windows",
525 ignore = "explicit live-DXVA GPU probe; VideoProcessorBlt can hang on affected drivers"
526 )]
527 #[tokio::test]
528 async fn live_multi_rung_hardware() {
529 if !hardware_available() {
530 eprintln!("skipping: no hardware decoder + encoder available");
531 return;
532 }
533 let (source, producer_task) = source_broadcast_live(3, 5);
540 let config = Config {
544 ladder: Ladder::new([
545 Rung::new(180, moq_net::bandwidth::Rate::from_bps(200_000)),
546 Rung::new(120, moq_net::bandwidth::Rate::from_bps(100_000)),
547 ])
548 .unwrap(),
549 encoder: moq_video::encode::Kind::Hardware,
550 decoder: moq_video::decode::Kind::Hardware,
551 source: None,
552 ..Default::default()
553 };
554
555 let output = moq_net::broadcast::Info::default().produce();
556 let consumer = output.consume();
557 let transcoder = tokio::spawn(run(source.broadcast.consume(), output, config));
558
559 let mut subscribers = Vec::new();
560 for name in ["video/180p", "video/120p"] {
561 let track = loop {
562 match consumer.track(name) {
563 Ok(track) => break track,
564 Err(moq_net::Error::NotFound) => tokio::task::yield_now().await,
565 Err(err) => panic!("rung track {name}: {err}"),
566 }
567 };
568 subscribers.push((name, track.subscribe(None).await.unwrap().ordered()));
569 }
570
571 for (name, subscriber) in &mut subscribers {
572 let mut group = subscriber.next_group().await.unwrap().unwrap();
573 let payload = group.read_frame().await.unwrap().unwrap();
574 let frame = hang::container::Frame::decode(payload.payload).unwrap();
575 assert!(
576 frame.payload.starts_with(&[0, 0, 0, 1]) || frame.payload.starts_with(&[0, 0, 1]),
577 "{name} output is not Annex-B"
578 );
579 while group.read_frame().await.unwrap().is_some() {}
580 assert_eq!(group.frame_count(), 5, "{name} dropped frames");
581 }
582
583 producer_task.abort();
584 transcoder.abort();
585 }
586
587 fn hardware_available() -> bool {
591 let mut encode = moq_video::encode::Config::new(160, 120, moq_video::Rate::new(30, 1).unwrap());
592 encode.kind = moq_video::encode::Kind::Hardware;
593 if moq_video::encode::Encoder::new(&encode).is_err() {
594 return false;
595 }
596
597 let video = hang::catalog::VideoConfig::new(hang::catalog::H264 {
598 inline: true,
599 profile: 0x42,
600 constraints: 0,
601 level: 30,
602 });
603 let mut decode = moq_video::decode::Config::new();
604 decode.kind = moq_video::decode::Kind::Hardware;
605 moq_video::decode::Decoder::new(&video, &decode).is_ok()
606 }
607
608 #[cfg(feature = "vaapi")]
609 fn vaapi_decoder_available() -> bool {
610 let video = hang::catalog::VideoConfig::new(hang::catalog::H264 {
611 inline: true,
612 profile: 0x42,
613 constraints: 0,
614 level: 30,
615 });
616 let mut decode = moq_video::decode::Config::new();
617 decode.kind = moq_video::decode::Kind::Named("vaapi".to_string());
618 moq_video::decode::Decoder::new(&video, &decode).is_ok()
619 }
620
621 #[cfg(feature = "vaapi")]
624 #[tokio::test]
625 async fn vaapi_fetch_keeps_the_buffered_tail() {
626 let source = source_broadcast(1, 5);
627 let config = Config {
628 ladder: Ladder::new([Rung::new(120, moq_net::bandwidth::Rate::from_bps(100_000))]).unwrap(),
629 encoder: moq_video::encode::Kind::Software,
630 decoder: moq_video::decode::Kind::Named("vaapi".to_string()),
631 source: None,
632 ..Default::default()
633 };
634
635 if !vaapi_decoder_available() {
636 eprintln!("skipping: no VA-API H.264 decoder");
637 return;
638 }
639 let output = moq_net::broadcast::Info::default().produce();
640 let consumer = output.consume();
641 let transcoder = tokio::spawn(run(source.broadcast.consume(), output, config));
642
643 let track = loop {
644 match consumer.track("video/120p") {
645 Ok(track) => break track,
646 Err(moq_net::Error::NotFound) => tokio::task::yield_now().await,
647 Err(err) => panic!("rung track: {err}"),
648 }
649 };
650 let mut fetched = track.fetch_group(0, None).await.unwrap();
651 while fetched.read_frame().await.unwrap().is_some() {}
652 assert_eq!(
653 fetched.frame_count(),
654 5,
655 "VAAPI dropped the fetched group's buffered tail"
656 );
657
658 transcoder.abort();
659 }
660
661 #[cfg(feature = "vaapi")]
664 #[tokio::test]
665 async fn vaapi_live_keeps_the_buffered_tail_in_its_group() {
666 if !vaapi_decoder_available() {
667 eprintln!("skipping: no VA-API H.264 decoder");
668 return;
669 }
670
671 let (source, producer_task) = source_broadcast_live(1, 5);
672 let config = Config {
673 ladder: Ladder::new([Rung::new(120, moq_net::bandwidth::Rate::from_bps(100_000))]).unwrap(),
674 encoder: moq_video::encode::Kind::Software,
675 decoder: moq_video::decode::Kind::Named("vaapi".to_string()),
676 source: None,
677 ..Default::default()
678 };
679
680 let output = moq_net::broadcast::Info::default().produce();
681 let consumer = output.consume();
682 let transcoder = tokio::spawn(run(source.broadcast.consume(), output, config));
683
684 let track = loop {
685 match consumer.track("video/120p") {
686 Ok(track) => break track,
687 Err(moq_net::Error::NotFound) => tokio::task::yield_now().await,
688 Err(err) => panic!("rung track: {err}"),
689 }
690 };
691 let mut subscriber = track.subscribe(None).await.unwrap();
692 let mut group = subscriber.recv_group().await.unwrap().unwrap();
693 while group.read_frame().await.unwrap().is_some() {}
694 assert_eq!(
695 group.frame_count(),
696 5,
697 "VAAPI moved the live group's buffered tail past its end"
698 );
699
700 producer_task.abort();
701 transcoder.abort();
702 }
703
704 #[cfg_attr(
709 target_os = "windows",
710 ignore = "explicit live-DXVA GPU probe; VideoProcessorBlt can hang on affected drivers"
711 )]
712 #[tokio::test]
713 async fn end_to_end_hardware() {
714 if !hardware_available() {
715 eprintln!("skipping: no hardware decoder + encoder available");
716 return;
717 }
718
719 let source = source_broadcast(2, 5);
720 let config = Config {
721 ladder: Ladder::new([Rung::new(120, moq_net::bandwidth::Rate::from_bps(100_000))]).unwrap(),
722 encoder: moq_video::encode::Kind::Hardware,
723 decoder: moq_video::decode::Kind::Hardware,
724 source: None,
725 ..Default::default()
726 };
727
728 let output = moq_net::broadcast::Info::default().produce();
729 let consumer = output.consume();
730 let transcoder = tokio::spawn(run(source.broadcast.consume(), output, config));
731
732 let track = loop {
735 match consumer.track("video/120p") {
736 Ok(track) => break track,
737 Err(moq_net::Error::NotFound) => tokio::task::yield_now().await,
738 Err(err) => panic!("rung track: {err}"),
739 }
740 };
741 let mut fetched = track.fetch_group(0, None).await.unwrap();
742 let payload = fetched.read_frame().await.unwrap().unwrap();
743 let frame = hang::container::Frame::decode(payload.payload).unwrap();
744 assert!(
745 frame.payload.starts_with(&[0, 0, 0, 1]) || frame.payload.starts_with(&[0, 0, 1]),
746 "hardware rung output is not Annex-B"
747 );
748 while fetched.read_frame().await.unwrap().is_some() {}
749 assert_eq!(fetched.frame_count(), 5, "hardware transcode dropped frames");
750
751 transcoder.abort();
752 }
753
754 #[tokio::test]
755 async fn end_to_end() {
756 let source = source_broadcast(2, 5);
757
758 let config = Config {
759 ladder: Ladder::new([Rung::new(120, moq_net::bandwidth::Rate::from_bps(100_000))]).unwrap(),
760 encoder: moq_video::encode::Kind::Software,
761 decoder: moq_video::decode::Kind::Software,
762 source: Some(moq_net::path::RelativeOwned::from(".".to_string())),
763 ..Default::default()
764 };
765
766 let origin = moq_tokio::origin::spawn();
770 let output = origin.create_broadcast("room/transcode").unwrap();
771 let consumer = output.consume();
772 let transcoder = tokio::spawn(run(source.broadcast.consume(), output, config));
773
774 let track = loop {
779 match consumer.track(hang::Catalog::DEFAULT_NAME) {
780 Ok(track) => break track,
781 Err(moq_net::Error::NotFound) => tokio::task::yield_now().await,
782 Err(err) => panic!("catalog track: {err}"),
783 }
784 };
785 let track = track.subscribe(None).await.unwrap();
786 let mut catalogs = moq_mux::catalog::hang::Consumer::<()>::new(track);
787 let derived = loop {
790 let snapshot = catalogs.next().await.unwrap().unwrap();
791 if snapshot.video.renditions.contains_key("video/120p") {
792 break snapshot;
793 }
794 };
795
796 let rung = derived.video.renditions.get("video/120p").expect("rung missing");
797 assert_eq!(rung.coded_width, Some(160));
798 assert_eq!(rung.coded_height, Some(120));
799 assert_eq!(rung.bitrate, Some(100_000));
800 assert!(rung.codec.to_string().starts_with("avc3."));
801
802 let passthrough = derived.video.renditions.get("video").expect("passthrough missing");
803 assert_eq!(passthrough.broadcast.as_ref().map(|b| b.as_ref()), Some("."));
804
805 let mut subscriber = consumer
808 .track("video/120p")
809 .unwrap()
810 .subscribe(None)
811 .await
812 .unwrap()
813 .ordered();
814 let mut group = subscriber.next_group().await.unwrap().unwrap();
815 assert!(group.sequence <= 1, "unexpected sequence {}", group.sequence);
816 let payload = group.read_frame().await.unwrap().unwrap();
817 let frame = hang::container::Frame::decode(payload.payload).unwrap();
818 assert!(
819 frame.payload.starts_with(&[0, 0, 0, 1]) || frame.payload.starts_with(&[0, 0, 1]),
820 "rung output is not Annex-B"
821 );
822
823 let mut fetched = consumer
825 .track("video/120p")
826 .unwrap()
827 .fetch_group(0, None)
828 .await
829 .unwrap();
830 let mut timestamps = Vec::new();
831 let mut first_payload = None;
832 while let Some(payload) = fetched.read_frame().await.unwrap() {
833 let frame = hang::container::Frame::decode(payload.payload).unwrap();
834 assert!(!frame.payload.is_empty());
835 timestamps.push(frame.timestamp.as_micros());
836 first_payload = first_payload.or(Some(frame.payload));
837 }
838
839 let types = nal_types(&first_payload.expect("the group had no frames"));
845 assert!(types.contains(&7), "group does not open with an SPS: {types:?}");
846 assert!(types.contains(&8), "group does not open with a PPS: {types:?}");
847 assert!(types.contains(&5), "group does not open with an IDR: {types:?}");
848 assert_eq!(timestamps, (0..5).map(|i| i * 33_333).collect::<Vec<u128>>());
852 let total = fetched.finished().await.unwrap();
855 assert_eq!(total, 5);
856
857 transcoder.abort();
858 }
859
860 #[tokio::test]
865 async fn reports_active_rungs() {
866 let source = source_broadcast(2, 5);
867
868 let config = Config {
869 ladder: Ladder::new([Rung::new(120, moq_net::bandwidth::Rate::from_bps(100_000))]).unwrap(),
870 encoder: moq_video::encode::Kind::Software,
871 decoder: moq_video::decode::Kind::Software,
872 source: None,
873 ..Default::default()
874 };
875
876 let output = moq_net::broadcast::Info::default().produce();
877 let consumer = output.consume();
878 let transcoder = Transcoder::new(source.broadcast.consume(), output, config).unwrap();
879 let mut active = transcoder.active();
880 let driver = tokio::spawn(transcoder.run());
881
882 let update = active.next().await.unwrap();
884 let rendition = update.rendition;
885 assert_eq!(rendition.name(), "video/120p");
886 assert_eq!(rendition.size().height, 120);
887 assert_eq!(rendition.bitrate(), moq_net::bandwidth::Rate::from_bps(100_000));
888 assert!(!update.encoding, "encoding before anyone asked");
889 assert_eq!(rendition.frames(), 0);
890
891 let mut subscriber = consumer
892 .track("video/120p")
893 .unwrap()
894 .subscribe(None)
895 .await
896 .unwrap()
897 .ordered();
898 let update = active.next().await.unwrap();
899 assert_eq!(update.rendition.name(), "video/120p");
900 assert!(update.encoding);
901
902 let mut group = subscriber.next_group().await.unwrap().unwrap();
904 group.read_frame().await.unwrap().unwrap();
905 assert!(rendition.frames() > 0);
906 assert!(rendition.bytes() > 0);
907
908 drop(group);
910 drop(subscriber);
911 let update = active.next().await.unwrap();
912 assert_eq!(update.rendition.name(), "video/120p");
913 assert!(!update.encoding);
914
915 assert!(rendition.frames() > 0);
917 assert!(rendition.bytes() > 0);
918
919 driver.abort();
920 }
921
922 #[tokio::test]
933 async fn a_resized_rung_takes_a_fresh_name() {
934 let mut source = source_catalog(640, 360);
935
936 let config = Config {
937 ladder: Ladder::new([Rung::new(120, moq_net::bandwidth::Rate::from_bps(100_000))]).unwrap(),
938 encoder: moq_video::encode::Kind::Software,
939 decoder: moq_video::decode::Kind::Software,
940 source: None,
941 ..Default::default()
942 };
943
944 let output = moq_net::broadcast::Info::default().produce();
945 let consumer = output.consume();
946 let transcoder = tokio::spawn(run(source.broadcast.consume(), output, config));
947
948 let track = loop {
949 match consumer.track(hang::Catalog::DEFAULT_NAME) {
950 Ok(track) => break track,
951 Err(moq_net::Error::NotFound) => tokio::task::yield_now().await,
952 Err(err) => panic!("catalog track: {err}"),
953 }
954 };
955 let mut catalogs = moq_mux::catalog::hang::Consumer::<()>::new(track.subscribe(None).await.unwrap());
956
957 let derived = await_catalog(&mut catalogs, |snapshot| {
958 snapshot.video.renditions.contains_key("video/120p")
959 })
960 .await;
961 assert_eq!(
962 derived.video.renditions.get("video/120p").and_then(|v| v.coded_width),
963 Some(212),
964 "640x360 should give 120p a 212 wide picture"
965 );
966 let mut retired = subscribe(&consumer, "video/120p").await;
967
968 source.resize(480, 360);
970
971 let derived = await_catalog(&mut catalogs, |snapshot| {
972 !snapshot.video.renditions.contains_key("video/120p")
973 })
974 .await;
975 let replacement = derived
976 .video
977 .renditions
978 .get("video/120p.2")
979 .expect("the resized rung was not republished under a fresh name");
980 assert_eq!(replacement.coded_width, Some(160));
981 assert_eq!(replacement.coded_height, Some(120));
982
983 let ended = tokio::time::timeout(std::time::Duration::from_secs(5), retired.recv_group())
986 .await
987 .expect("the retired rung never ended its track")
988 .expect("the retired rung aborted instead of finishing");
989 assert!(ended.is_none(), "expected a clean end, got a group");
990
991 subscribe(&consumer, "video/120p.2").await;
993
994 transcoder.abort();
995 }
996
997 #[tokio::test]
1002 async fn ladder_follows_a_source_resize() {
1003 let mut source = source_catalog(640, 360);
1004
1005 let config = Config {
1006 ladder: Ladder::new([
1009 Rung::new(360, moq_net::bandwidth::Rate::from_bps(900_000)),
1010 Rung::new(240, moq_net::bandwidth::Rate::from_bps(300_000)),
1011 Rung::new(120, moq_net::bandwidth::Rate::from_bps(100_000)),
1012 ])
1013 .unwrap(),
1014 encoder: moq_video::encode::Kind::Software,
1015 decoder: moq_video::decode::Kind::Software,
1016 source: Some(moq_net::path::RelativeOwned::from(".".to_string())),
1017 ..Default::default()
1018 };
1019
1020 let output = moq_net::broadcast::Info::default().produce();
1021 let consumer = output.consume();
1022 let transcoder = tokio::spawn(run(source.broadcast.consume(), output, config));
1023
1024 let track = loop {
1025 match consumer.track(hang::Catalog::DEFAULT_NAME) {
1026 Ok(track) => break track,
1027 Err(moq_net::Error::NotFound) => tokio::task::yield_now().await,
1028 Err(err) => panic!("catalog track: {err}"),
1029 }
1030 };
1031 let mut catalogs = moq_mux::catalog::hang::Consumer::<()>::new(track.subscribe(None).await.unwrap());
1032
1033 let derived = await_catalog(&mut catalogs, |snapshot| {
1034 snapshot.video.renditions.contains_key("video/360p")
1035 })
1036 .await;
1037 assert!(derived.video.renditions.contains_key("video/240p"));
1038 assert!(derived.video.renditions.contains_key("video/120p"));
1039 assert_eq!(
1040 derived.video.renditions.get("video").and_then(|v| v.coded_width),
1041 Some(640),
1042 "the passthrough entry should describe the source"
1043 );
1044
1045 let mut retired = subscribe(&consumer, "video/240p").await;
1048 let mut kept = subscribe(&consumer, "video/120p").await;
1049
1050 source.resize(320, 180);
1053
1054 let derived = await_catalog(&mut catalogs, |snapshot| {
1055 !snapshot.video.renditions.contains_key("video/360p")
1056 })
1057 .await;
1058 assert!(
1059 !derived.video.renditions.contains_key("video/240p"),
1060 "240p outlived the resize"
1061 );
1062 let rung = derived
1063 .video
1064 .renditions
1065 .get("video/120p")
1066 .expect("120p was retired too");
1067 assert_eq!(rung.coded_width, Some(212));
1068 assert_eq!(rung.coded_height, Some(120));
1069 assert_eq!(
1070 derived.video.renditions.get("video").and_then(|v| v.coded_width),
1071 Some(320),
1072 "the passthrough entry should follow the source"
1073 );
1074
1075 let ended = tokio::time::timeout(std::time::Duration::from_secs(5), retired.recv_group())
1079 .await
1080 .expect("the retired rung never ended its track")
1081 .expect("the retired rung aborted instead of finishing");
1082 assert!(ended.is_none(), "expected a clean end, got a group");
1083
1084 assert!(
1087 tokio::time::timeout(std::time::Duration::from_millis(100), kept.recv_group())
1088 .await
1089 .is_err(),
1090 "a rung that still fits was retired anyway"
1091 );
1092
1093 transcoder.abort();
1094 }
1095
1096 #[tokio::test]
1108 async fn retirement_rides_out_an_open_live_group() {
1109 let mut source = source_catalog(320, 240);
1110 let mut group = source._track.create_group(0u64.into()).unwrap();
1111 write_keyframe(&mut group);
1112
1113 let config = Config {
1114 ladder: Ladder::new([Rung::new(120, moq_net::bandwidth::Rate::from_bps(100_000))]).unwrap(),
1115 encoder: moq_video::encode::Kind::Software,
1116 decoder: moq_video::decode::Kind::Software,
1117 source: None,
1118 ..Default::default()
1119 };
1120 let output = moq_net::broadcast::Info::default().produce();
1121 let consumer = output.consume();
1122 let transcoder = tokio::spawn(run(source.broadcast.consume(), output, config));
1123
1124 let catalog = loop {
1125 match consumer.track(hang::Catalog::DEFAULT_NAME) {
1126 Ok(track) => break track,
1127 Err(moq_net::Error::NotFound) => tokio::task::yield_now().await,
1128 Err(err) => panic!("catalog track: {err}"),
1129 }
1130 };
1131 let mut catalogs = moq_mux::catalog::hang::Consumer::<()>::new(catalog.subscribe(None).await.unwrap());
1132 await_catalog(&mut catalogs, |snapshot| {
1133 snapshot.video.renditions.contains_key("video/120p")
1134 })
1135 .await;
1136
1137 let rung = consumer.track("video/120p").unwrap();
1141 rung.query().await.unwrap();
1142 while rung.latest() != Some(0) {
1143 tokio::task::yield_now().await;
1144 }
1145 let mut fetched = rung.fetch_group(0, None).await.unwrap();
1146
1147 source.resize(160, 90);
1150 tokio::time::timeout(
1151 std::time::Duration::from_secs(5),
1152 await_catalog(&mut catalogs, |snapshot| {
1153 !snapshot.video.renditions.contains_key("video/120p")
1154 }),
1155 )
1156 .await
1157 .expect("the ladder never retired the rung");
1158 group.finish().unwrap();
1159
1160 let finished = tokio::time::timeout(std::time::Duration::from_secs(5), async {
1161 while fetched.read_frame().await?.is_some() {}
1162 fetched.finished().await
1163 })
1164 .await
1165 .expect("the accepted fetch never finished");
1166 assert!(finished.is_ok(), "retirement aborted the accepted group: {finished:?}");
1167
1168 transcoder.abort();
1169 }
1170
1171 #[tokio::test]
1185 async fn retirement_waits_for_an_unclaimed_fetch() {
1186 let mut source = source_catalog(320, 240);
1187 let source_fetches = source._track.dynamic();
1190
1191 let config = Config {
1192 ladder: Ladder::new([Rung::new(120, moq_net::bandwidth::Rate::from_bps(100_000))]).unwrap(),
1193 encoder: moq_video::encode::Kind::Software,
1194 decoder: moq_video::decode::Kind::Software,
1195 source: None,
1196 ..Default::default()
1197 };
1198 let output = moq_net::broadcast::Info::default().produce();
1199 let consumer = output.consume();
1200 let transcoder = tokio::spawn(run(source.broadcast.consume(), output, config));
1201
1202 let catalog = loop {
1203 match consumer.track(hang::Catalog::DEFAULT_NAME) {
1204 Ok(track) => break track,
1205 Err(moq_net::Error::NotFound) => tokio::task::yield_now().await,
1206 Err(err) => panic!("catalog track: {err}"),
1207 }
1208 };
1209 let mut catalogs = moq_mux::catalog::hang::Consumer::<()>::new(catalog.subscribe(None).await.unwrap());
1210 await_catalog(&mut catalogs, |snapshot| {
1211 snapshot.video.renditions.contains_key("video/120p")
1212 })
1213 .await;
1214
1215 let rung = consumer.track("video/120p").unwrap();
1218 rung.query().await.unwrap();
1219 assert!(
1220 source._track.subscription_changed().await.unwrap().is_some(),
1221 "the rung never subscribed to the live source",
1222 );
1223
1224 let fetching = rung.fetch_group(7, None);
1227
1228 let request = source_fetches.requested_group().await.expect("the source track closed");
1231 assert_eq!(request.sequence(), 7);
1232
1233 source.resize(160, 90);
1236 await_catalog(&mut catalogs, |snapshot| {
1237 !snapshot.video.renditions.contains_key("video/120p")
1238 })
1239 .await;
1240 assert!(
1241 source._track.subscription_changed().await.unwrap().is_none(),
1242 "the rung kept its live source subscription after retirement",
1243 );
1244
1245 let mut group = request.accept(None).unwrap();
1248 write_keyframe(&mut group);
1249 group.finish().unwrap();
1250
1251 let mut fetched = fetching
1252 .await
1253 .expect("retirement finished the track before the fetch claimed its group");
1254 let frames = async {
1255 while fetched.read_frame().await?.is_some() {}
1256 fetched.finished().await
1257 }
1258 .await
1259 .expect("retirement aborted the accepted group");
1260 assert!(frames > 0, "the fetch claimed its group but produced no frames");
1261
1262 transcoder.abort();
1263 }
1264
1265 #[tokio::test]
1270 async fn a_rebuilt_decode_renames_every_rung() {
1271 let mut source = source_catalog(320, 240);
1272
1273 let config = Config {
1274 ladder: Ladder::new([Rung::new(120, moq_net::bandwidth::Rate::from_bps(100_000))]).unwrap(),
1275 encoder: moq_video::encode::Kind::Software,
1276 decoder: moq_video::decode::Kind::Software,
1277 source: None,
1278 ..Default::default()
1279 };
1280
1281 let output = moq_net::broadcast::Info::default().produce();
1282 let consumer = output.consume();
1283 let transcoder = tokio::spawn(run(source.broadcast.consume(), output, config));
1284
1285 let track = loop {
1286 match consumer.track(hang::Catalog::DEFAULT_NAME) {
1287 Ok(track) => break track,
1288 Err(moq_net::Error::NotFound) => tokio::task::yield_now().await,
1289 Err(err) => panic!("catalog track: {err}"),
1290 }
1291 };
1292 let mut catalogs = moq_mux::catalog::hang::Consumer::<()>::new(track.subscribe(None).await.unwrap());
1293 await_catalog(&mut catalogs, |snapshot| {
1294 snapshot.video.renditions.contains_key("video/120p")
1295 })
1296 .await;
1297 let mut retired = subscribe(&consumer, "video/120p").await;
1298
1299 source.describe(Some(bytes::Bytes::from_static(&[0x01, 0x42, 0x00, 0x1e])));
1302
1303 let derived = tokio::time::timeout(
1304 std::time::Duration::from_secs(5),
1305 await_catalog(&mut catalogs, |snapshot| {
1306 snapshot.video.renditions.contains_key("video/120p.2")
1307 }),
1308 )
1309 .await
1310 .expect("the rebuilt decode kept the retired rung name");
1311 assert!(
1312 !derived.video.renditions.contains_key("video/120p"),
1313 "the retired name is still advertised"
1314 );
1315 assert_eq!(
1316 derived.video.renditions.get("video/120p.2").and_then(|v| v.coded_width),
1317 Some(160),
1318 "the replacement should serve the same picture under a new name"
1319 );
1320
1321 let ended = tokio::time::timeout(std::time::Duration::from_secs(5), retired.recv_group())
1322 .await
1323 .expect("the retired rung never ended its track")
1324 .expect("the retired rung aborted instead of finishing");
1325 assert!(ended.is_none(), "expected a clean end, got a group");
1326 subscribe(&consumer, "video/120p.2").await;
1327
1328 transcoder.abort();
1329 }
1330
1331 #[tokio::test]
1334 async fn shuts_down_on_source_end() {
1335 let source = source_broadcast(1, 3);
1336
1337 let config = Config {
1338 ladder: Ladder::new([Rung::new(120, moq_net::bandwidth::Rate::from_bps(100_000))]).unwrap(),
1339 encoder: moq_video::encode::Kind::Software,
1340 decoder: moq_video::decode::Kind::Software,
1341 source: None,
1342 ..Default::default()
1343 };
1344
1345 let output = moq_net::broadcast::Info::default().produce();
1346 let consumer = output.consume();
1347 let transcoder = tokio::spawn(run(source.broadcast.consume(), output, config));
1348
1349 let track = loop {
1352 match consumer.track(hang::Catalog::DEFAULT_NAME) {
1353 Ok(track) => break track,
1354 Err(moq_net::Error::NotFound) => tokio::task::yield_now().await,
1355 Err(err) => panic!("catalog track: {err}"),
1356 }
1357 };
1358 let mut catalogs = moq_mux::catalog::hang::Consumer::<()>::new(track.subscribe(None).await.unwrap());
1359 catalogs.next().await.unwrap().unwrap();
1360
1361 drop(source);
1364
1365 let result = tokio::time::timeout(std::time::Duration::from_secs(5), transcoder).await;
1366 result.expect("run did not shut down within 5s").unwrap().unwrap();
1367 }
1368}