m3u8_reader/
media_playlist.rs

1use std::ops::Range;
2
3use crate::{
4	bail,
5	error::{Context as _, Error, Result},
6};
7use logos::Logos;
8
9#[derive(Logos, Debug, PartialEq)]
10#[logos(skip r"[ \t\n\f]+")]
11#[logos(error = String)]
12pub enum Token<'a> {
13	#[token("#EXTM3U")]
14	ExtM3U,
15	#[token("#EXT-X-ENDLIST")]
16	EndList,
17
18	#[token("#EXT-X-TARGETDURATION")]
19	TargetDuration,
20	#[token("#EXT-X-VERSION")]
21	Version,
22	#[token("#EXT-X-MEDIA-SEQUENCE")]
23	MediaSequence,
24	#[token("#EXT-X-KEY")]
25	Key,
26	#[token("#EXT-X-ALLOW-CACHE")]
27	AllowCache,
28	#[token("#EXT-X-PLAYLIST-TYPE")]
29	PlaylistType,
30	#[token("#EXT-X-I-FRAMES-ONLY")]
31	IFramesOnly,
32
33	#[token("#EXTINF")]
34	Inf,
35	#[token("#EXT-X-BYTERANGE")]
36	ByteRange,
37
38	#[token("METHOD")]
39	Method,
40	#[token("URI")]
41	Uri,
42
43	#[token("=")]
44	Equal,
45	#[token(",")]
46	Comma,
47	#[token(":")]
48	Colon,
49
50	#[regex(r"[0-9]+\.[0-9]+", |lex| lexical::parse(lex.slice()).ok())]
51	Float(f64),
52	#[regex(r"[0-9]+", |lex| lexical::parse(lex.slice()).ok())]
53	Integer(usize),
54	#[regex(r#""([^"]*)""#, |lex| lex.slice()[1..lex.slice().len() - 1].as_ref())]
55	String(&'a str),
56
57	#[regex(r"AES-128|SAMPLE-AES|NONE", |lex| match lex.slice() {
58		"AES-128" => Method::Aes128,
59		"SAMPLE-AES" => Method::SampleAes,
60		"NONE" => Method::None,
61		_ => unreachable!(),
62	})]
63	MethodValue(Method),
64	#[regex(r"YES|NO", |lex| match lex.slice() {
65		"YES" => true,
66		"NO" => false,
67		_ => unreachable!(),
68	})]
69	AllowCacheValue(bool),
70	#[regex(r"[0-9]+@[0-9]+", |lex| {
71		let mut parts = lex.slice().split('@');
72		let length: usize = lexical::parse(parts.next().unwrap()).unwrap();
73		let offset: usize = lexical::parse(parts.next().unwrap()).unwrap();
74		length..offset
75	})]
76	ByteRangeValue(Range<usize>),
77	#[regex(r"VOD|EVENT", |lex| match lex.slice() {
78		"VOD" => PlaylistType::Vod,
79		"EVENT" => PlaylistType::Event,
80		_ => unreachable!(),
81	})]
82	PlaylistTypeValue(PlaylistType),
83	#[regex(r"https?://[^ \t\n\f]+", |lex| lex.slice())]
84	UriValue(&'a str),
85}
86
87#[derive(Debug, PartialEq)]
88pub struct MediaPlaylist {
89	pub version: u8,
90	pub media_sequence: u32,
91	pub key: Option<Key>,
92	pub allow_cache: bool,
93	pub target_duration: u32,
94	pub playlist_type: PlaylistType,
95	pub iframes_only: bool,
96	pub segments: Vec<MediaSegment>,
97}
98
99#[derive(Debug, PartialEq)]
100pub struct Key {
101	pub method: Method,
102	pub uri: String,
103}
104
105#[derive(Debug, PartialEq)]
106pub enum Method {
107	Aes128,
108	SampleAes,
109	None,
110}
111
112#[derive(Debug, PartialEq)]
113pub enum PlaylistType {
114	Vod,
115	Event,
116}
117
118#[derive(Debug, PartialEq)]
119pub struct MediaSegment {
120	pub duration: f32,
121	pub byte_range: Option<Range<usize>>,
122	pub url: String,
123}
124
125pub fn parse(input: &str) -> Result<MediaPlaylist> {
126	let mut lexer = Token::lexer(input);
127	let mut playlist = MediaPlaylist {
128		version: 0,
129		media_sequence: 0,
130		key: None,
131		allow_cache: false,
132		target_duration: 0,
133		playlist_type: PlaylistType::Vod,
134		iframes_only: false,
135		segments: Vec::new(),
136	};
137
138	while let Some(token) = lexer.next() {
139		match token? {
140			Token::ExtM3U => (),
141			Token::Version => {
142				playlist.version = match lexer.nth(1).context("Invalid version")?? {
143					Token::Integer(version) => version as u8,
144					_ => bail!("Invalid version"),
145				};
146			}
147			Token::MediaSequence => {
148				playlist.media_sequence = match lexer.nth(1).context("Invalid media sequence")?? {
149					Token::Integer(sequence) => sequence as u32,
150					_ => bail!("Invalid media sequence"),
151				};
152			}
153			Token::Key => {
154				let method = match lexer.nth(3).context("Invalid method")?? {
155					Token::MethodValue(method) => method,
156					_ => bail!("Invalid method"),
157				};
158				let uri = match lexer.nth(3).context("Invalid URI")?? {
159					Token::String(uri) => uri.to_string(),
160					_ => bail!("Invalid key URL"),
161				};
162				playlist.key = Some(Key { method, uri });
163			}
164			Token::AllowCache => {
165				playlist.allow_cache = match lexer.nth(1).context("Invalid allow cache")?? {
166					Token::AllowCacheValue(allow_cache) => allow_cache,
167					_ => bail!("Invalid allow cache"),
168				};
169			}
170			Token::TargetDuration => {
171				playlist.target_duration =
172					match lexer.nth(1).context("Invalid target duration")?? {
173						Token::Integer(duration) => duration as u32,
174						_ => bail!("Invalid target duration"),
175					};
176			}
177			Token::PlaylistType => {
178				playlist.playlist_type = match lexer.nth(1).context("Invalid playlist type")?? {
179					Token::PlaylistTypeValue(playlist_type) => playlist_type,
180					_ => bail!("Invalid playlist type"),
181				};
182			}
183			Token::IFramesOnly => {
184				playlist.iframes_only = true;
185			}
186			Token::Inf => {
187				let duration = match lexer.nth(1).context("Invalid duration")?? {
188					Token::Float(duration) => duration as f32,
189					_ => bail!("Invalid duration"),
190				};
191
192				// let byte_range = match lexer.find(|token| matches!(token, Ok(Token::ByteRange))) {
193				// 	Some(Ok(Token::ByteRange)) => {
194				// 		lexer.next(); // Consume ByteRange token
195				// 		match lexer.next().context("Invalid byte range value")?? {
196				// 			Token::ByteRangeValue(range) => Some(range),
197				// 			_ => None,
198				// 		}
199				// 	}
200				// 	_ => None,
201				// };
202
203				let byte_range = playlist
204					.iframes_only
205					.then(|| match lexer.nth(3) {
206						Some(Ok(Token::ByteRangeValue(range))) => Some(range),
207						_ => None,
208					})
209					.flatten();
210
211				let url_advance = if byte_range.is_some() { 0 } else { 1 };
212				let url = match lexer.nth(url_advance).context("Invalid URL")?? {
213					Token::UriValue(uri) => uri.to_string(),
214					_ => bail!("Invalid URL"),
215				};
216				playlist.segments.push(MediaSegment {
217					duration,
218					byte_range,
219					url,
220				});
221			}
222			Token::EndList => break,
223			_ => (),
224		}
225	}
226
227	Ok(playlist)
228}
229
230#[cfg(test)]
231mod tests {
232	use super::*;
233
234	#[test]
235	fn test_media_playlist() {
236		let input = r#"
237			#EXTM3U
238			#EXT-X-TARGETDURATION:17
239			#EXT-X-ALLOW-CACHE:YES
240			#EXT-X-PLAYLIST-TYPE:VOD
241			#EXT-X-KEY:METHOD=AES-128,URI="https://example.com/mon.key"
242			#EXT-X-VERSION:3
243			#EXT-X-MEDIA-SEQUENCE:1
244			#EXTINF:6.006,
245			https://example.com/segment-1.ts
246			#EXTINF:4.588,
247			https://example.com/segment-2.ts
248			#EXT-X-ENDLIST
249		"#;
250
251		let media_playlist = parse(input).unwrap();
252		assert_eq!(
253			media_playlist,
254			MediaPlaylist {
255				version: 3,
256				media_sequence: 1,
257				key: Some(Key {
258					method: Method::Aes128,
259					uri: "https://example.com/mon.key".to_string(),
260				}),
261				allow_cache: true,
262				target_duration: 17,
263				playlist_type: PlaylistType::Vod,
264				iframes_only: false,
265				segments: vec![
266					MediaSegment {
267						duration: 6.006,
268						byte_range: None,
269						url: "https://example.com/segment-1.ts".to_string(),
270					},
271					MediaSegment {
272						duration: 4.588,
273						byte_range: None,
274						url: "https://example.com/segment-2.ts".to_string(),
275					},
276				],
277			}
278		)
279	}
280
281	#[allow(clippy::reversed_empty_ranges)]
282	#[test]
283	fn test_media_playlist_iframes() {
284		let input = r#"
285			#EXTM3U
286			#EXT-X-TARGETDURATION:3
287			#EXT-X-VERSION:4
288			#EXT-X-MEDIA-SEQUENCE:1
289			#EXT-X-PLAYLIST-TYPE:VOD
290			#EXT-X-I-FRAMES-ONLY
291			#EXTINF:1.120,
292			#EXT-X-BYTERANGE:1316@376
293			https://example.com/segment-1.ts
294			#EXTINF:6.720,
295			#EXT-X-BYTERANGE:44744@7896
296			https://example.com/segment-2.ts
297			#EXT-X-ENDLIST
298		"#;
299
300		let media_playlist = parse(input).unwrap();
301		assert_eq!(
302			media_playlist,
303			MediaPlaylist {
304				version: 4,
305				media_sequence: 1,
306				key: None,
307				allow_cache: false,
308				target_duration: 3,
309				playlist_type: PlaylistType::Vod,
310				iframes_only: true,
311				segments: vec![
312					MediaSegment {
313						duration: 1.12,
314						byte_range: Some(1316..376),
315						url: "https://example.com/segment-1.ts".to_string(),
316					},
317					MediaSegment {
318						duration: 6.72,
319						byte_range: Some(44744..7896),
320						url: "https://example.com/segment-2.ts".to_string(),
321					},
322				],
323			}
324		)
325	}
326}