src/srt.js (2326 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 (function (root) { 11 'use strict'; 12 13 const STAMP = /(?:(\d+):)?(\d{1,2}):(\d{2})[.,](\d{1,3})/; 14 15 function seconds(stamp) { 16 const m = STAMP.exec(stamp); 17 if (!m) return NaN; 18 return (+(m[1] || 0)) * 3600 + (+m[2]) * 60 + (+m[3]) + (+m[4].padEnd(3, '0')) / 1000; 19 } 20 21 /** Cues in order of start: [{ start, end, text }]. Blocks that are not cues are passed over. */ 22 function parse(source) { 23 const cues = []; 24 const blocks = String(source).replace(/^/, '').replace(/\r\n?/g, '\n').split(/\n{2,}/); 25 for (const block of blocks) { 26 const lines = block.split('\n'); 27 const at = lines.findIndex((l) => l.includes('-->')); 28 if (at < 0) continue; 29 const [from, to] = lines[at].split('-->'); 30 const start = seconds(from), end = seconds(to); 31 const text = lines.slice(at + 1).join('\n').replace(/<[^>]*>/g, '').trim(); 32 if (Number.isFinite(start) && Number.isFinite(end) && end > start && text) cues.push({ start, end, text }); 33 } 34 return cues.sort((a, b) => a.start - b.start); 35 } 36 37 /** The cue that is on at `t` (seconds on the clock of the subtitles), or null. */ 38 function cueAt(cues, t) { 39 let lo = 0, hi = cues.length - 1, found = -1; 40 while (lo <= hi) { // the last cue that starts at or before t 41 const mid = (lo + hi) >> 1; 42 if (cues[mid].start <= t) { found = mid; lo = mid + 1; } else hi = mid - 1; 43 } 44 // Cues can overlap a little; the latest one that is still on wins. 45 for (let i = found; i >= 0 && i > found - 4; i--) if (t < cues[i].end) return cues[i]; 46 return null; 47 } 48 49 const subtitleTime = (videoTime, offset, rate) => (videoTime - offset) * rate; 50 51 const api = { parse, cueAt, subtitleTime, seconds }; 52 if (typeof module !== 'undefined' && module.exports) module.exports = api; 53 else root.SubReadSrt = api; 54 })(typeof globalThis !== 'undefined' ? globalThis : this);