aboutsummaryrefslogtreecommitdiffstats
path: root/youtube/static/js/transcript-table.js
blob: 5cee97e4af2f2fa8a078877fd41c86ed7cdc9879 (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
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
let details_tt, select_tt, table_tt;

function renderCues() {
  const selectedTrack = QId("js-video-player").textTracks[select_tt.selectedIndex];
  const cuesList = [...selectedTrack.cues];
  const is_automatic = cuesList[0].text.startsWith(" \n");

  // Firefox ignores cues starting with a blank line containing a space
  // Automatic captions contain such a blank line in the first cue
  let ff_bug = false;
  if (!cuesList[0].text.length) { ff_bug = true; is_automatic = true };
  let rows;

  function forEachCue(callback) {
    for (let i=0; i < cuesList.length; i++) {
      let txt, startTime = selectedTrack.cues[i].startTime;
      if (is_automatic) {
        // Automatic captions repeat content. The new segment is displayed
        // on the bottom row; the old one is displayed on the top row.
        // So grab the bottom row only. Skip every other cue because the bottom
        // row is empty.
        if (i % 2) continue;
        if (ff_bug && !selectedTrack.cues[i].text.length) {
          txt = selectedTrack.cues[i+1].text;
        } else {
          txt = selectedTrack.cues[i].text.split('\n')[1].replace(/<[\d:.]*?><c>(.*?)<\/c>/g, "$1");
        }
      } else {
        txt = selectedTrack.cues[i].text;
      }
      callback(startTime, txt);
    }
  }

  function createTimestampLink(startTime, txt, title=null) {
    a = document.createElement("a");
    a.appendChild(text(txt));
    a.href = "javascript:;";  // TODO: replace this with ?t parameter
    if (title) a.title = title;
    a.addEventListener("click", (e) => {
      QId("js-video-player").currentTime = startTime;
    })
    return a;
  }

  clearNode(table_tt);
  console.log("render cues..", selectedTrack.cues.length);
  if (Q("input#transcript-use-table").checked) {
    forEachCue((startTime, txt) => {
      let tr, td, a;
      tr = document.createElement("tr");

      td = document.createElement("td")
      td.appendChild(createTimestampLink(startTime, toTimestamp(startTime)));
      tr.appendChild(td);

      td = document.createElement("td")
      td.appendChild(text(txt));
      tr.appendChild(td);

      table_tt.appendChild(tr);
    });
    rows = table_tt.rows;
  }
  else {
    forEachCue((startTime, txt) => {
      span = document.createElement("span");
      let idx = txt.indexOf(" ", 1);
      let [firstWord, rest] = [txt.slice(0, idx), txt.slice(idx)];

      span.appendChild(createTimestampLink(startTime, firstWord, toTimestamp(startTime)));
      if (rest) span.appendChild(text(rest + " "));
      table_tt.appendChild(span);
    });
    rows = table_tt.childNodes;
  }

  let lastActiveRow = null;
  let row;
  function colorCurRow(e) {
    // console.log("cuechange:", e);
    let activeCueIdx = cuesList.findIndex((c) => c == selectedTrack.activeCues[0]);
    let activeRowIdx = is_automatic ? Math.floor(activeCueIdx / 2) : activeCueIdx;

    if (lastActiveRow) lastActiveRow.style.backgroundColor = "";
    if (activeRowIdx < 0) return;
    row = rows[activeRowIdx];
    row.style.backgroundColor = "#0cc12e42";
    lastActiveRow = row;
  }
  selectedTrack.addEventListener("cuechange", colorCurRow);
}

function loadCues() {
  const textTracks = QId("js-video-player").textTracks;
  const selectedTrack = textTracks[select_tt.selectedIndex];

  // See https://developer.mozilla.org/en-US/docs/Web/API/TextTrack/mode
  // This code will (I think) make sure that the selected track's cues
  // are loaded even if the track subtitles aren't on (showing). Setting it
  // to hidden will load them.
  let selected_track_target_mode = "hidden";

  for (let track of textTracks) {
    // Want to avoid unshowing selected track if it's showing
    if (track.mode === "showing") selected_track_target_mode = "showing";

    if (track !== selectedTrack) track.mode = "disabled";
  }
  if (selectedTrack.mode == "disabled") {
    selectedTrack.mode = selected_track_target_mode;
  }

  let intervalID = setInterval(() => {
    if (selectedTrack.cues && selectedTrack.cues.length) {
      clearInterval(intervalID);
      renderCues();
    }
  }, 100);
}

window.addEventListener('DOMContentLoaded', function() {
  const textTracks = QId("js-video-player").textTracks;
  if (!textTracks.length) return;

  details_tt = Q("details#transcript-details");
  details_tt.addEventListener("toggle", () => {
    if (details_tt.open) loadCues();
  });

  select_tt = Q("select#select-tt");
  select_tt.selectedIndex = getDefaultTranscriptTrackIdx();
  select_tt.addEventListener("change", loadCues);

  table_tt = Q("table#transcript-table");
  table_tt.appendChild(text("loading..."));

  textTracks.addEventListener("change", (e) => {
    // console.log(e);
    let idx = getActiveTranscriptTrackIdx();  // sadly not provided by 'e'
    if (textTracks[idx].mode == "showing") {
      select_tt.selectedIndex = idx;
      loadCues();
    }
    else if (details_tt.open && textTracks[idx].mode == "disabled") {
      textTracks[idx].mode = "hidden";  // so we still receive 'oncuechange'
    }
  })

  Q("input#transcript-use-table").addEventListener("change", renderCues);
});