diff options
Diffstat (limited to 'web/script')
-rw-r--r-- | web/script/backbutton.ts | 8 | ||||
m--------- | web/script/jshelper | 0 | ||||
-rw-r--r-- | web/script/main.ts | 2 | ||||
-rw-r--r-- | web/script/player/mediacaps.ts | 66 | ||||
-rw-r--r-- | web/script/player/mod.ts | 3 | ||||
-rw-r--r-- | web/script/player/player.ts | 3 | ||||
-rw-r--r-- | web/script/player/profiles.ts | 13 | ||||
-rw-r--r-- | web/script/player/track.ts | 2 | ||||
-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 +} |