Skip to main content

arcly_stream/
transcode.rs

1//! Transcoding & adaptive-bitrate contracts.
2//!
3//! The actual codec work needs native encoders (FFmpeg/GStreamer, NVENC/QSV via
4//! [`HwAccelBackend`](crate::HwAccelBackend)) and is intentionally **not** shipped
5//! here. This module defines the seams: a [`Transcoder`] turns one input frame
6//! into zero or more output frames, and a [`RenditionSpec`] describes one rung
7//! of an ABR ladder. [`drive_abr_ladder`] drives a [`Transcoder`] from a live
8//! stream and republishes each output rendition to its layer-key stream, so the
9//! WebRTC egress's per-viewer layer selection treats transcoded rungs exactly
10//! like simulcast layers.
11
12use crate::bus::{PlaybackRegistry, PublishRegistry};
13use crate::frame::CodecId;
14use crate::{MediaFrame, Result, StreamKey};
15use async_trait::async_trait;
16use tokio_util::sync::CancellationToken;
17
18/// One target rendition in an adaptive-bitrate ladder.
19#[derive(Debug, Clone, PartialEq, Eq)]
20pub struct RenditionSpec {
21    /// Human label / variant id (e.g. `"720p"`).
22    pub name: String,
23    /// Target encoded width in pixels (0 = keep source).
24    pub width: u32,
25    /// Target encoded height in pixels (0 = keep source).
26    pub height: u32,
27    /// Target video bitrate in bits/sec.
28    pub video_bitrate_bps: u64,
29    /// Output video codec.
30    pub codec: CodecId,
31}
32
33impl RenditionSpec {
34    /// A 16:9 rendition of the given height at `video_bitrate_bps`.
35    pub fn new(
36        name: impl Into<String>,
37        height: u32,
38        video_bitrate_bps: u64,
39        codec: CodecId,
40    ) -> Self {
41        Self {
42            name: name.into(),
43            width: height * 16 / 9,
44            height,
45            video_bitrate_bps,
46            codec,
47        }
48    }
49}
50
51/// Transforms frames: decode/scale/encode, packetize, or fan into renditions.
52///
53/// Returning a `Vec` lets one input frame yield several outputs (e.g. one per
54/// ABR rung) or none (e.g. while priming an encoder).
55#[async_trait]
56pub trait Transcoder: Send + Sync {
57    /// Stable identifier (e.g. `"nvenc-abr"`).
58    fn id(&self) -> &str;
59
60    /// The renditions this transcoder emits.
61    fn renditions(&self) -> &[RenditionSpec];
62
63    /// Process one input frame into zero or more output frames.
64    async fn transcode(&mut self, frame: MediaFrame) -> Result<Vec<MediaFrame>>;
65
66    /// Flush any buffered output at end-of-stream.
67    async fn flush(&mut self) -> Result<Vec<MediaFrame>> {
68        Ok(Vec::new())
69    }
70}
71
72/// The [`track_id`](crate::MediaFrame::track_id) a [`Transcoder`] stamps on the
73/// **first** rendition's output frames; rendition *n* (its `renditions()[n]`)
74/// uses `RENDITION_TRACK_BASE + n`. This is the contract
75/// [`drive_abr_ladder`] relies on to route each output to its rung stream.
76pub const RENDITION_TRACK_BASE: u32 = 100;
77
78/// Drive an adaptive-bitrate ladder: subscribe to the `base` stream, run every
79/// frame through `transcoder`, and **republish each rendition** to its layer
80/// stream (`base.layer(&spec.name)`) so the WebRTC egress's per-viewer layer
81/// selection treats the transcoded rungs exactly like simulcast layers.
82///
83/// Codec-agnostic by design — the heavy decode/scale/encode lives in the injected
84/// [`Transcoder`] (e.g. the FFmpeg-backed `arcly-stream-transcode`); this driver
85/// is the bus plumbing that connects it to the ABR layer-key convention. Each
86/// output frame is routed by its [`track_id`](crate::MediaFrame::track_id) offset
87/// from [`RENDITION_TRACK_BASE`]. Runs until the base stream ends or `shutdown`
88/// fires.
89pub async fn drive_abr_ladder(
90    playback: &dyn PlaybackRegistry,
91    publish: &dyn PublishRegistry,
92    base: &StreamKey,
93    transcoder: &mut dyn Transcoder,
94    shutdown: &CancellationToken,
95) -> Result<()> {
96    // Open a publish session per rung up front, keyed by the shared layer
97    // convention so the egress can discover them.
98    let rung_keys: Vec<StreamKey> = transcoder
99        .renditions()
100        .iter()
101        .map(|spec| base.layer(&spec.name))
102        .collect();
103    let mut rungs = Vec::with_capacity(rung_keys.len());
104    for key in &rung_keys {
105        rungs.push(publish.start_publish(key).await?);
106    }
107
108    // Route one batch of transcoder outputs to their rung streams by track_id.
109    let route = |rungs: &[crate::bus::StreamHandle], outputs: Vec<MediaFrame>| -> Result<()> {
110        for out in outputs {
111            let idx = out.track_id.wrapping_sub(RENDITION_TRACK_BASE) as usize;
112            if let Some(handle) = rungs.get(idx) {
113                handle.publish_frame(out)?;
114            }
115        }
116        Ok(())
117    };
118
119    let mut sub = playback.get_stream(base)?.subscribe_resilient();
120    loop {
121        tokio::select! {
122            _ = shutdown.cancelled() => break,
123            next = sub.recv() => match next {
124                Some(frame) => route(&rungs, transcoder.transcode((*frame).clone()).await?)?,
125                None => break, // base stream ended
126            }
127        }
128    }
129    route(&rungs, transcoder.flush().await?)?;
130    for key in &rung_keys {
131        publish.end_publish(key).await?;
132    }
133    Ok(())
134}
135
136#[cfg(test)]
137mod tests {
138    use super::*;
139    use crate::bus::PlaybackRegistry;
140    use crate::inbound::IngestContext;
141    use crate::{AppSpec, Engine};
142    use std::sync::Arc;
143    use tokio_util::sync::CancellationToken;
144
145    /// A stand-in transcoder: fans every input frame out to one tagged output per
146    /// rung (no real codec work), so the driver's routing can be exercised.
147    struct FanoutTranscoder {
148        ladder: Vec<RenditionSpec>,
149    }
150
151    #[async_trait]
152    impl Transcoder for FanoutTranscoder {
153        fn id(&self) -> &str {
154            "fanout"
155        }
156        fn renditions(&self) -> &[RenditionSpec] {
157            &self.ladder
158        }
159        async fn transcode(&mut self, frame: MediaFrame) -> Result<Vec<MediaFrame>> {
160            Ok((0..self.ladder.len())
161                .map(|n| {
162                    let mut f = frame.clone();
163                    f.track_id = RENDITION_TRACK_BASE + n as u32;
164                    f
165                })
166                .collect())
167        }
168    }
169
170    #[tokio::test]
171    async fn republishes_renditions_to_layer_streams() {
172        let engine = Engine::builder()
173            .application(AppSpec::new("live").gop_cache(4))
174            .build();
175        let base = StreamKey::new("live", "cam");
176
177        // Publish a keyframe to the base so the driver's instant-start replay has
178        // something to transcode.
179        let ctx = IngestContext::new(engine.clone());
180        let session = ctx.open_publish(base.clone()).await.unwrap();
181        session
182            .publish_frame(MediaFrame::new_video(
183                0,
184                0,
185                bytes::Bytes::from_static(&[0, 0, 0, 1, 0x65]),
186                CodecId::H264,
187                true,
188            ))
189            .unwrap();
190
191        let shutdown = CancellationToken::new();
192        let sd = shutdown.clone();
193        let eng = Arc::clone(&engine);
194        let driver = tokio::spawn(async move {
195            let mut tc = FanoutTranscoder {
196                ladder: vec![
197                    RenditionSpec::new("720p", 720, 3_000_000, CodecId::H264),
198                    RenditionSpec::new("480p", 480, 1_200_000, CodecId::H264),
199                ],
200            };
201            drive_abr_ladder(eng.as_ref(), eng.as_ref(), &base, &mut tc, &sd)
202                .await
203                .unwrap();
204        });
205
206        // Wait for the rung streams to come up.
207        let mut up = false;
208        for _ in 0..200 {
209            if engine
210                .get_stream(&StreamKey::new("live", "cam~720p"))
211                .is_ok()
212                && engine
213                    .get_stream(&StreamKey::new("live", "cam~480p"))
214                    .is_ok()
215            {
216                up = true;
217                break;
218            }
219            tokio::time::sleep(std::time::Duration::from_millis(2)).await;
220        }
221        shutdown.cancel();
222        let _ = driver.await;
223        assert!(up, "both ABR rungs were republished as layer streams");
224    }
225}