aboutsummaryrefslogtreecommitdiff
path: root/web
diff options
context:
space:
mode:
authormetamuffin <metamuffin@disroot.org>2023-10-24 07:40:51 +0200
committermetamuffin <metamuffin@disroot.org>2023-10-24 07:40:51 +0200
commitb462195f2dcfe457eae7791c14e4b834b2d5ab29 (patch)
tree492ba43952fa6798320f2b2bb4d4bd5484e2e4f2 /web
parent6e9ccad881a7f887599bc8f3f6b9ca2424a2cc5e (diff)
parent55f7f06cecd5b6f5661f6f22e8bb3e0448b9713a (diff)
downloadjellything-b462195f2dcfe457eae7791c14e4b834b2d5ab29.tar
jellything-b462195f2dcfe457eae7791c14e4b834b2d5ab29.tar.bz2
jellything-b462195f2dcfe457eae7791c14e4b834b2d5ab29.tar.zst
Merge branch 'master' of codeberg.org:metamuffin/jellything
Diffstat (limited to 'web')
-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
11 files changed, 159 insertions, 58 deletions
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
+}