hls_playlist/
lib.rs

1#![doc = include_str!("../README.md")]
2// Copyright 2024 Logan Wemyss
3//
4// Licensed under the Apache License, Version 2.0 (the "License");
5// you may not use this file except in compliance with the License.
6// You may obtain a copy of the License at
7//
8//     http://www.apache.org/licenses/LICENSE-2.0
9//
10// Unless required by applicable law or agreed to in writing, software
11// distributed under the License is distributed on an "AS IS" BASIS,
12// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13// See the License for the specific language governing permissions and
14// limitations under the License.
15#![warn(clippy::pedantic, clippy::nursery, clippy::enum_glob_use)]
16#![allow(
17    clippy::module_name_repetitions,
18    clippy::too_many_lines,
19    clippy::cognitive_complexity
20)]
21#![cfg_attr(docsrs, feature(doc_cfg))]
22
23use std::{collections::HashMap, io, num::NonZeroU8};
24
25pub mod playlist;
26pub mod tags;
27
28#[cfg_attr(docsrs, doc(cfg(feature = "steering-manifest")))]
29#[cfg(feature = "steering-manifest")]
30pub mod steering_manifest;
31
32/// The priority in which a given rendition should be chosen over another rendition.
33#[derive(Debug, Clone, PartialEq, Eq)]
34pub enum RenditionPlaybackPriority {
35    /// Indicates that the Rendition contains content that is considered essential to play.
36    Default,
37
38    /// The client may choose to play this Rendition in the absence of explicit user
39    /// preference because it matches the current playback environment, such as
40    /// chosen system language.
41    AutoSelect,
42
43    /// This rendition may not be auto selected without explicit user preference.
44    None,
45}
46
47/// Specifies a Rendition within the segments in the `MediaPlaylist`.
48#[derive(Debug, Clone, PartialEq, Eq)]
49pub enum InStreamId {
50    /// Line 21 Data Services channel.
51    Cc1,
52
53    /// Line 21 Data Services channel.
54    Cc2,
55
56    /// Line 21 Data Services channel.
57    Cc3,
58
59    /// Line 21 Data Services channel.
60    Cc4,
61
62    /// A Digital Television Closed Captioning service block number between 1 and 63.
63    Service(NonZeroU8),
64}
65
66/// Information about the audio channels in a given rendition.
67#[derive(Debug, Clone, PartialEq, Eq)]
68pub enum AudioChannelInformation {
69    NumberOfChannelsOnly {
70        /// The count of audio channels.
71        number_of_channels: u64,
72    },
73    WithAudioCodingIdentifiers {
74        /// The count of audio channels.
75        number_of_channels: u64,
76
77        /// A list of Audio Coding Identifiers.
78        audio_coding_identifiers: Vec<String>,
79    },
80    WithSpecialUsageIdentifiers {
81        /// The count of audio channels.
82        number_of_channels: u64,
83
84        /// A list of Audio Coding Identifiers.
85        audio_coding_identifiers: Vec<String>,
86
87        /// The audio is binaural.
88        binaural: bool,
89
90        /// The audio is pre-processed content that should not be dynamically spatialized.
91        immersive: bool,
92
93        /// The audio is a downmix derivative of some other audio.
94        downmix: bool,
95    },
96}
97
98/// Metadata for a given stream.
99#[derive(Debug, Clone, PartialEq)]
100pub struct StreamInf {
101    /// Represents the peak segment bit rate of the Stream.
102    pub bandwidth_bits_per_second: u64,
103
104    /// Represents the average segment bit rate of the Stream.
105    pub average_bandwidth_bits_per_second: Option<u64>,
106
107    /// An abstract, relative measure of the playback quality-of-experience
108    /// of the Variant Stream.
109    pub score: Option<f64>,
110
111    /// A list of formats, where each format specifies a media sample type
112    /// that is present in the Stream.
113    pub codecs: Vec<String>,
114
115    /// Describes media samples with both a backward-compatible base layer
116    /// and a newer enhancement layer.
117    pub supplemental_codecs: Vec<SupplementalCodec>,
118
119    /// Describes the optimal pixel resolution at which to display the
120    /// video in the Stream.
121    pub resolution: Option<Resolution>,
122
123    /// Indicates that the stream could fail to play unless the
124    /// output is protected by High-bandwidth Digital Content Protection.
125    pub hdcp_level: Option<HdcpLevel>,
126
127    /// Indicates that the playback of the stream containing encrypted
128    /// `MediaSegments` is to be restricted to devices that guarantee
129    /// a certain level of content protection robustness.
130    pub allowed_cpc: Vec<ContentProtectionConfiguration>,
131    pub video_range: VideoRange,
132
133    /// Indicates whether the video content in the Stream requires
134    /// specialized rendering to be properly displayed.
135    pub required_video_layout: Vec<VideoChannelSpecifier>,
136
137    /// Allows the URI of the Stream to change between two distinct
138    /// downloads of the `MultivariantPlaylist`.
139    pub stable_variant_id: Option<String>,
140
141    /// Indicates that the Variant Stream belongs to the identified
142    /// Content Steering Pathway.
143    pub pathway_id: Option<String>,
144}
145
146/// Describes media samples with both a backward-compatible base layer
147/// and a newer enhancement layer.
148#[derive(Debug, Clone, PartialEq, Eq)]
149pub struct SupplementalCodec {
150    supplemental_codec: String,
151    compatibility_brands: Vec<String>,
152}
153
154/// The High-bandwidth Digital Content Protection level.
155#[derive(Debug, Clone, PartialEq, Eq)]
156pub enum HdcpLevel {
157    /// No High-bandwidth Digital Content Protection.
158    None,
159
160    /// Type 0 High-bandwidth Digital Content Protection.
161    Type0,
162
163    /// Type 1 High-bandwidth Digital Content Protection.
164    Type1,
165}
166
167/// A video resolution in pixels.
168#[derive(Debug, Clone, PartialEq, Eq)]
169pub struct Resolution {
170    pub width: u64,
171    pub height: u64,
172}
173
174/// Represents required content protection robustness for a given `key_format`
175#[derive(Debug, Clone, PartialEq, Eq)]
176pub struct ContentProtectionConfiguration {
177    pub key_format: String,
178
179    /// Classes of playback device that implements the `key_format`
180    /// with a certain level of content protection robustness.
181    pub cpc_labels: Vec<String>,
182}
183
184#[derive(Debug, Clone, PartialEq, Eq)]
185pub enum VideoRange {
186    Sdr,
187    Hlg,
188    Pq,
189    Other(String),
190}
191
192/// Indicates whether some video content is stereoscopic or not.
193#[derive(Debug, Clone, PartialEq, Eq)]
194pub enum VideoChannelSpecifier {
195    Stereo,
196    Mono,
197}
198
199/// Arbitrary session data.
200#[derive(Debug, Clone, PartialEq, Eq)]
201pub struct SessionData {
202    /// Identifies a particular `SessionData`.
203    pub data_id: String,
204
205    /// The data value.
206    pub value: crate::SessionDataValue,
207}
208
209/// Whether the data is stored inline or identified by a URI.
210#[derive(Debug, Clone, PartialEq, Eq)]
211pub enum SessionDataValue {
212    /// The data is stored inline.
213    Value {
214        /// The data value.
215        value: String,
216
217        /// The language that the value is in.
218        language: Option<String>,
219    },
220
221    /// The data is identified by a URI.
222    Uri {
223        /// The URI identifying the data value.
224        uri: String,
225
226        /// The format of the data identified by the URI.
227        format: UriFormat,
228    },
229}
230
231/// The format of the data value.
232#[derive(Debug, Clone, PartialEq, Eq, Default)]
233pub enum UriFormat {
234    /// The value is json data.
235    #[default]
236    Json,
237
238    /// The value is a binary file.
239    Raw,
240}
241
242/// Information about the encryption method of a given `MediaSegment`.
243#[derive(Debug, Clone, PartialEq, Eq)]
244pub enum EncryptionMethod {
245    Aes128 {
246        /// A URI that specifies how to obtain the key.
247        uri: String,
248
249        /// Specifies an initialization vector to be used with the key.
250        iv: Option<u128>,
251
252        /// Specifies how the key is represented in the resource identified by the URI.
253        key_format: KeyFormat,
254
255        /// Which versions of the `key_format` are this key is in compliance with.
256        key_format_versions: Vec<u64>,
257    },
258    SampleAes {
259        /// A URI that specifies how to obtain the key.
260        uri: String,
261
262        /// Specifies an initialization vector to be used with the key.
263        iv: Option<u128>,
264
265        /// Which versions of the `key_format` are this key is in compliance with.
266        key_format_versions: Vec<u64>,
267    },
268    SampleAesCtr {
269        /// A URI that specifies how to obtain the key.
270        uri: String,
271
272        /// Which versions of the `key_format` are this key is in compliance with.
273        key_format_versions: Vec<u64>,
274    },
275}
276
277/// Specifies how a given encryption key is represented.
278#[derive(Debug, Clone, PartialEq, Eq)]
279pub enum KeyFormat {
280    Identity,
281    Other(String),
282}
283
284/// Identifies a [`steering_manifest::SteeringManifest`].
285#[derive(Debug, Clone, PartialEq, Eq)]
286pub struct ContentSteering {
287    /// The URI identifying the [`steering_manifest::SteeringManifest`].
288    pub server_uri: String,
289    pub pathway_id: Option<String>,
290}
291
292/// A duration of time with specific attributes.
293#[derive(Debug, Clone, PartialEq)]
294pub struct DateRange {
295    /// Uniquely identifies the `DateRange` in a given Playlist.
296    pub id: String,
297
298    /// Identifies some set of attributes and their associated value semantics
299    /// for `client_attributes`.
300    pub class: Option<String>,
301
302    /// The time at which the `DateRange` begins.
303    pub start_date: chrono::DateTime<chrono::FixedOffset>,
304
305    /// Indicates when to trigger an action associated with the `DateRange`.
306    pub cue: Option<DateRangeCue>,
307
308    /// The time at which the `DateRange` ends.
309    pub end_date: Option<chrono::DateTime<chrono::FixedOffset>>,
310
311    /// The duration of the `DateRange` in seconds.
312    pub duration_seconds: Option<f64>,
313
314    /// The duration that the `DateRange` is expected to be in seconds.
315    pub planned_duration_seconds: Option<f64>,
316
317    /// Various client defined attributes. Keys are prefixed with `X-` and
318    /// unprefixed on serialization and deserialization respectively.
319    pub client_attributes: HashMap<String, AttributeValue>,
320
321    /// Used to carry SCTE-35 data.
322    pub scte35_cmd: Vec<u8>,
323
324    /// Used to carry SCTE-35 data.
325    pub scte35_in: Vec<u8>,
326
327    /// Used to carry SCTE-35 data
328    pub scte35_out: Vec<u8>,
329
330    /// Indicates that the end of the `DateRange` is equal to the `start_date`
331    /// of the range that is closest in time after this `DateRange` and has the same schema
332    /// of `client_attributes`.
333    pub end_on_next: bool,
334}
335
336/// When to trigger an action associated with a given `DateRange`.
337#[derive(Debug, Clone, PartialEq, Eq)]
338pub struct DateRangeCue {
339    /// Indicates that an action is to be triggered once and never again.
340    pub once: bool,
341
342    /// The relative time at which the action is to be triggered.
343    pub position: DateRangeCuePosition,
344}
345
346/// The relative time at which a given `DateRange` action is to be triggered.
347#[derive(Debug, Clone, PartialEq, Eq)]
348pub enum DateRangeCuePosition {
349    /// Indicates that an action is to be triggered before
350    /// playback of the primary asset begins.
351    Pre,
352
353    /// Indicates that an action is to be triggered after the
354    /// primary asset has been played to its end without error.
355    Post,
356
357    Neither,
358}
359
360#[derive(Debug, Clone, PartialEq)]
361pub enum AttributeValue {
362    String(String),
363    Bytes(Vec<u8>),
364    Float(f64),
365}
366
367/// A hint that the client should request a resource before
368/// it is available to be delivered.
369#[derive(Debug, Clone, PartialEq, Eq)]
370pub struct PreloadHint {
371    /// Whether the resource is a `PartialSegment` or a `MediaInitializationSection`.
372    pub hint_type: PreloadHintType,
373
374    /// The URI to the hinted resource.
375    pub uri: String,
376
377    /// The byte offset of the first byte of the hinted resource, from
378    /// the beginning of the resource identified by the URI.
379    pub start_byte_offset: u64,
380
381    /// If Some, the value is the length of the hinted resource.
382    /// If None, the last byte of the hinted resource is the last byte of the
383    /// resource identified by the URI.
384    pub length_in_bytes: Option<u64>,
385}
386
387/// Whether a given resource is a `PartialSegment` or a `MediaInitializationSection`.
388#[derive(Debug, Clone, PartialEq, Eq)]
389pub enum PreloadHintType {
390    /// The resource is a `PartialSegment`.
391    Part,
392
393    /// The resource is a `MediaInitializationSection`.
394    Map,
395}
396
397/// Represents a range of bytes in a given resource.
398#[derive(Debug, Clone, PartialEq, Eq)]
399pub struct ByteRange {
400    /// The length of the range in bytes.
401    pub length_bytes: u64,
402
403    /// The offset from the start of the resource to the start of the range
404    /// in bytes.
405    pub start_offset_bytes: Option<u64>,
406}
407
408/// Represents a range of bytes in a given resource.
409#[derive(Debug, Clone, PartialEq, Eq)]
410pub struct ByteRangeWithOffset {
411    /// The length of the range in bytes.
412    pub length_bytes: u64,
413
414    /// The offset from the start of the resource to the start of the range
415    /// in bytes.
416    pub start_offset_bytes: u64,
417}
418
419/// If `Event`, Media Segments can only be added to the end of the Media Playlist.
420/// If `Vod`, the Media Playlist cannot change.
421#[derive(Debug, Clone, PartialEq, Eq)]
422pub enum PlaylistType {
423    Event,
424    Vod,
425}
426
427/// Information about the server's playlist delta update capabilities.
428#[derive(Debug, Clone, PartialEq)]
429pub struct DeltaUpdateInfo {
430    pub skip_boundary_seconds: f64,
431
432    /// if the Server can produce Playlist Delta Updates that skip
433    /// older EXT-X-DATERANGE tags in addition to Media Segments.
434    pub can_skip_dateranges: bool,
435}
436
437/// Information about an associated Rendition that is as up-to-date as
438/// the Playlist that contains the report.
439#[derive(Debug, Clone, PartialEq, Eq)]
440pub struct RenditionReport {
441    /// The URI for the `MediaPlaylist` of the specified rendition.
442    pub uri: String,
443
444    /// The media sequence number of the last `MediaSegment` currently
445    /// in the specified Rendition.
446    pub last_sequence_number: Option<u64>,
447
448    /// The part index of the last `PartialSegment` currently in the
449    /// specified rendition whose media sequence number is equal to
450    /// `last_sequence_number`.
451    pub last_part_index: Option<u64>,
452}
453
454#[derive(Debug, Clone, PartialEq, Eq)]
455pub enum DefinitionType {
456    /// The variable is defined here.
457    Inline { name: String, value: String },
458
459    /// Use a variable defined in the Multivariant Playlist that referenced
460    /// this playlist.
461    Import { name: String },
462
463    /// Use the value of the query parameter named `name` from the current
464    /// playlist's URI. If the URI is redirected, look for the query
465    /// parameter in the 30x response URI.
466    QueryParameter { name: String },
467}
468
469#[derive(Debug, Clone, PartialEq)]
470pub enum FloatOrInteger {
471    Float(f64),
472    Integer(u64),
473}
474
475impl ByteRange {
476    fn serialize(&self, mut output: impl io::Write) -> io::Result<()> {
477        write!(output, "{}", self.length_bytes)?;
478        if let Some(start_offset_bytes) = self.start_offset_bytes {
479            write!(output, "@{start_offset_bytes}")?;
480        }
481
482        Ok(())
483    }
484}
485
486impl ByteRangeWithOffset {
487    fn serialize(&self, mut output: impl io::Write) -> io::Result<()> {
488        write!(output, "{}@{}", self.length_bytes, self.start_offset_bytes)
489    }
490}
491
492impl EncryptionMethod {
493    fn serialize(&self, mut output: impl io::Write) -> io::Result<()> {
494        match self {
495            Self::Aes128 { uri, .. } => write!(output, "METHOD=AES-128,URI=\"{uri}\"")?,
496            Self::SampleAes { uri, .. } => write!(output, "METHOD=SAMPLE-AES,URI=\"{uri}\"")?,
497            Self::SampleAesCtr { uri, .. } => {
498                write!(output, "METHOD=SAMPLE-AES-CTR,URI=\"{uri}\"")?;
499            }
500        }
501
502        match self {
503            Self::Aes128 { iv: Some(iv), .. } | Self::SampleAes { iv: Some(iv), .. } => {
504                write!(output, ",IV={iv:#X}")?;
505            }
506            _ => (),
507        }
508
509        if let Self::Aes128 {
510            key_format: KeyFormat::Other(key_format),
511            ..
512        } = self
513        {
514            write!(output, ",KEYFORMAT=\"{key_format}\"")?;
515        }
516
517        match self {
518            Self::Aes128 {
519                key_format_versions,
520                ..
521            }
522            | Self::SampleAes {
523                key_format_versions,
524                ..
525            }
526            | Self::SampleAesCtr {
527                key_format_versions,
528                ..
529            } => {
530                if !(key_format_versions.is_empty()
531                    || key_format_versions.len() == 1 && key_format_versions[0] == 1)
532                {
533                    write!(output, ",KEYFORMATVERSIONS=\"")?;
534
535                    if key_format_versions.len() == 1 {
536                        write!(output, "{}", key_format_versions[0])?;
537                    } else {
538                        for (i, version) in key_format_versions.iter().enumerate() {
539                            if i == key_format_versions.len() - 1 {
540                                write!(output, "{version}")?;
541                            } else {
542                                write!(output, "{version}/")?;
543                            }
544                        }
545                    }
546
547                    write!(output, "\"")?;
548                }
549            }
550        }
551
552        Ok(())
553    }
554}
555
556impl StreamInf {
557    fn serialize(&self, mut output: impl io::Write) -> io::Result<()> {
558        write!(output, "BANDWIDTH={}", self.bandwidth_bits_per_second)?;
559
560        if let Some(average_bandwidth) = self.average_bandwidth_bits_per_second {
561            write!(output, ",AVERAGE-BANDWIDTH={average_bandwidth}")?;
562        }
563
564        if let Some(score) = self.score {
565            write!(output, ",SCORE={score}")?;
566        }
567
568        if !self.codecs.is_empty() {
569            write!(output, ",CODECS=\"")?;
570
571            if self.codecs.len() == 1 {
572                write!(output, "{}", self.codecs[0])?;
573            } else {
574                for (i, codec) in self.codecs.iter().enumerate() {
575                    if i == self.codecs.len() - 1 {
576                        write!(output, "{codec}")?;
577                    } else {
578                        write!(output, "{codec},")?;
579                    }
580                }
581            }
582
583            write!(output, "\"")?;
584        }
585
586        if !self.supplemental_codecs.is_empty() {
587            write!(output, ",SUPPLEMENTAL-CODECS=\"")?;
588
589            if self.supplemental_codecs.len() == 1 {
590                self.supplemental_codecs[0].serialize(&mut output)?;
591            } else {
592                for (i, supplemental_codec) in self.supplemental_codecs.iter().enumerate() {
593                    supplemental_codec.serialize(&mut output)?;
594                    if i != self.supplemental_codecs.len() - 1 {
595                        write!(output, ",")?;
596                    }
597                }
598            }
599
600            write!(output, "\"")?;
601        }
602
603        if let Some(resolution) = &self.resolution {
604            write!(
605                output,
606                ",RESOLUTION={}x{}",
607                resolution.width, resolution.height
608            )?;
609        }
610
611        if let Some(level) = &self.hdcp_level {
612            match level {
613                HdcpLevel::None => write!(output, ",HDCP-LEVEL=NONE")?,
614                HdcpLevel::Type0 => write!(output, ",HDCP-LEVEL=TYPE-0")?,
615                HdcpLevel::Type1 => write!(output, ",HDCP-LEVEL=TYPE-1")?,
616            }
617        }
618
619        if !self.allowed_cpc.is_empty() {
620            write!(output, ",ALLOWED-CPC=\"")?;
621
622            if self.allowed_cpc.len() == 1 {
623                self.allowed_cpc[0].serialize(&mut output)?;
624            } else {
625                for (i, config) in self.allowed_cpc.iter().enumerate() {
626                    config.serialize(&mut output)?;
627                    if i != self.allowed_cpc.len() - 1 {
628                        write!(output, ",")?;
629                    }
630                }
631            }
632
633            write!(output, "\"")?;
634        }
635
636        match &self.video_range {
637            VideoRange::Sdr => (),
638            VideoRange::Hlg => write!(output, ",VIDEO-RANGE=HLG")?,
639            VideoRange::Pq => write!(output, ",VIDEO-RANGE=PQ")?,
640            VideoRange::Other(other) => write!(output, ",VIDEO-RANGE={other}")?,
641        }
642
643        if !(self.required_video_layout.is_empty()
644            || (self.required_video_layout.len() == 1
645                && matches!(self.required_video_layout[0], VideoChannelSpecifier::Mono)))
646        {
647            write!(output, ",REQ-VIDEO-LAYOUT=\"")?;
648
649            if self.required_video_layout.len() == 1 {
650                #[allow(clippy::match_on_vec_items)]
651                match self.required_video_layout[0] {
652                    VideoChannelSpecifier::Stereo => write!(output, "CH-STEREO")?,
653                    VideoChannelSpecifier::Mono => unreachable!(),
654                }
655            } else {
656                for (i, config) in self.required_video_layout.iter().enumerate() {
657                    match config {
658                        VideoChannelSpecifier::Stereo => write!(output, "CH-STEREO")?,
659                        VideoChannelSpecifier::Mono => write!(output, "CH-MONO")?,
660                    }
661                    if i != self.required_video_layout.len() - 1 {
662                        write!(output, ",")?;
663                    }
664                }
665            }
666
667            write!(output, "\"")?;
668        }
669
670        if let Some(id) = &self.stable_variant_id {
671            write!(output, ",STABLE-VARIANT-ID=\"{id}\"")?;
672        }
673
674        if let Some(id) = &self.pathway_id {
675            write!(output, ",PATHWAY-ID=\"{id}\"")?;
676        }
677
678        Ok(())
679    }
680}
681
682impl SupplementalCodec {
683    fn serialize(&self, mut output: impl io::Write) -> io::Result<()> {
684        write!(output, "{}", self.supplemental_codec)?;
685
686        for brand in &self.compatibility_brands {
687            write!(output, "/{brand}")?;
688        }
689
690        Ok(())
691    }
692}
693
694impl ContentProtectionConfiguration {
695    fn serialize(&self, mut output: impl io::Write) -> io::Result<()> {
696        write!(output, "{}:", self.key_format)?;
697
698        if self.cpc_labels.len() == 1 {
699            write!(output, "{}", self.cpc_labels[0])?;
700        } else {
701            for (i, label) in self.cpc_labels.iter().enumerate() {
702                if i == self.cpc_labels.len() - 1 {
703                    write!(output, "{label}")?;
704                } else {
705                    write!(output, "{label}/")?;
706                }
707            }
708        }
709
710        Ok(())
711    }
712}