# Etro: a layer and effect model for video editing in the browser

> A framework-agnostic TypeScript library that composites video, audio, text and image layers in a canvas and exposes effects as JavaScript and GLSL, with a GPL licence that shapes who can ship it.

**etro-js/etro** — Typescript video-editing library for the browser

- Repository: https://github.com/etro-js/etro
- Website: https://etrojs.dev
- Stars: 1,149 · Forks: 97
- Language: TypeScript
- License: GPL-3.0
- Published: 2026-10-06 · Updated: 2026-10-06 · Language: en
- Canonical page: https://hysenlabs.com/projects/etro-js-etro

## What problem a browser video framework actually solves

Etro's pitch is that you edit video by writing TypeScript rather than by driving a timeline in a UI. The object model is small: a `Movie` owns a canvas and an ordered list of layers, each layer has a source and a start time, and effects transform the output of a layer or of the whole movie. Everything else follows from that.

The layer types cover the sources a web app actually has: video, audio, text and image. Layers are composited in order onto the canvas, so the familiar compositing rules apply, with a start time deciding when a layer appears. The README describes Etro as framework-agnostic, which is the important architectural claim: it does not render any UI, so it can sit behind React, Vue or nothing at all.

What you give up by not using a timeline UI is obvious and, for many teams, the point. There is no clip dragging, no preview scrubber and no undo stack in the library. You build those if you need them, or you skip them because the output is a generated file rather than an artefact a person edits by hand.

## From an HTML video element to a downloadable blob

The README's basic example is the whole round trip. You construct a movie bound to a canvas, add a video layer sourced from a `videoElement`, then record the result and receive a blob.

```js
import etro from 'etro'

var movie = new etro.Movie({ canvas: outputCanvas })
var layer = new etro.layer.Video({ startTime: 0, source: videoElement })  // the layer starts at 0s
movie.addLayer(layer)

movie.record({ frameRate: 24 })  // or just `play` if you don't need to save it
    .then(blob => ...)
```

Two details matter more than they look. `frameRate` is a parameter of the record call rather than a global setting, so a single app can produce several outputs at different rates. And `play` is offered as the alternative when there is no need to save, which tells you rendering is a real-time pipeline rather than a batch job: the frames are composited as the movie advances.

The blob can then be downloaded as a video file or displayed in a `video` element. That is the whole storage story, and it is the reason Etro never has to know about file formats or upload endpoints.

## Effects as GLSL with a JavaScript surface

Effects attach to a layer or a movie through `addEffect`, and the built-in ones are described as hardware accelerated. The README is clear that effects can be written in JavaScript and GLSL, which means the escape hatch and the built-in path are the same path.

The feature list names the runtime pieces: built-in hardware accelerated effects, custom effects in JavaScript and GLSL, audio manipulation through the Web Audio API, and dynamic properties. Because the filters are GLSL, they run on the GPU through the browser's own graphics stack, so the cost of a brightness change or a blur is not proportional to the pixel count on the CPU.

A documentation wart worth knowing before you copy anything: the README's own effect snippet has unbalanced parentheses in the constructor call, so it will not paste and run as printed. Small thing, but a fair signal about how much care the inline examples have had compared with the reference site, which is where the layer and effect lists actually live.

## Keyframes and functions on the same property

Most effect and layer properties can be assigned a keyframe list or a plain function, and the same property accepts either. That is the feature that turns Etro from a compositor into something you can animate programmatically.

```js
// Keyframes
layer.effects[0].brightness = new etro.KeyFrame(
  [0, -75],  // brightness == -75 at 0 seconds
  [2, +75]  // +75 at 2 seconds
)

// Function
layer.effects[0].brightness = () => 100 * Math.random() - 50
```

The keyframe form takes time-value pairs, so `[0, -75]` means minus seventy-five at zero seconds and `[2, +75]` means plus seventy-five at two. The function form is evaluated as the movie plays, which is how you wire a property to live input such as an audio level or a mouse position. Both are assigned to the same property, so the calling code does not branch on which style it is using.

## Serialization arrived in 0.14.0, Node needs a wrapper

Movie serialization and deserialization landed in release v0.14.0 on 2026-06-06. That is the feature that turns an Etro movie from an ephemeral object into something you can store: build a session, save it as JSON, and load it back to render the same output later. The feature list describes it as serializing a session to JSON and deserializing it, and the release notes credit the pull request that added it.

Using Etro outside a browser takes an extra step. The README's section on Node points at a separate wrapper repository, `etro-js/etro-node`, rather than claiming native support. That is an honest admission of where the platform boundary sits: the library assumes a canvas, Web Audio and WebGL, all of which a plain Node process does not provide.

