1pub mod active;
25pub mod ladder;
26
27mod catalog;
28mod config;
29mod error;
30mod feed;
31mod pipeline;
32mod rung;
33
34pub use config::Config;
35pub use ladder::{Ladder, Rung};
36
37#[allow(deprecated)]
38pub use config::source_reference;
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)?;
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 mut 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 {
144 let mut guard = derived.lock();
145 catalog::populate(&mut guard, &snapshot, ladder.rungs(), config.source.as_ref())?;
146 }
147
148 let mut tasks = tokio::task::JoinSet::new();
150 loop {
151 tokio::select! {
152 request = dynamic.requested_track() => {
153 let Ok(request) = request else { break };
155 match ladder.rung(request.name())? {
156 Some(rung) => { tasks.spawn(rung::serve(rung, request)); }
157 None => request.reject(moq_net::Error::NotFound),
158 }
159 },
160 update = catalogs.next() => match update {
161 Ok(Some(snapshot)) => {
162 ladder.follow(&snapshot.video).await?;
163 let mut guard = derived.lock();
164 catalog::populate(&mut guard, &snapshot, ladder.rungs(), config.source.as_ref())?;
165 }
166 Ok(None) => break,
168 Err(err) => {
169 tracing::debug!(%err, "source catalog ended");
170 break;
171 }
172 },
173 Some(result) = tasks.join_next() => match result {
174 Ok(Ok(())) => {}
175 Ok(Err(err)) => tracing::warn!(%err, "rung failed"),
176 Err(err) => tracing::warn!(%err, "rung panicked"),
177 }
178 }
179 }
180
181 tasks.shutdown().await;
188
189 derived.finish()?;
190 output.finish();
191 Ok(())
192 }
193}
194
195#[cfg(test)]
196mod tests {
197
198 use super::*;
199
200 struct Source {
203 broadcast: moq_net::broadcast::Producer,
204 catalog: moq_mux::catalog::Producer,
205 _track: moq_net::track::Producer,
206 size: (u32, u32),
209 }
210
211 impl Source {
212 fn resize(&mut self, width: u32, height: u32) {
216 self.publish(width, height, None);
217 }
218
219 fn describe(&mut self, description: Option<bytes::Bytes>) {
222 let (width, height) = self.size;
223 self.publish(width, height, description);
224 }
225
226 fn publish(&mut self, width: u32, height: u32, description: Option<bytes::Bytes>) {
227 let mut video = hang::catalog::VideoConfig::new(hang::catalog::H264 {
228 inline: true,
229 profile: 0x42,
230 constraints: 0,
231 level: 30,
232 });
233 video.coded_width = Some(width);
234 video.coded_height = Some(height);
235 video.bitrate = Some(1_000_000);
236 video.framerate = Some(30.0);
237 video.description = description;
238 self.size = (width, height);
239
240 let mut guard = self.catalog.lock();
241 guard.video = hang::catalog::Video::default();
242 guard.video.insert("video", video).unwrap();
243 }
244 }
245
246 fn source_catalog(width: u32, height: u32) -> Source {
249 let mut broadcast = moq_net::broadcast::Info::default().produce();
250 let catalog = moq_mux::catalog::Producer::new(&mut broadcast).unwrap();
251 let track = broadcast.create_track("video", hang::container::track_info()).unwrap();
252
253 let mut source = Source {
254 broadcast,
255 catalog,
256 _track: track,
257 size: (width, height),
258 };
259 source.resize(width, height);
260 source
261 }
262
263 async fn await_catalog(
266 catalogs: &mut moq_mux::catalog::hang::Consumer<()>,
267 ready: impl Fn(&moq_mux::catalog::hang::Catalog) -> bool,
268 ) -> moq_mux::catalog::hang::Catalog {
269 loop {
270 let snapshot = catalogs.next().await.unwrap().unwrap();
271 if ready(&snapshot) {
272 return snapshot;
273 }
274 }
275 }
276
277 async fn subscribe(consumer: &moq_net::broadcast::Consumer, name: &str) -> moq_net::track::Subscriber {
279 let track = loop {
280 match consumer.track(name) {
281 Ok(track) => break track,
282 Err(moq_net::Error::NotFound) => tokio::task::yield_now().await,
283 Err(err) => panic!("track {name}: {err}"),
284 }
285 };
286 track.subscribe(None).await.unwrap()
287 }
288
289 fn nal_types(annexb: &[u8]) -> Vec<u8> {
292 let mut types = Vec::new();
293 let mut i = 0;
294 while i + 3 < annexb.len() {
295 if annexb[i..i + 3] == [0, 0, 1] {
296 types.push(annexb[i + 3] & 0x1f);
297 i += 3;
298 } else {
299 i += 1;
300 }
301 }
302 types
303 }
304
305 fn write_keyframe(group: &mut moq_net::group::Producer) {
308 let mut encoder = moq_video::encode::Encoder::new(&{
309 let mut config = moq_video::encode::Config::new(320, 240, 30);
310 config.kind = moq_video::encode::Kind::Software;
311 config
312 })
313 .unwrap();
314 encoder.keyframe();
315 let gray = vec![0x80u8; 320 * 240 * 4];
316 for encoded in encoder.encode(&gray_frame(&gray, 0)).unwrap() {
317 hang::container::Frame {
318 timestamp: encoded.timestamp,
319 payload: encoded.payload,
320 }
321 .write_to(group)
322 .unwrap();
323 }
324 }
325
326 fn gray_frame(rgba: &[u8], timestamp: u64) -> moq_video::Frame {
328 let surface = moq_video::Surface::rgba(rgba, moq_video::Size::new(320, 240)).unwrap();
329 moq_video::Frame::new(surface, moq_net::Timestamp::from_micros(timestamp).unwrap())
330 }
331
332 fn source_broadcast(groups: u64, frames: u64) -> Source {
335 let mut broadcast = moq_net::broadcast::Info::default().produce();
336 let mut catalog = moq_mux::catalog::Producer::new(&mut broadcast).unwrap();
337
338 let mut video = hang::catalog::VideoConfig::new(hang::catalog::H264 {
339 inline: true,
340 profile: 0x42,
341 constraints: 0,
342 level: 30,
343 });
344 video.coded_width = Some(320);
345 video.coded_height = Some(240);
346 video.bitrate = Some(1_000_000);
347 video.framerate = Some(30.0);
348 catalog.lock().video.insert("video", video).unwrap();
349
350 let info = hang::container::track_info();
351 let mut track = broadcast.create_track("video", info).unwrap();
352
353 let mut encoder = moq_video::encode::Encoder::new(&{
354 let mut config = moq_video::encode::Config::new(320, 240, 30);
355 config.kind = moq_video::encode::Kind::Software;
356 config
357 })
358 .unwrap();
359 let gray = vec![0x80u8; 320 * 240 * 4];
360
361 for sequence in 0..groups {
362 let mut group = track.create_group(sequence.into()).unwrap();
363 for index in 0..frames {
364 let timestamp = (sequence * frames + index) * 33_333;
365 if index == 0 {
366 encoder.keyframe();
367 }
368 for encoded in encoder.encode(&gray_frame(&gray, timestamp)).unwrap() {
369 let frame = hang::container::Frame {
370 timestamp: encoded.timestamp,
371 payload: encoded.payload,
372 };
373 frame.write_to(&mut group).unwrap();
374 }
375 }
376 group.finish().unwrap();
377 }
378
379 Source {
380 broadcast,
381 catalog,
382 _track: track,
383 size: (320, 240),
384 }
385 }
386
387 fn source_broadcast_live(groups: u64, frames: u64) -> (Source, tokio::task::JoinHandle<()>) {
392 let mut broadcast = moq_net::broadcast::Info::default().produce();
393 let mut catalog = moq_mux::catalog::Producer::new(&mut broadcast).unwrap();
394
395 let mut video = hang::catalog::VideoConfig::new(hang::catalog::H264 {
396 inline: true,
397 profile: 0x42,
398 constraints: 0,
399 level: 30,
400 });
401 video.coded_width = Some(320);
402 video.coded_height = Some(240);
403 video.bitrate = Some(1_000_000);
404 video.framerate = Some(30.0);
405 catalog.lock().video.insert("video", video).unwrap();
406
407 let info = hang::container::track_info();
408 let mut track = broadcast.create_track("video", info).unwrap();
409
410 let source = Source {
411 broadcast,
412 catalog,
413 _track: track.clone(),
416 size: (320, 240),
417 };
418
419 let task = tokio::spawn(async move {
420 let mut encoder = moq_video::encode::Encoder::new(&{
421 let mut config = moq_video::encode::Config::new(320, 240, 30);
422 config.kind = moq_video::encode::Kind::Software;
423 config
424 })
425 .unwrap();
426 let gray = vec![0x80u8; 320 * 240 * 4];
427
428 for sequence in 0..groups {
429 tokio::time::sleep(std::time::Duration::from_millis(100)).await;
433 let mut group = track.create_group(sequence.into()).unwrap();
434 for index in 0..frames {
435 let timestamp = (sequence * frames + index) * 33_333;
436 if index == 0 {
437 encoder.keyframe();
438 }
439 for encoded in encoder.encode(&gray_frame(&gray, timestamp)).unwrap() {
440 let frame = hang::container::Frame {
441 timestamp: encoded.timestamp,
442 payload: encoded.payload,
443 };
444 frame.write_to(&mut group).unwrap();
445 }
446 }
447 group.finish().unwrap();
448 }
449 std::future::pending::<()>().await;
451 });
452
453 (source, task)
454 }
455
456 #[tokio::test]
459 async fn live_multi_rung() {
460 let (source, producer_task) = source_broadcast_live(3, 5);
467 let config = Config {
468 ladder: Ladder::new([Rung::new(120, 100_000), Rung::new(60, 50_000)]).unwrap(),
469 encoder: moq_video::encode::Kind::Software,
470 decoder: moq_video::decode::Kind::Software,
471 source: None,
472 ..Default::default()
473 };
474
475 let output = moq_net::broadcast::Info::default().produce();
476 let consumer = output.consume();
477 let transcoder = tokio::spawn(run(source.broadcast.consume(), output, config));
478
479 let mut subscribers = Vec::new();
482 for name in ["video/120p", "video/60p"] {
483 let track = loop {
484 match consumer.track(name) {
485 Ok(track) => break track,
486 Err(moq_net::Error::NotFound) => tokio::task::yield_now().await,
487 Err(err) => panic!("rung track {name}: {err}"),
488 }
489 };
490 subscribers.push((name, track.subscribe(None).await.unwrap()));
491 }
492
493 for (name, subscriber) in &mut subscribers {
495 let mut group = subscriber.next_group().await.unwrap().unwrap();
496 let payload = group.read_frame().await.unwrap().unwrap();
497 let frame = hang::container::Frame::decode(payload.payload).unwrap();
498 assert!(
499 frame.payload.starts_with(&[0, 0, 0, 1]) || frame.payload.starts_with(&[0, 0, 1]),
500 "{name} output is not Annex-B"
501 );
502 while group.read_frame().await.unwrap().is_some() {}
503 assert_eq!(group.frame_count(), 5, "{name} dropped frames");
504 }
505
506 producer_task.abort();
507 transcoder.abort();
508 }
509
510 #[cfg_attr(
514 target_os = "windows",
515 ignore = "explicit live-DXVA GPU probe; VideoProcessorBlt can hang on affected drivers"
516 )]
517 #[tokio::test]
518 async fn live_multi_rung_hardware() {
519 if !hardware_available() {
520 eprintln!("skipping: no hardware decoder + encoder available");
521 return;
522 }
523 let (source, producer_task) = source_broadcast_live(3, 5);
530 let mut config = Config {
534 ladder: Ladder::new([Rung::new(180, 200_000), Rung::new(120, 100_000)]).unwrap(),
535 encoder: moq_video::encode::Kind::Hardware,
536 decoder: moq_video::decode::Kind::Hardware,
537 source: None,
538 ..Default::default()
539 };
540 config.resize.acceleration = moq_video::resize::Acceleration::Gpu;
541
542 let output = moq_net::broadcast::Info::default().produce();
543 let consumer = output.consume();
544 let transcoder = tokio::spawn(run(source.broadcast.consume(), output, config));
545
546 let mut subscribers = Vec::new();
547 for name in ["video/180p", "video/120p"] {
548 let track = loop {
549 match consumer.track(name) {
550 Ok(track) => break track,
551 Err(moq_net::Error::NotFound) => tokio::task::yield_now().await,
552 Err(err) => panic!("rung track {name}: {err}"),
553 }
554 };
555 subscribers.push((name, track.subscribe(None).await.unwrap()));
556 }
557
558 for (name, subscriber) in &mut subscribers {
559 let mut group = subscriber.next_group().await.unwrap().unwrap();
560 let payload = group.read_frame().await.unwrap().unwrap();
561 let frame = hang::container::Frame::decode(payload.payload).unwrap();
562 assert!(
563 frame.payload.starts_with(&[0, 0, 0, 1]) || frame.payload.starts_with(&[0, 0, 1]),
564 "{name} output is not Annex-B"
565 );
566 while group.read_frame().await.unwrap().is_some() {}
567 assert_eq!(group.frame_count(), 5, "{name} dropped frames");
568 }
569
570 producer_task.abort();
571 transcoder.abort();
572 }
573
574 fn hardware_available() -> bool {
578 let mut encode = moq_video::encode::Config::new(160, 120, 30);
579 encode.kind = moq_video::encode::Kind::Hardware;
580 if moq_video::encode::Encoder::new(&encode).is_err() {
581 return false;
582 }
583
584 let video = hang::catalog::VideoConfig::new(hang::catalog::H264 {
585 inline: true,
586 profile: 0x42,
587 constraints: 0,
588 level: 30,
589 });
590 let mut decode = moq_video::decode::Config::new();
591 decode.kind = moq_video::decode::Kind::Hardware;
592 moq_video::decode::Decoder::new(&video, &decode).is_ok()
593 }
594
595 #[cfg(feature = "vaapi")]
596 fn vaapi_decoder_available() -> bool {
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::Named("vaapi".to_string());
605 moq_video::decode::Decoder::new(&video, &decode).is_ok()
606 }
607
608 #[cfg(feature = "vaapi")]
611 #[tokio::test]
612 async fn vaapi_fetch_keeps_the_buffered_tail() {
613 let source = source_broadcast(1, 5);
614 let config = Config {
615 ladder: Ladder::new([Rung::new(120, 100_000)]).unwrap(),
616 encoder: moq_video::encode::Kind::Software,
617 decoder: moq_video::decode::Kind::Named("vaapi".to_string()),
618 source: None,
619 ..Default::default()
620 };
621
622 if !vaapi_decoder_available() {
623 eprintln!("skipping: no VA-API H.264 decoder");
624 return;
625 }
626 let output = moq_net::broadcast::Info::default().produce();
627 let consumer = output.consume();
628 let transcoder = tokio::spawn(run(source.broadcast.consume(), output, config));
629
630 let track = loop {
631 match consumer.track("video/120p") {
632 Ok(track) => break track,
633 Err(moq_net::Error::NotFound) => tokio::task::yield_now().await,
634 Err(err) => panic!("rung track: {err}"),
635 }
636 };
637 let mut fetched = track.fetch_group(0, None).await.unwrap();
638 while fetched.read_frame().await.unwrap().is_some() {}
639 assert_eq!(
640 fetched.frame_count(),
641 5,
642 "VAAPI dropped the fetched group's buffered tail"
643 );
644
645 transcoder.abort();
646 }
647
648 #[cfg(feature = "vaapi")]
651 #[tokio::test]
652 async fn vaapi_live_keeps_the_buffered_tail_in_its_group() {
653 if !vaapi_decoder_available() {
654 eprintln!("skipping: no VA-API H.264 decoder");
655 return;
656 }
657
658 let (source, producer_task) = source_broadcast_live(1, 5);
659 let config = Config {
660 ladder: Ladder::new([Rung::new(120, 100_000)]).unwrap(),
661 encoder: moq_video::encode::Kind::Software,
662 decoder: moq_video::decode::Kind::Named("vaapi".to_string()),
663 source: None,
664 ..Default::default()
665 };
666
667 let output = moq_net::broadcast::Info::default().produce();
668 let consumer = output.consume();
669 let transcoder = tokio::spawn(run(source.broadcast.consume(), output, config));
670
671 let track = loop {
672 match consumer.track("video/120p") {
673 Ok(track) => break track,
674 Err(moq_net::Error::NotFound) => tokio::task::yield_now().await,
675 Err(err) => panic!("rung track: {err}"),
676 }
677 };
678 let mut subscriber = track.subscribe(None).await.unwrap();
679 let mut group = subscriber.next_group().await.unwrap().unwrap();
680 while group.read_frame().await.unwrap().is_some() {}
681 assert_eq!(
682 group.frame_count(),
683 5,
684 "VAAPI moved the live group's buffered tail past its end"
685 );
686
687 producer_task.abort();
688 transcoder.abort();
689 }
690
691 #[cfg_attr(
696 target_os = "windows",
697 ignore = "explicit live-DXVA GPU probe; VideoProcessorBlt can hang on affected drivers"
698 )]
699 #[tokio::test]
700 async fn end_to_end_hardware() {
701 if !hardware_available() {
702 eprintln!("skipping: no hardware decoder + encoder available");
703 return;
704 }
705
706 let source = source_broadcast(2, 5);
707 let mut config = Config {
708 ladder: Ladder::new([Rung::new(120, 100_000)]).unwrap(),
709 encoder: moq_video::encode::Kind::Hardware,
710 decoder: moq_video::decode::Kind::Hardware,
711 source: None,
712 ..Default::default()
713 };
714 config.resize.acceleration = moq_video::resize::Acceleration::Gpu;
715
716 let output = moq_net::broadcast::Info::default().produce();
717 let consumer = output.consume();
718 let transcoder = tokio::spawn(run(source.broadcast.consume(), output, config));
719
720 let track = loop {
723 match consumer.track("video/120p") {
724 Ok(track) => break track,
725 Err(moq_net::Error::NotFound) => tokio::task::yield_now().await,
726 Err(err) => panic!("rung track: {err}"),
727 }
728 };
729 let mut fetched = track.fetch_group(0, None).await.unwrap();
730 let payload = fetched.read_frame().await.unwrap().unwrap();
731 let frame = hang::container::Frame::decode(payload.payload).unwrap();
732 assert!(
733 frame.payload.starts_with(&[0, 0, 0, 1]) || frame.payload.starts_with(&[0, 0, 1]),
734 "hardware rung output is not Annex-B"
735 );
736 while fetched.read_frame().await.unwrap().is_some() {}
737 assert_eq!(fetched.frame_count(), 5, "hardware transcode dropped frames");
738
739 transcoder.abort();
740 }
741
742 #[tokio::test]
743 async fn end_to_end() {
744 let source = source_broadcast(2, 5);
745
746 let config = Config {
747 ladder: Ladder::new([Rung::new(120, 100_000)]).unwrap(),
748 encoder: moq_video::encode::Kind::Software,
749 decoder: moq_video::decode::Kind::Software,
750 source: Some(moq_net::PathRelativeOwned::from(".".to_string())),
751 ..Default::default()
752 };
753
754 let output = moq_net::broadcast::Info::default().produce();
755 let consumer = output.consume();
756 let transcoder = tokio::spawn(run(source.broadcast.consume(), output, config));
757
758 let track = loop {
763 match consumer.track(hang::Catalog::DEFAULT_NAME) {
764 Ok(track) => break track,
765 Err(moq_net::Error::NotFound) => tokio::task::yield_now().await,
766 Err(err) => panic!("catalog track: {err}"),
767 }
768 };
769 let track = track.subscribe(None).await.unwrap();
770 let mut catalogs = moq_mux::catalog::hang::Consumer::<()>::new(track);
771 let derived = loop {
774 let snapshot = catalogs.next().await.unwrap().unwrap();
775 if snapshot.video.renditions.contains_key("video/120p") {
776 break snapshot;
777 }
778 };
779
780 let rung = derived.video.renditions.get("video/120p").expect("rung missing");
781 assert_eq!(rung.coded_width, Some(160));
782 assert_eq!(rung.coded_height, Some(120));
783 assert_eq!(rung.bitrate, Some(100_000));
784 assert!(rung.codec.to_string().starts_with("avc3."));
785
786 let passthrough = derived.video.renditions.get("video").expect("passthrough missing");
787 assert_eq!(passthrough.broadcast.as_ref().map(|b| b.as_ref()), Some("."));
788
789 let mut subscriber = consumer.track("video/120p").unwrap().subscribe(None).await.unwrap();
792 let mut group = subscriber.next_group().await.unwrap().unwrap();
793 assert!(group.sequence <= 1, "unexpected sequence {}", group.sequence);
794 let payload = group.read_frame().await.unwrap().unwrap();
795 let frame = hang::container::Frame::decode(payload.payload).unwrap();
796 assert!(
797 frame.payload.starts_with(&[0, 0, 0, 1]) || frame.payload.starts_with(&[0, 0, 1]),
798 "rung output is not Annex-B"
799 );
800
801 let mut fetched = consumer
803 .track("video/120p")
804 .unwrap()
805 .fetch_group(0, None)
806 .await
807 .unwrap();
808 let mut timestamps = Vec::new();
809 let mut first_payload = None;
810 while let Some(payload) = fetched.read_frame().await.unwrap() {
811 let frame = hang::container::Frame::decode(payload.payload).unwrap();
812 assert!(!frame.payload.is_empty());
813 timestamps.push(frame.timestamp.as_micros());
814 first_payload = first_payload.or(Some(frame.payload));
815 }
816
817 let types = nal_types(&first_payload.expect("the group had no frames"));
823 assert!(types.contains(&7), "group does not open with an SPS: {types:?}");
824 assert!(types.contains(&8), "group does not open with a PPS: {types:?}");
825 assert!(types.contains(&5), "group does not open with an IDR: {types:?}");
826 assert_eq!(timestamps, (0..5).map(|i| i * 33_333).collect::<Vec<u128>>());
830 let total = fetched.finished().await.unwrap();
833 assert_eq!(total, 5);
834
835 transcoder.abort();
836 }
837
838 #[tokio::test]
843 async fn reports_active_rungs() {
844 let source = source_broadcast(2, 5);
845
846 let config = Config {
847 ladder: Ladder::new([Rung::new(120, 100_000)]).unwrap(),
848 encoder: moq_video::encode::Kind::Software,
849 decoder: moq_video::decode::Kind::Software,
850 source: None,
851 ..Default::default()
852 };
853
854 let output = moq_net::broadcast::Info::default().produce();
855 let consumer = output.consume();
856 let transcoder = Transcoder::new(source.broadcast.consume(), output, config).unwrap();
857 let mut active = transcoder.active();
858 let driver = tokio::spawn(transcoder.run());
859
860 let update = active.next().await.unwrap();
862 let rendition = update.rendition;
863 assert_eq!(rendition.name(), "video/120p");
864 assert_eq!(rendition.size().height, 120);
865 assert_eq!(rendition.bitrate(), 100_000);
866 assert!(!update.encoding, "encoding before anyone asked");
867 assert_eq!(rendition.frames(), 0);
868
869 let mut subscriber = consumer.track("video/120p").unwrap().subscribe(None).await.unwrap();
870 let update = active.next().await.unwrap();
871 assert_eq!(update.rendition.name(), "video/120p");
872 assert!(update.encoding);
873
874 let mut group = subscriber.next_group().await.unwrap().unwrap();
876 group.read_frame().await.unwrap().unwrap();
877 assert!(rendition.frames() > 0);
878 assert!(rendition.bytes() > 0);
879
880 drop(group);
882 drop(subscriber);
883 let update = active.next().await.unwrap();
884 assert_eq!(update.rendition.name(), "video/120p");
885 assert!(!update.encoding);
886
887 assert!(rendition.frames() > 0);
889 assert!(rendition.bytes() > 0);
890
891 driver.abort();
892 }
893
894 #[tokio::test]
905 async fn a_resized_rung_takes_a_fresh_name() {
906 let mut source = source_catalog(640, 360);
907
908 let config = Config {
909 ladder: Ladder::new([Rung::new(120, 100_000)]).unwrap(),
910 encoder: moq_video::encode::Kind::Software,
911 decoder: moq_video::decode::Kind::Software,
912 source: None,
913 ..Default::default()
914 };
915
916 let output = moq_net::broadcast::Info::default().produce();
917 let consumer = output.consume();
918 let transcoder = tokio::spawn(run(source.broadcast.consume(), output, config));
919
920 let track = loop {
921 match consumer.track(hang::Catalog::DEFAULT_NAME) {
922 Ok(track) => break track,
923 Err(moq_net::Error::NotFound) => tokio::task::yield_now().await,
924 Err(err) => panic!("catalog track: {err}"),
925 }
926 };
927 let mut catalogs = moq_mux::catalog::hang::Consumer::<()>::new(track.subscribe(None).await.unwrap());
928
929 let derived = await_catalog(&mut catalogs, |snapshot| {
930 snapshot.video.renditions.contains_key("video/120p")
931 })
932 .await;
933 assert_eq!(
934 derived.video.renditions.get("video/120p").and_then(|v| v.coded_width),
935 Some(212),
936 "640x360 should give 120p a 212 wide picture"
937 );
938 let mut retired = subscribe(&consumer, "video/120p").await;
939
940 source.resize(480, 360);
942
943 let derived = await_catalog(&mut catalogs, |snapshot| {
944 !snapshot.video.renditions.contains_key("video/120p")
945 })
946 .await;
947 let replacement = derived
948 .video
949 .renditions
950 .get("video/120p.2")
951 .expect("the resized rung was not republished under a fresh name");
952 assert_eq!(replacement.coded_width, Some(160));
953 assert_eq!(replacement.coded_height, Some(120));
954
955 let ended = tokio::time::timeout(std::time::Duration::from_secs(5), retired.next_group())
958 .await
959 .expect("the retired rung never ended its track")
960 .expect("the retired rung aborted instead of finishing");
961 assert!(ended.is_none(), "expected a clean end, got a group");
962
963 subscribe(&consumer, "video/120p.2").await;
965
966 transcoder.abort();
967 }
968
969 #[tokio::test]
974 async fn ladder_follows_a_source_resize() {
975 let mut source = source_catalog(640, 360);
976
977 let config = Config {
978 ladder: Ladder::new([
981 Rung::new(360, 900_000),
982 Rung::new(240, 300_000),
983 Rung::new(120, 100_000),
984 ])
985 .unwrap(),
986 encoder: moq_video::encode::Kind::Software,
987 decoder: moq_video::decode::Kind::Software,
988 source: Some(moq_net::PathRelativeOwned::from(".".to_string())),
989 ..Default::default()
990 };
991
992 let output = moq_net::broadcast::Info::default().produce();
993 let consumer = output.consume();
994 let transcoder = tokio::spawn(run(source.broadcast.consume(), output, config));
995
996 let track = loop {
997 match consumer.track(hang::Catalog::DEFAULT_NAME) {
998 Ok(track) => break track,
999 Err(moq_net::Error::NotFound) => tokio::task::yield_now().await,
1000 Err(err) => panic!("catalog track: {err}"),
1001 }
1002 };
1003 let mut catalogs = moq_mux::catalog::hang::Consumer::<()>::new(track.subscribe(None).await.unwrap());
1004
1005 let derived = await_catalog(&mut catalogs, |snapshot| {
1006 snapshot.video.renditions.contains_key("video/360p")
1007 })
1008 .await;
1009 assert!(derived.video.renditions.contains_key("video/240p"));
1010 assert!(derived.video.renditions.contains_key("video/120p"));
1011 assert_eq!(
1012 derived.video.renditions.get("video").and_then(|v| v.coded_width),
1013 Some(640),
1014 "the passthrough entry should describe the source"
1015 );
1016
1017 let mut retired = subscribe(&consumer, "video/240p").await;
1020 let mut kept = subscribe(&consumer, "video/120p").await;
1021
1022 source.resize(320, 180);
1025
1026 let derived = await_catalog(&mut catalogs, |snapshot| {
1027 !snapshot.video.renditions.contains_key("video/360p")
1028 })
1029 .await;
1030 assert!(
1031 !derived.video.renditions.contains_key("video/240p"),
1032 "240p outlived the resize"
1033 );
1034 let rung = derived
1035 .video
1036 .renditions
1037 .get("video/120p")
1038 .expect("120p was retired too");
1039 assert_eq!(rung.coded_width, Some(212));
1040 assert_eq!(rung.coded_height, Some(120));
1041 assert_eq!(
1042 derived.video.renditions.get("video").and_then(|v| v.coded_width),
1043 Some(320),
1044 "the passthrough entry should follow the source"
1045 );
1046
1047 let ended = tokio::time::timeout(std::time::Duration::from_secs(5), retired.next_group())
1051 .await
1052 .expect("the retired rung never ended its track")
1053 .expect("the retired rung aborted instead of finishing");
1054 assert!(ended.is_none(), "expected a clean end, got a group");
1055
1056 assert!(
1059 tokio::time::timeout(std::time::Duration::from_millis(100), kept.next_group())
1060 .await
1061 .is_err(),
1062 "a rung that still fits was retired anyway"
1063 );
1064
1065 transcoder.abort();
1066 }
1067
1068 #[tokio::test]
1080 async fn retirement_rides_out_an_open_live_group() {
1081 let mut source = source_catalog(320, 240);
1082 let mut group = source._track.create_group(0u64.into()).unwrap();
1083 write_keyframe(&mut group);
1084
1085 let config = Config {
1086 ladder: Ladder::new([Rung::new(120, 100_000)]).unwrap(),
1087 encoder: moq_video::encode::Kind::Software,
1088 decoder: moq_video::decode::Kind::Software,
1089 source: None,
1090 ..Default::default()
1091 };
1092 let output = moq_net::broadcast::Info::default().produce();
1093 let consumer = output.consume();
1094 let transcoder = tokio::spawn(run(source.broadcast.consume(), output, config));
1095
1096 let catalog = loop {
1097 match consumer.track(hang::Catalog::DEFAULT_NAME) {
1098 Ok(track) => break track,
1099 Err(moq_net::Error::NotFound) => tokio::task::yield_now().await,
1100 Err(err) => panic!("catalog track: {err}"),
1101 }
1102 };
1103 let mut catalogs = moq_mux::catalog::hang::Consumer::<()>::new(catalog.subscribe(None).await.unwrap());
1104 await_catalog(&mut catalogs, |snapshot| {
1105 snapshot.video.renditions.contains_key("video/120p")
1106 })
1107 .await;
1108
1109 let rung = consumer.track("video/120p").unwrap();
1113 rung.info().await.unwrap();
1114 while rung.latest() != Some(0) {
1115 tokio::task::yield_now().await;
1116 }
1117 let mut fetched = rung.fetch_group(0, None).await.unwrap();
1118
1119 source.resize(160, 90);
1122 tokio::time::timeout(
1123 std::time::Duration::from_secs(5),
1124 await_catalog(&mut catalogs, |snapshot| {
1125 !snapshot.video.renditions.contains_key("video/120p")
1126 }),
1127 )
1128 .await
1129 .expect("the ladder never retired the rung");
1130 group.finish().unwrap();
1131
1132 let finished = tokio::time::timeout(std::time::Duration::from_secs(5), async {
1133 while fetched.read_frame().await?.is_some() {}
1134 fetched.finished().await
1135 })
1136 .await
1137 .expect("the accepted fetch never finished");
1138 assert!(finished.is_ok(), "retirement aborted the accepted group: {finished:?}");
1139
1140 transcoder.abort();
1141 }
1142
1143 #[tokio::test]
1157 async fn retirement_waits_for_an_unclaimed_fetch() {
1158 let mut source = source_catalog(320, 240);
1159 let source_fetches = source._track.dynamic();
1162
1163 let config = Config {
1164 ladder: Ladder::new([Rung::new(120, 100_000)]).unwrap(),
1165 encoder: moq_video::encode::Kind::Software,
1166 decoder: moq_video::decode::Kind::Software,
1167 source: None,
1168 ..Default::default()
1169 };
1170 let output = moq_net::broadcast::Info::default().produce();
1171 let consumer = output.consume();
1172 let transcoder = tokio::spawn(run(source.broadcast.consume(), output, config));
1173
1174 let catalog = loop {
1175 match consumer.track(hang::Catalog::DEFAULT_NAME) {
1176 Ok(track) => break track,
1177 Err(moq_net::Error::NotFound) => tokio::task::yield_now().await,
1178 Err(err) => panic!("catalog track: {err}"),
1179 }
1180 };
1181 let mut catalogs = moq_mux::catalog::hang::Consumer::<()>::new(catalog.subscribe(None).await.unwrap());
1182 await_catalog(&mut catalogs, |snapshot| {
1183 snapshot.video.renditions.contains_key("video/120p")
1184 })
1185 .await;
1186
1187 let rung = consumer.track("video/120p").unwrap();
1190 rung.info().await.unwrap();
1191 assert!(
1192 source._track.subscription_changed().await.unwrap().is_some(),
1193 "the rung never subscribed to the live source",
1194 );
1195
1196 let fetching = rung.fetch_group(7, None);
1199
1200 let request = source_fetches.requested_group().await.expect("the source track closed");
1203 assert_eq!(request.sequence(), 7);
1204
1205 source.resize(160, 90);
1208 await_catalog(&mut catalogs, |snapshot| {
1209 !snapshot.video.renditions.contains_key("video/120p")
1210 })
1211 .await;
1212 assert!(
1213 source._track.subscription_changed().await.unwrap().is_none(),
1214 "the rung kept its live source subscription after retirement",
1215 );
1216
1217 let mut group = request.accept(None).unwrap();
1220 write_keyframe(&mut group);
1221 group.finish().unwrap();
1222
1223 let mut fetched = fetching
1224 .await
1225 .expect("retirement finished the track before the fetch claimed its group");
1226 let frames = async {
1227 while fetched.read_frame().await?.is_some() {}
1228 fetched.finished().await
1229 }
1230 .await
1231 .expect("retirement aborted the accepted group");
1232 assert!(frames > 0, "the fetch claimed its group but produced no frames");
1233
1234 transcoder.abort();
1235 }
1236
1237 #[tokio::test]
1242 async fn a_rebuilt_decode_renames_every_rung() {
1243 let mut source = source_catalog(320, 240);
1244
1245 let config = Config {
1246 ladder: Ladder::new([Rung::new(120, 100_000)]).unwrap(),
1247 encoder: moq_video::encode::Kind::Software,
1248 decoder: moq_video::decode::Kind::Software,
1249 source: None,
1250 ..Default::default()
1251 };
1252
1253 let output = moq_net::broadcast::Info::default().produce();
1254 let consumer = output.consume();
1255 let transcoder = tokio::spawn(run(source.broadcast.consume(), output, config));
1256
1257 let track = loop {
1258 match consumer.track(hang::Catalog::DEFAULT_NAME) {
1259 Ok(track) => break track,
1260 Err(moq_net::Error::NotFound) => tokio::task::yield_now().await,
1261 Err(err) => panic!("catalog track: {err}"),
1262 }
1263 };
1264 let mut catalogs = moq_mux::catalog::hang::Consumer::<()>::new(track.subscribe(None).await.unwrap());
1265 await_catalog(&mut catalogs, |snapshot| {
1266 snapshot.video.renditions.contains_key("video/120p")
1267 })
1268 .await;
1269 let mut retired = subscribe(&consumer, "video/120p").await;
1270
1271 source.describe(Some(bytes::Bytes::from_static(&[0x01, 0x42, 0x00, 0x1e])));
1274
1275 let derived = tokio::time::timeout(
1276 std::time::Duration::from_secs(5),
1277 await_catalog(&mut catalogs, |snapshot| {
1278 snapshot.video.renditions.contains_key("video/120p.2")
1279 }),
1280 )
1281 .await
1282 .expect("the rebuilt decode kept the retired rung name");
1283 assert!(
1284 !derived.video.renditions.contains_key("video/120p"),
1285 "the retired name is still advertised"
1286 );
1287 assert_eq!(
1288 derived.video.renditions.get("video/120p.2").and_then(|v| v.coded_width),
1289 Some(160),
1290 "the replacement should serve the same picture under a new name"
1291 );
1292
1293 let ended = tokio::time::timeout(std::time::Duration::from_secs(5), retired.next_group())
1294 .await
1295 .expect("the retired rung never ended its track")
1296 .expect("the retired rung aborted instead of finishing");
1297 assert!(ended.is_none(), "expected a clean end, got a group");
1298 subscribe(&consumer, "video/120p.2").await;
1299
1300 transcoder.abort();
1301 }
1302
1303 #[tokio::test]
1306 async fn shuts_down_on_source_end() {
1307 let source = source_broadcast(1, 3);
1308
1309 let config = Config {
1310 ladder: Ladder::new([Rung::new(120, 100_000)]).unwrap(),
1311 encoder: moq_video::encode::Kind::Software,
1312 decoder: moq_video::decode::Kind::Software,
1313 source: None,
1314 ..Default::default()
1315 };
1316
1317 let output = moq_net::broadcast::Info::default().produce();
1318 let consumer = output.consume();
1319 let transcoder = tokio::spawn(run(source.broadcast.consume(), output, config));
1320
1321 let track = loop {
1324 match consumer.track(hang::Catalog::DEFAULT_NAME) {
1325 Ok(track) => break track,
1326 Err(moq_net::Error::NotFound) => tokio::task::yield_now().await,
1327 Err(err) => panic!("catalog track: {err}"),
1328 }
1329 };
1330 let mut catalogs = moq_mux::catalog::hang::Consumer::<()>::new(track.subscribe(None).await.unwrap());
1331 catalogs.next().await.unwrap().unwrap();
1332
1333 drop(source);
1336
1337 let result = tokio::time::timeout(std::time::Duration::from_secs(5), transcoder).await;
1338 result.expect("run did not shut down within 5s").unwrap().unwrap();
1339 }
1340}