use std::cmp::Ordering;
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
#[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd)]
pub enum MediaType {
Audio,
Video,
Subtitles,
ClosedCaptions,
}
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum ExtractorType {
MpegDash,
Hls,
HttpLive,
Mss,
}
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum Choice {
No,
Yes,
}
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum RoleType {
Subtitle,
Main,
Alternate,
Supplementary,
Commentary,
Dub,
Description,
Sign,
Metadata,
ForcedSubtitle,
Numeric(i32),
}
impl RoleType {
pub const fn from_number(value: i32) -> Self {
match value {
0 => Self::Subtitle,
1 => Self::Main,
2 => Self::Alternate,
3 => Self::Supplementary,
4 => Self::Commentary,
5 => Self::Dub,
6 => Self::Description,
7 => Self::Sign,
8 => Self::Metadata,
9 => Self::ForcedSubtitle,
other => Self::Numeric(other),
}
}
pub fn parse_enum_token(value: &str) -> Option<Self> {
let trimmed = value.trim();
parse_role_name(trimmed).or_else(|| trimmed.parse::<i32>().ok().map(Self::from_number))
}
}
fn parse_role_name(value: &str) -> Option<RoleType> {
match value.to_ascii_lowercase().as_str() {
"subtitle" => Some(RoleType::Subtitle),
"main" => Some(RoleType::Main),
"alternate" => Some(RoleType::Alternate),
"supplementary" => Some(RoleType::Supplementary),
"commentary" => Some(RoleType::Commentary),
"dub" => Some(RoleType::Dub),
"description" => Some(RoleType::Description),
"sign" => Some(RoleType::Sign),
"metadata" => Some(RoleType::Metadata),
"forcedsubtitle" => Some(RoleType::ForcedSubtitle),
_ => None,
}
}
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
pub enum EncryptionMethod {
#[default]
None,
Aes128,
Aes128Ecb,
SampleAes,
SampleAesCtr,
Cenc,
Chacha20,
Unknown,
}
impl EncryptionMethod {
pub fn parse(value: Option<&str>) -> Self {
match value.map(normalize_token).as_deref() {
Some("none") => Self::None,
Some("aes128") => Self::Aes128,
Some("aes128ecb") => Self::Aes128Ecb,
Some("sampleaes") => Self::SampleAes,
Some("sampleaesctr") => Self::SampleAesCtr,
Some("cenc") => Self::Cenc,
Some("chacha20") => Self::Chacha20,
Some(_) => Self::Unknown,
None => Self::Unknown,
}
}
}
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
pub enum KeySource {
#[default]
None,
Inline,
Uri,
File,
KeyTextFile,
Custom,
}
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
#[derive(Clone, Debug, Default, Eq, PartialEq)]
pub struct EncryptionInfo {
pub method: EncryptionMethod,
pub key: Option<Vec<u8>>,
pub iv: Option<Vec<u8>>,
pub kid: Option<Vec<u8>>,
pub scheme: Option<String>,
pub protection_data: Option<Vec<u8>>,
pub source: KeySource,
}
impl EncryptionInfo {
pub fn is_encrypted(&self) -> bool {
self.method != EncryptionMethod::None
}
}
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct ByteRange {
pub start: i64,
pub length: i64,
}
impl ByteRange {
pub fn end(&self) -> Option<i64> {
Some(self.start.wrapping_add(self.length).wrapping_sub(1))
}
}
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
#[derive(Clone, Debug)]
pub struct MediaSegment {
pub index: i64,
pub duration: f64,
pub title: Option<String>,
pub program_date_time: Option<String>,
pub start_range: Option<i64>,
pub expected_length: Option<i64>,
pub encryption: EncryptionInfo,
pub url: String,
pub name_from_var: Option<String>,
}
impl Default for MediaSegment {
fn default() -> Self {
Self {
index: 0,
duration: 0.0,
title: None,
program_date_time: None,
start_range: None,
expected_length: None,
encryption: EncryptionInfo::default(),
url: String::new(),
name_from_var: None,
}
}
}
impl MediaSegment {
pub fn stop_range(&self) -> Option<i64> {
match (self.start_range, self.expected_length) {
(Some(start), Some(length)) => Some(start.wrapping_add(length).wrapping_sub(1)),
_ => None,
}
}
pub fn is_encrypted(&self) -> bool {
self.encryption.is_encrypted()
}
}
impl PartialEq for MediaSegment {
fn eq(&self, other: &Self) -> bool {
self.index == other.index
&& (self.duration - other.duration).abs() < 0.001
&& self.title == other.title
&& self.start_range == other.start_range
&& self.stop_range() == other.stop_range()
&& self.expected_length == other.expected_length
&& self.url == other.url
}
}
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
#[derive(Clone, Debug, Default, PartialEq)]
pub struct MediaPart {
pub media_segments: Vec<MediaSegment>,
}
impl MediaPart {
pub fn total_duration(&self) -> f64 {
self.media_segments
.iter()
.map(|segment| segment.duration)
.sum()
}
}
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
#[derive(Clone, Debug, PartialEq)]
pub struct Playlist {
pub url: String,
pub is_live: bool,
pub refresh_interval_ms: f64,
pub target_duration: Option<f64>,
pub media_init: Option<MediaSegment>,
pub media_parts: Vec<MediaPart>,
}
impl Default for Playlist {
fn default() -> Self {
Self {
url: String::new(),
is_live: false,
refresh_interval_ms: 15_000.0,
target_duration: None,
media_init: None,
media_parts: Vec::new(),
}
}
}
impl Playlist {
pub fn total_duration(&self) -> f64 {
self.media_parts.iter().map(MediaPart::total_duration).sum()
}
pub fn segments_count(&self) -> usize {
self.media_parts
.iter()
.map(|part| part.media_segments.len())
.sum()
}
}
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
#[derive(Clone, Debug, Default, Eq, PartialEq)]
pub struct MssData {
pub four_cc: String,
pub codec_private_data: String,
pub stream_type: String,
pub timescale: i32,
pub sampling_rate: i32,
pub channels: i32,
pub bits_per_sample: i32,
pub nal_unit_length_field: i32,
pub duration: i64,
pub is_protection: bool,
pub protection_system_id: String,
pub protection_data: String,
}
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
#[derive(Clone, Debug, Default, PartialEq)]
pub struct Stream {
pub id: String,
pub media_type: Option<MediaType>,
pub group_id: Option<String>,
pub language: Option<String>,
pub name: Option<String>,
pub default: Option<Choice>,
pub forced: Option<Choice>,
pub skipped_duration: Option<f64>,
pub mss_data: Option<MssData>,
pub bandwidth: Option<i64>,
pub codecs: Option<String>,
pub resolution: Option<String>,
pub frame_rate: Option<f64>,
pub channels: Option<String>,
pub extension: Option<String>,
pub role: Option<RoleType>,
pub video_range: Option<String>,
pub characteristics: Option<String>,
pub publish_time: Option<String>,
pub audio_id: Option<String>,
pub video_id: Option<String>,
pub subtitle_id: Option<String>,
pub period_id: Option<String>,
pub url: String,
pub original_url: String,
pub playlist: Option<Playlist>,
}
impl Stream {
pub fn segments_count(&self) -> usize {
self.playlist.as_ref().map_or(0, Playlist::segments_count)
}
pub fn total_duration(&self) -> Option<f64> {
self.playlist.as_ref().map(Playlist::total_duration)
}
}
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
#[derive(Clone, Debug, Default, PartialEq)]
pub struct Manifest {
pub extractor_type: Option<ExtractorType>,
pub source: Option<String>,
pub streams: Vec<Stream>,
pub warnings: Vec<String>,
pub is_live: bool,
}
pub fn sort_streams_compatible(streams: &mut [Stream]) {
streams.sort_by(compare_streams_compatible);
}
pub fn compare_streams_compatible(left: &Stream, right: &Stream) -> Ordering {
media_rank(left.media_type)
.cmp(&media_rank(right.media_type))
.then_with(|| compare_option_i64_desc(left.bandwidth, right.bandwidth))
.then_with(|| audio_channel_order(right).cmp(&audio_channel_order(left)))
}
pub fn audio_channel_order(stream: &Stream) -> i32 {
let Some(channels) = &stream.channels else {
return 0;
};
let first = channels.split('/').next().unwrap_or_default();
first.parse::<i32>().unwrap_or_default()
}
fn media_rank(media_type: Option<MediaType>) -> i32 {
match media_type {
None => -1,
Some(MediaType::Audio) => 0,
Some(MediaType::Video) => 1,
Some(MediaType::Subtitles) => 2,
Some(MediaType::ClosedCaptions) => 3,
}
}
fn compare_option_i64_desc(left: Option<i64>, right: Option<i64>) -> Ordering {
match (left, right) {
(Some(left), Some(right)) => right.cmp(&left),
(Some(_), None) => Ordering::Less,
(None, Some(_)) => Ordering::Greater,
(None, None) => Ordering::Equal,
}
}
fn normalize_token(value: &str) -> String {
value
.chars()
.filter(|ch| *ch != '-' && *ch != '_' && *ch != ' ')
.flat_map(char::to_lowercase)
.collect()
}
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
#[derive(Clone, Debug, Default, Eq, PartialEq)]
pub enum StreamSelector {
#[default]
Interactive,
Auto,
SubtitlesOnly,
ExplicitIds(Vec<String>),
}