Recently Written · git

subread-extension

Show your own .srt over a YouTube video, in time with it. Chrome and Firefox.

git clone https://github.com/equwal/subread-extension

Log | Files | Refs


src/srt.js (2758 bytes)

1 /* Subtitles as data: read an .srt (or .vtt), and find the cue for a moment of
2  * the video. No browser APIs here, so Node can test it.
3  *
4  * The video on YouTube is often not the same file as the audio the subtitles
5  * were made from: an intro was added, or the speed differs a little. So the
6  * time of the video is first moved onto the clock of the subtitles:
7  *
8  *     subtitle time = (video time - offset) * rate
9  *
10  * The transcript panel needs the other way round, from a cue's time back to a
11  * video time to seek to:
12  *
13  *     video time = subtitle time / rate + offset
14  */
15 (function (root) {
16   'use strict';
17 
18   const STAMP = /(?:(\d+):)?(\d{1,2}):(\d{2})[.,](\d{1,3})/;
19 
20   function seconds(stamp) {
21     const m = STAMP.exec(stamp);
22     if (!m) return NaN;
23     return (+(m[1] || 0)) * 3600 + (+m[2]) * 60 + (+m[3]) + (+m[4].padEnd(3, '0')) / 1000;
24   }
25 
26   /** Cues in order of start: [{ start, end, text }]. Blocks that are not cues are passed over. */
27   function parse(source) {
28     const cues = [];
29     const blocks = String(source).replace(/^/, '').replace(/\r\n?/g, '\n').split(/\n{2,}/);
30     for (const block of blocks) {
31       const lines = block.split('\n');
32       const at = lines.findIndex((l) => l.includes('-->'));
33       if (at < 0) continue;
34       const [from, to] = lines[at].split('-->');
35       const start = seconds(from), end = seconds(to);
36       const text = lines.slice(at + 1).join('\n').replace(/<[^>]*>/g, '').trim();
37       if (Number.isFinite(start) && Number.isFinite(end) && end > start && text) cues.push({ start, end, text });
38     }
39     return cues.sort((a, b) => a.start - b.start);
40   }
41 
42   /** The cue that is on at `t` (seconds on the clock of the subtitles), or null. */
43   function cueAt(cues, t) {
44     let lo = 0, hi = cues.length - 1, found = -1;
45     while (lo <= hi) {                       // the last cue that starts at or before t
46       const mid = (lo + hi) >> 1;
47       if (cues[mid].start <= t) { found = mid; lo = mid + 1; } else hi = mid - 1;
48     }
49     // Cues can overlap a little; the latest one that is still on wins.
50     for (let i = found; i >= 0 && i > found - 4; i--) if (t < cues[i].end) return cues[i];
51     return null;
52   }
53 
54   const subtitleTime = (videoT, offset, rate) => (videoT - offset) * rate;
55 
56   // The inverse: a moment on the clock of the subtitles, back to a moment of the video.
57   // Used by the transcript panel to seek: videoTime(subtitleTime(t, offset, rate), offset, rate) is t.
58   const videoTime = (subTime, offset, rate) => subTime / rate + offset;
59 
60   const api = { parse, cueAt, subtitleTime, videoTime, seconds };
61   if (typeof module !== 'undefined' && module.exports) module.exports = api;
62   else root.SubReadSrt = api;
63 })(typeof globalThis !== 'undefined' ? globalThis : this);