quick_m3u8/lib.rs
1#![warn(
2 missing_docs,
3 missing_copy_implementations,
4 missing_debug_implementations
5)]
6#![doc(
7 html_favicon_url = "https://raw.githubusercontent.com/theRealRobG/m3u8/refs/heads/main/quick-m3u8-logo.ico"
8)]
9#![doc(
10 html_logo_url = "https://raw.githubusercontent.com/theRealRobG/m3u8/refs/heads/main/quick-m3u8-logo.svg"
11)]
12
13//! # quick-m3u8
14//!
15//! quick-m3u8 aims to be a flexible and highly performant [M3U8] reader and writer. The API is
16//! event-driven (like SAX for XML) rather than serializing into a complete object model (like DOM
17//! for XML). The syntax (and name) is inspired by [quick-xml]. The [`Reader`] attempts to be almost
18//! zero-copy while still supporting mutation of the parsed data by utilizing [`std::borrow::Cow`]
19//! (Copy On Write) as much as possible. The library provides flexibility via support for custom tag
20//! registration ([`tag::CustomTag`]), which gives the user the ability to support parsing of tags
21//! not part of the main HLS specification, or overwrite parsing behavior of tags provided by the
22//! library.
23//!
24//! When parsing M3U8 data quick-m3u8 aims to be very lenient when it comes to validation. The
25//! philosophy is that the library does not want to get in the way of extracting meaningful
26//! information from the input data. If the library took a strict approach to validating adherence
27//! to all of the HLS specification, then there could be cases where a playlist is rejected, but a
28//! client may have accepted it. For example, consider the requirement for segment duration rounding
29//! declared by the [EXT-X-TARGETDURATION] tag:
30//! > The EXTINF duration of each Media Segment in a Playlist file, when rounded to the nearest
31//! > integer, MUST be less than or equal to the Target Duration.
32//!
33//! Despite this requirement, many players will tolerate several long-running segments in a playlist
34//! just fine, so interpreting this rule very strictly could lead to the manifest being rejected
35//! before it even reaches the video player (assuming this library is being used in a server
36//! implementation).
37//!
38//! Therefore, validating the sanity of the parsed values is deliberately left to the user of the
39//! library. Some examples of values that are not validated are:
40//! * URI lines are not validated as being valid URIs (any line that isn't blank and does not start
41//! with `#` is considered to be a URI line).
42//! * Enumerated strings (within [attribute-lists]) are not validated to have no whitespace.
43//! * A tag with a known name that fails the `TryFrom<ParsedTag>` conversion does not fail the line
44//! and instead is presented as [`tag::UnknownTag`].
45//!
46//! With that being said, the library does validate proper UTF-8 conversion from `&[u8]` input,
47//! enumerated strings and enumerated string lists are wrapped in convenience types
48//! ([`tag::hls::EnumeratedString`], [`tag::hls::EnumeratedStringList`]) that expose strongly typed
49//! enumerations when the value is valid, and the `TryFrom<ParsedTag>` implementation for all of the
50//! HLS tags supported by quick-m3u8 ensure that the required attributes are present for each tag.
51//!
52//! # Usage
53//!
54//! Usage is broken up into reading and writing.
55//!
56//! ## Reading
57//!
58//! The main entry point for using the library is the [`Reader`]. This provides an interface for
59//! reading lines from an input data source. For example, consider the [Simple Media Playlist]:
60//! ```
61//! const EXAMPLE_MANIFEST: &str = r#"#EXTM3U
62//! #EXT-X-TARGETDURATION:10
63//! #EXT-X-VERSION:3
64//! #EXTINF:9.009,
65//! first.ts
66//! #EXTINF:9.009,
67//! second.ts
68//! #EXTINF:3.003,
69//! third.ts
70//! #EXT-X-ENDLIST
71//! "#;
72//! ```
73//! We can use the Reader to read information about each line in the playlist as such:
74//! ```
75//! # use quick_m3u8::{
76//! # HlsLine, Reader,
77//! # config::ParsingOptionsBuilder,
78//! # tag::hls::{Endlist, Inf, M3u, Targetduration, Version},
79//! # };
80//! #
81//! # const EXAMPLE_MANIFEST: &str = r#"#EXTM3U
82//! # #EXT-X-TARGETDURATION:10
83//! # #EXT-X-VERSION:3
84//! # #EXTINF:9.009,
85//! # first.ts
86//! # #EXTINF:9.009,
87//! # second.ts
88//! # #EXTINF:3.003,
89//! # third.ts
90//! # #EXT-X-ENDLIST
91//! # "#;
92//! let mut reader = Reader::from_str(
93//! EXAMPLE_MANIFEST,
94//! ParsingOptionsBuilder::new()
95//! .with_parsing_for_all_tags()
96//! .build(),
97//! );
98//! assert_eq!(reader.read_line(), Ok(Some(HlsLine::from(M3u))));
99//! assert_eq!(reader.read_line(), Ok(Some(HlsLine::from(Targetduration::new(10)))));
100//! assert_eq!(reader.read_line(), Ok(Some(HlsLine::from(Version::new(3)))));
101//! assert_eq!(reader.read_line(), Ok(Some(HlsLine::from(Inf::new(9.009, "")))));
102//! assert_eq!(reader.read_line(), Ok(Some(HlsLine::uri("first.ts"))));
103//! assert_eq!(reader.read_line(), Ok(Some(HlsLine::from(Inf::new(9.009, "")))));
104//! assert_eq!(reader.read_line(), Ok(Some(HlsLine::uri("second.ts"))));
105//! assert_eq!(reader.read_line(), Ok(Some(HlsLine::from(Inf::new(3.003, "")))));
106//! assert_eq!(reader.read_line(), Ok(Some(HlsLine::uri("third.ts"))));
107//! assert_eq!(reader.read_line(), Ok(Some(HlsLine::from(Endlist))));
108//! assert_eq!(reader.read_line(), Ok(None));
109//! ```
110//!
111//! The example is basic but demonstrates a few points already. Firstly, `HlsLine` (the result of
112//! `read_line`) is an `enum`, which is in line with [Section 4.1] that states:
113//! > Each line is a URI, is blank, or starts with the character '#'. Lines that start with the
114//! > character '#' are either comments or tags. Tags begin with #EXT.
115//!
116//! Each case of the [`HlsLine`] is documented thoroughly; however, it's worth mentioning that in
117//! addition to what the HLS specification defines, the library also allows for `UnknownTag` (which
118//! is a tag, based on the `#EXT` prefix, but not one that we know about), and also allows
119//! `CustomTag`. The [`tag::CustomTag`] is a means for the user of the library to define support for
120//! their own custom tag specification in addition to what is provided via the HLS specification.
121//! The documentation for `CustomTag` provides more details on how that is achieved.
122//!
123//! The `Reader` also takes a configuration that allows the user to select what HLS tags the reader
124//! should parse. [`config::ParsingOptions`] provides more details, but in short, better performance
125//! can be squeezed out by only parsing the tags that you need.
126//!
127//! ## Writing
128//!
129//! The other component to quick-m3u8 is [`Writer`]. This allows the user to write to a given
130//! [`std::io::Write`] the parsed (or constructed) HLS lines. It should be noted that when writing,
131//! if no mutation of a tag has occurred, then the original reference slice of the line will be
132//! used. This allows us to avoid unnecessary allocations.
133//!
134//! A common use-case for reading and then writing is to modify a HLS playlist, perhaps in transit,
135//! in a proxy layer. Below is a toy example; however, the repo benchmark demonstrates a more
136//! complex example of how one may implement a HLS delta update (acting as a proxy layer).
137//! ```
138//! # use quick_m3u8::{
139//! # HlsLine, Reader, Writer,
140//! # config::ParsingOptions,
141//! # tag::{hls, KnownTag},
142//! # };
143//! # use std::io;
144//! let input_lines = concat!(
145//! "#EXTINF:4.00008,\n",
146//! "fileSequence268.mp4\n",
147//! "#EXTINF:4.00008,\n",
148//! "fileSequence269.mp4\n",
149//! );
150//! let mut reader = Reader::from_str(input_lines, ParsingOptions::default());
151//! let mut writer = Writer::new(Vec::new());
152//!
153//! let mut added_hello = false;
154//! while let Ok(Some(line)) = reader.read_line() {
155//! match line {
156//! HlsLine::KnownTag(KnownTag::Hls(hls::Tag::Inf(mut inf))) => {
157//! if added_hello {
158//! inf.set_title(" World!");
159//! } else {
160//! inf.set_title(" Hello,");
161//! added_hello = true;
162//! }
163//! writer.write_line(HlsLine::from(inf))?
164//! }
165//! line => writer.write_line(line)?,
166//! };
167//! }
168//!
169//! let expected_output_lines = concat!(
170//! "#EXTINF:4.00008, Hello,\n",
171//! "fileSequence268.mp4\n",
172//! "#EXTINF:4.00008, World!\n",
173//! "fileSequence269.mp4\n",
174//! );
175//! assert_eq!(
176//! expected_output_lines,
177//! String::from_utf8_lossy(&writer.into_inner())
178//! );
179//! # Ok::<(), io::Error>(())
180//! ```
181//!
182//! [M3U8]: https://datatracker.ietf.org/doc/draft-pantos-hls-rfc8216bis/
183//! [quick-xml]: https://crates.io/crates/quick-xml
184//! [EXT-X-TARGETDURATION]: https://datatracker.ietf.org/doc/html/draft-pantos-hls-rfc8216bis-18#section-4.4.3.1
185//! [attribute-lists]: https://datatracker.ietf.org/doc/html/draft-pantos-hls-rfc8216bis-18#section-4.2
186//! [Simple Media Playlist]: https://datatracker.ietf.org/doc/html/draft-pantos-hls-rfc8216bis-18#section-9.1
187//! [Section 4.1]: https://datatracker.ietf.org/doc/html/draft-pantos-hls-rfc8216bis-18#section-4.1
188
189pub mod config;
190pub mod date;
191pub mod error;
192mod line;
193mod reader;
194mod tag_internal;
195mod utils;
196mod writer;
197
198pub mod custom_parsing {
199 //! Methods and types used by the library for parsing M3U8.
200 //!
201 //! The preferred usage of the library is to rely on [`crate::Reader`]; however, these methods
202 //! are exposed to allow for more flexibility in parsing where necessary. The `Reader`
203 //! implementation provided by this library uses these methods internally.
204 pub use crate::line::{ParsedByteSlice, ParsedLineSlice};
205 pub mod line {
206 //! Methods for parsing a single HLS line.
207 //!
208 //! These methods provide responses that include what is parsed and what is remaining from
209 //! the line.
210 pub use crate::line::{parse, parse_bytes, parse_bytes_with_custom, parse_with_custom};
211 }
212 pub mod tag {
213 //! Method for parsing an unknown tag.
214 pub use crate::tag_internal::unknown::parse;
215 }
216}
217
218pub mod tag {
219 //! Container module for all HLS tag related modules, types, and methods.
220 pub use crate::tag_internal::hls;
221 pub use crate::tag_internal::{known::*, unknown::UnknownTag, value::*};
222}
223
224pub use line::HlsLine;
225pub use reader::Reader;
226pub use writer::Writer;
227
228// This allows the Rust compiler to validate any Rust snippets in my README, which seems like a very
229// cool trick. I saw this technique in clap-rs/clap, for example:
230// https://github.com/clap-rs/clap/blob/4d7ab1483cd0f0849668d274aa2fb6358872eca9/clap_complete_nushell/src/lib.rs#L239-L241
231#[doc = include_str!("../README.md")]
232#[cfg(doctest)]
233pub struct ReadmeDoctests;