1use crate::QuotedOrUnquoted;
7use std::collections::HashMap;
8use std::convert::{TryFrom, TryInto};
9use std::fmt;
10use std::fmt::Display;
11use std::io::Write;
12use std::str::FromStr;
13use std::sync::atomic::{AtomicUsize, Ordering};
14use std::usize::MAX;
15use std::{f32, usize};
16
17pub static WRITE_OPT_FLOAT_PRECISION: AtomicUsize = AtomicUsize::new(MAX);
19
20macro_rules! write_some_attribute_quoted {
21 ($w:expr, $tag:expr, $o:expr) => {
22 if let &Some(ref v) = $o {
23 write!($w, "{}=\"{}\"", $tag, v)
24 } else {
25 Ok(())
26 }
27 };
28}
29
30macro_rules! write_some_attribute {
31 ($w:expr, $tag:expr, $o:expr) => {
32 if let &Some(ref v) = $o {
33 write!($w, "{}={}", $tag, v)
34 } else {
35 Ok(())
36 }
37 };
38}
39
40macro_rules! write_some_other_attributes {
41 ($w:expr, $attr:expr) => {
42 if let &Some(ref attributes) = $attr {
43 let mut status = std::io::Result::Ok(());
44 for (name, val) in attributes {
45 let res = write!($w, ",{}={}", name, val);
46 if res.is_err() {
47 status = res;
48 break;
49 }
50 }
51 status
52 } else {
53 Ok(())
54 }
55 };
56}
57
58macro_rules! is_yes {
59 ($attrs:expr, $attr:expr) => {
60 match $attrs.remove($attr) {
61 Some(QuotedOrUnquoted::Unquoted(ref s)) if s == "YES" => true,
62 Some(QuotedOrUnquoted::Unquoted(ref s)) if s == "NO" => false,
63 Some(ref s) => {
64 return Err(format!(
65 "Can't create bool from {} for {} attribute",
66 s, $attr
67 ))
68 }
69 None => false,
70 }
71 };
72}
73
74macro_rules! quoted_string {
75 ($attrs:expr, $attr:expr) => {
76 match $attrs.remove($attr) {
77 Some(QuotedOrUnquoted::Quoted(s)) => Some(s),
78 Some(QuotedOrUnquoted::Unquoted(_)) => {
79 return Err(format!(
80 "Can't create {} attribute from unquoted string",
81 $attr
82 ))
83 }
84 None => None,
85 }
86 };
87}
88
89macro_rules! unquoted_string {
90 ($attrs:expr, $attr:expr) => {
91 match $attrs.remove($attr) {
92 Some(QuotedOrUnquoted::Unquoted(s)) => Some(s),
93 Some(QuotedOrUnquoted::Quoted(_)) => {
94 return Err(format!(
95 "Can't create {} attribute from quoted string",
96 $attr
97 ))
98 }
99 None => None,
100 }
101 };
102}
103
104macro_rules! unquoted_string_parse {
105 ($attrs:expr, $attr:expr, $parse:expr) => {
106 match $attrs.remove($attr) {
107 Some(QuotedOrUnquoted::Unquoted(s)) => Some(
108 ($parse(s.as_str())).map_err(|_| format!("Can't create attribute {}", $attr))?,
109 ),
110 Some(QuotedOrUnquoted::Quoted(_)) => {
111 return Err(format!(
112 "Can't create {} attribute from quoted string",
113 $attr
114 ))
115 }
116 None => None,
117 }
118 };
119 ($attrs:expr, $attr:expr) => {
120 unquoted_string_parse!($attrs, $attr, |s: &str| s.parse())
121 };
122}
123
124macro_rules! quoted_string_parse {
125 ($attrs:expr, $attr:expr, $parse:expr) => {
126 match $attrs.remove($attr) {
127 Some(QuotedOrUnquoted::Quoted(s)) => Some(
128 ($parse(s.as_str())).map_err(|_| format!("Can't create attribute {}", $attr))?,
129 ),
130 Some(QuotedOrUnquoted::Unquoted(_)) => {
131 return Err(format!(
132 "Can't create {} attribute from unquoted string",
133 $attr
134 ))
135 }
136 None => None,
137 }
138 };
139 ($attrs:expr, $attr:expr) => {
140 quoted_string_parse!($attrs, $attr, |s: &str| s.parse())
141 };
142}
143
144#[derive(Debug, PartialEq, Clone)]
152pub enum Playlist {
153 MasterPlaylist(MasterPlaylist),
154 MediaPlaylist(MediaPlaylist),
155}
156
157impl Playlist {
158 pub fn write_to<T: Write>(&self, writer: &mut T) -> std::io::Result<()> {
159 match *self {
160 Playlist::MasterPlaylist(ref pl) => pl.write_to(writer),
161 Playlist::MediaPlaylist(ref pl) => pl.write_to(writer),
162 }
163 }
164}
165
166#[derive(Debug, Default, PartialEq, Clone)]
174pub struct MasterPlaylist {
175 pub version: Option<usize>,
176 pub variants: Vec<VariantStream>,
177 pub session_data: Vec<SessionData>,
178 pub session_key: Vec<SessionKey>,
179 pub start: Option<Start>,
180 pub independent_segments: bool,
181 pub alternatives: Vec<AlternativeMedia>, pub unknown_tags: Vec<ExtTag>,
183}
184
185impl MasterPlaylist {
186 pub fn get_newest_variant(&mut self) -> Option<&mut VariantStream> {
187 self.variants.iter_mut().rev().find(|v| !v.is_i_frame)
188 }
189
190 pub fn write_to<T: Write>(&self, w: &mut T) -> std::io::Result<()> {
191 writeln!(w, "#EXTM3U")?;
192
193 if let Some(ref v) = self.version {
194 writeln!(w, "#EXT-X-VERSION:{}", v)?;
195 }
196 if self.independent_segments {
197 writeln!(w, "#EXT-X-INDEPENDENT-SEGMENTS")?;
198 }
199
200 for alternative in &self.alternatives {
201 alternative.write_to(w)?;
202 }
203
204 for variant in &self.variants {
205 variant.write_to(w)?;
206 }
207 for session_data in &self.session_data {
208 session_data.write_to(w)?;
209 }
210 for session_key in &self.session_key {
211 session_key.write_to(w)?;
212 }
213 if let Some(ref start) = self.start {
214 start.write_to(w)?;
215 }
216 for unknown_tag in &self.unknown_tags {
217 writeln!(w, "{}", unknown_tag)?;
218 }
219
220 Ok(())
221 }
222}
223
224#[derive(Debug, Default, PartialEq, Clone)]
239pub struct VariantStream {
240 pub is_i_frame: bool,
241 pub uri: String,
242
243 pub bandwidth: u64,
245 pub average_bandwidth: Option<u64>,
246 pub codecs: Option<String>,
247 pub resolution: Option<Resolution>,
248 pub frame_rate: Option<f64>,
249 pub hdcp_level: Option<HDCPLevel>,
250 pub audio: Option<String>,
251 pub video: Option<String>,
252 pub subtitles: Option<String>,
253 pub closed_captions: Option<ClosedCaptionGroupId>,
254 pub other_attributes: Option<HashMap<String, QuotedOrUnquoted>>,
256}
257
258impl VariantStream {
259 pub(crate) fn from_hashmap(
260 mut attrs: HashMap<String, QuotedOrUnquoted>,
261 is_i_frame: bool,
262 ) -> Result<VariantStream, String> {
263 let uri = quoted_string!(attrs, "URI").unwrap_or_default();
264 let bandwidth = unquoted_string_parse!(attrs, "BANDWIDTH", |s: &str| s
266 .parse::<u64>()
267 .map_err(|err| format!("Failed to parse BANDWIDTH attribute: {}", err)))
268 .ok_or_else(|| String::from("EXT-X-STREAM-INF without mandatory BANDWIDTH attribute"))?;
269 let average_bandwidth = unquoted_string_parse!(attrs, "AVERAGE-BANDWIDTH", |s: &str| s
270 .parse::<u64>()
271 .map_err(|err| format!("Failed to parse AVERAGE-BANDWIDTH: {}", err)));
272 let codecs = quoted_string!(attrs, "CODECS");
273 let resolution = unquoted_string_parse!(attrs, "RESOLUTION");
274 let frame_rate = unquoted_string_parse!(attrs, "FRAME-RATE", |s: &str| s
275 .parse::<f64>()
276 .map_err(|err| format!("Failed to parse FRAME-RATE attribute: {}", err)));
277 let hdcp_level = unquoted_string_parse!(attrs, "HDCP-LEVEL");
278 let audio = quoted_string!(attrs, "AUDIO");
279 let video = quoted_string!(attrs, "VIDEO");
280 let subtitles = quoted_string!(attrs, "SUBTITLES");
281 let closed_captions = attrs
282 .remove("CLOSED-CAPTIONS")
283 .map(|c| c.try_into())
284 .transpose()?;
285 let other_attributes = if attrs.is_empty() { None } else { Some(attrs) };
286
287 Ok(VariantStream {
288 is_i_frame,
289 uri,
290 bandwidth,
291 average_bandwidth,
292 codecs,
293 resolution,
294 frame_rate,
295 hdcp_level,
296 audio,
297 video,
298 subtitles,
299 closed_captions,
300 other_attributes,
301 })
302 }
303
304 pub(crate) fn write_to<T: Write>(&self, w: &mut T) -> std::io::Result<()> {
305 if self.is_i_frame {
306 write!(w, "#EXT-X-I-FRAME-STREAM-INF:")?;
307 self.write_stream_inf_common_attributes(w)?;
308 writeln!(w, ",URI=\"{}\"", self.uri)
309 } else {
310 write!(w, "#EXT-X-STREAM-INF:")?;
311 self.write_stream_inf_common_attributes(w)?;
312 write_some_attribute_quoted!(w, ",AUDIO", &self.audio)?;
313 write_some_attribute_quoted!(w, ",SUBTITLES", &self.subtitles)?;
314 if let Some(ref closed_captions) = self.closed_captions {
315 match closed_captions {
316 ClosedCaptionGroupId::None => write!(w, ",CLOSED-CAPTIONS=NONE")?,
317 ClosedCaptionGroupId::GroupId(s) => write!(w, ",CLOSED-CAPTIONS=\"{}\"", s)?,
318 ClosedCaptionGroupId::Other(s) => write!(w, ",CLOSED-CAPTIONS={}", s)?,
319 }
320 }
321 writeln!(w)?;
322 writeln!(w, "{}", self.uri)
323 }
324 }
325
326 fn write_stream_inf_common_attributes<T: Write>(&self, w: &mut T) -> std::io::Result<()> {
327 write!(w, "BANDWIDTH={}", &self.bandwidth)?;
328 write_some_attribute!(w, ",AVERAGE-BANDWIDTH", &self.average_bandwidth)?;
329 write_some_attribute_quoted!(w, ",CODECS", &self.codecs)?;
330 write_some_attribute!(w, ",RESOLUTION", &self.resolution)?;
331 write_some_attribute!(w, ",FRAME-RATE", &self.frame_rate)?;
332 write_some_attribute!(w, ",HDCP-LEVEL", &self.hdcp_level)?;
333 write_some_attribute_quoted!(w, ",VIDEO", &self.video)?;
334 write_some_other_attributes!(w, &self.other_attributes)?;
335 Ok(())
336 }
337}
338
339#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Copy, Clone)]
340pub struct Resolution {
341 pub width: u64,
342 pub height: u64,
343}
344
345impl Display for Resolution {
346 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
347 write!(f, "{}x{}", self.width, self.height)
348 }
349}
350
351impl FromStr for Resolution {
352 type Err = String;
353
354 fn from_str(s: &str) -> Result<Resolution, String> {
355 match s.split_once('x') {
356 Some((width, height)) => {
357 let width = width
358 .parse::<u64>()
359 .map_err(|err| format!("Can't parse RESOLUTION attribute width: {}", err))?;
360 let height = height
361 .parse::<u64>()
362 .map_err(|err| format!("Can't parse RESOLUTION attribute height: {}", err))?;
363 Ok(Resolution { width, height })
364 }
365 None => Err(String::from("Invalid RESOLUTION attribute")),
366 }
367 }
368}
369
370#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone)]
371pub enum HDCPLevel {
372 Type0,
373 Type1,
374 None,
375 Other(String),
376}
377
378impl FromStr for HDCPLevel {
379 type Err = String;
380
381 fn from_str(s: &str) -> Result<HDCPLevel, String> {
382 match s {
383 "TYPE-0" => Ok(HDCPLevel::Type0),
384 "TYPE-1" => Ok(HDCPLevel::Type1),
385 "NONE" => Ok(HDCPLevel::None),
386 _ => Ok(HDCPLevel::Other(String::from(s))),
387 }
388 }
389}
390
391impl Display for HDCPLevel {
392 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
393 write!(
394 f,
395 "{}",
396 match self {
397 HDCPLevel::Type0 => "TYPE-0",
398 HDCPLevel::Type1 => "TYPE-1",
399 HDCPLevel::None => "NONE",
400 HDCPLevel::Other(s) => s,
401 }
402 )
403 }
404}
405
406#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone)]
408pub enum ClosedCaptionGroupId {
409 None,
410 GroupId(String),
411 Other(String),
412}
413
414impl TryFrom<QuotedOrUnquoted> for ClosedCaptionGroupId {
415 type Error = String;
416
417 fn try_from(s: QuotedOrUnquoted) -> Result<ClosedCaptionGroupId, String> {
418 match s {
419 QuotedOrUnquoted::Unquoted(s) if s == "NONE" => Ok(ClosedCaptionGroupId::None),
420 QuotedOrUnquoted::Unquoted(s) => Ok(ClosedCaptionGroupId::Other(s)),
421 QuotedOrUnquoted::Quoted(s) => Ok(ClosedCaptionGroupId::GroupId(s)),
422 }
423 }
424}
425
426#[derive(Debug, Default, PartialEq, Eq, Clone)]
435pub struct AlternativeMedia {
436 pub media_type: AlternativeMediaType,
438 pub uri: Option<String>,
439 pub group_id: String,
440 pub language: Option<String>,
441 pub assoc_language: Option<String>,
442 pub name: String, pub default: bool, pub autoselect: bool, pub forced: bool, pub instream_id: Option<InstreamId>,
447 pub characteristics: Option<String>,
448 pub channels: Option<String>,
449 pub other_attributes: Option<HashMap<String, QuotedOrUnquoted>>,
450}
451
452impl AlternativeMedia {
453 pub(crate) fn from_hashmap(
454 mut attrs: HashMap<String, QuotedOrUnquoted>,
455 ) -> Result<AlternativeMedia, String> {
456 let media_type = unquoted_string_parse!(attrs, "TYPE")
457 .ok_or_else(|| String::from("EXT-X-MEDIA without mandatory TYPE attribute"))?;
458 let uri = quoted_string!(attrs, "URI");
459
460 if media_type == AlternativeMediaType::ClosedCaptions && uri.is_some() {
461 return Err(String::from(
462 "URI attribute must not be included in CLOSED-CAPTIONS Alternative Medias",
463 ));
464 }
465
466 let group_id = quoted_string!(attrs, "GROUP-ID")
467 .ok_or_else(|| String::from("EXT-X-MEDIA without mandatory GROUP-ID attribute"))?;
468 let language = quoted_string!(attrs, "LANGUAGE");
469 let assoc_language = quoted_string!(attrs, "ASSOC-LANGUAGE");
470 let name = quoted_string!(attrs, "NAME")
471 .ok_or_else(|| String::from("EXT-X-MEDIA without mandatory NAME attribute"))?;
472 let default = is_yes!(attrs, "DEFAULT");
473 let autoselect = is_yes!(attrs, "AUTOSELECT");
474
475 if media_type != AlternativeMediaType::Subtitles && attrs.contains_key("FORCED") {
476 return Err(String::from(
477 "FORCED attribute must not be included in non-SUBTITLE Alternative Medias",
478 ));
479 }
480 let forced = is_yes!(attrs, "FORCED");
481
482 if media_type != AlternativeMediaType::ClosedCaptions && attrs.contains_key("INSTREAM-ID") {
483 return Err(String::from("INSTREAM-ID attribute must not be included in non-CLOSED-CAPTIONS Alternative Medias"));
484 } else if media_type == AlternativeMediaType::ClosedCaptions
485 && !attrs.contains_key("INSTREAM-ID")
486 {
487 return Err(String::from(
488 "INSTREAM-ID attribute must be included in CLOSED-CAPTIONS Alternative Medias",
489 ));
490 }
491 let instream_id = quoted_string_parse!(attrs, "INSTREAM-ID");
492 let characteristics = quoted_string!(attrs, "CHARACTERISTICS");
493 let channels = quoted_string!(attrs, "CHANNELS");
494 let other_attributes = if attrs.is_empty() { None } else { Some(attrs) };
495
496 Ok(AlternativeMedia {
497 media_type,
498 uri,
499 group_id,
500 language,
501 assoc_language,
502 name,
503 default,
504 autoselect,
505 forced,
506 instream_id,
507 characteristics,
508 channels,
509 other_attributes,
510 })
511 }
512
513 pub(crate) fn write_to<T: Write>(&self, w: &mut T) -> std::io::Result<()> {
514 write!(w, "#EXT-X-MEDIA:")?;
515 write!(w, "TYPE={}", self.media_type)?;
516 if self.media_type != AlternativeMediaType::ClosedCaptions {
517 write_some_attribute_quoted!(w, ",URI", &self.uri)?;
518 }
519 write!(w, ",GROUP-ID=\"{}\"", self.group_id)?;
520 write_some_attribute_quoted!(w, ",LANGUAGE", &self.language)?;
521 write_some_attribute_quoted!(w, ",ASSOC-LANGUAGE", &self.assoc_language)?;
522 write!(w, ",NAME=\"{}\"", self.name)?;
523 if self.default {
524 write!(w, ",DEFAULT=YES")?;
525 }
526 if self.autoselect {
527 write!(w, ",AUTOSELECT=YES")?;
528 }
529 if self.forced && self.media_type == AlternativeMediaType::Subtitles {
530 write!(w, ",FORCED=YES")?;
531 }
532 if self.media_type == AlternativeMediaType::ClosedCaptions {
533 write_some_attribute_quoted!(w, ",INSTREAM-ID", &self.instream_id)?;
535 }
536 write_some_attribute_quoted!(w, ",CHARACTERISTICS", &self.characteristics)?;
537 write_some_attribute_quoted!(w, ",CHANNELS", &self.channels)?;
538 write_some_other_attributes!(w, &self.other_attributes)?;
539 writeln!(w)
540 }
541}
542
543#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone)]
544pub enum AlternativeMediaType {
545 Audio,
546 Video,
547 Subtitles,
548 ClosedCaptions,
549 Other(String),
550}
551
552impl FromStr for AlternativeMediaType {
553 type Err = String;
554
555 fn from_str(s: &str) -> Result<AlternativeMediaType, String> {
556 match s {
557 "AUDIO" => Ok(AlternativeMediaType::Audio),
558 "VIDEO" => Ok(AlternativeMediaType::Video),
559 "SUBTITLES" => Ok(AlternativeMediaType::Subtitles),
560 "CLOSED-CAPTIONS" => Ok(AlternativeMediaType::ClosedCaptions),
561 _ => Ok(AlternativeMediaType::Other(String::from(s))),
562 }
563 }
564}
565
566impl Default for AlternativeMediaType {
567 fn default() -> AlternativeMediaType {
568 AlternativeMediaType::Video
569 }
570}
571
572impl Display for AlternativeMediaType {
573 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
574 write!(
575 f,
576 "{}",
577 match self {
578 AlternativeMediaType::Audio => "AUDIO",
579 AlternativeMediaType::Video => "VIDEO",
580 AlternativeMediaType::Subtitles => "SUBTITLES",
581 AlternativeMediaType::ClosedCaptions => "CLOSED-CAPTIONS",
582 AlternativeMediaType::Other(s) => s.as_str(),
583 }
584 )
585 }
586}
587
588#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone)]
589pub enum InstreamId {
590 CC(u8),
591 Service(u8),
592 Other(String),
593}
594
595impl FromStr for InstreamId {
596 type Err = String;
597
598 fn from_str(s: &str) -> Result<InstreamId, String> {
599 if let Some(cc) = s.strip_prefix("CC") {
600 let cc = cc
601 .parse::<u8>()
602 .map_err(|err| format!("Unable to create InstreamId from {:?}: {}", s, err))?;
603 Ok(InstreamId::CC(cc))
604 } else if let Some(service) = s.strip_prefix("SERVICE") {
605 let service = service
606 .parse::<u8>()
607 .map_err(|err| format!("Unable to create InstreamId from {:?}: {}", s, err))?;
608 Ok(InstreamId::Service(service))
609 } else {
610 Ok(InstreamId::Other(String::from(s)))
611 }
612 }
613}
614
615impl Display for InstreamId {
616 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
617 match self {
618 InstreamId::CC(cc) => write!(f, "CC{}", cc),
619 InstreamId::Service(service) => write!(f, "SERVICE{}", service),
620 InstreamId::Other(s) => write!(f, "{}", s),
621 }
622 }
623}
624
625#[derive(Debug, Default, PartialEq, Eq, Clone)]
630pub struct SessionKey(pub Key);
631
632impl SessionKey {
633 pub(crate) fn write_to<T: Write>(&self, w: &mut T) -> std::io::Result<()> {
634 write!(w, "#EXT-X-SESSION-KEY:")?;
635 self.0.write_attributes_to(w)?;
636 writeln!(w)
637 }
638}
639
640#[derive(Debug, PartialEq, Eq, Clone)]
641pub enum SessionDataField {
642 Value(String),
643 Uri(String),
644}
645
646#[derive(Debug, PartialEq, Eq, Clone)]
650pub struct SessionData {
651 pub data_id: String,
652 pub field: SessionDataField,
653 pub language: Option<String>,
654 pub other_attributes: Option<HashMap<String, QuotedOrUnquoted>>,
655}
656
657impl SessionData {
658 pub(crate) fn from_hashmap(
659 mut attrs: HashMap<String, QuotedOrUnquoted>,
660 ) -> Result<SessionData, String> {
661 let data_id = quoted_string!(attrs, "DATA-ID")
662 .ok_or_else(|| String::from("EXT-X-SESSION-DATA field without DATA-ID attribute"))?;
663
664 let value = quoted_string!(attrs, "VALUE");
665 let uri = quoted_string!(attrs, "URI");
666
667 let field = match (value, uri) {
670 (Some(value), None) => SessionDataField::Value(value),
671 (None, Some(uri)) => SessionDataField::Uri(uri),
672 (Some(_), Some(_)) => {
673 return Err(format![
674 "EXT-X-SESSION-DATA tag {} contains both a value and an URI",
675 data_id
676 ])
677 }
678 (None, None) => {
679 return Err(format![
680 "EXT-X-SESSION-DATA tag {} must contain either a value or an URI",
681 data_id
682 ])
683 }
684 };
685
686 let language = quoted_string!(attrs, "LANGUAGE");
687 let other_attributes = if attrs.is_empty() { None } else { Some(attrs) };
688
689 Ok(SessionData {
690 data_id,
691 field,
692 language,
693 other_attributes,
694 })
695 }
696
697 pub(crate) fn write_to<T: Write>(&self, w: &mut T) -> std::io::Result<()> {
698 write!(w, "#EXT-X-SESSION-DATA:")?;
699 write!(w, "DATA-ID=\"{}\"", self.data_id)?;
700 match &self.field {
701 SessionDataField::Value(value) => write!(w, ",VALUE=\"{}\"", value)?,
702 SessionDataField::Uri(uri) => write!(w, ",URI=\"{}\"", uri)?,
703 };
704 write_some_attribute_quoted!(w, ",LANGUAGE", &self.language)?;
705 write_some_other_attributes!(w, &self.other_attributes)?;
706 writeln!(w)
707 }
708}
709
710#[derive(Debug, Default, PartialEq, Clone)]
718pub struct MediaPlaylist {
719 pub version: Option<usize>,
720 pub target_duration: u64,
722 pub media_sequence: u64,
724 pub segments: Vec<MediaSegment>,
725 pub discontinuity_sequence: u64,
727 pub end_list: bool,
729 pub playlist_type: Option<MediaPlaylistType>,
731 pub i_frames_only: bool,
733 pub start: Option<Start>,
735 pub independent_segments: bool,
737 pub unknown_tags: Vec<ExtTag>,
739}
740
741impl MediaPlaylist {
742 pub fn write_to<T: Write>(&self, w: &mut T) -> std::io::Result<()> {
743 writeln!(w, "#EXTM3U")?;
744
745 if let Some(ref v) = self.version {
746 writeln!(w, "#EXT-X-VERSION:{}", v)?;
747 }
748 if self.independent_segments {
749 writeln!(w, "#EXT-X-INDEPENDENT-SEGMENTS")?;
750 }
751 writeln!(w, "#EXT-X-TARGETDURATION:{}", self.target_duration)?;
752
753 if self.media_sequence != 0 {
754 writeln!(w, "#EXT-X-MEDIA-SEQUENCE:{}", self.media_sequence)?;
755 }
756 if self.discontinuity_sequence != 0 {
757 writeln!(
758 w,
759 "#EXT-X-DISCONTINUITY-SEQUENCE:{}",
760 self.discontinuity_sequence
761 )?;
762 }
763 if let Some(ref v) = self.playlist_type {
764 writeln!(w, "#EXT-X-PLAYLIST-TYPE:{}", v)?;
765 }
766 if self.i_frames_only {
767 writeln!(w, "#EXT-X-I-FRAMES-ONLY")?;
768 }
769 if let Some(ref start) = self.start {
770 start.write_to(w)?;
771 }
772 for segment in &self.segments {
773 segment.write_to(w)?;
774 }
775 if self.end_list {
776 writeln!(w, "#EXT-X-ENDLIST")?;
777 }
778
779 Ok(())
780 }
781}
782
783#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone)]
785pub enum MediaPlaylistType {
786 Event,
787 Vod,
788 Other(String),
789}
790
791impl FromStr for MediaPlaylistType {
792 type Err = String;
793
794 fn from_str(s: &str) -> Result<MediaPlaylistType, String> {
795 match s {
796 "EVENT" => Ok(MediaPlaylistType::Event),
797 "VOD" => Ok(MediaPlaylistType::Vod),
798 _ => Ok(MediaPlaylistType::Other(String::from(s))),
799 }
800 }
801}
802
803impl Display for MediaPlaylistType {
804 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
805 write!(
806 f,
807 "{}",
808 match self {
809 MediaPlaylistType::Event => "EVENT",
810 MediaPlaylistType::Vod => "VOD",
811 MediaPlaylistType::Other(s) => s,
812 }
813 )
814 }
815}
816
817impl Default for MediaPlaylistType {
818 fn default() -> MediaPlaylistType {
819 MediaPlaylistType::Event
820 }
821}
822
823#[derive(Debug, Default, PartialEq, Clone)]
830pub struct MediaSegment {
831 pub uri: String,
832 pub duration: f32,
834 pub title: Option<String>,
836 pub byte_range: Option<ByteRange>,
838 pub discontinuity: bool,
840 pub key: Option<Key>,
842 pub map: Option<Map>,
844 pub program_date_time: Option<chrono::DateTime<chrono::FixedOffset>>,
846 pub daterange: Option<DateRange>,
848 pub unknown_tags: Vec<ExtTag>,
850}
851
852impl MediaSegment {
853 pub fn empty() -> MediaSegment {
854 Default::default()
855 }
856
857 pub(crate) fn write_to<T: Write>(&self, w: &mut T) -> std::io::Result<()> {
858 if let Some(ref map) = self.map {
859 write!(w, "#EXT-X-MAP:")?;
860 map.write_attributes_to(w)?;
861 writeln!(w)?;
862 }
863 if let Some(ref byte_range) = self.byte_range {
864 write!(w, "#EXT-X-BYTERANGE:")?;
865 byte_range.write_value_to(w)?;
866 writeln!(w)?;
867 }
868 if self.discontinuity {
869 writeln!(w, "#EXT-X-DISCONTINUITY")?;
870 }
871 if let Some(ref key) = self.key {
872 write!(w, "#EXT-X-KEY:")?;
873 key.write_attributes_to(w)?;
874 writeln!(w)?;
875 }
876 if let Some(ref v) = self.program_date_time {
877 writeln!(
878 w,
879 "#EXT-X-PROGRAM-DATE-TIME:{}",
880 v.to_rfc3339_opts(chrono::SecondsFormat::Millis, true)
881 )?;
882 }
883 if let Some(ref v) = self.daterange {
884 write!(w, "#EXT-X-DATERANGE:")?;
885 v.write_attributes_to(w)?;
886 writeln!(w)?;
887 }
888 for unknown_tag in &self.unknown_tags {
889 writeln!(w, "{}", unknown_tag)?;
890 }
891
892 match WRITE_OPT_FLOAT_PRECISION.load(Ordering::Relaxed) {
893 MAX => {
894 write!(w, "#EXTINF:{},", self.duration)?;
895 }
896 n => {
897 write!(w, "#EXTINF:{:.*},", n, self.duration)?;
898 }
899 };
900
901 if let Some(ref v) = self.title {
902 writeln!(w, "{}", v)?;
903 } else {
904 writeln!(w)?;
905 }
906
907 writeln!(w, "{}", self.uri)
908 }
909}
910
911#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone)]
912pub enum KeyMethod {
913 None,
914 AES128,
915 SampleAES,
916 Other(String),
917}
918
919impl Default for KeyMethod {
920 fn default() -> Self {
921 KeyMethod::None
922 }
923}
924
925impl FromStr for KeyMethod {
926 type Err = String;
927
928 fn from_str(s: &str) -> Result<KeyMethod, String> {
929 match s {
930 "NONE" => Ok(KeyMethod::None),
931 "AES-128" => Ok(KeyMethod::AES128),
932 "SAMPLE-AES" => Ok(KeyMethod::SampleAES),
933 _ => Ok(KeyMethod::Other(String::from(s))),
934 }
935 }
936}
937
938impl Display for KeyMethod {
939 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
940 write!(
941 f,
942 "{}",
943 match self {
944 KeyMethod::None => "NONE",
945 KeyMethod::AES128 => "AES-128",
946 KeyMethod::SampleAES => "SAMPLE-AES",
947 KeyMethod::Other(s) => s,
948 }
949 )
950 }
951}
952
953#[derive(Debug, Default, PartialEq, Eq, Clone)]
962pub struct Key {
963 pub method: KeyMethod,
964 pub uri: Option<String>,
965 pub iv: Option<String>,
966 pub keyformat: Option<String>,
967 pub keyformatversions: Option<String>,
968}
969
970impl Key {
971 pub(crate) fn from_hashmap(
972 mut attrs: HashMap<String, QuotedOrUnquoted>,
973 ) -> Result<Key, String> {
974 let method: KeyMethod = unquoted_string_parse!(attrs, "METHOD")
975 .ok_or_else(|| String::from("EXT-X-KEY without mandatory METHOD attribute"))?;
976
977 let uri = quoted_string!(attrs, "URI");
978 let iv = unquoted_string!(attrs, "IV");
979 if method == KeyMethod::None && iv.is_none() {
980 return Err("IV is required unless METHOD is NONE".parse().unwrap());
981 }
982 let keyformat = quoted_string!(attrs, "KEYFORMAT");
983 let keyformatversions = quoted_string!(attrs, "KEYFORMATVERSIONS");
984
985 Ok(Key {
986 method,
987 uri,
988 iv,
989 keyformat,
990 keyformatversions,
991 })
992 }
993
994 pub fn write_attributes_to<T: Write>(&self, w: &mut T) -> std::io::Result<()> {
995 write!(w, "METHOD={}", self.method)?;
996 write_some_attribute_quoted!(w, ",URI", &self.uri)?;
997 write_some_attribute!(w, ",IV", &self.iv)?;
998 write_some_attribute_quoted!(w, ",KEYFORMAT", &self.keyformat)?;
999 write_some_attribute_quoted!(w, ",KEYFORMATVERSIONS", &self.keyformatversions)
1000 }
1001}
1002
1003#[derive(Debug, Default, PartialEq, Eq, Clone)]
1012pub struct Map {
1013 pub uri: String,
1014 pub byte_range: Option<ByteRange>,
1015 pub other_attributes: HashMap<String, QuotedOrUnquoted>,
1016}
1017
1018impl Map {
1019 pub fn write_attributes_to<T: Write>(&self, w: &mut T) -> std::io::Result<()> {
1020 write!(w, "URI=\"{}\"", self.uri)?;
1021 if let Some(ref byte_range) = self.byte_range {
1022 write!(w, ",BYTERANGE=\"")?;
1023 byte_range.write_value_to(w)?;
1024 write!(w, "\"")?;
1025 }
1026 Ok(())
1027 }
1028}
1029
1030#[derive(Debug, Default, PartialEq, Eq, Clone)]
1036pub struct ByteRange {
1037 pub length: u64,
1038 pub offset: Option<u64>,
1039}
1040
1041impl ByteRange {
1042 pub fn write_value_to<T: Write>(&self, w: &mut T) -> std::io::Result<()> {
1043 write!(w, "{}", self.length)?;
1044 if let Some(offset) = self.offset {
1045 write!(w, "@{}", offset)?;
1046 }
1047 Ok(())
1048 }
1049}
1050
1051#[derive(Debug, PartialEq, Clone)]
1057pub struct DateRange {
1058 pub id: String,
1059 pub class: Option<String>,
1060 pub start_date: chrono::DateTime<chrono::FixedOffset>,
1061 pub end_date: Option<chrono::DateTime<chrono::FixedOffset>>,
1062 pub duration: Option<f64>,
1063 pub planned_duration: Option<f64>,
1064 pub x_prefixed: Option<HashMap<String, QuotedOrUnquoted>>, pub end_on_next: bool,
1066 pub other_attributes: Option<HashMap<String, QuotedOrUnquoted>>,
1067}
1068
1069impl DateRange {
1070 pub fn from_hashmap(mut attrs: HashMap<String, QuotedOrUnquoted>) -> Result<DateRange, String> {
1071 let id = quoted_string!(attrs, "ID")
1072 .ok_or_else(|| String::from("EXT-X-DATERANGE without mandatory ID attribute"))?;
1073 let class = quoted_string!(attrs, "CLASS");
1074 let start_date =
1075 quoted_string_parse!(attrs, "START-DATE", chrono::DateTime::parse_from_rfc3339)
1076 .ok_or_else(|| {
1077 String::from("EXT-X-DATERANGE without mandatory START-DATE attribute")
1078 })?;
1079 let end_date =
1080 quoted_string_parse!(attrs, "END-DATE", chrono::DateTime::parse_from_rfc3339);
1081 let duration = unquoted_string_parse!(attrs, "DURATION", |s: &str| s
1082 .parse::<f64>()
1083 .map_err(|err| format!("Failed to parse DURATION attribute: {}", err)));
1084 let planned_duration = unquoted_string_parse!(attrs, "PLANNED-DURATION", |s: &str| s
1085 .parse::<f64>()
1086 .map_err(|err| format!("Failed to parse PLANNED-DURATION attribute: {}", err)));
1087 let end_on_next = is_yes!(attrs, "END-ON-NEXT");
1088 let mut x_prefixed = HashMap::new();
1089 let mut other_attributes = HashMap::new();
1090 for (k, v) in attrs.into_iter() {
1091 if k.starts_with("X-") {
1092 x_prefixed.insert(k, v);
1093 } else {
1094 other_attributes.insert(k, v);
1095 }
1096 }
1097
1098 Ok(DateRange {
1099 id,
1100 class,
1101 start_date,
1102 end_date,
1103 duration,
1104 planned_duration,
1105 x_prefixed: if x_prefixed.is_empty() {
1106 None
1107 } else {
1108 Some(x_prefixed)
1109 },
1110 end_on_next,
1111 other_attributes: if other_attributes.is_empty() {
1112 None
1113 } else {
1114 Some(other_attributes)
1115 },
1116 })
1117 }
1118
1119 pub fn write_attributes_to<T: Write>(&self, w: &mut T) -> std::io::Result<()> {
1120 write_some_attribute_quoted!(w, "ID", &Some(&self.id))?;
1121 write_some_attribute_quoted!(w, ",CLASS", &self.class)?;
1122 write_some_attribute_quoted!(w, ",START-DATE", &Some(&self.start_date.to_rfc3339()))?;
1123 write_some_attribute_quoted!(
1124 w,
1125 ",END-DATE",
1126 &self.end_date.as_ref().map(|dt| dt.to_rfc3339())
1127 )?;
1128 write_some_attribute!(w, ",DURATION", &self.duration)?;
1129 write_some_attribute!(w, ",PLANNED-DURATION", &self.planned_duration)?;
1130 if let Some(x_prefixed) = &self.x_prefixed {
1131 for (name, attr) in x_prefixed {
1132 write!(w, ",{}={}", name, attr)?;
1133 }
1134 }
1135 if self.end_on_next {
1136 write!(w, ",END-ON-NEXT=YES")?;
1137 }
1138 if let Some(other_attributes) = &self.other_attributes {
1139 for (name, attr) in other_attributes {
1140 write!(w, ",{}={}", name, attr)?;
1141 }
1142 }
1143 Ok(())
1144 }
1145}
1146
1147#[derive(Debug, Default, PartialEq, Clone)]
1157pub struct Start {
1158 pub time_offset: f64,
1159 pub precise: Option<bool>,
1160 pub other_attributes: HashMap<String, QuotedOrUnquoted>,
1161}
1162
1163impl Start {
1164 pub(crate) fn from_hashmap(
1165 mut attrs: HashMap<String, QuotedOrUnquoted>,
1166 ) -> Result<Start, String> {
1167 let time_offset = unquoted_string_parse!(attrs, "TIME-OFFSET", |s: &str| s
1168 .parse::<f64>()
1169 .map_err(|err| format!("Failed to parse TIME-OFFSET attribute: {}", err)))
1170 .ok_or_else(|| String::from("EXT-X-START without mandatory TIME-OFFSET attribute"))?;
1171 Ok(Start {
1172 time_offset,
1173 precise: is_yes!(attrs, "PRECISE").into(),
1174 other_attributes: attrs,
1175 })
1176 }
1177
1178 pub(crate) fn write_to<T: Write>(&self, w: &mut T) -> std::io::Result<()> {
1179 write!(w, "#EXT-X-START:TIME-OFFSET={}", self.time_offset)?;
1180 if let Some(precise) = self.precise {
1181 if precise {
1182 write!(w, ",PRECISE=YES")?;
1183 }
1184 }
1185 writeln!(w)?;
1186
1187 Ok(())
1188 }
1189}
1190
1191#[derive(Debug, Default, PartialEq, Eq, Clone)]
1193pub struct ExtTag {
1194 pub tag: String,
1195 pub rest: Option<String>,
1196}
1197
1198impl Display for ExtTag {
1199 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1200 write!(f, "#EXT-{}", self.tag)?;
1201 if let Some(v) = &self.rest {
1202 write!(f, ":{}", v)?;
1203 }
1204 Ok(())
1205 }
1206}
1207
1208#[cfg(test)]
1209mod test {
1210 use super::*;
1211
1212 #[test]
1213 fn ext_tag_with_value_is_printable() {
1214 let cue_out_tag = ExtTag {
1215 tag: "X-CUE-OUT".into(),
1216 rest: Some("DURATION=30".into()),
1217 };
1218
1219 let mut output = Vec::new();
1220 write!(output, "{}", cue_out_tag).unwrap();
1221
1222 assert_eq!(
1223 std::str::from_utf8(output.as_slice()).unwrap(),
1224 "#EXT-X-CUE-OUT:DURATION=30"
1225 )
1226 }
1227
1228 #[test]
1229 fn ext_tag_without_value_is_printable() {
1230 let cue_in_tag = ExtTag {
1231 tag: "X-CUE-IN".into(),
1232 rest: None,
1233 };
1234
1235 let mut output = Vec::new();
1236 write!(output, "{}", cue_in_tag).unwrap();
1237
1238 assert_eq!(
1239 std::str::from_utf8(output.as_slice()).unwrap(),
1240 "#EXT-X-CUE-IN"
1241 )
1242 }
1243}