xref: /xiu/protocol/hls/src/m3u8.rs (revision b754b692)
1 use {
2     super::{errors::MediaError, ts::Ts},
3     bytes::BytesMut,
4     std::{collections::VecDeque, fs, fs::File, io::Write},
5 };
6 
7 pub struct Segment {
8     /*ts duration*/
9     pub duration: i64,
10     pub discontinuity: bool,
11     /*ts name*/
12     pub name: String,
13     path: String,
14     pub is_eof: bool,
15 }
16 
17 impl Segment {
new( duration: i64, discontinuity: bool, name: String, path: String, is_eof: bool, ) -> Self18     pub fn new(
19         duration: i64,
20         discontinuity: bool,
21         name: String,
22         path: String,
23         is_eof: bool,
24     ) -> Self {
25         Self {
26             duration,
27             discontinuity,
28             name,
29             path,
30             is_eof,
31         }
32     }
33 }
34 
35 pub struct M3u8 {
36     version: u16,
37     sequence_no: u64,
38     /*What duration should media files be?
39     A duration of 10 seconds of media per file seems to strike a reasonable balance for most broadcast content.
40     http://devimages.apple.com/iphone/samples/bipbop/bipbopall.m3u8*/
41     duration: i64,
42     /*How many files should be listed in the index file during a continuous, ongoing session?
43     The normal recommendation is 3, but the optimum number may be larger.*/
44     live_ts_count: usize,
45 
46     segments: VecDeque<Segment>,
47 
48     m3u8_folder: String,
49     live_m3u8_name: String,
50 
51     ts_handler: Ts,
52 
53     need_record: bool,
54     vod_m3u8_content: String,
55     vod_m3u8_name: String,
56 }
57 
58 impl M3u8 {
new( duration: i64, live_ts_count: usize, app_name: String, stream_name: String, need_record: bool, ) -> Self59     pub fn new(
60         duration: i64,
61         live_ts_count: usize,
62         app_name: String,
63         stream_name: String,
64         need_record: bool,
65     ) -> Self {
66         let m3u8_folder = format!("./{app_name}/{stream_name}");
67         fs::create_dir_all(m3u8_folder.clone()).unwrap();
68 
69         let live_m3u8_name = format!("{stream_name}.m3u8");
70         let vod_m3u8_name = if need_record {
71             format!("vod_{stream_name}.m3u8")
72         } else {
73             String::default()
74         };
75 
76         let mut m3u8 = Self {
77             version: 3,
78             sequence_no: 0,
79             duration,
80             live_ts_count,
81             segments: VecDeque::new(),
82             m3u8_folder,
83             live_m3u8_name,
84             ts_handler: Ts::new(app_name, stream_name),
85             // record,
86             need_record,
87             vod_m3u8_content: String::default(),
88             vod_m3u8_name,
89         };
90 
91         if need_record {
92             m3u8.vod_m3u8_content = m3u8.generate_m3u8_header(true);
93         }
94         m3u8
95     }
96 
add_segment( &mut self, duration: i64, discontinuity: bool, is_eof: bool, ts_data: BytesMut, ) -> Result<(), MediaError>97     pub fn add_segment(
98         &mut self,
99         duration: i64,
100         discontinuity: bool,
101         is_eof: bool,
102         ts_data: BytesMut,
103     ) -> Result<(), MediaError> {
104         let segment_count = self.segments.len();
105 
106         if segment_count >= self.live_ts_count {
107             let segment = self.segments.pop_front().unwrap();
108             if !self.need_record {
109                 self.ts_handler.delete(segment.path);
110             }
111 
112             self.sequence_no += 1;
113         }
114         self.duration = std::cmp::max(duration, self.duration);
115         let (ts_name, ts_path) = self.ts_handler.write(ts_data)?;
116         let segment = Segment::new(duration, discontinuity, ts_name, ts_path, is_eof);
117 
118         if self.need_record {
119             self.update_vod_m3u8(&segment);
120         }
121 
122         self.segments.push_back(segment);
123 
124         Ok(())
125     }
126 
clear(&mut self) -> Result<(), MediaError>127     pub fn clear(&mut self) -> Result<(), MediaError> {
128         if self.need_record {
129             let vod_m3u8_path = format!("{}/{}", self.m3u8_folder, self.vod_m3u8_name);
130             let mut file_handler = File::create(vod_m3u8_path).unwrap();
131             self.vod_m3u8_content += "#EXT-X-ENDLIST\n";
132             file_handler.write_all(self.vod_m3u8_content.as_bytes())?;
133         } else {
134             for segment in &self.segments {
135                 self.ts_handler.delete(segment.path.clone());
136             }
137         }
138 
139         //clear live m3u8
140         let live_m3u8_path = format!("{}/{}", self.m3u8_folder, self.live_m3u8_name);
141         fs::remove_file(live_m3u8_path)?;
142 
143         Ok(())
144     }
145 
generate_m3u8_header(&self, is_vod: bool) -> String146     pub fn generate_m3u8_header(&self, is_vod: bool) -> String {
147         let mut m3u8_header = "#EXTM3U\n".to_string();
148         m3u8_header += format!("#EXT-X-VERSION:{}\n", self.version).as_str();
149         m3u8_header += format!("#EXT-X-TARGETDURATION:{}\n", (self.duration + 999) / 1000).as_str();
150 
151         if is_vod {
152             m3u8_header += "#EXT-X-MEDIA-SEQUENCE:0\n";
153             m3u8_header += "#EXT-X-PLAYLIST-TYPE:VOD\n";
154             m3u8_header += "#EXT-X-ALLOW-CACHE:YES\n";
155         } else {
156             m3u8_header += format!("#EXT-X-MEDIA-SEQUENCE:{}\n", self.sequence_no).as_str();
157         }
158 
159         m3u8_header
160     }
161 
refresh_playlist(&mut self) -> Result<String, MediaError>162     pub fn refresh_playlist(&mut self) -> Result<String, MediaError> {
163         let mut m3u8_content = self.generate_m3u8_header(false);
164 
165         for segment in &self.segments {
166             if segment.discontinuity {
167                 m3u8_content += "#EXT-X-DISCONTINUITY\n";
168             }
169             m3u8_content += format!(
170                 "#EXTINF:{:.3}\n{}\n",
171                 segment.duration as f64 / 1000.0,
172                 segment.name
173             )
174             .as_str();
175 
176             if segment.is_eof {
177                 m3u8_content += "#EXT-X-ENDLIST\n";
178                 break;
179             }
180         }
181 
182         let m3u8_path = format!("{}/{}", self.m3u8_folder, self.live_m3u8_name);
183 
184         let mut file_handler = File::create(m3u8_path).unwrap();
185         file_handler.write_all(m3u8_content.as_bytes())?;
186 
187         Ok(m3u8_content)
188     }
189 
update_vod_m3u8(&mut self, segment: &Segment)190     pub fn update_vod_m3u8(&mut self, segment: &Segment) {
191         if segment.discontinuity {
192             self.vod_m3u8_content += "#EXT-X-DISCONTINUITY\n";
193         }
194         self.vod_m3u8_content += format!(
195             "#EXTINF:{:.3}\n{}\n",
196             segment.duration as f64 / 1000.0,
197             segment.name
198         )
199         .as_str();
200     }
201 }
202