1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
use std::borrow::Cow;
use std::collections::HashSet;
use std::convert::TryFrom;
use std::fmt;
use std::str::FromStr;
use std::time::Duration;

use derive_builder::Builder;
use stable_vec::StableVec;

use crate::line::{Line, Lines, Tag};
use crate::media_segment::MediaSegment;
use crate::tags::{
    ExtM3u, ExtXByteRange, ExtXDiscontinuitySequence, ExtXEndList, ExtXIFramesOnly,
    ExtXIndependentSegments, ExtXKey, ExtXMediaSequence, ExtXStart, ExtXTargetDuration,
    ExtXVersion,
};
use crate::types::{
    DecryptionKey, EncryptionMethod, InitializationVector, KeyFormat, PlaylistType, ProtocolVersion,
};
use crate::utils::{tag, BoolExt};
use crate::{Error, RequiredVersion};

/// Media playlist.
#[derive(Builder, Debug, Clone, PartialEq, Eq)]
#[builder(build_fn(skip), setter(strip_option))]
#[non_exhaustive]
pub struct MediaPlaylist<'a> {
    /// Specifies the maximum [`MediaSegment::duration`]. A typical target
    /// duration is 10 seconds.
    ///
    /// ### Note
    ///
    /// This field is required.
    pub target_duration: Duration,
    /// The [`MediaSegment::number`] of the first [`MediaSegment`] that
    /// appears in a [`MediaPlaylist`].
    ///
    /// ### Note
    ///
    /// This field is optional and by default a value of 0 is assumed.
    #[builder(default)]
    pub media_sequence: usize,
    /// Allows synchronization between different renditions of the same
    /// [`VariantStream`].
    ///
    /// ### Note
    ///
    /// This field is optional and by default a vaule of 0 is assumed.
    ///
    /// [`VariantStream`]: crate::tags::VariantStream
    #[builder(default)]
    pub discontinuity_sequence: usize,
    /// Provides mutability information about a [`MediaPlaylist`].
    ///
    /// - [`PlaylistType::Vod`] indicates that the playlist must not change.
    ///
    /// - [`PlaylistType::Event`] indicates that the server does not change or
    /// delete any part of the playlist, but may append new lines to it.
    ///
    /// ### Note
    ///
    /// This field is optional.
    #[builder(default, setter(into))]
    pub playlist_type: Option<PlaylistType>,
    /// Indicates that each [`MediaSegment`] in the playlist describes a single
    /// I-frame. I-frames are encoded video frames, whose decoding does not
    /// depend on any other frame. I-frame Playlists can be used for trick
    /// play, such as fast forward, rapid reverse, and scrubbing.
    ///
    /// ### Note
    ///
    /// This field is optional.
    #[builder(default)]
    pub has_i_frames_only: bool,
    /// This indicates that all media samples in a [`MediaSegment`] can be
    /// decoded without information from other segments.
    ///
    /// ### Note
    ///
    /// This field is optional and by default `false`. If the value is `true` it
    /// applies to every [`MediaSegment`] in this [`MediaPlaylist`].
    #[builder(default)]
    pub has_independent_segments: bool,
    /// Indicates a preferred point at which to start playing a playlist. By
    /// default, clients should start playback at this point when beginning a
    /// playback session.
    ///
    /// ### Note
    ///
    /// This field is optional.
    #[builder(default, setter(into))]
    pub start: Option<ExtXStart>,
    /// Indicates that no more [`MediaSegment`]s will be added to the
    /// [`MediaPlaylist`] file.
    ///
    /// ### Note
    ///
    /// This field is optional and by default `false`.
    /// A `false` indicates that the client should reload the [`MediaPlaylist`]
    /// from the server, until a playlist is encountered, where this field is
    /// `true`.
    #[builder(default)]
    pub has_end_list: bool,
    /// A list of all [`MediaSegment`]s.
    ///
    /// ### Note
    ///
    /// This field is required.
    #[builder(setter(custom))]
    pub segments: StableVec<MediaSegment<'a>>,
    /// The allowable excess duration of each media segment in the
    /// associated playlist.
    ///
    /// ### Error
    ///
    /// If there is a media segment of which duration exceeds
    /// `#EXT-X-TARGETDURATION + allowable_excess_duration`,
    /// the invocation of `MediaPlaylistBuilder::build()` method will fail.
    ///
    ///
    /// ### Note
    ///
    /// This field is optional and the default value is
    /// `Duration::from_secs(0)`.
    #[builder(default = "Duration::from_secs(0)")]
    pub allowable_excess_duration: Duration,
    /// A list of unknown tags.
    ///
    /// ### Note
    ///
    /// This field is optional.
    #[builder(default, setter(into))]
    pub unknown: Vec<Cow<'a, str>>,
}

