Miraiclip SDK
Fundamentals

Project & time

The project store, its document, and microsecond timing.

A project holds everything: the composition document, undo history, and the playhead and selection. Create one per open video.

import { createProject } from "@miraiclip/core";

const project = createProject({ width: 1920, height: 1080, fps: 30 });
createProject(init, options?)
initSettings { width, height, fps, name? } for a new project, or a saved document from toJSON()
options.historyLimitUndo steps kept (default 500)

State

const state = project.getState();

state.doc;         // the composition: saved, undoable
state.playheadUs;  // ephemeral
state.selection;   // ephemeral, clip ids
PartChanged byUndoableSaved by toJSON()
docCommands onlyYesYes
playheadUsproject.setPlayhead(us), the playerNoNo
selectionproject.setSelection(ids)NoNo

Never mutate state you read. Dispatch a command instead.

The document

const { settings, assets, tracks, trackOrder, clips, transitions } = project.getState().doc;
FieldShape
settings{ width, height, fps, frameRate?, name? }
assetsRecord<id, Asset>: Assets
tracksRecord<id, Track>: Tracks
trackOrderTrack ids, bottom layer first
clipsRecord<id, Clip>: Clips
transitionsRecord<id, Transition>: Transitions

Subscribing

subscribe(selector, listener) calls the listener only when the selected value changes. It returns an unsubscribe function.

const stop = project.subscribe(
  (s) => s.doc.clips,
  (clips, previous) => console.log("clips changed"),
);
stop();

Wiring this into React, Vue or Svelte: Reading state.

Time is in microseconds

Every position and duration is a whole number of microseconds (µs): 1 second = 1,000,000 µs.

import { secondsToUs, usToSeconds, US_PER_SECOND } from "@miraiclip/core";

secondsToUs(2.5);        // 2_500_000
usToSeconds(1_500_000);  // 1.5
5 * US_PER_SECOND;       // 5_000_000

Integer µs keep the math exact: no rounding drift, even over an hour at 29.97 fps.

Frames

A frame rarely lasts a whole number of µs (33,333.3… at 30 fps). One rule maps between them everywhere:

HelperReturns
frameToUs(n, rate)Start of frame n: the first whole µs inside it
usToFrame(us, rate)The frame that contains us
snapToFrame(us, rate)The nearest frame start
frameCount(us, rate)Frames in a duration
frameDurationUs(rate)Length of one frame (fractional)
usToTimecode(us, rate, { dropFrame? })"HH:MM:SS:FF", or "HH:MM:SS;FF" with drop-frame
import { frameToUs, snapToFrame, usToFrame, usToTimecode } from "@miraiclip/core";

frameToUs(1, 30);                 // 33_334
usToFrame(33_334, 30);            // 1
snapToFrame(50_000, 30);          // 33_334
usToTimecode(3_600_000_000, 30);  // "01:00:00:00"

Snap user input (drags, typed times) with snapToFrame so edits land on frame boundaries.

NTSC rates

fps accepts decimals. 29.97, 23.976 and 59.94 are stored as exact ratios:

import { projectFrameRate } from "@miraiclip/core";

project.dispatch({ type: "project/set-settings", payload: { fps: 29.97 } });

project.getState().doc.settings.frameRate;          // { num: 30000, den: 1001 }
projectFrameRate(project.getState().doc.settings);  // { num: 30000, den: 1001 }

Pass projectFrameRate(settings) to the frame helpers: they accept a number or a { num, den } ratio.

Save and load

import { createProject } from "@miraiclip/core";

const saved = project.toJSON();          // plain JSON: store it anywhere
const restored = createProject(saved);   // history starts empty

toJSON() returns the document only. The playhead, selection and undo history aren't saved.

On this page