manifest_filter/
lib.rs

1//! manifest-filter is a lib used to modify video manifests.
2//!
3//! # Table of contents
4//!
5//! - [Features](#features)
6//! - [Examples](#examples)
7//!
8//! # Features
9//!
10//! - Modify master playlists (filter variants by bandwidth, fps, etc)
11//! - Modify media playlists (DVR, trim segments)
12//!
13//! More features are coming soon.
14//!
15//! # Examples
16//!
17//! You can try the example below, used to filter only the variants that are 30fps.
18//!
19//! ```rust
20//! use manifest_filter::Master;
21//! use std::io::Read;
22//!
23//! let mut file = std::fs::File::open("manifests/master.m3u8").unwrap();
24//! let mut content: Vec<u8> = Vec::new();
25//! file.read_to_end(&mut content).unwrap();
26//!
27//! let (_, master_playlist) = m3u8_rs::parse_master_playlist(&content).unwrap();
28//! let mut master = Master {
29//!     playlist: master_playlist,
30//! };
31//! master.filter_fps(Some(30.0));
32//! ```
33//!
34//! The result should be something like this
35//!
36//! ```not_rust
37//! #EXTM3U
38//! #EXT-X-VERSION:4
39//! #EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID="audio-aach-96",LANGUAGE="en",NAME="English",DEFAULT=YES,AUTOSELECT=YES,CHANNELS="2"
40//! #EXT-X-STREAM-INF:BANDWIDTH=600000,AVERAGE-BANDWIDTH=600000,CODECS="mp4a.40.5,avc1.64001F",RESOLUTION=384x216,FRAME-RATE=30,AUDIO="audio-aach-96",CLOSED-CAPTIONS=NONE
41//! variant-audio_1=96000-video=249984.m3u8
42//! #EXT-X-STREAM-INF:BANDWIDTH=800000,AVERAGE-BANDWIDTH=800000,CODECS="mp4a.40.5,avc1.64001F",RESOLUTION=768x432,FRAME-RATE=30,AUDIO="audio-aach-96",CLOSED-CAPTIONS=NONE
43//! variant-audio_1=96000-video=1320960.m3u8
44//! ```
45//!
46//! All functions can be chained. Call `filter_fps` to first remove variants with
47//! a frame rate different of the one choosen and call `filter_bandwith` right after to
48//! remove variants thare don't fit into the max/min range expected.
49//! The sampe applies for `Media`.
50
51use m3u8_rs::{MasterPlaylist, MediaPlaylist, Playlist};
52
53pub fn load_master(content: &[u8]) -> Result<MasterPlaylist, String> {
54    match m3u8_rs::parse_playlist(content) {
55        Result::Ok((_, Playlist::MasterPlaylist(pl))) => Ok(pl),
56        Result::Ok((_, Playlist::MediaPlaylist(_))) => Err("must be a master playlist".to_string()),
57        Result::Err(e) => Err(e.to_string()),
58    }
59}
60
61pub fn load_media(content: &[u8]) -> Result<MediaPlaylist, String> {
62    match m3u8_rs::parse_playlist(content) {
63        Result::Ok((_, Playlist::MediaPlaylist(pl))) => Ok(pl),
64        Result::Ok((_, Playlist::MasterPlaylist(_))) => Err("must be a media playlist".to_string()),
65        Result::Err(e) => Err(e.to_string()),
66    }
67}
68
69/// `Master` holds a reference to the master playlist. All
70/// functions implemented by this struct can be chained.
71pub struct Master {
72    pub playlist: MasterPlaylist,
73}
74
75/// `Media` holds a reference to the media playlist. All
76/// functions implemented by this struct can be chained.
77pub struct Media {
78    pub playlist: MediaPlaylist,
79}
80
81impl Master {
82    /// Filter variants from a master playlist based on the frame rate passed.
83    pub fn filter_fps(&mut self, rate: Option<f64>) -> &mut Self {
84        if let Some(r) = rate {
85            self.playlist.variants.retain(|v| v.frame_rate == Some(r));
86        }
87        self
88    }
89
90    /// Filter variants from a master playlist based on the bandwidh passed.
91    ///
92    /// Variants can be filtered using `min` and `max` values for bandwidth.
93    ///
94    /// There's no need to pass a `min` value if you don't need to. The
95    /// same happens for `max` value. For `min` we will set to zero by default
96    /// and for the `max` we'll use the `u64::MAX` value.
97    pub fn filter_bandwidth(&mut self, min: Option<u64>, max: Option<u64>) -> &mut Self {
98        let min = min.unwrap_or(0);
99        let max = max.unwrap_or(u64::MAX);
100
101        self.playlist
102            .variants
103            .retain(|v| v.bandwidth >= min && v.bandwidth <= max);
104        self
105    }
106
107    /// Set the first variant by index to appear in the playlist for the one that
108    /// best suites the device needs. Most of the times such feature will
109    /// be used to skip the initial variant (too low for some devices).
110    ///
111    /// If the `index` passed in could cause "out of bounds" error, the playlist
112    /// will keep untouched.
113    ///
114    /// # Arguments
115    /// * `index` - an Option containing the index you want to be the first variant. Variants will be swapped.
116    pub fn first_variant_by_index(&mut self, index: Option<u64>) -> &mut Self {
117        if let Some(i) = index {
118            if i as usize <= self.playlist.variants.len() {
119                self.playlist.variants.swap(0, i.try_into().unwrap());
120            }
121        }
122        self
123    }
124
125    /// Set the first variant by closes bandwidth to appear in the playlist for the one that
126    /// best suites the device needs. Most of the times such feature will
127    /// be used to skip the initial variant (too low for some devices).
128    ///
129    /// # Arguments
130    /// * `closest_bandwidth` - an Option containing an approximate bandwidth value you want for the first variant.
131    pub fn first_variant_by_closest_bandwidth(
132        &mut self,
133        closest_bandwidth: Option<u64>,
134    ) -> &mut Self {
135        if let Some(c) = closest_bandwidth {
136            let (idx, _) = self
137                .playlist
138                .variants
139                .iter()
140                .enumerate()
141                .min_by_key(|(_, v)| (c as i64 - v.bandwidth as i64).abs())
142                .unwrap();
143            let fv = self.playlist.variants.remove(idx);
144            self.playlist.variants.insert(0, fv);
145        }
146        self
147    }
148}
149
150impl Media {
151    /// Remove segments backwards from the media playlist, based on the duration
152    /// set. The duration is in seconds.
153    /// Media sequence will be affected: `<https://datatracker.ietf.org/doc/html/rfc8216#section-4.3.3.2>`
154    pub fn filter_dvr(&mut self, seconds: Option<u64>) -> &mut Self {
155        let mut acc = 0;
156        let total_segments = self.playlist.segments.len();
157
158        if let Some(s) = seconds {
159            self.playlist.segments = self
160                .playlist
161                .segments
162                .iter()
163                .rev()
164                .take_while(|segment| {
165                    acc += segment.duration as u64;
166                    acc <= s
167                })
168                .cloned()
169                .collect();
170            self.playlist.media_sequence += (total_segments - self.playlist.segments.len()) as u64;
171        }
172        self
173    }
174
175    /// Remove segments from the media playlist, based on the start/end passed.
176    /// Media sequence will be affected: `<https://datatracker.ietf.org/doc/html/rfc8216#section-4.3.3.2>`
177    pub fn trim(&mut self, start: Option<u64>, end: Option<u64>) -> &mut Self {
178        let start = start.unwrap_or(0);
179        let end = end.unwrap_or_else(|| self.playlist.segments.len().try_into().unwrap());
180
181        let segments = &self.playlist.segments[start as usize..end as usize];
182        let total_segments = self.playlist.segments.len();
183        self.playlist.segments = segments.to_vec();
184        self.playlist.media_sequence += (total_segments - self.playlist.segments.len()) as u64;
185        self
186    }
187}
188
189#[cfg(test)]
190mod tests {
191    use super::*;
192    use std::io::Read;
193
194    fn build_master() -> Master {
195        let mut file = std::fs::File::open("manifests/master.m3u8").unwrap();
196        let mut content: Vec<u8> = Vec::new();
197        file.read_to_end(&mut content).unwrap();
198
199        let (_, master_playlist) = m3u8_rs::parse_master_playlist(&content).unwrap();
200        Master {
201            playlist: master_playlist,
202        }
203    }
204
205    fn build_media() -> Media {
206        let mut file = std::fs::File::open("manifests/media.m3u8").unwrap();
207        let mut content: Vec<u8> = Vec::new();
208        file.read_to_end(&mut content).unwrap();
209
210        let (_, media_playlist) = m3u8_rs::parse_media_playlist(&content).unwrap();
211        Media {
212            playlist: media_playlist,
213        }
214    }
215
216    #[test]
217    fn filter_60_fps() {
218        let mut master = build_master();
219        master.filter_fps(Some(60.0));
220
221        assert_eq!(master.playlist.variants.len(), 2);
222    }
223
224    #[test]
225    fn filter_min_bandwidth() {
226        let mut master = build_master();
227
228        master.filter_bandwidth(Some(800000), None);
229
230        assert_eq!(master.playlist.variants.len(), 3);
231    }
232
233    #[test]
234    fn filter_max_bandwidth() {
235        let mut master = build_master();
236
237        master.filter_bandwidth(None, Some(800000));
238
239        assert_eq!(master.playlist.variants.len(), 6);
240    }
241
242    #[test]
243    fn filter_min_and_max_bandwidth() {
244        let mut master = build_master();
245
246        master.filter_bandwidth(Some(800000), Some(2000000));
247
248        assert_eq!(master.playlist.variants.len(), 3);
249    }
250
251    #[test]
252    fn set_first_variant_by_index() {
253        let mut master = build_master();
254
255        master.first_variant_by_index(Some(1));
256
257        assert_eq!(master.playlist.variants[0].bandwidth, 800000);
258        assert_eq!(master.playlist.variants[1].bandwidth, 600000);
259    }
260
261    #[test]
262    fn set_first_variant_by_out_of_bounds_index() {
263        let mut master = build_master();
264
265        master.first_variant_by_index(Some(100));
266
267        assert_eq!(master.playlist.variants[0].bandwidth, 600000);
268        assert_eq!(master.playlist.variants[1].bandwidth, 800000);
269    }
270
271    #[test]
272    fn set_first_variant_by_closest_bandwidth() {
273        let mut master = build_master();
274
275        master.first_variant_by_closest_bandwidth(Some(1650000));
276        assert_eq!(master.playlist.variants[0].bandwidth, 1500000);
277        assert_eq!(master.playlist.variants[1].bandwidth, 600000);
278    }
279
280    #[test]
281    fn filter_dvr_with_short_duration() {
282        let mut media = build_media();
283
284        media.filter_dvr(Some(15));
285
286        assert_eq!(media.playlist.segments.len(), 3);
287        assert_eq!(media.playlist.media_sequence, 320035373);
288    }
289
290    #[test]
291    fn filter_dvr_with_long_duration() {
292        let mut media = build_media();
293
294        media.filter_dvr(Some(u64::MAX));
295
296        assert_eq!(media.playlist.segments.len(), 20);
297        assert_eq!(media.playlist.media_sequence, 320035356);
298    }
299
300    #[test]
301    fn trim_media_playlist_with_start_only() {
302        let mut media = build_media();
303
304        media.trim(Some(5), None);
305
306        assert_eq!(media.playlist.segments.len(), 15);
307        assert_eq!(media.playlist.media_sequence, 320035361);
308    }
309
310    #[test]
311    fn trim_media_playlist_with_end_only() {
312        let mut media = build_media();
313
314        media.trim(None, Some(5));
315
316        assert_eq!(media.playlist.segments.len(), 5);
317        assert_eq!(media.playlist.media_sequence, 320035371);
318    }
319
320    #[test]
321    fn trim_media_playlist_with_start_and_end() {
322        let mut media = build_media();
323
324        media.trim(Some(5), Some(18));
325
326        assert_eq!(media.playlist.segments.len(), 13);
327        assert_eq!(media.playlist.media_sequence, 320035363);
328    }
329}