impl<'a> MediaPlaylistBuilder<'a> {
    fn validate(&self) -> Result<(), String> {
        if let Some(target_duration) = &self.target_duration {
            self.validate_media_segments(*target_duration)
                .map_err(|e| e.to_string())?;
        }

        Ok(())
    }

    fn validate_media_segments(&self, target_duration: Duration) -> crate::Result<()> {
        let mut last_range_uri = None;

        if let Some(segments) = &self.segments {
            // verify the independent segments
            if self.has_independent_segments.unwrap_or(false) {
                // If the encryption METHOD is AES-128 and the Playlist contains an EXT-
                // X-I-FRAMES-ONLY tag, the entire resource MUST be encrypted using
                // AES-128 CBC with PKCS7 padding [RFC5652].
                //
                // from the rfc: https://tools.ietf.org/html/rfc8216#section-6.2.3

                let is_aes128 = segments
                    .values()
                    // convert iterator of segments to iterator of keys
                    .flat_map(|s| s.keys.iter())
                    // filter out all empty keys
                    .filter_map(ExtXKey::as_ref)
                    .any(|k| k.method == EncryptionMethod::Aes128);

                if is_aes128 {
                    for key in segments.values().flat_map(|s| s.keys.iter()) {
                        if let ExtXKey(Some(key)) = key {
                            if key.method != EncryptionMethod::Aes128 {
                                return Err(Error::custom(concat!(
                                    "if any independent segment is encrypted with Aes128,",
                                    " all must be encrypted with Aes128"
                                )));
                            }
                        } else {
                            return Err(Error::custom(concat!(
                                "if any independent segment is encrypted with Aes128,",
                                " all must be encrypted with Aes128"
                            )));
                        }
                    }
                }
            }

            for segment in segments.values() {
                // CHECK: `#EXT-X-TARGETDURATION`
                let segment_duration = segment.duration.duration();

                // round the duration if it is .5s
                let rounded_segment_duration =
                    Duration::from_secs(segment_duration.as_secs_f64().round() as u64);

                let max_segment_duration = self
                    .allowable_excess_duration
                    .as_ref()
                    .map_or(target_duration, |value| target_duration + *value);

                if rounded_segment_duration > max_segment_duration {
                    return Err(Error::custom(format!(
                        "Too large segment duration: actual={:?}, max={:?}, target_duration={:?}, uri={:?}",
                        segment_duration,
                        max_segment_duration,
                        target_duration,
                        segment.uri()
                    )));
                }

                // CHECK: `#EXT-X-BYTE-RANGE`
                if let Some(range) = &segment.byte_range {
                    if range.start().is_none() {
                        // TODO: error messages
                        if last_range_uri.ok_or_else(Error::invalid_input)? != segment.uri() {
                            return Err(Error::invalid_input());
                        }
                    } else {
                        last_range_uri = Some(segment.uri());
                    }
                } else {
                    last_range_uri = None;
                }
            }
        }

        Ok(())
    }

    /// Adds a media segment to the resulting playlist and assigns the next free
    /// [`MediaSegment::number`] to the segment.
    pub fn push_segment(&mut self, segment: MediaSegment<'a>) -> &mut Self {
        let segments = self.segments.get_or_insert_with(StableVec::new);

        if segment.explicit_number {
            segments.reserve_for(segment.number);
            segments.insert(segment.number, segment);
        } else {
            segments.push(segment);
        }

        self
    }

