m3u8_reader/
multi_variant.rs

1use crate::error::{Context as _, Result};
2use memchr::memchr;
3use std::str;
4
5/// Master playlist that lists multiple variant streams of the same content
6#[derive(Debug)]
7pub struct MultiVariantPlaylist {
8	/// These lines define the variant streams.
9	/// Each line represents a different version of the same content, encoded at different bitrates and resolutions.
10	/// This allows the player to dynamically switch between streams based on the user's network conditions, a feature known as Adaptive Bitrate Streaming (ABR)
11	pub variant_streams: Vec<VariantStream>,
12	/// These lines provide information about the I-frame streams.
13	/// I-frames are keyframes in the video that contain the complete image information.
14	/// These streams allow for faster seeking and trick play.
15	pub frame_streams: Vec<FrameStream>,
16}
17
18#[derive(Debug)]
19pub struct VariantStream {
20	/// Identifies the program or content.
21	pub program_id: Option<u8>,
22	/// The average bitrate of the stream in bits per second.
23	pub bandwidth: u32,
24	/// The resolution of the video (e.g., 1440x1080).
25	pub resolution: (u16, u16),
26	/// The frame rate of the video.
27	pub frame_rate: Option<f32>,
28	/// Specifies the codecs used for the audio and video streams.
29	pub codecs: Option<String>,
30	/// The URI of the m3u8 file containing the media segments for this variant.
31	pub uri: String,
32}
33
34#[derive(Debug)]
35pub struct FrameStream {
36	/// The average bitrate of the I-frame stream.
37	pub bandwidth: u32,
38	/// The resolution of the I-frame stream (e.g., 1440x1080).
39	pub resolution: (u16, u16),
40	/// Specifies the codecs used for the I-frame stream.
41	pub codecs: String,
42	/// The URI of the m3u8 file containing the I-frames for this variant.
43	pub uri: String,
44}
45
46pub fn parse(bytes: &[u8]) -> Result<MultiVariantPlaylist> {
47	let mut variant_streams = Vec::new();
48	let mut frame_streams = Vec::new();
49
50	let mut position = 0;
51	while position < bytes.len() {
52		let newline_pos =
53			memchr(b'\n', &bytes[position..]).unwrap_or(bytes.len() - position) + position;
54		let line = &bytes[position..newline_pos];
55
56		if line.starts_with(b"#EXT-X-STREAM-INF") {
57			variant_streams.push(parse_variant_stream(line)?);
58		} else if line.ends_with(b".m3u8\r") {
59			if let Some(last_stream) = variant_streams.last_mut() {
60				last_stream.uri = str::from_utf8(line)?.trim().to_string();
61			}
62		} else if line.starts_with(b"#EXT-X-I-FRAME-STREAM-INF") {
63			frame_streams.push(parse_frame_stream(line)?);
64		}
65
66		position = newline_pos + 1;
67	}
68
69	Ok(MultiVariantPlaylist {
70		variant_streams,
71		frame_streams,
72	})
73}
74
75fn parse_variant_stream(line: &[u8]) -> Result<VariantStream> {
76	let mut in_quotes = false;
77	let mut attributes = Vec::new();
78	let mut current_attribute = Vec::new();
79
80	for &byte in &line[18..] {
81		if byte == b'"' {
82			in_quotes = !in_quotes;
83			current_attribute.push(byte);
84		} else if byte == b',' && !in_quotes {
85			attributes.push(current_attribute.clone());
86			current_attribute.clear();
87		} else {
88			current_attribute.push(byte);
89		}
90	}
91	attributes.push(current_attribute);
92
93	let mut stream = VariantStream {
94		program_id: None,
95		bandwidth: 0,
96		resolution: (0, 0),
97		frame_rate: None,
98		codecs: None,
99		uri: String::new(),
100	};
101
102	for attribute in attributes {
103		let mut parts = attribute.splitn(2, |&byte| byte == b'=');
104		let key = parts.next().context("Missing key")?;
105		let value = parts.next().context("Missing value")?;
106
107		match key {
108			b"PROGRAM-ID" => stream.program_id = str::from_utf8(value)?.parse().ok(),
109			b"BANDWIDTH" => stream.bandwidth = str::from_utf8(value)?.parse()?,
110			b"RESOLUTION" => {
111				let mut res_parts = value.trim_ascii().splitn(2, |&byte| byte == b'x');
112				stream.resolution.0 =
113					str::from_utf8(res_parts.next().context("Missing width")?)?.parse()?;
114				stream.resolution.1 =
115					str::from_utf8(res_parts.next().context("Missing height")?)?.parse()?;
116			}
117			b"FRAME-RATE" => stream.frame_rate = str::from_utf8(value)?.parse().ok(),
118			b"CODECS" => {
119				stream.codecs = Some(str::from_utf8(value)?.replace('\"', "").trim().to_string())
120			}
121			_ => eprintln!(
122				"Unknown attribute: {}",
123				str::from_utf8(key).unwrap_or("Invalid UTF-8")
124			),
125		}
126	}
127
128	Ok(stream)
129}
130
131fn parse_frame_stream(line: &[u8]) -> Result<FrameStream> {
132	let attributes = line[26..].split(|&byte| byte == b',').collect::<Vec<_>>();
133	let mut stream = FrameStream {
134		bandwidth: 0,
135		resolution: (0, 0),
136		codecs: String::new(),
137		uri: String::new(),
138	};
139
140	for attribute in attributes {
141		let mut parts = attribute.splitn(2, |&byte| byte == b'=');
142		let key = parts.next().context("Missing key")?;
143		let value = parts.next().context("Missing value")?;
144
145		match key {
146			b"BANDWIDTH" => stream.bandwidth = str::from_utf8(value)?.parse()?,
147			b"RESOLUTION" => {
148				let mut res_parts = value.trim_ascii().splitn(2, |&byte| byte == b'x');
149				stream.resolution.0 =
150					str::from_utf8(res_parts.next().context("Missing width")?)?.parse()?;
151				stream.resolution.1 =
152					str::from_utf8(res_parts.next().context("Missing height")?)?.parse()?;
153			}
154			b"CODECS" => {
155				stream.codecs = str::from_utf8(value)?.replace('\"', "").trim().to_string()
156			}
157			b"URI" => stream.uri = str::from_utf8(value)?.replace('\"', "").trim().to_string(),
158			_ => eprintln!(
159				"Unknown attribute: {}",
160				str::from_utf8(key).unwrap_or("Invalid UTF-8")
161			),
162		}
163	}
164
165	Ok(stream)
166}