Built an open-source React video editing SDK

Every team building a React video editing SDK faces the same architectural fork: either you re-implement editing in JavaScript on top of canvas and the platform media APIs, or you let a native engine do the rendering and write React for everything above it. Most teams start out re-implementing editing in JavaScript, and we did too, before settling where we are now. This is the engineering story of what we actually built — including the parts we decided not to build.

A note on scope, because the title deserves precision. What we publish alongside the SDK is companion source code: a reference React integration layer, structured to be productized rather than read once and abandoned. It is not a rewrite of the engine, and it is not a distributable package you pull from a registry. The example browser editor in that companion source runs on the web module of the SDK — based on the same broadcast-grade NLE core that powers the native mobile modules — so the JavaScript you read is glue, orchestration, and interface, not media rendering.

What follows is why that boundary is drawn where it is, what the layer contains, the arguments for and against packaging it as an installable library, and honestly what we think you should copy or rewrite in your own project.

1. Product Positioning

Meishe Video Editor SDK is an all-in-one video editing SDK built on a self-developed video and audio processing engine with a broadcast-grade NLE (Non-Linear Editing) architecture.

On the web, that core is exposed through a browser module; the React layer sits above it and speaks to it over an imperative interface. We deliberately do not own the hard parts — decode, compositing, GPU rendering, audio mixing, and encode stay inside the core. What we own around them is the control plane: session lifecycle, immutable view state, panel logic, keyboard shortcuts, undo affordances, and export orchestration.

2. What we built above the engine

The layer turned out to be four concerns, roughly in order of how much time they consumed.

Session lifecycle. Creating a session is asynchronous, holds GPU resources, and must be torn down deterministically. Most early bugs in our own integration were lifetime bugs: a session created twice after a fast remount, or leaked when a route changed mid-load.

State projection. The core emits events — playback position, duration changes, edit applied, export progress. The layer reduces them into one snapshot that React renders from. Nothing else is allowed to mutate what the UI displays.

Command dispatch. Every intent from the UI becomes a typed command toward the session: add a clip, seek, apply an effect stack, change a transition duration. Commands are asynchronous, so the layer also owns optimistic presentation and error surfacing.

Interface. Panels, timelines, the trim handle, subtitle editing, effect rails. This is ordinary product work, and it is where most of your own differentiation will live.

Built an open-source React video editing SDK

3. Why we publish source rather than a package

This decision gets argued about internally, so here is the reasoning rather than a verdict.

OptionWhat you gainWhat it costsWhy we landed where we did
Distribute a versioned libraryOne-line install, semantic upgrade pathWe freeze an opinionated UI API before your product knows what it needsRejected as the default: it would turn your product design decisions into our release notes
Ship readable companion sourceYou can fork, delete half of it, and keep the half that matches your productNo upgrade path; you own your fork and its maintenanceChosen: integration layers are product decisions, not dependencies
Keep everything closedLeast support surfaceEvery integrator rewrites the same lifecycle plumbingRejected: too much duplicated effort across teams

In short: a versioned package would force us to promise API stability for interface code that most teams immediately customize. Source lets you treat the layer as a starting point. The cost is honest and worth stating — you own your fork, and merging our later changes back is your merge, not ours.

The snippet below is schematic. Component names, method names, and signatures are illustrative and are not the shipped API surface.

// Illustrative: a React wrapper around the engine-owned preview surface.
// Everything except the canvas itself is ordinary React.
export function PreviewSurface({ projectId }: { projectId: string }) {
  const canvasRef = useRef<HTMLCanvasElement | null>(null);
  const sessionRef = useRef<PreviewSession | null>(null); // placeholder name
  const [status, setStatus] = useState<"idle" | "ready" | "error">("idle");

  useEffect(() => {
    let cancelled = false;
    const canvas = canvasRef.current;
    if (!canvas) return;

    createPreviewSession({ projectId, canvas }) // placeholder name
      .then((session) => {
        // Remounted before the promise resolved: release instead of adopting.
        if (cancelled) { session.close(); return; }
        sessionRef.current = session;
        setStatus("ready");
      })
      .catch(() => setStatus("error"));

    return () => {
      cancelled = true;
      sessionRef.current?.close();   // GPU resources outlive React's tree
      sessionRef.current = null;
    };
  }, [projectId]);

  return (
    <div className="preview-frame">
      <canvas ref={canvasRef} />
      {status === "ready" && <TransportBar session={sessionRef} />}
      {status === "error" && <PreviewFallback />}
    </div>
  );
}