    /// Parse the rest of the [`MediaPlaylist`] from an m3u8 file.
    pub fn parse(&mut self, input: &'a str) -> crate::Result<MediaPlaylist<'a>> {
        parse_media_playlist(input, self)
    }

    /// Adds segments to the resulting playlist and assigns a
    /// [`MediaSegment::number`] to each segment.
    ///
    /// ## Note
    ///
    /// The [`MediaSegment::number`] will be assigned based on the order of the
    /// input (e.g. the first element will be 0, second element 1, ..) or if a
    /// number has been set explicitly. This function assumes, that all segments
    /// will be present in the final media playlist and the following is only
    /// possible if the segment is marked with `ExtXDiscontinuity`.
    pub fn segments(&mut self, segments: Vec<MediaSegment<'a>>) -> &mut Self {
        let mut vec = StableVec::<MediaSegment<'a>>::with_capacity(segments.len());
        let mut remaining = Vec::with_capacity(segments.len());

        for segment in segments {
            if segment.explicit_number {
                vec.insert(segment.number, segment);
            } else {
                remaining.push(segment);
            }
        }

        for segment in remaining {
            vec.push(segment);
        }

        self.segments = Some(vec);
        self
    }

    /// Builds a new `MediaPlaylist`.
    ///
    /// # Errors
    ///
    /// If a required field has not been initialized.
    pub fn build(&self) -> Result<MediaPlaylist<'a>, String> {
        // validate builder
        self.validate()?;

        let sequence_number = self.media_sequence.unwrap_or(0);

        let mut segments = self
            .segments
            .clone()
            .ok_or_else(|| "missing field `segments`".to_string())?;

        // no segment should exist before the sequence_number
        if let Some(first_segment) = segments.find_first() {
            if sequence_number > first_segment.number && first_segment.explicit_number {
                return Err(format!(
                    "there should be no segment ({}) before the sequence_number ({})",
                    first_segment, sequence_number,
                ));
            }
        }

        let mut previous_range: Option<ExtXByteRange> = None;

        for (i, segment) in segments.iter_mut() {
            // assign the correct number to all implcitly numbered segments:
            if !segment.explicit_number {
                segment.number = i + sequence_number;
            }

            // add the segment number as iv, if the iv is missing:
            for key in &mut segment.keys {
                if let ExtXKey(Some(DecryptionKey {
                    method, iv, format, ..
                })) = key
                {
                    if *method == EncryptionMethod::Aes128 && *iv == InitializationVector::Missing {
                        if format.is_none() {
                            *iv = InitializationVector::Number(segment.number as u128);
                        } else if let Some(KeyFormat::Identity) = format {
                            *iv = InitializationVector::Number(segment.number as u128);
                        }
                    }
                }
            }

            // add the lower bound to the byterange automatically
            if let Some(range) = &mut segment.byte_range {
                if range.start().is_none() {
                    if let Some(previous_range) = previous_range {
                        // the end of the previous_range is the start of the next range
                        *range = range.saturating_add(previous_range.end());
                        range.set_start(Some(previous_range.end()));
                    } else {
                        // assume that the byte range starts at zero
                        range.set_start(Some(0));
                    }
                }

                previous_range = segment.byte_range;
            }
        }

        // TODO: can segments be missing?
        if !segments.is_compact() {
            // find the missing segment by iterating through all segments:
            // let missing = segments
            //     .iter()
            //     .enumerate()
            //     .find_map(|(i, e)| e.is_none().athen(i))
            //     .unwrap();
            return Err("a segment is missing".to_string());
        }

        Ok(MediaPlaylist {
            target_duration: self
                .target_duration
                .ok_or_else(|| "missing field `target_duration`".to_string())?,
            media_sequence: self.media_sequence.unwrap_or(0),
            discontinuity_sequence: self.discontinuity_sequence.unwrap_or(0),
            playlist_type: self.playlist_type.unwrap_or(None),
            has_i_frames_only: self.has_i_frames_only.unwrap_or(false),
            has_independent_segments: self.has_independent_segments.unwrap_or(false),
            start: self.start.unwrap_or(None),
            has_end_list: self.has_end_list.unwrap_or(false),
            segments,
            allowable_excess_duration: self
                .allowable_excess_duration
                .unwrap_or_else(|| Duration::from_secs(0)),
            unknown: self.unknown.clone().unwrap_or_else(Vec::new),
        })
    }
}

