Miraiclip SDK
Fundamentals

Clips

Video, audio, image, text, caption and HTML clips on the timeline.

project.dispatch({
  type: "clip/add",
  payload: {
    kind: "video",
    id: "intro",
    trackId: "main",
    assetId: "intro-mp4",
    startUs: 0,
    durationUs: 5_000_000,
  },
});

Kinds

KindNeedsTrackGuide
videoassetId of a video assetvideo—
audioassetId of an audio assetaudioAudio
imageassetId of an image assetvideo—
texttextvideoText & typography
captionwords with timingvideoCaptions
htmltemplatevideoHTML clips

You can also register your own kinds: Custom clip kinds.

Fields every clip has

FieldDefaultMeaning
id, trackIdrequired
startUsrequiredPosition on the timeline
durationUsrequiredLength on the timeline
transform{ x: 0.5, y: 0.5, scale: 1, rotation: 0, opacity: 1 }Placement; see below
animationsabsentKeyframes per property: Keyframes
effectsabsentEffect stack: Effects

Transform

FieldUnit
x, yClip center, normalized: 0.5, 0.5 is the canvas center, 0, 0 the top-left corner
scale1 fits the clip inside the canvas, keeping its aspect ratio
rotationDegrees
opacity0 to 1

Because scale: 1 means "fit", a 4K source on a 720p canvas fills the frame, and the layout doesn't change between preview and export.

Video and audio clips

FieldDefaultMeaning
trimStartUs0Where in the source the clip starts playing
volume1Gain; can be keyframed
fadeInUs, fadeOutUsabsentLinear fades at the clip's edges

The clip plays source media from trimStartUs to trimStartUs + durationUs. Source media outside that range is the clip's headroom; transitions draw from it. clipHeadroomUs(doc, clip, "in" | "out") reads it.

Editing

// Move on the timeline, or to another track
project.dispatch({ type: "clip/move", payload: { clipId: "intro", startUs: 1_000_000 } });

// Trim: set any of startUs, durationUs, trimStartUs
project.dispatch({
  type: "clip/trim",
  payload: { clipId: "intro", startUs: 2_000_000, durationUs: 3_000_000, trimStartUs: 1_000_000 },
});

// Split at a timeline time
project.dispatch({ type: "clip/split", payload: { clipId: "intro", atUs: 3_000_000, newClipId: "intro-b" } });

// Change properties: nested transform fields merge
project.dispatch({
  type: "clip/set-property",
  payload: { clipId: "intro", transform: { x: 0.25 }, volume: 0.5 },
});

// null clears an optional field
project.dispatch({ type: "clip/set-property", payload: { clipId: "intro", fadeInUs: null } });
CommandPayloadNotes
clip/addKind-specific, see above
clip/remove{ clipId }
clip/move{ clipId, startUs?, trackId? }The target track must accept the kind
clip/trim{ clipId, startUs?, durationUs?, trimStartUs? }
clip/split{ clipId, atUs, newClipId? }The right half gets newClipId (generated if omitted) and its trimStartUs advances. Fade-in stays on the left half, fade-out moves to the right
clip/duplicate{ clipId, newClipId?, startUs?, trackId? }Without startUs, the copy sits at the same time as the original
clip/set-property{ clipId, …fields }Any editable field of the clip's kind

What the core doesn't enforce

Your editor decides these rules:

  • Overlap: clips on the same track may overlap. Where they do, the clip that starts later is drawn on top. Most editors prevent overlaps in the UI and use separate tracks for layering.
  • Source length: durationUs can run past the end of the source media. Clamp against asset.durationUs when trimming.
  • Selection: removing a clip doesn't remove its id from selection. Filter out ids that no longer exist.

Reading a clip

import { isTextClip, isVideoClip } from "@miraiclip/core";

const clip = project.getState().doc.clips["intro"];
if (clip && isVideoClip(clip)) console.log(clip.trimStartUs, clip.volume);
if (clip && isTextClip(clip)) console.log(clip.text);

Guards: isVideoClip, isAudioClip, isImageClip, isTextClip, isCaptionClip, isHtmlClip.

On this page