1mod catalog;
25mod config;
26mod error;
27mod feed;
28mod rung;
29
30pub use config::{Config, Rung};
31pub use error::Error;
32
33pub async fn run(
45 source: moq_net::broadcast::Consumer,
46 mut output: moq_net::broadcast::Producer,
47 config: Config,
48) -> Result<(), Error> {
49 let mut derived = moq_mux::catalog::Producer::new(&mut output)?;
52 let mut dynamic = output.dynamic();
54
55 let track = source
58 .track(hang::Catalog::DEFAULT_NAME)?
59 .subscribe(hang::Catalog::default_subscription())
60 .await?;
61 let mut catalogs = moq_mux::catalog::hang::Consumer::<()>::new(track);
62 let (source_name, source_config, snapshot) = loop {
63 let Some(snapshot) = catalogs.next().await? else {
64 return Err(Error::NoSource);
65 };
66 match catalog::choose_source(&snapshot.video) {
67 Ok((name, config)) => break (name, config, snapshot),
68 Err(_) => tracing::debug!("no transcodable rendition yet; waiting for a catalog update"),
69 }
70 };
71 let rungs = catalog::resolve_rungs(&config.rungs, &source_name, &source_config)?;
72 tracing::info!(source = %source_name, rungs = rungs.len(), "transcoding");
73
74 let feed = feed::Feed::new(
77 source.track(&source_name)?,
78 source_config.clone(),
79 config.decoder.clone(),
80 );
81
82 let entries: Vec<_> = rungs
85 .iter()
86 .map(|rung| (rung.name.clone(), catalog::rung_entry(rung, &source_config)))
87 .collect();
88 {
89 let mut guard = derived.lock();
90 catalog::populate(&mut guard, &snapshot, &entries, config.source.as_ref())?;
91 }
92
93 let mut tasks = tokio::task::JoinSet::new();
98 loop {
99 tokio::select! {
100 request = dynamic.requested_track() => {
101 let Ok(request) = request else { break };
103 match rungs.iter().find(|rung| rung.name == request.name()) {
104 Some(info) => {
105 let rung = rung::Rung {
106 source: source.track(&source_name)?,
107 feed: feed.clone(),
108 broadcast: source.clone(),
109 config: source_config.clone(),
110 encoder: config.encoder.clone(),
111 decoder: config.decoder.clone(),
112 resize: config.resize,
113 info: info.clone(),
114 };
115 tasks.spawn(rung::serve(rung, request));
116 }
117 None => request.reject(moq_net::Error::NotFound),
118 }
119 },
120 update = catalogs.next() => match update {
121 Ok(Some(snapshot)) => {
122 let mut guard = derived.lock();
123 catalog::populate(&mut guard, &snapshot, &entries, config.source.as_ref())?;
124 }
125 Ok(None) => break,
127 Err(err) => {
128 tracing::debug!(%err, "source catalog ended");
129 break;
130 }
131 },
132 Some(result) = tasks.join_next() => match result {
133 Ok(Ok(())) => {}
134 Ok(Err(err)) => tracing::warn!(%err, "rung failed"),
135 Err(err) => tracing::warn!(%err, "rung panicked"),
136 }
137 }
138 }
139
140 tasks.shutdown().await;
147
148 derived.finish()?;
149 output.finish();
150 Ok(())
151}
152
153#[cfg(test)]
154mod tests {
155 use super::*;
156
157 struct Source {
160 broadcast: moq_net::broadcast::Producer,
161 _catalog: moq_mux::catalog::Producer,
162 _track: moq_net::track::Producer,
163 }
164
165 fn nal_types(annexb: &[u8]) -> Vec<u8> {
168 let mut types = Vec::new();
169 let mut i = 0;
170 while i + 3 < annexb.len() {
171 if annexb[i..i + 3] == [0, 0, 1] {
172 types.push(annexb[i + 3] & 0x1f);
173 i += 3;
174 } else {
175 i += 1;
176 }
177 }
178 types
179 }
180
181 fn gray_frame(rgba: &[u8], timestamp: u64) -> moq_video::Frame {
183 let surface = moq_video::Surface::rgba(rgba, moq_video::Size::new(320, 240)).unwrap();
184 moq_video::Frame::new(surface, moq_net::Timestamp::from_micros(timestamp).unwrap())
185 }
186
187 fn source_broadcast(groups: u64, frames: u64) -> Source {
190 let mut broadcast = moq_net::broadcast::Info::default().produce();
191 let mut catalog = moq_mux::catalog::Producer::new(&mut broadcast).unwrap();
192
193 let mut video = hang::catalog::VideoConfig::new(hang::catalog::H264 {
194 inline: true,
195 profile: 0x42,
196 constraints: 0,
197 level: 30,
198 });
199 video.coded_width = Some(320);
200 video.coded_height = Some(240);
201 video.bitrate = Some(1_000_000);
202 video.framerate = Some(30.0);
203 catalog.lock().video.insert("video", video).unwrap();
204
205 let info = hang::container::track_info();
206 let mut track = broadcast.create_track("video", info).unwrap();
207
208 let mut encoder = moq_video::encode::Encoder::new(&{
209 let mut config = moq_video::encode::Config::new(320, 240, 30);
210 config.kind = moq_video::encode::Kind::Software;
211 config
212 })
213 .unwrap();
214 let gray = vec![0x80u8; 320 * 240 * 4];
215
216 for sequence in 0..groups {
217 let mut group = track.create_group(sequence.into()).unwrap();
218 for index in 0..frames {
219 let timestamp = (sequence * frames + index) * 33_333;
220 if index == 0 {
221 encoder.keyframe();
222 }
223 for encoded in encoder.encode(&gray_frame(&gray, timestamp)).unwrap() {
224 let frame = hang::container::Frame {
225 timestamp: encoded.timestamp,
226 payload: encoded.payload,
227 };
228 frame.write_to(&mut group).unwrap();
229 }
230 }
231 group.finish().unwrap();
232 }
233
234 Source {
235 broadcast,
236 _catalog: catalog,
237 _track: track,
238 }
239 }
240
241 fn source_broadcast_live(groups: u64, frames: u64) -> (Source, tokio::task::JoinHandle<()>) {
246 let mut broadcast = moq_net::broadcast::Info::default().produce();
247 let mut catalog = moq_mux::catalog::Producer::new(&mut broadcast).unwrap();
248
249 let mut video = hang::catalog::VideoConfig::new(hang::catalog::H264 {
250 inline: true,
251 profile: 0x42,
252 constraints: 0,
253 level: 30,
254 });
255 video.coded_width = Some(320);
256 video.coded_height = Some(240);
257 video.bitrate = Some(1_000_000);
258 video.framerate = Some(30.0);
259 catalog.lock().video.insert("video", video).unwrap();
260
261 let info = hang::container::track_info();
262 let mut track = broadcast.create_track("video", info).unwrap();
263
264 let source = Source {
265 broadcast,
266 _catalog: catalog,
267 _track: track.clone(),
270 };
271
272 let task = tokio::spawn(async move {
273 let mut encoder = moq_video::encode::Encoder::new(&{
274 let mut config = moq_video::encode::Config::new(320, 240, 30);
275 config.kind = moq_video::encode::Kind::Software;
276 config
277 })
278 .unwrap();
279 let gray = vec![0x80u8; 320 * 240 * 4];
280
281 for sequence in 0..groups {
282 tokio::time::sleep(std::time::Duration::from_millis(100)).await;
286 let mut group = track.create_group(sequence.into()).unwrap();
287 for index in 0..frames {
288 let timestamp = (sequence * frames + index) * 33_333;
289 if index == 0 {
290 encoder.keyframe();
291 }
292 for encoded in encoder.encode(&gray_frame(&gray, timestamp)).unwrap() {
293 let frame = hang::container::Frame {
294 timestamp: encoded.timestamp,
295 payload: encoded.payload,
296 };
297 frame.write_to(&mut group).unwrap();
298 }
299 }
300 group.finish().unwrap();
301 }
302 std::future::pending::<()>().await;
304 });
305
306 (source, task)
307 }
308
309 #[tokio::test]
312 async fn live_multi_rung() {
313 let (source, producer_task) = source_broadcast_live(3, 5);
320 let config = Config {
321 rungs: vec![Rung::new(120, 100_000), Rung::new(60, 50_000)],
322 encoder: moq_video::encode::Kind::Software,
323 decoder: moq_video::decode::Kind::Software,
324 source: None,
325 ..Default::default()
326 };
327
328 let output = moq_net::broadcast::Info::default().produce();
329 let consumer = output.consume();
330 let transcoder = tokio::spawn(run(source.broadcast.consume(), output, config));
331
332 let mut subscribers = Vec::new();
335 for name in ["video/120p", "video/60p"] {
336 let track = loop {
337 match consumer.track(name) {
338 Ok(track) => break track,
339 Err(moq_net::Error::NotFound) => tokio::task::yield_now().await,
340 Err(err) => panic!("rung track {name}: {err}"),
341 }
342 };
343 subscribers.push((name, track.subscribe(None).await.unwrap()));
344 }
345
346 for (name, subscriber) in &mut subscribers {
348 let mut group = subscriber.next_group().await.unwrap().unwrap();
349 let payload = group.read_frame().await.unwrap().unwrap();
350 let frame = hang::container::Frame::decode(payload.payload).unwrap();
351 assert!(
352 frame.payload.starts_with(&[0, 0, 0, 1]) || frame.payload.starts_with(&[0, 0, 1]),
353 "{name} output is not Annex-B"
354 );
355 let total = group.finished().await.unwrap();
356 assert_eq!(total, 5, "{name} dropped frames");
357 }
358
359 producer_task.abort();
360 transcoder.abort();
361 }
362
363 #[cfg_attr(
367 target_os = "windows",
368 ignore = "explicit live-DXVA GPU probe; VideoProcessorBlt can hang on affected drivers"
369 )]
370 #[tokio::test]
371 async fn live_multi_rung_hardware() {
372 if !hardware_available() {
373 eprintln!("skipping: no hardware decoder + encoder available");
374 return;
375 }
376 let (source, producer_task) = source_broadcast_live(3, 5);
383 let mut config = Config {
387 rungs: vec![Rung::new(180, 200_000), Rung::new(120, 100_000)],
388 encoder: moq_video::encode::Kind::Hardware,
389 decoder: moq_video::decode::Kind::Hardware,
390 source: None,
391 ..Default::default()
392 };
393 config.resize.acceleration = moq_video::resize::Acceleration::Gpu;
394
395 let output = moq_net::broadcast::Info::default().produce();
396 let consumer = output.consume();
397 let transcoder = tokio::spawn(run(source.broadcast.consume(), output, config));
398
399 let mut subscribers = Vec::new();
400 for name in ["video/180p", "video/120p"] {
401 let track = loop {
402 match consumer.track(name) {
403 Ok(track) => break track,
404 Err(moq_net::Error::NotFound) => tokio::task::yield_now().await,
405 Err(err) => panic!("rung track {name}: {err}"),
406 }
407 };
408 subscribers.push((name, track.subscribe(None).await.unwrap()));
409 }
410
411 for (name, subscriber) in &mut subscribers {
412 let mut group = subscriber.next_group().await.unwrap().unwrap();
413 let payload = group.read_frame().await.unwrap().unwrap();
414 let frame = hang::container::Frame::decode(payload.payload).unwrap();
415 assert!(
416 frame.payload.starts_with(&[0, 0, 0, 1]) || frame.payload.starts_with(&[0, 0, 1]),
417 "{name} output is not Annex-B"
418 );
419 let total = group.finished().await.unwrap();
420 assert_eq!(total, 5, "{name} dropped frames");
421 }
422
423 producer_task.abort();
424 transcoder.abort();
425 }
426
427 fn hardware_available() -> bool {
431 let mut encode = moq_video::encode::Config::new(160, 120, 30);
432 encode.kind = moq_video::encode::Kind::Hardware;
433 if moq_video::encode::Encoder::new(&encode).is_err() {
434 return false;
435 }
436
437 let video = hang::catalog::VideoConfig::new(hang::catalog::H264 {
438 inline: true,
439 profile: 0x42,
440 constraints: 0,
441 level: 30,
442 });
443 let mut decode = moq_video::decode::Config::new();
444 decode.kind = moq_video::decode::Kind::Hardware;
445 moq_video::decode::Decoder::new(&video, &decode).is_ok()
446 }
447
448 #[cfg_attr(
453 target_os = "windows",
454 ignore = "explicit live-DXVA GPU probe; VideoProcessorBlt can hang on affected drivers"
455 )]
456 #[tokio::test]
457 async fn end_to_end_hardware() {
458 if !hardware_available() {
459 eprintln!("skipping: no hardware decoder + encoder available");
460 return;
461 }
462
463 let source = source_broadcast(2, 5);
464 let mut config = Config {
465 rungs: vec![Rung::new(120, 100_000)],
466 encoder: moq_video::encode::Kind::Hardware,
467 decoder: moq_video::decode::Kind::Hardware,
468 source: None,
469 ..Default::default()
470 };
471 config.resize.acceleration = moq_video::resize::Acceleration::Gpu;
472
473 let output = moq_net::broadcast::Info::default().produce();
474 let consumer = output.consume();
475 let transcoder = tokio::spawn(run(source.broadcast.consume(), output, config));
476
477 let track = loop {
480 match consumer.track("video/120p") {
481 Ok(track) => break track,
482 Err(moq_net::Error::NotFound) => tokio::task::yield_now().await,
483 Err(err) => panic!("rung track: {err}"),
484 }
485 };
486 let mut fetched = track.fetch_group(0, None).await.unwrap();
487 let payload = fetched.read_frame().await.unwrap().unwrap();
488 let frame = hang::container::Frame::decode(payload.payload).unwrap();
489 assert!(
490 frame.payload.starts_with(&[0, 0, 0, 1]) || frame.payload.starts_with(&[0, 0, 1]),
491 "hardware rung output is not Annex-B"
492 );
493 let total = fetched.finished().await.unwrap();
494 assert_eq!(total, 5, "hardware transcode dropped frames");
495
496 transcoder.abort();
497 }
498
499 #[tokio::test]
500 async fn end_to_end() {
501 let source = source_broadcast(2, 5);
502
503 let config = Config {
504 rungs: vec![Rung::new(120, 100_000)],
505 encoder: moq_video::encode::Kind::Software,
506 decoder: moq_video::decode::Kind::Software,
507 source: Some(moq_net::PathRelativeOwned::from("..".to_string())),
508 ..Default::default()
509 };
510
511 let output = moq_net::broadcast::Info::default().produce();
512 let consumer = output.consume();
513 let transcoder = tokio::spawn(run(source.broadcast.consume(), output, config));
514
515 let track = loop {
520 match consumer.track(hang::Catalog::DEFAULT_NAME) {
521 Ok(track) => break track,
522 Err(moq_net::Error::NotFound) => tokio::task::yield_now().await,
523 Err(err) => panic!("catalog track: {err}"),
524 }
525 };
526 let track = track.subscribe(None).await.unwrap();
527 let mut catalogs = moq_mux::catalog::hang::Consumer::<()>::new(track);
528 let derived = loop {
531 let snapshot = catalogs.next().await.unwrap().unwrap();
532 if snapshot.video.renditions.contains_key("video/120p") {
533 break snapshot;
534 }
535 };
536
537 let rung = derived.video.renditions.get("video/120p").expect("rung missing");
538 assert_eq!(rung.coded_width, Some(160));
539 assert_eq!(rung.coded_height, Some(120));
540 assert_eq!(rung.bitrate, Some(100_000));
541 assert!(rung.codec.to_string().starts_with("avc3."));
542
543 let passthrough = derived.video.renditions.get("video").expect("passthrough missing");
544 assert_eq!(passthrough.broadcast.as_ref().map(|b| b.as_ref()), Some(".."));
545
546 let mut subscriber = consumer.track("video/120p").unwrap().subscribe(None).await.unwrap();
549 let mut group = subscriber.next_group().await.unwrap().unwrap();
550 assert!(group.sequence <= 1, "unexpected sequence {}", group.sequence);
551 let payload = group.read_frame().await.unwrap().unwrap();
552 let frame = hang::container::Frame::decode(payload.payload).unwrap();
553 assert!(
554 frame.payload.starts_with(&[0, 0, 0, 1]) || frame.payload.starts_with(&[0, 0, 1]),
555 "rung output is not Annex-B"
556 );
557
558 let mut fetched = consumer
560 .track("video/120p")
561 .unwrap()
562 .fetch_group(0, None)
563 .await
564 .unwrap();
565 let mut timestamps = Vec::new();
566 let mut first_payload = None;
567 while let Some(payload) = fetched.read_frame().await.unwrap() {
568 let frame = hang::container::Frame::decode(payload.payload).unwrap();
569 assert!(!frame.payload.is_empty());
570 timestamps.push(frame.timestamp.as_micros());
571 first_payload = first_payload.or(Some(frame.payload));
572 }
573
574 let types = nal_types(&first_payload.expect("the group had no frames"));
580 assert!(types.contains(&7), "group does not open with an SPS: {types:?}");
581 assert!(types.contains(&8), "group does not open with a PPS: {types:?}");
582 assert!(types.contains(&5), "group does not open with an IDR: {types:?}");
583 assert_eq!(timestamps, (0..5).map(|i| i * 33_333).collect::<Vec<u128>>());
587 let total = fetched.finished().await.unwrap();
590 assert_eq!(total, 5);
591
592 transcoder.abort();
593 }
594
595 #[tokio::test]
598 async fn shuts_down_on_source_end() {
599 let source = source_broadcast(1, 3);
600
601 let config = Config {
602 rungs: vec![Rung::new(120, 100_000)],
603 encoder: moq_video::encode::Kind::Software,
604 decoder: moq_video::decode::Kind::Software,
605 source: None,
606 ..Default::default()
607 };
608
609 let output = moq_net::broadcast::Info::default().produce();
610 let consumer = output.consume();
611 let transcoder = tokio::spawn(run(source.broadcast.consume(), output, config));
612
613 let track = loop {
616 match consumer.track(hang::Catalog::DEFAULT_NAME) {
617 Ok(track) => break track,
618 Err(moq_net::Error::NotFound) => tokio::task::yield_now().await,
619 Err(err) => panic!("catalog track: {err}"),
620 }
621 };
622 let mut catalogs = moq_mux::catalog::hang::Consumer::<()>::new(track.subscribe(None).await.unwrap());
623 catalogs.next().await.unwrap().unwrap();
624
625 drop(source);
628
629 let result = tokio::time::timeout(std::time::Duration::from_secs(5), transcoder).await;
630 result.expect("run did not shut down within 5s").unwrap().unwrap();
631 }
632}