impl<'a> RequiredVersion for MediaPlaylistBuilder<'a> {
    fn required_version(&self) -> ProtocolVersion {
        required_version![
            self.target_duration.map(ExtXTargetDuration),
            (self.media_sequence.unwrap_or(0) != 0)
                .athen(|| ExtXMediaSequence(self.media_sequence.unwrap_or(0))),
            (self.discontinuity_sequence.unwrap_or(0) != 0)
                .athen(|| ExtXDiscontinuitySequence(self.discontinuity_sequence.unwrap_or(0))),
            self.playlist_type,
            self.has_i_frames_only
                .unwrap_or(false)
                .athen_some(ExtXIFramesOnly),
            self.has_independent_segments
                .unwrap_or(false)
                .athen_some(ExtXIndependentSegments),
            self.start,
            self.has_end_list.unwrap_or(false).athen_some(ExtXEndList),
            self.segments
        ]
    }
}

impl<'a> MediaPlaylist<'a> {
    /// Returns a builder for [`MediaPlaylist`].
    #[must_use]
    #[inline]
    pub fn builder() -> MediaPlaylistBuilder<'a> { MediaPlaylistBuilder::default() }

    /// Computes the `Duration` of the [`MediaPlaylist`], by adding each segment
    /// duration together.
    #[must_use]
    pub fn duration(&self) -> Duration {
        self.segments.values().map(|s| s.duration.duration()).sum()
    }

    /// Makes the struct independent of its lifetime, by taking ownership of all
    /// internal [`Cow`]s.
    ///
    /// # Note
    ///
    /// This is a relatively expensive operation.
    #[must_use]
    pub fn into_owned(self) -> MediaPlaylist<'static> {
        MediaPlaylist {
            target_duration: self.target_duration,
            media_sequence: self.media_sequence,
            discontinuity_sequence: self.discontinuity_sequence,
            playlist_type: self.playlist_type,
            has_i_frames_only: self.has_i_frames_only,
            has_independent_segments: self.has_independent_segments,
            start: self.start,
            has_end_list: self.has_end_list,
            segments: {
                self.segments
                    .into_iter()
                    .map(|(_, s)| s.into_owned())
                    .collect()
            },
            allowable_excess_duration: self.allowable_excess_duration,
            unknown: {
                self.unknown
                    .into_iter()
                    .map(|v| Cow::Owned(v.into_owned()))
                    .collect()
            },
        }
    }
}

impl<'a> RequiredVersion for MediaPlaylist<'a> {
    fn required_version(&self) -> ProtocolVersion {
        required_version![
            ExtXTargetDuration(self.target_duration),
            (self.media_sequence != 0).athen(|| ExtXMediaSequence(self.media_sequence)),
            (self.discontinuity_sequence != 0)
                .athen(|| ExtXDiscontinuitySequence(self.discontinuity_sequence)),
            self.playlist_type,
            self.has_i_frames_only.athen_some(ExtXIFramesOnly),
            self.has_independent_segments
                .athen_some(ExtXIndependentSegments),
            self.start,
            self.has_end_list.athen_some(ExtXEndList),
            self.segments
        ]
    }
}

impl<'a> fmt::Display for MediaPlaylist<'a> {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        writeln!(f, "{}", ExtM3u)?;

        if self.required_version() != ProtocolVersion::V1 {
            writeln!(f, "{}", ExtXVersion::new(self.required_version()))?;
        }

        writeln!(f, "{}", ExtXTargetDuration(self.target_duration))?;

        if self.media_sequence != 0 {
            writeln!(f, "{}", ExtXMediaSequence(self.media_sequence))?;
        }

        if self.discontinuity_sequence != 0 {
            writeln!(
                f,
                "{}",
                ExtXDiscontinuitySequence(self.discontinuity_sequence)
            )?;
        }

