aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--Cargo.lock3
-rw-r--r--base/src/permission.rs2
-rw-r--r--common/Cargo.toml1
-rw-r--r--common/src/lib.rs4
-rw-r--r--import/src/infojson.rs21
-rw-r--r--import/src/main.rs16
-rw-r--r--server/src/routes/ui/layout.rs6
-rw-r--r--server/src/routes/ui/node.rs41
-rw-r--r--server/src/routes/ui/player.rs1
-rw-r--r--server/src/routes/ui/sort.rs7
-rw-r--r--server/src/routes/ui/style.rs36
-rw-r--r--transcoder/Cargo.toml3
-rw-r--r--web/js-transition.css34
-rw-r--r--web/nodecard.css27
-rw-r--r--web/script/backbutton.ts8
m---------web/script/jshelper0
-rw-r--r--web/script/main.ts2
-rw-r--r--web/script/player/mediacaps.ts66
-rw-r--r--web/script/player/mod.ts3
-rw-r--r--web/script/player/player.ts3
-rw-r--r--web/script/player/profiles.ts13
-rw-r--r--web/script/player/track.ts2
-rw-r--r--web/script/transition.ts (renamed from web/script/transition.js)59
23 files changed, 266 insertions, 92 deletions
diff --git a/Cargo.lock b/Cargo.lock
index 767246b..d34f2e1 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -1260,7 +1260,7 @@ dependencies = [
[[package]]
name = "image"
version = "0.24.7"
-source = "git+https://github.com/image-rs/image#f1e34f365133a33a27dac5bc59c93d1cfb34a37e"
+source = "git+https://github.com/metamuffin/image-rs#89633ba32f9a9b9e44abc80de88040b91595f992"
dependencies = [
"bytemuck",
"byteorder",
@@ -1415,6 +1415,7 @@ name = "jellycommon"
version = "0.1.0"
dependencies = [
"bincode 2.0.0-rc.3",
+ "chrono",
"rocket",
"serde",
]
diff --git a/base/src/permission.rs b/base/src/permission.rs
index cc0e32c..275fe92 100644
--- a/base/src/permission.rs
+++ b/base/src/permission.rs
@@ -56,6 +56,6 @@ fn check_node_permission(perms: &PermissionSet, node: &Node) -> bool {
return v;
}
}
- return false;
+ return true;
}
}
diff --git a/common/Cargo.toml b/common/Cargo.toml
index b38b962..437fb0b 100644
--- a/common/Cargo.toml
+++ b/common/Cargo.toml
@@ -7,6 +7,7 @@ edition = "2021"
serde = { version = "1.0.188", features = ["derive"] }
bincode = { version = "2.0.0-rc.3", features = ["derive"] }
rocket = { workspace = true, optional = true }
+chrono = { version = "0.4.31", features = ["serde"] }
[features]
rocket = ["dep:rocket"]
diff --git a/common/src/lib.rs b/common/src/lib.rs
index 2bde0b9..8292e87 100644
--- a/common/src/lib.rs
+++ b/common/src/lib.rs
@@ -11,6 +11,9 @@ pub mod seek_index;
pub mod stream;
pub mod user;
+pub use chrono;
+
+use chrono::{DateTime, Utc};
#[cfg(feature = "rocket")]
use rocket::{FromFormField, UriDisplayQuery};
use serde::{Deserialize, Serialize};
@@ -42,6 +45,7 @@ pub struct NodePublic {
#[serde(default)] pub children: Vec<String>,
#[serde(default)] pub tagline: Option<String>,
#[serde(default)] pub description: Option<String>,
+ #[serde(default)] pub release_date: Option<DateTime<Utc>>,
#[serde(default)] pub index: Option<usize>,
#[serde(default)] pub media: Option<MediaInfo>,
#[serde(default)] pub ratings: BTreeMap<Rating, f64>,
diff --git a/import/src/infojson.rs b/import/src/infojson.rs
index ca02551..dd2151b 100644
--- a/import/src/infojson.rs
+++ b/import/src/infojson.rs
@@ -4,6 +4,8 @@
Copyright (C) 2023 metamuffin <metamuffin.org>
*/
+use anyhow::Context;
+use jellycommon::chrono::{format::Parsed, DateTime, Utc};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
@@ -120,3 +122,22 @@ pub struct YHeatmapSample {
pub end_time: f64,
pub value: f64,
}
+
+pub fn parse_upload_date(d: &str) -> anyhow::Result<DateTime<Utc>> {
+ let (year, month, day) = (&d[0..4], &d[4..6], &d[6..8]);
+ let (year, month, day) = (
+ year.parse().context("parsing year")?,
+ month.parse().context("parsing month")?,
+ day.parse().context("parsing day")?,
+ );
+
+ let mut p = Parsed::new();
+ p.year = Some(year);
+ p.month = Some(month);
+ p.day = Some(day);
+ p.hour_div_12 = Some(0);
+ p.hour_mod_12 = Some(0);
+ p.minute = Some(0);
+ p.second = Some(0);
+ Ok(p.to_datetime_with_timezone(&Utc)?)
+}
diff --git a/import/src/main.rs b/import/src/main.rs
index 57e6b99..c274e54 100644
--- a/import/src/main.rs
+++ b/import/src/main.rs
@@ -11,7 +11,7 @@ pub mod tmdb;
use anyhow::Context;
use base64::Engine;
use clap::{Parser, Subcommand};
-use infojson::YVideo;
+use infojson::{parse_upload_date, YVideo};
use jellycommon::{
config::GlobalConfig, AssetLocation, LocalTrack, MediaInfo, MediaSource, Node, NodeKind,
NodePrivate, NodePublic, Rating,
@@ -22,6 +22,7 @@ use log::{info, warn};
use rand::random;
use std::{
collections::BTreeMap,
+ fmt::Debug,
fs::{remove_file, File},
io::{stdin, BufReader, Write},
path::PathBuf,
@@ -354,6 +355,9 @@ fn main() -> anyhow::Result<()> {
duration: m.duration,
tracks: m.tracks.clone(),
}),
+ release_date: infojson
+ .as_ref()
+ .and_then(|j| ok_or_warn(parse_upload_date(&j.upload_date))),
..Default::default()
},
};
@@ -399,3 +403,13 @@ fn make_ident(s: &str) -> String {
}
out
}
+
+fn ok_or_warn<T, E: Debug>(r: Result<T, E>) -> Option<T> {
+ match r {
+ Ok(t) => Some(t),
+ Err(e) => {
+ warn!("{e:?}");
+ None
+ }
+ }
+}
diff --git a/server/src/routes/ui/layout.rs b/server/src/routes/ui/layout.rs
index 1c47247..cdac09c 100644
--- a/server/src/routes/ui/layout.rs
+++ b/server/src/routes/ui/layout.rs
@@ -27,7 +27,7 @@ use rocket::{
use std::io::Cursor;
markup::define! {
- Layout<'a, Main: Render>(title: String, main: Main, class: Option<&'a str>, session: Option<Session>, show_back: bool) {
+ Layout<'a, Main: Render>(title: String, main: Main, class: Option<&'a str>, session: Option<Session>) {
@markup::doctype()
html {
head {
@@ -37,7 +37,6 @@ markup::define! {
}
body[class=class.unwrap_or("")] {
nav {
- @if *show_back { a[href="javascript:history.back()"] { "<- Back" } } " "
h1 { a[href="/"] { @CONF.brand } } " "
@if let Some(_) = session {
a[href=uri!(r_library_node("library"))] { "My Library" } " "
@@ -80,7 +79,6 @@ pub type DynLayoutPage<'a> = LayoutPage<markup::DynRender<'a>>;
pub struct LayoutPage<T> {
pub title: String,
pub class: Option<&'static str>,
- pub show_back: bool,
pub content: T,
}
@@ -90,7 +88,6 @@ impl Default for LayoutPage<DynRender<'_>> {
class: None,
content: markup::new!(),
title: String::new(),
- show_back: false,
}
}
}
@@ -103,7 +100,6 @@ impl<'r, Main: Render> Responder<'r, 'static> for LayoutPage<Main> {
let session = block_on(req.guard::<Option<Session>>()).unwrap();
let mut out = String::new();
Layout {
- show_back: self.show_back,
main: self.content,
title: self.title,
class: self.class.as_deref(),
diff --git a/server/src/routes/ui/node.rs b/server/src/routes/ui/node.rs
index e4d53e6..bb97146 100644
--- a/server/src/routes/ui/node.rs
+++ b/server/src/routes/ui/node.rs
@@ -72,7 +72,6 @@ pub async fn r_library_node_filter<'a>(
Ok(Either::Left(LayoutPage {
title: node.title.to_string(),
- show_back: true, //- !matches!(node.kind, NodeKind::Collection),
content: markup::new! {
@NodePage { node: &node, id: &id, children: &children, filter: &filter }
},
@@ -82,24 +81,37 @@ pub async fn r_library_node_filter<'a>(
markup::define! {
NodeCard<'a>(id: &'a str, node: &'a NodePublic) {
- @let cls = format!("node card {}", match node.kind {NodeKind::Channel => "aspect-square", NodeKind::Video => "aspect-thumb", NodeKind::Collection => "aspect-land", _ => "aspect-port"});
+ @let cls = format!("node card poster {}", match node.kind {NodeKind::Channel => "aspect-square", NodeKind::Video => "aspect-thumb", NodeKind::Collection => "aspect-land", _ => "aspect-port"});
div[class=cls] {
.poster {
- .inner {
- a[href=uri!(r_library_node(id))] {
- img[src=uri!(r_item_assets(id, AssetRole::Poster, Some(1024)))];
+ a[href=uri!(r_library_node(id))] {
+ img[src=uri!(r_item_assets(id, AssetRole::Poster, Some(1024)))];
+ }
+ @if matches!(node.kind, NodeKind::Collection | NodeKind::Channel) {
+ .cardhover.open {
+ a[href=&uri!(r_library_node(id))] { "Open" }
+ @Props { node }
}
- div.details {
- h3 { @node.title }
+ } else {
+ .cardhover.item {
+ a.play[href=&uri!(r_player(id, PlayerConfig::default()))] { "▶" }
@Props { node }
- p.description { @node.description }
- @if matches!(node.kind, NodeKind::Collection | NodeKind::Channel) {
- a[href=&uri!(r_library_node(id))] { "Open" }
- } else {
- a.play[href=&uri!(r_player(id, PlayerConfig::default()))] { "Watch now" }
- }
}
}
+ // .inner {
+ // a[href=uri!(r_library_node(id))] {
+ // img[src=uri!(r_item_assets(id, AssetRole::Poster, Some(1024)))];
+ // }
+ // div.details {
+ // h3 { @node.title }
+ // p.descriptioüwn { @node.description }
+ // @if matches!(node.kind, NodeKind::Collection | NodeKind::Channel) {
+ // a[href=&uri!(r_library_node(id))] { "Open" }
+ // } else {
+ // a.play[href=&uri!(r_player(id, PlayerConfig::default()))] { "Watch now" }
+ // }
+ // }
+ // }
}
div.title {
a[href=uri!(r_library_node(id))] {
@@ -167,6 +179,9 @@ markup::define! {
Rating::Imdb => { "IMDb Rating: " @value }
} }
}
+ @if let Some(d) = &node.release_date {
+ p { "Released " @d.to_rfc2822() }
+ }
}
}
}
diff --git a/server/src/routes/ui/player.rs b/server/src/routes/ui/player.rs
index 4a636e6..d3a3342 100644
--- a/server/src/routes/ui/player.rs
+++ b/server/src/routes/ui/player.rs
@@ -70,7 +70,6 @@ pub fn r_player<'a>(
}
@conf
},
- show_back: true,
..Default::default()
})
}
diff --git a/server/src/routes/ui/sort.rs b/server/src/routes/ui/sort.rs
index da5061d..e1af9d5 100644
--- a/server/src/routes/ui/sort.rs
+++ b/server/src/routes/ui/sort.rs
@@ -53,11 +53,16 @@ pub fn filter_and_sort_nodes(f: &NodeFilterSort, nodes: &mut Vec<(String, NodePu
if let Some(kind) = &f.filter_kind {
o &= kind.contains(&node.kind)
}
+ if let Some(SortProperty::ReleaseDate) = &f.sort_by {
+ o &= node.release_date.is_some()
+ }
o
});
if let Some(sort_prop) = &f.sort_by {
match sort_prop {
- SortProperty::ReleaseDate => nodes.sort_by_key(|(_, _n)| 0), // TODO
+ SortProperty::ReleaseDate => {
+ nodes.sort_by_key(|(_, n)| n.release_date.expect("asserted above"))
+ }
SortProperty::Title => nodes.sort_by(|(_, a), (_, b)| a.title.cmp(&b.title)),
SortProperty::RatingRottenTomatoes => nodes.sort_by_cached_key(|(_, n)| {
SortAnyway(*n.ratings.get(&Rating::RottenTomatoes).unwrap_or(&0.))
diff --git a/server/src/routes/ui/style.rs b/server/src/routes/ui/style.rs
index 1f92690..bdaddd2 100644
--- a/server/src/routes/ui/style.rs
+++ b/server/src/routes/ui/style.rs
@@ -4,7 +4,11 @@
Copyright (C) 2023 metamuffin <metamuffin.org>
Copyright (C) 2023 tpart
*/
-use rocket::{get, http::ContentType};
+use rocket::{
+ get,
+ http::{ContentType, Header},
+ response::Responder,
+};
macro_rules! concat_files {
([$base: expr], $($files:literal),*) => {{
@@ -37,10 +41,22 @@ fn css_bundle() -> String {
"nodepage.css",
"nodecard.css",
"js-player.css",
+ "js-transition.css",
"forms.css"
)
}
+pub struct CachedAsset<T>(pub T);
+impl<'r, 'o: 'r, T: Responder<'r, 'o>> Responder<'r, 'o> for CachedAsset<T> {
+ fn respond_to(self, request: &'r rocket::Request<'_>) -> rocket::response::Result<'o> {
+ let mut res = self.0.respond_to(request)?;
+ if cfg!(not(debug_assertions)) {
+ res.set_header(Header::new("cache-control", "max-age=86400"));
+ }
+ Ok(res)
+ }
+}
+
fn js_bundle() -> String {
concat_files!([env!("OUT_DIR")], "bundle.js")
}
@@ -49,23 +65,23 @@ fn js_bundle_map() -> String {
}
#[get("/assets/style.css")]
-pub fn r_assets_style() -> (ContentType, String) {
- (ContentType::CSS, css_bundle())
+pub fn r_assets_style() -> CachedAsset<(ContentType, String)> {
+ CachedAsset((ContentType::CSS, css_bundle()))
}
#[get("/assets/cantarell.woff2")]
-pub fn r_assets_font() -> (ContentType, &'static [u8]) {
- (
+pub fn r_assets_font() -> CachedAsset<(ContentType, &'static [u8])> {
+ CachedAsset((
ContentType::WOFF2,
include_bytes!("../../../../web/cantarell.woff2"),
- )
+ ))
}
#[get("/assets/bundle.js")]
-pub fn r_assets_js() -> (ContentType, String) {
- (ContentType::JavaScript, js_bundle())
+pub fn r_assets_js() -> CachedAsset<(ContentType, String)> {
+ CachedAsset((ContentType::JavaScript, js_bundle()))
}
#[get("/assets/bundle.js.map")]
-pub fn r_assets_js_map() -> (ContentType, String) {
- (ContentType::JSON, js_bundle_map())
+pub fn r_assets_js_map() -> CachedAsset<(ContentType, String)> {
+ CachedAsset((ContentType::JSON, js_bundle_map()))
}
diff --git a/transcoder/Cargo.toml b/transcoder/Cargo.toml
index 16bd9d9..c4fa7e8 100644
--- a/transcoder/Cargo.toml
+++ b/transcoder/Cargo.toml
@@ -8,7 +8,8 @@ jellycommon = { path = "../common" }
jellybase = { path = "../base" }
jellyremuxer = { path = "../remuxer" }
log = { workspace = true }
-image = { git = "https://github.com/image-rs/image", features = [
+# TODO: change this back to crates.io when pr is merged
+image = { git = "https://github.com/metamuffin/image-rs", features = [
"avif-decoder",
] }
anyhow = "1.0.75"
diff --git a/web/js-transition.css b/web/js-transition.css
new file mode 100644
index 0000000..867e30c
--- /dev/null
+++ b/web/js-transition.css
@@ -0,0 +1,34 @@
+@keyframes jst-fadein {
+ from {
+ background-color: transparent;
+ }
+ to {
+ background-color: black;
+ }
+}
+@keyframes jst-fadeout {
+ from {
+ background-color: black;
+ }
+ to {
+ background-color: transparent;
+ }
+}
+
+.jst-fade {
+ position: fixed;
+ left: 0px;
+ top: 0px;
+ width: 100vw;
+ height: 100vh;
+ z-index: 100;
+}
+.jst-error {
+ position: fixed;
+ top: 50vh;
+ left: 50vw;
+ transform: translate(-50%, -50%);
+ color: rgb(247, 69, 69);
+ font-size: large;
+ z-index: 101;
+}
diff --git a/web/nodecard.css b/web/nodecard.css
index 078f53e..d32d5f4 100644
--- a/web/nodecard.css
+++ b/web/nodecard.css
@@ -40,10 +40,6 @@
.node.card .poster,
.node.card .poster img {
height: var(--card-size);
- transition: height 0.2s;
-}
-.node.card:hover .poster img {
- height: calc(var(--card-size) + 10em);
}
.node.card.aspect-port {
width: calc(var(--card-size) / var(--port-poster-aspect));
@@ -68,24 +64,6 @@
text-align: center;
}
-.node.card .inner {
- transition: margin 0.2s, padding 0.2s;
-}
-.node.card:hover .inner {
- background-color: rgba(0, 0, 0, 0.76);
- backdrop-filter: blur(10px);
- border-radius: 1em;
- padding: 1.5em;
- margin: -5em;
-}
-.node.card:not(:hover) .details {
- display: none;
-}
-.node.card:hover .title {
- display: none;
-}
-
-/*
.node.card .title {
margin-top: 0.1em;
text-align: center;
@@ -96,9 +74,8 @@
}
.node.card .poster a {
grid-area: 1 / 1;
-} */
+}
-/*
.node.card.poster .poster .cardhover.open {
transition: opacity 0.3s, backdrop-filter 0.3s;
opacity: 0;
@@ -168,4 +145,4 @@
position: absolute;
bottom: 0px;
left: 0px;
-} */
+}
diff --git a/web/script/backbutton.ts b/web/script/backbutton.ts
new file mode 100644
index 0000000..c1225c0
--- /dev/null
+++ b/web/script/backbutton.ts
@@ -0,0 +1,8 @@
+import { e } from "./jshelper/mod.ts";
+
+globalThis.addEventListener("DOMContentLoaded", () => {
+ document.getElementsByTagName("nav").item(0)?.prepend(
+ e("a", "<- Back", { onclick() { history.back() } })
+ )
+})
+
diff --git a/web/script/jshelper b/web/script/jshelper
-Subproject bd8b233316820cd35178085ef132f82279be050
+Subproject 2d36b0762459b8edfc1529827d0c15447edd266
diff --git a/web/script/main.ts b/web/script/main.ts
index b59f7af..dd168d5 100644
--- a/web/script/main.ts
+++ b/web/script/main.ts
@@ -4,3 +4,5 @@
Copyright (C) 2023 metamuffin <metamuffin.org>
*/
import "./player/mod.ts"
+import "./transition.ts"
+import "./backbutton.ts"
diff --git a/web/script/player/mediacaps.ts b/web/script/player/mediacaps.ts
new file mode 100644
index 0000000..ff143c0
--- /dev/null
+++ b/web/script/player/mediacaps.ts
@@ -0,0 +1,66 @@
+import { SourceTrack, SourceTrackKind } from "./jhls.d.ts";
+
+const cache = new Map<string, boolean>()
+
+// TODO this testing method makes the assumption, that if the codec is supported on its own, it can be
+// TODO arbitrarly combined with others that are supported. in reality this is true but the spec does not gurantee it.
+
+export async function test_media_capability(track: SourceTrack): Promise<boolean> {
+ const cache_key = `${get_track_kind(track.kind)};${track.codec}`
+ const cached = cache.get(cache_key);
+ if (cached !== undefined) return cached
+ const r = await test_media_capability_inner(track)
+ console.log(`${r ? "positive" : "negative"} media capability test finished for codec=${track.codec}`);
+ cache.set(cache_key, r)
+ return r
+}
+async function test_media_capability_inner(track: SourceTrack) {
+ if (track.kind.subtitles) {
+ return track.codec == "V_TEXT/WEBVTT" // TODO: actually implement it
+ }
+ let res;
+ const codec = MASTROSKA_CODEC_MAP[track.codec]
+ if (!codec) return console.warn(`unknown codec: ${track.codec}`), false
+ if (track.kind.audio) {
+ res = await navigator.mediaCapabilities.decodingInfo({
+ type: "media-source",
+ audio: {
+ contentType: `audio/webm^; codecs=${codec}`,
+ samplerate: track.kind.audio.sample_rate,
+ channels: "" + track.kind.audio.channels,
+ bitrate: 128 * 1000,
+ }
+ })
+ }
+ if (track.kind.video) {
+ res = await navigator.mediaCapabilities.decodingInfo({
+ type: "media-source",
+ video: {
+ contentType: `video/webm; codecs=${codec}`,
+ framerate: track.kind.video.fps || 30,
+ width: track.kind.video.width,
+ height: track.kind.video.height,
+ bitrate: 5 * 1000 * 1000 // TODO we dont know this but we should in the future
+ }
+ })
+ }
+ return res?.supported ?? false
+}
+
+const MASTROSKA_CODEC_MAP: { [key: string]: string } = {
+ "V_VP9": "vp9",
+ "V_VP8": "vp8",
+ "V_AV1": "av1",
+ "V_MPEG4/ISO/AVC": "h264",
+ "V_MPEGH/ISO/HEVC": "h265",
+ "A_OPUS": "opus",
+ "A_VORBIS": "vorbis",
+ "S_TEXT/WEBVTT": "webvtt",
+}
+
+export function get_track_kind(track: SourceTrackKind): "audio" | "video" | "subtitles" {
+ if (track.audio) return "audio"
+ if (track.video) return "video"
+ if (track.subtitles) return "subtitles"
+ throw new Error("invalid track");
+}
diff --git a/web/script/player/mod.ts b/web/script/player/mod.ts
index 8473280..ce3c113 100644
--- a/web/script/player/mod.ts
+++ b/web/script/player/mod.ts
@@ -9,12 +9,13 @@ import { Logger } from "../jshelper/src/log.ts";
import { EncodingProfile } from "./jhls.d.ts";
import { Player } from "./player.ts";
-document.addEventListener("DOMContentLoaded", () => {
+globalThis.addEventListener("DOMContentLoaded", () => {
if (document.body.classList.contains("player")) {
if (!globalThis.MediaSource) return alert("Media Source Extension API required")
const node_id = globalThis.location.pathname.split("/")[2];
const main = document.getElementById("main")!;
document.getElementsByTagName("footer")[0].remove()
+ globalThis.dispatchEvent(new Event("navigationrequiresreload"))
initialize_player(main, node_id)
}
})
diff --git a/web/script/player/player.ts b/web/script/player/player.ts
index 4c1d9fc..acf2a19 100644
--- a/web/script/player/player.ts
+++ b/web/script/player/player.ts
@@ -21,7 +21,7 @@ export class Player {
private cancel_buffering_pers: undefined | (() => void)
set_pers(s?: string) {
- if (this.cancel_buffering_pers) this.cancel_buffering_pers()
+ if (this.cancel_buffering_pers) this.cancel_buffering_pers(), this.cancel_buffering_pers = undefined
if (s) this.cancel_buffering_pers = this.logger?.log_persistent(s)
}
@@ -38,6 +38,7 @@ export class Player {
}
this.video.onwaiting = () => {
console.log("waiting");
+ if (this.video.currentTime > this.duration.value - 0.2) return this.set_pers("Playback finished")
this.set_pers("Buffering...")
this.canplay.value = false;
}
diff --git a/web/script/player/profiles.ts b/web/script/player/profiles.ts
index a768d47..caa46bd 100644
--- a/web/script/player/profiles.ts
+++ b/web/script/player/profiles.ts
@@ -1,5 +1,6 @@
import { OVar } from "../jshelper/mod.ts";
import { EncodingProfile, JhlsMetadata } from "./jhls.d.ts";
+import { test_media_capability } from "./mediacaps.ts";
import { Player } from "./player.ts";
const PROFILE_UP_FAC = 0.6
@@ -33,19 +34,25 @@ export class ProfileSelector {
if (i.subtitles) return this.profiles_subtitles
return []
}
- select_optimal_profile(track: number, profile: OVar<EncodingProfileExt | undefined>) {
+ async remux_supported(track: number): Promise<boolean> {
+ return await test_media_capability(this.metadata.tracks[track].info)
+ }
+ async select_optimal_profile(track: number, profile: OVar<EncodingProfileExt | undefined>) {
const profs = this.profile_list_for_track(track)
- const co = profile.value?.order ?? -1
+ const sup_remux = await this.remux_supported(track);
+ const min_prof = sup_remux ? -1 : 0
+ const co = profile.value?.order ?? min_prof
const current_bitrate = profile_byterate(profs[co], 5000 * 1000)
const next_bitrate = profile_byterate(profs[co - 1], 5000 * 1000)
// console.log({ current_bitrate, next_bitrate, co, bandwidth: this.bandwidth.value * 8 });
+ if (!sup_remux && !profile.value) profile.value = profs[co];
if (current_bitrate > this.bandwidth.value * PROFILE_DOWN_FAC && co + 1 < profs.length) {
console.log("profile up");
profile.value = profs[co + 1]
this.log_change(track, profile.value)
}
- if (next_bitrate < this.bandwidth.value * PROFILE_UP_FAC && co >= 0) {
+ if (next_bitrate < this.bandwidth.value * PROFILE_UP_FAC && co > min_prof) {
console.log("profile down");
profile.value = profs[co - 1]
this.log_change(track, profile.value)
diff --git a/web/script/player/track.ts b/web/script/player/track.ts
index e2d9d85..4173b12 100644
--- a/web/script/player/track.ts
+++ b/web/script/player/track.ts
@@ -81,7 +81,7 @@ export class PlayerTrack {
async load(index: number) {
this.loading.add(index)
- this.player.profile_selector.select_optimal_profile(this.track_index, this.profile)
+ await this.player.profile_selector.select_optimal_profile(this.track_index, this.profile)
const url = `/n/${encodeURIComponent(this.node_id)}/stream?format=hlsseg&tracks=${this.track_index}&index=${index}${this.profile.value ? `&profile=${this.profile.value.id}` : ""}`;
const buf = await this.player.downloader.download(url)
await new Promise<void>(cb => {
diff --git a/web/script/transition.js b/web/script/transition.ts
index 7d39176..e0ee6f5 100644
--- a/web/script/transition.js
+++ b/web/script/transition.ts
@@ -5,14 +5,20 @@
*/
/// <reference lib="dom" />
-const duration = 0.2
-globalThis.addEventListener("load", () => {
+import { e } from "./jshelper/src/element.ts";
+
+const duration = 200
+globalThis.addEventListener("DOMContentLoaded", () => {
patch_page()
})
-globalThis.addEventListener("popstate", (_e) => {
- transition_to(window.location.href, true)
- // transition_to(_e.state.href, true)
+globalThis.addEventListener("popstate", async (_e) => {
+ await transition_to(window.location.href, true)
+})
+
+let disable_transition = false
+globalThis.addEventListener("navigationrequiresreload", () => {
+ disable_transition = true
})
function patch_page() {
@@ -24,52 +30,51 @@ function patch_page() {
})
}
-async function transition_to(href, back) {
+async function transition_to(href: string, back?: boolean) {
+ if (disable_transition) return window.location.href = href
const trigger_load = prepare_load(href, back)
await fade(false)
trigger_load()
+ disable_transition = false;
+}
+
+function show_error(mesg: string) {
+ document.body.append(e("span", { class: "jst-error" }, mesg))
}
-function prepare_load(href, back) {
+function prepare_load(href: string, back?: boolean) {
const r_promise = fetch(href)
return async () => {
let rt = ""
try {
const r = await r_promise
- if (!r.ok) return document.body.innerHTML = "<h1>error</h1>"
+ if (!r.ok) return show_error("Error response. Try again.")
rt = await r.text()
} catch (e) {
- console.error(e)
- return
+ if (e instanceof TypeError) return show_error("Navigation failed. Check your connection.")
+ return show_error("unknown error when fetching page")
}
const [head, body] = rt.split("<head>")[1].split("</head>")
+ if (!back) window.history.pushState({}, "", href)
document.head.innerHTML = head
document.body.outerHTML = body
+ globalThis.dispatchEvent(new Event("DOMContentLoaded"))
fade(true)
- // if (!back) window.history.pushState({href}, "", href)
- if (!back) window.history.pushState({}, "", href)
- patch_page()
}
}
-function fade(dir) {
+function fade(dir: boolean) {
const overlay = document.createElement("div")
- overlay.style.position = "absolute"
- overlay.style.left = "0px"
- overlay.style.top = "0px"
- overlay.style.width = "100vw"
- overlay.style.height = "100vh"
+ overlay.classList.add("jst-fade")
overlay.style.backgroundColor = dir ? "black" : "transparent"
- overlay.style.transition = `background-color ${duration}s`
- overlay.style.zIndex = 99999;
- setTimeout(() => {
- overlay.style.backgroundColor = dir ? "transparent" : "black"
- }, 0)
+ overlay.style.animationName = dir ? "jst-fadeout" : "jst-fadein"
+ overlay.style.animationFillMode = "forwards"
+ overlay.style.animationDuration = `${duration}ms`
document.body.appendChild(overlay)
- return new Promise(res => {
+ return new Promise<void>(res => {
setTimeout(() => {
if (dir) document.body.removeChild(overlay)
res()
- }, duration * 1000)
+ }, duration)
})
-} \ No newline at end of file
+}