commit 40ad0b23a9f45654eab40b9a067e56889f7770c1 equwal <truex@equwal.com> 2026-09-24 12:08:56 -0700 Add the inverse clock mapping, video time from subtitle time The transcript panel needs to seek the video from a cue's start. Add videoTime as the inverse of subtitleTime, and test the round trip for many offsets and rates.
src/srt.js | 13 +++++++++++-- tests/srt.test.cjs | 21 ++++++++++++++++++++- 2 files changed, 31 insertions(+), 3 deletions(-)
diff --git a/src/srt.js b/src/srt.js index b9dc846..1ebcaae 100644 --- a/src/srt.js +++ b/src/srt.js @@ -6,6 +6,11 @@ * time of the video is first moved onto the clock of the subtitles: * * subtitle time = (video time - offset) * rate + * + * The transcript panel needs the other way round, from a cue's time back to a + * video time to seek to: + * + * video time = subtitle time / rate + offset */ (function (root) { 'use strict'; @@ -46,9 +51,13 @@ return null; } - const subtitleTime = (videoTime, offset, rate) => (videoTime - offset) * rate; + const subtitleTime = (videoT, offset, rate) => (videoT - offset) * rate; + + // The inverse: a moment on the clock of the subtitles, back to a moment of the video. + // Used by the transcript panel to seek: videoTime(subtitleTime(t, offset, rate), offset, rate) is t. + const videoTime = (subTime, offset, rate) => subTime / rate + offset; - const api = { parse, cueAt, subtitleTime, seconds }; + const api = { parse, cueAt, subtitleTime, videoTime, seconds }; if (typeof module !== 'undefined' && module.exports) module.exports = api; else root.SubReadSrt = api; })(typeof globalThis !== 'undefined' ? globalThis : this); diff --git a/tests/srt.test.cjs b/tests/srt.test.cjs index b27de93..34112c1 100644 --- a/tests/srt.test.cjs +++ b/tests/srt.test.cjs @@ -1,7 +1,7 @@ /* node --test tests */ const test = require('node:test'); const assert = require('node:assert/strict'); -const { parse, cueAt, subtitleTime, seconds } = require('../src/srt.js'); +const { parse, cueAt, subtitleTime, videoTime, seconds } = require('../src/srt.js'); test('an .srt is read: numbers, stamps, text of more than one line, tags removed', () => { const cues = parse('1\r\n00:00:01,500 --> 00:00:04,000\r\nIt was a <i>dark</i> night;\r\nthe rain fell.\r\n\r\n' + @@ -68,3 +68,22 @@ test('the time of the video is moved onto the clock of the subtitles', () => { assert.equal(subtitleTime(100, 12.5, 1), 87.5); // the upload has a 12.5 s intro assert.ok(Math.abs(subtitleTime(3600, 0, 1.001) - 3603.6) < 1e-9); // and runs a little slow }); + +test('the transcript panel moves a cue time back onto the clock of the video', () => { + assert.equal(videoTime(100, 0, 1), 100); + assert.equal(videoTime(87.5, 12.5, 1), 100); // the 12.5 s intro comes back + assert.ok(Math.abs(videoTime(3603.6, 0, 1.001) - 3600) < 1e-9); +}); + +test('videoTime undoes subtitleTime, and subtitleTime undoes videoTime, for many offsets and rates', () => { + let seed = 7; + const random = () => (seed = (seed * 1103515245 + 12345) >>> 0) / 2 ** 32; + for (let round = 0; round < 500; round++) { + const offset = (random() - 0.5) * 40; // seconds, either side of 0 + const rate = 0.9 + random() * 0.2; // the panel limits rate to 0.9 .. 1.1 + const videoT = random() * 7200; // up to two hours in + const subT = subtitleTime(videoT, offset, rate); + assert.ok(Math.abs(videoTime(subT, offset, rate) - videoT) < 1e-9); + assert.ok(Math.abs(subtitleTime(videoTime(subT, offset, rate), offset, rate) - subT) < 1e-9); + } +});