        if let Some(value) = &self.playlist_type {
            writeln!(f, "{}", value)?;
        }

        if self.has_i_frames_only {
            writeln!(f, "{}", ExtXIFramesOnly)?;
        }

        if self.has_independent_segments {
            writeln!(f, "{}", ExtXIndependentSegments)?;
        }

        if let Some(value) = &self.start {
            writeln!(f, "{}", value)?;
        }

        let mut available_keys = HashSet::<ExtXKey<'_>>::new();

        for segment in self.segments.values() {
            for key in &segment.keys {
                if let ExtXKey(Some(decryption_key)) = key {
                    // next segment will be encrypted, so the segment can not have an empty key
                    available_keys.remove(&ExtXKey::empty());

                    let mut decryption_key = decryption_key.clone();
                    let key = {
                        if let InitializationVector::Number(_) = decryption_key.iv {
                            // set the iv from a segment number to missing
                            // this does reduce the output size and the correct iv
                            // is automatically set, when parsing.
                            decryption_key.iv = InitializationVector::Missing;
                        }

                        ExtXKey(Some(decryption_key.clone()))
                    };

                    // only do something if a key has been overwritten
                    if available_keys.insert(key.clone()) {
                        let mut remove_key = None;

                        // an old key might be removed:
                        for k in &available_keys {
                            if let ExtXKey(Some(dk)) = k {
                                if dk.format == decryption_key.format && key != *k {
                                    remove_key = Some(k.clone());
                                    break;
                                }
                            } else {
                                unreachable!("empty keys should not exist in `available_keys`");
                            }
                        }

                        if let Some(k) = remove_key {
                            // this should always be true:
                            let res = available_keys.remove(&k);
                            debug_assert!(res);
                        }

                        writeln!(f, "{}", key)?;
                    }
                } else {
                    // the next segment is not encrypted, so remove all available keys
                    available_keys.clear();
                    available_keys.insert(ExtXKey::empty());
                    writeln!(f, "{}", key)?;
                }
            }

            write!(f, "{}", segment)?;
        }

        for value in &self.unknown {
            writeln!(f, "{}", value)?;
        }

        if self.has_end_list {
            writeln!(f, "{}", ExtXEndList)?;
        }

        Ok(())
    }
}

