use crate::bus::{PlaybackRegistry, PublishRegistry};
use crate::frame::CodecId;
use crate::{MediaFrame, Result, StreamKey};
use async_trait::async_trait;
use tokio_util::sync::CancellationToken;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct RenditionSpec {
pub name: String,
pub width: u32,
pub height: u32,
pub video_bitrate_bps: u64,
pub codec: CodecId,
}
impl RenditionSpec {
pub fn new(
name: impl Into<String>,
height: u32,
video_bitrate_bps: u64,
codec: CodecId,
) -> Self {
Self {
name: name.into(),
width: height * 16 / 9,
height,
video_bitrate_bps,
codec,
}
}
}
#[async_trait]
pub trait Transcoder: Send + Sync {
fn id(&self) -> &str;
fn renditions(&self) -> &[RenditionSpec];
async fn transcode(&mut self, frame: MediaFrame) -> Result<Vec<MediaFrame>>;
async fn flush(&mut self) -> Result<Vec<MediaFrame>> {
Ok(Vec::new())
}
}
pub const RENDITION_TRACK_BASE: u32 = 100;
pub async fn drive_abr_ladder(
playback: &dyn PlaybackRegistry,
publish: &dyn PublishRegistry,
base: &StreamKey,
transcoder: &mut dyn Transcoder,
shutdown: &CancellationToken,
) -> Result<()> {
let rung_keys: Vec<StreamKey> = transcoder
.renditions()
.iter()
.map(|spec| base.layer(&spec.name))
.collect();
let mut rungs = Vec::with_capacity(rung_keys.len());
for key in &rung_keys {
rungs.push(publish.start_publish(key).await?);
}
let route = |rungs: &[crate::bus::StreamHandle], outputs: Vec<MediaFrame>| -> Result<()> {
for out in outputs {
let idx = out.track_id.wrapping_sub(RENDITION_TRACK_BASE) as usize;
if let Some(handle) = rungs.get(idx) {
handle.publish_frame(out)?;
}
}
Ok(())
};
let mut sub = playback.get_stream(base)?.subscribe_resilient();
loop {
tokio::select! {
_ = shutdown.cancelled() => break,
next = sub.recv() => match next {
Some(frame) => route(&rungs, transcoder.transcode((*frame).clone()).await?)?,
None => break, }
}
}
route(&rungs, transcoder.flush().await?)?;
for key in &rung_keys {
publish.end_publish(key).await?;
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use crate::bus::PlaybackRegistry;
use crate::inbound::IngestContext;
use crate::{AppSpec, Engine};
use std::sync::Arc;
use tokio_util::sync::CancellationToken;
struct FanoutTranscoder {
ladder: Vec<RenditionSpec>,
}
#[async_trait]
impl Transcoder for FanoutTranscoder {
fn id(&self) -> &str {
"fanout"
}
fn renditions(&self) -> &[RenditionSpec] {
&self.ladder
}
async fn transcode(&mut self, frame: MediaFrame) -> Result<Vec<MediaFrame>> {
Ok((0..self.ladder.len())
.map(|n| {
let mut f = frame.clone();
f.track_id = RENDITION_TRACK_BASE + n as u32;
f
})
.collect())
}
}
#[tokio::test]
async fn republishes_renditions_to_layer_streams() {
let engine = Engine::builder()
.application(AppSpec::new("live").gop_cache(4))
.build();
let base = StreamKey::new("live", "cam");
let ctx = IngestContext::new(engine.clone());
let session = ctx.open_publish(base.clone()).await.unwrap();
session
.publish_frame(MediaFrame::new_video(
0,
0,
bytes::Bytes::from_static(&[0, 0, 0, 1, 0x65]),
CodecId::H264,
true,
))
.unwrap();
let shutdown = CancellationToken::new();
let sd = shutdown.clone();
let eng = Arc::clone(&engine);
let driver = tokio::spawn(async move {
let mut tc = FanoutTranscoder {
ladder: vec![
RenditionSpec::new("720p", 720, 3_000_000, CodecId::H264),
RenditionSpec::new("480p", 480, 1_200_000, CodecId::H264),
],
};
drive_abr_ladder(eng.as_ref(), eng.as_ref(), &base, &mut tc, &sd)
.await
.unwrap();
});
let mut up = false;
for _ in 0..200 {
if engine
.get_stream(&StreamKey::new("live", "cam~720p"))
.is_ok()
&& engine
.get_stream(&StreamKey::new("live", "cam~480p"))
.is_ok()
{
up = true;
break;
}
tokio::time::sleep(std::time::Duration::from_millis(2)).await;
}
shutdown.cancel();
let _ = driver.await;
assert!(up, "both ABR rungs were republished as layer streams");
}
}