Open-source project
pixel-point/aval avatar
pixel-point/aval

pixel-point/aval: a state-machine video format for prerendered motion on the web

A new open-source format for interactive video on the web, with a built-in state machine, frame-accurate transitions, and packed-alpha transparency.

1,391 stars80 forksTypeScriptMIT

At a glance

What is it?
AVAL packages one logical animation as per-codec .avl bundles with an authored state graph, frame-accurate transitions and packed alpha. It is aimed at teams that want loopable motion with named states rather than a plain video tag.
Who is it for?
Adopt AVAL if you need named states, bounded transitions and packed alpha in short prerendered motion, and you already control an FFmpeg build with libx264, libx265, libvpx-vp9 and libaom-av1. Do not adopt it for long-form video, for audio-led content, or if you cannot own an unsupported-browser path, because AVAL does not create or reveal fallback content by design.
Can I use it commercially?
Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
Is it still maintained?
Yes. The repository last received commits 45 days ago.
What is it written in?
Mainly TypeScript, according to GitHub's language statistics.

Answers come from the project's GitHub data, last synced on September 17, 2026, and from our analysis. They are not legal advice.

DEEP OPEN-SOURCE ANALYSIS

The gap AVAL targets: motion that has to answer to application state

A looping product animation usually ends up as a video file plus a pile of JavaScript that pauses it, seeks it, or swaps it for another file when the user hovers, submits, or fails. Seeking is the weak point: a seek is not guaranteed to land on the frame the designer authored, and swapping files means a decode hitch at exactly the moment the interface is supposed to feel immediate.

AVAL attacks that by moving the timeline into a declared graph. A project names states such as idle and success, binds each state to a body unit that is a frame range of a source, and declares edges between them. The README's minimal example compiles frames 0 to 120 of a 30 fps source as one looping unit and gives it a single state. Applications then call setState, which the README says works without media seeking.

The audience is narrow on purpose. This is for short prerendered motion: interface feedback, product loops, character-like reactions. It is not a general video player, and the README never presents it as one.

How the codec ladder and packed alpha actually work

One logical animation is published as a codec bundle. Each codec gets its own AVAL wire 1.1 file, and the browser picks the first candidate in the fixed AV1, then VP9, then H.265, then H.264 ladder that decodes and passes what the README calls pre-readiness output qualification. That order is policy, not markup order: the README states plainly that DOM source order does not change it. The state graph and authored timing are identical in every file, so the choice of codec is a delivery decision and never a behavioural one.

The authoring side is codec-major. A project has an encodings array, and each entry owns its own rendition ladder and constant-quality CRF settings. The controls differ per codec rather than being flattened into one shared set: H.264 and H.265 expose compression presets, VP9 exposes deadline and cpuUsed, AV1 exposes bitDepth, cpuUsed, tiles, rowMt and threads. Slower modes such as veryslow, VP9 best and AV1 cpuUsed: 0 are supported, which tells you the compiler is designed for offline builds rather than on-the-fly encoding.

Transparency is the other half of the pitch. The README describes packed transparency as part of the format, and the project example sets alpha to auto. That is the reason a designer can composite motion over a live interface instead of over a baked-in background.

Installing the compiler and producing your first bundle

There is no initialization step and no local compiler installation. The README says npx fetches the scoped compiler into npm's cache and runs its sole avl executable, without adding the compiler to the current project. You need a motion.json beside your source media. The README's minimal example expects motion.mov to be progressive, square-pixel, unrotated 1920x1080 at a constant 30 fps with at least 120 frames:

json
{
  "projectVersion": "1.0",
  "alpha": "auto",
  "canvas": { "width": 1920, "height": 1080, "fit": "contain", "pixelAspect": [1, 1], "colorSpace": "srgb" },
  "frameRate": { "numerator": 30, "denominator": 1 },
  "sources": [{ "id": "motion", "type": "video", "path": "motion.mov", "timing": { "mode": "exact" } }],
  "encodings": [{ "codec": "vp9", "deadline": "best", "cpuUsed": 0, "threads": 8,
    "renditions": [{ "id": "motion.1x", "width": 1920, "height": "auto", "crf": 40 }] }],
  "units": [{ "id": "idle.body", "kind": "body", "source": "motion", "range": [0, 120], "playback": "loop", "ports": [] }],
  "initialState": "idle",
  "states": [{ "id": "idle", "bodyUnit": "idle.body" }],
  "edges": [],
  "bindings": []
}

