Skip to content

Commit e3ed7dc

Browse files
authored
feat(hls): support EXT-X-DISCONTINUITY-SEQUENCE and EXT-X-DISCONTINUITY (#50)
1 parent 1f99b50 commit e3ed7dc

File tree

8 files changed

+126
-17
lines changed

8 files changed

+126
-17
lines changed

crates/iori-hls/src/m3u8_rs.rs

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -76,23 +76,37 @@ impl From<m3u8_rs::Resolution> for Resolution {
7676

7777
impl From<m3u8_rs::MediaPlaylist> for MediaPlaylist {
7878
fn from(value: m3u8_rs::MediaPlaylist) -> Self {
79+
let mut current_part_index = value.discontinuity_sequence;
80+
let mut segments = Vec::with_capacity(value.segments.len());
81+
82+
for segment in value.segments {
83+
if segment.discontinuity {
84+
current_part_index += 1;
85+
}
86+
87+
let segment: crate::models::MediaSegment = (segment, current_part_index).into();
88+
segments.push(segment);
89+
}
90+
7991
MediaPlaylist {
8092
media_sequence: value.media_sequence,
81-
segments: value.segments.into_iter().map(MediaSegment::from).collect(),
93+
segments,
8294
end_list: value.end_list,
95+
discontinuity_sequence: value.discontinuity_sequence,
8396
}
8497
}
8598
}
8699

87-
impl From<m3u8_rs::MediaSegment> for MediaSegment {
88-
fn from(value: m3u8_rs::MediaSegment) -> Self {
100+
impl From<(m3u8_rs::MediaSegment, u64)> for MediaSegment {
101+
fn from((value, part_index): (m3u8_rs::MediaSegment, u64)) -> Self {
89102
MediaSegment {
90103
uri: value.uri,
91104
duration: (value.duration as f64).into(),
92105
title: value.title,
93106
byte_range: value.byte_range.map(ByteRange::from),
94107
key: value.key.map(Key::from),
95108
map: value.map.map(Map::from),
109+
part_index,
96110
}
97111
}
98112
}

crates/iori-hls/src/models.rs

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,7 @@ impl From<hls::MediaType> for AlternativeMediaType {
104104
#[derive(Debug, Clone, PartialEq, Comparable)]
105105
pub struct MediaPlaylist {
106106
pub media_sequence: u64,
107+
pub discontinuity_sequence: u64,
107108
pub segments: Vec<MediaSegment>,
108109
pub end_list: bool,
109110
}
@@ -116,6 +117,7 @@ pub struct MediaSegment {
116117
pub byte_range: Option<ByteRange>,
117118
pub key: Option<Key>,
118119
pub map: Option<Map>,
120+
pub part_index: u64,
119121
}
120122

121123
impl<'a>
@@ -125,15 +127,17 @@ impl<'a>
125127
Option<ByteRange>,
126128
Option<Key>,
127129
Option<Map>,
130+
u64,
128131
)> for MediaSegment
129132
{
130133
fn from(
131-
(inf, uri, byte_range, key, map): (
134+
(inf, uri, byte_range, key, map, part_index): (
132135
hls::Inf<'a>,
133136
Cow<'a, str>,
134137
Option<ByteRange>,
135138
Option<Key>,
136139
Option<Map>,
140+
u64,
137141
),
138142
) -> Self {
139143
Self {
@@ -146,6 +150,7 @@ impl<'a>
146150
byte_range,
147151
key,
148152
map,
153+
part_index,
149154
}
150155
}
151156
}

crates/iori-hls/src/parse.rs

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,18 +13,28 @@ pub fn parse_playlist_res(input: &[u8]) -> Result<Playlist, M3u8ParseError> {
1313

1414
let mut is_master = false;
1515

16-
// master playlist
16+
// <FOR MASTER PLAYLIST>
1717
let mut variants = Vec::new();
1818
let mut alternatives = Vec::new();
1919

20-
// media playlist
20+
// <FOR MEDIA PLAYLIST>
21+
// [RFC8216 Section 4.3.3.2](https://datatracker.ietf.org/doc/html/rfc8216#section-4.3.3.2)
22+
// > If the Media Playlist file does not contain an EXT-X-MEDIA-SEQUENCE
23+
// > tag, then the Media Sequence Number of the first Media Segment in the
24+
// > Media Playlist SHALL be considered to be 0.
2125
let mut media_sequence = 0;
26+
// [RFC8216 Section 4.3.3.3](https://datatracker.ietf.org/doc/html/rfc8216#section-4.3.3.3)
27+
// > If the Media Playlist does not contain an EXT-X-DISCONTINUITY-
28+
// > SEQUENCE tag, then the Discontinuity Sequence Number of the first
29+
// > Media Segment in the Playlist SHALL be considered to be 0.
30+
let mut discontinuity_sequence = 0;
2231
let mut segments: Vec<MediaSegment> = Vec::new();
2332
let mut end_list = false;
2433

2534
// Maps(initial segment information) and keys(encryption information)
2635
let mut current_key: Option<Key> = None;
2736
let mut current_map: Option<Map> = None;
37+
let mut current_part_index = 0;
2838

2939
// Pending tags, which should be cleared after the URI line is processed
3040
let mut pending_inf: Option<hls::Inf> = None;
@@ -35,6 +45,13 @@ pub fn parse_playlist_res(input: &[u8]) -> Result<Playlist, M3u8ParseError> {
3545
match line {
3646
HlsLine::KnownTag(KnownTag::Hls(tag)) => match tag {
3747
hls::Tag::MediaSequence(seq) => media_sequence = seq.media_sequence(),
48+
hls::Tag::DiscontinuitySequence(seq) => {
49+
discontinuity_sequence = seq.discontinuity_sequence();
50+
current_part_index = discontinuity_sequence;
51+
}
52+
hls::Tag::Discontinuity(_) => {
53+
current_part_index += 1;
54+
}
3855
hls::Tag::Inf(inf) => pending_inf = Some(inf),
3956
hls::Tag::Byterange(range) => pending_byterange = Some(range.into()),
4057
hls::Tag::Key(key) => current_key = Some(key.into()),
@@ -63,6 +80,7 @@ pub fn parse_playlist_res(input: &[u8]) -> Result<Playlist, M3u8ParseError> {
6380
pending_byterange.take(),
6481
current_key.clone(),
6582
current_map.clone(),
83+
current_part_index,
6684
)
6785
.into(),
6886
);
@@ -84,6 +102,7 @@ pub fn parse_playlist_res(input: &[u8]) -> Result<Playlist, M3u8ParseError> {
84102
} else {
85103
Playlist::MediaPlaylist(MediaPlaylist {
86104
media_sequence,
105+
discontinuity_sequence,
87106
segments,
88107
end_list,
89108
})

crates/iori-hls/tests/cases.rs

Lines changed: 0 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -22,17 +22,6 @@ fn test_accuracy_archive_01() {
2222
assert_changes!(old_result, new_result, Changed::Unchanged);
2323
}
2424

25-
#[test]
26-
fn test_accuracy_archive_02() {
27-
let data = include_bytes!("./fixtures/archive_02.m3u8");
28-
let old_result = iori_hls::m3u8_rs::parse_playlist_res(data);
29-
let new_result = iori_hls::parse::parse_playlist_res(data);
30-
31-
let old_result = old_result.expect("Old parse engine should not error");
32-
let new_result = new_result.expect("New parse engine should not error");
33-
assert_changes!(old_result, new_result, Changed::Unchanged);
34-
}
35-
3625
#[test]
3726
fn test_accuracy_archive_03() {
3827
let data = include_bytes!("./fixtures/archive_03.m3u8");
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
use comparable::{Changed, assert_changes};
2+
3+
#[test]
4+
fn test_accuracy_discontinuity() {
5+
let data = include_bytes!("./fixtures/discontinuity/playlist.m3u8");
6+
let old_result = iori_hls::m3u8_rs::parse_playlist_res(data);
7+
let new_result = iori_hls::parse::parse_playlist_res(data);
8+
9+
let old_result = old_result.expect("Old parse engine should not error");
10+
let new_result = new_result.expect("New parse engine should not error");
11+
assert_changes!(old_result, new_result, Changed::Unchanged);
12+
13+
let media_playlist = match new_result {
14+
iori_hls::Playlist::MediaPlaylist(media_playlist) => media_playlist,
15+
_ => panic!("Expected media playlist"),
16+
};
17+
assert_eq!(media_playlist.discontinuity_sequence, 5);
18+
19+
let segments = media_playlist.segments;
20+
assert_eq!(segments.len(), 3);
21+
assert_eq!(segments[0].part_index, 5);
22+
assert_eq!(segments[1].part_index, 6);
23+
assert_eq!(segments[2].part_index, 7);
24+
}
25+
26+
#[test]
27+
fn test_accuracy_discontinuity_02() {
28+
let data = include_bytes!("./fixtures/discontinuity/playlist2.m3u8");
29+
let old_result = iori_hls::m3u8_rs::parse_playlist_res(data);
30+
let new_result = iori_hls::parse::parse_playlist_res(data);
31+
32+
let old_result = old_result.expect("Old parse engine should not error");
33+
let new_result = new_result.expect("New parse engine should not error");
34+
assert_changes!(old_result, new_result, Changed::Unchanged);
35+
}
36+
37+
#[test]
38+
fn test_accuracy_discontinuity_no_seq() {
39+
let data = include_bytes!("./fixtures/discontinuity/no_seq.m3u8");
40+
let old_result = iori_hls::m3u8_rs::parse_playlist_res(data);
41+
let new_result = iori_hls::parse::parse_playlist_res(data);
42+
43+
let old_result = old_result.expect("Old parse engine should not error");
44+
let new_result = new_result.expect("New parse engine should not error");
45+
assert_changes!(old_result, new_result, Changed::Unchanged);
46+
47+
let media_playlist = match new_result {
48+
iori_hls::Playlist::MediaPlaylist(media_playlist) => media_playlist,
49+
_ => panic!("Expected media playlist"),
50+
};
51+
assert_eq!(media_playlist.discontinuity_sequence, 0);
52+
53+
let segments = media_playlist.segments;
54+
assert_eq!(segments.len(), 2);
55+
assert_eq!(segments[0].part_index, 0);
56+
assert_eq!(segments[1].part_index, 1);
57+
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
#EXTM3U
2+
#EXT-X-TARGETDURATION:10
3+
#EXT-X-VERSION:3
4+
#EXT-X-MEDIA-SEQUENCE:0
5+
#EXTINF:10.0,
6+
segment1.ts
7+
#EXT-X-DISCONTINUITY
8+
#EXTINF:10.0,
9+
segment2.ts
10+
#EXT-X-ENDLIST
11+
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
#EXTM3U
2+
#EXT-X-TARGETDURATION:10
3+
#EXT-X-VERSION:3
4+
#EXT-X-MEDIA-SEQUENCE:0
5+
#EXT-X-DISCONTINUITY-SEQUENCE:5
6+
#EXTINF:10.0,
7+
segment1.ts
8+
#EXT-X-DISCONTINUITY
9+
#EXTINF:10.0,
10+
segment2.ts
11+
#EXT-X-DISCONTINUITY
12+
#EXTINF:10.0,
13+
segment3.ts
14+
#EXT-X-ENDLIST
File renamed without changes.

0 commit comments

Comments
 (0)