diff options
| author | metamuffin <metamuffin@disroot.org> | 2026-01-18 23:43:12 +0100 |
|---|---|---|
| committer | metamuffin <metamuffin@disroot.org> | 2026-01-18 23:43:12 +0100 |
| commit | ed19a428cb5eef84c8cf3fed5fda3afd5fc96305 (patch) | |
| tree | 39e3167a4f8b7423a15b3a5f56e973554bdb3195 /ui/client-scripts/src/player/player.ts | |
| parent | 901dff07ed357694eb35284a58c3cc6c003c53ce (diff) | |
| download | jellything-ed19a428cb5eef84c8cf3fed5fda3afd5fc96305.tar jellything-ed19a428cb5eef84c8cf3fed5fda3afd5fc96305.tar.bz2 jellything-ed19a428cb5eef84c8cf3fed5fda3afd5fc96305.tar.zst | |
Move client scripts to build-crate
Diffstat (limited to 'ui/client-scripts/src/player/player.ts')
| -rw-r--r-- | ui/client-scripts/src/player/player.ts | 173 |
1 files changed, 173 insertions, 0 deletions
diff --git a/ui/client-scripts/src/player/player.ts b/ui/client-scripts/src/player/player.ts new file mode 100644 index 0000000..4f59f8a --- /dev/null +++ b/ui/client-scripts/src/player/player.ts @@ -0,0 +1,173 @@ +/* + This file is part of jellything (https://codeberg.org/metamuffin/jellything) + which is licensed under the GNU Affero General Public License (version 3); see /COPYING. + Copyright (C) 2026 metamuffin <metamuffin.org> +*/ +/// <reference lib="dom" /> +import { OVar, e } from "../jshelper/mod.ts"; +import { SegmentDownloader } from "./download.ts"; +import { PlayerTrack } from "./track/mod.ts"; +import { Logger } from "../jshelper/src/log.ts"; +import { create_track } from "./track/create.ts"; +import { StreamInfo, TimeRange, TrackInfo } from "./types_stream.ts"; + +export interface BufferRange extends TimeRange { status: "buffered" | "loading" | "queued" } +export class Player { + public video = e("video") + public media_source = new MediaSource(); + public streaminfo?: StreamInfo; + public tracks?: TrackInfo[]; + public active_tracks = new OVar<PlayerTrack[]>([]); + public downloader: SegmentDownloader = new SegmentDownloader(); + + public position = new OVar(0) + public duration = new OVar(1) + public volume = new OVar(0) + public playing = new OVar(false) + public canplay = new OVar(false) + public error = new OVar<string | undefined>(undefined) + + private cancel_buffering_pers: undefined | (() => void) + set_pers(s?: string) { + 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) + } + + constructor(public base_url: string, poster: string, private start_time: number, public logger?: Logger<string>) { + this.video.poster = poster + this.volume.value = this.video.volume + let skip_change = false; + this.volume.onchange(v => { + if (v > 1.) return this.volume.value = 1; + if (v < 0.) return this.volume.value = 0; + if (!skip_change) this.video.volume = v + skip_change = false + }) + this.video.onvolumechange = () => { + skip_change = true; + this.volume.value = this.video.volume + } + + this.video.onloadedmetadata = () => { } + this.video.ondurationchange = () => { } + this.video.ontimeupdate = () => { + this.position.value = this.video.currentTime + this.update() // TODO maybe not here + } + this.video.onplay = () => { + console.log("play"); + this.set_pers("Resuming playback...") + } + 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; + } + this.video.onplaying = () => { + console.log("playing"); + this.playing.value = true; + this.set_pers() + } + this.video.onpause = () => { + console.log("pause"); + this.playing.value = false + } + this.video.oncanplay = () => { + console.log("canplay"); + this.set_pers() + this.canplay.value = true + } + this.video.onseeking = () => { + console.log("seeking"); + this.set_pers("Seeking...") + } + this.video.onseeked = () => { + console.log("seeked"); + this.set_pers() + } + this.video.onerror = e => { + console.error("video element error:", e); + this.set_pers("MSE crash -_-"); + } + this.video.onabort = e => { + console.error("video element abort:", e); + this.set_pers("Aborted"); + } + this.fetch_meta() + } + + async fetch_meta() { + this.set_pers("Loading stream metadata...") + const res = await fetch(`${this.base_url}?info`, { headers: { "Accept": "application/json" } }) + if (!res.ok) return this.error.value = "Cannot download stream info." + + let streaminfo!: StreamInfo & { error: string } + try { streaminfo = await res.json() } + catch (_) { this.set_pers("Error: Node data invalid") } + if (streaminfo.error) return this.set_pers("server error: " + streaminfo.error) + + this.set_pers() + //! bad code: assignment order is important because chapter callbacks use duration + this.duration.value = streaminfo.duration + this.streaminfo = streaminfo + this.tracks = streaminfo!.tracks; + console.log("aaa", this.tracks); + this.video.src = URL.createObjectURL(this.media_source) + this.media_source.addEventListener("sourceopen", async () => { + let video = false, audio = false, subtitle = false; + for (let i = 0; i < this.tracks!.length; i++) { + const t = this.tracks![i]; + if (t.kind == "video" && !video) + video = true, await this.set_track_enabled(i, true, false) + if (t.kind == "audio" && !audio) + audio = true, await this.set_track_enabled(i, true, false) + if (t.kind == "subtitle" && !subtitle) + subtitle = true, await this.set_track_enabled(i, true, false) + } + + this.set_pers("Buffering initial stream fragments...") + + this.update(this.start_time) + this.video.currentTime = this.start_time + + await this.canplay.wait_for(true) + this.set_pers() + }) + } + + async update(newt?: number) { + await Promise.all(this.active_tracks.value.map(t => t.update(newt ?? this.video.currentTime))) + } + + async set_track_enabled(index: number, state: boolean, update = true) { + console.log(`(${index}) set enabled ${state}`); + const active_index = this.active_tracks.value.findIndex(t => t.track_index == index) + if (!state && active_index != -1) { + this.logger?.log(`Disabled track ${index}: ${display_track(this.tracks![index])}`) + const [track] = this.active_tracks.value.splice(active_index, 1) + track.abort.abort() + } else if (state && active_index == -1) { + this.logger?.log(`Enabled track ${index}: ${display_track(this.tracks![index])}`) + this.active_tracks.value.push(create_track(this, this.base_url, index, this.tracks![index])!) + if (update) await this.update() + } + this.active_tracks.change() + } + + play() { this.video.play() } + pause() { this.video.pause() } + frame_forward() { + //@ts-ignore trust me bro + this.video["seekToNextFrame"]() + } + async seek(p: number) { + this.set_pers("Buffering at target...") + await this.update(p) + this.video.currentTime = p + } +} + +function display_track(t: TrackInfo): string { + return `${t.name}` +} |