aboutsummaryrefslogtreecommitdiff
path: root/web/script
diff options
context:
space:
mode:
Diffstat (limited to 'web/script')
-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
9 files changed, 123 insertions, 33 deletions
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
+}