Skip to content

Timeline Coordinate Systems

Sapari uses two coordinate systems for positioning edits and captions on the timeline. Understanding the distinction is critical when working with the frontend.

Main-Video-Relative Time (Backend)

The backend stores all edit positions relative to the main video, where 0 = the first frame of the uploaded video. This is the canonical coordinate system — it never changes regardless of what intro/outro assets exist.

Main video: [0ms ────────────────────── 60000ms]
Edit at:     [5000ms ── 8000ms]   (silence removal)

All Edit records in the database use this coordinate system. The start_ms and end_ms fields always reference the main video timeline.

For multi-clip projects, the "main video" is the concatenation of all Clip rows in display_order, and edit.end_ms is bounded by SUM(ClipFile.duration_ms) across the project's clips. EditService.create and EditService.update enforce end_ms ≤ total via clip.utils.get_project_total_duration_ms. See that helper's docstring for how the ready/not-ready/empty states are handled; the short version is that edits on a project with no clips are rejected, edits on a project where any clip is still processing skip the check, and the bulk-insert worker path is intentionally unvalidated (trusted caller).

Effective Timeline (Frontend)

When an intro clip exists, the frontend needs to display it before the main video on the timeline. This shifts everything forward by introOffsetMs (the intro's duration).

Intro:      [0ms ── 3000ms]
Main video: [3000ms ────────────────────── 63000ms]
Edit at:     [8000ms ── 11000ms]  (same edit, shifted by 3000ms)
Outro:      [63000ms ── 66000ms]

The effective timeline is what users see and interact with. It includes intro, main video, and outro as one continuous strip.

The Shift: introOffsetMs

introOffsetMs = intro clip exists ? intro.duration_ms : 0

Converting Backend → Frontend (Display)

Add the offset to show edits at correct positions on the effective timeline:

const displayStartMs = edit.start_ms + introOffsetMs;
const displayEndMs = edit.end_ms + introOffsetMs;

Converting Frontend → Backend (Save)

Subtract the offset before sending to the API:

const backendStartMs = displayStartMs - introOffsetMs;
const backendEndMs = displayEndMs - introOffsetMs;

Display Data Pattern

All coordinate conversion happens once in Dashboard.tsx, producing display* versions of backend data:

Raw (backend) Display (frontend) How
edits displayEdits Shift non-fixed edits by introOffsetMs, add fixed-position assets at correct effective positions
captionLines displayCaptionLines Shift timestamps by introOffsetMs

Components receive the display* versions and never need to do offset math themselves.

Fixed-Position Assets

Intro, outro, watermark, and background audio have fixedPosition set. These assets must not be shifted like normal edits — they have their own positioning logic:

Asset Effective Timeline Position
Intro 0intro.duration_ms
Outro effectiveTotalDurationMs - outro.duration_mseffectiveTotalDurationMs
Watermark 0effectiveTotalDurationMs (spans entire timeline)
Background Audio 0effectiveTotalDurationMs

The displayEdits computation filters out fixed-position edits from the normal shift, then re-adds them at the correct positions:

// 1. Filter out fixed-position edits
const shiftableEdits = edits.filter(e => !e.fixedPosition);

// 2. Shift normal edits
const shifted = shiftableEdits.map(e => ({
  ...e,
  start_ms: e.start_ms + introOffsetMs,
  end_ms: e.end_ms + introOffsetMs,
}));

// 3. Add fixed assets at correct positions
const introEdit = { start_ms: 0, end_ms: introDuration, ... };
const outroEdit = { start_ms: totalDuration - outroDuration, end_ms: totalDuration, ... };
const watermarkEdit = { start_ms: 0, end_ms: totalDuration, ... };

Common Gotchas

Shifting fixed-position edits: Raw edits from the backend includes intro/outro/watermark. If you shift ALL edits by introOffsetMs, fixed assets end up at wrong positions. Always filter them out first.

Watermark span: When intro/outro exist, the watermark needs start_ms: 0, end_ms: effectiveTotalDurationMs — not just the main video portion.

Drag operations: When users drag edits on the timeline, the drag position is in effective-timeline coordinates. Subtract introOffsetMs before calling updateEditLocally or saving to the backend.

Seeking to edits: When seeking the video player to an edit from raw edits state, add introOffsetMs to get the correct effective-timeline position.


← Workers Backend Models →