From 8ee25c9ddd5ba5b6f74f7ec3b212020886e366c1 Mon Sep 17 00:00:00 2001 From: metamuffin Date: Sun, 2 Mar 2025 21:01:28 +0100 Subject: a --- common/src/stream.rs | 39 ++++++++++++++- server/src/routes/stream.rs | 111 +++++++++++++++++++++-------------------- server/src/routes/ui/player.rs | 60 ++++++++++------------ 3 files changed, 123 insertions(+), 87 deletions(-) diff --git a/common/src/stream.rs b/common/src/stream.rs index 0e8f810..9a00ce0 100644 --- a/common/src/stream.rs +++ b/common/src/stream.rs @@ -4,7 +4,7 @@ Copyright (C) 2025 metamuffin */ use serde::{Deserialize, Serialize}; -use std::fmt::Display; +use std::{collections::BTreeMap, fmt::Display, str::FromStr}; #[derive(Debug, Clone, Deserialize, Serialize)] pub enum StreamSpec { @@ -106,6 +106,31 @@ impl StreamSpec { } => format!("?fragment&segment={segment}&track={track}&index={index}&container={container}&format={format}"), } } + pub fn from_query_kv(query: &BTreeMap) -> Result { + let get_num = |k: &'static str| { + query + .get(k) + .ok_or(k) + .and_then(|a| a.parse().map_err(|_| "invalid number")) + }; + let get_container = || { + query + .get("container") + .ok_or("container") + .and_then(|s| s.parse().map_err(|()| "unknown container")) + }; + if query.contains_key("fragment") { + Ok(Self::Fragment { + segment: get_num("segment")?, + track: get_num("track")? as usize, + index: get_num("index")?, + container: get_container()?, + format: get_num("format")? as usize, + }) + } else { + Err("invalid stream spec") + } + } } impl Display for StreamContainer { @@ -118,3 +143,15 @@ impl Display for StreamContainer { }) } } +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, + _ => return Err(()), + }) + } +} diff --git a/server/src/routes/stream.rs b/server/src/routes/stream.rs index 1fb136c..d65b346 100644 --- a/server/src/routes/stream.rs +++ b/server/src/routes/stream.rs @@ -21,7 +21,11 @@ use rocket::{ response::{self, Redirect, Responder}, Either, Request, Response, State, }; -use std::{collections::HashSet, ops::Range}; +use std::{ + collections::{BTreeMap, HashSet}, + ops::Range, + sync::Arc, +}; use tokio::io::{duplex, DuplexStream}; #[head("/n/<_id>/stream?")] @@ -46,65 +50,66 @@ pub async fn r_stream( db: &State, id: &str, range: Option, - spec: StreamSpec, + spec: BTreeMap, ) -> Result, MyError> { // TODO perm let node = db .get_node_slug(id)? .ok_or(anyhow!("node does not exist"))?; - let media = node - .media - .as_ref() - .ok_or(anyhow!("item does not contain media"))?; + let media = Arc::new( + node.media + .clone() + .ok_or(anyhow!("item does not contain media"))?, + ); // TODO its unclear how requests with multiple tracks should be handled. - if spec.track.len() == 1 { - let ti = spec.track[0]; - if let TrackSource::Remote(remote_index) = media.tracks[ti].source { - session - .user - .permissions - .assert(&UserPermission::FederatedContent)?; - - let track = &node.media.as_ref().ok_or(anyhow!("no media"))?.tracks[ti]; - let host = track - .federated - .last() - .ok_or(anyhow!("federation inconsistent"))?; - - let FederationAccount { - password, username, .. - } = SECRETS - .federation - .get(host) - .ok_or(anyhow!("no credentials on the server-side"))?; - - info!("creating session on {host}"); - let instance = federation.get_instance(host)?.to_owned(); - let session = instance - .login(CreateSessionParams { - username: username.to_owned(), - password: password.to_owned(), - expire: Some(60), - drop_permissions: Some(HashSet::from_iter([ - UserPermission::ManageSelf, - UserPermission::Admin, // in case somebody federated the admin :))) - ])), - }) - .await?; - - let uri = session.stream_url( - node.slug.clone().into(), - &StreamSpec { - track: vec![remote_index], - ..spec - }, - ); - info!("federation redirect"); - return Ok(Either::Right(RedirectResponse(uri))); - } - } + // if spec.track.len() == 1 { + // let ti = spec.track[0]; + // if let TrackSource::Remote(remote_index) = media.tracks[ti].source { + // session + // .user + // .permissions + // .assert(&UserPermission::FederatedContent)?; + + // let track = &node.media.as_ref().ok_or(anyhow!("no media"))?.tracks[ti]; + // let host = track + // .federated + // .last() + // .ok_or(anyhow!("federation inconsistent"))?; + + // let FederationAccount { + // password, username, .. + // } = SECRETS + // .federation + // .get(host) + // .ok_or(anyhow!("no credentials on the server-side"))?; + + // info!("creating session on {host}"); + // let instance = federation.get_instance(host)?.to_owned(); + // let session = instance + // .login(CreateSessionParams { + // username: username.to_owned(), + // password: password.to_owned(), + // expire: Some(60), + // drop_permissions: Some(HashSet::from_iter([ + // UserPermission::ManageSelf, + // UserPermission::Admin, // in case somebody federated the admin :))) + // ])), + // }) + // .await?; + + // let uri = session.stream_url( + // node.slug.clone().into(), + // &StreamSpec { + // track: vec![remote_index], + // ..spec + // }, + // ); + // info!("federation redirect"); + // return Ok(Either::Right(RedirectResponse(uri))); + // } + // } info!( "stream request (range={})", @@ -124,7 +129,7 @@ pub async fn r_stream( let head = jellystream::stream_head(&spec); - match jellystream::stream(node, spec, urange, &session.user.permissions).await { + match jellystream::stream(media, spec, urange, &session.user.permissions).await { Ok(stream) => Ok(Either::Left(StreamResponse { stream, range, diff --git a/server/src/routes/ui/player.rs b/server/src/routes/ui/player.rs index 2f28f74..aa567ab 100644 --- a/server/src/routes/ui/player.rs +++ b/server/src/routes/ui/player.rs @@ -9,16 +9,14 @@ use super::{ }; use crate::{ database::Database, - routes::{ - stream::rocket_uri_macro_r_stream, - ui::{assets::rocket_uri_macro_r_item_backdrop, error::MyResult, layout::DynLayoutPage}, + routes::ui::{ + assets::rocket_uri_macro_r_item_backdrop, error::MyResult, layout::DynLayoutPage, }, uri, }; use anyhow::anyhow; use jellybase::{permission::PermissionSetExt, CONF}; use jellycommon::{ - stream::{StreamFormat, StreamSpec}, user::{PermissionSet, PlayerKind, UserPermission}, Node, NodeID, SourceTrackKind, TrackID, }; @@ -47,13 +45,15 @@ impl PlayerConfig { fn jellynative_url(action: &str, seek: f64, secret: &str, node: &str, session: &str) -> String { let protocol = if CONF.tls { "https" } else { "http" }; let host = &CONF.hostname; - let stream_url = uri!(r_stream( - node, - StreamSpec { - format: StreamFormat::HlsMaster, - ..Default::default() - } - )); + let stream_url = ""; + // TODO + // uri!(r_stream( + // node, + // StreamSpec { + // format: StreamFormat::HlsMaster, + // ..Default::default() + // } + // )); format!("jellynative://{action}/{secret}/{session}/{seek}/{protocol}://{host}{stream_url}",) } @@ -67,14 +67,6 @@ pub fn r_player( let node = db.get_node(id)?.ok_or(anyhow!("node does not exist"))?; let native_session = |action: &str| { - let perm = [ - UserPermission::StreamFormat(StreamFormat::HlsMaster), - UserPermission::StreamFormat(StreamFormat::HlsVariant), - UserPermission::StreamFormat(StreamFormat::Fragment), - ]; - for perm in &perm { - sess.user.permissions.assert(perm)?; - } Ok(Either::Right(Redirect::temporary(jellynative_url( action, conf.t.unwrap_or(0.), @@ -82,7 +74,7 @@ pub fn r_player( &id.to_string(), &token::create( sess.user.name, - PermissionSet(perm.map(|e| (e, true)).into()), + PermissionSet::default(), // TODO chrono::Duration::hours(24), ), )))) @@ -98,19 +90,20 @@ pub fn r_player( } } - let spec = StreamSpec { - track: None - .into_iter() - .chain(conf.v) - .chain(conf.a) - .chain(conf.s) - .collect::>(), - format: StreamFormat::Matroska, - webm: Some(true), - ..Default::default() - }; + // TODO + // let spec = StreamSpec { + // track: None + // .into_iter() + // .chain(conf.v) + // .chain(conf.a) + // .chain(conf.s) + // .collect::>(), + // format: StreamFormat::Matroska, + // webm: Some(true), + // ..Default::default() + // }; - let playing = !spec.track.is_empty(); + let playing = false; // !spec.track.is_empty(); let conf = player_conf(node.clone(), playing)?; Ok(Either::Left(LayoutPage { @@ -118,7 +111,8 @@ pub fn r_player( class: Some("player"), content: markup::new! { @if playing { - video[src=uri!(r_stream(&node.slug, &spec)), controls, preload="auto"]{} + // TODO + // video[src=uri!(r_stream(&node.slug, &spec)), controls, preload="auto"]{} } else { img.backdrop[src=uri!(r_item_backdrop(&node.slug, Some(2048))).to_string()]; } -- cgit v1.2.3-70-g09d2