aboutsummaryrefslogtreecommitdiff
path: root/web/script/player/track
diff options
context:
space:
mode:
Diffstat (limited to 'web/script/player/track')
-rw-r--r--web/script/player/track/create.ts15
-rw-r--r--web/script/player/track/mod.ts25
-rw-r--r--web/script/player/track/mse.ts208
-rw-r--r--web/script/player/track/vtt.ts96
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
-}