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