aboutsummaryrefslogtreecommitdiff
path: root/web/script/player/track
diff options
context:
space:
mode:
authormetamuffin <metamuffin@disroot.org>2024-01-28 01:08:49 +0100
committermetamuffin <metamuffin@disroot.org>2024-01-28 01:08:49 +0100
commitb514ec8cea2c2143e0bd7a0eb377c96a6f091d0d (patch)
tree25b3f8c80e1754b3e9e5d5419d5be276f940b1c1 /web/script/player/track
parent59ef86b0a637ec3ce44ca495c6d22ddf61649134 (diff)
downloadjellything-b514ec8cea2c2143e0bd7a0eb377c96a6f091d0d.tar
jellything-b514ec8cea2c2143e0bd7a0eb377c96a6f091d0d.tar.bz2
jellything-b514ec8cea2c2143e0bd7a0eb377c96a6f091d0d.tar.zst
add broken positioning code für vtt
Diffstat (limited to 'web/script/player/track')
-rw-r--r--web/script/player/track/create.ts12
-rw-r--r--web/script/player/track/mod.ts24
-rw-r--r--web/script/player/track/mse.ts140
-rw-r--r--web/script/player/track/vtt.ts80
4 files changed, 256 insertions, 0 deletions
diff --git a/web/script/player/track/create.ts b/web/script/player/track/create.ts
new file mode 100644
index 0000000..f674c3a
--- /dev/null
+++ b/web/script/player/track/create.ts
@@ -0,0 +1,12 @@
+import { get_track_kind } from "../mediacaps.ts";
+import { create_vtt_track } from "./vtt.ts";
+import { create_mse_track } from "./mse.ts";
+import { Player } from "../player.ts";
+import { SourceTrack } from "../jhls.d.ts";
+import { PlayerTrack } from "./mod.ts";
+
+export async function create_track(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 create_vtt_track(player, node_id, track_index, metadata)
+ else return await create_mse_track(player, node_id, track_index, metadata)
+}
diff --git a/web/script/player/track/mod.ts b/web/script/player/track/mod.ts
new file mode 100644
index 0000000..cdb07cc
--- /dev/null
+++ b/web/script/player/track/mod.ts
@@ -0,0 +1,24 @@
+/*
+ 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 { TimeRange } from "../jhls.d.ts";
+import { OVar } from "../../jshelper/mod.ts";
+import { BufferRange } from "../player.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 {
+ constructor(
+ public track_index: number,
+ ) { }
+ public buffered = new OVar<BufferRange[]>([]);
+ public abort = new AbortController()
+ async update(_target: number) { }
+}
+
diff --git a/web/script/player/track/mse.ts b/web/script/player/track/mse.ts
new file mode 100644
index 0000000..623e149
--- /dev/null
+++ b/web/script/player/track/mse.ts
@@ -0,0 +1,140 @@
+import { JhlsTrackIndex, SourceTrack } 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 { PlayerTrack, AppendRange, TARGET_BUFFER_DURATION, MIN_BUFFER_DURATION } from "./mod.ts";
+
+export async function create_mse_track(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;
+ }
+}
+
+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);
+
+ 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);
+ }
+ }
+}
diff --git a/web/script/player/track/vtt.ts b/web/script/player/track/vtt.ts
new file mode 100644
index 0000000..c426fa1
--- /dev/null
+++ b/web/script/player/track/vtt.ts
@@ -0,0 +1,80 @@
+import { SourceTrack, JvttCue } from "../jhls.d.ts";
+import { Player } from "../player.ts";
+import { PlayerTrack } from "./mod.ts";
+
+export async function create_vtt_track(player: Player, node_id: string, track_index: number, metadata: SourceTrack): Promise<VttPlayerTrack | undefined> {
+ let index: JvttCue[];
+ 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 ai!: JvttCue[] & { error: string; };
+ try { ai = await res.json(); }
+ catch (_) { player.set_pers("Error: Failed to fetch node"); }
+ if (ai.error) return player.set_pers("server error: " + ai.error), undefined;
+ index = ai;
+ } catch (e) {
+ if (e instanceof TypeError) {
+ player.set_pers("Cannot download subtitles: Network Error");
+ return undefined
+ } else throw e;
+ }
+ const t = new VttPlayerTrack(player, node_id, track_index, metadata, index);
+ return t;
+}
+
+export class VttPlayerTrack extends PlayerTrack {
+ private track: TextTrack;
+
+ 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) {
+ this.track.addCue(create_cue(cue));
+ }
+ this.track.mode = "showing";
+ this.abort.signal.addEventListener("abort", () => {
+ // TODO disable subtitles properly
+ this.track.mode = "hidden";
+ });
+ }
+}
+
+function create_cue(cue: JvttCue): 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 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
+}