Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Server-side rendered pages with progressive enhancement #604

Draft
wants to merge 23 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
388 changes: 342 additions & 46 deletions Cargo.lock

Large diffs are not rendered by default.

2 changes: 2 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ members = [
"dim-web",
"dim-utils",
]
resolver = "2"

[workspace.package]
version = "0.4.0-dev"
Expand All @@ -18,3 +19,4 @@ license = "AGPL-3.0-only"

[profile.dev]
codegen-units = 16
incremental = true
2 changes: 1 addition & 1 deletion dim-core/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ vaapi = ["nightfall/vaapi"]

[dependencies]
# git dependencies
nightfall = { git = "https://github.com/Dusk-Labs/nightfall", tag = "0.3.12-rc4", default-features = false, features = [
nightfall = { path = "../../nightfall", default-features = false, features = [
"cuda",
"ssa_transmux",
] }
Expand Down
17 changes: 6 additions & 11 deletions dim-core/src/errors.rs
Original file line number Diff line number Diff line change
Expand Up @@ -34,8 +34,7 @@ pub enum DimError {
Unauthenticated,
/// Invalid Media type supplied.
InvalidMediaType,
/// An error in the streaming module has occured
#[error(transparent)]
/// An error in the streaming module has occured: {0}
StreamingError(#[from] StreamingErrors),
/// User has no permission to access this route.
Unauthorized,
Expand Down Expand Up @@ -100,7 +99,7 @@ impl IntoResponse for DimError {
| Self::NotFoundError
| Self::ExternalSearchError(_) => {
(StatusCode::NOT_FOUND, self.to_string()).into_response()
},
}
Self::StreamingError(_)
| Self::DatabaseError { .. }
| Self::UnknownError
Expand All @@ -109,18 +108,16 @@ impl IntoResponse for DimError {
| Self::UploadFailed
| Self::ScannerError(_) => {
(StatusCode::INTERNAL_SERVER_ERROR, self.to_string()).into_response()
},
}
Self::Unauthenticated
| Self::Unauthorized
| Self::InvalidCredentials
| Self::CookieError(_)
| Self::NoToken
| Self::UserNotFound => {
(StatusCode::UNAUTHORIZED, self.to_string()).into_response()
},
| Self::UserNotFound => (StatusCode::UNAUTHORIZED, self.to_string()).into_response(),
Self::UsernameNotAvailable => {
(StatusCode::BAD_REQUEST, self.to_string()).into_response()
},
}
Self::UnsupportedFile | Self::InvalidMediaType | Self::MissingFieldInBody { .. } => {
(StatusCode::NOT_ACCEPTABLE, self.to_string()).into_response()
}
Expand Down Expand Up @@ -184,9 +181,7 @@ impl IntoResponse for StreamingErrors {
Self::NoMediaFileFound(_) | Self::FileDoesNotExist => {
(StatusCode::NOT_FOUND, self.to_string()).into_response()
}
_ => {
(StatusCode::INTERNAL_SERVER_ERROR, self.to_string()).into_response()
}
_ => (StatusCode::INTERNAL_SERVER_ERROR, self.to_string()).into_response(),
}
}
}
1 change: 0 additions & 1 deletion dim-core/src/scanner/movie.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
#![allow(unstable_name_collisions)]

use crate::inspect::ResultExt;
use crate::scanner::format_path;
use dim_extern_api::ExternalMedia;
use dim_extern_api::ExternalQueryIntoShow;
Expand Down
1 change: 0 additions & 1 deletion dim-core/src/scanner/tv_show.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ use super::WorkUnit;
use dim_extern_api::ExternalEpisode;
use dim_extern_api::ExternalMedia;

use crate::inspect::ResultExt;
use dim_extern_api::ExternalQueryIntoShow;
use dim_extern_api::ExternalQueryShow;
use dim_extern_api::ExternalSeason;
Expand Down
154 changes: 135 additions & 19 deletions dim-core/src/stream_tracking.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
use std::collections::HashMap;
use std::fmt::Write;
use std::sync::Arc;

use crate::core::StateManager;
Expand All @@ -15,6 +16,7 @@ pub enum ContentType {
Video,
Audio,
Subtitle,
Thumbnail,
}

impl std::fmt::Display for ContentType {
Expand All @@ -26,6 +28,7 @@ impl std::fmt::Display for ContentType {
ContentType::Audio => "audio",
ContentType::Subtitle => "subtitle",
ContentType::Video => "video",
ContentType::Thumbnail => "thumbnail",
}
)
}
Expand All @@ -40,6 +43,7 @@ pub struct VirtualManifest {
pub mime: String,
pub codecs: String,
pub bandwidth: u64,
pub channels: i64,
#[serde(flatten)]
pub args: HashMap<String, String>,
pub duration: Option<i32>,
Expand Down Expand Up @@ -69,6 +73,7 @@ impl VirtualManifest {
mime: String::new(),
codecs: String::new(),
bandwidth: 0,
channels: 2,
args: Default::default(),
duration: None,
label: String::new(),
Expand Down Expand Up @@ -102,6 +107,11 @@ impl VirtualManifest {
self
}

pub fn set_channels(mut self, channels: i64) -> Self {
self.channels = channels;
self
}

pub fn set_duration(mut self, duration: Option<i32>) -> Self {
self.duration = duration;
self
Expand Down Expand Up @@ -143,14 +153,52 @@ impl VirtualManifest {
self
}

pub fn compile(&self, w: &mut XmlWriter, start_num: u64) {
pub fn compile_dash(&self, w: &mut XmlWriter, start_num: u64) {
match self.content_type {
ContentType::Subtitle => self.compile_dash_sub(w),
ContentType::Thumbnail => self.compile_dash_thumbnails(w),
_ => self.compile_dash_av(w, start_num),
}
}

pub fn compile_hls(&self, w: &mut String, start_num: u64) {
match self.content_type {
ContentType::Subtitle => {
// self.compile_hls_sub(w)
}
ContentType::Thumbnail => {
// self.compile_hls_thumbnails(w)
}
_ => self.compile_hls_av(w, start_num),
}
}

fn compile_hls_av(&self, w: &mut String, _start_num: u64) {
match self.content_type {
ContentType::Subtitle => self.compile_sub(w),
_ => self.compile_av(w, start_num),
ContentType::Video => {
let width = self.args.get("width").unwrap();
let height = self.args.get("height").unwrap();
let resolution = format!("{}x{}", width, height);
writeln!(
w,
"#EXT-X-STREAM-INF:BANDWIDTH={},RESOLUTION={},CODECS=\"{}\"",
&self.bandwidth, resolution, &self.codecs
)
.unwrap();
}
_ => {
writeln!(
w,
"#EXT-X-STREAM-INF:BANDWIDTH={},CODECS=\"{}\"",
&self.bandwidth, &self.codecs
)
.unwrap();
}
}
writeln!(w, "/api/v1/stream/{}/data/playlist.m3u8", &self.id).unwrap();
}

fn compile_av(&self, w: &mut XmlWriter, start_num: u64) {
fn compile_dash_av(&self, w: &mut XmlWriter, start_num: u64) {
if matches!(self.content_type, ContentType::Audio | ContentType::Video) {
// Each audio stream must be in a separate adaptation set otherwise theyre treated as
// different bitrates of the same track rather than separate tracks.
Expand All @@ -166,6 +214,20 @@ impl VirtualManifest {
let init = format!("{}?start_num={}", self.init_seg.clone().unwrap(), start_num);
let chunk_path = self.chunk_path.clone();

// mark the default video track
if matches!(self.content_type, ContentType::Video) && self.is_default {
w.start_element("Label");
w.set_preserve_whitespaces(true);
w.write_text("Direct Play");
w.end_element();
w.set_preserve_whitespaces(false);

w.start_element("Role");
w.write_attribute("schemeIdUri", "urn:mpeg:dash:role:2011");
w.write_attribute("value", "main");
w.end_element();
}

// write representations
w.start_element("Representation");
w.write_attribute("id", &self.id);
Expand All @@ -184,15 +246,7 @@ impl VirtualManifest {
"schemeIdUri",
"urn:mpeg:dash:23003:3:audio_channel_configuration:2011",
);
w.write_attribute("value", "2"); // FIXME: At some point we need to stop hardcoding 2ch audio
w.end_element();
}

// mark the default video track
if matches!(self.content_type, ContentType::Audio | ContentType::Video) && self.is_default {
w.start_element("Role");
w.write_attribute("schemeIdUri", "urn:mpeg:dash:role:2011");
w.write_attribute("value", "main");
w.write_attribute("value", &self.channels);
w.end_element();
}

Expand All @@ -213,8 +267,8 @@ impl VirtualManifest {
}
}

fn compile_sub(&self, w: &mut XmlWriter) {
w.start_element("AdapationSet");
fn compile_dash_sub(&self, w: &mut XmlWriter) {
w.start_element("AdaptationSet");
w.write_attribute("mimeType", &self.mime);
w.write_attribute("id", &self.set_id);

Expand All @@ -236,6 +290,33 @@ impl VirtualManifest {
w.end_element();
w.end_element();
}

fn compile_dash_thumbnails(&self, w: &mut XmlWriter) {
w.start_element("AdaptationSet");
w.write_attribute("mimeType", &self.mime);
w.write_attribute("contentType", "image");
w.write_attribute("id", &self.set_id);

w.start_element("SegmentTemplate");
w.write_attribute("media", &format!("{}/$Number%010d$.jpg", &self.chunk_path));
w.write_attribute("duration", "96"); // 8x6 thumbnail grid with an image for every 2 seconds
w.end_element();

w.start_element("Representation");
w.write_attribute("id", "thumbnails_high");
for (k, v) in self.args.iter() {
w.write_attribute(k, v);
}

w.start_element("EssentialProperty");
w.write_attribute("schemeIdUri", "http://dashif.org/thumbnail_tile");
w.write_attribute("value", "8x6");
w.end_element();

w.end_element();

w.end_element();
}
}

#[derive(Debug)]
Expand Down Expand Up @@ -298,7 +379,7 @@ impl StreamTracking {
Some(())
}

pub async fn compile(&self, gid: &Uuid, start_num: u64) -> Option<String> {
pub async fn compile_dash(&self, gid: &Uuid, start_num: u64) -> Option<String> {
let lock = self.streaming_sessions.read().await;
let manifests = lock.get(gid)?;
let duration = ts_to_xml(manifests.first().and_then(|x| x.duration)? as u64);
Expand All @@ -323,19 +404,54 @@ impl StreamTracking {
w.end_element();

for track in manifests {
track.compile(&mut w, start_num);
track.compile_dash(&mut w, start_num);
}

Some(w.end_document())
}

pub async fn compile_only(
pub async fn compile_dash_only(
&self,
gid: &Uuid,
start_num: u64,
_filter: Vec<String>,
) -> Option<String> {
self.compile_dash(gid, start_num).await
}

pub async fn compile_hls(&self, gid: &Uuid, start_num: u64) -> Option<String> {
let lock = self.streaming_sessions.read().await;
let manifests = lock.get(gid)?;
// let target_duration = manifests
// .first()
// .and_then(|x| Some(x.target_duration as u64))?;

let mut w = String::new();
writeln!(w, "#EXTM3U").unwrap();
// writeln!(w, "#EXT-X-VERSION:7").unwrap();
// writeln!(
// w,
// "#EXT-X-START:TIME-OFFSET={}",
// start_num * target_duration
// )
// .unwrap();
// writeln!(w, "#EXT-X-PLAYLIST-TYPE:VOD").unwrap();
// writeln!(w, "#EXT-X-TARGETDURATION:{}", target_duration).unwrap();

for track in manifests {
track.compile_hls(&mut w, start_num);
}

Some(w)
}

pub async fn compile_hls_only(
&self,
gid: &Uuid,
start_num: u64,
_filter: Vec<String>,
) -> Option<String> {
self.compile(gid, start_num).await
self.compile_hls(gid, start_num).await
}
}

Expand Down
Loading