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 info: info.clone(),
113 };
114 tasks.spawn(rung::serve(rung, request));
115 }
116 None => request.reject(moq_net::Error::NotFound),
117 }
118 },
119 update = catalogs.next() => match update {
120 Ok(Some(snapshot)) => {
121 let mut guard = derived.lock();
122 catalog::populate(&mut guard, &snapshot, &entries, config.source.as_ref())?;
123 }
124 Ok(None) => break,
126 Err(err) => {
127 tracing::debug!(%err, "source catalog ended");
128 break;
129 }
130 },
131 Some(result) = tasks.join_next() => match result {
132 Ok(Ok(())) => {}
133 Ok(Err(err)) => tracing::warn!(%err, "rung failed"),
134 Err(err) => tracing::warn!(%err, "rung panicked"),
135 }
136 }
137 }
138
139 tasks.shutdown().await;
146
147 derived.finish()?;
148 output.finish();
149 Ok(())
150}
151
152#[cfg(test)]
153mod tests {
154 use super::*;
155
156 struct Source {
159 broadcast: moq_net::broadcast::Producer,
160 _catalog: moq_mux::catalog::Producer,
161 _track: moq_net::track::Producer,
162 }
163
164 fn source_broadcast(groups: u64, frames: u64) -> Source {
167 let mut broadcast = moq_net::broadcast::Info::default().produce();
168 let mut catalog = moq_mux::catalog::Producer::new(&mut broadcast).unwrap();
169
170 let mut video = hang::catalog::VideoConfig::new(hang::catalog::H264 {
171 inline: true,
172 profile: 0x42,
173 constraints: 0,
174 level: 30,
175 });
176 video.coded_width = Some(320);
177 video.coded_height = Some(240);
178 video.bitrate = Some(1_000_000);
179 video.framerate = Some(30.0);
180 catalog.lock().video.insert("video", video).unwrap();
181
182 let info = hang::container::track_info();
183 let mut track = broadcast.create_track("video", info).unwrap();
184
185 let mut encoder = moq_video::encode::Encoder::new(&{
186 let mut config = moq_video::encode::Config::new(320, 240, 30);
187 config.kind = moq_video::encode::Kind::Software;
188 config
189 })
190 .unwrap();
191 let gray = vec![0x80u8; 320 * 240 * 4];
192
193 for sequence in 0..groups {
194 let mut group = track.create_group(sequence.into()).unwrap();
195 for index in 0..frames {
196 let timestamp = (sequence * frames + index) * 33_333;
197 for payload in encoder
198 .encode_rgba(&gray, moq_video::Size::new(320, 240), index == 0)
199 .unwrap()
200 {
201 let frame = hang::container::Frame {
202 timestamp: moq_net::Timestamp::from_micros(timestamp).unwrap(),
203 payload,
204 };
205 frame.write_to(&mut group).unwrap();
206 }
207 }
208 group.finish().unwrap();
209 }
210
211 Source {
212 broadcast,
213 _catalog: catalog,
214 _track: track,
215 }
216 }
217
218 fn source_broadcast_live(groups: u64, frames: u64) -> (Source, tokio::task::JoinHandle<()>) {
223 let mut broadcast = moq_net::broadcast::Info::default().produce();
224 let mut catalog = moq_mux::catalog::Producer::new(&mut broadcast).unwrap();
225
226 let mut video = hang::catalog::VideoConfig::new(hang::catalog::H264 {
227 inline: true,
228 profile: 0x42,
229 constraints: 0,
230 level: 30,
231 });
232 video.coded_width = Some(320);
233 video.coded_height = Some(240);
234 video.bitrate = Some(1_000_000);
235 video.framerate = Some(30.0);
236 catalog.lock().video.insert("video", video).unwrap();
237
238 let info = hang::container::track_info();
239 let mut track = broadcast.create_track("video", info).unwrap();
240
241 let source = Source {
242 broadcast,
243 _catalog: catalog,
244 _track: track.clone(),
247 };
248
249 let task = tokio::spawn(async move {
250 let mut encoder = moq_video::encode::Encoder::new(&{
251 let mut config = moq_video::encode::Config::new(320, 240, 30);
252 config.kind = moq_video::encode::Kind::Software;
253 config
254 })
255 .unwrap();
256 let gray = vec![0x80u8; 320 * 240 * 4];
257
258 for sequence in 0..groups {
259 tokio::time::sleep(std::time::Duration::from_millis(100)).await;
262 let mut group = track.create_group(sequence.into()).unwrap();
263 for index in 0..frames {
264 let timestamp = (sequence * frames + index) * 33_333;
265 for payload in encoder
266 .encode_rgba(&gray, moq_video::Size::new(320, 240), index == 0)
267 .unwrap()
268 {
269 let frame = hang::container::Frame {
270 timestamp: moq_net::Timestamp::from_micros(timestamp).unwrap(),
271 payload,
272 };
273 frame.write_to(&mut group).unwrap();
274 }
275 }
276 group.finish().unwrap();
277 }
278 std::future::pending::<()>().await;
280 });
281
282 (source, task)
283 }
284
285 #[tokio::test]
288 async fn live_multi_rung() {
289 tokio::time::pause();
290
291 let (source, producer_task) = source_broadcast_live(3, 5);
292 let config = Config {
293 rungs: vec![Rung::new(120, 100_000), Rung::new(60, 50_000)],
294 encoder: moq_video::encode::Kind::Software,
295 decoder: moq_video::decode::Kind::Software,
296 source: None,
297 };
298
299 let output = moq_net::broadcast::Info::default().produce();
300 let consumer = output.consume();
301 let transcoder = tokio::spawn(run(source.broadcast.consume(), output, config));
302
303 let mut subscribers = Vec::new();
306 for name in ["video/120p", "video/60p"] {
307 let track = loop {
308 match consumer.track(name) {
309 Ok(track) => break track,
310 Err(moq_net::Error::NotFound) => tokio::task::yield_now().await,
311 Err(err) => panic!("rung track {name}: {err}"),
312 }
313 };
314 subscribers.push((name, track.subscribe(None).await.unwrap()));
315 }
316
317 for (name, subscriber) in &mut subscribers {
319 let mut group = subscriber.next_group().await.unwrap().unwrap();
320 let payload = group.read_frame().await.unwrap().unwrap();
321 let frame = hang::container::Frame::decode(payload.payload).unwrap();
322 assert!(
323 frame.payload.starts_with(&[0, 0, 0, 1]) || frame.payload.starts_with(&[0, 0, 1]),
324 "{name} output is not Annex-B"
325 );
326 let total = group.finished().await.unwrap();
327 assert_eq!(total, 5, "{name} dropped frames");
328 }
329
330 producer_task.abort();
331 transcoder.abort();
332 }
333
334 #[tokio::test]
338 async fn live_multi_rung_hardware() {
339 if !hardware_available() {
340 eprintln!("skipping: no hardware decoder + encoder available");
341 return;
342 }
343 tokio::time::pause();
344
345 let (source, producer_task) = source_broadcast_live(3, 5);
346 let config = Config {
350 rungs: vec![Rung::new(180, 200_000), Rung::new(120, 100_000)],
351 encoder: moq_video::encode::Kind::Hardware,
352 decoder: moq_video::decode::Kind::Hardware,
353 source: None,
354 };
355
356 let output = moq_net::broadcast::Info::default().produce();
357 let consumer = output.consume();
358 let transcoder = tokio::spawn(run(source.broadcast.consume(), output, config));
359
360 let mut subscribers = Vec::new();
361 for name in ["video/180p", "video/120p"] {
362 let track = loop {
363 match consumer.track(name) {
364 Ok(track) => break track,
365 Err(moq_net::Error::NotFound) => tokio::task::yield_now().await,
366 Err(err) => panic!("rung track {name}: {err}"),
367 }
368 };
369 subscribers.push((name, track.subscribe(None).await.unwrap()));
370 }
371
372 for (name, subscriber) in &mut subscribers {
373 let mut group = subscriber.next_group().await.unwrap().unwrap();
374 let payload = group.read_frame().await.unwrap().unwrap();
375 let frame = hang::container::Frame::decode(payload.payload).unwrap();
376 assert!(
377 frame.payload.starts_with(&[0, 0, 0, 1]) || frame.payload.starts_with(&[0, 0, 1]),
378 "{name} output is not Annex-B"
379 );
380 let total = group.finished().await.unwrap();
381 assert_eq!(total, 5, "{name} dropped frames");
382 }
383
384 producer_task.abort();
385 transcoder.abort();
386 }
387
388 fn hardware_available() -> bool {
392 let mut encode = moq_video::encode::Config::new(160, 120, 30);
393 encode.kind = moq_video::encode::Kind::Hardware;
394 if moq_video::encode::Encoder::new(&encode).is_err() {
395 return false;
396 }
397
398 let video = hang::catalog::VideoConfig::new(hang::catalog::H264 {
399 inline: true,
400 profile: 0x42,
401 constraints: 0,
402 level: 30,
403 });
404 let mut decode = moq_video::decode::Config::new();
405 decode.kind = moq_video::decode::Kind::Hardware;
406 moq_video::decode::Decoder::new(&video, &decode).is_ok()
407 }
408
409 #[tokio::test]
414 async fn end_to_end_hardware() {
415 if !hardware_available() {
416 eprintln!("skipping: no hardware decoder + encoder available");
417 return;
418 }
419
420 let source = source_broadcast(2, 5);
421 let config = Config {
422 rungs: vec![Rung::new(120, 100_000)],
423 encoder: moq_video::encode::Kind::Hardware,
424 decoder: moq_video::decode::Kind::Hardware,
425 source: None,
426 };
427
428 let output = moq_net::broadcast::Info::default().produce();
429 let consumer = output.consume();
430 let transcoder = tokio::spawn(run(source.broadcast.consume(), output, config));
431
432 let track = loop {
435 match consumer.track("video/120p") {
436 Ok(track) => break track,
437 Err(moq_net::Error::NotFound) => tokio::task::yield_now().await,
438 Err(err) => panic!("rung track: {err}"),
439 }
440 };
441 let mut fetched = track.fetch_group(0, None).await.unwrap();
442 let payload = fetched.read_frame().await.unwrap().unwrap();
443 let frame = hang::container::Frame::decode(payload.payload).unwrap();
444 assert!(
445 frame.payload.starts_with(&[0, 0, 0, 1]) || frame.payload.starts_with(&[0, 0, 1]),
446 "hardware rung output is not Annex-B"
447 );
448 let total = fetched.finished().await.unwrap();
449 assert_eq!(total, 5, "hardware transcode dropped frames");
450
451 transcoder.abort();
452 }
453
454 #[tokio::test]
455 async fn end_to_end() {
456 let source = source_broadcast(2, 5);
457
458 let config = Config {
459 rungs: vec![Rung::new(120, 100_000)],
460 encoder: moq_video::encode::Kind::Software,
461 decoder: moq_video::decode::Kind::Software,
462 source: Some(moq_net::PathRelativeOwned::from("..".to_string())),
463 };
464
465 let output = moq_net::broadcast::Info::default().produce();
466 let consumer = output.consume();
467 let transcoder = tokio::spawn(run(source.broadcast.consume(), output, config));
468
469 let track = loop {
474 match consumer.track(hang::Catalog::DEFAULT_NAME) {
475 Ok(track) => break track,
476 Err(moq_net::Error::NotFound) => tokio::task::yield_now().await,
477 Err(err) => panic!("catalog track: {err}"),
478 }
479 };
480 let track = track.subscribe(None).await.unwrap();
481 let mut catalogs = moq_mux::catalog::hang::Consumer::<()>::new(track);
482 let derived = loop {
485 let snapshot = catalogs.next().await.unwrap().unwrap();
486 if snapshot.video.renditions.contains_key("video/120p") {
487 break snapshot;
488 }
489 };
490
491 let rung = derived.video.renditions.get("video/120p").expect("rung missing");
492 assert_eq!(rung.coded_width, Some(160));
493 assert_eq!(rung.coded_height, Some(120));
494 assert_eq!(rung.bitrate, Some(100_000));
495 assert!(rung.codec.to_string().starts_with("avc3."));
496
497 let passthrough = derived.video.renditions.get("video").expect("passthrough missing");
498 assert_eq!(passthrough.broadcast.as_ref().map(|b| b.as_ref()), Some(".."));
499
500 let mut subscriber = consumer.track("video/120p").unwrap().subscribe(None).await.unwrap();
503 let mut group = subscriber.next_group().await.unwrap().unwrap();
504 assert!(group.sequence <= 1, "unexpected sequence {}", group.sequence);
505 let payload = group.read_frame().await.unwrap().unwrap();
506 let frame = hang::container::Frame::decode(payload.payload).unwrap();
507 assert!(
508 frame.payload.starts_with(&[0, 0, 0, 1]) || frame.payload.starts_with(&[0, 0, 1]),
509 "rung output is not Annex-B"
510 );
511
512 let mut fetched = consumer
514 .track("video/120p")
515 .unwrap()
516 .fetch_group(0, None)
517 .await
518 .unwrap();
519 let payload = fetched.read_frame().await.unwrap().unwrap();
520 let frame = hang::container::Frame::decode(payload.payload).unwrap();
521 assert!(!frame.payload.is_empty());
522 let total = fetched.finished().await.unwrap();
525 assert_eq!(total, 5);
526
527 transcoder.abort();
528 }
529
530 #[tokio::test]
533 async fn shuts_down_on_source_end() {
534 let source = source_broadcast(1, 3);
535
536 let config = Config {
537 rungs: vec![Rung::new(120, 100_000)],
538 encoder: moq_video::encode::Kind::Software,
539 decoder: moq_video::decode::Kind::Software,
540 source: None,
541 };
542
543 let output = moq_net::broadcast::Info::default().produce();
544 let consumer = output.consume();
545 let transcoder = tokio::spawn(run(source.broadcast.consume(), output, config));
546
547 let track = loop {
550 match consumer.track(hang::Catalog::DEFAULT_NAME) {
551 Ok(track) => break track,
552 Err(moq_net::Error::NotFound) => tokio::task::yield_now().await,
553 Err(err) => panic!("catalog track: {err}"),
554 }
555 };
556 let mut catalogs = moq_mux::catalog::hang::Consumer::<()>::new(track.subscribe(None).await.unwrap());
557 catalogs.next().await.unwrap().unwrap();
558
559 drop(source);
562
563 let result = tokio::time::timeout(std::time::Duration::from_secs(5), transcoder).await;
564 result.expect("run did not shut down within 5s").unwrap().unwrap();
565 }
566}