1pub mod active;
25
26mod catalog;
27mod config;
28mod error;
29mod feed;
30mod rung;
31
32pub use config::{Config, Rung};
33
34#[allow(deprecated)]
35pub use config::source_reference;
36pub use error::Error;
37
38pub async fn run(
43 source: moq_net::broadcast::Consumer,
44 output: moq_net::broadcast::Producer,
45 config: Config,
46) -> Result<(), Error> {
47 Transcoder::new(source, output, config)?.run().await
48}
49
50pub struct Transcoder {
62 source: moq_net::broadcast::Consumer,
63 output: moq_net::broadcast::Producer,
64 config: Config,
65 derived: moq_mux::catalog::Producer,
66 dynamic: moq_net::broadcast::Dynamic,
68 active: active::Producer,
69}
70
71impl Transcoder {
72 pub fn new(
77 source: moq_net::broadcast::Consumer,
78 mut output: moq_net::broadcast::Producer,
79 config: Config,
80 ) -> Result<Self, Error> {
81 let derived = moq_mux::catalog::Producer::new(&mut output)?;
84 let dynamic = output.dynamic();
85
86 Ok(Self {
87 source,
88 output,
89 config,
90 derived,
91 dynamic,
92 active: active::Producer::default(),
93 })
94 }
95
96 pub fn active(&self) -> active::Consumer {
102 self.active.consume()
103 }
104
105 pub async fn run(self) -> Result<(), Error> {
107 let Self {
108 source,
109 mut output,
110 config,
111 mut derived,
112 mut dynamic,
113 active,
114 } = self;
115
116 let track = source
119 .track(hang::Catalog::DEFAULT_NAME)?
120 .subscribe(hang::Catalog::default_subscription())
121 .await?;
122 let mut catalogs = moq_mux::catalog::hang::Consumer::<()>::new(track);
123 let (source_name, source_config, snapshot) = loop {
124 let Some(snapshot) = catalogs.next().await? else {
125 return Err(Error::NoSource);
126 };
127 match catalog::choose_source(&snapshot.video) {
128 Ok((name, config)) => break (name, config, snapshot),
129 Err(_) => tracing::debug!("no transcodable rendition yet; waiting for a catalog update"),
130 }
131 };
132 let rungs = catalog::resolve_rungs(&config.rungs, &source_name, &source_config)?;
133 tracing::info!(source = %source_name, rungs = rungs.len(), "transcoding");
134 active.declare(&rungs);
137
138 let feed = feed::Feed::new(
141 source.track(&source_name)?,
142 source_config.clone(),
143 config.decoder.clone(),
144 );
145
146 let mut entries = Vec::with_capacity(rungs.len());
149 for rung in &rungs {
150 let entry = catalog::rung_entry(rung, &source_config, &config.encoder).await?;
151 entries.push((rung.name.clone(), entry));
152 }
153 {
154 let mut guard = derived.lock();
155 catalog::populate(&mut guard, &snapshot, &entries, config.source.as_ref())?;
156 }
157
158 let mut tasks = tokio::task::JoinSet::new();
163 loop {
164 tokio::select! {
165 request = dynamic.requested_track() => {
166 let Ok(request) = request else { break };
168 match rungs.iter().find(|rung| rung.name == request.name()) {
169 Some(info) => {
170 let rung = rung::Rung {
171 source: source.track(&source_name)?,
172 feed: feed.clone(),
173 broadcast: source.clone(),
174 config: source_config.clone(),
175 encoder: config.encoder.clone(),
176 decoder: config.decoder.clone(),
177 resize: config.resize,
178 active: active.clone(),
179 info: info.clone(),
180 };
181 tasks.spawn(rung::serve(rung, request));
182 }
183 None => request.reject(moq_net::Error::NotFound),
184 }
185 },
186 update = catalogs.next() => match update {
187 Ok(Some(snapshot)) => {
188 let mut guard = derived.lock();
189 catalog::populate(&mut guard, &snapshot, &entries, config.source.as_ref())?;
190 }
191 Ok(None) => break,
193 Err(err) => {
194 tracing::debug!(%err, "source catalog ended");
195 break;
196 }
197 },
198 Some(result) = tasks.join_next() => match result {
199 Ok(Ok(())) => {}
200 Ok(Err(err)) => tracing::warn!(%err, "rung failed"),
201 Err(err) => tracing::warn!(%err, "rung panicked"),
202 }
203 }
204 }
205
206 tasks.shutdown().await;
213
214 derived.finish()?;
215 output.finish();
216 Ok(())
217 }
218}
219
220#[cfg(test)]
221mod tests {
222
223 use super::*;
224
225 struct Source {
228 broadcast: moq_net::broadcast::Producer,
229 _catalog: moq_mux::catalog::Producer,
230 _track: moq_net::track::Producer,
231 }
232
233 fn nal_types(annexb: &[u8]) -> Vec<u8> {
236 let mut types = Vec::new();
237 let mut i = 0;
238 while i + 3 < annexb.len() {
239 if annexb[i..i + 3] == [0, 0, 1] {
240 types.push(annexb[i + 3] & 0x1f);
241 i += 3;
242 } else {
243 i += 1;
244 }
245 }
246 types
247 }
248
249 fn gray_frame(rgba: &[u8], timestamp: u64) -> moq_video::Frame {
251 let surface = moq_video::Surface::rgba(rgba, moq_video::Size::new(320, 240)).unwrap();
252 moq_video::Frame::new(surface, moq_net::Timestamp::from_micros(timestamp).unwrap())
253 }
254
255 fn source_broadcast(groups: u64, frames: u64) -> Source {
258 let mut broadcast = moq_net::broadcast::Info::default().produce();
259 let mut catalog = moq_mux::catalog::Producer::new(&mut broadcast).unwrap();
260
261 let mut video = hang::catalog::VideoConfig::new(hang::catalog::H264 {
262 inline: true,
263 profile: 0x42,
264 constraints: 0,
265 level: 30,
266 });
267 video.coded_width = Some(320);
268 video.coded_height = Some(240);
269 video.bitrate = Some(1_000_000);
270 video.framerate = Some(30.0);
271 catalog.lock().video.insert("video", video).unwrap();
272
273 let info = hang::container::track_info();
274 let mut track = broadcast.create_track("video", info).unwrap();
275
276 let mut encoder = moq_video::encode::Encoder::new(&{
277 let mut config = moq_video::encode::Config::new(320, 240, 30);
278 config.kind = moq_video::encode::Kind::Software;
279 config
280 })
281 .unwrap();
282 let gray = vec![0x80u8; 320 * 240 * 4];
283
284 for sequence in 0..groups {
285 let mut group = track.create_group(sequence.into()).unwrap();
286 for index in 0..frames {
287 let timestamp = (sequence * frames + index) * 33_333;
288 if index == 0 {
289 encoder.keyframe();
290 }
291 for encoded in encoder.encode(&gray_frame(&gray, timestamp)).unwrap() {
292 let frame = hang::container::Frame {
293 timestamp: encoded.timestamp,
294 payload: encoded.payload,
295 };
296 frame.write_to(&mut group).unwrap();
297 }
298 }
299 group.finish().unwrap();
300 }
301
302 Source {
303 broadcast,
304 _catalog: catalog,
305 _track: track,
306 }
307 }
308
309 fn source_broadcast_live(groups: u64, frames: u64) -> (Source, tokio::task::JoinHandle<()>) {
314 let mut broadcast = moq_net::broadcast::Info::default().produce();
315 let mut catalog = moq_mux::catalog::Producer::new(&mut broadcast).unwrap();
316
317 let mut video = hang::catalog::VideoConfig::new(hang::catalog::H264 {
318 inline: true,
319 profile: 0x42,
320 constraints: 0,
321 level: 30,
322 });
323 video.coded_width = Some(320);
324 video.coded_height = Some(240);
325 video.bitrate = Some(1_000_000);
326 video.framerate = Some(30.0);
327 catalog.lock().video.insert("video", video).unwrap();
328
329 let info = hang::container::track_info();
330 let mut track = broadcast.create_track("video", info).unwrap();
331
332 let source = Source {
333 broadcast,
334 _catalog: catalog,
335 _track: track.clone(),
338 };
339
340 let task = tokio::spawn(async move {
341 let mut encoder = moq_video::encode::Encoder::new(&{
342 let mut config = moq_video::encode::Config::new(320, 240, 30);
343 config.kind = moq_video::encode::Kind::Software;
344 config
345 })
346 .unwrap();
347 let gray = vec![0x80u8; 320 * 240 * 4];
348
349 for sequence in 0..groups {
350 tokio::time::sleep(std::time::Duration::from_millis(100)).await;
354 let mut group = track.create_group(sequence.into()).unwrap();
355 for index in 0..frames {
356 let timestamp = (sequence * frames + index) * 33_333;
357 if index == 0 {
358 encoder.keyframe();
359 }
360 for encoded in encoder.encode(&gray_frame(&gray, timestamp)).unwrap() {
361 let frame = hang::container::Frame {
362 timestamp: encoded.timestamp,
363 payload: encoded.payload,
364 };
365 frame.write_to(&mut group).unwrap();
366 }
367 }
368 group.finish().unwrap();
369 }
370 std::future::pending::<()>().await;
372 });
373
374 (source, task)
375 }
376
377 #[tokio::test]
380 async fn live_multi_rung() {
381 let (source, producer_task) = source_broadcast_live(3, 5);
388 let config = Config {
389 rungs: vec![Rung::new(120, 100_000), Rung::new(60, 50_000)],
390 encoder: moq_video::encode::Kind::Software,
391 decoder: moq_video::decode::Kind::Software,
392 source: None,
393 ..Default::default()
394 };
395
396 let output = moq_net::broadcast::Info::default().produce();
397 let consumer = output.consume();
398 let transcoder = tokio::spawn(run(source.broadcast.consume(), output, config));
399
400 let mut subscribers = Vec::new();
403 for name in ["video/120p", "video/60p"] {
404 let track = loop {
405 match consumer.track(name) {
406 Ok(track) => break track,
407 Err(moq_net::Error::NotFound) => tokio::task::yield_now().await,
408 Err(err) => panic!("rung track {name}: {err}"),
409 }
410 };
411 subscribers.push((name, track.subscribe(None).await.unwrap()));
412 }
413
414 for (name, subscriber) in &mut subscribers {
416 let mut group = subscriber.next_group().await.unwrap().unwrap();
417 let payload = group.read_frame().await.unwrap().unwrap();
418 let frame = hang::container::Frame::decode(payload.payload).unwrap();
419 assert!(
420 frame.payload.starts_with(&[0, 0, 0, 1]) || frame.payload.starts_with(&[0, 0, 1]),
421 "{name} output is not Annex-B"
422 );
423 let total = group.finished().await.unwrap();
424 assert_eq!(total, 5, "{name} dropped frames");
425 }
426
427 producer_task.abort();
428 transcoder.abort();
429 }
430
431 #[cfg_attr(
435 target_os = "windows",
436 ignore = "explicit live-DXVA GPU probe; VideoProcessorBlt can hang on affected drivers"
437 )]
438 #[tokio::test]
439 async fn live_multi_rung_hardware() {
440 if !hardware_available() {
441 eprintln!("skipping: no hardware decoder + encoder available");
442 return;
443 }
444 let (source, producer_task) = source_broadcast_live(3, 5);
451 let mut config = Config {
455 rungs: vec![Rung::new(180, 200_000), Rung::new(120, 100_000)],
456 encoder: moq_video::encode::Kind::Hardware,
457 decoder: moq_video::decode::Kind::Hardware,
458 source: None,
459 ..Default::default()
460 };
461 config.resize.acceleration = moq_video::resize::Acceleration::Gpu;
462
463 let output = moq_net::broadcast::Info::default().produce();
464 let consumer = output.consume();
465 let transcoder = tokio::spawn(run(source.broadcast.consume(), output, config));
466
467 let mut subscribers = Vec::new();
468 for name in ["video/180p", "video/120p"] {
469 let track = loop {
470 match consumer.track(name) {
471 Ok(track) => break track,
472 Err(moq_net::Error::NotFound) => tokio::task::yield_now().await,
473 Err(err) => panic!("rung track {name}: {err}"),
474 }
475 };
476 subscribers.push((name, track.subscribe(None).await.unwrap()));
477 }
478
479 for (name, subscriber) in &mut subscribers {
480 let mut group = subscriber.next_group().await.unwrap().unwrap();
481 let payload = group.read_frame().await.unwrap().unwrap();
482 let frame = hang::container::Frame::decode(payload.payload).unwrap();
483 assert!(
484 frame.payload.starts_with(&[0, 0, 0, 1]) || frame.payload.starts_with(&[0, 0, 1]),
485 "{name} output is not Annex-B"
486 );
487 let total = group.finished().await.unwrap();
488 assert_eq!(total, 5, "{name} dropped frames");
489 }
490
491 producer_task.abort();
492 transcoder.abort();
493 }
494
495 fn hardware_available() -> bool {
499 let mut encode = moq_video::encode::Config::new(160, 120, 30);
500 encode.kind = moq_video::encode::Kind::Hardware;
501 if moq_video::encode::Encoder::new(&encode).is_err() {
502 return false;
503 }
504
505 let video = hang::catalog::VideoConfig::new(hang::catalog::H264 {
506 inline: true,
507 profile: 0x42,
508 constraints: 0,
509 level: 30,
510 });
511 let mut decode = moq_video::decode::Config::new();
512 decode.kind = moq_video::decode::Kind::Hardware;
513 moq_video::decode::Decoder::new(&video, &decode).is_ok()
514 }
515
516 #[cfg_attr(
521 target_os = "windows",
522 ignore = "explicit live-DXVA GPU probe; VideoProcessorBlt can hang on affected drivers"
523 )]
524 #[tokio::test]
525 async fn end_to_end_hardware() {
526 if !hardware_available() {
527 eprintln!("skipping: no hardware decoder + encoder available");
528 return;
529 }
530
531 let source = source_broadcast(2, 5);
532 let mut config = Config {
533 rungs: vec![Rung::new(120, 100_000)],
534 encoder: moq_video::encode::Kind::Hardware,
535 decoder: moq_video::decode::Kind::Hardware,
536 source: None,
537 ..Default::default()
538 };
539 config.resize.acceleration = moq_video::resize::Acceleration::Gpu;
540
541 let output = moq_net::broadcast::Info::default().produce();
542 let consumer = output.consume();
543 let transcoder = tokio::spawn(run(source.broadcast.consume(), output, config));
544
545 let track = loop {
548 match consumer.track("video/120p") {
549 Ok(track) => break track,
550 Err(moq_net::Error::NotFound) => tokio::task::yield_now().await,
551 Err(err) => panic!("rung track: {err}"),
552 }
553 };
554 let mut fetched = track.fetch_group(0, None).await.unwrap();
555 let payload = fetched.read_frame().await.unwrap().unwrap();
556 let frame = hang::container::Frame::decode(payload.payload).unwrap();
557 assert!(
558 frame.payload.starts_with(&[0, 0, 0, 1]) || frame.payload.starts_with(&[0, 0, 1]),
559 "hardware rung output is not Annex-B"
560 );
561 let total = fetched.finished().await.unwrap();
562 assert_eq!(total, 5, "hardware transcode dropped frames");
563
564 transcoder.abort();
565 }
566
567 #[tokio::test]
568 async fn end_to_end() {
569 let source = source_broadcast(2, 5);
570
571 let config = Config {
572 rungs: vec![Rung::new(120, 100_000)],
573 encoder: moq_video::encode::Kind::Software,
574 decoder: moq_video::decode::Kind::Software,
575 source: Some(moq_net::PathRelativeOwned::from(".".to_string())),
576 ..Default::default()
577 };
578
579 let output = moq_net::broadcast::Info::default().produce();
580 let consumer = output.consume();
581 let transcoder = tokio::spawn(run(source.broadcast.consume(), output, config));
582
583 let track = loop {
588 match consumer.track(hang::Catalog::DEFAULT_NAME) {
589 Ok(track) => break track,
590 Err(moq_net::Error::NotFound) => tokio::task::yield_now().await,
591 Err(err) => panic!("catalog track: {err}"),
592 }
593 };
594 let track = track.subscribe(None).await.unwrap();
595 let mut catalogs = moq_mux::catalog::hang::Consumer::<()>::new(track);
596 let derived = loop {
599 let snapshot = catalogs.next().await.unwrap().unwrap();
600 if snapshot.video.renditions.contains_key("video/120p") {
601 break snapshot;
602 }
603 };
604
605 let rung = derived.video.renditions.get("video/120p").expect("rung missing");
606 assert_eq!(rung.coded_width, Some(160));
607 assert_eq!(rung.coded_height, Some(120));
608 assert_eq!(rung.bitrate, Some(100_000));
609 assert!(rung.codec.to_string().starts_with("avc3."));
610
611 let passthrough = derived.video.renditions.get("video").expect("passthrough missing");
612 assert_eq!(passthrough.broadcast.as_ref().map(|b| b.as_ref()), Some("."));
613
614 let mut subscriber = consumer.track("video/120p").unwrap().subscribe(None).await.unwrap();
617 let mut group = subscriber.next_group().await.unwrap().unwrap();
618 assert!(group.sequence <= 1, "unexpected sequence {}", group.sequence);
619 let payload = group.read_frame().await.unwrap().unwrap();
620 let frame = hang::container::Frame::decode(payload.payload).unwrap();
621 assert!(
622 frame.payload.starts_with(&[0, 0, 0, 1]) || frame.payload.starts_with(&[0, 0, 1]),
623 "rung output is not Annex-B"
624 );
625
626 let mut fetched = consumer
628 .track("video/120p")
629 .unwrap()
630 .fetch_group(0, None)
631 .await
632 .unwrap();
633 let mut timestamps = Vec::new();
634 let mut first_payload = None;
635 while let Some(payload) = fetched.read_frame().await.unwrap() {
636 let frame = hang::container::Frame::decode(payload.payload).unwrap();
637 assert!(!frame.payload.is_empty());
638 timestamps.push(frame.timestamp.as_micros());
639 first_payload = first_payload.or(Some(frame.payload));
640 }
641
642 let types = nal_types(&first_payload.expect("the group had no frames"));
648 assert!(types.contains(&7), "group does not open with an SPS: {types:?}");
649 assert!(types.contains(&8), "group does not open with a PPS: {types:?}");
650 assert!(types.contains(&5), "group does not open with an IDR: {types:?}");
651 assert_eq!(timestamps, (0..5).map(|i| i * 33_333).collect::<Vec<u128>>());
655 let total = fetched.finished().await.unwrap();
658 assert_eq!(total, 5);
659
660 transcoder.abort();
661 }
662
663 #[tokio::test]
668 async fn reports_active_rungs() {
669 let source = source_broadcast(2, 5);
670
671 let config = Config {
672 rungs: vec![Rung::new(120, 100_000)],
673 encoder: moq_video::encode::Kind::Software,
674 decoder: moq_video::decode::Kind::Software,
675 source: None,
676 ..Default::default()
677 };
678
679 let output = moq_net::broadcast::Info::default().produce();
680 let consumer = output.consume();
681 let transcoder = Transcoder::new(source.broadcast.consume(), output, config).unwrap();
682 let mut active = transcoder.active();
683 let driver = tokio::spawn(transcoder.run());
684
685 let update = active.next().await.unwrap();
687 let rendition = update.rendition;
688 assert_eq!(rendition.name(), "video/120p");
689 assert_eq!(rendition.size().height, 120);
690 assert_eq!(rendition.bitrate(), 100_000);
691 assert!(!update.encoding, "encoding before anyone asked");
692 assert_eq!(rendition.frames(), 0);
693
694 let mut subscriber = consumer.track("video/120p").unwrap().subscribe(None).await.unwrap();
695 let update = active.next().await.unwrap();
696 assert_eq!(update.rendition.name(), "video/120p");
697 assert!(update.encoding);
698
699 let mut group = subscriber.next_group().await.unwrap().unwrap();
701 group.read_frame().await.unwrap().unwrap();
702 assert!(rendition.frames() > 0);
703 assert!(rendition.bytes() > 0);
704
705 drop(group);
707 drop(subscriber);
708 let update = active.next().await.unwrap();
709 assert_eq!(update.rendition.name(), "video/120p");
710 assert!(!update.encoding);
711
712 assert!(rendition.frames() > 0);
714 assert!(rendition.bytes() > 0);
715
716 driver.abort();
717 }
718
719 #[tokio::test]
722 async fn shuts_down_on_source_end() {
723 let source = source_broadcast(1, 3);
724
725 let config = Config {
726 rungs: vec![Rung::new(120, 100_000)],
727 encoder: moq_video::encode::Kind::Software,
728 decoder: moq_video::decode::Kind::Software,
729 source: None,
730 ..Default::default()
731 };
732
733 let output = moq_net::broadcast::Info::default().produce();
734 let consumer = output.consume();
735 let transcoder = tokio::spawn(run(source.broadcast.consume(), output, config));
736
737 let track = loop {
740 match consumer.track(hang::Catalog::DEFAULT_NAME) {
741 Ok(track) => break track,
742 Err(moq_net::Error::NotFound) => tokio::task::yield_now().await,
743 Err(err) => panic!("catalog track: {err}"),
744 }
745 };
746 let mut catalogs = moq_mux::catalog::hang::Consumer::<()>::new(track.subscribe(None).await.unwrap());
747 catalogs.next().await.unwrap().unwrap();
748
749 drop(source);
752
753 let result = tokio::time::timeout(std::time::Duration::from_secs(5), transcoder).await;
754 result.expect("run did not shut down within 5s").unwrap().unwrap();
755 }
756}