m3u8_reader/
multi_variant.rs

1use crate::{
2	bail,
3	error::{Context as _, Error, Result},
4};
5use logos::Logos;
6
7#[derive(Logos, Debug, PartialEq)]
8#[logos(skip r"[ \t\n\f]+")]
9#[logos(error = String)]
10pub enum Token<'a> {
11	#[token("#EXTM3U")]
12	ExtM3U,
13	#[token("#EXT-X-STREAM-INF")]
14	StreamInf,
15	#[token("#EXT-X-I-FRAME-STREAM-INF")]
16	IFrameStreamInf,
17
18	#[token("PROGRAM-ID")]
19	ProgramId,
20	#[token("BANDWIDTH")]
21	Bandwidth,
22	#[token("RESOLUTION")]
23	Resolution,
24	#[token("FRAME-RATE")]
25	FrameRate,
26	#[token("CODECS")]
27	Codecs,
28	#[token("URI")]
29	Uri,
30
31	#[token("=")]
32	Equal,
33	#[token(",")]
34	Comma,
35	#[token(":")]
36	Colon,
37
38	#[regex(r"[0-9]+\.[0-9]+", |lex| lexical::parse(lex.slice()).ok())]
39	Float(f64),
40	#[regex(r"[0-9]+", |lex| lexical::parse(lex.slice()).ok())]
41	Integer(usize),
42	#[regex(r#""([^"]*)""#, |lex| lex.slice()[1..lex.slice().len() - 1].as_ref())]
43	String(&'a str),
44
45	#[regex(r"[0-9]+x[0-9]+", |lex| {
46		let mut parts = lex.slice().split('x');
47		let width = parts.next().unwrap().parse().unwrap();
48		let height = parts.next().unwrap().parse().unwrap();
49		(width, height)
50	})]
51	ResolutionValue((usize, usize)),
52	#[regex(r"[a-zA-Z0-9\-_]+\.m3u8")]
53	UriValue(&'a str),
54}
55
56#[derive(Debug, PartialEq)]
57pub struct MultiVariantPlaylist {
58	/// These lines define the variant streams.
59	/// Each line represents a different version of the same content, encoded at different bitrates and resolutions.
60	/// This allows the player to dynamically switch between streams based on the user's network conditions, a feature known as Adaptive Bitrate Streaming (ABR)
61	pub variant_streams: Vec<VariantStream>,
62	/// These lines provide information about the I-frame streams.
63	/// I-frames are keyframes in the video that contain the complete image information.
64	/// These streams allow for faster seeking and trick play.
65	pub frame_streams: Vec<FrameStream>,
66}
67
68#[derive(Debug, PartialEq)]
69pub struct VariantStream {
70	/// Identifies the program or content.
71	pub program_id: Option<u8>,
72	/// The average bitrate of the stream in bits per second.
73	pub bandwidth: u32,
74	/// The resolution of the video (e.g., 1440x1080).
75	pub resolution: (u16, u16),
76	/// The frame rate of the video.
77	pub frame_rate: Option<f32>,
78	/// Specifies the codecs used for the audio and video streams.
79	pub codecs: Option<String>,
80	/// The URI of the m3u8 file containing the media segments for this variant.
81	pub uri: String,
82}
83
84#[derive(Debug, PartialEq)]
85pub struct FrameStream {
86	/// The average bitrate of the I-frame stream.
87	pub bandwidth: u32,
88	/// The resolution of the I-frame stream (e.g., 1440x1080).
89	pub resolution: (u16, u16),
90	/// Specifies the codecs used for the I-frame stream.
91	pub codecs: String,
92	/// The URI of the m3u8 file containing the I-frames for this variant.
93	pub uri: String,
94}
95
96pub fn parse(input: &str) -> Result<MultiVariantPlaylist> {
97	let mut lexer = Token::lexer(input);
98	let mut variant_streams = Vec::new();
99	let mut frame_streams = Vec::new();
100
101	while let Some(token) = lexer.next() {
102		match token? {
103			Token::ExtM3U => (),
104			Token::StreamInf => {
105				variant_streams.push(parse_variant_stream(&mut lexer)?);
106			}
107			Token::IFrameStreamInf => {
108				frame_streams.push(parse_frame_stream(&mut lexer)?);
109			}
110			_ => (),
111		}
112	}
113
114	Ok(MultiVariantPlaylist {
115		variant_streams,
116		frame_streams,
117	})
118}
119
120fn parse_variant_stream<'a>(lexer: &mut logos::Lexer<'a, Token<'a>>) -> Result<VariantStream> {
121	let mut program_id = None;
122	let mut bandwidth = 0;
123	let mut resolution = (0, 0);
124	let mut frame_rate = None;
125	let mut codecs = None;
126	let mut uri = String::new();
127
128	while let Some(token) = lexer.next() {
129		match token? {
130			Token::Colon => (),
131			Token::Equal => (),
132			Token::Comma => (),
133			Token::ProgramId => {
134				program_id = match lexer.nth(1).context("program id")?? {
135					Token::Integer(value) => Some(value as u8),
136					_ => bail!("Invalid program id"),
137				};
138			}
139			Token::Bandwidth => {
140				bandwidth = match lexer.nth(1).context("bandwidth")?? {
141					Token::Integer(value) => value as u32,
142					_ => bail!("Invalid bandwidth"),
143				};
144			}
145			Token::Resolution => {
146				resolution = match lexer.nth(1).context("resolution")?? {
147					Token::ResolutionValue(res) => (res.0 as u16, res.1 as u16),
148					_ => bail!("Invalid resolution"),
149				};
150			}
151			Token::FrameRate => {
152				frame_rate = match lexer.nth(1).context("frame rate")?? {
153					Token::Float(rate) => Some(rate as f32),
154					_ => bail!("Invalid frame rate"),
155				};
156			}
157			Token::Codecs => {
158				codecs = Some(match lexer.nth(1).context("codecs")?? {
159					Token::String(codec) => codec.to_string(),
160					_ => bail!("Invalid codecs"),
161				});
162			}
163			Token::UriValue(value) => {
164				uri = value.to_string();
165				break;
166			}
167			_ => bail!("Invalid variant stream"),
168		}
169	}
170
171	Ok(VariantStream {
172		program_id,
173		bandwidth,
174		resolution,
175		frame_rate,
176		codecs,
177		uri,
178	})
179}
180
181fn parse_frame_stream<'a>(lexer: &mut logos::Lexer<'a, Token<'a>>) -> Result<FrameStream> {
182	let mut bandwidth = 0;
183	let mut resolution = (0, 0);
184	let mut codecs = String::new();
185	let mut uri = String::new();
186
187	while let Some(token) = lexer.next() {
188		match token? {
189			Token::Colon => (),
190			Token::Equal => (),
191			Token::Comma => (),
192			Token::Bandwidth => {
193				bandwidth = match lexer.nth(1).context("bandwidth")?? {
194					Token::Integer(value) => value as u32,
195					_ => bail!("Invalid bandwidth"),
196				};
197			}
198			Token::Resolution => {
199				resolution = match lexer.nth(1).context("resolution")?? {
200					Token::ResolutionValue(res) => (res.0 as u16, res.1 as u16),
201					_ => bail!("Invalid resolution"),
202				};
203			}
204			Token::Codecs => {
205				codecs = match lexer.nth(1).context("codecs")?? {
206					Token::String(codec) => codec.to_string(),
207					_ => bail!("Invalid codecs"),
208				};
209			}
210			Token::Uri => {
211				uri = match lexer.nth(1).context("uri")?? {
212					Token::String(uri) => uri.to_string(),
213					_ => bail!("Invalid uri"),
214				};
215				break;
216			}
217			_ => bail!("Invalid frame stream"),
218		}
219	}
220
221	Ok(FrameStream {
222		bandwidth,
223		resolution,
224		codecs,
225		uri,
226	})
227}
228
229#[test]
230fn test_variant_stream_token() {
231	let input = "
232		#EXTM3U
233		#EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=2553505,RESOLUTION=1920x1080,FRAME-RATE=25.000,CODECS=\"avc1.640032,mp4a.40.2\"
234		index-f1-v1-a1.m3u8
235		#EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=1420969,RESOLUTION=1280x720,FRAME-RATE=25.000,CODECS=\"avc1.64001f,mp4a.40.2\"
236		index-f2-v1-a1.m3u8
237		#EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=641061,RESOLUTION=640x360,FRAME-RATE=25.000,CODECS=\"avc1.64001e,mp4a.40.2\"
238		index-f3-v1-a1.m3u8
239
240		#EXT-X-I-FRAME-STREAM-INF:BANDWIDTH=217533,RESOLUTION=1920x1080,CODECS=\"avc1.640032\",URI=\"iframes-f1-v1-a1.m3u8\"
241		#EXT-X-I-FRAME-STREAM-INF:BANDWIDTH=140609,RESOLUTION=1280x720,CODECS=\"avc1.64001f\",URI=\"iframes-f2-v1-a1.m3u8\"
242		#EXT-X-I-FRAME-STREAM-INF:BANDWIDTH=58096,RESOLUTION=640x360,CODECS=\"avc1.64001e\",URI=\"iframes-f3-v1-a1.m3u8\"
243	";
244
245	let multi_variant_playlist = parse(input).unwrap();
246	assert_eq!(
247		multi_variant_playlist,
248		MultiVariantPlaylist {
249			variant_streams: vec![
250				VariantStream {
251					program_id: Some(1),
252					bandwidth: 2553505,
253					resolution: (1920, 1080),
254					frame_rate: Some(25.0),
255					codecs: Some("avc1.640032,mp4a.40.2".to_string()),
256					uri: "index-f1-v1-a1.m3u8".to_string(),
257				},
258				VariantStream {
259					program_id: Some(1),
260					bandwidth: 1420969,
261					resolution: (1280, 720),
262					frame_rate: Some(25.0),
263					codecs: Some("avc1.64001f,mp4a.40.2".to_string()),
264					uri: "index-f2-v1-a1.m3u8".to_string(),
265				},
266				VariantStream {
267					program_id: Some(1),
268					bandwidth: 641061,
269					resolution: (640, 360),
270					frame_rate: Some(25.0),
271					codecs: Some("avc1.64001e,mp4a.40.2".to_string()),
272					uri: "index-f3-v1-a1.m3u8".to_string(),
273				},
274			],
275			frame_streams: vec![
276				FrameStream {
277					bandwidth: 217533,
278					resolution: (1920, 1080),
279					codecs: "avc1.640032".to_string(),
280					uri: "iframes-f1-v1-a1.m3u8".to_string(),
281				},
282				FrameStream {
283					bandwidth: 140609,
284					resolution: (1280, 720),
285					codecs: "avc1.64001f".to_string(),
286					uri: "iframes-f2-v1-a1.m3u8".to_string(),
287				},
288				FrameStream {
289					bandwidth: 58096,
290					resolution: (640, 360),
291					codecs: "avc1.64001e".to_string(),
292					uri: "iframes-f3-v1-a1.m3u8".to_string(),
293				},
294			],
295		}
296	);
297}