use std::task::{Poll, ready};
use bytes::Bytes;
use hang::catalog::{AudioConfig, VideoCodec, VideoConfig};
use super::consumer::Event;
use crate::catalog::hang::Container as HangContainer;
use crate::codec::h264::Avc1;
use crate::codec::h265::Hvc1;
use crate::container::{Consumer, Frame};
pub(crate) enum VideoTransform {
Avc1(Avc1),
Hvc1(Hvc1),
}
impl VideoTransform {
pub(crate) fn codec_private(&self) -> Option<&Bytes> {
match self {
VideoTransform::Avc1(t) => t.avcc(),
VideoTransform::Hvc1(t) => t.hvcc(),
}
}
pub(crate) fn transform(&mut self, payload: Bytes) -> crate::Result<Option<Bytes>> {
match self {
VideoTransform::Avc1(t) => Ok(t.transform(payload)?),
VideoTransform::Hvc1(t) => Ok(t.transform(payload)?),
}
}
}
enum SourceState {
Requesting(kio::Pending<moq_net::origin::Requesting>, String),
Subscribing(kio::Pending<moq_net::track::Subscribing>),
Active(Box<Consumer<HangContainer>>),
}
pub(crate) struct ExportSource {
state: SourceState,
media: Option<HangContainer>,
max_age: std::time::Duration,
transform: Option<VideoTransform>,
description: Option<Bytes>,
video_codec: Option<VideoCodec>,
video_dimensions: Option<(u32, u32)>,
}
impl ExportSource {
pub fn for_video(
source: &crate::Source,
name: &str,
config: &VideoConfig,
max_age: std::time::Duration,
) -> Result<Option<Self>, crate::Error> {
Self::video(source, name, config, max_age, build_video_transform(config))
}
pub fn for_video_raw(
source: &crate::Source,
name: &str,
config: &VideoConfig,
max_age: std::time::Duration,
) -> Result<Option<Self>, crate::Error> {
Self::video(source, name, config, max_age, None)
}
fn video(
source: &crate::Source,
name: &str,
config: &VideoConfig,
max_age: std::time::Duration,
transform: Option<VideoTransform>,
) -> Result<Option<Self>, crate::Error> {
let media: HangContainer = config.try_into()?;
let description = config.description.as_ref().filter(|b| !b.is_empty()).cloned();
let Some(request) = source.try_request(config.broadcast.as_ref()) else {
return Ok(None);
};
let mut source = Self {
state: SourceState::Requesting(request, name.to_string()),
media: Some(media),
max_age,
transform,
description,
video_codec: Some(config.codec.clone()),
video_dimensions: catalog_dimensions(config),
};
source.resolve_video_dimensions(&[])?;
Ok(Some(source))
}
pub fn for_audio(
source: &crate::Source,
name: &str,
config: &AudioConfig,
max_age: std::time::Duration,
) -> Result<Option<Self>, crate::Error> {
let media: HangContainer = config.try_into()?;
let description = config.description.as_ref().filter(|b| !b.is_empty()).cloned();
let Some(request) = source.try_request(config.broadcast.as_ref()) else {
return Ok(None);
};
Ok(Some(Self {
state: SourceState::Requesting(request, name.to_string()),
media: Some(media),
max_age,
transform: None,
description,
video_codec: None,
video_dimensions: None,
}))
}
pub fn for_stream(source: &crate::Source, name: &str, max_age: std::time::Duration) -> Result<Self, crate::Error> {
Ok(Self {
state: SourceState::Requesting(source.request_catalog(), name.to_string()),
media: Some(HangContainer::Legacy(crate::container::Kind::Data)),
max_age,
transform: None,
description: None,
video_codec: None,
video_dimensions: None,
})
}
pub fn description(&self) -> Option<&Bytes> {
self.description.as_ref()
}
pub fn discontinuity(&self) -> u64 {
match &self.state {
SourceState::Active(consumer) => consumer.discontinuity(),
_ => 0,
}
}
pub fn header_ready(&self) -> bool {
self.transform.is_none() || self.description.is_some()
}
pub fn video_config(&self, config: &VideoConfig) -> Option<VideoConfig> {
if catalog_dimensions(config).is_some() {
return Some(config.clone());
}
let (width, height) = self.video_dimensions?;
let mut config = config.clone();
config.coded_width = Some(width);
config.coded_height = Some(height);
Some(config)
}
pub fn video_geometry_ready(&self, config: &VideoConfig) -> bool {
!matches!(
config.codec,
VideoCodec::H264(_) | VideoCodec::H265(_) | VideoCodec::VP8 | VideoCodec::VP9(_) | VideoCodec::AV1(_)
) || self.video_config(config).is_some()
}
pub fn poll_read(&mut self, waiter: &kio::Waiter) -> Poll<crate::Result<Option<Frame>>> {
loop {
match ready!(self.poll_event(waiter))? {
Some(Event::Frame(frame)) => return Poll::Ready(Ok(Some(frame))),
Some(Event::GroupEnd | Event::FrameEnd(_)) => continue,
None => return Poll::Ready(Ok(None)),
}
}
}
pub fn poll_event(&mut self, waiter: &kio::Waiter) -> Poll<crate::Result<Option<Event>>> {
if matches!(self.state, SourceState::Requesting(..)) {
let (broadcast, name) = {
let SourceState::Requesting(pending, name) = &self.state else {
unreachable!("just matched Requesting");
};
(ready!(pending.poll_ok(waiter))?, name.clone())
};
let subscription = moq_net::track::Subscription::default().with_max_age(self.max_age);
self.state = SourceState::Subscribing(broadcast.track(&name)?.subscribe(subscription));
}
if matches!(self.state, SourceState::Subscribing(_)) {
let track = {
let SourceState::Subscribing(pending) = &self.state else {
unreachable!("just matched Subscribing");
};
ready!(pending.poll_ok(waiter))?
};
let media = self
.media
.take()
.expect("media present until the subscription resolves");
self.state = SourceState::Active(Box::new(Consumer::new(track, media)));
}
loop {
let frame = {
let SourceState::Active(consumer) = &mut self.state else {
unreachable!("subscription resolved into an Active consumer");
};
match ready!(consumer.poll_event(waiter))? {
Some(Event::Frame(frame)) => frame,
event => return Poll::Ready(Ok(event)),
}
};
let Some(transform) = self.transform.as_mut() else {
self.resolve_video_dimensions(&frame.payload)?;
return Poll::Ready(Ok(Some(Event::Frame(frame))));
};
match transform.transform(frame.payload.clone())? {
None => {
self.refresh_description();
self.resolve_video_dimensions(&frame.payload)?;
continue;
}
Some(payload) => {
self.refresh_description();
self.resolve_video_dimensions(&payload)?;
return Poll::Ready(Ok(Some(Event::Frame(Frame { payload, ..frame }))));
}
}
}
}
fn refresh_description(&mut self) {
if let Some(transform) = self.transform.as_ref()
&& let Some(d) = transform.codec_private()
&& self.description.as_ref() != Some(d)
{
self.description = Some(d.clone());
}
}
fn resolve_video_dimensions(&mut self, payload: &[u8]) -> crate::Result<()> {
if self.video_dimensions.is_some() {
return Ok(());
}
let Some(codec) = self.video_codec.as_ref() else {
return Ok(());
};
self.video_dimensions = codec_dimensions(codec, self.description.as_deref(), payload)?;
Ok(())
}
}
pub(crate) fn catalog_dimensions(config: &VideoConfig) -> Option<(u32, u32)> {
let dimensions = (config.coded_width?, config.coded_height?);
(dimensions.0 > 0 && dimensions.1 > 0).then_some(dimensions)
}
pub(crate) fn codec_dimensions(
codec: &VideoCodec,
description: Option<&[u8]>,
payload: &[u8],
) -> crate::Result<Option<(u32, u32)>> {
let dimensions = match codec {
VideoCodec::H264(_) => match description {
Some(description) => catalog_dimensions(&crate::codec::h264::config(description)?),
None => None,
},
VideoCodec::H265(_) => match description {
Some(description) => catalog_dimensions(&crate::codec::h265::config(description)?),
None => None,
},
VideoCodec::VP8 if !payload.is_empty() => crate::codec::vp8::FrameHeader::parse(payload)?
.dimensions
.map(|(width, height)| (u32::from(width), u32::from(height))),
VideoCodec::VP9(_) if !payload.is_empty() => crate::codec::vp9::config_from_keyframe(payload)?
.as_ref()
.and_then(catalog_dimensions),
VideoCodec::AV1(_) if !payload.is_empty() => crate::codec::av1::dimensions(payload)?,
_ => None,
};
Ok(dimensions.filter(|(width, height)| *width > 0 && *height > 0))
}
pub(crate) fn build_video_transform(config: &VideoConfig) -> Option<VideoTransform> {
let needs_transform = config.description.as_ref().map(|d| d.is_empty()).unwrap_or(true);
if !needs_transform {
return None;
}
match &config.codec {
VideoCodec::H264(_) => Some(VideoTransform::Avc1(Avc1::new())),
VideoCodec::H265(_) => Some(VideoTransform::Hvc1(Hvc1::new())),
_ => None,
}
}
#[cfg(test)]
mod tests {
use hang::catalog::{AudioCodec, Container, H264};
use moq_net::path::Relative;
use super::*;
use crate::container::test_util::Live;
fn video(broadcast: Option<&str>) -> VideoConfig {
let mut config = VideoConfig::new(H264 {
profile: 0x42,
constraints: 0xc0,
level: 0x1f,
inline: true,
});
config.container = Container::Legacy;
config.broadcast = broadcast.map(|b| Relative::new(b).into_owned());
config
}
fn audio(broadcast: Option<&str>) -> AudioConfig {
let mut config = AudioConfig::new(AudioCodec::Opus, 48_000, 2);
config.container = Container::Legacy;
config.broadcast = broadcast.map(|b| Relative::new(b).into_owned());
config
}
#[tokio::test]
async fn escaping_reference_skips_the_rendition() {
let live = Live::avc3();
let source = live.source();
let max_age = std::time::Duration::ZERO;
let escaping = |result: Result<Option<ExportSource>, crate::Error>, what: &str| match result {
Ok(None) => {}
Err(err) => panic!("{what} failed instead of being skipped: {err:?}"),
Ok(Some(_)) => panic!("{what} should not resolve"),
};
for reference in ["..", "../source", "../../elsewhere"] {
let config = video(Some(reference));
escaping(ExportSource::for_video(&source, "video", &config, max_age), reference);
escaping(
ExportSource::for_video_raw(&source, "video", &config, max_age),
reference,
);
escaping(
ExportSource::for_audio(&source, "audio", &audio(Some(reference)), max_age),
reference,
);
}
}
#[tokio::test]
async fn legal_reference_keeps_the_rendition() {
let live = Live::avc3();
let source = live.source();
let max_age = std::time::Duration::ZERO;
for reference in [None, Some(""), Some("./source"), Some("sub"), Some(".")] {
ExportSource::for_video(&source, "video", &video(reference), max_age)
.unwrap_or_else(|err| panic!("{reference:?} should keep the rendition: {err:?}"))
.unwrap_or_else(|| panic!("{reference:?} should keep the rendition"));
ExportSource::for_audio(&source, "audio", &audio(reference), max_age)
.unwrap_or_else(|err| panic!("{reference:?} should keep the rendition: {err:?}"))
.unwrap_or_else(|| panic!("{reference:?} should keep the rendition"));
}
}
#[tokio::test]
async fn latency_is_sent_with_the_initial_subscription() {
let live = Live::avc3();
let max_age = std::time::Duration::from_secs(10);
let mut export = ExportSource::for_video(&live.source(), live.track.name(), &video(None), max_age)
.unwrap()
.expect("fixture should produce a video rendition");
let observed = kio::wait(|waiter| {
let _ = export.poll_read(waiter);
match live.track.subscription() {
Some(subscription) => Poll::Ready(subscription.max_age),
None => Poll::Pending,
}
})
.await;
assert_eq!(observed, max_age);
}
}