> For the complete documentation index, see [llms.txt](https://channyeintun.gitbook.io/blogs/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://channyeintun.gitbook.io/blogs/engineering/i-built-an-interactive-code-recording-engine-with-xstate-instead-of-recording-videos.md).

# I Built an Interactive Code Recording Engine with XState Instead of Recording Videos

Next Editor records editor frames, workspace events, and runtime state so coding lessons can be replayed as interactive sessions instead of flat videos.

Most coding lessons are still published as videos.

That format is easy to ship, but it is a bad medium for learning code. A viewer can watch me type, but they cannot inspect the workspace at minute 4, change a file, or replay only the part where the preview broke. Video preserves pixels. It does not preserve the actual editor state.

I wanted something closer to a real coding session.

Scrimba's scrim format was a direct inspiration for me here. It showed that coding lessons do not have to be passive videos, and that idea pushed me to build my own take on interactive code recording.

That idea became Next Editor: a browser-based lesson editor that records Monaco editor frames, preview state, slides, workspace changes, and runtime events, then replays them as an interactive lesson instead of a screencast.

The part that made the whole thing manageable was XState.

### The Real Problem Was Coordination, Not Recording

At first glance, recording an editor looks simple:

* listen for text changes
* save snapshots
* replay them later

That falls apart quickly once the product behaves like a real workspace.

I was not just capturing text. I needed to coordinate:

* editor content, selection, cursor position, and scroll state
* mouse position for playback cursor visualization
* optional audio recording and playback
* preview panel state
* slide state and slide events
* multi-file workspace changes
* runtime events from the in-browser dev environment
* pause-on-user-interaction rules during playback

That is not a "few hooks and booleans" problem. It is a mode-and-lifecycle problem.

So I modeled the recorder as a state machine.

### Why XState Fit Better Than More React State

The core machine in Next Editor has a small set of explicit states:

* `idle`
* `startingRecording`
* `recording`
* `stoppingRecording`
* `loading`
* `playback.ready`
* `playback.playing`
* `playback.paused`
* `playback.ended`

That gave me a few things React state alone was not giving me cleanly:

* explicit legal transitions
* isolated side effects on state entry and exit
* actor boundaries for separate concerns
* fewer impossible combinations like "playing but not loaded"

Instead of sprinkling recording logic across components, the app sends events such as `START_RECORDING`, `CAPTURE_FRAME`, `PLAY`, `PAUSE`, and `SEEK` into one machine.

The machine then coordinates a few child actors:

* a `timeline` actor that advances playback time with `requestAnimationFrame`
* an `audioRecording` actor for microphone capture
* an `audioPlayback` actor for synchronized audio replay
* a `mouseTracking` actor that also watches iframe content when possible

That actor split mattered. The timeline should care about time. Audio should care about media lifecycle. Mouse tracking should care about pointer sources. The editor machine sits above them and decides how those pieces affect the overall recording session.

### What I Actually Record

The important design decision was this: I am not recording video frames.

I am recording restorable editor and workspace state.

Each captured frame can include:

* full editor content
* selection and cursor position
* Monaco view state
* mouse cursor position
* slide preview state
* current slide index
* preview state

On top of the frame stream, the recorder also stores timed event streams for:

* slides
* preview interactions
* workspace snapshots and file operations
* runtime snapshots and runtime events

That distinction is what makes the playback interactive. During replay, Next Editor can restore the active file, preview content, workspace state, and runtime state from the recording itself. It is not pretending to be a video player with an editor skin.

### Recording Starts With Full Frames, But Storage Does Not

During capture, storing only diffs would have complicated the hot path too early. I found it simpler to capture full editor frames first and compress them when the recording is finalized.

That gave me a cleaner pipeline:

1. Capture complete frames while recording.
2. Finalize the session.
3. Compress the frame list into a keyframe-plus-delta format.
4. Load the result back into playback mode.

The storage format uses a keyframe every 120 frames and delta frames in between.

Why 120?

Because it is a practical balance. Frequent keyframes make seeking cheap. Delta frames keep file size under control. In my docs, this format reduced storage by roughly 70 to 90 percent compared to full snapshots while keeping replay precise.

The content delta itself is intentionally simple:

```ts
interface ContentDelta {
  prefixLen: number;
  suffixLen: number;
  insert: string;
}
```

Instead of storing a whole new document for every frame, I store:

* the unchanged prefix length
* the unchanged suffix length
* the inserted middle section

Cursor and selection changes are also stored as relative deltas where possible, because most edits move locally.

### I Used WebAssembly for the Diff Hot Path

String diffing happens a lot when you are compressing a long recording.

To keep that predictable, the project uses AssemblyScript compiled to WebAssembly for common-prefix and common-suffix detection. The Wasm path works on encoded byte arrays, which made it a good fit for repeated diff work.

There was one non-obvious edge case here: UTF-8 boundaries.

If a prefix or suffix calculation lands in the middle of a multibyte character, you can corrupt the reconstructed string. So the implementation explicitly backs up to valid character boundaries before decoding. That was one of those details that seemed minor until I imagined someone recording code with emoji, non-Latin text, or other multibyte input in comments and strings.

Small detail, big reliability win.

### Playback Is Also a Coordinated System

Playback is where XState paid off even more.

The timeline actor advances current time using `requestAnimationFrame`. On each tick, the editor machine:

* computes the correct frame for the current time
* reconstructs editor state from the nearest keyframe plus subsequent deltas
* applies content, cursor, selection, and view state to Monaco
* replays preview, slide, workspace, and runtime events up to that point
* keeps audio synchronized

Seeking works the same way. The player finds the nearest previous keyframe, rebuilds forward, and applies the result. Because the keyframe interval is fixed, seeking stays bounded even for long recordings.

I also added a behavior that matters a lot in practice: playback pauses on meaningful user interaction. If someone starts typing, pastes, or otherwise takes control, the system should stop acting like a movie and hand control back to the user. That keeps the replay experience from fighting the person using it.

Another subtle playback detail was duration. If audio exists, I use the audio blob's actual duration as the source of truth when loading a recording. That prevents trailing silence caused by ordinary wall-clock recording overhead.

### The Jump From "Editor Replay" to "Workspace Replay"

The most important architectural shift was moving beyond a single-file editor model.

In the newer recording format, Next Editor can also store:

* workspace snapshots
* runtime snapshots
* file and folder changes
* runtime output and environment events

That changed the product from "record text edits" into "record a coding session."

This matters because real lessons are not just typing. They include switching files, refreshing previews, running commands, and reacting to output. A useful teaching tool has to preserve those moments too.

That is also why I am much more interested in recording state than recording pixels. Pixels only show that something happened. State lets the learner step back into that moment.

### A Few Things I Learned Building It

1. If your product clearly has modes, model the modes directly.

Recording, loading, playing, paused, and ended are not UI details. They are the product. Once I treated them that way, the logic got simpler.

2. Keep capture format and storage format separate.

Capturing full frames first made the live path easier to reason about. Compression belongs in the finalize step, not in every keystroke handler.

3. Interactive replay is mostly a synchronization problem.

The hard part is not showing text. The hard part is keeping editor state, preview state, runtime state, slide state, and audio moving together.

4. Keyframes are not an optimization detail. They are a product feature.

Users expect seeking to feel instant. That requirement should shape the format from day one.

5. State machines become more valuable as asynchronous surfaces multiply.

Audio, timers, iframes, editor state, and runtime events all have separate failure modes. XState gave me a single place to express how those pieces are allowed to interact.

### Why I Think This Format Is Better Than Video For Code Lessons

If I publish a video tutorial, the learner watches me work.

If I publish an interactive recording, the learner can inspect the code, scrub the lesson meaningfully, see the workspace evolve, and understand the sequence of changes that led to the result.

That is a much closer match to how developers actually learn.

I built Next Editor because I wanted coding lessons to behave like software, not like screen capture.

If you are building anything with overlapping async flows, mode-heavy UI, or coordinated playback behavior, I would seriously consider modeling it with XState early. It let me spend less time debugging impossible states and more time improving the actual recording format.

The project is MIT licensed, and I am still iterating on the lesson format. If you have built something similar, I would love to compare approaches, especially around replay fidelity, compression strategy, and authoring workflow.

{% embed url="<https://nexteditor.dev/>" %}
