pub mod aggregate;
mod consume;
mod produce;
pub use consume::{Consumer, ConsumerConfig, SessionsConsumer, TrafficConsumer};
pub use produce::{Producer, ProducerConfig};
use std::collections::BTreeMap;
pub use moq_net::stats::{Handle, Presence, Registry, Role, Tier, Traffic};
use moq_net::{AsPath, Path, PathOwned};
pub type TrafficFrame = BTreeMap<String, Traffic>;
pub type SessionsFrame = BTreeMap<String, Presence>;
pub const COMPRESSED_SUFFIX: &str = ".z";
pub fn traffic_track(tier: &Tier, role: Role, compressed: bool) -> String {
let mut name = tier.track_name(&format!("{}.json", role.as_str()));
if compressed {
name.push_str(COMPRESSED_SUFFIX);
}
name
}
pub fn sessions_track(tier: &Tier, compressed: bool) -> String {
let mut name = tier.track_name("sessions.json");
if compressed {
name.push_str(COMPRESSED_SUFFIX);
}
name
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub struct NodePath {
pub group: PathOwned,
pub node: PathOwned,
}
pub fn parse_node_path(prefix: impl AsPath, depth: usize, path: impl AsPath) -> Option<NodePath> {
let prefix = prefix.as_path();
let path = path.as_path();
let rest = if prefix.is_empty() {
path.as_str()
} else {
path.as_str().strip_prefix(prefix.as_str())?.strip_prefix('/')?
};
let mut segments = rest.split('/');
let mut group: Vec<&str> = Vec::new();
loop {
let segment = segments.next()?;
if segment == "node" {
break;
}
if group.len() >= depth {
return None;
}
group.push(segment);
}
let node = segments.collect::<Vec<_>>().join("/");
Some(NodePath {
group: Path::new(&group.join("/")).to_owned(),
node: Path::new(&node).to_owned(),
})
}
#[derive(thiserror::Error, Debug, Clone)]
#[non_exhaustive]
pub enum Error {
#[error(transparent)]
Net(#[from] moq_net::Error),
#[error(transparent)]
Json(#[from] moq_json::Error),
}
pub type Result<T> = std::result::Result<T, Error>;
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parse_node_path_variants() {
let parse = |depth, path| parse_node_path(".stats", depth, path);
assert_eq!(
parse(0, ".stats/node/sjc"),
Some(NodePath {
group: Path::empty().to_owned(),
node: Path::new("sjc").to_owned(),
})
);
assert_eq!(
parse(0, ".stats/node/sjc/1").unwrap().node,
Path::new("sjc/1").to_owned(),
"multi-segment node"
);
assert_eq!(
parse(0, ".stats/node"),
Some(NodePath {
group: Path::empty().to_owned(),
node: Path::empty().to_owned(),
}),
"nodeless path"
);
assert_eq!(
parse(1, ".stats/acme/node/sjc"),
Some(NodePath {
group: Path::new("acme").to_owned(),
node: Path::new("sjc").to_owned(),
})
);
assert_eq!(parse(1, ".stats/node/sjc").unwrap().group, Path::empty().to_owned());
assert_eq!(parse(0, "other/node/sjc"), None);
assert_eq!(parse(1, ".stats/acme/vod/sjc"), None, "sibling category filtered");
assert_eq!(parse(0, ".stats/acme/node/sjc"), None, "group deeper than depth");
}
#[test]
fn track_names() {
let default = Tier::default();
let regional = Tier::new("region/sjc");
assert_eq!(traffic_track(&default, Role::Publisher, false), "publisher.json");
assert_eq!(traffic_track(&default, Role::Subscriber, true), "subscriber.json.z");
assert_eq!(
traffic_track(®ional, Role::Publisher, false),
"region/sjc/publisher.json"
);
assert_eq!(sessions_track(&default, false), "sessions.json");
assert_eq!(sessions_track(®ional, true), "region/sjc/sessions.json.z");
}
}