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


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);
+  }
+});