arcly_stream/
transcode.rs1use 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#[derive(Debug, Clone, PartialEq, Eq)]
20pub struct RenditionSpec {
21 pub name: String,
23 pub width: u32,
25 pub height: u32,
27 pub video_bitrate_bps: u64,
29 pub codec: CodecId,
31}
32
33impl RenditionSpec {
34 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#[async_trait]
56pub trait Transcoder: Send + Sync {
57 fn id(&self) -> &str;
59
60 fn renditions(&self) -> &[RenditionSpec];
62
63 async fn transcode(&mut self, frame: MediaFrame) -> Result<Vec<MediaFrame>>;
65
66 async fn flush(&mut self) -> Result<Vec<MediaFrame>> {
68 Ok(Vec::new())
69 }
70}
71
72pub const RENDITION_TRACK_BASE: u32 = 100;
77
78pub 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 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 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, }
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 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 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 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}