---
title: "Recording: filming the world and keeping the video"
description: "av turns what the engine draws into a video file: a clip a player captures of something fun, a replay, a trailer or cutscene filmed frame by frame…"
section: "Topics"
slug: "topics-recording"
canonical: "https://origozero.ai/docs/topics-recording"
updated: "2026-09-27T17:43:58.333607900+00:00"
tags: ["documentation", "guide"]
---

# Recording: filming the world and keeping the video

## The shortest thing that works

```lua
task.spawn(function()
    -- Record five seconds of what the player sees. The call waits until the
    -- encoder is open, so a path back means the take is running.
    local take, why = av.record("clip.mp4", { max_duration_sec = 5 })
    if not take then error(why) end

    -- A take is filed when it ends.
    while av.is_recording() do task.wait(0.1) end
    print(av.status().recordingEnd, av.status().recordingBytes)
end)
```

A relative name lands under `/zero/runtime/recordings/`. That folder lives as
long as the engine does: in the browser it is gone when the tab closes, which
is why a clip a player wants to keep has to be saved to their device.

In the browser a take under that folder is written into the page's private
storage frame by frame as it records, and stays there once it is filed: the
file reads, plays and saves like any other, and a long take holds none of
itself in the engine's memory while it records or after. A take filed
anywhere else (a world path under `/zero/source/`) is written into the VFS as
bytes when it ends.

## Keeping a clip: `av.save`

```lua
-- In the handler of the button the player clicks to keep the clip:
task.spawn(function()
    local ok, where, report = av.save("clip.mp4")
    if not ok then print("not saved: " .. where) end
end)
```

The person at the screen decides where the file lands:

- **In the browser**, the clip **downloads at once when the save rides the
  player's click or key press**, which is why the call belongs in a button's
  click handler. A save with no such gesture behind it (a timer, the end of a
  take, an agent's call) makes the page show the player an offer, which they
  save with a click of their own: a browser starts a download only for a person
  who just acted. `report.state` reads `"saved"` for the first and `"offered"`
  for the second.
- **In a native engine**, a save dialog opens in the player's downloads folder
  with the clip's name filled in, and the file is written where they choose
  (`report.state` is `"offered"`). An engine with no window refuses.

So a clip feature that downloads in one click has two controls: one that
records the take, and one, shown once the take has ended, whose click saves it.
Saving a take that is still recording is refused: it is filed when it ends.
`av.save` answers false and a reason for that, for a path with no file, and for
a clip too large for the browser to hold. The saved file is the take byte for
byte. `userfile.save(path)` saves any other file the engine holds the same way.

In the editor, a take is saved without any code: the Files panel's right-click
menu on the take (in `/zero/runtime/recordings/`) holds **Save to this
device**, and that click is the gesture the download rides.

## Choosing what the take holds

- `max_duration_sec` or `frames` bound the take; without either it runs until
  `av.stop_recording()`.
- `renderLayers = "all !EditorUI !debug"` films the scene without the editor's
  chrome; add `!ui` to leave the HUD out too (the viewport shows what the take
  films while it runs).
- `camera` names the camera the take is filmed from.
- `codec` picks the video codec from `av.status().codecs`; `"h264"` is the one
  every player and site opens.
- `cadence = "frame"` films a fixed-step timeline (a cutscene) at the length it
  runs to however slowly the engine draws it.

`man modules/av/record` and `man modules/av/save` hold the full reference, and
`av.status()` reports how the last take ended and what it produced.
