use crate::error::{Error, Result};
use alloc::{
format,
string::{String, ToString},
vec::Vec,
};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[non_exhaustive]
pub enum Scte35Cue {
Out,
In,
Cmd,
}
impl Scte35Cue {
pub fn name(&self) -> &'static str {
match self {
Scte35Cue::Out => "out",
Scte35Cue::In => "in",
Scte35Cue::Cmd => "cmd",
}
}
fn attr_key(&self) -> &'static str {
match self {
Scte35Cue::Out => "SCTE35-OUT",
Scte35Cue::In => "SCTE35-IN",
Scte35Cue::Cmd => "SCTE35-CMD",
}
}
}
dvb_common::impl_spec_display!(Scte35Cue);
#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct Scte35Attr {
pub cue: Scte35Cue,
pub raw: Vec<u8>,
}
#[derive(Debug, Clone, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct DateRange {
pub id: String,
pub start_date: String,
pub class: Option<String>,
pub duration: Option<f64>,
pub planned_duration: Option<f64>,
pub scte35: Option<Scte35Attr>,
}
const TAG: &str = "#EXT-X-DATERANGE:";
impl DateRange {
pub fn to_tag_line(&self) -> String {
let mut out = String::from(TAG);
out.push_str(&format!("ID=\"{}\"", self.id));
out.push_str(&format!(",START-DATE=\"{}\"", self.start_date));
if let Some(c) = &self.class {
out.push_str(&format!(",CLASS=\"{}\"", c));
}
if let Some(d) = self.duration {
out.push_str(&format!(",DURATION={}", fmt_f64(d)));
}
if let Some(d) = self.planned_duration {
out.push_str(&format!(",PLANNED-DURATION={}", fmt_f64(d)));
}
if let Some(s) = &self.scte35 {
out.push_str(&format!(",{}=0x{}", s.cue.attr_key(), to_hex_upper(&s.raw)));
}
out
}
pub fn parse_tag_line(s: &str) -> Result<DateRange> {
let body = s
.strip_prefix(TAG)
.ok_or_else(|| Error::AttrParse("missing #EXT-X-DATERANGE: prefix".to_string()))?;
let mut dr = DateRange {
id: String::new(),
start_date: String::new(),
class: None,
duration: None,
planned_duration: None,
scte35: None,
};
let mut seen_id = false;
for (k, v) in split_attrs(body) {
match k {
"ID" => {
dr.id = unquote(v);
seen_id = true;
}
"START-DATE" => dr.start_date = unquote(v),
"CLASS" => dr.class = Some(unquote(v)),
"DURATION" => dr.duration = Some(parse_f64(v)?),
"PLANNED-DURATION" => dr.planned_duration = Some(parse_f64(v)?),
"SCTE35-OUT" => {
dr.scte35 = Some(Scte35Attr {
cue: Scte35Cue::Out,
raw: parse_hex(v)?,
})
}
"SCTE35-IN" => {
dr.scte35 = Some(Scte35Attr {
cue: Scte35Cue::In,
raw: parse_hex(v)?,
})
}
"SCTE35-CMD" => {
dr.scte35 = Some(Scte35Attr {
cue: Scte35Cue::Cmd,
raw: parse_hex(v)?,
})
}
_ => {} }
}
if !seen_id {
return Err(Error::AttrParse("DATERANGE missing ID".to_string()));
}
Ok(dr)
}
}
fn fmt_f64(v: f64) -> String {
let trunc = v as i64;
if v == trunc as f64 {
format!("{}", trunc)
} else {
format!("{}", v)
}
}
fn to_hex_upper(b: &[u8]) -> String {
let mut s = String::with_capacity(b.len() * 2);
for byte in b {
s.push_str(&format!("{:02X}", byte));
}
s
}
fn unquote(v: &str) -> String {
v.trim_matches('"').to_string()
}
fn parse_f64(v: &str) -> Result<f64> {
v.parse::<f64>()
.map_err(|_| Error::AttrParse(format!("bad number: {v}")))
}
fn parse_hex(v: &str) -> Result<Vec<u8>> {
let h = v
.strip_prefix("0x")
.or_else(|| v.strip_prefix("0X"))
.unwrap_or(v);
if h.len() % 2 != 0 {
return Err(Error::AttrParse("odd-length hex".to_string()));
}
(0..h.len())
.step_by(2)
.map(|i| {
u8::from_str_radix(&h[i..i + 2], 16)
.map_err(|_| Error::AttrParse("bad hex".to_string()))
})
.collect()
}
fn split_attrs(body: &str) -> Vec<(&str, &str)> {
let mut pairs = Vec::new();
let bytes = body.as_bytes();
let (mut start, mut in_q) = (0usize, false);
let mut i = 0;
while i <= bytes.len() {
let at_end = i == bytes.len();
let c = if at_end { b',' } else { bytes[i] };
match c {
b'"' => in_q = !in_q,
b',' if !in_q => {
let field = &body[start..i];
if let Some(eq) = field.find('=') {
pairs.push((&field[..eq], &field[eq + 1..]));
}
start = i + 1;
}
_ => {}
}
i += 1;
}
pairs
}
#[cfg(test)]
mod tests {
use super::*;
use alloc::{string::ToString, vec};
fn sample() -> DateRange {
DateRange {
id: "2002".to_string(),
start_date: "2018-10-29T10:38:00.000Z".to_string(),
class: None,
duration: None,
planned_duration: Some(24.0),
scte35: Some(Scte35Attr {
cue: Scte35Cue::Out,
raw: vec![0xFC, 0x30, 0x21],
}),
}
}
#[test]
fn tag_round_trips_byte_identical() {
let dr = sample();
let line = dr.to_tag_line();
assert!(line.starts_with("#EXT-X-DATERANGE:"));
assert!(line.contains("SCTE35-OUT=0xFC3021"));
let back = DateRange::parse_tag_line(&line).unwrap();
assert_eq!(back, dr);
}
#[test]
fn cue_labels() {
assert_eq!(Scte35Cue::Out.name(), "out");
assert_eq!(alloc::format!("{}", Scte35Cue::In), "in");
}
}