#[cfg(feature = "capture")]
use std::time::Instant;
use moq_mux::catalog::hang::CatalogExt;
#[cfg(feature = "capture")]
use moq_mux::rate::{Control, Policy};
#[cfg(any(feature = "capture", test))]
use moq_net::Timestamp;
use crate::Error;
#[cfg(feature = "capture")]
use crate::Rate;
#[cfg(feature = "capture")]
use crate::capture;
use super::Encoded;
#[cfg(feature = "capture")]
use super::Sink;
#[cfg(any(feature = "capture", test))]
use super::encoder;
#[cfg(feature = "capture")]
use super::encoder::Codec;
#[cfg(feature = "capture")]
const DEFAULT_FRAMERATE: Rate = Rate::integer(30);
fn rendition_hint(rendition: hang::catalog::VideoConfig) -> moq_mux::catalog::VideoHint {
let mut hint = moq_mux::catalog::VideoHint::default();
hint.codec = Some(rendition.codec);
hint.coded_width = rendition.coded_width;
hint.coded_height = rendition.coded_height;
hint.display_aspect_width = rendition.display_aspect_width;
hint.display_aspect_height = rendition.display_aspect_height;
hint.framerate = rendition.framerate;
hint.bitrate = rendition.bitrate;
hint.optimize_for_latency = rendition.optimize_for_latency;
hint.container = rendition.container;
hint
}
enum Codecs {
H264 {
split: moq_mux::codec::h264::Split,
import: moq_mux::codec::h264::Import,
},
H265 {
split: moq_mux::codec::h265::Split,
import: moq_mux::codec::h265::Import,
},
}
pub struct Producer<E: CatalogExt = ()> {
codecs: Codecs,
_ext: std::marker::PhantomData<fn() -> E>,
}
impl<E: CatalogExt> Producer<E> {
pub fn new(
broadcast: moq_net::broadcast::Producer,
catalog: moq_mux::catalog::Producer<E>,
rendition: hang::catalog::VideoConfig,
) -> Result<Self, Error> {
let suffix = match &rendition.codec {
hang::catalog::VideoCodec::H264(_) => ".avc3",
hang::catalog::VideoCodec::H265(_) => ".hev1",
other => {
return Err(Error::Codec(anyhow::anyhow!(
"{other} is not a codec this producer can publish"
)));
}
};
let track = broadcast.unique_track(suffix, catalog.track_info(hang::catalog::PRIORITY.video))?;
Self::with_track(track, catalog, rendition)
}
pub fn with_track(
track: moq_net::track::Producer,
catalog: moq_mux::catalog::Producer<E>,
rendition: hang::catalog::VideoConfig,
) -> Result<Self, Error> {
let codecs = match &rendition.codec {
hang::catalog::VideoCodec::H264(_) => Codecs::H264 {
split: moq_mux::codec::h264::Split::new(),
import: moq_mux::codec::h264::Import::new(track, catalog.reserve(), rendition_hint(rendition))?,
},
hang::catalog::VideoCodec::H265(_) => Codecs::H265 {
split: moq_mux::codec::h265::Split::new(),
import: moq_mux::codec::h265::Import::new(track, catalog.reserve(), rendition_hint(rendition))?,
},
other => {
return Err(Error::Codec(anyhow::anyhow!(
"{other} is not a codec this producer can publish"
)));
}
};
Ok(Self {
codecs,
_ext: std::marker::PhantomData,
})
}
pub fn demand(&self) -> moq_net::track::Demand {
match &self.codecs {
Codecs::H264 { import, .. } => import.demand(),
Codecs::H265 { import, .. } => import.demand(),
}
}
pub fn publish(&mut self, encoded: &[Encoded]) -> Result<(), Error> {
for frame in encoded {
let timestamp = Some(frame.timestamp);
match &mut self.codecs {
Codecs::H264 { split, import } => {
let mut frames = split.decode(&frame.payload, timestamp)?;
frames.extend(split.flush(timestamp)?);
import.decode(frames)?;
import.flush(frame.timestamp, std::time::Instant::now())?;
}
Codecs::H265 { split, import } => {
let mut frames = split.decode(&frame.payload, timestamp)?;
frames.extend(split.flush(timestamp)?);
import.decode(frames)?;
import.flush(frame.timestamp, std::time::Instant::now())?;
}
}
}
Ok(())
}
pub fn observe_lag(&mut self, lag: std::time::Duration) -> Result<(), Error> {
match &mut self.codecs {
Codecs::H264 { import, .. } => import.observe_lag(lag)?,
Codecs::H265 { import, .. } => import.observe_lag(lag)?,
}
Ok(())
}
pub fn tick(&mut self) -> Result<(), Error> {
match &mut self.codecs {
Codecs::H264 { import, .. } => import.tick()?,
Codecs::H265 { import, .. } => import.tick()?,
}
Ok(())
}
pub fn idle(&mut self) -> Result<(), Error> {
match &mut self.codecs {
Codecs::H264 { import, .. } => import.idle()?,
Codecs::H265 { import, .. } => import.idle()?,
}
Ok(())
}
pub fn discontinuity(&mut self) -> Result<(), Error> {
match &mut self.codecs {
Codecs::H264 { import, .. } => import.discontinuity()?,
Codecs::H265 { import, .. } => import.discontinuity()?,
}
Ok(())
}
pub fn finish(&mut self) -> Result<(), Error> {
match &mut self.codecs {
Codecs::H264 { import, .. } => import.finish()?,
Codecs::H265 { import, .. } => import.finish()?,
}
Ok(())
}
pub fn abort(self, err: moq_net::Error) {
match self.codecs {
Codecs::H264 { import, .. } => import.abort(err),
Codecs::H265 { import, .. } => import.abort(err),
}
}
}
#[derive(Clone, Debug, Default)]
#[non_exhaustive]
#[cfg(feature = "capture")]
pub struct Options {
pub bitrate: Option<moq_net::bandwidth::Rate>,
pub codec: Codec,
pub kind: encoder::Kind,
pub bandwidth: moq_net::bandwidth::Allocator,
}
#[cfg(feature = "capture")]
pub async fn publish_capture<E: CatalogExt>(
broadcast: moq_net::broadcast::Producer,
catalog: moq_mux::catalog::Producer<E>,
capture: capture::Config,
encode: Options,
clock: moq_mux::Clock,
) -> Result<(), Error> {
let rendition = {
let camera = capture::open(&capture).await?;
let mut probe_config = encoder::Config::new(
camera.width(),
camera.height(),
capture
.framerate
.or_else(|| camera.framerate())
.unwrap_or(DEFAULT_FRAMERATE),
);
probe_config.bitrate = encode.bitrate;
probe_config.codec = encode.codec;
probe_config.kind = encode.kind.clone();
probe_config.color = camera.color();
probe_config.probe().await?
};
let mut producer = Producer::new(broadcast, catalog, rendition)?;
let demand = producer.demand();
let result = capture_loop(&mut producer, &demand, &mut DeviceSource, &capture, &encode, &clock).await;
match &result {
Ok(()) => {
if let Err(err) = producer.finish() {
tracing::debug!(error = %err, "video track finish after capture ended");
}
}
Err(err) => producer.abort(moq_net::Error::Transport(err.to_string())),
}
result
}
#[cfg(all(feature = "capture", not(target_os = "macos")))]
#[allow(dead_code)]
fn assert_publish_capture_send(
broadcast: moq_net::broadcast::Producer,
catalog: moq_mux::catalog::Producer,
capture: capture::Config,
encode: Options,
clock: moq_mux::Clock,
) {
fn is_send<T: Send>(_: &T) {}
is_send(&publish_capture(broadcast, catalog, capture, encode, clock));
}
#[cfg(feature = "capture")]
trait CaptureSource {
async fn open(&mut self, config: &capture::Config) -> Result<capture::Stream, Error>;
}
#[cfg(feature = "capture")]
struct DeviceSource;
#[cfg(feature = "capture")]
impl CaptureSource for DeviceSource {
async fn open(&mut self, config: &capture::Config) -> Result<capture::Stream, Error> {
capture::open(config).await
}
}
#[cfg(feature = "capture")]
type RateControl = Option<(moq_net::bandwidth::Consumer, Control)>;
#[cfg(feature = "capture")]
async fn next_estimate(rate: &mut RateControl) -> Option<Option<moq_net::bandwidth::Rate>> {
match rate {
Some((bandwidth, _)) => bandwidth.changed().await.ok(),
None => std::future::pending().await,
}
}
#[cfg(feature = "capture")]
async fn apply_estimate(
encoder: &mut Sink,
rate: &mut RateControl,
estimate: Option<Option<moq_net::bandwidth::Rate>>,
) {
let Some((_, control)) = rate.as_mut() else { return };
let Some(estimate) = estimate else {
tracing::debug!("bandwidth estimate ended; holding the current encoder bitrate");
*rate = None;
return;
};
let Some(bitrate) = control.update(estimate, Instant::now()) else {
return;
};
match encoder.set_bitrate(bitrate).await {
Ok(()) => tracing::debug!(bitrate = bitrate.as_bps(), "adjusted encoder bitrate"),
Err(Error::BitrateUnsupported(name)) => {
tracing::warn!(encoder = name, "encoder cannot follow the bandwidth estimate");
*rate = None;
}
Err(err) => tracing::warn!(error = %err, bitrate = bitrate.as_bps(), "failed to adjust encoder bitrate"),
}
}
#[cfg(feature = "capture")]
fn log_track_ended(err: moq_net::Error) {
if matches!(err, moq_net::Error::Dropped | moq_net::Error::Closed) {
tracing::debug!("video track no longer announced; stopping capture");
} else {
tracing::warn!(error = %err, "video track aborted; stopping capture");
}
}
#[cfg(any(feature = "capture", all(test, feature = "openh264")))]
fn capture_stopped<E: CatalogExt>(producer: &mut Producer<E>) -> Result<(), Error> {
producer.discontinuity()
}
#[cfg(feature = "capture")]
async fn wait_capture<E: CatalogExt, T>(
producer: &mut Producer<E>,
demand: &moq_net::track::Demand,
work: impl std::future::Future<Output = Result<T, Error>>,
) -> Result<Option<T>, Error> {
let mut work = std::pin::pin!(work);
let mut timer = tokio::time::interval(hang::catalog::stalled::DEFAULT_INTERVAL);
loop {
tokio::select! {
biased;
res = demand.unused() => {
if let Err(err) = res {
log_track_ended(err);
}
producer.idle()?;
return Ok(None);
}
_ = timer.tick() => producer.tick()?,
res = &mut work => return res.map(Some),
}
}
}
#[cfg(feature = "capture")]
async fn capture_loop<E: CatalogExt, S: CaptureSource>(
producer: &mut Producer<E>,
demand: &moq_net::track::Demand,
source: &mut S,
capture: &capture::Config,
encode: &Options,
clock: &moq_mux::Clock,
) -> Result<(), Error> {
let mut reservation: Option<moq_net::bandwidth::Reservation> = None;
loop {
if let Err(err) = demand.used().await {
log_track_ended(err);
return Ok(());
}
let Some(mut camera) = wait_capture(producer, demand, source.open(capture)).await? else {
continue;
};
let capture_epoch =
u64::try_from(clock.now().as_micros().saturating_sub(camera.now().as_micros())).unwrap_or(u64::MAX);
let framerate = capture
.framerate
.or_else(|| camera.framerate())
.unwrap_or(DEFAULT_FRAMERATE);
let mut encoder_config = encoder::Config::new(camera.width(), camera.height(), framerate);
encoder_config.bitrate = encode.bitrate;
encoder_config.codec = encode.codec;
encoder_config.kind = encode.kind.clone();
encoder_config.color = camera.color();
let Some(mut encoder) = wait_capture(producer, demand, Sink::open(&encoder_config)).await? else {
continue;
};
tracing::info!(encoder = encoder.name(), device = camera.label(), "capturing");
let ceiling = encoder_config.resolved_bitrate();
let reservation = reservation.get_or_insert_with(|| encode.bandwidth.reserve(demand, ceiling));
reservation.update(ceiling);
let mut rate = Some((reservation.consumer(), Control::new(Policy::new(ceiling))));
loop {
let interval = hang::catalog::stalled::interval_from_fps(Some(framerate.as_f64()));
let frame = tokio::select! {
biased;
res = demand.unused() => {
if let Err(err) = res {
log_track_ended(err);
return Ok(());
}
break; }
estimate = next_estimate(&mut rate) => {
apply_estimate(&mut encoder, &mut rate, estimate).await;
continue;
}
frame = tokio::time::timeout(interval, camera.read()) => match frame {
Ok(frame) => frame?,
Err(_) => {
producer.tick()?;
continue;
}
},
};
let Some(mut frame) = frame else { break };
frame.timestamp = map_capture_timestamp(capture_epoch, frame.timestamp)?;
let started = Instant::now();
let Some(encoded) = wait_capture(producer, demand, encoder.encode(frame)).await? else {
break;
};
let lag = started.elapsed();
producer.observe_lag(lag)?;
producer.publish(&encoded)?;
}
drop(camera);
drop(encoder);
producer.idle()?;
capture_stopped(producer)?;
tracing::info!("capture stopped; released source");
}
}
#[cfg(feature = "capture")]
fn map_capture_timestamp(epoch_micros: u64, timestamp: Timestamp) -> Result<Timestamp, Error> {
let capture_micros = u64::try_from(timestamp.as_micros()).unwrap_or(u64::MAX);
Ok(Timestamp::from_micros(epoch_micros.saturating_add(capture_micros))?)
}
#[cfg(test)]
mod tests {
#![cfg_attr(not(feature = "openh264"), allow(dead_code, unused_imports))]
use moq_mux::catalog::Stream as _;
use super::*;
use crate::Frame;
use crate::encode::{Codec, Config, Encoder};
#[cfg(feature = "capture")]
#[test]
fn capture_clock_mapping_is_monotonic() {
let first = map_capture_timestamp(10_000, Timestamp::from_micros(2_000).unwrap()).unwrap();
let second = map_capture_timestamp(10_000, Timestamp::from_micros(2_001).unwrap()).unwrap();
assert!(second > first);
}
async fn roundtrip_rendition(codec: Codec, kind: encoder::Kind) -> (String, hang::catalog::VideoConfig) {
let mut broadcast = moq_net::broadcast::Info::new().produce();
let catalog = moq_mux::catalog::Producer::new(&mut broadcast, moq_mux::catalog::Config::default()).unwrap();
let mut config = Config::new(320, 240, crate::Rate::new(30, 1).unwrap());
config.codec = codec;
config.kind = kind;
let mut producer = Producer::new(broadcast, catalog.clone(), config.probe().await.unwrap()).unwrap();
let advertised = rendition(&catalog).expect("the rendition publishes before any frame").1;
let mut encoder = Encoder::new(&config).unwrap();
assert_eq!(encoder.codec(), codec);
let rgba = vec![0x80u8; 320 * 240 * 4];
for i in 0..10u64 {
let surface = crate::Surface::rgba(&rgba, crate::Size::new(320, 240)).unwrap();
let frame = Frame::new(surface, Timestamp::from_micros(i * 33_333).unwrap());
producer.publish(&encoder.encode(&frame).unwrap()).unwrap();
}
producer.publish(&encoder.finish().unwrap()).unwrap();
let (name, resolved) = rendition(&catalog).expect("the importer should have registered a video rendition");
let (mut before, mut after) = (advertised, resolved.clone());
before.jitter = None;
after.jitter = None;
assert_eq!(
before, after,
"the first keyframe should confirm the advertised rendition, not correct it"
);
(name, resolved)
}
fn rendition(catalog: &moq_mux::catalog::Producer) -> Option<(String, hang::catalog::VideoConfig)> {
let snapshot = catalog.snapshot();
let (name, config) = snapshot.video.renditions.iter().next()?;
Some((name.clone(), config.clone()))
}
async fn collect_groups(mut consumer: moq_net::track::Subscriber) -> Vec<usize> {
let mut groups = Vec::new();
while let Some(mut group) = consumer.recv_group().await.unwrap() {
let mut frames = 0;
while group.next_frame().await.unwrap().is_some() {
frames += 1;
}
groups.push(frames);
}
groups
}
#[tokio::test]
#[cfg(feature = "openh264")]
async fn idle_capture_publishes_a_discontinuity_before_resume() {
let mut broadcast = moq_net::broadcast::Info::new().produce();
let catalog = moq_mux::catalog::Producer::new(&mut broadcast, moq_mux::catalog::Config::default()).unwrap();
let replay = std::time::Duration::from_secs(11);
let track = broadcast
.create_track(
"video",
catalog.track_info(hang::catalog::PRIORITY.video).with_max_age(replay),
)
.unwrap();
let consumer = track.subscribe(moq_net::track::Subscription::default().with_max_age(replay));
let mut config = Config::new(320, 240, crate::Rate::new(30, 1).unwrap());
config.kind = encoder::Kind::Software;
let mut producer = Producer::with_track(track, catalog, config.probe().await.unwrap()).unwrap();
let mut encoder = Encoder::new(&config).unwrap();
let rgba = vec![0x80u8; 320 * 240 * 4];
for timestamp in [0, 10_000_000] {
if timestamp > 0 {
capture_stopped(&mut producer).unwrap();
}
encoder.cut().unwrap();
let surface = crate::Surface::rgba(&rgba, crate::Size::new(320, 240)).unwrap();
let frame = Frame::new(surface, Timestamp::from_micros(timestamp).unwrap());
producer.publish(&encoder.encode(&frame).unwrap()).unwrap();
}
producer.finish().unwrap();
assert_eq!(collect_groups(consumer).await, vec![1, 1, 1]);
}
#[tokio::test]
#[cfg(feature = "openh264")]
async fn source_resize_updates_the_published_rendition() {
let mut broadcast = moq_net::broadcast::Info::new().produce();
let catalog = moq_mux::catalog::Producer::new(&mut broadcast, moq_mux::catalog::Config::default()).unwrap();
let mut initial = Config::new(320, 240, crate::Rate::new(30, 1).unwrap());
initial.kind = encoder::Kind::Software;
let mut producer = Producer::new(broadcast, catalog.clone(), initial.probe().await.unwrap()).unwrap();
for (timestamp, config) in [
(0, initial),
(33_333, Config::new(640, 360, crate::Rate::new(30, 1).unwrap())),
] {
let mut config = config;
config.kind = encoder::Kind::Software;
let mut encoder = Encoder::new(&config).unwrap();
encoder.cut().unwrap();
let rgba = vec![0x80u8; usize::try_from(config.width * config.height * 4).unwrap()];
let surface = crate::Surface::rgba(&rgba, crate::Size::new(config.width, config.height)).unwrap();
let frame = Frame::new(surface, Timestamp::from_micros(timestamp).unwrap());
producer.publish(&encoder.encode(&frame).unwrap()).unwrap();
capture_stopped(&mut producer).unwrap();
}
let (_, rendition) = rendition(&catalog).expect("the resized rendition should be published");
assert_eq!(rendition.coded_width, Some(640));
assert_eq!(rendition.coded_height, Some(360));
}
#[tokio::test]
#[cfg(feature = "openh264")]
async fn a_selected_container_survives_the_rendition_hint() {
let mut broadcast = moq_net::broadcast::Info::new().produce();
let catalog = moq_mux::catalog::Producer::new(&mut broadcast, moq_mux::catalog::Config::default()).unwrap();
let mut config = Config::new(320, 240, crate::Rate::new(30, 1).unwrap());
config.kind = encoder::Kind::Software;
let mut selected = config.probe().await.unwrap();
selected.container = hang::catalog::Container::Loc;
let _producer = Producer::new(broadcast, catalog.clone(), selected).unwrap();
let (_, published) = rendition(&catalog).expect("the rendition publishes before any frame");
assert_eq!(published.container, hang::catalog::Container::Loc);
}
#[tokio::test]
#[cfg(feature = "openh264")]
async fn the_rendition_reaches_the_wire_before_the_first_frame() {
let mut broadcast = moq_net::broadcast::Info::new().produce();
let consumer = broadcast.consume();
let catalog = moq_mux::catalog::Producer::new(&mut broadcast, moq_mux::catalog::Config::default()).unwrap();
let mut config = Config::new(1920, 1080, crate::Rate::new(30, 1).unwrap());
config.bitrate = Some(moq_net::bandwidth::Rate::from_mbps(6));
config.kind = encoder::Kind::Software;
let _producer = Producer::new(broadcast, catalog, config.probe().await.unwrap()).unwrap();
let mut stream = moq_mux::catalog::Consumer::<()>::new(&consumer, moq_mux::catalog::CatalogFormat::Hang)
.await
.unwrap();
let snapshot = stream.next().await.unwrap().expect("a catalog before any frame");
let (name, rendition) = snapshot
.video
.renditions
.iter()
.next()
.expect("the track must be discoverable before it has encoded anything");
assert!(name.ends_with(".avc3"));
let hang::catalog::VideoCodec::H264(h264) = &rendition.codec else {
panic!("expected H.264, got {}", rendition.codec)
};
assert!(h264.inline, "an avc3 track carries its parameter sets in band");
assert_eq!(rendition.coded_width, Some(1920));
assert_eq!(rendition.coded_height, Some(1080));
assert_eq!(rendition.framerate, Some(30.0));
assert_eq!(rendition.bitrate, Some(6_000_000));
}
#[tokio::test]
#[cfg(feature = "openh264")]
async fn abort_after_finish() {
let mut broadcast = moq_net::broadcast::Info::new().produce();
let catalog = moq_mux::catalog::Producer::new(&mut broadcast, moq_mux::catalog::Config::default()).unwrap();
let mut config = Config::new(320, 240, crate::Rate::new(30, 1).unwrap());
config.kind = encoder::Kind::Software;
let track = broadcast
.create_track("video", catalog.track_info(hang::catalog::PRIORITY.video))
.unwrap();
let mut subscriber = track.subscribe(None);
let mut producer = Producer::with_track(track, catalog, config.probe().await.unwrap()).unwrap();
let mut encoder = Encoder::new(&config).unwrap();
let rgba = vec![0x80u8; 320 * 240 * 4];
let surface = crate::Surface::rgba(&rgba, crate::Size::new(320, 240)).unwrap();
let frame = Frame::new(surface, Timestamp::from_micros(0).unwrap());
producer.publish(&encoder.encode(&frame).unwrap()).unwrap();
producer.finish().unwrap();
assert!(subscriber.recv_group().await.unwrap().is_some());
producer.abort(moq_net::Error::Cancel);
}
#[tokio::test]
#[cfg(feature = "openh264")]
async fn h264_roundtrip_publishes_avc3() {
let (name, config) = roundtrip_rendition(Codec::H264, encoder::Kind::Software).await;
assert!(name.ends_with(".avc3"));
assert_eq!(config.coded_width, Some(320));
assert_eq!(config.coded_height, Some(240));
}
#[cfg(target_os = "macos")]
#[tokio::test]
async fn h265_roundtrip_publishes_hev1() {
let (name, config) = roundtrip_rendition(Codec::H265, encoder::Kind::Hardware).await;
assert!(name.ends_with(".hev1"));
assert_eq!(config.coded_width, Some(320));
assert_eq!(config.coded_height, Some(240));
}
#[cfg(all(feature = "capture", feature = "openh264"))]
mod clock {
use std::time::{Duration, Instant, SystemTime};
use super::*;
use crate::capture::Synthetic;
const SAMPLING: Duration = Duration::from_millis(250);
const ROUNDING: u64 = 2;
const RETAIN: Duration = Duration::from_secs(600);
struct Opens(tokio::sync::mpsc::UnboundedReceiver<capture::Stream>);
impl CaptureSource for Opens {
async fn open(&mut self, _config: &capture::Config) -> Result<capture::Stream, Error> {
self.0
.recv()
.await
.ok_or_else(|| Error::SourceUnavailable("the fixture stopped opening cameras".to_string()))
}
}
struct Fixture {
epoch: Instant,
clock: moq_mux::Clock,
catalog: moq_mux::catalog::Producer,
consumer: moq_net::broadcast::Consumer,
_broadcast: moq_net::broadcast::Producer,
opens: tokio::sync::mpsc::UnboundedSender<capture::Stream>,
stop: Option<tokio::sync::oneshot::Sender<()>>,
task: tokio::task::JoinHandle<Result<(), Error>>,
}
impl Fixture {
async fn start(behind: Duration, wall: SystemTime) -> Self {
let epoch = Instant::now()
.checked_sub(behind)
.expect("a monotonic clock that far back");
let clock = moq_mux::Clock::at(epoch, wall).unwrap();
let mut broadcast = moq_net::broadcast::Info::new().produce();
let consumer = broadcast.consume();
let config = moq_mux::catalog::Config::default()
.with_clock(clock)
.with_max_age(RETAIN);
let catalog = moq_mux::catalog::Producer::new(&mut broadcast, config).unwrap();
let track = broadcast
.create_track(
"video",
catalog.track_info(hang::catalog::PRIORITY.video).with_max_age(RETAIN),
)
.unwrap();
let mut probe = Config::new(320, 240, crate::Rate::new(30, 1).unwrap());
probe.kind = encoder::Kind::Software;
let mut producer = Producer::with_track(track, catalog.clone(), probe.probe().await.unwrap()).unwrap();
let demand = producer.demand();
let (opens, rx) = tokio::sync::mpsc::unbounded_channel();
let (stop, stopped) = tokio::sync::oneshot::channel::<()>();
let task = tokio::spawn(async move {
let mut source = Opens(rx);
let options = Options {
kind: encoder::Kind::Software,
..Options::default()
};
let config = capture::Config::default();
tokio::select! {
res = capture_loop(&mut producer, &demand, &mut source, &config, &options, &clock) => res?,
_ = stopped => {}
}
producer.finish()
});
Self {
epoch,
clock,
catalog,
consumer,
_broadcast: broadcast,
opens,
stop: Some(stop),
task,
}
}
async fn subscribe(&self) -> moq_mux::container::Consumer<moq_mux::catalog::hang::Container> {
let snapshot = self.catalog.snapshot();
let (name, rendition) = snapshot.video.renditions.iter().next().expect("the probed rendition");
let container = moq_mux::catalog::hang::Container::try_from(rendition).unwrap();
let track = self
.consumer
.track(name)
.unwrap()
.subscribe(moq_net::track::Subscription::default().with_max_age(RETAIN))
.await
.unwrap();
moq_mux::container::Consumer::new(track, container)
}
fn camera(&self) -> Synthetic {
let (camera, stream) = Synthetic::open(crate::Size::new(320, 240), crate::Rate::new(30, 1).unwrap());
self.opens.send(stream).unwrap();
camera
}
fn at(&self, instant: Instant) -> u64 {
u64::try_from(instant.duration_since(self.epoch).as_micros()).unwrap()
}
async fn finish(mut self) -> (moq_mux::catalog::Producer, moq_net::broadcast::Consumer) {
let _ = self.stop.take().expect("finished once").send(());
self.task.await.unwrap().unwrap();
(self.catalog, self.consumer)
}
fn assert_acquired(&self, published: u64, captured: Instant) {
let exact = self.at(captured);
let early = u64::try_from(SAMPLING.as_micros()).unwrap();
assert!(
published + early >= exact && published <= exact + ROUNDING,
"published {published}us, acquired at {exact}us on the broadcast clock"
);
}
}
fn surface() -> crate::frame::Surface {
crate::frame::Surface::I420(crate::frame::I420 {
width: 320,
height: 240,
data: vec![0x80; 320 * 240 * 3 / 2],
color: None,
})
}
fn us(micros: u64) -> Timestamp {
Timestamp::from_micros(micros).unwrap()
}
async fn read(track: &mut moq_mux::container::Consumer<moq_mux::catalog::hang::Container>) -> u64 {
let frame = track.read().await.unwrap().expect("a published frame");
u64::try_from(frame.timestamp.as_micros()).unwrap()
}
async fn read_new(
track: &mut moq_mux::container::Consumer<moq_mux::catalog::hang::Container>,
seen: &[u64],
) -> u64 {
loop {
let timestamp = read(track).await;
if !seen.contains(×tamp) {
return timestamp;
}
}
}
#[tokio::test]
async fn a_late_first_frame_publishes_its_acquisition() {
let fixture = Fixture::start(Duration::from_secs(5), SystemTime::now()).await;
let mut track = fixture.subscribe().await;
let camera = fixture.camera();
let captured = Instant::now();
tokio::time::sleep(Duration::from_millis(50)).await;
camera.push_at(surface(), captured);
let published = read(&mut track).await;
assert!(published >= 4_000_000, "{published}us restarted the broadcast at zero");
fixture.assert_acquired(published, captured);
fixture.finish().await;
}
#[tokio::test]
async fn a_device_clock_restart_continues_forward() {
let fixture = Fixture::start(Duration::from_secs(1), SystemTime::now()).await;
let mut track = fixture.subscribe().await;
let camera = fixture.camera();
camera.push_native(surface(), us(0));
let first = read(&mut track).await;
tokio::time::sleep(Duration::from_millis(40)).await;
camera.push_native(surface(), us(40_000));
let second = read(&mut track).await;
assert_eq!(second - first, 40_000, "the device's spacing survives");
camera.push_native(surface(), us(0));
let restarted = read(&mut track).await;
assert!(restarted >= second, "{restarted}us rewound behind {second}us");
tokio::time::sleep(Duration::from_millis(40)).await;
camera.push_native(surface(), us(40_000));
let resumed = read(&mut track).await;
assert_eq!(resumed - restarted, 40_000, "the device's spacing resumes");
camera.close();
let camera = fixture.camera();
let pushed = Instant::now();
camera.push_native(surface(), us(0));
let reopened = read(&mut track).await;
let arrived = fixture.at(Instant::now());
assert!(reopened >= resumed, "{reopened}us rewound across the reopen");
let early = u64::try_from(SAMPLING.as_micros()).unwrap();
assert!(reopened + early >= fixture.at(pushed) && reopened <= arrived + ROUNDING);
fixture.finish().await;
}
#[tokio::test]
async fn a_restart_after_idle_keeps_the_gap() {
let idle = Duration::from_millis(300);
let fixture = Fixture::start(Duration::from_secs(1), SystemTime::now()).await;
let mut track = fixture.subscribe().await;
let camera = fixture.camera();
let captured = Instant::now();
camera.push_at(surface(), captured);
let before = read(&mut track).await;
fixture.assert_acquired(before, captured);
drop(track);
drop(camera);
tokio::time::sleep(idle).await;
let mut track = fixture.subscribe().await;
let camera = fixture.camera();
let captured = Instant::now();
camera.push_at(surface(), captured);
let after = read_new(&mut track, &[before]).await;
fixture.assert_acquired(after, captured);
assert!(
after - before >= u64::try_from(idle.as_micros()).unwrap(),
"the {idle:?} idle gap collapsed to {}us",
after - before
);
fixture.finish().await;
}
#[tokio::test]
async fn a_system_wall_adjustment_retimes_nothing() {
let now = SystemTime::now().duration_since(SystemTime::UNIX_EPOCH).unwrap();
let wall = SystemTime::UNIX_EPOCH + Duration::from_secs(now.as_secs() - 3600);
let fixture = Fixture::start(Duration::from_secs(1), wall).await;
let advertised = fixture.catalog.snapshot().clock;
assert_eq!(advertised, Some(fixture.clock.wall()));
let mut track = fixture.subscribe().await;
let camera = fixture.camera();
let captured = Instant::now();
camera.push_at(surface(), captured);
let published = read(&mut track).await;
fixture.assert_acquired(published, captured);
let mapped = advertised.unwrap().wall_clock(us(published)).unwrap();
assert_eq!(mapped, wall + Duration::from_millis(published / 1000));
assert_eq!(fixture.catalog.snapshot().clock, advertised);
fixture.finish().await;
}
#[tokio::test]
async fn retained_archive_playback_keeps_the_live_timestamps() {
let fixture = Fixture::start(Duration::from_secs(1), SystemTime::now()).await;
let section = fixture
.catalog
.snapshot()
.archive
.expect("the video track enrolls an archive");
let mut timeline = moq_mux::timeline::Consumer::<()>::subscribe(&fixture.consumer, §ion)
.await
.unwrap();
let mut live = Vec::new();
for _ in 0..2 {
let mut track = fixture.subscribe().await;
let camera = fixture.camera();
let captured = Instant::now();
camera.push_at(surface(), captured);
let published = read_new(&mut track, &live).await;
fixture.assert_acquired(published, captured);
live.push(published);
drop(track);
tokio::time::sleep(moq_mux::timeline::DEFAULT_DURATION_MIN + Duration::from_millis(100)).await;
}
let (catalog, _consumer) = fixture.finish().await;
catalog.timeline().finish().unwrap();
let mut archived = Vec::new();
while let Some(event) = timeline.next().await.unwrap() {
match event {
moq_mux::timeline::Event::Push { entry, .. } => archived.push(entry),
other => panic!("unexpected timeline event {other:?}"),
}
}
assert_eq!(archived.len(), live.len(), "one segment per capture run: {archived:?}");
for (entry, live) in archived.iter().zip(&live) {
assert_eq!(entry.pts.as_micros() / 1000, u128::from(*live / 1000), "{archived:?}");
assert!(entry.tracks.contains_key("video"), "{archived:?}");
}
let first = &archived[0];
assert!(
archived[1].pts.as_micros() >= first.pts.as_micros() + first.duration.as_micros(),
"the resumed segment overlaps the one before it: {archived:?}"
);
}
}
}