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 nal_types(annexb: &[u8]) -> Vec<u8> {
167 let mut types = Vec::new();
168 let mut i = 0;
169 while i + 3 < annexb.len() {
170 if annexb[i..i + 3] == [0, 0, 1] {
171 types.push(annexb[i + 3] & 0x1f);
172 i += 3;
173 } else {
174 i += 1;
175 }
176 }
177 types
178 }
179
180 fn gray_frame(rgba: &[u8], timestamp: u64) -> moq_video::Frame {
182 let surface = moq_video::Surface::rgba(rgba, moq_video::Size::new(320, 240)).unwrap();
183 moq_video::Frame::new(surface, moq_net::Timestamp::from_micros(timestamp).unwrap())
184 }
185
186 fn source_broadcast(groups: u64, frames: u64) -> Source {
189 let mut broadcast = moq_net::broadcast::Info::default().produce();
190 let mut catalog = moq_mux::catalog::Producer::new(&mut broadcast).unwrap();
191
192 let mut video = hang::catalog::VideoConfig::new(hang::catalog::H264 {
193 inline: true,
194 profile: 0x42,
195 constraints: 0,
196 level: 30,
197 });
198 video.coded_width = Some(320);
199 video.coded_height = Some(240);
200 video.bitrate = Some(1_000_000);
201 video.framerate = Some(30.0);
202 catalog.lock().video.insert("video", video).unwrap();
203
204 let info = hang::container::track_info();
205 let mut track = broadcast.create_track("video", info).unwrap();
206
207 let mut encoder = moq_video::encode::Encoder::new(&{
208 let mut config = moq_video::encode::Config::new(320, 240, 30);
209 config.kind = moq_video::encode::Kind::Software;
210 config
211 })
212 .unwrap();
213 let gray = vec![0x80u8; 320 * 240 * 4];
214
215 for sequence in 0..groups {
216 let mut group = track.create_group(sequence.into()).unwrap();
217 for index in 0..frames {
218 let timestamp = (sequence * frames + index) * 33_333;
219 if index == 0 {
220 encoder.keyframe();
221 }
222 for encoded in encoder.encode(&gray_frame(&gray, timestamp)).unwrap() {
223 let frame = hang::container::Frame {
224 timestamp: encoded.timestamp,
225 payload: encoded.payload,
226 };
227 frame.write_to(&mut group).unwrap();
228 }
229 }
230 group.finish().unwrap();
231 }
232
233 Source {
234 broadcast,
235 _catalog: catalog,
236 _track: track,
237 }
238 }
239
240 fn source_broadcast_live(groups: u64, frames: u64) -> (Source, tokio::task::JoinHandle<()>) {
245 let mut broadcast = moq_net::broadcast::Info::default().produce();
246 let mut catalog = moq_mux::catalog::Producer::new(&mut broadcast).unwrap();
247
248 let mut video = hang::catalog::VideoConfig::new(hang::catalog::H264 {
249 inline: true,
250 profile: 0x42,
251 constraints: 0,
252 level: 30,
253 });
254 video.coded_width = Some(320);
255 video.coded_height = Some(240);
256 video.bitrate = Some(1_000_000);
257 video.framerate = Some(30.0);
258 catalog.lock().video.insert("video", video).unwrap();
259
260 let info = hang::container::track_info();
261 let mut track = broadcast.create_track("video", info).unwrap();
262
263 let source = Source {
264 broadcast,
265 _catalog: catalog,
266 _track: track.clone(),
269 };
270
271 let task = tokio::spawn(async move {
272 let mut encoder = moq_video::encode::Encoder::new(&{
273 let mut config = moq_video::encode::Config::new(320, 240, 30);
274 config.kind = moq_video::encode::Kind::Software;
275 config
276 })
277 .unwrap();
278 let gray = vec![0x80u8; 320 * 240 * 4];
279
280 for sequence in 0..groups {
281 tokio::time::sleep(std::time::Duration::from_millis(100)).await;
284 let mut group = track.create_group(sequence.into()).unwrap();
285 for index in 0..frames {
286 let timestamp = (sequence * frames + index) * 33_333;
287 if index == 0 {
288 encoder.keyframe();
289 }
290 for encoded in encoder.encode(&gray_frame(&gray, timestamp)).unwrap() {
291 let frame = hang::container::Frame {
292 timestamp: encoded.timestamp,
293 payload: encoded.payload,
294 };
295 frame.write_to(&mut group).unwrap();
296 }
297 }
298 group.finish().unwrap();
299 }
300 std::future::pending::<()>().await;
302 });
303
304 (source, task)
305 }
306
307 #[tokio::test]
310 async fn live_multi_rung() {
311 tokio::time::pause();
312
313 let (source, producer_task) = source_broadcast_live(3, 5);
314 let config = Config {
315 rungs: vec![Rung::new(120, 100_000), Rung::new(60, 50_000)],
316 encoder: moq_video::encode::Kind::Software,
317 decoder: moq_video::decode::Kind::Software,
318 source: None,
319 };
320
321 let output = moq_net::broadcast::Info::default().produce();
322 let consumer = output.consume();
323 let transcoder = tokio::spawn(run(source.broadcast.consume(), output, config));
324
325 let mut subscribers = Vec::new();
328 for name in ["video/120p", "video/60p"] {
329 let track = loop {
330 match consumer.track(name) {
331 Ok(track) => break track,
332 Err(moq_net::Error::NotFound) => tokio::task::yield_now().await,
333 Err(err) => panic!("rung track {name}: {err}"),
334 }
335 };
336 subscribers.push((name, track.subscribe(None).await.unwrap()));
337 }
338
339 for (name, subscriber) in &mut subscribers {
341 let mut group = subscriber.next_group().await.unwrap().unwrap();
342 let payload = group.read_frame().await.unwrap().unwrap();
343 let frame = hang::container::Frame::decode(payload.payload).unwrap();
344 assert!(
345 frame.payload.starts_with(&[0, 0, 0, 1]) || frame.payload.starts_with(&[0, 0, 1]),
346 "{name} output is not Annex-B"
347 );
348 let total = group.finished().await.unwrap();
349 assert_eq!(total, 5, "{name} dropped frames");
350 }
351
352 producer_task.abort();
353 transcoder.abort();
354 }
355
356 #[tokio::test]
360 async fn live_multi_rung_hardware() {
361 if !hardware_available() {
362 eprintln!("skipping: no hardware decoder + encoder available");
363 return;
364 }
365 tokio::time::pause();
366
367 let (source, producer_task) = source_broadcast_live(3, 5);
368 let config = Config {
372 rungs: vec![Rung::new(180, 200_000), Rung::new(120, 100_000)],
373 encoder: moq_video::encode::Kind::Hardware,
374 decoder: moq_video::decode::Kind::Hardware,
375 source: None,
376 };
377
378 let output = moq_net::broadcast::Info::default().produce();
379 let consumer = output.consume();
380 let transcoder = tokio::spawn(run(source.broadcast.consume(), output, config));
381
382 let mut subscribers = Vec::new();
383 for name in ["video/180p", "video/120p"] {
384 let track = loop {
385 match consumer.track(name) {
386 Ok(track) => break track,
387 Err(moq_net::Error::NotFound) => tokio::task::yield_now().await,
388 Err(err) => panic!("rung track {name}: {err}"),
389 }
390 };
391 subscribers.push((name, track.subscribe(None).await.unwrap()));
392 }
393
394 for (name, subscriber) in &mut subscribers {
395 let mut group = subscriber.next_group().await.unwrap().unwrap();
396 let payload = group.read_frame().await.unwrap().unwrap();
397 let frame = hang::container::Frame::decode(payload.payload).unwrap();
398 assert!(
399 frame.payload.starts_with(&[0, 0, 0, 1]) || frame.payload.starts_with(&[0, 0, 1]),
400 "{name} output is not Annex-B"
401 );
402 let total = group.finished().await.unwrap();
403 assert_eq!(total, 5, "{name} dropped frames");
404 }
405
406 producer_task.abort();
407 transcoder.abort();
408 }
409
410 fn hardware_available() -> bool {
414 let mut encode = moq_video::encode::Config::new(160, 120, 30);
415 encode.kind = moq_video::encode::Kind::Hardware;
416 if moq_video::encode::Encoder::new(&encode).is_err() {
417 return false;
418 }
419
420 let video = hang::catalog::VideoConfig::new(hang::catalog::H264 {
421 inline: true,
422 profile: 0x42,
423 constraints: 0,
424 level: 30,
425 });
426 let mut decode = moq_video::decode::Config::new();
427 decode.kind = moq_video::decode::Kind::Hardware;
428 moq_video::decode::Decoder::new(&video, &decode).is_ok()
429 }
430
431 #[tokio::test]
436 async fn end_to_end_hardware() {
437 if !hardware_available() {
438 eprintln!("skipping: no hardware decoder + encoder available");
439 return;
440 }
441
442 let source = source_broadcast(2, 5);
443 let config = Config {
444 rungs: vec![Rung::new(120, 100_000)],
445 encoder: moq_video::encode::Kind::Hardware,
446 decoder: moq_video::decode::Kind::Hardware,
447 source: None,
448 };
449
450 let output = moq_net::broadcast::Info::default().produce();
451 let consumer = output.consume();
452 let transcoder = tokio::spawn(run(source.broadcast.consume(), output, config));
453
454 let track = loop {
457 match consumer.track("video/120p") {
458 Ok(track) => break track,
459 Err(moq_net::Error::NotFound) => tokio::task::yield_now().await,
460 Err(err) => panic!("rung track: {err}"),
461 }
462 };
463 let mut fetched = track.fetch_group(0, None).await.unwrap();
464 let payload = fetched.read_frame().await.unwrap().unwrap();
465 let frame = hang::container::Frame::decode(payload.payload).unwrap();
466 assert!(
467 frame.payload.starts_with(&[0, 0, 0, 1]) || frame.payload.starts_with(&[0, 0, 1]),
468 "hardware rung output is not Annex-B"
469 );
470 let total = fetched.finished().await.unwrap();
471 assert_eq!(total, 5, "hardware transcode dropped frames");
472
473 transcoder.abort();
474 }
475
476 #[tokio::test]
477 async fn end_to_end() {
478 let source = source_broadcast(2, 5);
479
480 let config = Config {
481 rungs: vec![Rung::new(120, 100_000)],
482 encoder: moq_video::encode::Kind::Software,
483 decoder: moq_video::decode::Kind::Software,
484 source: Some(moq_net::PathRelativeOwned::from("..".to_string())),
485 };
486
487 let output = moq_net::broadcast::Info::default().produce();
488 let consumer = output.consume();
489 let transcoder = tokio::spawn(run(source.broadcast.consume(), output, config));
490
491 let track = loop {
496 match consumer.track(hang::Catalog::DEFAULT_NAME) {
497 Ok(track) => break track,
498 Err(moq_net::Error::NotFound) => tokio::task::yield_now().await,
499 Err(err) => panic!("catalog track: {err}"),
500 }
501 };
502 let track = track.subscribe(None).await.unwrap();
503 let mut catalogs = moq_mux::catalog::hang::Consumer::<()>::new(track);
504 let derived = loop {
507 let snapshot = catalogs.next().await.unwrap().unwrap();
508 if snapshot.video.renditions.contains_key("video/120p") {
509 break snapshot;
510 }
511 };
512
513 let rung = derived.video.renditions.get("video/120p").expect("rung missing");
514 assert_eq!(rung.coded_width, Some(160));
515 assert_eq!(rung.coded_height, Some(120));
516 assert_eq!(rung.bitrate, Some(100_000));
517 assert!(rung.codec.to_string().starts_with("avc3."));
518
519 let passthrough = derived.video.renditions.get("video").expect("passthrough missing");
520 assert_eq!(passthrough.broadcast.as_ref().map(|b| b.as_ref()), Some(".."));
521
522 let mut subscriber = consumer.track("video/120p").unwrap().subscribe(None).await.unwrap();
525 let mut group = subscriber.next_group().await.unwrap().unwrap();
526 assert!(group.sequence <= 1, "unexpected sequence {}", group.sequence);
527 let payload = group.read_frame().await.unwrap().unwrap();
528 let frame = hang::container::Frame::decode(payload.payload).unwrap();
529 assert!(
530 frame.payload.starts_with(&[0, 0, 0, 1]) || frame.payload.starts_with(&[0, 0, 1]),
531 "rung output is not Annex-B"
532 );
533
534 let mut fetched = consumer
536 .track("video/120p")
537 .unwrap()
538 .fetch_group(0, None)
539 .await
540 .unwrap();
541 let mut timestamps = Vec::new();
542 let mut first_payload = None;
543 while let Some(payload) = fetched.read_frame().await.unwrap() {
544 let frame = hang::container::Frame::decode(payload.payload).unwrap();
545 assert!(!frame.payload.is_empty());
546 timestamps.push(frame.timestamp.as_micros());
547 first_payload = first_payload.or(Some(frame.payload));
548 }
549
550 let types = nal_types(&first_payload.expect("the group had no frames"));
556 assert!(types.contains(&7), "group does not open with an SPS: {types:?}");
557 assert!(types.contains(&8), "group does not open with a PPS: {types:?}");
558 assert!(types.contains(&5), "group does not open with an IDR: {types:?}");
559 assert_eq!(timestamps, (0..5).map(|i| i * 33_333).collect::<Vec<u128>>());
563 let total = fetched.finished().await.unwrap();
566 assert_eq!(total, 5);
567
568 transcoder.abort();
569 }
570
571 #[tokio::test]
574 async fn shuts_down_on_source_end() {
575 let source = source_broadcast(1, 3);
576
577 let config = Config {
578 rungs: vec![Rung::new(120, 100_000)],
579 encoder: moq_video::encode::Kind::Software,
580 decoder: moq_video::decode::Kind::Software,
581 source: None,
582 };
583
584 let output = moq_net::broadcast::Info::default().produce();
585 let consumer = output.consume();
586 let transcoder = tokio::spawn(run(source.broadcast.consume(), output, config));
587
588 let track = loop {
591 match consumer.track(hang::Catalog::DEFAULT_NAME) {
592 Ok(track) => break track,
593 Err(moq_net::Error::NotFound) => tokio::task::yield_now().await,
594 Err(err) => panic!("catalog track: {err}"),
595 }
596 };
597 let mut catalogs = moq_mux::catalog::hang::Consumer::<()>::new(track.subscribe(None).await.unwrap());
598 catalogs.next().await.unwrap().unwrap();
599
600 drop(source);
603
604 let result = tokio::time::timeout(std::time::Duration::from_secs(5), transcoder).await;
605 result.expect("run did not shut down within 5s").unwrap().unwrap();
606 }
607}