use std::collections::BTreeMap;
use std::sync::Arc;
use std::sync::atomic::{AtomicBool, Ordering};
use std::task::{Poll, ready};
use crate::catalog::Resolved;
pub struct Update {
pub rendition: Rendition,
pub encoding: bool,
}
#[derive(Clone)]
pub struct Rendition(Arc<Meter>);
impl Rendition {
fn new(rung: &Resolved) -> Self {
Self(Arc::new(Meter {
rung: rung.clone(),
counts: kio::Lock::new(Counts::default()),
}))
}
pub fn name(&self) -> &str {
&self.0.rung.name
}
pub fn size(&self) -> moq_video::Size {
self.0.rung.size
}
pub fn bitrate(&self) -> moq_net::bandwidth::Rate {
self.0.rung.bitrate
}
pub fn framerate(&self) -> Option<moq_video::Rate> {
self.0.rung.framerate
}
pub fn frames(&self) -> u64 {
self.0.counts.lock().frames
}
pub fn bytes(&self) -> u64 {
self.0.counts.lock().bytes
}
fn produced(&self, frames: u64, bytes: u64) {
let mut counts = self.0.counts.lock();
counts.frames += frames;
counts.bytes += bytes;
}
}
impl std::fmt::Debug for Rendition {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Rendition")
.field("name", &self.name())
.field("frames", &self.frames())
.finish()
}
}
struct Meter {
rung: Resolved,
counts: kio::Lock<Counts>,
}
#[derive(Default)]
struct Counts {
frames: u64,
bytes: u64,
}
struct Entry {
rendition: Rendition,
refs: usize,
}
#[derive(Default)]
struct State {
ladder: BTreeMap<String, Entry>,
}
#[derive(Clone, Default)]
pub(crate) struct Producer {
state: kio::Producer<State>,
}
impl Producer {
pub(crate) fn consume(&self) -> Consumer {
Consumer {
state: self.state.consume(),
seen: BTreeMap::new(),
}
}
pub(crate) fn declare<'a>(&self, rungs: impl IntoIterator<Item = &'a Resolved>) {
let Ok(mut state) = self.state.write() else { return };
for rung in rungs {
state.entry(rung);
}
}
pub(crate) fn attach(&self, rung: &Resolved) -> Guard {
let rendition = match self.state.write() {
Ok(mut state) => state.entry(rung).rendition.clone(),
Err(_) => Rendition::new(rung),
};
Guard {
state: self.state.clone(),
rendition,
producing: AtomicBool::new(false),
}
}
}
impl State {
fn entry(&mut self, rung: &Resolved) -> &mut Entry {
self.ladder.entry(rung.name.clone()).or_insert_with(|| Entry {
rendition: Rendition::new(rung),
refs: 0,
})
}
fn count(&mut self, name: &str, delta: isize) {
let Some(entry) = self.ladder.get_mut(name) else { return };
entry.refs = entry.refs.saturating_add_signed(delta);
}
}
pub(crate) struct Guard {
state: kio::Producer<State>,
rendition: Rendition,
producing: AtomicBool,
}
impl Guard {
pub(crate) fn produced(&self, frames: u64, bytes: u64) {
if frames == 0 {
return;
}
self.rendition.produced(frames, bytes);
if self.producing.swap(true, Ordering::Relaxed) {
return;
}
if let Ok(mut state) = self.state.write() {
state.count(self.rendition.name(), 1);
}
}
}
impl Drop for Guard {
fn drop(&mut self) {
if !self.producing.load(Ordering::Relaxed) {
return;
}
if let Ok(mut state) = self.state.write() {
state.count(self.rendition.name(), -1);
}
}
}
pub struct Consumer {
state: kio::Consumer<State>,
seen: BTreeMap<String, bool>,
}
impl Consumer {
pub async fn next(&mut self) -> Option<Update> {
kio::wait(|waiter| self.poll_next(waiter)).await
}
pub fn poll_next(&mut self, waiter: &kio::Waiter) -> Poll<Option<Update>> {
let update = {
let seen = &self.seen;
match ready!(self.state.poll(waiter, |state| match next_update(state, seen) {
Some(update) => Poll::Ready(update),
None => Poll::Pending,
})) {
Ok(update) => update,
Err(_) => return Poll::Ready(None),
}
};
Poll::Ready(Some(self.advance(update)))
}
pub fn try_next(&mut self) -> Option<Update> {
let update = {
let seen = &self.seen;
next_update(&self.state.read(), seen)?
};
Some(self.advance(update))
}
pub fn is_closed(&self) -> bool {
self.state.is_closed()
}
fn advance(&mut self, update: Update) -> Update {
self.seen.insert(update.rendition.name().to_string(), update.encoding);
update
}
}
fn next_update(state: &State, seen: &BTreeMap<String, bool>) -> Option<Update> {
state.ladder.iter().find_map(|(name, entry)| {
let encoding = entry.refs > 0;
if seen.get(name) == Some(&encoding) {
return None;
}
Some(Update {
rendition: entry.rendition.clone(),
encoding,
})
})
}
#[cfg(test)]
mod tests {
use std::time::Duration;
use super::*;
fn resolved(name: &str, height: u32) -> Resolved {
Resolved {
name: name.to_string(),
height,
size: moq_video::Size::new(height * 16 / 9, height),
bitrate: moq_net::bandwidth::Rate::from_bps(100_000),
framerate: Some(moq_video::Rate::new(30, 1).unwrap()),
}
}
#[tokio::test]
async fn reports_the_ladder_then_each_edge() {
let active = Producer::default();
let rung = resolved("video/360p", 360);
let mut cursor = active.consume();
assert!(cursor.try_next().is_none());
active.declare(std::slice::from_ref(&rung));
let update = cursor.next().await.unwrap();
assert_eq!(update.rendition.name(), "video/360p");
assert_eq!(update.rendition.size().height, 360);
assert!(!update.encoding);
assert!(cursor.try_next().is_none());
let guard = active.attach(&rung);
guard.produced(1, 1_000);
assert!(cursor.next().await.unwrap().encoding);
assert!(cursor.try_next().is_none());
drop(guard);
assert!(!cursor.next().await.unwrap().encoding);
}
#[tokio::test]
async fn attaching_without_producing_is_not_encoding() {
let active = Producer::default();
let rung = resolved("video/360p", 360);
active.declare(std::slice::from_ref(&rung));
let mut cursor = active.consume();
let rendition = cursor.next().await.unwrap().rendition;
let guard = active.attach(&rung);
tokio::time::sleep(Duration::from_millis(20)).await;
assert!(cursor.try_next().is_none(), "attaching reported an edge");
assert_eq!(rendition.frames(), 0);
guard.produced(1, 1_000);
assert!(cursor.next().await.unwrap().encoding);
assert_eq!(rendition.frames(), 1);
drop(guard);
assert!(!cursor.next().await.unwrap().encoding);
}
#[tokio::test]
async fn concurrent_pipelines_are_one_rendition() {
let active = Producer::default();
let low = resolved("video/240p", 240);
let high = resolved("video/360p", 360);
let mut cursor = active.consume();
let live = active.attach(&high);
live.produced(2, 2_000);
let rendition = cursor.next().await.unwrap().rendition;
let fetch = active.attach(&high);
fetch.produced(1, 500);
let other = active.attach(&low);
other.produced(1, 400);
let update = cursor.next().await.unwrap();
assert_eq!(update.rendition.name(), "video/240p");
assert!(update.encoding);
assert!(cursor.try_next().is_none());
drop(fetch);
assert!(cursor.try_next().is_none());
drop(live);
let update = cursor.next().await.unwrap();
assert_eq!(update.rendition.name(), "video/360p");
assert!(!update.encoding);
drop(other);
assert!(!cursor.next().await.unwrap().encoding);
assert_eq!(rendition.frames(), 3);
assert_eq!(rendition.bytes(), 2_500);
}
#[tokio::test]
async fn a_fresh_cursor_reports_the_current_set() {
let active = Producer::default();
let rung = resolved("video/480p", 480);
let guard = active.attach(&rung);
guard.produced(1, 1_000);
let mut cursor = active.consume();
let update = cursor.next().await.unwrap();
assert_eq!(update.rendition.name(), "video/480p");
assert!(update.encoding);
assert!(cursor.try_next().is_none());
assert!(
tokio::time::timeout(Duration::from_millis(50), cursor.next())
.await
.is_err()
);
}
#[tokio::test]
async fn a_transient_pipeline_is_metered_without_an_edge() {
let active = Producer::default();
let rung = resolved("video/360p", 360);
active.declare(std::slice::from_ref(&rung));
let mut cursor = active.consume();
let rendition = cursor.next().await.unwrap().rendition;
assert_eq!(rendition.frames(), 0);
let guard = active.attach(&rung);
guard.produced(30, 30_000);
drop(guard);
assert!(cursor.try_next().is_none());
assert_eq!(rendition.frames(), 30);
assert_eq!(rendition.bytes(), 30_000);
}
#[tokio::test]
async fn the_counters_survive_an_idle_gap() {
let active = Producer::default();
let rung = resolved("video/360p", 360);
let mut cursor = active.consume();
let guard = active.attach(&rung);
guard.produced(10, 10_000);
let rendition = cursor.next().await.unwrap().rendition;
drop(guard);
cursor.next().await;
assert_eq!(rendition.frames(), 10, "the totals reset when the rendition went idle");
let guard = active.attach(&rung);
guard.produced(5, 5_000);
assert!(cursor.next().await.unwrap().encoding);
assert_eq!(rendition.frames(), 15);
assert_eq!(rendition.bytes(), 15_000);
drop(guard);
}
#[tokio::test]
async fn the_cursor_closes_with_the_producer() {
let active = Producer::default();
let mut cursor = active.consume();
assert!(!cursor.is_closed());
drop(active);
assert!(cursor.is_closed());
assert!(cursor.next().await.is_none());
}
}