diff options
Diffstat (limited to 'web/script/player/track.ts')
-rw-r--r-- | web/script/player/track.ts | 207 |
1 files changed, 0 insertions, 207 deletions
diff --git a/web/script/player/track.ts b/web/script/player/track.ts deleted file mode 100644 index b6a280f..0000000 --- a/web/script/player/track.ts +++ /dev/null @@ -1,207 +0,0 @@ -/* - 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) 2024 metamuffin <metamuffin.org> -*/ -/// <reference lib="dom" /> -import { JhlsTrackIndex, SourceTrack, TimeRange } from "./jhls.d.ts"; -import { OVar } from "../jshelper/mod.ts"; -import { profile_to_partial_track, track_to_content_type } from "./mediacaps.ts"; -import { BufferRange, Player } from "./player.ts"; -import { EncodingProfileExt, ProfileSelector } from "./profiles.ts"; -import { JvttCue } from "./jhls.d.ts"; -import { get_track_kind } from "./mediacaps.ts"; - -export const TARGET_BUFFER_DURATION = 10 -export const MIN_BUFFER_DURATION = 1 - -export interface AppendRange extends TimeRange { buf: ArrayBuffer, index: number, cb: () => void } - -export abstract class PlayerTrack { - public static async new(player: Player, node_id: string, track_index: number, metadata: SourceTrack): Promise<PlayerTrack | undefined> { - const kind = get_track_kind(metadata.kind) - if (kind == "subtitles") return await VttPlayerTrack.new(player, node_id, track_index, metadata) - else return await MSEPlayerTrack.new(player, node_id, track_index, metadata) - } - - constructor( - public track_index: number, - ) { } - public buffered = new OVar<BufferRange[]>([]); - public abort = new AbortController() - async update(_target: number) { } -} - -export class VttPlayerTrack extends PlayerTrack { - private track: TextTrack - - public static async new(player: Player, node_id: string, track_index: number, metadata: SourceTrack): Promise<VttPlayerTrack | undefined> { - try { - const res = await fetch(`/n/${encodeURIComponent(player.node_id)}/stream?format=jvtt&tracks=${track_index}`, { headers: { "Accept": "application/json" } }) - if (!res.ok) return player.error.value = "Cannot download index.", undefined - let index!: JvttCue[] & { error: string } - try { index = await res.json() } - catch (_) { player.set_pers("Error: Failed to fetch node") } - if (index.error) return player.set_pers("server error: " + index.error), undefined - - const t = new VttPlayerTrack(player, node_id, track_index, metadata, index) - return t - } catch (e) { - if (e instanceof TypeError) { - player.set_pers("Cannot download subtitles: Network Error") - } else throw e - } - } - constructor( - private player: Player, - private node_id: string, - track_index: number, - private metadata: SourceTrack, - public cues: JvttCue[], - ) { - super(track_index) - this.track = this.player.video.addTextTrack("subtitles", metadata.name, metadata.language) - for (const cue of cues) { - const c = new VTTCue(cue.start, cue.end, cue.content); - c.line = -2; - this.track.addCue(c) - } - this.track.mode = "showing" - this.abort.signal.addEventListener("abort", () => { - // TODO disable subtitles properly - this.track.mode = "hidden" - }) - } -} - -export class MSEPlayerTrack extends PlayerTrack { - public source_buffer!: SourceBuffer; - private current_load?: AppendRange; - private loading = new Set<number>(); - private append_queue: AppendRange[] = []; - public profile_selector: ProfileSelector - public profile = new OVar<EncodingProfileExt | undefined>(undefined); - - public static async new(player: Player, node_id: string, track_index: number, metadata: SourceTrack): Promise<MSEPlayerTrack | undefined> { - try { - const res = await fetch(`/n/${encodeURIComponent(player.node_id)}/stream?format=jhlsi&tracks=${track_index}`, { headers: { "Accept": "application/json" } }) - if (!res.ok) return player.error.value = "Cannot download index.", undefined - let index!: JhlsTrackIndex & { error: string } - try { index = await res.json() } - catch (_) { player.set_pers("Error: Failed to fetch node") } - if (index.error) return player.set_pers("server error: " + index.error), undefined - - const t = new MSEPlayerTrack(player, node_id, track_index, metadata, index) - await t.init() - return t - } catch (e) { - if (e instanceof TypeError) { - player.set_pers("Cannot download index: Network Error") - } else throw e - } - } - constructor( - private player: Player, - private node_id: string, - track_index: number, - private metadata: SourceTrack, - public index: JhlsTrackIndex, - ) { - super(track_index) - this.profile_selector = new ProfileSelector(player, this, player.downloader.bandwidth) - } - async init() { - await this.profile_selector.select_optimal_profile(this.track_index, this.profile); - const ct = track_to_content_type(this.track_from_profile())! - console.log(`track ${this.track_index} source buffer content-type: ${ct}`); - this.source_buffer = this.player.media_source.addSourceBuffer(ct); - this.abort.signal.addEventListener("abort", () => { - console.log(`destroy source buffer for track ${this.track_index}`); - this.player.media_source.removeSourceBuffer(this.source_buffer) - }) - this.source_buffer.mode = "segments"; - this.source_buffer.addEventListener("updateend", () => { - if (this.abort.signal.aborted) return - if (this.current_load) { - this.current_load.cb(); - this.loading.delete(this.current_load.index); - this.current_load = undefined; - } - this.update_buf_ranges(); - this.tick_append(); - }); - this.source_buffer.addEventListener("error", e => { - console.error("sourcebuffer error", e); - }); - this.source_buffer.addEventListener("abort", e => { - console.error("sourcebuffer abort", e); - }); - } - track_from_profile(): SourceTrack { - if (this.profile.value) return profile_to_partial_track(this.profile.value) - else return this.metadata - } - - update_buf_ranges() { - const ranges: BufferRange[] = []; - for (let i = 0; i < this.source_buffer.buffered.length; i++) { - ranges.push({ - start: this.source_buffer.buffered.start(i), - end: this.source_buffer.buffered.end(i), - status: "buffered" - }); - } - for (const r of this.loading) { - ranges.push({ ...this.index.segments[r], status: "loading" }); - } - this.buffered.value = ranges; - } - - async update(target: number) { - this.update_buf_ranges(); // TODO required? - - const blocking = []; - for (let i = 0; i < this.index.segments.length; i++) { - const seg = this.index.segments[i]; - if (seg.end < target) continue; - if (seg.start >= target + TARGET_BUFFER_DURATION) break; - if (!this.check_buf_collision(seg.start, seg.end)) continue; - if (seg.start <= target + MIN_BUFFER_DURATION) - blocking.push(this.load(i)); - else - this.load(i); - } - await Promise.all(blocking); - } - check_buf_collision(start: number, end: number) { - const EPSILON = 0.01; - for (const r of this.buffered.value) - if (r.end - EPSILON > start && r.start < end - EPSILON) - return false; - return true; - } - - async load(index: number) { - this.loading.add(index); - await this.profile_selector.select_optimal_profile(this.track_index, this.profile); - const url = `/n/${encodeURIComponent(this.node_id)}/stream?format=snippet&webm=true&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 => { - if (this.abort.signal.aborted) return - this.append_queue.push({ buf, ...this.index.segments[index], index, cb }); - this.tick_append(); - }); - } - tick_append() { - if (this.source_buffer.updating) return; - if (this.append_queue.length) { - const seg = this.append_queue[0]; - this.append_queue.splice(0, 1); - this.current_load = seg; - // TODO why is appending so unreliable?! sometimes it does not add it - this.source_buffer.changeType(track_to_content_type(this.track_from_profile())!); - this.source_buffer.timestampOffset = seg.start; - this.source_buffer.appendBuffer(seg.buf); - } - } -} |