The pattern worth copying is not the component but the discipline around it: resources are released in cleanup, a stale async result is discarded rather than adopted, and the engine-owned element has exactly one owner in the tree.

4. Discipline for re-renders and effects

React will happily rebuild a component frequently; the session underneath will not appreciate being re-initialized each time. Three rules kept the two in agreement.

Keep the session out of render state. It is a mutable resource with a lifetime, so it belongs in a ref, and changes to it should not trigger a rebuild by themselves. Project events into a snapshot that can be diffed cheaply, and let the renderer decide what that implies.

Put side effects behind one typed interface rather than scattering await calls through handlers. A command queue gives you cancel semantics during scrubbing and a single place to attach error handling, which matters once a fast foreground gesture can outrun a slow engine call.

Treat the timeline, not the DOM, as the source of truth for what is on screen. Positioning trim handles by pixel arithmetic from DOM nodes works until a zoom level changes, and then everything silently drifts.

5. What you can reasonably fork — and what you should not

Read the companion source for three things: lifecycle handling, the boundary between view state and engine state, and the command patterns for common edits. Those transfer to almost any architecture.

Rewrite rather than adopt anything that encodes product decisions: panel layouts, the design system, keyboard maps, and particularly the business rules about what a user may do to another user’s project. Those are yours, and shipping someone else’s opinion about them means fighting it for years.

Do not attempt to move rendering into JavaScript because the source makes it look approachable. The browser module owns decode, effects, audio mixing, and encode; replacing that pipeline means owning codec drift and GPU quirks permanently. The example web editor in the companion source is based on the same broadcast-grade NLE core used by the native modules — that shared core is exactly why the integration layer can stay thin.

6. Where collaboration happens

Integration is not a download-and-go exercise in this category, which shapes how support is offered alongside the source. Work is scoped 1V1: which modules you need, which targets you ship to, whether exports stay on the client or move to render nodes, and whether your deployment needs to stay inside your own network. Optional modules such as AI one-click video creation, portrait segmentation, and AI Virtual Anchor (Avatar) are adopted independently, so nothing in the React layer assumes them.

That is also why the layer is delivered as source rather than as a boundary-less framework: the conversation about architecture happens while you read it, not after you fight it.

FAQ

Is the SDK engine itself published as open source?
No. The core is a commercial product. What ships alongside it is companion DEMO source code for the integration layer — readable, modifiable, and structured to be productized — while rendering, audio mixing, and encode remain inside the SDK. You receive that layer as source you can edit in your own repository, not as a package installed from a registry.

How do we get the companion source?
Through an evaluation or trial, along with the module set you are assessing. It arrives as project source you can open in your own repository rather than as an installed dependency.

Why isn’t it distributed as an installable library?
Packaging would lock an interface API we would then owe stability for, and most teams replace much of it anyway. Source lets you keep what fits and delete what does not, at the cost of owning your fork.

Can we replace the entire interface and keep only the session plumbing?
Yes, and that is the common path over time. Nothing above the command interface is load-bearing: keep lifecycle and state projection, rebuild the panels as your own product requires.

Does the browser module support less than the native ones?
The same core serves all targets, so the difference you observe comes from the host environment — available decoders, compositing overhead, and device memory — rather than from features being withheld in the web build.

Ready to see it in your own stack? Request a free trial or talk to our integration team — we’ll help you scope the modules you actually need.