aboutsummaryrefslogtreecommitdiff
path: root/web/script/player/track/vtt.ts
blob: efdafe00f8a9d21ef6ab7342d60d67aa564b2d93 (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
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.buffered.value = [{ start: 0, end: player.duration.value, status: "buffered" }]
    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
}