aboutsummaryrefslogtreecommitdiff
path: root/web/script/player/track/vtt.ts
blob: 4dddbc708ec041ec3976424644f3adda9ddfb7bd (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
import { SourceTrack, JvttCue } from "../jhls.d.ts";
import { Player } from "../player.ts";
import { PlayerTrack } from "./mod.ts";

export class VttPlayerTrack extends PlayerTrack {
  private track: TextTrack;
  public cues?: JvttCue[]

  constructor(
    private player: Player,
    private node_id: string,
    track_index: number,
    private metadata: SourceTrack,
  ) {
    super(track_index);
    this.track = this.player.video.addTextTrack("subtitles", this.metadata.name, this.metadata.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(`/n/${encodeURIComponent(this.node_id)}/stream?format=jvtt&tracks=${this.track_index}`, { headers: { "Accept": "application/json" } });
      if (!res.ok) return this.player.error.value = "Cannot download index.", undefined;
      let ai!: JvttCue[] & { 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()
  }
}

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
}