quick_m3u8/writer.rs
1use crate::{
2 line::HlsLine,
3 tag::{IntoInnerTag, WritableCustomTag},
4};
5use std::{
6 borrow::Cow,
7 io::{self, Write},
8};
9
10/// A writer of HLS lines.
11///
12/// This structure wraps a [`Write`] with methods that make writing parsed (or user constructed) HLS
13/// lines easier. The `Writer` handles inserting new lines where necessary and formatting for tags.
14/// An important note to make, is that with every tag implementation within [`crate::tag::hls`], the
15/// reference to the original input data is used directly when writing. This means that we avoid
16/// unnecessary allocations unless the data has been mutated. The same is true of
17/// [`crate::tag::KnownTag::Custom`] tags (described in [`crate::tag::CustomTagAccess`]). Where
18/// necessary, the inner [`Write`] can be accessed in any type of ownership semantics (owned via
19/// [`Self::into_inner`], mutable borrow via [`Self::get_mut`], borrow via [`Self::get_ref`]).
20///
21/// ## Mutate data as proxy
22///
23/// A common use case for using `Writer` is when implementing a proxy service for a HLS stream that
24/// modifies the playlist. In that case, the [`crate::Reader`] is used to extract information from
25/// the upstream bytes, the various tag types can be used to modify the data where necessary, and
26/// the `Writer` is then used to write the result to data for the body of the HTTP response. Below
27/// we provide a toy example of this (for a more interesting example, the repository includes an
28/// implementation of a HLS delta update in `benches/delta_update_bench.rs`).
29/// ```
30/// # use quick_m3u8::{config::ParsingOptions, HlsLine, tag::{hls, KnownTag}, Reader, Writer};
31/// # use std::io::{self, Write};
32/// const INPUT: &str = r#"
33/// #EXTINF:4
34/// segment_100.mp4
35/// #EXTINF:4
36/// segment_101.mp4
37/// "#;
38///
39/// let mut reader = Reader::from_str(INPUT, ParsingOptions::default());
40/// let mut writer = Writer::new(Vec::new());
41///
42/// let mut added_hello = false;
43/// loop {
44/// match reader.read_line() {
45/// // In this branch we match the #EXTINF tag and update the title property to add a
46/// // message.
47/// Ok(Some(HlsLine::KnownTag(KnownTag::Hls(hls::Tag::Inf(mut tag))))) => {
48/// if added_hello {
49/// tag.set_title("World!");
50/// } else {
51/// tag.set_title("Hello,");
52/// added_hello = true;
53/// }
54/// writer.write_line(HlsLine::from(tag))?;
55/// }
56/// // For all other lines we just write out what we received as input.
57/// Ok(Some(line)) => {
58/// writer.write_line(line)?;
59/// }
60/// // When we encounter `Ok(None)` it indicates that we have reached the end of the
61/// // playlist and so we break the loop.
62/// Ok(None) => break,
63/// // Even when encountering errors we can access the original problem line, then take a
64/// // mutable borrow on the inner writer, and write out the bytes. In this way we can be a
65/// // very unopinionated proxy. This is completely implementation specific, and other use
66/// // cases may require an implementation that rejects the playlist, or we may also choose
67/// // to implement tracing in such cases. We're just showing the possibility here.
68/// Err(e) => writer.get_mut().write_all(e.errored_line.as_bytes())?,
69/// };
70/// }
71///
72/// const EXPECTED: &str = r#"
73/// #EXTINF:4,Hello,
74/// segment_100.mp4
75/// #EXTINF:4,World!
76/// segment_101.mp4
77/// "#;
78/// assert_eq!(EXPECTED, String::from_utf8_lossy(&writer.into_inner()));
79/// # Ok::<(), Box<dyn std::error::Error>>(())
80/// ```
81///
82/// ## Construct a playlist output
83///
84/// It may also be the case that a user may want to write a complete playlist out without having to
85/// parse any data. This is also possible (and may be made easier in the future if we implement a
86/// playlist and playlist builder type). And of course, the user can mix and match, parsing some
87/// input, mutating where necessary, introducing new lines as needed, and writing it all out. Below
88/// is another toy example of how we may construct the [9.4. Multivariant Playlist] example provided
89/// in the HLS specification.
90///
91/// ```
92/// # use quick_m3u8::{
93/// # HlsLine, Writer,
94/// # tag::hls::{M3u, StreamInf},
95/// # };
96/// # use std::error::Error;
97/// const EXPECTED: &str = r#"#EXTM3U
98/// #EXT-X-STREAM-INF:BANDWIDTH=1280000,AVERAGE-BANDWIDTH=1000000
99/// http://example.com/low.m3u8
100/// #EXT-X-STREAM-INF:BANDWIDTH=2560000,AVERAGE-BANDWIDTH=2000000
101/// http://example.com/mid.m3u8
102/// #EXT-X-STREAM-INF:BANDWIDTH=7680000,AVERAGE-BANDWIDTH=6000000
103/// http://example.com/hi.m3u8
104/// #EXT-X-STREAM-INF:BANDWIDTH=65000,CODECS="mp4a.40.5"
105/// http://example.com/audio-only.m3u8
106/// "#;
107///
108/// let mut writer = Writer::new(Vec::new());
109/// writer.write_line(HlsLine::from(M3u))?;
110/// writer.write_line(HlsLine::from(
111/// StreamInf::builder()
112/// .with_bandwidth(1280000)
113/// .with_average_bandwidth(1000000)
114/// .finish(),
115/// ))?;
116/// writer.write_uri("http://example.com/low.m3u8")?;
117/// writer.write_line(HlsLine::from(
118/// StreamInf::builder()
119/// .with_bandwidth(2560000)
120/// .with_average_bandwidth(2000000)
121/// .finish(),
122/// ))?;
123/// writer.write_uri("http://example.com/mid.m3u8")?;
124/// writer.write_line(HlsLine::from(
125/// StreamInf::builder()
126/// .with_bandwidth(7680000)
127/// .with_average_bandwidth(6000000)
128/// .finish(),
129/// ))?;
130/// writer.write_uri("http://example.com/hi.m3u8")?;
131/// writer.write_line(HlsLine::from(
132/// StreamInf::builder()
133/// .with_bandwidth(65000)
134/// .with_codecs("mp4a.40.5")
135/// .finish(),
136/// ))?;
137/// writer.write_uri("http://example.com/audio-only.m3u8")?;
138///
139/// assert_eq!(EXPECTED, std::str::from_utf8(&writer.into_inner())?);
140/// # Ok::<(), Box<dyn Error>>(())
141/// ```
142///
143/// [9.4. Multivariant Playlist]: https://datatracker.ietf.org/doc/html/draft-pantos-hls-rfc8216bis-18#section-9.4
144#[derive(Debug, Clone)]
145pub struct Writer<W>
146where
147 W: Write,
148{
149 /// underlying writer
150 writer: W,
151}
152
153impl<W> Writer<W>
154where
155 W: Write,
156{
157 /// Creates a `Writer` from a generic writer.
158 pub const fn new(inner: W) -> Writer<W> {
159 Writer { writer: inner }
160 }
161
162 /// Consumes this `Writer`, returning the underlying writer.
163 pub fn into_inner(self) -> W {
164 self.writer
165 }
166
167 /// Get a mutable reference to the underlying writer.
168 pub fn get_mut(&mut self) -> &mut W {
169 &mut self.writer
170 }
171
172 /// Get a reference to the underlying writer.
173 pub const fn get_ref(&self) -> &W {
174 &self.writer
175 }
176
177 /// Write the `HlsLine` to the underlying writer. Returns the number of bytes consumed during
178 /// writing or an `io::Error` from the underlying writer.
179 ///
180 /// In this case the `CustomTag` generic is the default `NoCustomTag` struct. See [`Self`] for
181 /// more detailed documentation.
182 pub fn write_line(&mut self, line: HlsLine) -> io::Result<usize> {
183 self.write_custom_line(line)
184 }
185
186 /// Example:
187 /// ```
188 /// # use quick_m3u8::Writer;
189 /// let mut writer = Writer::new(b"#EXTM3U\n".to_vec());
190 /// writer.write_blank().unwrap();
191 /// writer.write_comment(" Note blank line above.").unwrap();
192 /// let expected = r#"#EXTM3U
193 ///
194 /// ## Note blank line above.
195 /// "#;
196 /// assert_eq!(expected.as_bytes(), writer.into_inner());
197 /// ```
198 pub fn write_blank(&mut self) -> io::Result<usize> {
199 self.write_line(HlsLine::Blank)
200 }
201
202 /// Example:
203 /// ```
204 /// # use quick_m3u8::Writer;
205 /// let mut writer = Writer::new(Vec::new());
206 /// writer.write_comment(" This is a comment.").unwrap();
207 /// assert_eq!("# This is a comment.\n".as_bytes(), writer.into_inner());
208 /// ```
209 pub fn write_comment<'a>(&mut self, comment: impl Into<Cow<'a, str>>) -> io::Result<usize> {
210 self.write_line(HlsLine::Comment(comment.into()))
211 }
212
213 /// Example:
214 /// ```
215 /// # use quick_m3u8::Writer;
216 /// let mut writer = Writer::new(Vec::new());
217 /// writer.write_uri("example.m3u8").unwrap();
218 /// assert_eq!("example.m3u8\n".as_bytes(), writer.into_inner());
219 /// ```
220 pub fn write_uri<'a>(&mut self, uri: impl Into<Cow<'a, str>>) -> io::Result<usize> {
221 self.write_line(HlsLine::Uri(uri.into()))
222 }
223
224 /// Write a custom tag implementation to the inner writer.
225 ///
226 /// Note that if the custom tag is derived from parsed data (i.e. not user constructed), then
227 /// this method should be avoided, as it will allocate data perhaps unnecessarily. In that case
228 /// use [`Self::write_custom_line`] with [`crate::tag::CustomTagAccess`], as this will use the
229 /// original parsed data if no mutation has occurred.
230 ///
231 /// Example:
232 /// ```
233 /// # use quick_m3u8::Writer;
234 /// # use quick_m3u8::tag::{CustomTag, WritableCustomTag, WritableTag, UnknownTag};
235 /// # use quick_m3u8::error::{ValidationError, ParseTagValueError};
236 /// # use std::borrow::Cow;
237 /// #[derive(Debug, PartialEq, Clone)]
238 /// struct ExampleCustomTag {
239 /// answer: u64,
240 /// }
241 /// impl TryFrom<UnknownTag<'_>> for ExampleCustomTag {
242 /// type Error = ValidationError;
243 /// fn try_from(tag: UnknownTag) -> Result<Self, Self::Error> {
244 /// if tag.name() != "-X-MEANING-OF-LIFE" {
245 /// return Err(ValidationError::UnexpectedTagName)
246 /// }
247 /// Ok(Self {
248 /// answer: tag
249 /// .value()
250 /// .ok_or(ParseTagValueError::UnexpectedEmpty)?
251 /// .try_as_decimal_integer()?
252 /// })
253 /// }
254 /// }
255 /// impl CustomTag<'_> for ExampleCustomTag {
256 /// fn is_known_name(name: &str) -> bool {
257 /// name == "-X-MEANING-OF-LIFE"
258 /// }
259 /// }
260 /// impl WritableCustomTag<'_> for ExampleCustomTag {
261 /// fn into_writable_tag(self) -> WritableTag<'static> {
262 /// WritableTag::new("-X-MEANING-OF-LIFE", self.answer)
263 /// }
264 /// }
265 ///
266 /// let mut writer = Writer::new(Vec::new());
267 /// let custom_tag = ExampleCustomTag { answer: 42 };
268 /// writer.write_custom_tag(custom_tag).unwrap();
269 /// assert_eq!(
270 /// "#EXT-X-MEANING-OF-LIFE:42\n".as_bytes(),
271 /// writer.into_inner()
272 /// );
273 /// ```
274 pub fn write_custom_tag<'a, Custom>(&mut self, tag: Custom) -> io::Result<usize>
275 where
276 Custom: WritableCustomTag<'a>,
277 {
278 let mut count = self.write(tag.into_inner().value())?;
279 count += self.write(b"\n")?;
280 Ok(count)
281 }
282
283 /// Write the `HlsLine` to the underlying writer. Returns the number of bytes consumed during
284 /// writing or an `io::Error` from the underlying writer. Ultimately, all the other write
285 /// methods are wrappers for this method.
286 ///
287 /// This method is necessary to use where the input lines carry a custom tag type (other than
288 /// [`crate::tag::NoCustomTag`]). For example, say we are parsing some data using a reader that
289 /// supports our own custom defined tag (`SomeCustomTag`).
290 /// ```
291 /// # use quick_m3u8::{
292 /// # Reader,
293 /// # config::ParsingOptions,
294 /// # tag::{CustomTag, WritableCustomTag, WritableTag, UnknownTag},
295 /// # error::ValidationError
296 /// # };
297 /// # use std::marker::PhantomData;
298 /// # #[derive(Debug, PartialEq, Clone)]
299 /// # struct SomeCustomTag;
300 /// # impl TryFrom<UnknownTag<'_>> for SomeCustomTag {
301 /// # type Error = ValidationError;
302 /// # fn try_from(_: UnknownTag) -> Result<Self, Self::Error> { todo!() }
303 /// # }
304 /// # impl CustomTag<'_> for SomeCustomTag {
305 /// # fn is_known_name(_: &str) -> bool { todo!() }
306 /// # }
307 /// # impl<'a> WritableCustomTag<'a> for SomeCustomTag {
308 /// # fn into_writable_tag(self) -> WritableTag<'a> { todo!() }
309 /// # }
310 /// # let input = "";
311 /// # let options = ParsingOptions::default();
312 /// let mut reader = Reader::with_custom_from_str(
313 /// input,
314 /// options,
315 /// PhantomData::<SomeCustomTag>
316 /// );
317 /// ```
318 /// If we tried to use the [`Self::write_line`] method, it would fail to compile (as that method
319 /// expects that the generic `Custom` type is [`crate::tag::NoCustomTag`], which is a struct
320 /// provided by the library that never succeeds the [`crate::tag::CustomTag::is_known_name`]
321 /// check so is never parsed). Therefore we must use the `write_custom_line` method in this case
322 /// (even if we are not writing the custom tag itself):
323 /// ```
324 /// # use quick_m3u8::{
325 /// # Reader, Writer,
326 /// # config::ParsingOptions,
327 /// # tag::{CustomTag, WritableCustomTag, WritableTag, UnknownTag},
328 /// # error::ValidationError
329 /// # };
330 /// # use std::{error::Error, marker::PhantomData};
331 /// # #[derive(Debug, PartialEq, Clone)]
332 /// # struct SomeCustomTag;
333 /// # impl TryFrom<UnknownTag<'_>> for SomeCustomTag {
334 /// # type Error = ValidationError;
335 /// # fn try_from(_: UnknownTag) -> Result<Self, Self::Error> { todo!() }
336 /// # }
337 /// # impl CustomTag<'_> for SomeCustomTag {
338 /// # fn is_known_name(_: &str) -> bool { todo!() }
339 /// # }
340 /// # impl<'a> WritableCustomTag<'a> for SomeCustomTag {
341 /// # fn into_writable_tag(self) -> WritableTag<'a> { todo!() }
342 /// # }
343 /// # let input = "";
344 /// # let options = ParsingOptions::default();
345 /// # let mut reader = Reader::with_custom_from_str(
346 /// # input,
347 /// # options,
348 /// # PhantomData::<SomeCustomTag>
349 /// # );
350 /// let mut writer = Writer::new(Vec::new());
351 /// loop {
352 /// match reader.read_line() {
353 /// // --snip--
354 /// Ok(Some(line)) => {
355 /// writer.write_custom_line(line)?;
356 /// }
357 /// // --snip--
358 /// # Ok(None) => break,
359 /// # _ => todo!(),
360 /// };
361 /// }
362 /// # Ok::<(), Box<dyn Error>>(())
363 /// ```
364 pub fn write_custom_line<'a, Custom>(&mut self, line: HlsLine<'a, Custom>) -> io::Result<usize>
365 where
366 Custom: WritableCustomTag<'a>,
367 {
368 let mut count = 0usize;
369 match line {
370 HlsLine::Blank => (),
371 HlsLine::Comment(c) => {
372 count += self.write(b"#")?;
373 count += self.write(c.as_bytes())?;
374 }
375 HlsLine::Uri(u) => count += self.write(u.as_bytes())?,
376 HlsLine::UnknownTag(t) => count += self.write(t.as_bytes())?,
377 HlsLine::KnownTag(t) => count += self.write(t.into_inner().value())?,
378 };
379 count += self.write(b"\n")?;
380 Ok(count)
381 }
382
383 fn write(&mut self, mut buf: &[u8]) -> io::Result<usize> {
384 let mut count = 0usize;
385 while !buf.is_empty() {
386 match self.writer.write(buf) {
387 Ok(0) => {
388 return Err(io::Error::new(
389 std::io::ErrorKind::WriteZero,
390 "failed to write whole buffer",
391 ));
392 }
393 Ok(n) => {
394 count += n;
395 buf = &buf[n..];
396 }
397 Err(ref e) if e.kind() == std::io::ErrorKind::Interrupted => {}
398 Err(e) => return Err(e),
399 }
400 }
401 Ok(count)
402 }
403}
404
405#[cfg(test)]
406mod tests {
407 use super::*;
408 use crate::{
409 config::ParsingOptionsBuilder,
410 date_time,
411 error::ValidationError,
412 tag::{
413 CustomTag, DecimalResolution, UnknownTag, WritableAttributeValue, WritableTag,
414 WritableTagValue,
415 hls::{self, Inf, M3u, MediaSequence, Targetduration, Version},
416 },
417 };
418 use pretty_assertions::assert_eq;
419
420 #[derive(Debug, PartialEq, Clone)]
421 enum TestTag {
422 Empty,
423 Type,
424 Int,
425 Range,
426 Float { title: &'static str },
427 Date,
428 List,
429 }
430
431 impl TryFrom<UnknownTag<'_>> for TestTag {
432 type Error = ValidationError;
433
434 fn try_from(_: UnknownTag<'_>) -> Result<Self, Self::Error> {
435 Err(ValidationError::NotImplemented)
436 }
437 }
438
439 impl CustomTag<'_> for TestTag {
440 fn is_known_name(_: &str) -> bool {
441 true
442 }
443 }
444
445 impl WritableCustomTag<'_> for TestTag {
446 fn into_writable_tag(self) -> WritableTag<'static> {
447 let value = match self {
448 TestTag::Empty => WritableTagValue::Empty,
449 TestTag::Type => WritableTagValue::from("VOD"),
450 TestTag::Int => WritableTagValue::from(42),
451 TestTag::Range => WritableTagValue::from((1024, Some(512))),
452 TestTag::Float { title } => WritableTagValue::from((42.42, title)),
453 TestTag::Date => {
454 WritableTagValue::from(date_time!(2025-06-17 T 01:37:15.129 -05:00))
455 }
456 TestTag::List => WritableTagValue::from([
457 ("TEST-INT", WritableAttributeValue::DecimalInteger(42)),
458 (
459 "TEST-FLOAT",
460 WritableAttributeValue::SignedDecimalFloatingPoint(-42.42),
461 ),
462 (
463 "TEST-RESOLUTION",
464 WritableAttributeValue::DecimalResolution(DecimalResolution {
465 width: 1920,
466 height: 1080,
467 }),
468 ),
469 (
470 "TEST-QUOTED-STRING",
471 WritableAttributeValue::QuotedString("test".into()),
472 ),
473 (
474 "TEST-ENUMERATED-STRING",
475 WritableAttributeValue::UnquotedString("test".into()),
476 ),
477 ]),
478 };
479 WritableTag::new("-X-TEST-TAG", value)
480 }
481 }
482
483 #[test]
484 fn to_string_on_empty_is_valid() {
485 let test = TestTag::Empty;
486 assert_eq!("#EXT-X-TEST-TAG", string_from(test).as_str());
487 }
488
489 #[test]
490 fn to_string_on_type_is_valid() {
491 let test = TestTag::Type;
492 assert_eq!("#EXT-X-TEST-TAG:VOD", string_from(test).as_str());
493 }
494
495 #[test]
496 fn to_string_on_int_is_valid() {
497 let test = TestTag::Int;
498 assert_eq!("#EXT-X-TEST-TAG:42", string_from(test).as_str());
499 }
500
501 #[test]
502 fn to_string_on_range_is_valid() {
503 let test = TestTag::Range;
504 assert_eq!("#EXT-X-TEST-TAG:1024@512", string_from(test).as_str());
505 }
506
507 #[test]
508 fn to_string_on_float_is_valid() {
509 let test = TestTag::Float { title: "" };
510 assert_eq!("#EXT-X-TEST-TAG:42.42", string_from(test).as_str());
511 let test = TestTag::Float {
512 title: " A useful comment",
513 };
514 assert_eq!(
515 "#EXT-X-TEST-TAG:42.42, A useful comment",
516 string_from(test).as_str()
517 );
518 }
519
520 #[test]
521 fn to_string_on_date_is_valid() {
522 let test = TestTag::Date;
523 assert_eq!(
524 "#EXT-X-TEST-TAG:2025-06-17T01:37:15.129-05:00",
525 string_from(test).as_str()
526 );
527 }
528
529 #[test]
530 fn to_string_on_list_is_valid() {
531 let test = TestTag::List;
532 let mut found_int = false;
533 let mut found_float = false;
534 let mut found_resolution = false;
535 let mut found_quote = false;
536 let mut found_enum = false;
537 let tag_string = string_from(test);
538 let mut name_value_split = tag_string.split(':');
539 assert_eq!("#EXT-X-TEST-TAG", name_value_split.next().unwrap());
540 let attrs = name_value_split.next().unwrap().split(',').enumerate();
541 for (index, attr) in attrs {
542 match index {
543 0..5 => match attr.split('=').next().unwrap() {
544 "TEST-INT" => {
545 if found_int {
546 panic!("Unexpected duplicated attribute {attr}");
547 }
548 found_int = true;
549 assert_eq!("TEST-INT=42", attr);
550 }
551 "TEST-FLOAT" => {
552 if found_float {
553 panic!("Unexpected duplicated attribute {attr}");
554 }
555 found_float = true;
556 assert_eq!("TEST-FLOAT=-42.42", attr);
557 }
558 "TEST-RESOLUTION" => {
559 if found_resolution {
560 panic!("Unexpected duplicated attribute {attr}");
561 }
562 found_resolution = true;
563 assert_eq!("TEST-RESOLUTION=1920x1080", attr);
564 }
565 "TEST-QUOTED-STRING" => {
566 if found_quote {
567 panic!("Unexpected duplicated attribute {attr}");
568 }
569 found_quote = true;
570 assert_eq!("TEST-QUOTED-STRING=\"test\"", attr);
571 }
572 "TEST-ENUMERATED-STRING" => {
573 if found_enum {
574 panic!("Unexpected duplicated attribute {attr}");
575 }
576 found_enum = true;
577 assert_eq!("TEST-ENUMERATED-STRING=test", attr);
578 }
579 x => panic!("Unexpected attribute {x}"),
580 },
581 _ => panic!("Unexpected index {index}"),
582 }
583 }
584 assert!(found_int);
585 assert!(found_float);
586 assert!(found_resolution);
587 assert!(found_quote);
588 assert!(found_enum);
589 }
590
591 fn string_from(test_tag: TestTag) -> String {
592 let mut writer = Writer::new(Vec::new());
593 writer
594 .write_custom_tag(test_tag)
595 .expect("should not fail to write tag");
596 String::from_utf8_lossy(&writer.into_inner())
597 .trim_end()
598 .to_string()
599 }
600
601 #[test]
602 fn writer_should_output_expected() {
603 let mut writer = Writer::new(Vec::new());
604 writer.write_line(HlsLine::from(M3u)).unwrap();
605 writer.write_line(HlsLine::from(Version::new(3))).unwrap();
606 writer
607 .write_line(HlsLine::from(Targetduration::new(8)))
608 .unwrap();
609 writer
610 .write_line(HlsLine::from(MediaSequence::new(2680)))
611 .unwrap();
612 writer.write_line(HlsLine::Blank).unwrap();
613 writer
614 .write_line(HlsLine::from(Inf::new(7.975, "".to_string())))
615 .unwrap();
616 writer
617 .write_line(HlsLine::Uri(
618 "https://priv.example.com/fileSequence2680.ts".into(),
619 ))
620 .unwrap();
621 writer
622 .write_line(HlsLine::from(Inf::new(7.941, "".to_string())))
623 .unwrap();
624 writer
625 .write_line(HlsLine::Uri(
626 "https://priv.example.com/fileSequence2681.ts".into(),
627 ))
628 .unwrap();
629 writer
630 .write_line(HlsLine::from(Inf::new(7.975, "".to_string())))
631 .unwrap();
632 writer
633 .write_line(HlsLine::Uri(
634 "https://priv.example.com/fileSequence2682.ts".into(),
635 ))
636 .unwrap();
637 assert_eq!(
638 EXPECTED_WRITE_OUTPUT,
639 std::str::from_utf8(&writer.into_inner()).unwrap()
640 );
641 }
642
643 #[test]
644 fn write_line_should_return_correct_byte_count() {
645 let mut writer = Writer::new(Vec::new());
646 assert_eq!(
647 12, // 1 (#) + 10 (str) + 1 (\n) == 12
648 writer
649 .write_line(HlsLine::Comment(" A comment".into()))
650 .unwrap()
651 );
652 assert_eq!(
653 13, // 12 (str) + 1 (\n) == 13
654 writer
655 .write_line(HlsLine::Uri("example.m3u8".into()))
656 .unwrap()
657 );
658 assert_eq!(
659 22, // 21 (#EXTINF:6.006,PTS:0.0) + 1 (\n) == 22
660 writer
661 .write_line(HlsLine::from(hls::Tag::Inf(Inf::new(
662 6.006,
663 "PTS:0.0".to_string()
664 ))))
665 .unwrap()
666 );
667 }
668
669 #[test]
670 fn writing_with_no_manipulation_should_leave_output_unchaged_except_for_new_lines() {
671 let mut writer = Writer::new(Vec::new());
672 let options = ParsingOptionsBuilder::new()
673 .with_parsing_for_m3u()
674 .with_parsing_for_version()
675 .build();
676 let mut remaining = Some(EXPECTED_WRITE_OUTPUT);
677 while let Some(line) = remaining {
678 let slice = crate::line::parse(line, &options).unwrap();
679 remaining = slice.remaining;
680 writer.write_line(slice.parsed).unwrap();
681 }
682 let mut expected = EXPECTED_WRITE_OUTPUT.to_string();
683 expected.push('\n');
684 assert_eq!(
685 expected.as_str(),
686 std::str::from_utf8(&writer.into_inner()).unwrap()
687 );
688 }
689}
690
691#[cfg(test)]
692const EXPECTED_WRITE_OUTPUT: &str = r#"#EXTM3U
693#EXT-X-VERSION:3
694#EXT-X-TARGETDURATION:8
695#EXT-X-MEDIA-SEQUENCE:2680
696
697#EXTINF:7.975
698https://priv.example.com/fileSequence2680.ts
699#EXTINF:7.941
700https://priv.example.com/fileSequence2681.ts
701#EXTINF:7.975
702https://priv.example.com/fileSequence2682.ts
703"#;