moq_transcode/config.rs
1//! Transcoder configuration: the rung ladder and catalog wiring.
2
3use moq_net::{AsPath, PathRelativeOwned};
4
5/// Compute the source broadcast reference for an output nested beneath it.
6///
7/// Both paths are normalized before comparison. Returns `None` when `output` is
8/// not a descendant of `source`, so callers can omit passthrough renditions.
9pub fn source_reference(source: impl AsPath, output: impl AsPath) -> Option<PathRelativeOwned> {
10 let source = source.as_path();
11 let output = output.as_path();
12 let rest = output.strip_prefix(&source)?;
13 if rest.is_empty() {
14 return None;
15 }
16
17 let parents = rest.parts().count().saturating_sub(1);
18 let rel = if parents == 0 {
19 ".".to_string()
20 } else {
21 vec![".."; parents].join("/")
22 };
23 Some(PathRelativeOwned::from(rel))
24}
25
26/// One candidate output rendition: a target resolution (by height) and bitrate.
27///
28/// The width is derived from the source aspect ratio at runtime, and a rung is
29/// only offered when it is strictly below the source (see [`Config::rungs`]).
30#[derive(Clone, Copy, Debug, PartialEq, Eq)]
31#[non_exhaustive]
32pub struct Rung {
33 /// Output height in pixels. Rounded down to even (I420 chroma is 2x2).
34 pub height: u32,
35
36 /// Target bitrate in bits per second: the CBR target and the bitrate
37 /// advertised in the derivative catalog.
38 pub bitrate: u64,
39}
40
41impl Rung {
42 /// A rung at `height` pixels and `bitrate` bits per second.
43 pub fn new(height: u32, bitrate: u64) -> Self {
44 Self { height, bitrate }
45 }
46}
47
48/// Transcoder configuration for [`run`](crate::run).
49///
50/// `#[non_exhaustive]`: build via `Config::default()` and set fields, so future
51/// knobs don't break callers.
52#[derive(Clone, Debug)]
53#[non_exhaustive]
54pub struct Config {
55 /// Candidate output renditions. Only rungs strictly below the source
56 /// survive: a rung is dropped when its height exceeds the source, when its
57 /// bitrate is not below the source bitrate (when known), or when it matches
58 /// the source height without a known source bitrate to undercut. A 480p
59 /// source is never transcoded up to 720p.
60 pub rungs: Vec<Rung>,
61
62 /// Where the source broadcast lives relative to the output broadcast, e.g.
63 /// `"."` when the output is published at `<source>/transcode.hang`. When
64 /// set, the derivative catalog references the source renditions (all video
65 /// and audio) through this path so players fetch them from the source
66 /// directly; the transcoder never proxies or subscribes them. `None` omits
67 /// them from the derivative catalog.
68 pub source: Option<PathRelativeOwned>,
69
70 /// Which video encoder implementation encodes the rungs. The default
71 /// prefers hardware (NVENC on Linux, VideoToolbox on macOS, Media
72 /// Foundation on Windows) and falls back to openh264.
73 pub encoder: moq_video::encode::Kind,
74
75 /// Which video decoder implementation decodes the source. The default
76 /// prefers hardware and falls back to openh264 (H.264 only; H.265 sources
77 /// need a hardware decoder).
78 pub decoder: moq_video::decode::Kind,
79
80 /// Frame resize behavior. Automatic mode keeps GPU-backed frames on the GPU.
81 pub resize: moq_video::resize::Config,
82}
83
84impl Default for Config {
85 fn default() -> Self {
86 Self {
87 // The default ladder, top rung first, filtered against the source at
88 // runtime so only strictly-lower renditions are offered.
89 rungs: vec![
90 Rung::new(1080, 5_000_000),
91 Rung::new(720, 2_500_000),
92 Rung::new(480, 1_200_000),
93 Rung::new(360, 600_000),
94 Rung::new(240, 350_000),
95 ],
96 source: None,
97 encoder: moq_video::encode::Kind::default(),
98 decoder: moq_video::decode::Kind::default(),
99 resize: moq_video::resize::Config::default(),
100 }
101 }
102}
103
104#[cfg(test)]
105mod tests {
106 use super::*;
107
108 #[test]
109 fn source_reference_normalizes_and_counts_output_depth() {
110 assert_eq!(source_reference("a/b", "a/b/transcode.hang").unwrap().as_str(), ".");
111 assert_eq!(source_reference("/a//b/", "a/b/dir/").unwrap().as_str(), ".");
112 assert_eq!(
113 source_reference("a/b", "a/b/dir/transcode.hang").unwrap().as_str(),
114 ".."
115 );
116 assert_eq!(
117 source_reference("a/b", "a/b/one/two/transcode.hang").unwrap().as_str(),
118 "../.."
119 );
120 assert!(source_reference("a/b", "other/transcode.hang").is_none());
121 assert!(source_reference("a/b", "a/b").is_none());
122 }
123}