Video, written as markup.
Describe a composition with <vi-track> and <vi-text> the way you would write HTML. A Rust compositor builds the timeline, treats seamless loops, and streams h264 while it renders — so the first frame plays before the file is finished. Or just ask for the finished mp4.
Edit the markup. Stream it.
Hit play and the composition streams back over a session socket — the first frame plays before the file is finished. The same socket reports which element the playhead is on and what is being said, on the composition clock, which is what the log below is showing. Both scenes carry a bake tag, so after the first render they freeze in the background and the next render reads them from artifacts.
<vi-composition width="1280" height="720" fps="30" background="black">
<!-- ACT I · the wide world. A vi-group is a run of things in one lane, and
it is the layer a bake freezes — so the whole act reads from one frozen
artifact on every render after the first. -->
<vi-group id="wild" bake="wild-v1">
<!-- A 2.5s trim asked to hold 7s: the picture loops, seamlessly. -->
<vi-track id="open" duration="7s">
<vi-video src="https://cdn.vidom.io/media/reel-shore.mp4"
cut="1s,3.5s" duration="fill" loop="crossfade" />
</vi-track>
<vi-transition type="fade" duration="0.8s" />
<!-- Push in on the ridge line. fit alone would only centre-crop. -->
<vi-track id="ridge" duration="4s">
<vi-video src="https://cdn.vidom.io/media/reel-ridge.mp4"
duration="fill" scale="1.35" y="-6%" />
</vi-track>
<vi-transition type="fade" duration="0.5s" />
<!-- Half speed, so 3s of source becomes 6s of timeline. -->
<vi-track id="falls" duration="6s">
<vi-video src="https://cdn.vidom.io/media/reel-falls.mp4"
cut="0s,3s" speed="0.5" duration="fill" />
</vi-track>
<vi-transition type="fade" duration="0.6s" />
<!-- Two layers as one shot: the coast, dimmed, under a title. -->
<vi-track id="title" duration="5s">
<vi-video src="https://cdn.vidom.io/media/reel-coast.mp4" duration="fill" opacity="0.75" />
<vi-text start="0.6s" duration="3.6s" class="text-5xl safe-center">VIDOM</vi-text>
<vi-text start="1.4s" duration="2.8s" class="text-lg safe-bottom">video, written as markup</vi-text>
</vi-track>
</vi-group>
<!-- Cut to black. The absence is the shot. -->
<vi-gap duration="0.9s" />
<!-- ACT II · the desk. Its own scene, its own generation. -->
<vi-group id="desk" bake="desk-v1">
<!-- Held on the last frame instead of looping: no motion to repeat. -->
<vi-track id="quiet" duration="4.5s">
<vi-video src="https://cdn.vidom.io/media/reel-desk.mp4"
cut="0s,3s" duration="fill" loop="hold" />
</vi-track>
<vi-transition type="fade" duration="0.5s" />
<!-- Reframed onto the editor pane, with a text band over it. -->
<vi-track id="terminal" duration="5s">
<vi-video src="https://cdn.vidom.io/media/reel-code.mp4"
duration="fill" scale="1.6" x="-8%" y="4%" />
<vi-text start="0.5s" duration="4s" class="text-xl cap-lower">
one POST, and the bytes come back
</vi-text>
</vi-track>
<vi-transition type="fade" duration="0.7s" />
<vi-track id="signoff" duration="4.5s">
<vi-video src="https://cdn.vidom.io/media/reel-shore.mp4" cut="2s,"
duration="fill" opacity="0.5" scale="1.15" />
<vi-text start="0.8s" duration="3s" class="text-4xl safe-center">vidom.io</vi-text>
</vi-track>
</vi-group>
<!-- The bed is its own lane, timed from the top of ACT I to the end of
ACT II — it is inside neither, so no nesting could express it, and
being outside both is what keeps it out of the bakes above. -->
<vi-track start="wild" end="desk">
<vi-audio src="https://cdn.vidom.io/media/reel-bed.m4a"
duration="fill" gain="-13db" fade-in="2s" fade-out="3s" />
</vi-track>
</vi-composition>- status
- idle
- duration
- —
- first byte
- —
- total
- —
- on air
- —
- bake
- —
Press play. Every element boundary and spoken word arrives here as a DOM event —vi-enter fires on the element entered, then bubbles.
Built for programmatic video.
The parts that work today: markup authoring, streamed output, seamless loops, and frozen scene artifacts. Sessions and live inputs are next.
Loops that don't pop
Crossfade and ping-pong wraps are treated into seamless clips before the render starts, so short sources fill long slots without a visible seam.
Streams as it composes
Chunked h264 that plays while the renderer is still working. Or ask for the finished mp4 and get one response.
Built for agents
Markup a model can write without a schema. Every block on this site copies out agent-ready.
Nothing to run
No ffmpeg build to maintain, no render box to keep warm. One POST, and the bytes come back.
Pay for frames, not seats.
Every plan is the same API — the plan sets the ceiling. Metered on rendered output minutes, so a quiet month costs what it should.
Prototype and ship a demo. Watermarked output.
Production streaming for a product that ships video.
- No watermark, 4K output
- Chunked streaming edge
- Webhooks + render queue
- Email support
Agent pipelines and platforms rendering at volume.
- Dedicated worker pool
- Regional streaming edges
- SSO + audit log
- Shared Slack channel
An output minute is one minute of finished video, counted once at the duration you asked for — not per frame, not per resolution, and not again when you re-stream a render you already paid for. Failed renders are not billed.