#![allow(dead_code)]
#![allow(clippy::doc_markdown)]
#![allow(clippy::similar_names)]
#![allow(clippy::unreadable_literal)]
#![allow(clippy::cast_possible_truncation)]
#![allow(clippy::cast_precision_loss)]
#![allow(clippy::cast_lossless)]
#![allow(clippy::cast_sign_loss)]
#![allow(clippy::match_same_arms)]
#![allow(clippy::many_single_char_names)]
#![allow(clippy::unnecessary_wraps)]
#![allow(clippy::range_plus_one)]
#![allow(clippy::needless_pass_by_value)]
#![allow(clippy::manual_div_ceil)]
#![allow(clippy::comparison_chain)]
#![allow(clippy::unused_self)]
#![allow(clippy::trivially_copy_pass_by_ref)]
#![allow(clippy::missing_errors_doc)]
#![allow(clippy::too_many_arguments)]
#![allow(clippy::struct_excessive_bools)]
#![allow(clippy::needless_range_loop)]
#![allow(clippy::redundant_closure_for_method_calls)]
#![allow(clippy::must_use_candidate)]
#![allow(clippy::should_implement_trait)]
#![allow(clippy::items_after_statements)]
#![allow(clippy::if_not_else)]
#![allow(clippy::format_push_string)]
#![allow(clippy::single_match_else)]
#![allow(clippy::redundant_slicing)]
#![allow(clippy::uninlined_format_args)]
#![allow(clippy::map_unwrap_or)]
#![allow(clippy::derivable_impls)]
#![allow(clippy::assigning_clones)]
#![allow(clippy::if_same_then_else)]
#![allow(clippy::format_collect)]
#![allow(clippy::useless_conversion)]
#![allow(clippy::unused_async)]
#![allow(clippy::identity_op)]
use crate::error::{NetError, NetResult};
use std::time::Duration;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum MpdType {
#[default]
Static,
Dynamic,
}
impl MpdType {
#[must_use]
pub fn from_str(s: &str) -> Self {
match s.to_lowercase().as_str() {
"dynamic" => Self::Dynamic,
_ => Self::Static,
}
}
#[must_use]
pub const fn is_live(&self) -> bool {
matches!(self, Self::Dynamic)
}
}
#[derive(Debug, Clone, Default)]
pub struct UrlType {
pub source_url: Option<String>,
pub range: Option<(u64, u64)>,
}
impl UrlType {
#[must_use]
pub fn new(source_url: impl Into<String>) -> Self {
Self {
source_url: Some(source_url.into()),
range: None,
}
}
#[must_use]
pub const fn with_range(mut self, start: u64, end: u64) -> Self {
self.range = Some((start, end));
self
}
}
#[derive(Debug, Clone, Default)]
pub struct ProgramInformation {
pub lang: Option<String>,
pub more_info_url: Option<String>,
pub title: Option<String>,
pub source: Option<String>,
pub copyright: Option<String>,
}
#[derive(Debug, Clone)]
pub struct Descriptor {
pub scheme_id_uri: String,
pub value: Option<String>,
pub id: Option<String>,
}
impl Descriptor {
#[must_use]
pub fn new(scheme_id_uri: impl Into<String>) -> Self {
Self {
scheme_id_uri: scheme_id_uri.into(),
value: None,
id: None,
}
}
}
#[derive(Debug, Clone)]
pub struct ContentProtection {
pub scheme_id_uri: String,
pub value: Option<String>,
pub default_kid: Option<String>,
pub pssh: Option<String>,
}
impl ContentProtection {
#[must_use]
pub fn new(scheme_id_uri: impl Into<String>) -> Self {
Self {
scheme_id_uri: scheme_id_uri.into(),
value: None,
default_kid: None,
pssh: None,
}
}
#[must_use]
pub fn is_widevine(&self) -> bool {
self.scheme_id_uri
.contains("edef8ba9-79d6-4ace-a3c8-27dcd51d21ed")
}
#[must_use]
pub fn is_playready(&self) -> bool {
self.scheme_id_uri
.contains("9a04f079-9840-4286-ab92-e65be0885f95")
}
}
#[derive(Debug, Clone, Copy)]
pub struct SegmentTimelineEntry {
pub start: Option<u64>,
pub duration: u64,
pub repeat: i32,
}
impl SegmentTimelineEntry {
#[must_use]
pub const fn new(duration: u64) -> Self {
Self {
start: None,
duration,
repeat: 0,
}
}
#[must_use]
pub const fn with_start(mut self, start: u64) -> Self {
self.start = Some(start);
self
}
#[must_use]
pub const fn with_repeat(mut self, repeat: i32) -> Self {
self.repeat = repeat;
self
}
#[must_use]
pub const fn segment_count(&self) -> u32 {
if self.repeat < 0 {
u32::MAX
} else {
(self.repeat + 1) as u32
}
}
}
#[derive(Debug, Clone, Default)]
pub struct SegmentTimeline {
pub entries: Vec<SegmentTimelineEntry>,
}
impl SegmentTimeline {
#[must_use]
pub fn new() -> Self {
Self {
entries: Vec::new(),
}
}
pub fn add_entry(&mut self, entry: SegmentTimelineEntry) {
self.entries.push(entry);
}
#[must_use]
pub fn total_duration(&self) -> u64 {
let mut total = 0u64;
for entry in &self.entries {
let count = if entry.repeat < 0 {
1
} else {
(entry.repeat + 1) as u64
};
total += entry.duration * count;
}
total
}
pub fn iter_segments(&self) -> impl Iterator<Item = (u64, u64)> + '_ {
SegmentTimelineIterator {
entries: &self.entries,
entry_idx: 0,
repeat_idx: 0,
current_time: 0,
}
}
}
struct SegmentTimelineIterator<'a> {
entries: &'a [SegmentTimelineEntry],
entry_idx: usize,
repeat_idx: i32,
current_time: u64,
}
impl Iterator for SegmentTimelineIterator<'_> {
type Item = (u64, u64);
fn next(&mut self) -> Option<Self::Item> {
while self.entry_idx < self.entries.len() {
let entry = &self.entries[self.entry_idx];
if self.repeat_idx == 0 {
if let Some(start) = entry.start {
self.current_time = start;
}
}
if entry.repeat < 0 || self.repeat_idx <= entry.repeat {
let start = self.current_time;
let duration = entry.duration;
self.current_time += duration;
self.repeat_idx += 1;
if entry.repeat >= 0 && self.repeat_idx > entry.repeat {
self.entry_idx += 1;
self.repeat_idx = 0;
}
return Some((start, duration));
}
self.entry_idx += 1;
self.repeat_idx = 0;
}
None
}
}
#[derive(Debug, Clone, Default)]
pub struct SegmentBase {
pub timescale: Option<u32>,
pub presentation_time_offset: Option<u64>,
pub index_range: Option<(u64, u64)>,
pub initialization: Option<UrlType>,
pub representation_index: Option<UrlType>,
}
#[derive(Debug, Clone, Default)]
pub struct SegmentList {
pub timescale: Option<u32>,
pub duration: Option<u64>,
pub start_number: Option<u64>,
pub initialization: Option<UrlType>,
pub segment_urls: Vec<UrlType>,
}
#[derive(Debug, Clone, Default)]
pub struct SegmentTemplate {
pub timescale: u32,
pub duration: Option<u64>,
pub start_number: u64,
pub presentation_time_offset: Option<u64>,
pub media: Option<String>,
pub initialization: Option<String>,
pub segment_timeline: Option<SegmentTimeline>,
}
impl SegmentTemplate {
#[must_use]
pub fn new(timescale: u32) -> Self {
Self {
timescale,
start_number: 1,
..Default::default()
}
}
#[must_use]
pub fn with_media(mut self, media: impl Into<String>) -> Self {
self.media = Some(media.into());
self
}
#[must_use]
pub fn with_initialization(mut self, init: impl Into<String>) -> Self {
self.initialization = Some(init.into());
self
}
#[must_use]
pub fn media_url(
&self,
representation_id: &str,
number: u64,
time: Option<u64>,
) -> Option<String> {
let template = self.media.as_ref()?;
let url = substitute_template(template, representation_id, number, time, self.timescale);
Some(url)
}
#[must_use]
pub fn initialization_url(&self, representation_id: &str) -> Option<String> {
let template = self.initialization.as_ref()?;
let url = substitute_template(template, representation_id, 0, None, self.timescale);
Some(url)
}
#[must_use]
pub fn segment_duration_secs(&self) -> Option<f64> {
self.duration.map(|d| d as f64 / self.timescale as f64)
}
}
fn substitute_template(
template: &str,
representation_id: &str,
number: u64,
time: Option<u64>,
bandwidth: u32,
) -> String {
let mut result = template.to_string();
result = result.replace("$RepresentationID$", representation_id);
result = result.replace("$Number$", &number.to_string());
result = result.replace("$Bandwidth$", &bandwidth.to_string());
if let Some(t) = time {
result = result.replace("$Time$", &t.to_string());
}
result = substitute_with_format(&result, "Number", number);
result = substitute_with_format(&result, "Time", time.unwrap_or(0));
result
}
fn substitute_with_format(s: &str, var: &str, value: u64) -> String {
let pattern = format!("${var}%");
let mut result = s.to_string();
let mut search_start = 0;
while let Some(start) = result[search_start..].find(&pattern) {
let abs_start = search_start + start;
if let Some(end) = result[abs_start..].find("d$") {
let format_spec = &result[abs_start + pattern.len()..abs_start + end + 1];
let width: usize = format_spec.trim_start_matches('0').parse().unwrap_or(0);
let pad_char = if format_spec.starts_with('0') {
'0'
} else {
' '
};
let formatted = if pad_char == '0' && width > 0 {
format!("{value:0>width$}")
} else {
format!("{value:>width$}")
};
let full_pattern = format!("${var}%{format_spec}d$");
result = result.replace(&full_pattern, &formatted);
} else {
search_start = abs_start + 1;
}
}
result
}
#[derive(Debug, Clone, Default)]
pub struct ContentComponent {
pub id: Option<String>,
pub content_type: Option<String>,
pub lang: Option<String>,
}
#[derive(Debug, Clone, Default)]
pub struct Representation {
pub id: String,
pub bandwidth: u64,
pub width: Option<u32>,
pub height: Option<u32>,
pub frame_rate: Option<String>,
pub audio_sampling_rate: Option<u32>,
pub codecs: Option<String>,
pub mime_type: Option<String>,
pub segment_base: Option<SegmentBase>,
pub segment_list: Option<SegmentList>,
pub segment_template: Option<SegmentTemplate>,
pub base_urls: Vec<String>,
pub content_protection: Vec<ContentProtection>,
}
impl Representation {
#[must_use]
pub fn new(id: impl Into<String>, bandwidth: u64) -> Self {
Self {
id: id.into(),
bandwidth,
..Default::default()
}
}
#[must_use]
pub fn resolution(&self) -> Option<(u32, u32)> {
match (self.width, self.height) {
(Some(w), Some(h)) => Some((w, h)),
_ => None,
}
}
#[must_use]
pub fn is_video(&self) -> bool {
self.mime_type
.as_ref()
.is_some_and(|m| m.starts_with("video/"))
|| self.width.is_some()
}
#[must_use]
pub fn is_audio(&self) -> bool {
self.mime_type
.as_ref()
.is_some_and(|m| m.starts_with("audio/"))
|| self.audio_sampling_rate.is_some()
}
}
#[derive(Debug, Clone, Default)]
pub struct AdaptationSet {
pub id: Option<u32>,
pub group: Option<u32>,
pub content_type: Option<String>,
pub lang: Option<String>,
pub mime_type: Option<String>,
pub codecs: Option<String>,
pub width: Option<u32>,
pub height: Option<u32>,
pub frame_rate: Option<String>,
pub audio_sampling_rate: Option<u32>,
pub segment_alignment: bool,
pub subsegment_alignment: bool,
pub bitstream_switching: bool,
pub segment_base: Option<SegmentBase>,
pub segment_list: Option<SegmentList>,
pub segment_template: Option<SegmentTemplate>,
pub content_components: Vec<ContentComponent>,
pub representations: Vec<Representation>,
pub content_protection: Vec<ContentProtection>,
pub accessibility: Vec<Descriptor>,
pub role: Vec<Descriptor>,
}
impl AdaptationSet {
#[must_use]
pub fn new() -> Self {
Self::default()
}
#[must_use]
pub fn is_video(&self) -> bool {
self.content_type.as_deref() == Some("video")
|| self
.mime_type
.as_ref()
.is_some_and(|m| m.starts_with("video/"))
|| self.representations.iter().any(Representation::is_video)
}
#[must_use]
pub fn is_audio(&self) -> bool {
self.content_type.as_deref() == Some("audio")
|| self
.mime_type
.as_ref()
.is_some_and(|m| m.starts_with("audio/"))
|| self.representations.iter().any(Representation::is_audio)
}
#[must_use]
pub fn is_text(&self) -> bool {
self.content_type.as_deref() == Some("text")
|| self
.mime_type
.as_ref()
.is_some_and(|m| m.starts_with("text/"))
}
#[must_use]
pub fn representations_by_bandwidth(&self) -> Vec<&Representation> {
let mut reps: Vec<_> = self.representations.iter().collect();
reps.sort_by_key(|r| r.bandwidth);
reps
}
}
#[derive(Debug, Clone, Default)]
pub struct Period {
pub id: Option<String>,
pub start: Option<Duration>,
pub duration: Option<Duration>,
pub bitstream_switching: bool,
pub segment_base: Option<SegmentBase>,
pub segment_list: Option<SegmentList>,
pub segment_template: Option<SegmentTemplate>,
pub adaptation_sets: Vec<AdaptationSet>,
pub base_urls: Vec<String>,
}
impl Period {
#[must_use]
pub fn new() -> Self {
Self::default()
}
#[must_use]
pub fn video_adaptation_sets(&self) -> Vec<&AdaptationSet> {
self.adaptation_sets
.iter()
.filter(|a| a.is_video())
.collect()
}
#[must_use]
pub fn audio_adaptation_sets(&self) -> Vec<&AdaptationSet> {
self.adaptation_sets
.iter()
.filter(|a| a.is_audio())
.collect()
}
#[must_use]
pub fn text_adaptation_sets(&self) -> Vec<&AdaptationSet> {
self.adaptation_sets
.iter()
.filter(|a| a.is_text())
.collect()
}
}
#[derive(Debug, Clone, Default)]
pub struct Mpd {
pub mpd_type: MpdType,
pub min_buffer_time: Duration,
pub media_presentation_duration: Option<Duration>,
pub availability_start_time: Option<String>,
pub availability_end_time: Option<String>,
pub publish_time: Option<String>,
pub minimum_update_period: Option<Duration>,
pub suggested_presentation_delay: Option<Duration>,
pub time_shift_buffer_depth: Option<Duration>,
pub profiles: Vec<String>,
pub base_urls: Vec<String>,
pub program_information: Option<ProgramInformation>,
pub periods: Vec<Period>,
}
impl Mpd {
#[must_use]
pub fn new() -> Self {
Self {
min_buffer_time: Duration::from_secs(2),
..Default::default()
}
}
pub fn parse(xml: &str) -> NetResult<Self> {
if !xml.contains("<MPD") {
return Err(NetError::parse(0, "Missing MPD root element"));
}
let mut mpd = Self::new();
if let Some(type_value) = extract_attribute(xml, "MPD", "type") {
mpd.mpd_type = MpdType::from_str(&type_value);
}
if let Some(mbt) = extract_attribute(xml, "MPD", "minBufferTime") {
if let Some(dur) = parse_iso8601_duration(&mbt) {
mpd.min_buffer_time = dur;
}
}
if let Some(mpd_dur) = extract_attribute(xml, "MPD", "mediaPresentationDuration") {
mpd.media_presentation_duration = parse_iso8601_duration(&mpd_dur);
}
if let Some(profiles) = extract_attribute(xml, "MPD", "profiles") {
mpd.profiles = profiles.split(',').map(|s| s.trim().to_string()).collect();
}
Ok(mpd)
}
#[must_use]
pub const fn is_live(&self) -> bool {
self.mpd_type.is_live()
}
#[must_use]
pub fn duration(&self) -> Option<Duration> {
self.media_presentation_duration
}
#[must_use]
pub fn first_period(&self) -> Option<&Period> {
self.periods.first()
}
}
fn extract_attribute(xml: &str, element: &str, attr: &str) -> Option<String> {
let element_start = xml.find(&format!("<{element}"))?;
let element_end = xml[element_start..].find('>')? + element_start;
let element_str = &xml[element_start..element_end];
let attr_pattern = format!("{attr}=\"");
let attr_start = element_str.find(&attr_pattern)? + attr_pattern.len();
let attr_end = element_str[attr_start..].find('"')? + attr_start;
Some(element_str[attr_start..attr_end].to_string())
}
#[must_use]
pub fn parse_iso8601_duration(s: &str) -> Option<Duration> {
let s = s.trim();
if !s.starts_with('P') {
return None;
}
let s = &s[1..];
let mut total_secs = 0.0;
let mut in_time = false;
let mut num_str = String::new();
for ch in s.chars() {
match ch {
'T' => in_time = true,
'Y' if !in_time => {
if let Ok(n) = num_str.parse::<f64>() {
total_secs += n * 365.25 * 24.0 * 60.0 * 60.0;
}
num_str.clear();
}
'M' if !in_time => {
if let Ok(n) = num_str.parse::<f64>() {
total_secs += n * 30.0 * 24.0 * 60.0 * 60.0;
}
num_str.clear();
}
'D' => {
if let Ok(n) = num_str.parse::<f64>() {
total_secs += n * 24.0 * 60.0 * 60.0;
}
num_str.clear();
}
'H' => {
if let Ok(n) = num_str.parse::<f64>() {
total_secs += n * 60.0 * 60.0;
}
num_str.clear();
}
'M' if in_time => {
if let Ok(n) = num_str.parse::<f64>() {
total_secs += n * 60.0;
}
num_str.clear();
}
'S' => {
if let Ok(n) = num_str.parse::<f64>() {
total_secs += n;
}
num_str.clear();
}
c if c.is_ascii_digit() || c == '.' => num_str.push(c),
_ => {}
}
}
if total_secs > 0.0 {
Some(Duration::from_secs_f64(total_secs))
} else {
None
}
}
#[must_use]
#[allow(dead_code)]
pub fn format_iso8601_duration(dur: Duration) -> String {
let total_secs = dur.as_secs_f64();
if total_secs < 60.0 {
return format!("PT{total_secs:.3}S");
}
let hours = (total_secs / 3600.0).floor() as u64;
let minutes = ((total_secs % 3600.0) / 60.0).floor() as u64;
let seconds = total_secs % 60.0;
let mut result = String::from("PT");
if hours > 0 {
result.push_str(&format!("{hours}H"));
}
if minutes > 0 {
result.push_str(&format!("{minutes}M"));
}
if seconds > 0.0 || result == "PT" {
result.push_str(&format!("{seconds:.3}S"));
}
result
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_mpd_type() {
assert!(!MpdType::Static.is_live());
assert!(MpdType::Dynamic.is_live());
assert_eq!(MpdType::from_str("dynamic"), MpdType::Dynamic);
assert_eq!(MpdType::from_str("static"), MpdType::Static);
}
#[test]
fn test_parse_iso8601_duration() {
assert_eq!(
parse_iso8601_duration("PT10S"),
Some(Duration::from_secs(10))
);
assert_eq!(
parse_iso8601_duration("PT1M30S"),
Some(Duration::from_secs(90))
);
assert_eq!(
parse_iso8601_duration("PT1H"),
Some(Duration::from_secs(3600))
);
assert_eq!(
parse_iso8601_duration("PT1H30M45S"),
Some(Duration::from_secs(5445))
);
assert_eq!(
parse_iso8601_duration("P1D"),
Some(Duration::from_secs(86400))
);
let dur = parse_iso8601_duration("PT10.5S").expect("should succeed in test");
assert!((dur.as_secs_f64() - 10.5).abs() < 0.001);
}
#[test]
fn test_format_iso8601_duration() {
assert_eq!(
format_iso8601_duration(Duration::from_secs(10)),
"PT10.000S"
);
assert_eq!(
format_iso8601_duration(Duration::from_secs(90)),
"PT1M30.000S"
);
assert_eq!(format_iso8601_duration(Duration::from_secs(3600)), "PT1H");
}
#[test]
fn test_segment_template() {
let template = SegmentTemplate::new(90000)
.with_media("video_$RepresentationID$_$Number$.m4s")
.with_initialization("video_$RepresentationID$_init.mp4");
let media_url = template
.media_url("720p", 1, None)
.expect("should succeed in test");
assert_eq!(media_url, "video_720p_1.m4s");
let init_url = template
.initialization_url("720p")
.expect("should succeed in test");
assert_eq!(init_url, "video_720p_init.mp4");
}
#[test]
fn test_segment_template_with_time() {
let template = SegmentTemplate::new(90000).with_media("segment_$Time$.m4s");
let url = template
.media_url("v1", 1, Some(900000))
.expect("should succeed in test");
assert_eq!(url, "segment_900000.m4s");
}
#[test]
fn test_segment_timeline() {
let mut timeline = SegmentTimeline::new();
timeline.add_entry(SegmentTimelineEntry::new(90000).with_start(0));
timeline.add_entry(SegmentTimelineEntry::new(90000).with_repeat(2));
let segments: Vec<_> = timeline.iter_segments().collect();
assert_eq!(segments.len(), 4);
assert_eq!(segments[0], (0, 90000));
assert_eq!(segments[1], (90000, 90000));
}
#[test]
fn test_representation() {
let rep = Representation::new("720p", 1_500_000);
assert_eq!(rep.id, "720p");
assert_eq!(rep.bandwidth, 1_500_000);
}
#[test]
fn test_adaptation_set_type() {
let mut video_as = AdaptationSet::new();
video_as.content_type = Some("video".to_string());
assert!(video_as.is_video());
assert!(!video_as.is_audio());
let mut audio_as = AdaptationSet::new();
audio_as.mime_type = Some("audio/mp4".to_string());
assert!(audio_as.is_audio());
assert!(!audio_as.is_video());
}
#[test]
fn test_mpd_parse_basic() {
let xml = r#"<?xml version="1.0"?>
<MPD type="static" minBufferTime="PT2S" mediaPresentationDuration="PT1H30M">
</MPD>"#;
let mpd = Mpd::parse(xml).expect("should succeed in test");
assert_eq!(mpd.mpd_type, MpdType::Static);
assert_eq!(mpd.min_buffer_time, Duration::from_secs(2));
assert_eq!(
mpd.media_presentation_duration,
Some(Duration::from_secs(5400))
);
}
#[test]
fn test_mpd_parse_live() {
let xml = r#"<MPD type="dynamic" minBufferTime="PT4S"></MPD>"#;
let mpd = Mpd::parse(xml).expect("should succeed in test");
assert!(mpd.is_live());
assert_eq!(mpd.min_buffer_time, Duration::from_secs(4));
}
#[test]
fn test_url_type() {
let url = UrlType::new("init.mp4").with_range(0, 999);
assert_eq!(url.source_url, Some("init.mp4".to_string()));
assert_eq!(url.range, Some((0, 999)));
}
#[test]
fn test_content_protection() {
let cp = ContentProtection::new("urn:uuid:edef8ba9-79d6-4ace-a3c8-27dcd51d21ed");
assert!(cp.is_widevine());
assert!(!cp.is_playready());
}
}