fn parse_media_playlist<'a>(
    input: &'a str,
    builder: &mut MediaPlaylistBuilder<'a>,
) -> crate::Result<MediaPlaylist<'a>> {
    let input = tag(input, "#EXTM3U")?;

    let mut segment = MediaSegment::builder();
    let mut segments = vec![];

    let mut has_partial_segment = false;
    let mut has_discontinuity_tag = false;
    let mut unknown = vec![];
    let mut available_keys = HashSet::new();

    for line in Lines::from(input) {
        match line? {
            Line::Tag(tag) => {
                match tag {
                    Tag::ExtInf(t) => {
                        has_partial_segment = true;
                        segment.duration(t);
                    }
                    Tag::ExtXByteRange(t) => {
                        has_partial_segment = true;
                        segment.byte_range(t);
                    }
                    Tag::ExtXDiscontinuity(_) => {
                        has_discontinuity_tag = true;
                        has_partial_segment = true;
                        segment.has_discontinuity(true);
                    }
                    Tag::ExtXKey(key) => {
                        has_partial_segment = true;

                        // An ExtXKey applies to every MediaSegment and to every Media
                        // Initialization Section declared by an ExtXMap tag, that appears
                        // between it and the next ExtXKey tag in the Playlist file with the
                        // same KEYFORMAT attribute (or the end of the Playlist file).

                        let mut is_new_key = true;
                        let mut remove = None;

                        if let ExtXKey(Some(decryption_key)) = &key {
                            for old_key in &available_keys {
                                if let ExtXKey(Some(old_decryption_key)) = &old_key {
                                    if old_decryption_key.format == decryption_key.format {
                                        // remove the old key
                                        remove = Some(old_key.clone());

                                        // there are no keys with the same format in
                                        // available_keys so the loop can stop here:
                                        break;
                                    }
                                } else {
                                    // remove an empty key
                                    remove = Some(ExtXKey::empty());
                                    break;
                                }
                            }
                        } else {
                            available_keys.clear();
                            available_keys.insert(ExtXKey::empty());
                            is_new_key = false;
                        }

                        if let Some(key) = &remove {
                            available_keys.remove(key);
                        }

                        if is_new_key {
                            available_keys.insert(key);
                        }
                    }
                    Tag::ExtXMap(mut t) => {
                        has_partial_segment = true;

                        t.keys = available_keys.iter().cloned().collect();
                        segment.map(t);
                    }
                    Tag::ExtXProgramDateTime(t) => {
                        has_partial_segment = true;
                        segment.program_date_time(t);
                    }
                    Tag::ExtXDateRange(t) => {
                        has_partial_segment = true;
                        segment.date_range(t);
                    }
                    Tag::ExtXTargetDuration(t) => {
                        builder.target_duration(t.0);
                    }
                    Tag::ExtXMediaSequence(t) => {
                        builder.media_sequence(t.0);
                    }
                    Tag::ExtXDiscontinuitySequence(t) => {
                        // this tag must appear before the first MediaSegment in the playlist
                        // https://tools.ietf.org/html/rfc8216#section-4.3.3.3
                        if !segments.is_empty() {
                            return Err(Error::custom("discontinuity sequence tag must appear before the first media segment in the playlist"));
                        }

                        // this tag must appear before any ExtXDiscontinuity tag
                        // https://tools.ietf.org/html/rfc8216#section-4.3.3.3
                        if has_discontinuity_tag {
                            return Err(Error::custom("discontinuity sequence tag must appear before any `ExtXDiscontinuity` tag"));
                        }

                        builder.discontinuity_sequence(t.0);
                    }
                    Tag::ExtXEndList(_) => {
                        builder.has_end_list(true);
                    }
                    Tag::PlaylistType(t) => {
                        builder.playlist_type(t);
                    }
                    Tag::ExtXIFramesOnly(_) => {
                        builder.has_i_frames_only(true);
                    }
                    Tag::ExtXMedia(_)
                    | Tag::VariantStream(_)
                    | Tag::ExtXSessionData(_)
                    | Tag::ExtXSessionKey(_) => {
                        return Err(Error::unexpected_tag(tag));
                    }
                    Tag::ExtXIndependentSegments(_) => {
                        builder.has_independent_segments(true);
                    }
                    Tag::ExtXStart(t) => {
                        builder.start(t);
                    }
                    Tag::ExtXVersion(_) => {}
                    Tag::Unknown(s) => {
                        // [6.3.1. General Client Responsibilities]
                        // > ignore any unrecognized tags.
                        unknown.push(Cow::Borrowed(s));
                    }
                }
            }
            Line::Uri(uri) => {
                segment.uri(uri);
                segment.keys(available_keys.iter().cloned().collect::<Vec<_>>());
                segments.push(segment.build().map_err(Error::builder)?);

                segment = MediaSegment::builder();
                has_partial_segment = false;
            }
            Line::Comment(_) => {}
        }
    }

    if has_partial_segment {
        return Err(Error::custom("Missing URI for the last `MediaSegment`"));
    }

    builder.unknown(unknown);
    builder.segments(segments);
    builder.build().map_err(Error::builder)
}

impl FromStr for MediaPlaylist<'static> {
    type Err = Error;

    fn from_str(input: &str) -> Result<Self, Self::Err> {
        Ok(parse_media_playlist(input, &mut MediaPlaylist::builder())?.into_owned())
    }
}

