Skip to main content

lvqr_transcode/
passthrough.rs

1//! [`PassthroughTranscoder`] + [`PassthroughTranscoderFactory`].
2//!
3//! Scaffold implementation for session 104 A. Observes source
4//! fragments and counts them but does not actually transcode or
5//! republish. Exists to prove the
6//! [`lvqr_fragment::FragmentBroadcasterRegistry`] callback /
7//! subscribe / drain / panic-isolation wiring end-to-end before
8//! session 105 B pulls `gstreamer-rs` in.
9//!
10//! The factory defaults to video-only: it returns `None` for any
11//! track other than `"0.mp4"`. Operators who want to observe
12//! audio / captions / catalog tracks can construct their own
13//! factory with a wider track filter.
14
15use lvqr_fragment::Fragment;
16use tracing::{debug, info};
17
18use crate::rendition::RenditionSpec;
19use crate::transcoder::{Transcoder, TranscoderContext, TranscoderFactory};
20
21/// Source track the 104 A pass-through accepts. Video only; audio
22/// passes through the registry untouched, and caption / catalog
23/// tracks have no transcoder use case on the 4.6 ladder.
24const DEFAULT_SOURCE_TRACK: &str = "0.mp4";
25
26/// Pass-through transcoder: logs each fragment and counts calls
27/// but does NOT encode or republish. The real encoder lives in
28/// session 105 B.
29///
30/// Held by the [`crate::TranscodeRunner`]'s drain task. One
31/// instance per `(source_broadcast, rendition)` pair the factory
32/// opts into.
33pub struct PassthroughTranscoder {
34    rendition_name: String,
35    fragments_seen: u64,
36}
37
38impl PassthroughTranscoder {
39    /// Construct a fresh transcoder for `rendition`. The
40    /// [`crate::TranscodeRunner`] owns the call site; operators
41    /// typically use [`PassthroughTranscoderFactory`] instead of
42    /// constructing one directly.
43    pub fn new(rendition: &RenditionSpec) -> Self {
44        Self {
45            rendition_name: rendition.name.clone(),
46            fragments_seen: 0,
47        }
48    }
49
50    /// How many fragments this transcoder has observed. Exposed
51    /// for test assertions; production code should consult the
52    /// [`crate::TranscodeRunnerHandle`]'s per-
53    /// `(transcoder, rendition, broadcast, track)` counters
54    /// instead.
55    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
92/// Factory that builds a [`PassthroughTranscoder`] for each
93/// video-track source stream a [`crate::TranscodeRunner`] sees.
94///
95/// Constructed with a single [`RenditionSpec`]; register N
96/// factory instances on the runner (typically three, one per
97/// rung of [`RenditionSpec::default_ladder`]) to scaffold an
98/// ABR ladder's observability without the gstreamer dep.
99pub struct PassthroughTranscoderFactory {
100    rendition: RenditionSpec,
101}
102
103impl PassthroughTranscoderFactory {
104    /// Build a factory for the supplied rendition.
105    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}