/* 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) 2026 metamuffin */ use serde::{Deserialize, Serialize}; use std::{collections::BTreeMap, fmt::Display, fmt::Write, 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, HlsVariant { track: TrackNum, format: FormatNum, }, Dash, Info, FragmentIndex { track: TrackNum, }, FragmentInit { track: TrackNum, container: StreamContainer, format: FormatNum, }, 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 codec_param: 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, } impl std::hash::Hash for StreamFormatInfo { fn hash(&self, state: &mut H) { self.codec.hash(state); self.codec_param.hash(state); (self.bitrate as i64).hash(state); self.remux.hash(state); self.containers.hash(state); self.width.hash(state); self.height.hash(state); (self.samplerate.unwrap_or(0.) as i64).hash(state); self.channels.hash(state); self.bit_depth.hash(state); } } impl StreamFormatInfo { pub fn metadata_str(&self) -> String { let mut o = String::new(); if let Some(w) = self.width { write!(o, "w{w}").unwrap(); } if let Some(h) = self.height { write!(o, "h{h}").unwrap(); } if let Some(r) = self.samplerate { write!(o, "r{r:.02}").unwrap(); } if let Some(b) = self.bit_depth { write!(o, "b{b}").unwrap(); } if let Some(c) = self.channels { write!(o, "c{c}").unwrap(); } o } } #[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize, Serialize, Hash)] #[serde(rename_all = "lowercase")] pub enum StreamContainer { WebM, Matroska, WebVTT, MPEG4, JVTT, } impl StreamContainer { pub fn mime_type(&self, kind: TrackKind) -> &'static str { match (self, kind) { (Self::WebM, TrackKind::Audio) => "audio/webm", (Self::WebM, _) => "video/webm", (Self::Matroska, TrackKind::Audio) => "audio/x-matroska", (Self::Matroska, _) => "video/x-matroska", (Self::MPEG4, TrackKind::Audio) => "audio/mp4", (Self::MPEG4, _) => "video/mp4", (Self::WebVTT, _) => "text/vtt", (Self::JVTT, _) => "application/jellything-vtt+json", } } } impl Display for TrackKind { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.write_str(match self { TrackKind::Video => "video", TrackKind::Audio => "audio", TrackKind::Subtitle => "subtitle", }) } } 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 => { format!("?hlsmultivariant") } StreamSpec::HlsVariant { track, format } => { format!("?hlsvariant&track={track}&format={format}") } StreamSpec::Info => "?info".to_string(), StreamSpec::Dash => "?info".to_string(), StreamSpec::FragmentIndex { track } => { format!("?fragmentindex&track={track}") } StreamSpec::FragmentInit { track, container, format, } => format!("?fragmentinit&track={track}&container={container}&format={format}"), 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 => { format!("?hlsmultivariant") } StreamSpec::HlsVariant { track, format } => { format!("?hlsvariant&t={track}&f={format}") } StreamSpec::Info => "?info".to_string(), StreamSpec::Dash => "?dash".to_string(), StreamSpec::FragmentIndex { track } => { format!("?fragmentindex&t={track}") } StreamSpec::FragmentInit { track, container, format, } => format!("?fragmentinit&t={track}&c={container}&f={format}"), 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("dash") { Ok(Self::Dash) } else if query.contains_key("hlsmultivariant") { Ok(Self::HlsMultiVariant) } else if query.contains_key("hlsvariant") { Ok(Self::HlsVariant { track: get_num("track", "t")? as TrackNum, format: get_num("format", "f")? as FormatNum, }) } else if query.contains_key("fragmentinit") { Ok(Self::FragmentInit { 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(()), }) } }