Build Frame.io

KINDAreplaces $25/mosaves $300/yrback to the verdict

0%0 of 20 items done

Saved on this device only. Tick prerequisites first, then work the phases in order · do not start one until the checks above it pass.

A watch folder that transcodes a review proxy, an unlisted page where reviewers scrub and comment on an exact frame, versions stacked so a client can compare, and an export of every note as timecode your editing application understands.

estimated effort weekendthe files for this build are in the project pack

Transcodeffmpeg producing a review proxy with a fixed keyframe intervalRuntimeNode 22 with SQLitePlayerA player driven by frame number, not secondsStorageLocal disk or S3-compatible storage behind a reverse proxyWatchingA folder watcher with a settle check

Before step 1

Everything below is assumed from the first step. Tick each one when you actually have it, not when you plan to.

  1. installfree

    Why Runs the watcher, the server and the share pages.

    Get it nodejs.org, or nvm install 22. open ↗

    Verify node --version prints v22 or higher

  2. installfree

    Why Probing for the exact frame rate and transcoding proxies both run through them.

    Get it brew install ffmpeg or your package manager. open ↗

    Verify ffprobe reports a rational frame rate for a test file

  3. have readyfree

    Why The timecode work is only testable against a real 29.97 file, and rounding errors only become visible at length.

    Get it Use your own footage. If you have a drop-frame file, use that; otherwise generate one at 30000 over 1001.

    Verify Both are available and one reports a non-integer rational rate

  4. have ready10 to 40 US dollars a month depending on storage and transfer

    Why Proxies are large and clients stream them. A small instance will transcode slowly and serve worse.

    Get it A virtual server with enough disk for your active projects and bandwidth you have checked the price of.

    Verify A proxy transcodes and streams smoothly to another network

  5. installfree if you already have one

    Why Phase 5's export is only correct if the markers land where the comments were, and only the editing application can tell you.

    Get it Whichever you already use. The marker import format differs, so target the one you actually work in.

    Verify Exported markers import and land on the intended frames

Data model

Create these before the first phase that stores anything. Changing a table later is the expensive kind of change.

- assets(id, source_path, title, frame_rate_num, frame_rate_den, drop_frame, duration_frames, timecode_start_frames, proxy_key, state)
- versions(id, asset_group_id, asset_id, ordinal, label, uploaded_at) · a group is one shot reviewed repeatedly
- shares(id, asset_group_id, token, password_hash, expires_at, allow_download, created_at)
- comments(id, version_id, frame, author_name, body, created_at, resolved_at, parent_id)
- annotations(comment_id, shape_json) · a drawing over the frame, stored in normalised coordinates
- The frame rate is stored as a numerator and a denominator. Storing it as a decimal is the bug that produces off-by-one frames an hour into a long file.

Environment variables

These go in a .env file the app reads at startup. The pack's .env.example is this table as a file · copy it, never commit the filled-in version.

VariableNeededExampleWhere the value comes from
WATCH_DIRrequired./incomingFolder watched for new exports. Files here are never modified.
PROXY_DIRrequired./proxiesWhere review proxies are written. Treat as a cache.
PROXY_KEYFRAME_FRAMESrequired12Keyframe interval in frames. Short for scrubbing, at the cost of file size.
PROXY_CRFrequired23Quality setting for the proxy. Lower is better and larger.
SIGNED_MEDIA_TTL_SECONDSrequired7200How long a signed media URL lasts. Long enough for a review session.
PROXY_RETENTION_DAYSrequired120Age at which an inactive project's proxies are removed, after a warning.
PUBLIC_URLrequiredhttps://review.example.comUsed in share links. Must be HTTPS.

The build, in order

  1. Frame rates and timecode, before anything else

    You can convert between frame number and timecode correctly for a non-integer rate.

    1. Read the rational frame rate rather than the rounded one, plus duration and start timecode.

      Files src/probe.js

      terminal
      ffprobe -v error -select_streams v:0 -show_entries stream=r_frame_rate,nb_frames -show_entries format_tags=timecode -of json input.mov
    2. Frame to timecode and back, handling drop-frame and a non-zero start.

      Files src/timecode.js

    3. Every frame of a one hour file, both directions. This is cheap and catches every rounding mistake.

      Files test/timecode.test.js

    done when · tick each as it passes
    watch out
    • Storing 29.97 rather than 30000 over 1001 accumulates error, and after an hour the computed frame is wrong. Keep the rate rational throughout.
    • Drop-frame timecode skips frame numbers rather than frames: it omits numbers 0 and 1 at the start of each minute except every tenth minute, so wall clock and timecode stay close. Converting a drop-frame timecode with non-drop arithmetic is wrong by several seconds an hour, and the error grows.
  2. A proxy that scrubs well

    A reviewer can drag the playhead and see frames update immediately.

    1. Size stable across two checks before touching a file. An export in progress is the common case here.

      Files src/watch.js

    2. Short interval for scrubbing, matching the source frame rate exactly rather than normalising it.

      terminal
      ffmpeg -i source.mov -c:v libx264 -g 12 -keyint_min 12 -sc_threshold 0 -crf 23 -c:a aac proxy.mp4
    3. Source and proxy must have the same duration in frames, or every comment is offset.

      Files src/verify.js

    done when · tick each as it passes
    watch out
    • A proxy with a long keyframe interval forces the player to decode from the last keyframe on every seek, which makes scrubbing feel broken even though playback is fine. Use a short interval, accept the larger file, and remember that reviewers judge this tool on scrubbing rather than on playback.
  3. A review link that plays for a client

    Someone with the link can watch and scrub without an account.

    1. Unguessable token, optional password, expiry. No public listing anywhere.

      Files src/share.js

    2. The proxy is private and reachable only through a short-lived signature issued after the gate.

      Files src/media.js

    3. Arrow keys step one frame, the display shows timecode, and every seek is computed from a frame number.

      Files public/player.js

    done when · tick each as it passes
    watch out
    • An unguessable page URL with a predictable video file URL means the video circulates without the page, which for unreleased client work is exactly what the review link was supposed to prevent. Sign the media requests and keep the file private.
  4. Comments pinned to a frame

    A comment lands on the exact frame the reviewer was looking at.

    1. Not seconds. The frame is what the reviewer saw and what the editor needs.

      Files src/comments.js

    2. Relative to frame dimensions, so drawings scale with the display.

      Files public/annotate.js

    3. Open the same link on a phone and a desktop. The drawing must land in the same place.

    done when · tick each as it passes
    watch out
    • Storing an annotation in pixel coordinates makes it land in the wrong place on any other display size or if the proxy resolution changes. Normalise to the frame's dimensions, so the drawing scales with whatever the reviewer is watching on.
  5. Versions and the notes going back

    A client can compare versions, and you get every note as timecode.

    1. One share, several versions, comments per version, with a visible current version.

      Files src/versions.js

    2. Rational rate and drop-frame flag applied. Verify a note near the end rather than the start.

      Files src/export.js

    3. Import the markers and confirm placement. This is the step that reveals a rounding error.

    done when · tick each as it passes
    watch out
    • Exporting notes as timecode computed with a rounded frame rate produces markers that drift from the comment position, and the drift grows through the file so the last notes are the most wrong. Use the stored rational rate and the drop-frame flag for every conversion in the export.
what this build does not replace
after v1, if you want it

Need the files? The project pack on the verdict page hands your agent the whole brief · more audio & video.