use std::sync::{Arc, Mutex};
use std::time::Instant;
use tokio::sync::Notify;
use crate::Error;
use crate::frame::{Frame, Surface};
use moq_net::Timestamp;
pub(super) struct FrameChannel {
state: Mutex<State>,
notify: Notify,
epoch: Instant,
}
struct State {
frame: Option<Frame>,
#[cfg(any(target_os = "linux", target_os = "windows", test))]
native_anchor: Option<(Timestamp, Timestamp)>,
closed: bool,
error: Option<Error>,
}
impl FrameChannel {
pub(super) fn new() -> Arc<Self> {
Arc::new(Self {
state: Mutex::new(State {
frame: None,
#[cfg(any(target_os = "linux", target_os = "windows", test))]
native_anchor: None,
closed: false,
error: None,
}),
notify: Notify::new(),
epoch: Instant::now(),
})
}
pub(super) fn push(&self, frame: Surface) {
self.push_at(frame, Instant::now());
}
fn push_at(&self, surface: Surface, captured: Instant) {
let micros = captured.saturating_duration_since(self.epoch).as_micros();
let micros = u64::try_from(micros).unwrap_or(u64::MAX);
let frame = Frame::new(surface, Timestamp::from_micros(micros).expect("capture timestamp fits"));
self.publish(frame);
}
#[cfg(any(target_os = "linux", target_os = "windows", test))]
pub(super) fn push_native(&self, surface: Surface, source: Timestamp) {
let local = self.now();
let mut state = self.state.lock().unwrap();
if state.closed {
return;
}
let (source_anchor, local_anchor) = *state.native_anchor.get_or_insert((source, local));
let timestamp = source
.checked_sub(source_anchor)
.and_then(|elapsed| local_anchor.checked_add(elapsed))
.unwrap_or(local);
state.frame = Some(Frame::new(surface, timestamp));
drop(state);
self.notify.notify_one();
}
fn publish(&self, frame: Frame) {
let mut state = self.state.lock().unwrap();
if state.closed {
return;
}
state.frame = Some(frame);
drop(state);
self.notify.notify_one();
}
pub(super) fn close(&self) {
let mut state = self.state.lock().unwrap();
state.closed = true;
drop(state);
self.wake();
}
pub(super) fn fail(&self, error: Error) {
let mut state = self.state.lock().unwrap();
if state.closed {
return;
}
state.frame = None;
state.error = Some(error);
state.closed = true;
drop(state);
self.wake();
}
fn wake(&self) {
self.notify.notify_one();
}
pub(super) async fn recv(&self) -> Result<Option<Frame>, Error> {
loop {
let notified = self.notify.notified();
{
let mut state = self.state.lock().unwrap();
if let Some(error) = state.error.take() {
return Err(error);
}
if let Some(frame) = state.frame.take() {
return Ok(Some(frame));
}
if state.closed {
return Ok(None);
}
}
notified.await;
}
}
pub(super) fn now(&self) -> Timestamp {
let micros = u64::try_from(self.epoch.elapsed().as_micros()).unwrap_or(u64::MAX);
Timestamp::from_micros(micros).expect("capture timestamp fits")
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::frame::I420;
fn frame(id: u32) -> Surface {
Surface::I420(I420 {
width: id,
height: 2,
data: Vec::new(),
color: None,
})
}
#[tokio::test]
async fn recv_returns_frames_in_order() {
let chan = FrameChannel::new();
chan.push(frame(1));
assert_eq!(chan.recv().await.unwrap().unwrap().surface.width(), 1);
chan.push(frame(2));
assert_eq!(chan.recv().await.unwrap().unwrap().surface.width(), 2);
}
#[tokio::test]
async fn slow_consumer_receives_only_the_latest_frame() {
let chan = FrameChannel::new();
for id in 1..=6 {
chan.push(frame(id));
}
assert_eq!(chan.recv().await.unwrap().unwrap().surface.width(), 6);
}
#[tokio::test]
async fn close_returns_none_after_the_pending_frame() {
let chan = FrameChannel::new();
chan.push(frame(1));
chan.close();
assert_eq!(chan.recv().await.unwrap().unwrap().surface.width(), 1);
assert!(chan.recv().await.unwrap().is_none());
}
#[tokio::test]
async fn failure_discards_a_pending_frame_and_surfaces_the_cause() {
let chan = FrameChannel::new();
chan.push(frame(1));
chan.fail(Error::SourceUnavailable("window closed".to_string()));
assert!(matches!(
chan.recv().await,
Err(Error::SourceUnavailable(reason)) if reason == "window closed"
));
assert!(chan.recv().await.unwrap().is_none());
}
#[tokio::test]
async fn closing_retains_a_wakeup_for_a_consumer_that_has_not_parked() {
let chan = FrameChannel::new();
chan.close();
chan.notify.notified().await;
let chan = FrameChannel::new();
chan.fail(Error::SourceUnavailable("stream stopped".to_string()));
chan.notify.notified().await;
}
#[tokio::test]
async fn recv_is_cancel_safe() {
let chan = FrameChannel::new();
tokio::select! {
_ = chan.recv() => panic!("no frame pushed yet"),
_ = std::future::ready(()) => {}
}
chan.push(frame(7));
assert_eq!(chan.recv().await.unwrap().unwrap().surface.width(), 7);
}
#[tokio::test]
async fn timestamp_is_captured_before_queued_delay() {
let chan = FrameChannel::new();
let captured = chan.epoch + std::time::Duration::from_millis(12);
chan.push_at(frame(1), captured);
assert_eq!(chan.recv().await.unwrap().unwrap().timestamp.as_micros(), 12_000);
}
#[tokio::test]
async fn native_timestamps_keep_deltas_without_exposing_the_device_epoch() {
let chan = FrameChannel::new();
let first_source = Timestamp::from_micros(9_000_000).unwrap();
chan.push_native(frame(1), first_source);
let first = chan.recv().await.unwrap().unwrap().timestamp;
chan.push_native(frame(2), Timestamp::from_micros(9_033_367).unwrap());
let second = chan.recv().await.unwrap().unwrap().timestamp;
assert_eq!(second.as_micros() - first.as_micros(), 33_367);
assert!(first.as_micros() < 9_000_000);
}
}