diff --git a/Cargo.lock b/Cargo.lock index ea84ae0..e582b18 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,6 +2,53 @@ # It is not intended for manual editing. version = 3 +[[package]] +name = "aho-corasick" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" +dependencies = [ + "memchr", +] + [[package]] name = "m3u8-parser" version = "0.5.0" +dependencies = [ + "regex", +] + +[[package]] +name = "memchr" +version = "2.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" + +[[package]] +name = "regex" +version = "1.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "368758f23274712b504848e9d5a6f010445cc8b87a7cdb4d7cbee666c1288da3" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" diff --git a/Cargo.toml b/Cargo.toml index 6544ed7..a728972 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -10,3 +10,4 @@ repository = "https://github.com/includeamin/m3u8-parser" readme = "README.md" [dependencies] +regex = "1.11.1" diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..5f521dd --- /dev/null +++ b/Makefile @@ -0,0 +1,4 @@ +check: + cargo fmt + cargo test + cargo clippy --fix --allow-dirty --allow-staged diff --git a/src/m3u8/playlist/builder.rs b/src/m3u8/playlist/builder.rs index 9ddd4bd..ea9b5c1 100644 --- a/src/m3u8/playlist/builder.rs +++ b/src/m3u8/playlist/builder.rs @@ -1,49 +1,13 @@ -//! A builder for creating M3U8 playlists with a fluent interface. -//! -//! This module provides the `PlaylistBuilder` struct, which allows users -//! to construct an M3U8 playlist step-by-step. Each method corresponds -//! to a specific tag defined in the M3U8 specification, enabling the -//! creation of valid playlists in a clear and concise manner. -//! -//! # Example -//! -//! ``` -//! use m3u8_parser::m3u8::playlist::builder::PlaylistBuilder; -//! -//! let playlist = PlaylistBuilder::new() -//! .extm3u() -//! .version(3) -//! .extinf(10.0, Some("Sample Title".to_string())) -//! .uri("http://example.com/media.ts".to_string()) -//! .end_list() -//! .build() -//! .expect("Failed to build playlist"); -//! ``` -//! -//! ## Methods -//! -//! - `new`: Creates a new `PlaylistBuilder` instance. -//! - `extm3u`: Adds an `ExtM3U` tag to the playlist. -//! - `version`: Adds an `ExtXVersion` tag with the specified version number. -//! - `extinf`: Adds an `ExtInf` tag with the duration and an optional title. -//! - `target_duration`: Adds an `ExtXTargetDuration` tag with the specified duration. -//! - `media_sequence`: Adds an `ExtXMediaSequence` tag with the specified sequence number. -//! - `discontinuity_sequence`: Adds an `ExtXDiscontinuitySequence` tag with the specified sequence number. -//! - `end_list`: Adds an `ExtXEndList` tag, indicating the end of the playlist. -//! - `key`: Adds an `ExtXKey` tag with encryption details. -//! - `map`: Adds an `ExtXMap` tag with the specified URI and optional byte range. -//! - `program_date_time`: Adds an `ExtXProgramDateTime` tag with the specified date and time. -//! - `date_range`: Adds an `ExtXDateRange` tag with details for a date range. -//! - `uri`: Adds a `Uri` tag for a media segment. -//! - `build`: Constructs the final `Playlist` and validates it, returning the playlist or a list of validation errors. - use crate::m3u8::playlist::Playlist; use crate::m3u8::tags::Tag; use crate::m3u8::validation::ValidationError; +use std::cell::RefCell; +use std::rc::Rc; /// A builder for creating a `Playlist` with a chained interface. +#[derive(Clone)] pub struct PlaylistBuilder { - tags: Vec, + tags: Rc>>, } impl Default for PlaylistBuilder { @@ -55,126 +19,253 @@ impl Default for PlaylistBuilder { impl PlaylistBuilder { /// Creates a new `PlaylistBuilder`. pub fn new() -> Self { - Self { tags: Vec::new() } + Self { + tags: Rc::new(RefCell::new(Vec::new())), + } } /// Adds an `ExtM3U` tag. - pub fn extm3u(mut self) -> Self { - self.tags.push(Tag::ExtM3U); + pub fn extm3u(self) -> Self { + self.tags.borrow_mut().push(Tag::ExtM3U); self } /// Adds an `ExtXVersion` tag. - pub fn version(mut self, version: u8) -> Self { - self.tags.push(Tag::ExtXVersion(version)); + pub fn version(self, version: u8) -> Self { + self.tags.borrow_mut().push(Tag::ExtXVersion(version)); self } /// Adds an `ExtInf` tag. - pub fn extinf(mut self, duration: f32, title: Option) -> Self { - self.tags.push(Tag::ExtInf(duration, title)); + pub fn extinf(self, url: &str, duration: f32, title: Option) -> Self { + self.tags + .borrow_mut() + .push(Tag::ExtInf(url.to_string(), duration, title)); self } /// Adds an `ExtXTargetDuration` tag. - pub fn target_duration(mut self, duration: u32) -> Self { - self.tags.push(Tag::ExtXTargetDuration(duration)); + pub fn target_duration(self, duration: u64) -> Self { + self.tags + .borrow_mut() + .push(Tag::ExtXTargetDuration(duration)); self } /// Adds an `ExtXMediaSequence` tag. - pub fn media_sequence(mut self, sequence: u64) -> Self { - self.tags.push(Tag::ExtXMediaSequence(sequence)); + pub fn media_sequence(self, sequence: u64) -> Self { + self.tags + .borrow_mut() + .push(Tag::ExtXMediaSequence(sequence)); self } /// Adds an `ExtXDiscontinuitySequence` tag. - pub fn discontinuity_sequence(mut self, sequence: u32) -> Self { - self.tags.push(Tag::ExtXDiscontinuitySequence(sequence)); + pub fn discontinuity_sequence(self, sequence: u32) -> Self { + self.tags + .borrow_mut() + .push(Tag::ExtXDiscontinuitySequence(sequence)); self } /// Adds an `ExtXEndList` tag. - pub fn end_list(mut self) -> Self { - self.tags.push(Tag::ExtXEndList); + pub fn end_list(self) -> Self { + self.tags.borrow_mut().push(Tag::ExtXEndList); self } /// Adds an `ExtXKey` tag. pub fn key( - mut self, - method: String, - uri: Option, - iv: Option, - keyformat: Option, - keyformatversions: Option, + self, + method: &str, + uri: Option<&str>, + iv: Option<&str>, + keyformat: Option<&str>, + keyformatversions: Option<&str>, ) -> Self { - self.tags.push(Tag::ExtXKey { - method, - uri, - iv, - keyformat, - keyformatversions, + self.tags.borrow_mut().push(Tag::ExtXKey { + method: method.to_string(), + uri: uri.map(|s| s.to_string()), + iv: iv.map(|s| s.to_string()), + keyformat: keyformat.map(|s| s.to_string()), + keyformatversions: keyformatversions.map(|s| s.to_string()), }); self } /// Adds an `ExtXMap` tag. - pub fn map(mut self, uri: String, byterange: Option) -> Self { - self.tags.push(Tag::ExtXMap { uri, byterange }); + pub fn map(self, uri: &str, byterange: Option<&str>) -> Self { + self.tags.borrow_mut().push(Tag::ExtXMap { + uri: uri.to_string(), + byterange: byterange.map(|s| s.to_string()), + }); self } /// Adds an `ExtXProgramDateTime` tag. - pub fn program_date_time(mut self, date_time: String) -> Self { - self.tags.push(Tag::ExtXProgramDateTime(date_time)); + pub fn program_date_time(self, date_time: &str) -> Self { + self.tags + .borrow_mut() + .push(Tag::ExtXProgramDateTime(date_time.to_string())); + self + } + + /// Adds an `ExtXGap` tag. + pub fn gap(self) -> Self { + self.tags.borrow_mut().push(Tag::ExtXGap); self } - /// Adds an `ExtXDateRange` tag. + /// Adds an `ExtXByteRange` tag. + pub fn byte_range(self, byterange: &str) -> Self { + self.tags + .borrow_mut() + .push(Tag::ExtXByteRange(byterange.to_string())); + self + } + + /// Adds an `ExtXDefine` tag. + pub fn define(self, value: &str) -> Self { + self.tags + .borrow_mut() + .push(Tag::ExtXDefine(value.to_string())); + self + } + + /// Adds an `ExtXMedia` tag. + #[allow(clippy::too_many_arguments)] + pub fn media( + self, + type_: &str, + group_id: &str, + name: Option<&str>, + uri: Option<&str>, + default: Option, + autoplay: Option, + characteristics: Option<&str>, + language: Option<&str>, + forced: Option, + language_codec: Option<&str>, + instream_id: Option<&str>, + ) -> Self { + self.tags.borrow_mut().push(Tag::ExtXMedia { + type_: type_.to_string(), + group_id: group_id.to_string(), + name: name.map(|s| s.to_string()), + uri: uri.map(|s| s.to_string()), + default, + autoplay, + characteristics: characteristics.map(|s| s.to_string()), + language: language.map(|s| s.to_string()), + instream_id: instream_id.map(|s| s.to_string()), + language_codec: language_codec.map(|s| s.to_string()), + forced, + }); + self + } + + /// Adds an `ExtXStreamInf` tag. #[allow(clippy::too_many_arguments)] - pub fn date_range( - mut self, - id: String, - start_date: String, - end_date: Option, - duration: Option, - planned_duration: Option, - scte35_cmd: Option, - scte35_out: Option, - scte35_in: Option, - end_on_next: Option, + pub fn stream_inf( + self, + bandwidth: u32, + codecs: Option<&str>, + resolution: Option<&str>, + frame_rate: Option, + audio: Option<&str>, + video: Option<&str>, + subtitle: Option<&str>, + closed_captions: Option<&str>, ) -> Self { - self.tags.push(Tag::ExtXDateRange { - id, - start_date, - end_date, - duration, - planned_duration, - scte35_cmd, - scte35_out, - scte35_in, - end_on_next, + self.tags.borrow_mut().push(Tag::ExtXStreamInf { + bandwidth, + codecs: codecs.map(|s| s.to_string()), + resolution: resolution.map(|s| s.to_string()), + frame_rate, + audio: audio.map(|s| s.to_string()), + video: video.map(|s| s.to_string()), + subtitle: subtitle.map(|s| s.to_string()), + closed_captions: closed_captions.map(|s| s.to_string()), }); self } - /// Adds a `Uri` tag. - pub fn uri(mut self, uri: String) -> Self { - self.tags.push(Tag::Uri(uri)); + /// Adds an `ExtXIFrameStreamInf` tag. + pub fn iframe_stream_inf( + self, + bandwidth: u32, + codecs: Option<&str>, + resolution: Option<&str>, + frame_rate: Option, + uri: &str, + ) -> Self { + self.tags.borrow_mut().push(Tag::ExtXIFrameStreamInf { + bandwidth, + codecs: codecs.map(|s| s.to_string()), + resolution: resolution.map(|s| s.to_string()), + frame_rate, + uri: uri.to_string(), + }); self } - /// Builds the `Playlist`, validating it according to RFC 8216. - /// - /// # Returns - /// - /// A result containing a `Playlist` if valid, or a list of validation errors. + /// Adds an `ExtXBitrate` tag. + pub fn bitrate(self, bitrate: u32) -> Self { + self.tags.borrow_mut().push(Tag::ExtXBitrate(bitrate)); + self + } + + /// Adds an `ExtXIndependentSegments` tag. + pub fn independent_segments(self) -> Self { + self.tags.borrow_mut().push(Tag::ExtXIndependentSegments); + self + } + + /// Adds an `ExtXStart` tag. + pub fn start(self, time_offset: &str, precise: Option) -> Self { + self.tags.borrow_mut().push(Tag::ExtXStart { + time_offset: time_offset.to_string(), + precise, + }); + self + } + + /// Adds an `ExtXSessionData` tag. + pub fn session_data(self, id: &str, value: &str, language: Option<&str>) -> Self { + self.tags.borrow_mut().push(Tag::ExtXSessionData { + id: id.to_string(), + value: value.to_string(), + language: language.map(|s| s.to_string()), + }); + self + } + + /// Adds an `ExtXSessionKey` tag. + pub fn session_key(self, method: &str, uri: Option<&str>, iv: Option<&str>) -> Self { + self.tags.borrow_mut().push(Tag::ExtXSessionKey { + method: method.to_string(), + uri: uri.map(|s| s.to_string()), + iv: iv.map(|s| s.to_string()), + }); + self + } + + /// Constructs the final `Playlist` and validates it. pub fn build(self) -> Result> { - let playlist = Playlist { tags: self.tags }; + let playlist = Playlist { + tags: self.tags.borrow().clone(), + }; match playlist.validate() { Ok(_) => Ok(playlist), Err(errors) => Err(errors), } } + + /// Adds an `ExtXPlaylistType` tag. + pub fn playlist_type(self, playlist_type: &str) -> Self { + self.tags + .borrow_mut() + .push(Tag::ExtXPlaylistType(playlist_type.to_string())); + self + } } diff --git a/src/m3u8/playlist/mod.rs b/src/m3u8/playlist/mod.rs index 7af52dd..02f5ffb 100644 --- a/src/m3u8/playlist/mod.rs +++ b/src/m3u8/playlist/mod.rs @@ -31,15 +31,14 @@ pub mod builder; -use crate::m3u8::parser::parse_attributes; use crate::m3u8::tags::Tag; use crate::m3u8::validation::ValidationError; use std::fs::File; -use std::io; -use std::io::Write; -use std::io::{BufRead, BufReader}; +use std::io::{self, BufRead, BufReader, Write}; use std::path::Path; +use regex::Regex; + /// Represents a playlist containing multiple tags. #[derive(Debug, PartialEq)] pub struct Playlist { @@ -48,120 +47,33 @@ pub struct Playlist { impl Playlist { /// Creates a new `Playlist` by reading tags from a buffered reader. - /// - /// # Arguments - /// - /// * `reader` - A buffered reader providing lines of the playlist. - /// - /// # Returns - /// - /// A result containing a `Playlist` or an error message as a string. - pub fn from_reader(reader: R) -> Result { + pub fn from_reader(mut reader: R) -> Result { let mut tags = Vec::new(); - for line in reader.lines() { - let line = line.map_err(|e| e.to_string())?; + + let mut content = String::new(); + reader + .read_to_string(&mut content) + .map_err(|e| e.to_string())?; + + for line in content.split("#") { if line.is_empty() { continue; } - if line.starts_with("#EXTM3U") { - tags.push(Tag::ExtM3U); - } else if let Some(stripped) = line.strip_prefix("#EXT-X-VERSION:") { - let version = stripped.parse().unwrap(); - tags.push(Tag::ExtXVersion(version)); - } else if let Some(stripped) = line.strip_prefix("#EXTINF:") { - let parts: Vec<&str> = stripped.splitn(2, ',').collect(); - let duration = parts[0].parse().unwrap(); - let title = if parts.len() > 1 && !parts[1].to_string().is_empty() { - Some(parts[1].to_string()) - } else { - None - }; - tags.push(Tag::ExtInf(duration, title)); - } else if let Some(stripped) = line.strip_prefix("#EXT-X-TARGETDURATION:") { - let duration = stripped.parse().unwrap(); - tags.push(Tag::ExtXTargetDuration(duration)); - } else if let Some(stripped) = line.strip_prefix("#EXT-X-MEDIA-SEQUENCE:") { - let sequence = stripped.parse().unwrap(); - tags.push(Tag::ExtXMediaSequence(sequence)); - } else if let Some(stripped) = line.strip_prefix("#EXT-X-DISCONTINUITY-SEQUENCE:") { - let sequence = stripped.parse().unwrap(); - tags.push(Tag::ExtXDiscontinuitySequence(sequence)); - } else if line.starts_with("#EXT-X-ENDLIST") { - tags.push(Tag::ExtXEndList); - } else if let Some(stripped) = line.strip_prefix("#EXT-X-KEY:") { - let attributes = parse_attributes(stripped)?; - tags.push(Tag::ExtXKey { - method: attributes - .get("METHOD") - .ok_or("Missing METHOD attribute")? - .clone(), - uri: attributes.get("URI").cloned(), - iv: attributes.get("IV").cloned(), - keyformat: attributes.get("KEYFORMAT").cloned(), - keyformatversions: attributes.get("KEYFORMATVERSIONS").cloned(), - }); - } else if let Some(stripped) = line.strip_prefix("#EXT-X-MAP:") { - let attributes = parse_attributes(stripped)?; - tags.push(Tag::ExtXMap { - uri: attributes - .get("URI") - .ok_or("Missing URI attribute")? - .clone(), - byterange: attributes.get("BYTERANGE").cloned(), - }); - } else if let Some(stripped) = line.strip_prefix("#EXT-X-PROGRAM-DATE-TIME:") { - tags.push(Tag::ExtXProgramDateTime(stripped.to_string())); - } else if let Some(stripped) = line.strip_prefix("#EXT-X-DATERANGE:") { - let attributes = parse_attributes(stripped)?; - tags.push(Tag::ExtXDateRange { - id: attributes.get("ID").ok_or("Missing ID attribute")?.clone(), - start_date: attributes - .get("START-DATE") - .ok_or("Missing START-DATE attribute")? - .clone(), - end_date: attributes.get("END-DATE").cloned(), - duration: attributes - .get("DURATION") - .map(|s| s.parse::().unwrap()), - planned_duration: attributes - .get("PLANNED-DURATION") - .map(|s| s.parse().unwrap()), - scte35_cmd: attributes.get("SCTE35-CMD").cloned(), - scte35_out: attributes.get("SCTE35-OUT").cloned(), - scte35_in: attributes.get("SCTE35-IN").cloned(), - end_on_next: attributes.get("END-ON-NEXT").map(|s| s == "YES"), - }); - } else if !line.starts_with('#') { - tags.push(Tag::Uri(line)); + + if let Some(tag) = Self::parse_line(line)? { + tags.push(tag); } } Ok(Playlist { tags }) } /// Creates a new `Playlist` by reading tags from a file. - /// - /// # Arguments - /// - /// * `path` - The path to the file containing the playlist. - /// - /// # Returns - /// - /// A result containing a `Playlist` or an error message as a string. pub fn from_file>(path: P) -> Result { let file = File::open(path).map_err(|e| e.to_string())?; - let reader = BufReader::new(file); - Self::from_reader(reader) + Self::from_reader(BufReader::new(file)) } /// Writes the playlist to a file. - /// - /// # Arguments - /// - /// * `path` - The path where the playlist should be saved. - /// - /// # Returns - /// - /// A result indicating success or an error if the write fails. pub fn write_to_file>(&self, path: P) -> io::Result<()> { let mut file = File::create(path)?; for tag in &self.tags { @@ -171,91 +83,15 @@ impl Playlist { } /// Validates the playlist according to RFC 8216. - /// - /// # Returns - /// - /// A result indicating success or a list of validation errors. pub fn validate(&self) -> Result<(), Vec> { let mut errors = Vec::new(); - // Ensure the playlist starts with #EXTM3U - match self.tags.first() { - Some(Tag::ExtM3U) => {} - _ => errors.push(ValidationError::MissingExtM3U), + if !self.tags.iter().any(|tag| matches!(tag, Tag::ExtM3U)) { + errors.push(ValidationError::MissingExtM3U); } - // Validate each tag according to its rules for tag in &self.tags { - match tag { - Tag::ExtXVersion(version) => { - if *version < 1 || *version > 7 { - errors.push(ValidationError::InvalidVersion(*version)); - } - } - Tag::ExtInf(duration, _) => { - if *duration <= 0.0 { - errors.push(ValidationError::InvalidDuration(*duration)); - } - } - Tag::ExtXTargetDuration(duration) => { - if *duration == 0 { - errors.push(ValidationError::InvalidTargetDuration(*duration)); - } - } - Tag::ExtXMediaSequence(sequence) => { - if *sequence == 0 { - errors.push(ValidationError::InvalidMediaSequence(*sequence)); - } - } - Tag::ExtXKey { method, .. } => { - if method != "NONE" && method != "AES-128" && method != "SAMPLE-AES" { - errors.push(ValidationError::InvalidKeyMethod(method.clone())); - } - } - Tag::ExtXMap { uri, .. } => { - if uri.is_empty() { - errors.push(ValidationError::InvalidMapUri); - } - } - Tag::ExtXProgramDateTime(date_time) => { - if date_time.is_empty() { - errors.push(ValidationError::InvalidProgramDateTime); - } - } - Tag::ExtXDateRange { - id, - start_date, - end_date, - duration, - planned_duration, - .. - } => { - if id.is_empty() { - errors.push(ValidationError::InvalidDateRangeId); - } - if start_date.is_empty() { - errors.push(ValidationError::InvalidDateRangeStartDate); - } - if let Some(end_date) = end_date { - if end_date < start_date { - errors.push(ValidationError::InvalidDateRangeEndDate); - } - } - if let Some(duration) = duration { - if *duration < 0.0 { - errors.push(ValidationError::InvalidDateRangeDuration(*duration)); - } - } - if let Some(planned_duration) = planned_duration { - if *planned_duration < 0.0 { - errors.push(ValidationError::InvalidDateRangePlannedDuration( - *planned_duration, - )); - } - } - } - _ => {} - } + self.validate_tag(tag, &mut errors); } if errors.is_empty() { @@ -264,4 +100,413 @@ impl Playlist { Err(errors) } } + + fn parse_line(line: &str) -> Result, String> { + let trimmed = line.trim(); + + if trimmed.starts_with("EXTM3U") { + return Ok(Some(Tag::ExtM3U)); + } + + if trimmed.starts_with("EXT-X-VERSION") { + // Example: #EXT-X-VERSION:7 + let version_re = Regex::new(r#"EXT-X-VERSION:(\d+)"#).unwrap(); + if let Some(caps) = version_re.captures(trimmed) { + let version = caps.get(1).unwrap().as_str(); + return Ok(Some(Tag::ExtXVersion(version.parse().unwrap()))); + } + } + + if trimmed.starts_with("EXT-X-TARGETDURATION") { + // Example #EXT-X-TARGETDURATION:10 + let target_duration_re = Regex::new(r#"EXT-X-TARGETDURATION:(\d+)"#).unwrap(); + if let Some(caps) = target_duration_re.captures(trimmed) { + let target = caps.get(1).unwrap().as_str(); + return Ok(Some(Tag::ExtXTargetDuration(target.parse().unwrap()))); + } + } + + if trimmed.starts_with("EXT-X-PLAYLIST-TYPE") { + // Example: #EXT-X-PLAYLIST-TYPE:EVENT + let playlist_type_re = Regex::new(r#"EXT-X-PLAYLIST-TYPE:(\w+)"#).unwrap(); + if let Some(caps) = playlist_type_re.captures(trimmed) { + let playlist_type = caps.get(1).unwrap().as_str(); + return Ok(Some(Tag::ExtXPlaylistType(playlist_type.to_string()))); + } + } + + if trimmed.starts_with("EXT-X-MEDIA-SEQUENCE") { + // Example: #EXT-X-MEDIA-SEQUENCE:0 + let media_sequence_re = Regex::new(r#"EXT-X-MEDIA-SEQUENCE:(\d+)"#).unwrap(); + if let Some(caps) = media_sequence_re.captures(trimmed) { + let sequence = caps.get(1).unwrap().as_str(); + return Ok(Some(Tag::ExtXMediaSequence(sequence.parse().unwrap()))); + } + } + + if trimmed.starts_with("EXT-X-DISCONTINUITY-SEQUENCE") { + // Example: #EXT-X-DISCONTINUITY-SEQUENCE:0 + let discontinuity_seq_re = Regex::new(r#"EXT-X-DISCONTINUITY-SEQUENCE:(\d+)"#).unwrap(); + if let Some(caps) = discontinuity_seq_re.captures(trimmed) { + let sequence = caps.get(1).unwrap().as_str(); + return Ok(Some(Tag::ExtXDiscontinuitySequence( + sequence.parse().unwrap(), + ))); + } + } + + if trimmed.starts_with("EXT-X-ENDLIST") { + return Ok(Some(Tag::ExtXEndList)); + } + + if trimmed.starts_with("EXT-X-KEY") { + // Example: #EXT-X-KEY:METHOD=AES-128,URI="https://example.com/key",IV="0x1234567890ABCDEF",KEYFORMAT="identity",KEYFORMATVERSIONS="1" + let key_re = Regex::new(r#"EXT-X-KEY:METHOD=([A-Za-z0-9\-]+),URI="([^"]+)"(?:,IV="([^"]*)")?(?:,KEYFORMAT="([^"]+)")?(?:,KEYFORMATVERSIONS="([^"]+)")?"#).unwrap(); + + if let Some(caps) = key_re.captures(trimmed) { + let method = caps.get(1).map(|m| m.as_str()).unwrap_or_default(); + let uri = caps.get(2).map(|m| m.as_str().to_string()); + let iv = caps.get(3).map(|m| m.as_str().to_string()); + let keyformat = caps.get(4).map(|m| m.as_str().to_string()); + let keyformatversions = caps.get(5).map(|m| m.as_str().to_string()); + + return Ok(Some(Tag::ExtXKey { + method: method.to_string(), + uri, + iv, + keyformat, + keyformatversions, + })); + } + } + + if trimmed.starts_with("EXT-X-MAP") { + // Example: #EXT-X-MAP:URI="init.mp4",BYTERANGE="800@0" + let map_re = Regex::new(r#"EXT-X-MAP:URI="([^"]+)"(?:,BYTERANGE="([^"]+)")?"#).unwrap(); + if let Some(caps) = map_re.captures(trimmed) { + let uri = caps.get(1).unwrap().as_str(); + let byterange = caps.get(2).map(|m| m.as_str().to_string()); + if byterange.clone().is_none() || byterange.clone().unwrap() == "" { + return Ok(Some(Tag::ExtXMap { + uri: uri.to_string(), + byterange: None, + })); + } + + return Ok(Some(Tag::ExtXMap { + uri: uri.to_string(), + byterange, + })); + } + } + + if trimmed.starts_with("EXT-X-PROGRAM-DATE-TIME") { + // Example: #EXT-X-PROGRAM-DATE-TIME:2024-11-05T12:00:00Z + let datetime_re = Regex::new(r#"EXT-X-PROGRAM-DATE-TIME:([^\s]+)"#).unwrap(); + if let Some(caps) = datetime_re.captures(trimmed) { + let datetime = caps.get(1).unwrap().as_str(); + return Ok(Some(Tag::ExtXProgramDateTime(datetime.to_string()))); + } + } + + if trimmed.starts_with("EXT-X-DISCONTINUITY") { + return Ok(Some(Tag::ExtXDiscontinuity)); + } + + if trimmed.starts_with("EXT-X-PART") { + // Example: #EXT-X-PART:URI="part1.ts",DURATION=5.0 + let part_re = Regex::new(r#"EXT-X-PART:URI="([^\"]+)",DURATION=([\d\.]+)"#).unwrap(); + if let Some(caps) = part_re.captures(trimmed) { + let uri = caps.get(1).unwrap().as_str(); + let duration = caps.get(2).unwrap().as_str().parse().unwrap(); + return Ok(Some(Tag::ExtXPart { + uri: uri.to_string(), + duration: Some(duration), + })); + } + } + + if trimmed.starts_with("EXT-X-PART-INF") { + // Example: #EXT-X-PART-INF:PART-TARGET-DURATION=5.0,PART-HOLD-BACK=2.0 + let part_inf_re = Regex::new( + r#"EXT-X-PART-INF:PART-TARGET-DURATION=([\d\.]+),PART-HOLD-BACK=([\d\.]+)"#, + ) + .unwrap(); + if let Some(caps) = part_inf_re.captures(trimmed) { + let part_target_duration = caps.get(1).unwrap().as_str().parse().unwrap(); + let part_hold_back = caps.get(2).map(|m| m.as_str().parse().unwrap()); + return Ok(Some(Tag::ExtXPartInf { + part_target_duration, + part_hold_back, + part_number: None, + })); + } + } + + if trimmed.starts_with("EXT-X-SERVER-CONTROL") { + // Example: #EXT-X-SERVER-CONTROL:CAN-PLAY=YES,CAN-SEEK=YES,CAN-PAUSE=YES,MIN-BUFFER-TIME=10.0 + let server_control_re = Regex::new(r#"EXT-X-SERVER-CONTROL:CAN-PLAY=(\w+),CAN-SEEK=(\w+),CAN-PAUSE=(\w+),MIN-BUFFER-TIME=([\d\.]+)"#).unwrap(); + if let Some(caps) = server_control_re.captures(trimmed) { + let can_play = caps.get(1).unwrap().as_str() == "YES"; + let can_seek = caps.get(2).unwrap().as_str() == "YES"; + let can_pause = caps.get(3).unwrap().as_str() == "YES"; + let min_buffer_time = caps.get(4).unwrap().as_str().parse().unwrap(); + return Ok(Some(Tag::ExtXServerControl { + can_play: Some(can_play), + can_seek: Some(can_seek), + can_pause: Some(can_pause), + min_buffer_time: Some(min_buffer_time), + })); + } + } + + if trimmed.starts_with("EXT-X-SKIP") { + // Example: #EXT-X-SKIP:SKIPPED-SEGMENTS=3,URI="skip_segment2.ts" + let skip_re = + Regex::new(r#"EXT-X-SKIP:SKIPPED-SEGMENTS=(\d+),URI="([^\"]+)""#).unwrap(); + if let Some(caps) = skip_re.captures(trimmed) { + let skipped_segments = caps.get(1).unwrap().as_str().parse().unwrap(); + let uri = caps.get(2).unwrap().as_str(); + return Ok(Some(Tag::ExtXSkip { + uri: uri.to_string(), + skipped_segments, + duration: None, + reason: None, + })); + } + } + + if trimmed.starts_with("EXT-X-START") { + // Example: #EXT-X-START:TIME-OFFSET=0.0,PRECISE=YES + let start_re = + Regex::new(r#"EXT-X-START:TIME-OFFSET=([\d\.]+),PRECISE=(\w+)"#).unwrap(); + if let Some(caps) = start_re.captures(trimmed) { + let time_offset = caps.get(1).unwrap().as_str().to_string(); + let precise = caps.get(2).unwrap().as_str() == "YES"; + return Ok(Some(Tag::ExtXStart { + time_offset, + precise: Some(precise), + })); + } + } + + if trimmed.starts_with("EXT-X-INDEPENDENT-SEGMENTS") { + return Ok(Some(Tag::ExtXIndependentSegments)); + } + + if trimmed.starts_with("EXT-X-STREAM-INF") { + // Example: #EXT-X-STREAM-INF:BANDWIDTH=500000,RESOLUTION=640x360,CODECS="avc1.42c01e,mp4a.40.2" + let stream_inf_re = Regex::new( + r#"EXT-X-STREAM-INF:BANDWIDTH=(\d+),RESOLUTION=([^,]+),CODECS="([^"]+)"\s*(\S+)"#, + ) + .unwrap(); + if let Some(caps) = stream_inf_re.captures(trimmed) { + let bandwidth = caps.get(1).unwrap().as_str().parse().unwrap(); + let resolution = caps.get(2).unwrap().as_str().to_string(); + let codecs = caps.get(3).unwrap().as_str().to_string(); + return Ok(Some(Tag::ExtXStreamInf { + bandwidth, + resolution: Some(resolution), + codecs: Some(codecs), + frame_rate: None, + audio: None, + video: None, + subtitle: None, + closed_captions: None, + })); + } + } + + if trimmed.starts_with("EXT-X-MEDIA") { + // Example: #EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID="audio",NAME="English",LANGUAGE="en",DEFAULT=YES,AUTOSELECT=YES,URI="audio_en.m3u8" + let media_re = Regex::new(r#"EXT-X-MEDIA:TYPE=(\w+),GROUP-ID="([^"]+)",(?:NAME="([^"]+)")?,(?:LANGUAGE="([^"]+)")?,(?:DEFAULT=(YES|NO))?,(?:AUTOSELECT=(YES|NO))?,(?:URI="([^"]+)")?,(?:CHARACTERISTICS=([^,]+))?,(?:LANGUAGE-CODEC="([^"]+)")?,(?:INSTREAM-ID="([^"]+)")?,(?:FORCED=(YES|NO))?"#).unwrap(); + if let Some(caps) = media_re.captures(trimmed) { + let type_ = caps.get(1).unwrap().as_str().to_string(); + let group_id = caps.get(2).unwrap().as_str().to_string(); + let name = Some(caps.get(3).unwrap().as_str().to_string()); + let language = Some(caps.get(4).unwrap().as_str().to_string()); + let default = Some(caps.get(5).unwrap().as_str() == "YES"); + let auto_select = Some(caps.get(6).unwrap().as_str() == "YES"); + let uri = Some(caps.get(7).unwrap().as_str().to_string()); + let instream_id = Some(caps.get(8).unwrap().as_str().to_string()); + let language_codec = Some(caps.get(9).unwrap().as_str().to_string()); + let characteristics = Some(caps.get(10).unwrap().as_str().to_string()); + let forced = Some(caps.get(11).unwrap().as_str() == "YES"); + + return Ok(Some(Tag::ExtXMedia { + type_, + group_id, + name, + language, + instream_id, + language_codec, + default, + autoplay: auto_select, + characteristics, + uri, + forced, + })); + } + } + + if trimmed.starts_with("EXT-X-RENDITION-REPORT") { + // Example: #EXT-X-RENDITION-REPORT:URI="rendition_report.m3u8",BANDWIDTH=1000000 + let rendition_report_re = + Regex::new(r#"EXT-X-RENDITION-REPORT:URI="([^"]+)",BANDWIDTH=(\d+)"#).unwrap(); + if let Some(caps) = rendition_report_re.captures(trimmed) { + let uri = caps.get(1).unwrap().as_str().to_string(); + let bandwidth = caps.get(2).unwrap().as_str().parse().unwrap(); + return Ok(Some(Tag::ExtXRenditionReport { uri, bandwidth })); + } + } + + if trimmed.starts_with("EXT-X-BYTERANGE") { + // Example: #EXT-X-BYTERANGE:500@1000 + let byte_range_re = Regex::new(r#"EXT-X-BYTERANGE:([^\s]+)"#).unwrap(); + if let Some(caps) = byte_range_re.captures(trimmed) { + let byte_range = caps.get(1).unwrap().as_str().to_string(); + return Ok(Some(Tag::ExtXByteRange(byte_range))); + } + } + + if trimmed.starts_with("EXT-X-I-FRAME-STREAM-INF") { + // Example: #EXT-X-I-FRAME-STREAM-INF:BANDWIDTH=300000,URI="iframe.m3u8" + let iframe_re = + Regex::new(r#"EXT-X-I-FRAME-STREAM-INF:BANDWIDTH=(\d+),URI="([^"]+)""#).unwrap(); + if let Some(caps) = iframe_re.captures(trimmed) { + let bandwidth = caps.get(1).unwrap().as_str().parse().unwrap(); + let uri = caps.get(2).unwrap().as_str().to_string(); + return Ok(Some(Tag::ExtXIFrameStreamInf { + bandwidth, + codecs: None, + resolution: None, + frame_rate: None, + uri, + })); + } + } + + if trimmed.starts_with("EXT-X-SESSION-DATA") { + // Example: #EXT-X-SESSION-DATA:ID="session1",VALUE="value1",LANGUAGE="en" + let session_data_re = + Regex::new(r#"EXT-X-SESSION-DATA:ID="([^"]+)",VALUE="([^"]+)",LANGUAGE="([^"]+)""#) + .unwrap(); + if let Some(caps) = session_data_re.captures(trimmed) { + let id = caps.get(1).unwrap().as_str().to_string(); + let value = caps.get(2).unwrap().as_str().to_string(); + let language = Some(caps.get(3).unwrap().as_str().to_string()); + return Ok(Some(Tag::ExtXSessionData { + id, + value, + language, + })); + } + } + + if trimmed.starts_with("EXT-X-PRELOAD-HINT") { + // Example: #EXT-X-PRELOAD-HINT:URI="preload_segment.ts",BYTERANGE="1000@2000" + let preload_hint_re = + Regex::new(r#"EXT-X-PRELOAD-HINT:URI="([^"]+)",BYTERANGE="([^"]+)""#).unwrap(); + if let Some(caps) = preload_hint_re.captures(trimmed) { + let uri = caps.get(1).unwrap().as_str().to_string(); + let byterange = Some(caps.get(2).unwrap().as_str().to_string()); + return Ok(Some(Tag::ExtXPreloadHint { uri, byterange })); + } + } + + if trimmed.starts_with("EXTINF") { + // let split = trimmed.split("\n").collect::>(); + // + // let metadata_line = split.get(0).unwrap(); + // let segment = split.get(1).unwrap(); + + let extinf_re = Regex::new(r#"EXTINF:(\d+(\.\d+)?),\s*(.*?),?\s*(\S+)"#).unwrap(); + if let Some(caps) = extinf_re.captures(trimmed) { + let duration: f32 = caps.get(1).unwrap().as_str().parse().unwrap(); + let title = caps + .get(3) + .map(|m| m.as_str().trim().to_string()) + .unwrap_or_else(|| "".to_string()); + let segment = caps.get(4).unwrap().as_str().trim().to_string(); + + if title.is_empty() { + return Ok(Some(Tag::ExtInf(segment, duration, None))); + } + + // Return parsed values wrapped in Tag::ExtInf + return Ok(Some(Tag::ExtInf(segment, duration, Some(title)))); + } + } + + if trimmed.starts_with("EXT-X-SESSION-KEY") { + // Example: #EXT-X-SESSION-KEY:METHOD=AES-128,URI="https://example.com/session_key",IV="0x9876543210ABCDEF" + let session_key_re = + Regex::new(r#"EXT-X-SESSION-KEY:METHOD=([^,]+),URI="([^"]+)",IV="([^"]+)""#) + .unwrap(); + if let Some(caps) = session_key_re.captures(trimmed) { + let method = caps.get(1).unwrap().as_str().to_string(); + let uri = Some(caps.get(2).unwrap().as_str().to_string()); + let iv = Some(caps.get(3).unwrap().as_str().to_string()); + return Ok(Some(Tag::ExtXSessionKey { method, uri, iv })); + } + } + + Ok(None) + } + + fn validate_tag(&self, tag: &Tag, errors: &mut Vec) { + match tag { + Tag::ExtXVersion(version) => { + if *version < 1 || *version > 7 { + errors.push(ValidationError::InvalidVersion(*version)); + } + } + Tag::ExtInf(_, duration, _) if *duration <= 0.0 => { + errors.push(ValidationError::InvalidDuration(*duration)); + } + Tag::ExtXTargetDuration(duration) if *duration == 0 => { + errors.push(ValidationError::InvalidTargetDuration(*duration)); + } + Tag::ExtXKey { method, .. } + if !matches!(method.as_str(), "NONE" | "AES-128" | "SAMPLE-AES") => + { + errors.push(ValidationError::InvalidKeyMethod(method.clone())); + } + Tag::ExtXMap { uri, .. } if uri.is_empty() => { + errors.push(ValidationError::InvalidMapUri); + } + Tag::ExtXProgramDateTime(date_time) if date_time.is_empty() => { + errors.push(ValidationError::InvalidProgramDateTime); + } + Tag::ExtXGap => { + // Validation for EXT-X-GAP if necessary + // TODO: maybe we can make it configurable? + } + Tag::ExtXBitrate(bitrate) if bitrate < &0 => { + errors.push(ValidationError::InvalidBitrate(*bitrate)); + } + Tag::ExtXIndependentSegments => { + // No specific validation needed + } + Tag::ExtXStart { time_offset, .. } if time_offset.is_empty() => { + errors.push(ValidationError::InvalidStartOffset); + } + Tag::ExtXSkip { duration, .. } if duration.unwrap() <= 0.0 => { + errors.push(ValidationError::InvalidSkipTag( + "Duration must be positive".to_string(), + )); + } + Tag::ExtXPreloadHint { uri, .. } if uri.is_empty() => { + errors.push(ValidationError::InvalidPreloadHintUri); + } + Tag::ExtXRenditionReport { uri, .. } if uri.is_empty() => { + errors.push(ValidationError::InvalidRenditionReportUri); + } + Tag::ExtXServerControl { .. } => { + // Add specific validations if needed + // TODO: maybe we can make it configurable? + } + _ => {} + } + } } diff --git a/src/m3u8/tags.rs b/src/m3u8/tags.rs index 4458bae..e533e68 100644 --- a/src/m3u8/tags.rs +++ b/src/m3u8/tags.rs @@ -2,16 +2,20 @@ /// /// Each variant corresponds to a specific type of tag defined in the M3U8 specification. /// This enum allows for easy manipulation and representation of these tags in a playlist. -#[derive(Debug, PartialEq)] +#[derive(Debug, PartialEq, Clone)] pub enum Tag { /// Indicates the start of an M3U8 file. ExtM3U, /// Specifies the version of the M3U8 playlist. ExtXVersion(u8), + /// The EXT-X-PLAYLIST-TYPE tag provides mutability information about the + // Media Playlist file. It applies to the entire Media Playlist file. + // It is OPTIONAL. Its format is: + ExtXPlaylistType(String), /// Represents a media segment with a duration and an optional title. - ExtInf(f32, Option), + ExtInf(String, f32, Option), /// Indicates the target duration for media segments. - ExtXTargetDuration(u32), + ExtXTargetDuration(u64), /// Specifies the media sequence number. ExtXMediaSequence(u64), /// Represents a discontinuity sequence number. @@ -33,20 +37,102 @@ pub enum Tag { }, /// Specifies the program date and time. ExtXProgramDateTime(String), - /// Represents a date range for events within the playlist. - ExtXDateRange { - id: String, - start_date: String, - end_date: Option, + /// Represents a byte range. + ExtXByteRange(String), + /// Defines a custom tag with a specific value. + ExtXDefine(String), + /// Represents media information. + ExtXMedia { + type_: String, + group_id: String, + name: Option, + uri: Option, + default: Option, + autoplay: Option, + characteristics: Option, + language: Option, + instream_id: Option, + language_codec: Option, + forced: Option, + }, + /// Represents stream information. + ExtXStreamInf { + bandwidth: u32, + codecs: Option, + resolution: Option, + frame_rate: Option, + audio: Option, + video: Option, + subtitle: Option, + closed_captions: Option, + }, + /// Represents an I-frame stream information. + ExtXIFrameStreamInf { + bandwidth: u32, + codecs: Option, + resolution: Option, + frame_rate: Option, + uri: String, + }, + /// Indicates a gap in the playlist. + ExtXGap, + /// Specifies the bitrate of the stream. + ExtXBitrate(u32), + /// Indicates that segments are independent. + ExtXIndependentSegments, + /// Specifies the start time offset. + ExtXStart { + time_offset: String, + precise: Option, + }, + /// Provides server control information. + ExtXServerControl { + can_play: Option, + can_seek: Option, + can_pause: Option, + min_buffer_time: Option, + }, + /// Represents part information. + ExtXPartInf { + part_target_duration: f32, + part_hold_back: Option, + part_number: Option, + }, + /// Represents a preload hint. + ExtXPreloadHint { + uri: String, + /// Optional byte range for the preload hint. + byterange: Option, + }, + /// Represents a rendition report. + ExtXRenditionReport { uri: String, bandwidth: u32 }, + /// Represents a part of a media segment. + ExtXPart { + uri: String, duration: Option, - planned_duration: Option, - scte35_cmd: Option, - scte35_out: Option, - scte35_in: Option, - end_on_next: Option, + // additional fields if necessary + }, + /// Indicates a skip in the playlist. + ExtXSkip { + uri: String, + duration: Option, + skipped_segments: u32, + reason: Option, + }, + /// Indicates a discontinuity in the media stream. + ExtXDiscontinuity, + /// Represents session data for tracking and metadata. + ExtXSessionData { + id: String, + value: String, + // Optional fields for additional parameters + language: Option, + }, + ExtXSessionKey { + method: String, + uri: Option, + iv: Option, }, - /// Represents a URI to a media segment. - Uri(String), } impl std::fmt::Display for Tag { @@ -75,11 +161,11 @@ impl std::fmt::Display for Tag { match self { Tag::ExtM3U => write!(f, "#EXTM3U"), Tag::ExtXVersion(version) => write!(f, "#EXT-X-VERSION:{}", version), - Tag::ExtInf(duration, title) => { + Tag::ExtInf(url, duration, title) => { if let Some(title) = title { - write!(f, "#EXTINF:{},{},", duration, title) + write!(f, "#EXTINF:{},{}\n {}", duration, title, url) } else { - write!(f, "#EXTINF:{},", duration) + write!(f, "#EXTINF:{},\n{}", duration, url) } } Tag::ExtXTargetDuration(duration) => { @@ -124,50 +210,247 @@ impl std::fmt::Display for Tag { Tag::ExtXProgramDateTime(date_time) => { write!(f, "#EXT-X-PROGRAM-DATE-TIME:{}", date_time) } - Tag::ExtXDateRange { - id, - start_date, - end_date, - duration, - planned_duration, - scte35_cmd, - scte35_out, - scte35_in, - end_on_next, + Tag::ExtXByteRange(byterange) => { + write!(f, "#EXT-X-BYTERANGE:{}", byterange) + } + Tag::ExtXDefine(value) => { + write!(f, "#EXT-X-DEFINE:{}", value) + } + Tag::ExtXMedia { + type_, + group_id, + name, + uri, + default, + autoplay, + characteristics, + language, + instream_id, + language_codec, + forced, + } => { + // Basic required fields + write!(f, "#EXT-X-MEDIA:TYPE={},GROUP-ID=\"{}\"", type_, group_id)?; + + // Optional URI field + if let Some(uri) = uri { + write!(f, ",URI=\"{}\"", uri)?; + } + + // Optional name field + if let Some(name) = name { + write!(f, ",NAME=\"{}\"", name)?; + } + + // Optional default field + if let Some(default) = default { + write!(f, ",DEFAULT={}", if *default { "YES" } else { "NO" })?; + } + + // Optional autoplay field + if let Some(autoplay) = autoplay { + write!(f, ",AUTOPLAY={}", if *autoplay { "YES" } else { "NO" })?; + } + + // Optional forced field + if let Some(forced) = forced { + write!(f, ",FORCED={}", if *forced { "YES" } else { "NO" })?; + } + + // Optional instream_id field + if let Some(instream_id) = instream_id { + write!(f, ",INSTREAM-ID=\"{}\"", instream_id)?; + } + + // Optional characteristics field + if let Some(characteristics) = characteristics { + write!(f, ",CHARACTERISTICS={}", characteristics)?; + } + + // Optional language field + if let Some(language) = language { + write!(f, ",LANGUAGE=\"{}\"", language)?; + } + + // Optional language_codec field + if let Some(language_codec) = language_codec { + write!(f, ",LANGUAGE-CODEC=\"{}\"", language_codec)?; + } + + Ok(()) + } + Tag::ExtXStreamInf { + bandwidth, + codecs, + resolution, + frame_rate, + audio, + video, + subtitle, + closed_captions, + } => { + write!(f, "#EXT-X-STREAM-INF:BANDWIDTH={}", bandwidth)?; + if let Some(codecs) = codecs { + write!(f, ",CODECS=\"{}\"", codecs)?; + } + if let Some(resolution) = resolution { + write!(f, ",RESOLUTION={}", resolution)?; + } + if let Some(frame_rate) = frame_rate { + write!(f, ",FRAME-RATE={}", frame_rate)?; + } + if let Some(audio) = audio { + write!(f, ",AUDIO=\"{}\"", audio)?; + } + if let Some(video) = video { + write!(f, ",VIDEO=\"{}\"", video)?; + } + if let Some(subtitle) = subtitle { + write!(f, ",SUBTITLES=\"{}\"", subtitle)?; + } + if let Some(closed_captions) = closed_captions { + write!(f, ",CLOSED-CAPTIONS=\"{}\"", closed_captions)?; + } + Ok(()) + } + Tag::ExtXIFrameStreamInf { + bandwidth, + codecs, + resolution, + frame_rate, + uri, } => { + write!(f, "#EXT-X-I-FRAME-STREAM-INF:BANDWIDTH={}", bandwidth)?; + if let Some(codecs) = codecs { + write!(f, ",CODECS=\"{}\"", codecs)?; + } + if let Some(resolution) = resolution { + write!(f, ",RESOLUTION={}", resolution)?; + } + if let Some(frame_rate) = frame_rate { + write!(f, ",FRAME-RATE={}", frame_rate)?; + } + write!(f, ",URI=\"{}\"", uri)?; + Ok(()) + } + Tag::ExtXGap => write!(f, "#EXT-X-GAP"), + Tag::ExtXBitrate(bitrate) => { + write!(f, "#EXT-X-BITRATE:{}", bitrate) + } + Tag::ExtXIndependentSegments => write!(f, "#EXT-X-INDEPENDENT-SEGMENTS"), + Tag::ExtXStart { + time_offset, + precise, + } => { + write!(f, "#EXT-X-START:TIME-OFFSET={}", time_offset)?; + if let Some(precise) = precise { + write!(f, ",PRECISE={}", if *precise { "YES" } else { "NO" })?; + } + Ok(()) + } + Tag::ExtXServerControl { + can_play, + can_seek, + can_pause, + min_buffer_time, + } => { + write!(f, "#EXT-X-SERVER-CONTROL")?; + if let Some(can_play) = can_play { + write!(f, ",CAN-PLAY={}", if *can_play { "YES" } else { "NO" })?; + } + if let Some(can_seek) = can_seek { + write!(f, ",CAN-SEEK={}", if *can_seek { "YES" } else { "NO" })?; + } + if let Some(can_pause) = can_pause { + write!(f, ",CAN-PAUSE={}", if *can_pause { "YES" } else { "NO" })?; + } + if let Some(min_buffer_time) = min_buffer_time { + write!(f, ",MIN-BUFFER-TIME={}", min_buffer_time)?; + } + Ok(()) + } + Tag::ExtXPartInf { + part_target_duration, + part_hold_back, + part_number, + } => { + write!(f, "#EXT-X-PART-INF:PART-TARGET={}", part_target_duration)?; + if let Some(part_hold_back) = part_hold_back { + write!(f, ",PART-HOLD-BACK={}", part_hold_back)?; + } + if let Some(part_number) = part_number { + write!(f, ",PART-NUMBER={}", part_number)?; + } + Ok(()) + } + Tag::ExtXPreloadHint { uri, byterange } => { + let mut result = format!("#EXT-X-PRELOAD-HINT:URI=\"{}\"", uri); + if let Some(byterange) = byterange { + result.push_str(&format!(",BYTERANGE={}", byterange)); + } + write!(f, "{}", result) + } + Tag::ExtXRenditionReport { uri, bandwidth } => { write!( f, - "#EXT-X-DATERANGE:ID=\"{}\",START-DATE=\"{}\"", - id, start_date - )?; - if let Some(end_date) = end_date { - write!(f, ",END-DATE=\"{}\"", end_date)?; - } + "#EXT-X-RENDITION-REPORT:URI=\"{}\",BANDWIDTH={}", + uri, bandwidth + ) + } + Tag::ExtXPart { uri, duration } => { + write!(f, "#EXT-X-PART:URI=\"{}\"", uri)?; if let Some(duration) = duration { write!(f, ",DURATION={}", duration)?; } - if let Some(planned_duration) = planned_duration { - write!(f, ",PLANNED-DURATION={}", planned_duration)?; + Ok(()) + } + Tag::ExtXSkip { + uri, + duration, + skipped_segments, + reason, + } => { + let mut output = format!( + "#EXT-X-SKIP:URI=\"{}\",SKIPPED-SEGMENTS={}", + uri, skipped_segments + ); + + if let Some(duration) = duration { + output.push_str(&format!(",DURATION={}", duration)); } - if let Some(scte35_cmd) = scte35_cmd { - write!(f, ",SCTE35-CMD={}", scte35_cmd)?; + + if let Some(reason) = reason { + output.push_str(&format!(",REASON=\"{}\"", reason)); } - if let Some(scte35_out) = scte35_out { - write!(f, ",SCTE35-OUT={}", scte35_out)?; + + write!(f, "{}", output) + } + Tag::ExtXDiscontinuity => write!(f, "#EXT-X-DISCONTINUITY"), + Tag::ExtXSessionData { + id, + value, + language, + } => { + write!(f, "#EXT-X-SESSION-DATA:ID=\"{}\",VALUE=\"{}\"", id, value)?; + if let Some(language) = language { + write!(f, ",LANGUAGE=\"{}\"", language)?; } - if let Some(scte35_in) = scte35_in { - write!(f, ",SCTE35-IN={}", scte35_in)?; + Ok(()) + } + Tag::ExtXSessionKey { method, uri, iv } => { + write!(f, "#EXT-X-SESSION-KEY:METHOD={}", method)?; + if let Some(uri) = uri { + write!(f, ",URI={}", uri)?; } - if let Some(end_on_next) = end_on_next { - write!( - f, - ",END-ON-NEXT={}", - if *end_on_next { "YES" } else { "NO" } - )?; + if let Some(iv) = iv { + write!(f, ",IV={}", iv)?; } Ok(()) } - Tag::Uri(uri) => write!(f, "{}", uri), + Tag::ExtXPlaylistType(playlist_type) => { + write!(f, "#EXT-X-PLAYLIST-TYPE:{}", playlist_type)?; + Ok(()) + } } } } diff --git a/src/m3u8/tests/lib_tests.rs b/src/m3u8/tests/lib_tests.rs index 2e41eca..75e98b7 100644 --- a/src/m3u8/tests/lib_tests.rs +++ b/src/m3u8/tests/lib_tests.rs @@ -10,7 +10,7 @@ mod tests { fn test_parse_simple_playlist() { let data = r#" #EXTM3U -#EXT-X-VERSION:3 +#EXT-X-VERSION:7 #EXT-X-TARGETDURATION:10 #EXTINF:5.005, https://media.example.com/first.ts @@ -26,14 +26,23 @@ https://media.example.com/third.ts playlist.tags, vec![ Tag::ExtM3U, - Tag::ExtXVersion(3), + Tag::ExtXVersion(7), Tag::ExtXTargetDuration(10), - Tag::ExtInf(5.005, None), - Tag::Uri("https://media.example.com/first.ts".to_string()), - Tag::ExtInf(5.005, None), - Tag::Uri("https://media.example.com/second.ts".to_string()), - Tag::ExtInf(3.003, None), - Tag::Uri("https://media.example.com/third.ts".to_string()), + Tag::ExtInf( + "https://media.example.com/first.ts".to_string(), + 5.005, + None + ), + Tag::ExtInf( + "https://media.example.com/second.ts".to_string(), + 5.005, + None + ), + Tag::ExtInf( + "https://media.example.com/third.ts".to_string(), + 3.003, + None + ), Tag::ExtXEndList, ] ); @@ -44,14 +53,23 @@ https://media.example.com/third.ts let playlist = Playlist { tags: vec![ Tag::ExtM3U, - Tag::ExtXVersion(3), + Tag::ExtXVersion(7), Tag::ExtXTargetDuration(10), - Tag::ExtInf(5.005, None), - Tag::Uri("https://media.example.com/first.ts".to_string()), - Tag::ExtInf(5.005, None), - Tag::Uri("https://media.example.com/second.ts".to_string()), - Tag::ExtInf(3.003, None), - Tag::Uri("https://media.example.com/third.ts".to_string()), + Tag::ExtInf( + "https://media.example.com/first.ts".to_string(), + 5.005, + None, + ), + Tag::ExtInf( + "https://media.example.com/second.ts".to_string(), + 5.005, + None, + ), + Tag::ExtInf( + "https://media.example.com/third.ts".to_string(), + 3.003, + None, + ), Tag::ExtXEndList, ], }; @@ -62,8 +80,8 @@ https://media.example.com/third.ts } let output = String::from_utf8(output).unwrap(); - let expected = "#EXTM3U -#EXT-X-VERSION:3 + let expected = r#"#EXTM3U +#EXT-X-VERSION:7 #EXT-X-TARGETDURATION:10 #EXTINF:5.005, https://media.example.com/first.ts @@ -72,7 +90,7 @@ https://media.example.com/second.ts #EXTINF:3.003, https://media.example.com/third.ts #EXT-X-ENDLIST -"; +"#; assert_eq!(output, expected); } @@ -81,7 +99,7 @@ https://media.example.com/third.ts fn test_parse_playlist_with_key() { let data = r#" #EXTM3U -#EXT-X-VERSION:3 +#EXT-X-VERSION:7 #EXT-X-TARGETDURATION:10 #EXT-X-KEY:METHOD=AES-128,URI="https://priv.example.com/key.php?r=52" #EXTINF:5.005, @@ -98,7 +116,7 @@ https://media.example.com/third.ts playlist.tags, vec![ Tag::ExtM3U, - Tag::ExtXVersion(3), + Tag::ExtXVersion(7), Tag::ExtXTargetDuration(10), Tag::ExtXKey { method: "AES-128".to_string(), @@ -107,12 +125,21 @@ https://media.example.com/third.ts keyformat: None, keyformatversions: None, }, - Tag::ExtInf(5.005, None), - Tag::Uri("https://media.example.com/first.ts".to_string()), - Tag::ExtInf(5.005, None), - Tag::Uri("https://media.example.com/second.ts".to_string()), - Tag::ExtInf(3.003, None), - Tag::Uri("https://media.example.com/third.ts".to_string()), + Tag::ExtInf( + "https://media.example.com/first.ts".to_string(), + 5.005, + None + ), + Tag::ExtInf( + "https://media.example.com/second.ts".to_string(), + 5.005, + None + ), + Tag::ExtInf( + "https://media.example.com/third.ts".to_string(), + 3.003, + None + ), Tag::ExtXEndList, ] ); @@ -123,7 +150,7 @@ https://media.example.com/third.ts let playlist = Playlist { tags: vec![ Tag::ExtM3U, - Tag::ExtXVersion(3), + Tag::ExtXVersion(7), Tag::ExtXTargetDuration(10), Tag::ExtXKey { method: "AES-128".to_string(), @@ -132,12 +159,21 @@ https://media.example.com/third.ts keyformat: None, keyformatversions: None, }, - Tag::ExtInf(5.005, None), - Tag::Uri("https://media.example.com/first.ts".to_string()), - Tag::ExtInf(5.005, None), - Tag::Uri("https://media.example.com/second.ts".to_string()), - Tag::ExtInf(3.003, None), - Tag::Uri("https://media.example.com/third.ts".to_string()), + Tag::ExtInf( + "https://media.example.com/first.ts".to_string(), + 5.005, + None, + ), + Tag::ExtInf( + "https://media.example.com/second.ts".to_string(), + 5.005, + None, + ), + Tag::ExtInf( + "https://media.example.com/third.ts".to_string(), + 3.003, + None, + ), Tag::ExtXEndList, ], }; @@ -148,10 +184,10 @@ https://media.example.com/third.ts } let output = String::from_utf8(output).unwrap(); - let expected = "#EXTM3U -#EXT-X-VERSION:3 + let expected = r#"#EXTM3U +#EXT-X-VERSION:7 #EXT-X-TARGETDURATION:10 -#EXT-X-KEY:METHOD=AES-128,URI=\"https://priv.example.com/key.php?r=52\" +#EXT-X-KEY:METHOD=AES-128,URI="https://priv.example.com/key.php?r=52" #EXTINF:5.005, https://media.example.com/first.ts #EXTINF:5.005, @@ -159,7 +195,7 @@ https://media.example.com/second.ts #EXTINF:3.003, https://media.example.com/third.ts #EXT-X-ENDLIST -"; +"#; assert_eq!(output, expected); } @@ -191,12 +227,21 @@ https://media.example.com/third.ts uri: "init.mp4".to_string(), byterange: None, }, - Tag::ExtInf(5.005, None), - Tag::Uri("https://media.example.com/first.ts".to_string()), - Tag::ExtInf(5.005, None), - Tag::Uri("https://media.example.com/second.ts".to_string()), - Tag::ExtInf(3.003, None), - Tag::Uri("https://media.example.com/third.ts".to_string()), + Tag::ExtInf( + "https://media.example.com/first.ts".to_string(), + 5.005, + None + ), + Tag::ExtInf( + "https://media.example.com/second.ts".to_string(), + 5.005, + None + ), + Tag::ExtInf( + "https://media.example.com/third.ts".to_string(), + 3.003, + None + ), Tag::ExtXEndList, ] ); @@ -213,12 +258,21 @@ https://media.example.com/third.ts uri: "init.mp4".to_string(), byterange: None, }, - Tag::ExtInf(5.005, None), - Tag::Uri("https://media.example.com/first.ts".to_string()), - Tag::ExtInf(5.005, None), - Tag::Uri("https://media.example.com/second.ts".to_string()), - Tag::ExtInf(3.003, None), - Tag::Uri("https://media.example.com/third.ts".to_string()), + Tag::ExtInf( + "https://media.example.com/first.ts".to_string(), + 5.005, + None, + ), + Tag::ExtInf( + "https://media.example.com/second.ts".to_string(), + 5.005, + None, + ), + Tag::ExtInf( + "https://media.example.com/third.ts".to_string(), + 3.003, + None, + ), Tag::ExtXEndList, ], }; @@ -229,10 +283,10 @@ https://media.example.com/third.ts } let output = String::from_utf8(output).unwrap(); - let expected = "#EXTM3U + let expected = r#"#EXTM3U #EXT-X-VERSION:6 #EXT-X-TARGETDURATION:10 -#EXT-X-MAP:URI=\"init.mp4\" +#EXT-X-MAP:URI="init.mp4" #EXTINF:5.005, https://media.example.com/first.ts #EXTINF:5.005, @@ -240,7 +294,7 @@ https://media.example.com/second.ts #EXTINF:3.003, https://media.example.com/third.ts #EXT-X-ENDLIST -"; +"#; assert_eq!(output, expected); } @@ -249,7 +303,7 @@ https://media.example.com/third.ts fn test_parse_playlist_with_program_date_time() { let data = r#" #EXTM3U -#EXT-X-VERSION:3 +#EXT-X-VERSION:7 #EXT-X-TARGETDURATION:10 #EXT-X-PROGRAM-DATE-TIME:2020-01-01T00:00:00Z #EXTINF:5.005, @@ -266,15 +320,24 @@ https://media.example.com/third.ts playlist.tags, vec![ Tag::ExtM3U, - Tag::ExtXVersion(3), + Tag::ExtXVersion(7), Tag::ExtXTargetDuration(10), Tag::ExtXProgramDateTime("2020-01-01T00:00:00Z".to_string()), - Tag::ExtInf(5.005, None), - Tag::Uri("https://media.example.com/first.ts".to_string()), - Tag::ExtInf(5.005, None), - Tag::Uri("https://media.example.com/second.ts".to_string()), - Tag::ExtInf(3.003, None), - Tag::Uri("https://media.example.com/third.ts".to_string()), + Tag::ExtInf( + "https://media.example.com/first.ts".to_string(), + 5.005, + None + ), + Tag::ExtInf( + "https://media.example.com/second.ts".to_string(), + 5.005, + None + ), + Tag::ExtInf( + "https://media.example.com/third.ts".to_string(), + 3.003, + None + ), Tag::ExtXEndList, ] ); @@ -285,15 +348,24 @@ https://media.example.com/third.ts let playlist = Playlist { tags: vec![ Tag::ExtM3U, - Tag::ExtXVersion(3), + Tag::ExtXVersion(7), Tag::ExtXTargetDuration(10), Tag::ExtXProgramDateTime("2020-01-01T00:00:00Z".to_string()), - Tag::ExtInf(5.005, None), - Tag::Uri("https://media.example.com/first.ts".to_string()), - Tag::ExtInf(5.005, None), - Tag::Uri("https://media.example.com/second.ts".to_string()), - Tag::ExtInf(3.003, None), - Tag::Uri("https://media.example.com/third.ts".to_string()), + Tag::ExtInf( + "https://media.example.com/first.ts".to_string(), + 5.005, + None, + ), + Tag::ExtInf( + "https://media.example.com/second.ts".to_string(), + 5.005, + None, + ), + Tag::ExtInf( + "https://media.example.com/third.ts".to_string(), + 3.003, + None, + ), Tag::ExtXEndList, ], }; @@ -304,8 +376,8 @@ https://media.example.com/third.ts } let output = String::from_utf8(output).unwrap(); - let expected = "#EXTM3U -#EXT-X-VERSION:3 + let expected = r#"#EXTM3U +#EXT-X-VERSION:7 #EXT-X-TARGETDURATION:10 #EXT-X-PROGRAM-DATE-TIME:2020-01-01T00:00:00Z #EXTINF:5.005, @@ -315,7 +387,7 @@ https://media.example.com/second.ts #EXTINF:3.003, https://media.example.com/third.ts #EXT-X-ENDLIST -"; +"#; assert_eq!(output, expected); } @@ -326,7 +398,6 @@ https://media.example.com/third.ts #EXTM3U #EXT-X-VERSION:7 #EXT-X-TARGETDURATION:10 -#EXT-X-DATERANGE:ID="ad-break",START-DATE="2020-01-01T00:00:00Z",DURATION=60.0 #EXTINF:5.005, https://media.example.com/first.ts #EXTINF:5.005, @@ -343,90 +414,35 @@ https://media.example.com/third.ts Tag::ExtM3U, Tag::ExtXVersion(7), Tag::ExtXTargetDuration(10), - Tag::ExtXDateRange { - id: "ad-break".to_string(), - start_date: "2020-01-01T00:00:00Z".to_string(), - end_date: None, - duration: Some(60.0), - planned_duration: None, - scte35_cmd: None, - scte35_out: None, - scte35_in: None, - end_on_next: None, - }, - Tag::ExtInf(5.005, None), - Tag::Uri("https://media.example.com/first.ts".to_string()), - Tag::ExtInf(5.005, None), - Tag::Uri("https://media.example.com/second.ts".to_string()), - Tag::ExtInf(3.003, None), - Tag::Uri("https://media.example.com/third.ts".to_string()), + Tag::ExtInf( + "https://media.example.com/first.ts".to_string(), + 5.005, + None + ), + Tag::ExtInf( + "https://media.example.com/second.ts".to_string(), + 5.005, + None + ), + Tag::ExtInf( + "https://media.example.com/third.ts".to_string(), + 3.003, + None + ), Tag::ExtXEndList, ] ); } - #[test] - fn test_write_playlist_with_daterange() { - let playlist = Playlist { - tags: vec![ - Tag::ExtM3U, - Tag::ExtXVersion(7), - Tag::ExtXTargetDuration(10), - Tag::ExtXDateRange { - id: "ad-break".to_string(), - start_date: "2020-01-01T00:00:00Z".to_string(), - end_date: None, - duration: Some(60.6), - planned_duration: None, - scte35_cmd: None, - scte35_out: None, - scte35_in: None, - end_on_next: None, - }, - Tag::ExtInf(5.005, None), - Tag::Uri("https://media.example.com/first.ts".to_string()), - Tag::ExtInf(5.005, None), - Tag::Uri("https://media.example.com/second.ts".to_string()), - Tag::ExtInf(3.003, None), - Tag::Uri("https://media.example.com/third.ts".to_string()), - Tag::ExtXEndList, - ], - }; - - let mut output = Vec::new(); - for tag in &playlist.tags { - writeln!(output, "{}", tag).unwrap(); - } - let output = String::from_utf8(output).unwrap(); - - let expected = "#EXTM3U -#EXT-X-VERSION:7 -#EXT-X-TARGETDURATION:10 -#EXT-X-DATERANGE:ID=\"ad-break\",START-DATE=\"2020-01-01T00:00:00Z\",DURATION=60.6 -#EXTINF:5.005, -https://media.example.com/first.ts -#EXTINF:5.005, -https://media.example.com/second.ts -#EXTINF:3.003, -https://media.example.com/third.ts -#EXT-X-ENDLIST -"; - - assert_eq!(output, expected); - } - #[test] fn test_playlist_builder() { let playlist = PlaylistBuilder::new() .extm3u() - .version(3) + .version(7) .target_duration(10) - .extinf(5.005, None) - .uri("https://media.example.com/first.ts".to_string()) - .extinf(5.005, None) - .uri("https://media.example.com/second.ts".to_string()) - .extinf(3.003, None) - .uri("https://media.example.com/third.ts".to_string()) + .extinf("https://media.example.com/first.ts", 5.005, None) + .extinf("https://media.example.com/second.ts", 5.005, None) + .extinf("https://media.example.com/third.ts", 3.003, None) .end_list() .build() .unwrap(); @@ -435,14 +451,23 @@ https://media.example.com/third.ts playlist.tags, vec![ Tag::ExtM3U, - Tag::ExtXVersion(3), + Tag::ExtXVersion(7), Tag::ExtXTargetDuration(10), - Tag::ExtInf(5.005, None), - Tag::Uri("https://media.example.com/first.ts".to_string()), - Tag::ExtInf(5.005, None), - Tag::Uri("https://media.example.com/second.ts".to_string()), - Tag::ExtInf(3.003, None), - Tag::Uri("https://media.example.com/third.ts".to_string()), + Tag::ExtInf( + "https://media.example.com/first.ts".to_string(), + 5.005, + None + ), + Tag::ExtInf( + "https://media.example.com/second.ts".to_string(), + 5.005, + None + ), + Tag::ExtInf( + "https://media.example.com/third.ts".to_string(), + 3.003, + None + ), Tag::ExtXEndList, ] ); @@ -454,7 +479,7 @@ https://media.example.com/third.ts let output = String::from_utf8(output).unwrap(); let expected = "#EXTM3U -#EXT-X-VERSION:3 +#EXT-X-VERSION:7 #EXT-X-TARGETDURATION:10 #EXTINF:5.005, https://media.example.com/first.ts @@ -474,12 +499,9 @@ https://media.example.com/third.ts .extm3u() .version(3) .target_duration(10) - .extinf(5.005, None) - .uri("https://media.example.com/first.ts".to_string()) - .extinf(5.005, None) - .uri("https://media.example.com/second.ts".to_string()) - .extinf(3.003, None) - .uri("https://media.example.com/third.ts".to_string()) + .extinf("https://media.example.com/first.ts", 5.005, None) + .extinf("https://media.example.com/second.ts", 5.005, None) + .extinf("https://media.example.com/third.ts", 3.003, None) .end_list() .build(); @@ -491,12 +513,9 @@ https://media.example.com/third.ts let playlist = PlaylistBuilder::new() .version(3) .target_duration(10) - .extinf(5.005, None) - .uri("https://media.example.com/first.ts".to_string()) - .extinf(5.005, None) - .uri("https://media.example.com/second.ts".to_string()) - .extinf(3.003, None) - .uri("https://media.example.com/third.ts".to_string()) + .extinf("https://media.example.com/first.ts", 5.005, None) + .extinf("https://media.example.com/second.ts", 5.005, None) + .extinf("https://media.example.com/third.ts", 3.003, None) .end_list() .build(); @@ -509,12 +528,9 @@ https://media.example.com/third.ts .extm3u() .version(8) // Invalid version .target_duration(10) - .extinf(5.005, None) - .uri("https://media.example.com/first.ts".to_string()) - .extinf(5.005, None) - .uri("https://media.example.com/second.ts".to_string()) - .extinf(3.003, None) - .uri("https://media.example.com/third.ts".to_string()) + .extinf("https://media.example.com/first.ts", 5.005, None) + .extinf("https://media.example.com/second.ts", 5.005, None) + .extinf("https://media.example.com/third.ts", 3.003, None) .end_list() .build(); @@ -527,12 +543,9 @@ https://media.example.com/third.ts .extm3u() .version(3) .target_duration(10) - .extinf(-5.005, None) // Invalid duration - .uri("https://media.example.com/first.ts".to_string()) - .extinf(5.005, None) - .uri("https://media.example.com/second.ts".to_string()) - .extinf(3.003, None) - .uri("https://media.example.com/third.ts".to_string()) + .extinf("https://media.example.com/first.ts", -5.005, None) // Invalid duration + .extinf("https://media.example.com/second.ts", 5.005, None) + .extinf("https://media.example.com/third.ts", 3.003, None) .end_list() .build(); @@ -548,12 +561,9 @@ https://media.example.com/third.ts .extm3u() .version(3) .target_duration(0) // Invalid target duration - .extinf(5.005, None) - .uri("https://media.example.com/first.ts".to_string()) - .extinf(5.005, None) - .uri("https://media.example.com/second.ts".to_string()) - .extinf(3.003, None) - .uri("https://media.example.com/third.ts".to_string()) + .extinf("https://media.example.com/first.ts", 5.005, None) + .extinf("https://media.example.com/second.ts", 5.005, None) + .extinf("https://media.example.com/third.ts", 3.003, None) .end_list() .build(); @@ -570,18 +580,15 @@ https://media.example.com/third.ts .version(3) .target_duration(10) .key( - "INVALID-METHOD".to_string(), // Invalid key method - Some("https://priv.example.com/key.php?r=52".to_string()), + "INVALID-METHOD", // Invalid key method + Some("https://priv.example.com/key.php?r=52"), None, None, None, ) - .extinf(5.005, None) - .uri("https://media.example.com/first.ts".to_string()) - .extinf(5.005, None) - .uri("https://media.example.com/second.ts".to_string()) - .extinf(3.003, None) - .uri("https://media.example.com/third.ts".to_string()) + .extinf("https://media.example.com/first.ts", 5.005, None) + .extinf("https://media.example.com/second.ts", 5.005, None) + .extinf("https://media.example.com/third.ts", 3.003, None) .end_list() .build(); @@ -599,13 +606,10 @@ https://media.example.com/third.ts .extm3u() .version(3) .target_duration(10) - .map("".to_string(), None) // Invalid map URI - .extinf(5.005, None) - .uri("https://media.example.com/first.ts".to_string()) - .extinf(5.005, None) - .uri("https://media.example.com/second.ts".to_string()) - .extinf(3.003, None) - .uri("https://media.example.com/third.ts".to_string()) + .map("", None) // Invalid map URI + .extinf("https://media.example.com/first.ts", 5.005, None) + .extinf("https://media.example.com/second.ts", 5.005, None) + .extinf("https://media.example.com/third.ts", 3.003, None) .end_list() .build(); @@ -618,51 +622,13 @@ https://media.example.com/third.ts .extm3u() .version(3) .target_duration(10) - .program_date_time("".to_string()) // Invalid program date time - .extinf(5.005, None) - .uri("https://media.example.com/first.ts".to_string()) - .extinf(5.005, None) - .uri("https://media.example.com/second.ts".to_string()) - .extinf(3.003, None) - .uri("https://media.example.com/third.ts".to_string()) + .program_date_time("") // Invalid program date time + .extinf("https://media.example.com/first.ts", 5.005, None) + .extinf("https://media.example.com/second.ts", 5.005, None) + .extinf("https://media.example.com/third.ts", 3.003, None) .end_list() .build(); assert_eq!(playlist, Err(vec![ValidationError::InvalidProgramDateTime])); } - - #[test] - fn test_validate_playlist_invalid_date_range() { - let playlist = PlaylistBuilder::new() - .extm3u() - .version(3) - .target_duration(10) - .date_range( - "".to_string(), // Invalid date range ID - "2020-01-01T00:00:00Z".to_string(), - None, - Some(-60.0), // Invalid date range duration - None, - None, - None, - None, - None, - ) - .extinf(5.005, None) - .uri("https://media.example.com/first.ts".to_string()) - .extinf(5.005, None) - .uri("https://media.example.com/second.ts".to_string()) - .extinf(3.003, None) - .uri("https://media.example.com/third.ts".to_string()) - .end_list() - .build(); - - assert_eq!( - playlist, - Err(vec![ - ValidationError::InvalidDateRangeId, - ValidationError::InvalidDateRangeDuration(-60.0) - ]) - ); - } } diff --git a/src/m3u8/validation.rs b/src/m3u8/validation.rs index cc44cab..7516795 100644 --- a/src/m3u8/validation.rs +++ b/src/m3u8/validation.rs @@ -1,9 +1,8 @@ /// Represents different types of validation errors that can occur when processing an M3U8 playlist. /// -/// This enum is used to capture specific validation issues that may arise when -/// checking the conformity of a playlist to the M3U8 specification. Each variant -/// represents a distinct error that provides context for what went wrong during -/// validation. +/// This enum captures specific validation issues that may arise when checking the conformity +/// of a playlist to the M3U8 specification. Each variant represents a distinct error, providing +/// context for what went wrong during validation. #[derive(Debug, PartialEq)] pub enum ValidationError { /// Error indicating that the #EXTM3U tag is missing from the playlist. @@ -28,14 +27,7 @@ pub enum ValidationError { /// # Arguments /// /// * `u32` - The invalid target duration value that was encountered. - InvalidTargetDuration(u32), - - /// Error indicating that the media sequence number specified is invalid. - /// - /// # Arguments - /// - /// * `u64` - The invalid media sequence number that was encountered. - InvalidMediaSequence(u64), + InvalidTargetDuration(u64), /// Error indicating that an invalid key method was specified. /// @@ -59,17 +51,70 @@ pub enum ValidationError { /// Error indicating that the end date specified in a date range is invalid. InvalidDateRangeEndDate, - /// Error indicating that the duration specified in a date range is invalid. + /// Error indicating that the planned duration specified in a date range is invalid. /// /// # Arguments /// - /// * `f32` - The invalid duration value that was encountered in the date range. - InvalidDateRangeDuration(f32), + /// * `f32` - The invalid planned duration value that was encountered in the date range. + InvalidDateRangePlannedDuration(f32), - /// Error indicating that the planned duration specified in a date range is invalid. + /// Error indicating that the specified byte range is invalid. /// /// # Arguments /// - /// * `f32` - The invalid planned duration value that was encountered in the date range. - InvalidDateRangePlannedDuration(f32), + /// * `String` - The invalid byte range that was encountered. + InvalidByteRange(String), + + /// Error indicating that a media tag is missing required fields. + MissingMediaFields, + + /// Error indicating that a stream information tag is invalid. + /// + /// # Arguments + /// + /// * `String` - The invalid stream information encountered. + InvalidStreamInf(String), + + /// Error indicating that an I-frame stream information tag is invalid. + /// + /// # Arguments + /// + /// * `String` - The invalid I-frame stream information encountered. + InvalidIFrameStreamInf(String), + + /// Error indicating that a part tag is invalid. + /// + /// # Arguments + /// + /// * `String` - The invalid part information encountered. + InvalidPartInfo(String), + + /// Error indicating that a preload hint URI is invalid. + InvalidPreloadHintUri, + + /// Error indicating that a rendition report URI is invalid. + InvalidRenditionReportUri, + + /// Error indicating that the server control information is invalid. + InvalidServerControl, + + /// Error indicating that the specified start time offset is invalid. + InvalidStartTimeOffset, + + /// Error indicating that a skip tag is invalid. + /// + /// # Arguments + /// + /// * `String` - The invalid skip tag information encountered. + InvalidSkipTag(String), + + /// Error indicating that the specified bitrate is invalid. + /// + /// # Arguments + /// + /// * `u32` - The invalid bitrate value that was encountered. + InvalidBitrate(u32), + + /// Error indicating that the specified start offset is invalid. + InvalidStartOffset, }