/* This file is part of jellything (https://codeberg.org/metamuffin/jellything) which is licensed under the GNU Affero General Public License (version 3); see /COPYING. Copyright (C) 2025 metamuffin */ use serde::{Deserialize, Serialize}; use std::{collections::BTreeMap, fmt::Display, str::FromStr}; pub type TrackNum = usize; pub type FormatNum = usize; pub type IndexNum = usize; #[derive(Debug, Clone, Deserialize, Serialize)] pub enum StreamSpec { // Whep { // track: TrackNum, // seek: u64, // }, // WhepControl { // token: String, // }, Remux { tracks: Vec, container: StreamContainer, }, Original { track: TrackNum, }, HlsMultiVariant { container: StreamContainer, }, HlsVariant { track: TrackNum, container: StreamContainer, format: FormatNum, }, Info, FragmentIndex { track: TrackNum, }, Fragment { track: TrackNum, index: IndexNum, container: StreamContainer, format: FormatNum, }, // Track { // segment: SegmentNum, // track: TrackNum, // container: StreamContainer, // foramt: FormatNum, // }, } #[derive(Debug, Clone, Deserialize, Serialize)] pub struct StreamInfo { pub name: Option, pub duration: f64, pub tracks: Vec, } #[derive(Debug, Clone, Deserialize, Serialize)] pub struct StreamTrackInfo { pub name: Option, pub kind: TrackKind, pub formats: Vec, } #[derive(Debug, Copy, Clone, PartialEq, Eq, Deserialize, Serialize)] #[serde(rename_all = "snake_case")] pub enum TrackKind { Video, Audio, Subtitle, } #[derive(Debug, Clone, Deserialize, Serialize, Default)] pub struct StreamFormatInfo { pub codec: String, pub bitrate: f64, pub remux: bool, pub containers: Vec, pub width: Option, pub height: Option, pub samplerate: Option, pub channels: Option, pub bit_depth: Option, } #[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize, Serialize)] #[serde(rename_all = "lowercase")] pub enum StreamContainer { WebM, Matroska, WebVTT, MPEG4, JVTT, } impl StreamSpec { pub fn to_query(&self) -> String { match self { StreamSpec::Remux { tracks, container } => { format!( "?remux&tracks={}&container={container}", tracks .iter() .map(|t| t.to_string()) .collect::>() .join(",") ) } StreamSpec::Original { track } => format!("?original&track={track}"), StreamSpec::HlsMultiVariant { container } => { format!("?hlsmultivariant&container={container}") } StreamSpec::HlsVariant { track, container, format, } => format!("?hlsvariant&track={track}&container={container}&format={format}"), StreamSpec::Info => "?info".to_string(), StreamSpec::FragmentIndex { track } => { format!("?fragmentindex&track={track}") } StreamSpec::Fragment { track, index, container, format, } => format!( "?fragment&track={track}&index={index}&container={container}&format={format}" ), } } pub fn to_query_short(&self) -> String { match self { StreamSpec::Remux { tracks, container } => { format!( "?remux&ts={}&c={container}", tracks .iter() .map(|t| t.to_string()) .collect::>() .join(",") ) } StreamSpec::Original { track } => format!("?original&t={track}"), StreamSpec::HlsMultiVariant { container } => { format!("?hlsmultivariant&c={container}") } StreamSpec::HlsVariant { track, container, format, } => format!("?hlsvariant&t={track}&c={container}&f={format}"), StreamSpec::Info => "?info".to_string(), StreamSpec::FragmentIndex { track } => { format!("?fragmentindex&t={track}") } StreamSpec::Fragment { track, index, container, format, } => format!("?fragment&t={track}&i={index}&c={container}&f={format}"), } } pub fn from_query_kv(query: &BTreeMap) -> Result { let get_num = |k: &'static str, ks: &'static str| { query .get(k) .or(query.get(ks)) .ok_or(k) .and_then(|a| a.parse::().map_err(|_| "invalid number")) }; let get_container = || { query .get("container") .or(query.get("c")) .ok_or("container") .and_then(|s| s.parse().map_err(|()| "unknown container")) }; if query.contains_key("info") { Ok(Self::Info) } else if query.contains_key("hlsmultivariant") { Ok(Self::HlsMultiVariant { container: get_container()?, }) } else if query.contains_key("hlsvariant") { Ok(Self::HlsVariant { track: get_num("track", "t")? as TrackNum, format: get_num("format", "f")? as FormatNum, container: get_container()?, }) } else if query.contains_key("fragment") { Ok(Self::Fragment { track: get_num("track", "t")? as TrackNum, format: get_num("format", "f")? as FormatNum, index: get_num("index", "i")? as IndexNum, container: get_container()?, }) } else if query.contains_key("fragmentindex") { Ok(Self::FragmentIndex { track: get_num("track", "t")? as TrackNum, }) } else { Err("invalid stream spec") } } } impl Display for StreamContainer { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.write_str(match self { StreamContainer::WebM => "webm", StreamContainer::Matroska => "matroska", StreamContainer::WebVTT => "webvtt", StreamContainer::JVTT => "jvtt", StreamContainer::MPEG4 => "mpeg4", }) } } impl FromStr for StreamContainer { type Err = (); fn from_str(s: &str) -> Result { Ok(match s { "webm" => StreamContainer::WebM, "matroska" => StreamContainer::Matroska, "webvtt" => StreamContainer::WebVTT, "jvtt" => StreamContainer::JVTT, "mpeg4" => StreamContainer::MPEG4, _ => return Err(()), }) } }