lvqr_transcode/
passthrough.rs1use lvqr_fragment::Fragment;
16use tracing::{debug, info};
17
18use crate::rendition::RenditionSpec;
19use crate::transcoder::{Transcoder, TranscoderContext, TranscoderFactory};
20
21const DEFAULT_SOURCE_TRACK: &str = "0.mp4";
25
26pub struct PassthroughTranscoder {
34 rendition_name: String,
35 fragments_seen: u64,
36}
37
38impl PassthroughTranscoder {
39 pub fn new(rendition: &RenditionSpec) -> Self {
44 Self {
45 rendition_name: rendition.name.clone(),
46 fragments_seen: 0,
47 }
48 }
49
50 pub fn fragments_seen(&self) -> u64 {
56 self.fragments_seen
57 }
58}
59
60impl Transcoder for PassthroughTranscoder {
61 fn on_start(&mut self, ctx: &TranscoderContext) {
62 info!(
63 broadcast = %ctx.broadcast,
64 track = %ctx.track,
65 rendition = %ctx.rendition.name,
66 width = ctx.rendition.width,
67 height = ctx.rendition.height,
68 "passthrough transcoder started (scaffold; does not re-encode)",
69 );
70 }
71
72 fn on_fragment(&mut self, fragment: &Fragment) {
73 self.fragments_seen = self.fragments_seen.saturating_add(1);
74 debug!(
75 rendition = %self.rendition_name,
76 group_id = fragment.group_id,
77 object_id = fragment.object_id,
78 bytes = fragment.payload.len(),
79 "passthrough transcoder observed fragment",
80 );
81 }
82
83 fn on_stop(&mut self) {
84 info!(
85 rendition = %self.rendition_name,
86 seen = self.fragments_seen,
87 "passthrough transcoder stopped",
88 );
89 }
90}
91
92pub struct PassthroughTranscoderFactory {
100 rendition: RenditionSpec,
101}
102
103impl PassthroughTranscoderFactory {
104 pub fn new(rendition: RenditionSpec) -> Self {
106 Self { rendition }
107 }
108}
109
110impl TranscoderFactory for PassthroughTranscoderFactory {
111 fn name(&self) -> &str {
112 "passthrough"
113 }
114
115 fn rendition(&self) -> &RenditionSpec {
116 &self.rendition
117 }
118
119 fn build(&self, ctx: &TranscoderContext) -> Option<Box<dyn Transcoder>> {
120 if ctx.track != DEFAULT_SOURCE_TRACK {
121 return None;
122 }
123 Some(Box::new(PassthroughTranscoder::new(&ctx.rendition)))
124 }
125}
126
127#[cfg(test)]
128mod tests {
129 use super::*;
130 use bytes::Bytes;
131 use lvqr_fragment::{Fragment, FragmentFlags, FragmentMeta};
132
133 fn ctx(track: &str, rendition: RenditionSpec) -> TranscoderContext {
134 TranscoderContext {
135 broadcast: "live/demo".into(),
136 track: track.into(),
137 meta: FragmentMeta::new("avc1.640028", 90_000),
138 rendition,
139 }
140 }
141
142 fn frag(idx: u64) -> Fragment {
143 Fragment::new(
144 "0.mp4",
145 idx,
146 0,
147 0,
148 idx * 1000,
149 idx * 1000,
150 1000,
151 FragmentFlags::DELTA,
152 Bytes::from(vec![0xAB; 16]),
153 )
154 }
155
156 #[test]
157 fn factory_returns_transcoder_for_video_track() {
158 let factory = PassthroughTranscoderFactory::new(RenditionSpec::preset_720p());
159 let ctx = ctx("0.mp4", factory.rendition().clone());
160 assert!(factory.build(&ctx).is_some());
161 }
162
163 #[test]
164 fn factory_skips_non_video_tracks() {
165 let factory = PassthroughTranscoderFactory::new(RenditionSpec::preset_720p());
166 for track in ["1.mp4", "captions", "catalog", "0-alt.mp4"] {
167 let ctx = ctx(track, factory.rendition().clone());
168 assert!(factory.build(&ctx).is_none(), "factory must skip track {track}");
169 }
170 }
171
172 #[test]
173 fn factory_name_is_stable_snake_case() {
174 let factory = PassthroughTranscoderFactory::new(RenditionSpec::preset_480p());
175 assert_eq!(factory.name(), "passthrough");
176 }
177
178 #[test]
179 fn factory_exposes_configured_rendition() {
180 let factory = PassthroughTranscoderFactory::new(RenditionSpec::preset_240p());
181 assert_eq!(factory.rendition().name, "240p");
182 assert_eq!(factory.rendition().width, 426);
183 }
184
185 #[test]
186 fn transcoder_counts_each_fragment() {
187 let mut t = PassthroughTranscoder::new(&RenditionSpec::preset_720p());
188 let ctx = ctx("0.mp4", RenditionSpec::preset_720p());
189 t.on_start(&ctx);
190 for i in 0..5 {
191 t.on_fragment(&frag(i));
192 }
193 assert_eq!(t.fragments_seen(), 5);
194 t.on_stop();
195 }
196}