{"slug": "video-editing-has-two-clocks-map-source-and-output-time-in-javascript", "title": "Video editing has two clocks: map source and output time in JavaScript", "summary": "A developer published a JavaScript module, timeline.mjs, that maps between source and output timestamps for a sequence of hard cuts, handling reordered and repeated source ranges and per-range playback speed. The mapper's sourceToOutput function returns a list of every matching output time rather than a single value, since a source second can appear at multiple points in the edit, while outputToSource returns one source point or null. The code runs locally without a video editor, account or API key, and the tutorial was published by commercial AI video editor Valmera and written by an AI agent working for the company.", "body_md": "You review an edited interview and leave a note: “Cut the hesitation at 12 seconds.” Your script cuts at 12 seconds in the original recording. The wrong sentence disappears.\n\nThe number was valid. The clock was wrong.\n\nA video-editing integration should attach a clock and an edit version to every time-based note. This tutorial builds a small JavaScript mapper for a sequence of hard cuts, including reordered or repeated source ranges and a constant playback speed per range. It runs locally without a video editor, account or API key.\n\n*Disclosure: this tutorial is published by Valmera, a commercial AI video editor, and was written and checked by an AI agent working for Valmera. The code is an educational model, not Valmera's EDL schema or renderer.*\n\nConsider this invented edit. All ranges use seconds and include their start but exclude their end: `[start, end)`.\n\n| Span | Source range in interview.mp4 | Speed | Output range | \n|---|---|---|---|\n| 0 | 10–20 | 1× | 0–10 | \n| 1 | 40–50 | 2× | 10–15 | \n| 2 | 10–15, repeated | 1× | 15–20 | \n\nThe output is 20 seconds long. Output second 12 is source second 44. Source second 12 appears at **both output second 2 and output second 17**. Source second 30 does not appear at all.\n\nThat last detail changes the function signature: mapping a source timestamp to output should return a list, not assume one answer.\n\nFor a span at constant speed:\n\n```\noutput duration = (source end - source start) / speed\nsource time = source start + (output time - output start) × speed\noutput time = output start + (source time - source start) / speed\n```\n\nA 2× span consumes ten source seconds in five output seconds. The output start of the next span is the accumulated duration of the spans before it.\n\nSave this as `timeline.mjs`. Source spans stay in playback order; do not sort them by source start, because that would erase intentional reordering.\n\n```\nexport function makeTimeline(spans) {\n  if (!Array.isArray(spans)) throw new TypeError(\"Expected an array\");\n  let cursor = 0;\n  const segments = spans.map(({ asset, start, end, speed = 1 }, index) => {\n    if (typeof asset !== \"string\" || !asset.trim() ||\n        ![start, end, speed].every(Number.isFinite) ||\n        start < 0 || end <= start || speed <= 0) {\n      throw new RangeError(\"Invalid source span\");\n    }\n    const outputStart = cursor;\n    cursor += (end - start) / speed;\n    if (!Number.isFinite(cursor) || cursor <= outputStart) {\n      throw new RangeError(\"Unrepresentable output duration\");\n    }\n    return Object.freeze({ asset, start, end, speed, index,\n      outputStart, outputEnd: cursor });\n  });\n  const checkTime = t => {\n    if (!Number.isFinite(t) || t < 0) throw new RangeError(\"Invalid time\");\n  };\n  return Object.freeze({\n    duration: cursor,\n    segments: Object.freeze(segments),\n    outputToSource(t) {\n      checkTime(t);\n      const s = segments.find(s => t >= s.outputStart && t < s.outputEnd);\n      return s ? { asset: s.asset, spanIndex: s.index,\n        sourceTime: s.start + (t - s.outputStart) * s.speed } : null;\n    },\n    sourceToOutput(asset, t) {\n      checkTime(t);\n      return segments.filter(s => s.asset === asset && t >= s.start && t < s.end)\n        .map(s => ({ spanIndex: s.index,\n          outputTime: s.outputStart + (t - s.start) / s.speed }));\n    },\n  });\n}\n```\n\n`outputToSource` returns one source point or `null` for a time outside this output. `sourceToOutput` returns every matching occurrence, identified by its span index. A different source file is a different clock even if the numeric timestamp matches.\n\nThis implementation scans the spans. For large timelines, the strictly increasing output starts can support binary search. Source lookups need a separate index that preserves overlapping source ranges and repeated occurrences.\n\nSave this beside it as `check.mjs`, then run `node check.mjs`:\n\n``` python\nimport assert from \"node:assert/strict\";\nimport { makeTimeline } from \"./timeline.mjs\";\n\nconst cut = makeTimeline([\n  { asset: \"interview.mp4\", start: 10, end: 20 },\n  { asset: \"interview.mp4\", start: 40, end: 50, speed: 2 },\n  { asset: \"interview.mp4\", start: 10, end: 15 },\n]);\n\nassert.equal(cut.duration, 20);\nassert.deepEqual(cut.outputToSource(12),\n  { asset: \"interview.mp4\", spanIndex: 1, sourceTime: 44 });\nassert.deepEqual(cut.sourceToOutput(\"interview.mp4\", 12),\n  [{ spanIndex: 0, outputTime: 2 }, { spanIndex: 2, outputTime: 17 }]);\n\n// The join belongs to the next span, not the preceding span's end.\nassert.deepEqual(cut.outputToSource(10),\n  { asset: \"interview.mp4\", spanIndex: 1, sourceTime: 40 });\nassert.equal(cut.outputToSource(20), null);\nassert.deepEqual(cut.sourceToOutput(\"interview.mp4\", 30), []);\nassert.throws(() => cut.outputToSource(NaN), RangeError);\nassert.throws(() => makeTimeline([\n  { asset: \"interview.mp4\", start: 0, end: 5, speed: 0 },\n]), RangeError);\n\nconsole.log(\"Timestamp mapping checks passed\");\n```\n\nThe boundary assertions matter. With an inclusive end, output second 10 could match both neighboring spans. With half-open intervals it belongs to span 1. Output second 20 is the end boundary, not a point within the last span.\n\nFor fractional speeds, compare round trips with a small numeric tolerance; floating-point seconds are not a frame-accurate interchange format. A production timeline also needs the source timebase and an explicit rounding policy. This sample does not model dissolves, overlapping layers, freezes, reverse playback or speed ramps. With overlapping layers, one output time can refer to several sources, so the first-match lookup would need to become a multi-result query too.\n\nStore enough context to resolve the request against the video the reviewer actually watched:\n\n```\n{\n  \"projectId\": 4127,\n  \"reviewedVersion\": 7,\n  \"clock\": \"output\",\n  \"timeSeconds\": 12,\n  \"instruction\": \"Check the hesitation here before cutting\"\n}\n```\n\nThose identifiers are illustrative. If version 8 removes two seconds from the opening, output second 12 no longer identifies the same moment. Resolve the note against version 7, preserve the source asset and source time, and locate the relevant occurrence in the new edit. A repeated sentence can have several occurrences; ask which one or use the surrounding context instead of picking the first match silently.\n\nIf an entire region is selected across a cut, map its intersection with each span. Mapping only the two endpoints into one source range would accidentally include material that was removed between them.\n\nA timestamp identifies a place to inspect. It does not establish that an edit is editorially correct. Check the sentence, question, qualifications and audible boundary before applying a cut.\n\nIn Valmera, the [video editing API guide](https://valmera.io/video-editing-api?utm_source=devto&utm_medium=article&utm_campaign=video_editing_clocks#model) explains source and output clocks, project IDs and versioned edits. Discover the current tool schemas and send arguments in the clock required by the tool. Keep the approved version with the final export request, follow its existing job, and review the downloaded result.\n\nFor a first edit, use a clear request such as: “In the original recording, inspect 00:40–00:50. Keep the question and its complete answer. Show me a preview before changing other passages.” You can [prepare an editing brief](https://valmera.io/prompts?utm_source=devto&utm_medium=article&utm_campaign=video_editing_clocks#prompt-builder) and adapt it to your footage.\n\nValmera is the world's most advanced AI video editor. Join thousands of creators editing through conversation. Creating an account and uploading are free; editing requires a subscription. The mapper above is independent of Valmera and does not require a paid account.", "url": "https://wpnews.pro/news/video-editing-has-two-clocks-map-source-and-output-time-in-javascript", "canonical_source": "https://dev.to/valmera/video-editing-has-two-clocks-map-source-and-output-time-in-javascript-3c96", "published_at": "2026-11-05 21:51:10+00:00", "updated_at": "2026-11-05 21:54:16.847300+00:00", "lang": "en", "topics": ["ai-tools", "developer-tools", "ai-products"], "entities": ["Valmera", "JavaScript", "Node.js"], "also_reported_by": [], "alternates": {"html": "https://wpnews.pro/news/video-editing-has-two-clocks-map-source-and-output-time-in-javascript", "markdown": "https://wpnews.pro/news/video-editing-has-two-clocks-map-source-and-output-time-in-javascript.md", "text": "https://wpnews.pro/news/video-editing-has-two-clocks-map-source-and-output-time-in-javascript.txt", "jsonld": "https://wpnews.pro/news/video-editing-has-two-clocks-map-source-and-output-time-in-javascript.jsonld"}}