diff options
| author | metamuffin <metamuffin@disroot.org> | 2026-03-07 04:02:48 +0100 |
|---|---|---|
| committer | metamuffin <metamuffin@disroot.org> | 2026-03-07 04:02:48 +0100 |
| commit | 4ce6d64648634bd8d22e8ed0676e0e5b22947dc3 (patch) | |
| tree | c1df9c9f623603651157006b9fd249de6d63fc7b | |
| parent | d3ed810656a563fc733771e760b2abbb05bd98cb (diff) | |
| download | jellything-4ce6d64648634bd8d22e8ed0676e0e5b22947dc3.tar jellything-4ce6d64648634bd8d22e8ed0676e0e5b22947dc3.tar.bz2 jellything-4ce6d64648634bd8d22e8ed0676e0e5b22947dc3.tar.zst | |
new media path format
| -rw-r--r-- | server/src/main.rs | 2 | ||||
| -rw-r--r-- | server/src/routes/stream.rs | 32 | ||||
| -rw-r--r-- | stream/src/dash.rs | 8 | ||||
| -rw-r--r-- | stream/src/hls.rs | 17 | ||||
| -rw-r--r-- | stream/types/src/lib.rs | 189 | ||||
| -rw-r--r-- | stream/types/src/path.rs | 79 |
6 files changed, 156 insertions, 171 deletions
diff --git a/server/src/main.rs b/server/src/main.rs index 0c42bb6..2837940 100644 --- a/server/src/main.rs +++ b/server/src/main.rs @@ -3,7 +3,7 @@ which is licensed under the GNU Affero General Public License (version 3); see /COPYING. Copyright (C) 2026 metamuffin <metamuffin.org> */ -#![feature(int_roundings, str_as_str, duration_constructors)] +#![feature(int_roundings, str_as_str, duration_constructors, never_type)] #![allow(clippy::needless_borrows_for_generic_args)] #![recursion_limit = "4096"] diff --git a/server/src/routes/stream.rs b/server/src/routes/stream.rs index a72e0d9..f117070 100644 --- a/server/src/routes/stream.rs +++ b/server/src/routes/stream.rs @@ -14,28 +14,34 @@ use jellystream::SMediaInfo; use log::{info, warn}; use rocket::{ Either, Request, Response, get, head, - http::{Header, Status}, - request::{self, FromRequest}, + http::{Header, Status, uri::Segments}, + request::{self, FromRequest, FromSegments}, response::{self, Redirect, Responder}, }; -use std::{ - collections::{BTreeMap, BTreeSet}, - ops::Range, - sync::Arc, -}; +use std::{collections::BTreeSet, ops::Range, sync::Arc}; use tokio::{ io::{DuplexStream, duplex}, task::spawn_blocking, }; use tokio_util::io::SyncIoBridge; -#[head("/n/<_id>/stream?<spec..>")] +pub struct StringPath<'a>(Vec<&'a str>); +impl<'r> FromSegments<'r> for StringPath<'r> { + type Error = !; + fn from_segments( + segments: Segments<'r, rocket::http::uri::fmt::Path>, + ) -> Result<Self, Self::Error> { + Ok(Self(segments.collect())) + } +} + +#[head("/n/<_id>/media/<path..>")] pub async fn r_stream_head( _sess: RequestInfo<'_>, _id: &str, - spec: BTreeMap<String, String>, + path: StringPath<'_>, ) -> Result<Either<StreamResponse, Redirect>, MyError> { - let spec = StreamSpec::from_query_kv(&spec).map_err(|x| anyhow!("spec invalid: {x}"))?; + let spec = StreamSpec::from_path(&path.0).map_err(|x| anyhow!("media path invalid: {x}"))?; let head = jellystream::stream_head(&spec); Ok(Either::Left(StreamResponse { stream: duplex(0).0, @@ -45,14 +51,14 @@ pub async fn r_stream_head( })) } -#[get("/n/<slug>/stream?<spec..>")] +#[get("/n/<slug>/media/<path..>")] pub async fn r_stream( ri: RequestInfo<'_>, slug: &str, range: Option<RequestRange>, - spec: BTreeMap<String, String>, + path: StringPath<'_>, ) -> Result<StreamResponse, MyError> { - let spec = StreamSpec::from_query_kv(&spec).map_err(|x| anyhow!("spec invalid: {x}"))?; + let spec = StreamSpec::from_path(&path.0).map_err(|x| anyhow!("media path invalid: {x}"))?; let mut node = None; ri.state.database.transaction(&mut |txn| { diff --git a/stream/src/dash.rs b/stream/src/dash.rs index 6060501..9e66a0f 100644 --- a/stream/src/dash.rs +++ b/stream/src/dash.rs @@ -164,9 +164,11 @@ fn write_segment_template( out, "<SegmentTemplate \ timescale=\"1000\" \ - initialization=\"stream?fragmentinit&t={as_id}&c={container}&f=$RepresentationID$\" \ - media=\"stream?fragment&t={as_id}&c={container}&f=$RepresentationID$&i=$Number$\" \ - startNumber=\"0\">" + initialization=\"{as_id}/$RepresentationID$/init.{}\" \ + media=\"{as_id}/$RepresentationID$/frag$Number$.{}\" \ + startNumber=\"0\">", + container.file_ext(TrackKind::Video), + container.file_ext(TrackKind::Video) )?; writeln!(out, "{}", Timeline(&frags))?; writeln!(out, "</SegmentTemplate>")?; diff --git a/stream/src/hls.rs b/stream/src/hls.rs index 70a0d3c..759b196 100644 --- a/stream/src/hls.rs +++ b/stream/src/hls.rs @@ -23,12 +23,12 @@ pub fn hls_multivariant_stream(info: &SMediaInfo) -> Result<Box<dyn Read + Send for (i, t) in info.tracks.iter().enumerate() { for (j, f) in t.formats.iter().enumerate() { let uri = format!( - "stream{}", + "{}", StreamSpec::HlsVariant { track: i, format: j } - .to_query() + .to_path() ); let r#type = match t.kind { TrackKind::Video => "VIDEO", @@ -50,7 +50,7 @@ pub fn hls_multivariant_stream(info: &SMediaInfo) -> Result<Box<dyn Read + Send pub fn hls_variant_stream( info: &SMediaInfo, track: TrackNum, - format: FormatNum, + _format: FormatNum, ) -> Result<Box<dyn Read + Send + Sync>> { let frags = fragment_index(&info, track)?; let (_, info) = stream_info(&info)?; @@ -61,19 +61,14 @@ pub fn hls_variant_stream( writeln!(out, "#EXT-X-TARGETDURATION:{}", info.duration)?; writeln!(out, "#EXT-X-VERSION:4")?; writeln!(out, "#EXT-X-MEDIA-SEQUENCE:0")?; + writeln!(out, "#EXT-X-MAP:URI=\"init.webm\"")?; for (index, Range { start, end }) in frags.iter().enumerate() { writeln!(out, "#EXTINF:{:},", end - start)?; writeln!( out, - "stream{}", - StreamSpec::Fragment { - track, - index, - container: StreamContainer::MP4, - format, - } - .to_query() + "frag{index}.{}", + StreamContainer::WebM.file_ext(TrackKind::Video), )?; } diff --git a/stream/types/src/lib.rs b/stream/types/src/lib.rs index 6a4fc79..cbf5dcb 100644 --- a/stream/types/src/lib.rs +++ b/stream/types/src/lib.rs @@ -4,7 +4,9 @@ Copyright (C) 2026 metamuffin <metamuffin.org> */ use serde::{Deserialize, Serialize}; -use std::{collections::BTreeMap, fmt::Display, fmt::Write, str::FromStr}; +use std::{fmt::Display, fmt::Write, str::FromStr}; + +pub mod path; pub type TrackNum = usize; pub type FormatNum = usize; @@ -12,41 +14,41 @@ pub type IndexNum = usize; #[derive(Debug, Clone, Deserialize, Serialize)] pub enum StreamSpec { - // Whep { - // track: TrackNum, - // seek: u64, - // }, - // WhepControl { - // token: String, - // }, - Remux { - tracks: Vec<usize>, - container: StreamContainer, - }, - Original { - track: TrackNum, - }, + /// stream.m3u8 HlsMultiVariant, + /// stream.mpd + Dash, + /// stream.json + Info, + /// <track>/<format>/variant.m3u8 HlsVariant { track: TrackNum, format: FormatNum, }, - Dash, - Info, + /// <track>/fragindex.json FragmentIndex { track: TrackNum, }, + /// <track>/<format>/init.<cont> FragmentInit { track: TrackNum, container: StreamContainer, format: FormatNum, }, + /// <track>/<format>/frag<i>.<cont> Fragment { track: TrackNum, index: IndexNum, container: StreamContainer, format: FormatNum, }, + Remux { + tracks: Vec<usize>, + container: StreamContainer, + }, + Original { + track: TrackNum, + }, // Track { // segment: SegmentNum, // track: TrackNum, @@ -85,10 +87,15 @@ pub struct StreamFormatInfo { pub remux: bool, pub containers: Vec<StreamContainer>, + #[serde(skip_serializing_if = "Option::is_none")] pub width: Option<u64>, + #[serde(skip_serializing_if = "Option::is_none")] pub height: Option<u64>, + #[serde(skip_serializing_if = "Option::is_none")] pub samplerate: Option<f64>, + #[serde(skip_serializing_if = "Option::is_none")] pub channels: Option<usize>, + #[serde(skip_serializing_if = "Option::is_none")] pub bit_depth: Option<u8>, } @@ -151,6 +158,28 @@ impl StreamContainer { (Self::JVTT, _) => "application/jellything-vtt+json", } } + pub fn from_file_ext(ext: &str) -> Option<Self> { + Some(match ext { + "weba" | "webm" => Self::WebM, + "mkv" | "mka" | "mks" => Self::Matroska, + "mp4" | "m4a" | "m4s" => Self::MP4, + "vtt" => Self::WebVTT, + "json" => Self::JVTT, + _ => return None, + }) + } + pub fn file_ext(&self, kind: TrackKind) -> &'static str { + match (self, kind) { + (Self::WebM, TrackKind::Audio) => "weba", + (Self::WebM, _) => "webm", + (Self::Matroska, TrackKind::Audio) => "mka", + (Self::Matroska, _) => "mkv", + (Self::MP4, TrackKind::Audio) => "m4a", + (Self::MP4, _) => "mp4", + (Self::WebVTT, _) => "vtt", + (Self::JVTT, _) => "json", + } + } } impl Display for TrackKind { @@ -163,132 +192,6 @@ impl Display for TrackKind { } } -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::<Vec<String>>() - .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::<Vec<String>>() - .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<String, String>) -> Result<Self, &'static str> { - let get_num = |k: &'static str, ks: &'static str| { - query - .get(k) - .or(query.get(ks)) - .ok_or(k) - .and_then(|a| a.parse::<usize>().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 { diff --git a/stream/types/src/path.rs b/stream/types/src/path.rs new file mode 100644 index 0000000..4a91457 --- /dev/null +++ b/stream/types/src/path.rs @@ -0,0 +1,79 @@ +/* + 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 <metamuffin.org> +*/ + +use crate::{FormatNum, IndexNum, StreamContainer, StreamSpec, TrackKind, TrackNum}; + +impl StreamSpec { + pub fn to_path(&self) -> String { + match self { + StreamSpec::HlsMultiVariant => format!("stream.m3u8"), + StreamSpec::Dash => format!("stream.mpd"), + StreamSpec::Info => format!("formats.json"), + StreamSpec::HlsVariant { track, format } => format!("{track}/{format}/variant.m3u8"), + StreamSpec::FragmentIndex { track } => format!("{track}/fragindex.json"), + StreamSpec::FragmentInit { + track, + container, + format, + } => format!( + "{track}/{format}/init.{}", + container.file_ext(TrackKind::Video) + ), + StreamSpec::Fragment { + track, + index, + container, + format, + } => format!( + "{track}/{format}/frag/{index}.{}", + container.file_ext(TrackKind::Video) + ), + _ => todo!(), + } + } + pub fn from_path(segments: &[&str]) -> Result<Self, &'static str> { + let mut segs = segments.into_iter(); + match *segs.next().ok_or("no path")? { + "stream.mpd" => Ok(Self::Dash), + "stream.m3u8" => Ok(Self::HlsMultiVariant), + "formats.json" => Ok(Self::Info), + track => { + let track = track.parse::<TrackNum>().ok().ok_or("invalid track")?; + match *segs.next().ok_or("<track> is a directory")? { + "fragindex.json" => Ok(Self::FragmentIndex { track }), + format => { + let format = format.parse::<FormatNum>().ok().ok_or("invalid format")?; + match *segs.next().ok_or("<track>/<format> is a directory")? { + "variant.m3u8" => Ok(Self::HlsVariant { track, format }), + f if let Some(ext) = f.strip_prefix("init.") + && let Some(container) = StreamContainer::from_file_ext(ext) => + { + Ok(Self::FragmentInit { + track, + container, + format, + }) + } + f if let Some(rest) = f.strip_prefix("frag") + && let Some((index, ext)) = rest.split_once(".") + && let Ok(index) = index.parse::<IndexNum>() + && let Some(container) = StreamContainer::from_file_ext(ext) => + { + Ok(Self::Fragment { + track, + container, + format, + index, + }) + } + _ => Err("not found"), + } + } + } + } + } + } +} |