aboutsummaryrefslogtreecommitdiff
path: root/web/script/player/track/vtt.ts
blob: 43413bdf231b66b02ee585d19bb4f388602ff0aa (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
/*
    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) 2025 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
}