1mod catalog;
25mod config;
26mod error;
27mod feed;
28mod rung;
29
30pub use config::{Config, Rung};
31
32#[allow(deprecated)]
33pub use config::source_reference;
34pub use error::Error;
35
36pub async fn run(
48 source: moq_net::broadcast::Consumer,
49 mut output: moq_net::broadcast::Producer,
50 config: Config,
51) -> Result<(), Error> {
52 let mut derived = moq_mux::catalog::Producer::new(&mut output)?;
55 let mut dynamic = output.dynamic();
57
58 let track = source
61 .track(hang::Catalog::DEFAULT_NAME)?
62 .subscribe(hang::Catalog::default_subscription())
63 .await?;
64 let mut catalogs = moq_mux::catalog::hang::Consumer::<()>::new(track);
65 let (source_name, source_config, snapshot) = loop {
66 let Some(snapshot) = catalogs.next().await? else {
67 return Err(Error::NoSource);
68 };
69 match catalog::choose_source(&snapshot.video) {
70 Ok((name, config)) => break (name, config, snapshot),
71 Err(_) => tracing::debug!("no transcodable rendition yet; waiting for a catalog update"),
72 }
73 };
74 let rungs = catalog::resolve_rungs(&config.rungs, &source_name, &source_config)?;
75 tracing::info!(source = %source_name, rungs = rungs.len(), "transcoding");
76
77 let feed = feed::Feed::new(
80 source.track(&source_name)?,
81 source_config.clone(),
82 config.decoder.clone(),
83 );
84
85 let mut entries = Vec::with_capacity(rungs.len());
88 for rung in &rungs {
89 let entry = catalog::rung_entry(rung, &source_config, &config.encoder).await?;
90 entries.push((rung.name.clone(), entry));
91 }
92 {
93 let mut guard = derived.lock();
94 catalog::populate(&mut guard, &snapshot, &entries, config.source.as_ref())?;
95 }
96
97 let mut tasks = tokio::task::JoinSet::new();
102 loop {
103 tokio::select! {
104 request = dynamic.requested_track() => {
105 let Ok(request) = request else { break };
107 match rungs.iter().find(|rung| rung.name == request.name()) {
108 Some(info) => {
109 let rung = rung::Rung {
110 source: source.track(&source_name)?,
111 feed: feed.clone(),
112 broadcast: source.clone(),
113 config: source_config.clone(),
114 encoder: config.encoder.clone(),
115 decoder: config.decoder.clone(),
116 resize: config.resize,
117 info: info.clone(),
118 };
119 tasks.spawn(rung::serve(rung, request));
120 }
121 None => request.reject(moq_net::Error::NotFound),
122 }
123 },
124 update = catalogs.next() => match update {
125 Ok(Some(snapshot)) => {
126 let mut guard = derived.lock();
127 catalog::populate(&mut guard, &snapshot, &entries, config.source.as_ref())?;
128 }
129 Ok(None) => break,
131 Err(err) => {
132 tracing::debug!(%err, "source catalog ended");
133 break;
134 }
135 },
136 Some(result) = tasks.join_next() => match result {
137 Ok(Ok(())) => {}
138 Ok(Err(err)) => tracing::warn!(%err, "rung failed"),
139 Err(err) => tracing::warn!(%err, "rung panicked"),
140 }
141 }
142 }
143
144 tasks.shutdown().await;
151
152 derived.finish()?;
153 output.finish();
154 Ok(())
155}
156
157#[cfg(test)]
158mod tests {
159 use super::*;
160
161 struct Source {
164 broadcast: moq_net::broadcast::Producer,
165 _catalog: moq_mux::catalog::Producer,
166 _track: moq_net::track::Producer,
167 }
168
169 fn nal_types(annexb: &[u8]) -> Vec<u8> {
172 let mut types = Vec::new();
173 let mut i = 0;
174 while i + 3 < annexb.len() {
175 if annexb[i..i + 3] == [0, 0, 1] {
176 types.push(annexb[i + 3] & 0x1f);
177 i += 3;
178 } else {
179 i += 1;
180 }
181 }
182 types
183 }
184
185 fn gray_frame(rgba: &[u8], timestamp: u64) -> moq_video::Frame {
187 let surface = moq_video::Surface::rgba(rgba, moq_video::Size::new(320, 240)).unwrap();
188 moq_video::Frame::new(surface, moq_net::Timestamp::from_micros(timestamp).unwrap())
189 }
190
191 fn source_broadcast(groups: u64, frames: u64) -> Source {
194 let mut broadcast = moq_net::broadcast::Info::default().produce();
195 let mut catalog = moq_mux::catalog::Producer::new(&mut broadcast).unwrap();
196
197 let mut video = hang::catalog::VideoConfig::new(hang::catalog::H264 {
198 inline: true,
199 profile: 0x42,
200 constraints: 0,
201 level: 30,
202 });
203 video.coded_width = Some(320);
204 video.coded_height = Some(240);
205 video.bitrate = Some(1_000_000);
206 video.framerate = Some(30.0);
207 catalog.lock().video.insert("video", video).unwrap();
208
209 let info = hang::container::track_info();
210 let mut track = broadcast.create_track("video", info).unwrap();
211
212 let mut encoder = moq_video::encode::Encoder::new(&{
213 let mut config = moq_video::encode::Config::new(320, 240, 30);
214 config.kind = moq_video::encode::Kind::Software;
215 config
216 })
217 .unwrap();
218 let gray = vec![0x80u8; 320 * 240 * 4];
219
220 for sequence in 0..groups {
221 let mut group = track.create_group(sequence.into()).unwrap();
222 for index in 0..frames {
223 let timestamp = (sequence * frames + index) * 33_333;
224 if index == 0 {
225 encoder.keyframe();
226 }
227 for encoded in encoder.encode(&gray_frame(&gray, timestamp)).unwrap() {
228 let frame = hang::container::Frame {
229 timestamp: encoded.timestamp,
230 payload: encoded.payload,
231 };
232 frame.write_to(&mut group).unwrap();
233 }
234 }
235 group.finish().unwrap();
236 }
237
238 Source {
239 broadcast,
240 _catalog: catalog,
241 _track: track,
242 }
243 }
244
245 fn source_broadcast_live(groups: u64, frames: u64) -> (Source, tokio::task::JoinHandle<()>) {
250 let mut broadcast = moq_net::broadcast::Info::default().produce();
251 let mut catalog = moq_mux::catalog::Producer::new(&mut broadcast).unwrap();
252
253 let mut video = hang::catalog::VideoConfig::new(hang::catalog::H264 {
254 inline: true,
255 profile: 0x42,
256 constraints: 0,
257 level: 30,
258 });
259 video.coded_width = Some(320);
260 video.coded_height = Some(240);
261 video.bitrate = Some(1_000_000);
262 video.framerate = Some(30.0);
263 catalog.lock().video.insert("video", video).unwrap();
264
265 let info = hang::container::track_info();
266 let mut track = broadcast.create_track("video", info).unwrap();
267
268 let source = Source {
269 broadcast,
270 _catalog: catalog,
271 _track: track.clone(),
274 };
275
276 let task = tokio::spawn(async move {
277 let mut encoder = moq_video::encode::Encoder::new(&{
278 let mut config = moq_video::encode::Config::new(320, 240, 30);
279 config.kind = moq_video::encode::Kind::Software;
280 config
281 })
282 .unwrap();
283 let gray = vec![0x80u8; 320 * 240 * 4];
284
285 for sequence in 0..groups {
286 tokio::time::sleep(std::time::Duration::from_millis(100)).await;
290 let mut group = track.create_group(sequence.into()).unwrap();
291 for index in 0..frames {
292 let timestamp = (sequence * frames + index) * 33_333;
293 if index == 0 {
294 encoder.keyframe();
295 }
296 for encoded in encoder.encode(&gray_frame(&gray, timestamp)).unwrap() {
297 let frame = hang::container::Frame {
298 timestamp: encoded.timestamp,
299 payload: encoded.payload,
300 };
301 frame.write_to(&mut group).unwrap();
302 }
303 }
304 group.finish().unwrap();
305 }
306 std::future::pending::<()>().await;
308 });
309
310 (source, task)
311 }
312
313 #[tokio::test]
316 async fn live_multi_rung() {
317 let (source, producer_task) = source_broadcast_live(3, 5);
324 let config = Config {
325 rungs: vec![Rung::new(120, 100_000), Rung::new(60, 50_000)],
326 encoder: moq_video::encode::Kind::Software,
327 decoder: moq_video::decode::Kind::Software,
328 source: None,
329 ..Default::default()
330 };
331
332 let output = moq_net::broadcast::Info::default().produce();
333 let consumer = output.consume();
334 let transcoder = tokio::spawn(run(source.broadcast.consume(), output, config));
335
336 let mut subscribers = Vec::new();
339 for name in ["video/120p", "video/60p"] {
340 let track = loop {
341 match consumer.track(name) {
342 Ok(track) => break track,
343 Err(moq_net::Error::NotFound) => tokio::task::yield_now().await,
344 Err(err) => panic!("rung track {name}: {err}"),
345 }
346 };
347 subscribers.push((name, track.subscribe(None).await.unwrap()));
348 }
349
350 for (name, subscriber) in &mut subscribers {
352 let mut group = subscriber.next_group().await.unwrap().unwrap();
353 let payload = group.read_frame().await.unwrap().unwrap();
354 let frame = hang::container::Frame::decode(payload.payload).unwrap();
355 assert!(
356 frame.payload.starts_with(&[0, 0, 0, 1]) || frame.payload.starts_with(&[0, 0, 1]),
357 "{name} output is not Annex-B"
358 );
359 let total = group.finished().await.unwrap();
360 assert_eq!(total, 5, "{name} dropped frames");
361 }
362
363 producer_task.abort();
364 transcoder.abort();
365 }
366
367 #[cfg_attr(
371 target_os = "windows",
372 ignore = "explicit live-DXVA GPU probe; VideoProcessorBlt can hang on affected drivers"
373 )]
374 #[tokio::test]
375 async fn live_multi_rung_hardware() {
376 if !hardware_available() {
377 eprintln!("skipping: no hardware decoder + encoder available");
378 return;
379 }
380 let (source, producer_task) = source_broadcast_live(3, 5);
387 let mut config = Config {
391 rungs: vec![Rung::new(180, 200_000), Rung::new(120, 100_000)],
392 encoder: moq_video::encode::Kind::Hardware,
393 decoder: moq_video::decode::Kind::Hardware,
394 source: None,
395 ..Default::default()
396 };
397 config.resize.acceleration = moq_video::resize::Acceleration::Gpu;
398
399 let output = moq_net::broadcast::Info::default().produce();
400 let consumer = output.consume();
401 let transcoder = tokio::spawn(run(source.broadcast.consume(), output, config));
402
403 let mut subscribers = Vec::new();
404 for name in ["video/180p", "video/120p"] {
405 let track = loop {
406 match consumer.track(name) {
407 Ok(track) => break track,
408 Err(moq_net::Error::NotFound) => tokio::task::yield_now().await,
409 Err(err) => panic!("rung track {name}: {err}"),
410 }
411 };
412 subscribers.push((name, track.subscribe(None).await.unwrap()));
413 }
414
415 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 fn hardware_available() -> bool {
435 let mut encode = moq_video::encode::Config::new(160, 120, 30);
436 encode.kind = moq_video::encode::Kind::Hardware;
437 if moq_video::encode::Encoder::new(&encode).is_err() {
438 return false;
439 }
440
441 let video = hang::catalog::VideoConfig::new(hang::catalog::H264 {
442 inline: true,
443 profile: 0x42,
444 constraints: 0,
445 level: 30,
446 });
447 let mut decode = moq_video::decode::Config::new();
448 decode.kind = moq_video::decode::Kind::Hardware;
449 moq_video::decode::Decoder::new(&video, &decode).is_ok()
450 }
451
452 #[cfg_attr(
457 target_os = "windows",
458 ignore = "explicit live-DXVA GPU probe; VideoProcessorBlt can hang on affected drivers"
459 )]
460 #[tokio::test]
461 async fn end_to_end_hardware() {
462 if !hardware_available() {
463 eprintln!("skipping: no hardware decoder + encoder available");
464 return;
465 }
466
467 let source = source_broadcast(2, 5);
468 let mut config = Config {
469 rungs: vec![Rung::new(120, 100_000)],
470 encoder: moq_video::encode::Kind::Hardware,
471 decoder: moq_video::decode::Kind::Hardware,
472 source: None,
473 ..Default::default()
474 };
475 config.resize.acceleration = moq_video::resize::Acceleration::Gpu;
476
477 let output = moq_net::broadcast::Info::default().produce();
478 let consumer = output.consume();
479 let transcoder = tokio::spawn(run(source.broadcast.consume(), output, config));
480
481 let track = loop {
484 match consumer.track("video/120p") {
485 Ok(track) => break track,
486 Err(moq_net::Error::NotFound) => tokio::task::yield_now().await,
487 Err(err) => panic!("rung track: {err}"),
488 }
489 };
490 let mut fetched = track.fetch_group(0, None).await.unwrap();
491 let payload = fetched.read_frame().await.unwrap().unwrap();
492 let frame = hang::container::Frame::decode(payload.payload).unwrap();
493 assert!(
494 frame.payload.starts_with(&[0, 0, 0, 1]) || frame.payload.starts_with(&[0, 0, 1]),
495 "hardware rung output is not Annex-B"
496 );
497 let total = fetched.finished().await.unwrap();
498 assert_eq!(total, 5, "hardware transcode dropped frames");
499
500 transcoder.abort();
501 }
502
503 #[tokio::test]
504 async fn end_to_end() {
505 let source = source_broadcast(2, 5);
506
507 let config = Config {
508 rungs: vec![Rung::new(120, 100_000)],
509 encoder: moq_video::encode::Kind::Software,
510 decoder: moq_video::decode::Kind::Software,
511 source: Some(moq_net::PathRelativeOwned::from(".".to_string())),
512 ..Default::default()
513 };
514
515 let output = moq_net::broadcast::Info::default().produce();
516 let consumer = output.consume();
517 let transcoder = tokio::spawn(run(source.broadcast.consume(), output, config));
518
519 let track = loop {
524 match consumer.track(hang::Catalog::DEFAULT_NAME) {
525 Ok(track) => break track,
526 Err(moq_net::Error::NotFound) => tokio::task::yield_now().await,
527 Err(err) => panic!("catalog track: {err}"),
528 }
529 };
530 let track = track.subscribe(None).await.unwrap();
531 let mut catalogs = moq_mux::catalog::hang::Consumer::<()>::new(track);
532 let derived = loop {
535 let snapshot = catalogs.next().await.unwrap().unwrap();
536 if snapshot.video.renditions.contains_key("video/120p") {
537 break snapshot;
538 }
539 };
540
541 let rung = derived.video.renditions.get("video/120p").expect("rung missing");
542 assert_eq!(rung.coded_width, Some(160));
543 assert_eq!(rung.coded_height, Some(120));
544 assert_eq!(rung.bitrate, Some(100_000));
545 assert!(rung.codec.to_string().starts_with("avc3."));
546
547 let passthrough = derived.video.renditions.get("video").expect("passthrough missing");
548 assert_eq!(passthrough.broadcast.as_ref().map(|b| b.as_ref()), Some("."));
549
550 let mut subscriber = consumer.track("video/120p").unwrap().subscribe(None).await.unwrap();
553 let mut group = subscriber.next_group().await.unwrap().unwrap();
554 assert!(group.sequence <= 1, "unexpected sequence {}", group.sequence);
555 let payload = group.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 "rung output is not Annex-B"
560 );
561
562 let mut fetched = consumer
564 .track("video/120p")
565 .unwrap()
566 .fetch_group(0, None)
567 .await
568 .unwrap();
569 let mut timestamps = Vec::new();
570 let mut first_payload = None;
571 while let Some(payload) = fetched.read_frame().await.unwrap() {
572 let frame = hang::container::Frame::decode(payload.payload).unwrap();
573 assert!(!frame.payload.is_empty());
574 timestamps.push(frame.timestamp.as_micros());
575 first_payload = first_payload.or(Some(frame.payload));
576 }
577
578 let types = nal_types(&first_payload.expect("the group had no frames"));
584 assert!(types.contains(&7), "group does not open with an SPS: {types:?}");
585 assert!(types.contains(&8), "group does not open with a PPS: {types:?}");
586 assert!(types.contains(&5), "group does not open with an IDR: {types:?}");
587 assert_eq!(timestamps, (0..5).map(|i| i * 33_333).collect::<Vec<u128>>());
591 let total = fetched.finished().await.unwrap();
594 assert_eq!(total, 5);
595
596 transcoder.abort();
597 }
598
599 #[tokio::test]
602 async fn shuts_down_on_source_end() {
603 let source = source_broadcast(1, 3);
604
605 let config = Config {
606 rungs: vec![Rung::new(120, 100_000)],
607 encoder: moq_video::encode::Kind::Software,
608 decoder: moq_video::decode::Kind::Software,
609 source: None,
610 ..Default::default()
611 };
612
613 let output = moq_net::broadcast::Info::default().produce();
614 let consumer = output.consume();
615 let transcoder = tokio::spawn(run(source.broadcast.consume(), output, config));
616
617 let track = loop {
620 match consumer.track(hang::Catalog::DEFAULT_NAME) {
621 Ok(track) => break track,
622 Err(moq_net::Error::NotFound) => tokio::task::yield_now().await,
623 Err(err) => panic!("catalog track: {err}"),
624 }
625 };
626 let mut catalogs = moq_mux::catalog::hang::Consumer::<()>::new(track.subscribe(None).await.unwrap());
627 catalogs.next().await.unwrap().unwrap();
628
629 drop(source);
632
633 let result = tokio::time::timeout(std::time::Duration::from_secs(5), transcoder).await;
634 result.expect("run did not shut down within 5s").unwrap().unwrap();
635 }
636}