impl<'a> TryFrom<&'a str> for MediaPlaylist<'a> {
    type Error = Error;

    fn try_from(input: &'a str) -> Result<Self, Self::Error> {
        parse_media_playlist(input, &mut Self::builder())
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use pretty_assertions::assert_eq;

    #[test]
    fn too_large_segment_duration_test() {
        let playlist = concat!(
            "#EXTM3U\n",
            "#EXT-X-TARGETDURATION:8\n",
            "#EXT-X-VERSION:3\n",
            "#EXTINF:9.009,\n",
            "http://media.example.com/first.ts\n",
            "#EXTINF:9.509,\n",
            "http://media.example.com/second.ts\n",
            "#EXTINF:3.003,\n",
            "http://media.example.com/third.ts\n",
            "#EXT-X-ENDLIST\n"
        );

        // Error (allowable segment duration = target duration = 8)
        assert!(MediaPlaylist::try_from(playlist).is_err());

        // Error (allowable segment duration = 9)
        assert!(MediaPlaylist::builder()
            .allowable_excess_duration(Duration::from_secs(1))
            .parse(playlist)
            .is_err());

        // Ok (allowable segment duration = 10)
        assert_eq!(
            MediaPlaylist::builder()
                .allowable_excess_duration(Duration::from_secs(2))
                .parse(playlist)
                .unwrap(),
            MediaPlaylist::builder()
                .allowable_excess_duration(Duration::from_secs(2))
                .target_duration(Duration::from_secs(8))
                .segments(vec![
                    MediaSegment::builder()
                        .duration(Duration::from_secs_f64(9.009))
                        .uri("http://media.example.com/first.ts")
                        .build()
                        .unwrap(),
                    MediaSegment::builder()
                        .duration(Duration::from_secs_f64(9.509))
                        .uri("http://media.example.com/second.ts")
                        .build()
                        .unwrap(),
                    MediaSegment::builder()
                        .duration(Duration::from_secs_f64(3.003))
                        .uri("http://media.example.com/third.ts")
                        .build()
                        .unwrap(),
                ])
                .has_end_list(true)
                .build()
                .unwrap()
        );
    }

    #[test]
    fn test_segment_number_simple() {
        let playlist = MediaPlaylist::builder()
            .allowable_excess_duration(Duration::from_secs(2))
            .target_duration(Duration::from_secs(8))
            .segments(vec![
                MediaSegment::builder()
                    .duration(Duration::from_secs_f64(9.009))
                    .uri("http://media.example.com/first.ts")
                    .build()
                    .unwrap(),
                MediaSegment::builder()
                    .duration(Duration::from_secs_f64(9.509))
                    .uri("http://media.example.com/second.ts")
                    .build()
                    .unwrap(),
                MediaSegment::builder()
                    .duration(Duration::from_secs_f64(3.003))
                    .uri("http://media.example.com/third.ts")
                    .build()
                    .unwrap(),
            ])
            .build()
            .unwrap();

        let mut segments = playlist.segments.into_iter().map(|(k, v)| (k, v.number));
        assert_eq!(segments.next(), Some((0, 0)));
        assert_eq!(segments.next(), Some((1, 1)));
        assert_eq!(segments.next(), Some((2, 2)));
        assert_eq!(segments.next(), None);
    }

    #[test]
    fn test_segment_number_sequence() {
        let playlist = MediaPlaylist::builder()
            .target_duration(Duration::from_secs(8))
            .media_sequence(2680)
            .segments(vec![
                MediaSegment::builder()
                    .duration(Duration::from_secs_f64(7.975))
                    .uri("https://priv.example.com/fileSequence2680.ts")
                    .build()
                    .unwrap(),
                MediaSegment::builder()
                    .duration(Duration::from_secs_f64(7.941))
                    .uri("https://priv.example.com/fileSequence2681.ts")
                    .build()
                    .unwrap(),
                MediaSegment::builder()
                    .duration(Duration::from_secs_f64(7.975))
                    .uri("https://priv.example.com/fileSequence2682.ts")
                    .build()
                    .unwrap(),
            ])
            .build()
            .unwrap();
        let mut segments = playlist.segments.into_iter().map(|(k, v)| (k, v.number));
        assert_eq!(segments.next(), Some((0, 2680)));
        assert_eq!(segments.next(), Some((1, 2681)));
        assert_eq!(segments.next(), Some((2, 2682)));
        assert_eq!(segments.next(), None);
    }

    #[test]
    fn test_empty_playlist() {
        let playlist = "";
        assert!(MediaPlaylist::try_from(playlist).is_err());
    }
}