Compile it directly from the command line:

sh
npx @pixel-point/aval-compiler compile motion.json --out dist/motion

The compiler publishes a directory rather than a single output file. The README says this example produces dist/motion/vp9.avl and dist/motion/build.json. The compiler uses caller-installed FFmpeg and FFprobe with the requested libx264, libx265, libvpx-vp9 and libaom-av1 encoders, and it bundles and downloads no native codec tool, so those binaries have to be on the machine before the command is useful. Encoding has no default wall-clock media timeout; builds that need a deadline opt in with --media-timeout-ms.

Mounting aval-player and owning the failure path

Browser integration uses literal direct-child source elements, each with one required lowercase data-codec family. Preference is derived from that attribute. The host element does not carry src, and data-codec accepts exactly av1, vp9, h265 or h264, with each family appearing at most once. Missing, unknown or duplicate declarations are invalid configuration rather than something the runtime repairs.

html
<aval-player id="motion" width="320" height="320">
  <source
    src="/motion/av1.avl"
    data-codec="av1"
  >
  <source
    src="/motion/vp9.avl"
    data-codec="vp9"
  >
  <source
    src="/motion/h265.avl"
    data-codec="h265"
  >
  <source
    src="/motion/h264.avl"
    data-codec="h264"
  >
</aval-player>

<script type="module" src="/motion.js"></script>

The required application boundary is the part most integrators will get wrong. The README is explicit that AVAL does not create, select, reveal or hide fallback content, and that the element's direct error listener should be installed before explicit registration so an upgrade-time failure cannot outrun the application boundary. Branch on failure.code, not on the message text:

js
import {
  AvalPlaybackError,
  defineAvalElement
} from "@pixel-point/aval-element";

const motion = document.querySelector("#motion");
motion.addEventListener("error", (event) => {
  if (event.detail.fatal) {
    console.error("AVAL playback unavailable", event.detail.failure);
  }
});
defineAvalElement();

try {
  await motion.prepare();
} catch (error) {
  if (!(error instanceof AvalPlaybackError)) throw error;
}

Once prepared, switching state is a single call, and the README states it needs no media seeking:

js
const motion = document.querySelector("aval-player");
await motion?.setState("success");

For React 18.3 and 19 there is @pixel-point/aval-react with a useAval() hook described as SSR-safe, and @pixel-point/aval-svelte ships a Svelte 5 component plus a read-only reactive controller store. The element package is the canonical browser runtime and is described as SSR-safe as well.

Where AVAL stops being the right tool

The failure modes are stated rather than hidden, which is unusual and worth taking literally. A browser may lack the required WebCodecs interfaces, every authored codec may be unsupported, or another terminal playback failure may stop the source generation. In those cases prepare() rejects with AvalPlaybackError and the element raises one fatal error event with failure.code set to a value such as unsupported-profile or unsupported-browser. There is no built-in poster, no automatic image fallback, no text fallback. If your product cannot render its own substitute, AVAL will leave a hole where the animation was.

Duration is the second boundary. The README frames the format around short prerendered motion, and the whole model of frame ranges, units and bounded transitions assumes a compact authored clip. Nothing in it suggests long-form playback, scrubbing a timeline, or audio-led content; there is no mention of audio handling at all.

The third cost is build-time. The compiler relies on caller-installed FFmpeg and FFprobe with specific encoders, and the README states that codec, patent, source-media and distribution obligations remain the publisher's responsibility. Choosing AV1 or H.265 in your ladder is a licensing and compatibility decision you make, not one the tool makes for you.

How AVAL differs from Rive and from a plain video element

