use crate::error::Result;
use alloc::string::String;
use alloc::vec::Vec;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[non_exhaustive]
pub enum SnapMode {
Out,
In,
}
impl SnapMode {
pub fn name(&self) -> &'static str {
match self {
SnapMode::Out => "OUT",
SnapMode::In => "IN",
}
}
}
broadcast_common::impl_spec_display!(SnapMode);
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[non_exhaustive]
pub enum RestrictMode {
Skip,
Jump,
}
impl RestrictMode {
pub fn name(&self) -> &'static str {
match self {
RestrictMode::Skip => "SKIP",
RestrictMode::Jump => "JUMP",
}
}
}
broadcast_common::impl_spec_display!(RestrictMode);
#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[non_exhaustive]
pub enum AssetSource {
Uri(String),
List(String),
}
#[derive(Debug, Clone, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct AdBreakDecision {
pub id: String,
pub asset: AssetSource,
pub resume_offset: Option<f64>,
pub playout_limit: Option<f64>,
pub snap: Vec<SnapMode>,
pub restrict: Vec<RestrictMode>,
}
impl AdBreakDecision {
pub fn single_asset(id: impl Into<String>, uri: impl Into<String>) -> Self {
AdBreakDecision {
id: id.into(),
asset: AssetSource::Uri(uri.into()),
resume_offset: None,
playout_limit: None,
snap: Vec::new(),
restrict: Vec::new(),
}
}
pub fn asset_list(id: impl Into<String>, list_uri: impl Into<String>) -> Self {
AdBreakDecision {
id: id.into(),
asset: AssetSource::List(list_uri.into()),
resume_offset: None,
playout_limit: None,
snap: Vec::new(),
restrict: Vec::new(),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct BreakContext {
pub session_id: String,
pub splice_event_id: Option<u32>,
pub out_of_network: bool,
pub break_duration_ticks: Option<u64>,
pub splice_point_pts: u64,
}
pub trait AdDecisionProvider {
fn decide(&self, ctx: &BreakContext) -> Result<AdBreakDecision>;
}
#[cfg(test)]
mod tests {
use super::*;
struct AlwaysSameAd;
impl AdDecisionProvider for AlwaysSameAd {
fn decide(&self, ctx: &BreakContext) -> Result<AdBreakDecision> {
Ok(AdBreakDecision::single_asset(
alloc::format!("break-{}", ctx.splice_event_id.unwrap_or(0)),
"https://ads.example.com/creative.m3u8",
))
}
}
#[test]
fn provider_trait_is_object_safe_and_callable() {
let provider: &dyn AdDecisionProvider = &AlwaysSameAd;
let ctx = BreakContext {
session_id: "s1".into(),
splice_event_id: Some(7),
out_of_network: true,
break_duration_ticks: Some(1_800_000),
splice_point_pts: 12_345,
};
let decision = provider.decide(&ctx).unwrap();
assert_eq!(decision.id, "break-7");
assert_eq!(
decision.asset,
AssetSource::Uri("https://ads.example.com/creative.m3u8".into())
);
}
#[test]
fn single_asset_and_asset_list_builders() {
let single = AdBreakDecision::single_asset("a", "https://x/a.m3u8");
assert_eq!(single.asset, AssetSource::Uri("https://x/a.m3u8".into()));
let list = AdBreakDecision::asset_list("b", "https://x/list.json");
assert_eq!(list.asset, AssetSource::List("https://x/list.json".into()));
}
#[test]
fn label_convention() {
assert_eq!(SnapMode::Out.name(), "OUT");
assert_eq!(alloc::format!("{}", SnapMode::In), "IN");
assert_eq!(RestrictMode::Skip.name(), "SKIP");
assert_eq!(alloc::format!("{}", RestrictMode::Jump), "JUMP");
}
}