diff options
Diffstat (limited to 'web/script/player/track')
| -rw-r--r-- | web/script/player/track/create.ts | 15 | ||||
| -rw-r--r-- | web/script/player/track/mod.ts | 25 | ||||
| -rw-r--r-- | web/script/player/track/mse.ts | 208 | ||||
| -rw-r--r-- | web/script/player/track/vtt.ts | 96 |
4 files changed, 0 insertions, 344 deletions
diff --git a/web/script/player/track/create.ts b/web/script/player/track/create.ts deleted file mode 100644 index a83a26a..0000000 --- a/web/script/player/track/create.ts +++ /dev/null @@ -1,15 +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) 2026 metamuffin <metamuffin.org> -*/ -import { VttPlayerTrack } from "./vtt.ts"; -import { MSEPlayerTrack } from "./mse.ts"; -import { Player } from "../player.ts"; -import { PlayerTrack } from "./mod.ts"; -import { TrackInfo } from "../types_stream.ts"; - -export function create_track(player: Player, base_url: string, track_index: number, track_info: TrackInfo): PlayerTrack | undefined { - if (track_info.kind == "subtitle") return new VttPlayerTrack(player, base_url, track_index, track_info) - else return new MSEPlayerTrack(player, base_url, track_index, track_info) -} diff --git a/web/script/player/track/mod.ts b/web/script/player/track/mod.ts deleted file mode 100644 index 57f6820..0000000 --- a/web/script/player/track/mod.ts +++ /dev/null @@ -1,25 +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) 2026 metamuffin <metamuffin.org> -*/ -/// <reference lib="dom" /> -import { TimeRange } from "../types_stream.ts"; -import { OVar } from "../../jshelper/mod.ts"; -import { BufferRange } from "../player.ts"; - -export const TARGET_BUFFER_DURATION = 20 -export const MIN_BUFFER_DURATION = 1 - -export interface AppendRange extends TimeRange { buf: ArrayBuffer, index: number, cb: () => void } - -export abstract class PlayerTrack { - constructor( - public track_index: number, - ) { } - public buffered = new OVar<BufferRange[]>([]); - public abort = new AbortController() - async update(_target: number) { } - public abstract debug(): HTMLElement | OVar<HTMLElement> -} - diff --git a/web/script/player/track/mse.ts b/web/script/player/track/mse.ts deleted file mode 100644 index efcd0d5..0000000 --- a/web/script/player/track/mse.ts +++ /dev/null @@ -1,208 +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) 2026 metamuffin <metamuffin.org> -*/ -import { OVar, show } from "../../jshelper/mod.ts"; -import { test_media_capability, track_to_content_type } from "../mediacaps.ts"; -import { BufferRange, Player } from "../player.ts"; -import { PlayerTrack, AppendRange, TARGET_BUFFER_DURATION, MIN_BUFFER_DURATION } from "./mod.ts"; -import { e } from "../../jshelper/src/element.ts"; -import { FormatInfo, FragmentIndex, StreamContainer, TrackInfo } from "../types_stream.ts"; -import { show_format } from "../mod.ts"; - -interface UsableFormat { format_index: number, usable_index: number, format: FormatInfo, container: StreamContainer } - -export class MSEPlayerTrack extends PlayerTrack { - public source_buffer!: SourceBuffer; - private current_load?: AppendRange; - private loading = new Set<number>(); - private append_queue: AppendRange[] = []; - public index?: FragmentIndex - public active_format = new OVar<UsableFormat | undefined>(undefined); - public usable_formats: UsableFormat[] = [] - - constructor( - private player: Player, - private base_url: string, - track_index: number, - public trackinfo: TrackInfo, - ) { - super(track_index); - this.init() - } - - async init() { - this.buffered.value = [{ start: 0, end: this.player.duration.value, status: "loading" }] - try { - const res = await fetch(`${this.base_url}?fragmentindex&t=${this.track_index}`, { headers: { "Accept": "application/json" } }); - if (!res.ok) return this.player.error.value = "Cannot download index.", undefined; - let index!: FragmentIndex & { error: string; }; - try { index = await res.json(); } - catch (_) { this.player.set_pers("Error: Failed to fetch node"); } - if (index.error) return this.player.set_pers("server error: " + index.error), undefined; - this.index = index - } catch (e) { - if (e instanceof TypeError) { - this.player.set_pers("Cannot download index: Network Error"); - } else throw e; - } - this.buffered.value = [] - - console.log(this.trackinfo); - - for (let i = 0; i < this.trackinfo.formats.length; i++) { - const format = this.trackinfo.formats[i]; - for (const container of format.containers) { - if (container != "webm" && container != "mpeg4") continue; - if (await test_media_capability(format, container)) - this.usable_formats.push({ container, format, format_index: i, usable_index: this.usable_formats.length }) - } - } - - // TODO prefer newer codecs - const sort_key = (f: FormatInfo) => f.remux ? Infinity : f.bitrate - this.usable_formats.sort((a, b) => sort_key(b.format) - sort_key(a.format)) - if (!this.usable_formats.length) - return this.player.logger?.log("No availble format is supported by this device. The track can't be played back.") - this.active_format.value = this.usable_formats[0] - - const ct = track_to_content_type(this.active_format.value!.format, this.active_format.value!.container); - 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.loading.delete(this.current_load.index); - const cb = this.current_load.cb; - this.current_load = undefined; - cb() - } else { - console.warn("updateend but nothing is loading") - } - 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); - }); - - this.update(this.player.video.currentTime) - } - - choose_format() { - if (!this.active_format.value) return - const ai = this.active_format.value.usable_index - const current_br = this.active_format.value.format.bitrate - const avail_br = this.player.downloader.bandwidth_avail.value * 8 - const prev_format = this.active_format.value - if (ai < this.usable_formats.length - 1 && current_br > avail_br) { - this.active_format.value = this.usable_formats[ai + 1] - } - if (ai > 0 && avail_br > this.usable_formats[ai - 1].format.bitrate * 1.2) { - this.active_format.value = this.usable_formats[ai - 1] - } - if (prev_format != this.active_format.value) { - console.log(`abr ${show.metric(prev_format.format.bitrate)} -> ${show.metric(this.active_format.value.format.bitrate)}`); - this.choose_format() - } - } - - update_buf_ranges() { - if (!this.index) return; - 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[r], status: "loading" }); - } - this.buffered.value = ranges; - } - - override async update(target: number) { - if (!this.index) return; - this.update_buf_ranges(); // TODO required? - - const buffer_to = target + (target < 20 ? Math.max(1, target) : TARGET_BUFFER_DURATION) - - const blocking = []; - for (let i = 0; i < this.index.length; i++) { - const frag = this.index[i]; - if (frag.end < target) continue; - if (frag.start >= buffer_to) break; - if (!this.check_buf_collision(frag.start, frag.end)) continue; - if (frag.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.choose_format() - this.loading.add(index); - this.update_buf_ranges() - // TODO update format selection - const url = `${this.base_url}?fragment&t=${this.track_index}&f=${this.active_format.value!.format_index}&i=${index}&c=${this.active_format.value!.container}`; - const buf = await this.player.downloader.download(url); - await new Promise<void>(cb => { - if (!this.index) return; - if (this.abort.signal.aborted) return; - this.append_queue.push({ buf, ...this.index[index], index, cb }); - this.tick_append(); - }); - } - tick_append() { - if (this.source_buffer.updating || this.current_load) return; - if (this.append_queue.length) { - const frag = this.append_queue[0]; - this.append_queue.splice(0, 1); - this.current_load = frag; - // TODO why is appending so unreliable?! sometimes it does not add it - this.source_buffer.changeType(track_to_content_type(this.active_format.value!.format, this.active_format.value!.container)); - this.source_buffer.timestampOffset = this.active_format.value?.container == "mpeg4" ? frag.start : 0 - // this.source_buffer.timestampOffset = this.trackinfo.kind == "video" && !this.active_format.value!.format.remux ? frag.start : 0 - // this.source_buffer.timestampOffset = this.active_format.value?.format.remux ? 0 : frag.start - // this.source_buffer.timestampOffset = 0 - this.source_buffer.appendBuffer(frag.buf); - } - } - - public debug(): OVar<HTMLElement> { - const rtype = (t: string, b: BufferRange[]) => { - const c = b.filter(r => r.status == t); - // ${c.length} range${c.length != 1 ? "s" : ""} - return `${c.reduce((a, v) => a + v.end - v.start, 0).toFixed(2)}s` - } - return this.active_format.liftA2(this.buffered, (p, b) => - e("pre", - p ? - `mse track ${this.track_index}: format ${p.format_index} (${p.format.remux ? "remux" : "transcode"})` - + `\n\tformat: ${show_format(p.format)}` - + `\n\tbuffer type: ${track_to_content_type(p.format, p.container)}` - + `\n\tbuffered: ${rtype("buffered", b)} / queued: ${rtype("queued", b)} / loading: ${rtype("loading", b)}` - : "" - ) as HTMLElement - ) - } -} diff --git a/web/script/player/track/vtt.ts b/web/script/player/track/vtt.ts deleted file mode 100644 index 2152b97..0000000 --- a/web/script/player/track/vtt.ts +++ /dev/null @@ -1,96 +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) 2026 metamuffin <metamuffin.org> -*/ -import { e } from "../../jshelper/src/element.ts"; -import { Player } from "../player.ts"; -import { SubtitleCue, TrackInfo } from "../types_stream.ts"; -import { PlayerTrack } from "./mod.ts"; - -export class VttPlayerTrack extends PlayerTrack { - private track: TextTrack; - public cues?: SubtitleCue[] - - constructor( - private player: Player, - private node_id: string, - track_index: number, - private track_info: TrackInfo, - ) { - super(track_index); - this.track = this.player.video.addTextTrack("subtitles", this.track_info.name, this.track_info.language); - this.buffered.value = [{ start: 0, end: this.player.duration.value, status: "loading" }] - this.init() - } - - private on_ready() { - if (!this.cues) return - this.buffered.value = [{ start: 0, end: this.player.duration.value, status: "buffered" }] - for (const cue of this.cues) { - this.track.addCue(create_cue(cue)); - } - this.track.mode = "showing"; - this.abort.signal.addEventListener("abort", () => { - // TODO disable subtitles properly - this.track.mode = "hidden"; - }); - } - - async init() { - try { - const res = await fetch(`${this.player.base_url}?format=remux&segment=0&container=jvtt&track=${this.track_index}`, { headers: { "Accept": "application/json" } }); - if (!res.ok) return this.player.error.value = "Cannot download index.", undefined; - let ai!: SubtitleCue[] & { error: string; }; - try { ai = await res.json(); } - catch (_) { this.player.set_pers("Error: Failed to fetch node"); } - if (ai.error) return this.player.set_pers("server error: " + ai.error), undefined; - this.cues = ai; - } catch (e) { - if (e instanceof TypeError) { - this.player.set_pers("Cannot download subtitles: Network Error"); - return undefined - } else throw e; - } - this.on_ready() - } - - public debug(): HTMLElement { - return e("pre", `vtt track ${this.track_index}\n\t${this.cues?.length} cues loaded`) - } -} - -function create_cue(cue: SubtitleCue): VTTCue { - const c = new VTTCue(cue.start, cue.end, cue.content); - const props = parse_layout_properties(cue.content.split("\n")[0]) - if (props) { - c.text = cue.content.split("\n").slice(1).join("\n") - // TODO re-enable when it works - // // TODO this does not work at all... - // const region = new VTTRegion() - // if ("position" in props && props.position.endsWith("%")) - // region.regionAnchorX = parseFloat(props.position.replace("%", "")) - // if ("line" in props && props.line.endsWith("%")) - // region.regionAnchorY = parseFloat(props.line.replace("%", "")) - // if ("align" in props) - // c.align = props.align as AlignSetting - // c.region = region - } else { - c.line = -2; - } - return c -} - -function parse_layout_properties(s: string): undefined | Record<string, string> { - const o: Record<string, string> = {} - for (const tok of s.split(" ")) { - const [k, v, ...rest] = tok.split(":") - if (!v || rest.length) return undefined - o[k] = v - } - // some common keys to prevent false positives - if ("position" in o) return o - if ("align" in o) return o - if ("line" in o) return o - return undefined -} |