aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authormetamuffin <metamuffin@disroot.org>2026-03-07 04:02:48 +0100
committermetamuffin <metamuffin@disroot.org>2026-03-07 04:02:48 +0100
commit4ce6d64648634bd8d22e8ed0676e0e5b22947dc3 (patch)
treec1df9c9f623603651157006b9fd249de6d63fc7b
parentd3ed810656a563fc733771e760b2abbb05bd98cb (diff)
downloadjellything-4ce6d64648634bd8d22e8ed0676e0e5b22947dc3.tar
jellything-4ce6d64648634bd8d22e8ed0676e0e5b22947dc3.tar.bz2
jellything-4ce6d64648634bd8d22e8ed0676e0e5b22947dc3.tar.zst
new media path format
-rw-r--r--server/src/main.rs2
-rw-r--r--server/src/routes/stream.rs32
-rw-r--r--stream/src/dash.rs8
-rw-r--r--stream/src/hls.rs17
-rw-r--r--stream/types/src/lib.rs189
-rw-r--r--stream/types/src/path.rs79
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&amp;t={as_id}&amp;c={container}&amp;f=$RepresentationID$\" \
- media=\"stream?fragment&amp;t={as_id}&amp;c={container}&amp;f=$RepresentationID$&amp;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"),
+ }
+ }
+ }
+ }
+ }
+ }
+}