The two 'coming soon' items in the feature list are worth reading twice: audio effects and offline rendering. Audio manipulation exists through the Web Audio API, but effect processing for it is not there yet, and rendering is realtime with offline rendering still to come. A project that needs to produce a 60 second clip in less than 60 seconds is not what this release line supports today.

## Rollup, Karma and image comparison in the test stack

The repository layout explains how the library is built and checked. `src/` holds the TypeScript, `spec/` the tests, and `examples/` contains three runnable programs: `application/`, `assets/` and `introduction/`. Rollup does the bundling and TypeScript the compiling, configured through `rollup.config.js` and `tsconfig.json`.

```
npm i
npm run build
npm start
```

That sequence is for the examples, and the README is careful to say the development server exists for convenience, since Etro itself does not require a server. Once it is running you can open a specific page, for example `http://127.0.0.1:8080/examples/introduction/hello-world1.html`.

The test stack is the more revealing part of `package.json`. Karma runs the specs in real Chrome and Firefox, Jasmine is the spec framework, and `resemblejs` is present as a pixel-comparison library, which is how you assert on rendered frames. Puppeteer appears in the development dependencies too. Husky and ship.js handle commit hooks and releases, and the presence of `AGENTS.md` in the tree suggests the project has been worked on with tooling that expects instructions in the repository. Package metadata publishes `dist/etro-cjs.js` as the entry point with TypeScript declarations from `dist/index.d.ts`, and the current version in the file is 0.14.1, matching the last release.

## GPL-3.0 is the adoption question, not a footnote

Etro is distributed under the GNU General Public License v3, stated plainly in both the README and the LICENSE file. For a library that plugs into a website's own front end, that is the constraint that decides whether you use it at all. Copyleft obligations attach to the combined work when you distribute it, so a closed-source commercial product that bundles Etro needs a decision from whoever handles licensing. This is not a judgement about the library's quality, only the practical fact that a permissive alternative would be easier to adopt here.

On the technical side the honest comparison is with two different approaches. WebCodecs is the browser's own frame-level encode and decode interface, which gives you the codec plumbing and leaves composition, timing and effects to you; Etro sits on top of that world and supplies the object model instead. Compiling FFmpeg to WebAssembly with something like ffmpeg.wasm gives you an entire filter graph and codec support in exchange for a multi-megabyte payload and the fact that filters are configured as command lines rather than subclassed in a language. Etro is the opposite bet: a small TypeScript object model with GPU filters and no CLI, which suits generated output and hurts if you need arbitrary codec control or offline batch rendering.

Where Etro is genuinely hard to replace is the extension story. Writing your own layer or effect in TypeScript and GLSL means a custom transition or a brand new filter is a subclass, not a plugin format you have to reverse engineer.

## Conclusion

Etro fits a browser product that needs programmatic video assembly rather than a downloadable editor: a notification clip generator, a render farm in a web app, a watermark pipeline. You get a layer model, hardware accelerated effects and keyframes in about ten lines of code, and the layer and effect extension points mean an in-house renderer can stay small. Two things decide it. The GPL-3.0 licence is the bigger one: shipping it inside a closed-source product is a decision your legal team has to make, not a technical footnote. The second is that offline rendering and audio effects are both still marked as coming soon, so a project that needs to render faster than realtime is building against the wrong layer of the library today. Start with the introduction example, then read the layers and effects reference on etrojs.dev before designing around it.

## FAQ

### Can Etro be used outside a browser, in Node?

Not directly. The README has a section on using Etro in Node that points at a separate wrapper repository, etro-js/etro-node. The library itself assumes browser APIs such as canvas, WebGL and the Web Audio API, so Node use goes through that wrapper.

### How do you render an Etro movie and save it as a file?

Call `movie.record({ frameRate: 24 })`, which returns a promise resolving to a blob. The README says that blob can be downloaded as a video file or displayed using a `video` element. Use `play` instead of `record` when the output does not need to be saved. Offline rendering is still listed as coming soon.

### Does Etro require React, Vue or any UI framework?

No. The README describes Etro as framework-agnostic, and the feature list says the same in its own words. The library renders to a canvas you pass in, so the surrounding user interface is whatever you already use.

### What licence does Etro use?

Etro is distributed under the GNU General Public License v3, with the licence file in the repository root and the terms restated in the README. If you plan to bundle it into a product you distribute, check what that obliges you to do.

## Sources

- [etro-js/etro on GitHub](https://github.com/etro-js/etro)
- [License: GPL-3.0](https://github.com/etro-js/etro/blob/master/LICENSE)
- [Project website](https://etrojs.dev)
- [README](https://github.com/etro-js/etro/blob/master/README.md)
- [Releases](https://github.com/etro-js/etro/releases)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/etro-js-etro
