#![allow(dead_code)]
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub enum DeliveryDestination {
Broadcast,
Streaming,
Cinema,
Social,
}
impl DeliveryDestination {
#[must_use]
pub fn name(&self) -> &'static str {
match self {
Self::Broadcast => "Broadcast",
Self::Streaming => "Streaming",
Self::Cinema => "Cinema",
Self::Social => "Social Media",
}
}
#[must_use]
pub fn max_bitrate_mbps(&self) -> f64 {
match self {
Self::Broadcast => 50.0,
Self::Streaming => 25.0,
Self::Cinema => 250.0,
Self::Social => 8.0,
}
}
#[must_use]
pub fn loudness_target_lufs(&self) -> f64 {
match self {
Self::Broadcast => -24.0,
Self::Streaming => -23.0,
Self::Cinema => -26.0,
Self::Social => -14.0,
}
}
}
#[derive(Clone, Debug)]
pub struct DeliveryRoute {
pub asset_id: String,
pub destination: DeliveryDestination,
pub variant: Option<String>,
pub assigned_bitrate_mbps: f64,
}
impl DeliveryRoute {
pub fn new(
asset_id: impl Into<String>,
destination: DeliveryDestination,
assigned_bitrate_mbps: f64,
) -> Self {
Self {
asset_id: asset_id.into(),
destination,
variant: None,
assigned_bitrate_mbps,
}
}
pub fn with_variant(mut self, variant: impl Into<String>) -> Self {
self.variant = Some(variant.into());
self
}
#[must_use]
pub fn bitrate_is_compliant(&self) -> bool {
self.assigned_bitrate_mbps <= self.destination.max_bitrate_mbps()
}
}
#[derive(Clone, Debug, Default)]
pub struct DeliveryMap {
routes: Vec<DeliveryRoute>,
}
impl DeliveryMap {
#[must_use]
pub fn new() -> Self {
Self { routes: Vec::new() }
}
pub fn add_route(&mut self, route: DeliveryRoute) {
self.routes.push(route);
}
#[must_use]
pub fn find_for_destination(&self, dest: DeliveryDestination) -> Vec<&DeliveryRoute> {
self.routes
.iter()
.filter(|r| r.destination == dest)
.collect()
}
#[must_use]
pub fn find_for_asset(&self, asset_id: &str) -> Vec<&DeliveryRoute> {
self.routes
.iter()
.filter(|r| r.asset_id == asset_id)
.collect()
}
#[must_use]
pub fn route_count(&self) -> usize {
self.routes.len()
}
#[must_use]
pub fn routes(&self) -> &[DeliveryRoute] {
&self.routes
}
}
#[derive(Clone, Debug, Default)]
pub struct DeliveryMapValidator;
impl DeliveryMapValidator {
#[must_use]
pub fn new() -> Self {
Self
}
#[must_use]
pub fn validate(&self, map: &DeliveryMap) -> Vec<String> {
let mut errors = Vec::new();
for route in map.routes() {
if !route.bitrate_is_compliant() {
errors.push(format!(
"Asset '{}' -> {}: bitrate {:.1} Mbps exceeds {} limit {:.1} Mbps",
route.asset_id,
route.destination.name(),
route.assigned_bitrate_mbps,
route.destination.name(),
route.destination.max_bitrate_mbps(),
));
}
}
errors
}
#[must_use]
pub fn is_valid(&self, map: &DeliveryMap) -> bool {
self.validate(map).is_empty()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_destination_name_broadcast() {
assert_eq!(DeliveryDestination::Broadcast.name(), "Broadcast");
}
#[test]
fn test_destination_name_streaming() {
assert_eq!(DeliveryDestination::Streaming.name(), "Streaming");
}
#[test]
fn test_destination_name_cinema() {
assert_eq!(DeliveryDestination::Cinema.name(), "Cinema");
}
#[test]
fn test_destination_name_social() {
assert_eq!(DeliveryDestination::Social.name(), "Social Media");
}
#[test]
fn test_max_bitrate_broadcast() {
assert_eq!(DeliveryDestination::Broadcast.max_bitrate_mbps(), 50.0);
}
#[test]
fn test_max_bitrate_cinema_highest() {
assert!(DeliveryDestination::Cinema.max_bitrate_mbps() > 100.0);
}
#[test]
fn test_max_bitrate_social_lowest() {
assert!(
DeliveryDestination::Social.max_bitrate_mbps()
< DeliveryDestination::Streaming.max_bitrate_mbps()
);
}
#[test]
fn test_loudness_target_social_highest() {
assert!(
DeliveryDestination::Social.loudness_target_lufs()
> DeliveryDestination::Cinema.loudness_target_lufs()
);
}
#[test]
fn test_route_bitrate_compliant() {
let route = DeliveryRoute::new("asset-001", DeliveryDestination::Broadcast, 40.0);
assert!(route.bitrate_is_compliant());
}
#[test]
fn test_route_bitrate_non_compliant() {
let route = DeliveryRoute::new("asset-001", DeliveryDestination::Social, 15.0);
assert!(!route.bitrate_is_compliant());
}
#[test]
fn test_route_with_variant() {
let route =
DeliveryRoute::new("a", DeliveryDestination::Streaming, 10.0).with_variant("HDR10");
assert_eq!(route.variant.as_deref(), Some("HDR10"));
}
#[test]
fn test_delivery_map_add_and_count() {
let mut map = DeliveryMap::new();
map.add_route(DeliveryRoute::new(
"x",
DeliveryDestination::Broadcast,
20.0,
));
map.add_route(DeliveryRoute::new("y", DeliveryDestination::Streaming, 5.0));
assert_eq!(map.route_count(), 2);
}
#[test]
fn test_find_for_destination() {
let mut map = DeliveryMap::new();
map.add_route(DeliveryRoute::new(
"a",
DeliveryDestination::Broadcast,
20.0,
));
map.add_route(DeliveryRoute::new("b", DeliveryDestination::Social, 5.0));
map.add_route(DeliveryRoute::new(
"c",
DeliveryDestination::Broadcast,
15.0,
));
let bc = map.find_for_destination(DeliveryDestination::Broadcast);
assert_eq!(bc.len(), 2);
}
#[test]
fn test_find_for_asset() {
let mut map = DeliveryMap::new();
map.add_route(DeliveryRoute::new(
"clip-42",
DeliveryDestination::Broadcast,
20.0,
));
map.add_route(DeliveryRoute::new(
"clip-42",
DeliveryDestination::Social,
5.0,
));
map.add_route(DeliveryRoute::new(
"clip-99",
DeliveryDestination::Streaming,
10.0,
));
let found = map.find_for_asset("clip-42");
assert_eq!(found.len(), 2);
}
#[test]
fn test_validator_all_compliant() {
let mut map = DeliveryMap::new();
map.add_route(DeliveryRoute::new(
"a",
DeliveryDestination::Broadcast,
30.0,
));
map.add_route(DeliveryRoute::new(
"b",
DeliveryDestination::Streaming,
10.0,
));
let v = DeliveryMapValidator::new();
assert!(v.is_valid(&map));
assert!(v.validate(&map).is_empty());
}
#[test]
fn test_validator_non_compliant_route() {
let mut map = DeliveryMap::new();
map.add_route(DeliveryRoute::new("a", DeliveryDestination::Social, 20.0)); let v = DeliveryMapValidator::new();
assert!(!v.is_valid(&map));
let errors = v.validate(&map);
assert_eq!(errors.len(), 1);
assert!(errors[0].contains("asset 'a'") || errors[0].contains("Asset 'a'"));
}
}