The obvious comparison is Rive, and the README invites it by describing @pixel-point/aval-react as an SSR-safe Rive-like useAval() integration. The difference is where the pixels come from. Rive renders vector art from a runtime document, so the runtime owns the drawing and the file stays small and resolution-independent. AVAL encodes prerendered frames with a real video codec and ships per-codec .avl files, so what you get is authored raster output with packed alpha, and the fidelity is whatever the encoder produced. You cannot restyle an AVAL clip in the browser the way you can restyle a Rive state machine, but you also do not pay for a vector renderer on the main thread.

The second comparison is the plain video element. A video element gives you play, pause and seek, and the application is left to infer state from the timeline. AVAL gives you named states, authored triggers, bounded transitions and reversals as first-class concepts in the format, with the state graph duplicated identically in every codec file. That is the actual trade: you take on a compiler, a bundle directory and a codec ladder in exchange for transitions that are declared rather than improvised with seeks.

Licence, maintenance and what an upgrade actually costs

The repository is MIT licensed, which covers the code in packages, schemas and examples. It does not cover the codecs. The README is direct that codec, patent, source-media and distribution obligations remain the publisher's responsibility, and the compiler bundles and downloads no native codec tool. Shipping an AV1 or H.265 rendition is a separate question from shipping the MIT-licensed runtime, and the repository carries THIRD_PARTY_NOTICES.md and SECURITY.md at the top level for that reason. This is not legal advice; read those files before you publish a bundle.

On maintenance, the last push to the default branch was on 2026-08-04. The repository is not archived. There are no retrieved releases, so versioning and changelog practice cannot be judged from the repository layout here.

The upgrade cost is structural rather than incidental. The format has a wire version, currently 1.1, parsed and validated by @pixel-point/aval-format, and a project format version, 1.0, handled by the compiler. A project that pins motion.json to projectVersion 1.0 and ships .avl files to browsers has two version surfaces to track, and the compiler records per-file hashes and exact MIME codec strings in build.json, which is the artefact you would diff when rebuilding. Because the compiler is invoked through npx rather than installed, a rebuild can pick up a newer compiler than the one that produced the bundle currently in production unless you pin it.

Editorial conclusion

Adopt AVAL if you need named states, bounded transitions and packed alpha in short prerendered motion, and you already control an FFmpeg build with libx264, libx265, libvpx-vp9 and libaom-av1. Do not adopt it for long-form video, for audio-led content, or if you cannot own an unsupported-browser path, because AVAL does not create or reveal fallback content by design. Before committing, verify that your target browsers expose the WebCodecs interfaces AVAL needs and that at least one of av1, vp9, h265 or h264 decodes in each of them, by compiling motion.json and checking the error detail from prepare().

Frequently asked questions

What is a pixel video in pixel-point/aval?

AVAL does not use that term. It describes a web format and runtime for short prerendered motion, where one logical animation is published as per-codec AVAL wire 1.1 files and the browser selects the first candidate in the AV1, VP9, H.265, H.264 ladder that decodes.

How do I install pixel-point/aval?

You do not install the compiler. The README says npx fetches @pixel-point/aval-compiler into npm's cache and runs its sole avl executable without adding it to the current project. The compiler does require caller-installed FFmpeg and FFprobe with the requested libx264, libx265, libvpx-vp9 and libaom-av1 encoders.

What happens in pixel-point/aval when the browser supports none of the codecs?

prepare() rejects with AvalPlaybackError and the element raises one fatal error event with failure.code set to a value such as unsupported-profile or unsupported-browser. AVAL deliberately does not create, select, reveal or hide fallback content, so the application has to decide what to show instead.

Can I switch between states in pixel-point/aval without seeking the video?

Yes. The README states that applications can select any authored state without media seeking, using a call such as await motion.setState("success"). The state graph and authored timing are identical in every codec file of a bundle.

Official sources

  1. Issues
  2. License: MIT
  3. pixel-point/aval on GitHub
  4. Project website
  5. README
Community notes

Community notes