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