av
The av namespace — 22 functions.
globals/av/is_live
av.is_live() -> boolean
True if a live-stream session is currently active.
Returns boolean — Whether the live encoder is running.
if av.is_live() then av.stop_live() end
globals/av/is_recording
av.is_recording() -> boolean
True if a recording session is currently active.
Returns boolean — Whether a recording is in progress.
print("recording:", av.is_recording())
globals/av/live
av.live(opts: LiveOpts?) -> string?
Start the live-stream encoder. The stream is served at
/engine/live.stream and reverse-proxied at
/stream/<instance>/live.stream as a binary length-prefixed
protocol consumed by the multiviewer UI's WebCodecs decoder.
When texture_handle is set, the encoder reads from that GPU
texture's guid (a Camera pointed at it via setTargetTexture)
instead of the scene's viewport — that's how spectator cameras
work. Returns a stream URL, or nil when unsupported or a
session is already active.
Parameters
optsLiveOpts(optional) — Encoder options.
Returns string? — Stream URL or nil.
local url = av.live({ width = 1280, height = 720, fps = 60 })
globals/av/record
av.record(path: string, opts: RecordOpts?) -> (string?, string?)
Start recording the engine output to a VFS path. Default dir
is /zero/runtime/recordings/ when path is not absolute. The
take runs until av.stop_recording() unless opts bounds it
with max_duration_sec (seconds of the take's own clock) or
frames (captured frames); max_duration_sec wins when both are
given, and the bound in force reads back as
av.status().recordingBound. With
no chroma/range opts the format defaults to full-range 4:4:4
HEVC where the GPU supports it, else 4:2:0. On the "software"
backend the take is H.264 encoded on the CPU, which costs the run
it records: read av.status().recordingAchievedFps against
recordingRequestedFps to see the rate it reached. What the take
did with the master mix reads back as
av.status().recordingAudio. cadence picks the clock the take
stamps its frames from. "realtime" (the default) stamps each
frame with the wall-clock slot it was captured in, so a recorded
session is watched back at the speed it happened and an engine
ticking under fps leaves slots empty. "frame" stamps every
rendered frame one fixed slot after the last, so a timeline whose
own clock advances a step per rendered frame — a cutscene, a
scripted demo, anything on a fixed timestep — is delivered at the
length that timeline runs to, however slowly the engine drew it: a
take of frames = n at fps is n / fps seconds of film, and a
max_duration_sec bound counts that film's seconds. A "frame"
take records silent, because the master mix plays in wall-clock
seconds and cannot share a file with a fixed-step picture;
recordingAudio says so, and an explicit audio = true beside it
is refused. Record the sound as a second "realtime" take.
camera names the camera the take draws its film from — an entity
proxy, an entity id, or an entity name. That camera holds the viewport
for as long as the take runs, above the priority contest and above
camera.setEditorOverride, so the film is its view and the frames
carry everything the presented frame carries. Only frames that camera
drew go into the film, and a camera that never draws the viewport ends
the take with the reason on av.status().recordingError — so a take is
the view it named or it is no take. The camera belongs to the take:
nothing is written to it, and the viewport is back under its own
contest the moment the take ends. It reads back as
av.status().recordingCamera while the take runs. Omitted, the take
records whichever camera holds the viewport, which in an engine on the
editor profile is the editor's own fly camera rather than the scene's.
renderLayers is the render-layer include spec the viewport is
drawn under for as long as the take runs — the same token string a
capture takes: all seeds every layer, name adds one and !name
drops one, so "all !EditorUI !debug" films the scene without the
editor's chrome or the authoring overlays (gizmos, light and probe
icons, frustums, collider wireframes) over it, and
"all !ui !EditorUI !debug" drops the authored HUD as well. The
viewport admits geometry and screens by that one spec, so it states
the whole picture. It belongs to the take: nothing is written to the
camera it is stated against, and the moment the take ends — its
bound reached, stopped, or refused — the viewport is back under the
camera's own spec. While a take states layers, the window shows what
the film holds, and av.status().recordingLayers reads the spec
back. Omitted, the take records the engine output as presented.
Returns the destination path of a session that is open and
recording, or nil
and the reason it is not — an adapter that cannot encode, a take
already running, an option the encoder rejects, a resolution the
device refuses. The engine opens the session, so the call waits
for it: run it where it can yield, wrapping it in task.spawn
from a callback that cannot. How a take finished reads back as
av.status().recordingEnd; a refused request puts its reason
there and on recordingError and leaves no take report behind,
while a request refused because a take is already running leaves
that take's report as it is.
Parameters
pathstring— VFS destination path.optsRecordOpts(optional) — Encoder options (optional).
Returns (string?, string?) — Destination VFS path of the open recording, or nil. Why the recording was refused, when it was.
local clip = av.record("intro.mp4", { fps = 60 })
local film = av.record("cut.mp4", { fps = 24, frames = 24 * 181, cadence = "frame" })
local clean = av.record("take.mp4", { fps = 24, renderLayers = "all !EditorUI !debug" })
local shot = av.record("film.mp4", { fps = 24, frames = 240, camera = "FilmCamera" })
globals/av/status
av.status() -> AvStatus
Report the encoder subsystem's state. Always available
regardless of GPU support. backend is the encode backend in use
— "vulkan" or "vaapi" on an adapter with a media engine,
"software" where encode runs on the CPU — and hardware is true
for the first two, so a caller that pays for the take in engine
time knows which it is getting. codecs lists what the backend
encodes with the recording default first. live is true while the
av.live stream is running. A take of its own reads back on the
recording fields: recording is the destination of the take in
flight, recordingBound what will end it, recordingEnd how the
most recent one ended, and recordingError why one produced no
file. recordingAudio is the codec the take is writing the master
mix with ("opus"), or the reason the file carries no audio track
— read it to tell a film with a soundtrack from a silent one.
recordingLayers is the render-layer include spec the armed take is
drawing the viewport under, in the words its caller wrote, and nil for
a take that stated none — the reading that answers what is in the
picture rather than how much of it there is. recordingCamera is the
entity id of the camera the take's most recent captured frame was drawn
from, and stands as the source of the most recent take once that take
has ended — it is read off the frame the engine drew, so it answers
which view a film holds whether or not the take named a camera.
recordingCadence is the clock the take stamps its frames from,
"realtime" or "frame", and so what the tally below is a reading
against. What the take produced reads off recordingFrames,
recordingBytes (every byte the take has produced so far, climbing
while it runs and ending equal to the size of the file),
recordingSeconds (the timeline those frames cover),
recordingAchievedFps (the rate they arrived at) and
recordingRequestedFps (the rate asked for) — live while a take
runs, and its final tally once it ends. On "realtime" the
requested rate is a ceiling and an engine ticking under it reaches
less; on "frame" every rendered frame is a slot of the recorded
timeline, so recordingSeconds is that timeline's length and
recordingAchievedFps is the rate it plays back at.
Returns AvStatus — Encoder status table.
local s = av.status(); print(s.backend, s.hardware, s.recordingAudio)
globals/av/stop_live
av.stop_live() -> boolean
Stop any active live-stream session.
Returns boolean — True if a session was stopped, false if none was active.
av.stop_live()
globals/av/stop_recording
av.stop_recording(handle: string?) -> (boolean, string?)
Stop the active recording (or the one for the given promise
handle). Returns true when a recording was armed at call time. A
false return carries a second value naming how the most recent
recording already ended — the bound it reached, or the failure
that cut it short — and nil when no recording has run at all. The
engine finalizes the take on its next tick: wait for
av.is_recording() to go false, then read what it produced off
av.status().
Parameters
handlestring(optional) — Promise handle of a specific recording (optional).
Returns (boolean, string?) — True if a recording was stopped. How the most recent recording ended, when nothing was armed.
local stopped, ended = av.stop_recording()
modules/av/README
require("@builtin/modules/api/engine/av") -- av (also available as global 'av')
Audio/video encode + mux control — live streaming, recording, encoder/muxer primitives. Public Luau surface over the __av Internal FFI namespace.
Usage: local av = require("@builtin/modules/api/engine/av") Also available as global: av
modules/av/is_live
is_live(): boolean
True if a live-stream session is currently active.
if av.is_live() then av.stop_live() end
modules/av/is_recording
is_recording(): boolean
True if a recording session is currently active.
print("recording:", av.is_recording())
modules/av/live
live(opts: LiveOpts?): string?
Start the live-stream encoder. The stream is served at
/engine/live.stream and reverse-proxied at
/stream/<instance>/live.stream as a binary length-prefixed
protocol consumed by the multiviewer UI's WebCodecs decoder.
When texture_handle is set, the encoder reads from that GPU
texture's guid (a Camera pointed at it via setTargetTexture)
instead of the scene's viewport — that's how spectator cameras
work. Returns a stream URL, or nil when unsupported or a
session is already active.
Parameters
optsLiveOpts?(optional) — Encoder options.
local url = av.live({ width = 1280, height = 720, fps = 60 })
modules/av/record
record(path: string, opts: RecordOpts?): (string?, string?)
Start recording the engine output to a VFS path. Default dir
is /zero/runtime/recordings/ when path is not absolute. The
take runs until av.stop_recording() unless opts bounds it
with max_duration_sec (seconds of the take's own clock) or
frames (captured frames); max_duration_sec wins when both are
given, and the bound in force reads back as
av.status().recordingBound. With
no chroma/range opts the format defaults to full-range 4:4:4
HEVC where the GPU supports it, else 4:2:0. On the "software"
backend the take is H.264 encoded on the CPU, which costs the run
it records: read av.status().recordingAchievedFps against
recordingRequestedFps to see the rate it reached. What the take
did with the master mix reads back as
av.status().recordingAudio. cadence picks the clock the take
stamps its frames from. "realtime" (the default) stamps each
frame with the wall-clock slot it was captured in, so a recorded
session is watched back at the speed it happened and an engine
ticking under fps leaves slots empty. "frame" stamps every
rendered frame one fixed slot after the last, so a timeline whose
own clock advances a step per rendered frame — a cutscene, a
scripted demo, anything on a fixed timestep — is delivered at the
length that timeline runs to, however slowly the engine drew it: a
take of frames = n at fps is n / fps seconds of film, and a
max_duration_sec bound counts that film's seconds. A "frame"
take records silent, because the master mix plays in wall-clock
seconds and cannot share a file with a fixed-step picture;
recordingAudio says so, and an explicit audio = true beside it
is refused. Record the sound as a second "realtime" take.
camera names the camera the take draws its film from — an entity
proxy, an entity id, or an entity name. That camera holds the viewport
for as long as the take runs, above the priority contest and above
camera.setEditorOverride, so the film is its view and the frames
carry everything the presented frame carries. Only frames that camera
drew go into the film, and a camera that never draws the viewport ends
the take with the reason on av.status().recordingError — so a take is
the view it named or it is no take. The camera belongs to the take:
nothing is written to it, and the viewport is back under its own
contest the moment the take ends. It reads back as
av.status().recordingCamera while the take runs. Omitted, the take
records whichever camera holds the viewport, which in an engine on the
editor profile is the editor's own fly camera rather than the scene's.
renderLayers is the render-layer include spec the viewport is
drawn under for as long as the take runs — the same token string a
capture takes: all seeds every layer, name adds one and !name
drops one, so "all !EditorUI !debug" films the scene without the
editor's chrome or the authoring overlays (gizmos, light and probe
icons, frustums, collider wireframes) over it, and
"all !ui !EditorUI !debug" drops the authored HUD as well. The
viewport admits geometry and screens by that one spec, so it states
the whole picture. It belongs to the take: nothing is written to the
camera it is stated against, and the moment the take ends — its
bound reached, stopped, or refused — the viewport is back under the
camera's own spec. While a take states layers, the window shows what
the film holds, and av.status().recordingLayers reads the spec
back. Omitted, the take records the engine output as presented.
Returns the destination path of a session that is open and
recording, or nil
and the reason it is not — an adapter that cannot encode, a take
already running, an option the encoder rejects, a resolution the
device refuses. The engine opens the session, so the call waits
for it: run it where it can yield, wrapping it in task.spawn
from a callback that cannot. How a take finished reads back as
av.status().recordingEnd; a refused request puts its reason
there and on recordingError and leaves no take report behind,
while a request refused because a take is already running leaves
that take's report as it is.
Parameters
pathstring— VFS destination path.optsRecordOpts?(optional) — Encoder options (optional).
local clip = av.record("intro.mp4", { fps = 60 })
local film = av.record("cut.mp4", { fps = 24, frames = 24 * 181, cadence = "frame" })
local clean = av.record("take.mp4", { fps = 24, renderLayers = "all !EditorUI !debug" })
local shot = av.record("film.mp4", { fps = 24, frames = 240, camera = "FilmCamera" })
modules/av/status
status(): AvStatus
Report the encoder subsystem's state. Always available
regardless of GPU support. backend is the encode backend in use
— "vulkan" or "vaapi" on an adapter with a media engine,
"software" where encode runs on the CPU — and hardware is true
for the first two, so a caller that pays for the take in engine
time knows which it is getting. codecs lists what the backend
encodes with the recording default first. live is true while the
av.live stream is running. A take of its own reads back on the
recording fields: recording is the destination of the take in
flight, recordingBound what will end it, recordingEnd how the
most recent one ended, and recordingError why one produced no
file. recordingAudio is the codec the take is writing the master
mix with ("opus"), or the reason the file carries no audio track
— read it to tell a film with a soundtrack from a silent one.
recordingLayers is the render-layer include spec the armed take is
drawing the viewport under, in the words its caller wrote, and nil for
a take that stated none — the reading that answers what is in the
picture rather than how much of it there is. recordingCamera is the
entity id of the camera the take's most recent captured frame was drawn
from, and stands as the source of the most recent take once that take
has ended — it is read off the frame the engine drew, so it answers
which view a film holds whether or not the take named a camera.
recordingCadence is the clock the take stamps its frames from,
"realtime" or "frame", and so what the tally below is a reading
against. What the take produced reads off recordingFrames,
recordingBytes (every byte the take has produced so far, climbing
while it runs and ending equal to the size of the file),
recordingSeconds (the timeline those frames cover),
recordingAchievedFps (the rate they arrived at) and
recordingRequestedFps (the rate asked for) — live while a take
runs, and its final tally once it ends. On "realtime" the
requested rate is a ceiling and an engine ticking under it reaches
less; on "frame" every rendered frame is a slot of the recorded
timeline, so recordingSeconds is that timeline's length and
recordingAchievedFps is the rate it plays back at.
local s = av.status(); print(s.backend, s.hardware, s.recordingAudio)
modules/av/stop_live
stop_live(): boolean
Stop any active live-stream session.
av.stop_live()
modules/av/stop_recording
stop_recording(handle: string?): (boolean, string?)
Stop the active recording (or the one for the given promise
handle). Returns true when a recording was armed at call time. A
false return carries a second value naming how the most recent
recording already ended — the bound it reached, or the failure
that cut it short — and nil when no recording has run at all. The
engine finalizes the take on its next tick: wait for
av.is_recording() to go false, then read what it produced off
av.status().
Parameters
handlestring?(optional) — Promise handle of a specific recording (optional).
local stopped, ended = av.stop_recording()
typed/builtin//modules/api/engine/av/av/is_live
av.is_live() -> boolean
True if a live-stream session is currently active.
Returns boolean — Whether the live encoder is running.
if av.is_live() then av.stop_live() end
typed/builtin//modules/api/engine/av/av/is_recording
av.is_recording() -> boolean
True if a recording session is currently active.
Returns boolean — Whether a recording is in progress.
print("recording:", av.is_recording())
typed/builtin//modules/api/engine/av/av/live
av.live(opts: LiveOpts?) -> string?
Start the live-stream encoder. The stream is served at
/engine/live.stream and reverse-proxied at
/stream/<instance>/live.stream as a binary length-prefixed
protocol consumed by the multiviewer UI's WebCodecs decoder.
When texture_handle is set, the encoder reads from that GPU
texture's guid (a Camera pointed at it via setTargetTexture)
instead of the scene's viewport — that's how spectator cameras
work. Returns a stream URL, or nil when unsupported or a
session is already active.
Parameters
optsLiveOpts(optional) — Encoder options.
Returns string? — Stream URL or nil.
local url = av.live({ width = 1280, height = 720, fps = 60 })
typed/builtin//modules/api/engine/av/av/record
av.record(path: string, opts: RecordOpts?) -> (string?, string?)
Start recording the engine output to a VFS path. Default dir
is /zero/runtime/recordings/ when path is not absolute. The
take runs until av.stop_recording() unless opts bounds it
with max_duration_sec (seconds of the take's own clock) or
frames (captured frames); max_duration_sec wins when both are
given, and the bound in force reads back as
av.status().recordingBound. With
no chroma/range opts the format defaults to full-range 4:4:4
HEVC where the GPU supports it, else 4:2:0. On the "software"
backend the take is H.264 encoded on the CPU, which costs the run
it records: read av.status().recordingAchievedFps against
recordingRequestedFps to see the rate it reached. What the take
did with the master mix reads back as
av.status().recordingAudio. cadence picks the clock the take
stamps its frames from. "realtime" (the default) stamps each
frame with the wall-clock slot it was captured in, so a recorded
session is watched back at the speed it happened and an engine
ticking under fps leaves slots empty. "frame" stamps every
rendered frame one fixed slot after the last, so a timeline whose
own clock advances a step per rendered frame — a cutscene, a
scripted demo, anything on a fixed timestep — is delivered at the
length that timeline runs to, however slowly the engine drew it: a
take of frames = n at fps is n / fps seconds of film, and a
max_duration_sec bound counts that film's seconds. A "frame"
take records silent, because the master mix plays in wall-clock
seconds and cannot share a file with a fixed-step picture;
recordingAudio says so, and an explicit audio = true beside it
is refused. Record the sound as a second "realtime" take.
camera names the camera the take draws its film from — an entity
proxy, an entity id, or an entity name. That camera holds the viewport
for as long as the take runs, above the priority contest and above
camera.setEditorOverride, so the film is its view and the frames
carry everything the presented frame carries. Only frames that camera
drew go into the film, and a camera that never draws the viewport ends
the take with the reason on av.status().recordingError — so a take is
the view it named or it is no take. The camera belongs to the take:
nothing is written to it, and the viewport is back under its own
contest the moment the take ends. It reads back as
av.status().recordingCamera while the take runs. Omitted, the take
records whichever camera holds the viewport, which in an engine on the
editor profile is the editor's own fly camera rather than the scene's.
renderLayers is the render-layer include spec the viewport is
drawn under for as long as the take runs — the same token string a
capture takes: all seeds every layer, name adds one and !name
drops one, so "all !EditorUI !debug" films the scene without the
editor's chrome or the authoring overlays (gizmos, light and probe
icons, frustums, collider wireframes) over it, and
"all !ui !EditorUI !debug" drops the authored HUD as well. The
viewport admits geometry and screens by that one spec, so it states
the whole picture. It belongs to the take: nothing is written to the
camera it is stated against, and the moment the take ends — its
bound reached, stopped, or refused — the viewport is back under the
camera's own spec. While a take states layers, the window shows what
the film holds, and av.status().recordingLayers reads the spec
back. Omitted, the take records the engine output as presented.
Returns the destination path of a session that is open and
recording, or nil
and the reason it is not — an adapter that cannot encode, a take
already running, an option the encoder rejects, a resolution the
device refuses. The engine opens the session, so the call waits
for it: run it where it can yield, wrapping it in task.spawn
from a callback that cannot. How a take finished reads back as
av.status().recordingEnd; a refused request puts its reason
there and on recordingError and leaves no take report behind,
while a request refused because a take is already running leaves
that take's report as it is.
Parameters
pathstring— VFS destination path.optsRecordOpts(optional) — Encoder options (optional).
Returns (string?, string?) — Destination VFS path of the open recording, or nil. Why the recording was refused, when it was.
local clip = av.record("intro.mp4", { fps = 60 })
local film = av.record("cut.mp4", { fps = 24, frames = 24 * 181, cadence = "frame" })
local clean = av.record("take.mp4", { fps = 24, renderLayers = "all !EditorUI !debug" })
local shot = av.record("film.mp4", { fps = 24, frames = 240, camera = "FilmCamera" })
typed/builtin//modules/api/engine/av/av/status
av.status() -> AvStatus
Report the encoder subsystem's state. Always available
regardless of GPU support. backend is the encode backend in use
— "vulkan" or "vaapi" on an adapter with a media engine,
"software" where encode runs on the CPU — and hardware is true
for the first two, so a caller that pays for the take in engine
time knows which it is getting. codecs lists what the backend
encodes with the recording default first. live is true while the
av.live stream is running. A take of its own reads back on the
recording fields: recording is the destination of the take in
flight, recordingBound what will end it, recordingEnd how the
most recent one ended, and recordingError why one produced no
file. recordingAudio is the codec the take is writing the master
mix with ("opus"), or the reason the file carries no audio track
— read it to tell a film with a soundtrack from a silent one.
recordingLayers is the render-layer include spec the armed take is
drawing the viewport under, in the words its caller wrote, and nil for
a take that stated none — the reading that answers what is in the
picture rather than how much of it there is. recordingCamera is the
entity id of the camera the take's most recent captured frame was drawn
from, and stands as the source of the most recent take once that take
has ended — it is read off the frame the engine drew, so it answers
which view a film holds whether or not the take named a camera.
recordingCadence is the clock the take stamps its frames from,
"realtime" or "frame", and so what the tally below is a reading
against. What the take produced reads off recordingFrames,
recordingBytes (every byte the take has produced so far, climbing
while it runs and ending equal to the size of the file),
recordingSeconds (the timeline those frames cover),
recordingAchievedFps (the rate they arrived at) and
recordingRequestedFps (the rate asked for) — live while a take
runs, and its final tally once it ends. On "realtime" the
requested rate is a ceiling and an engine ticking under it reaches
less; on "frame" every rendered frame is a slot of the recorded
timeline, so recordingSeconds is that timeline's length and
recordingAchievedFps is the rate it plays back at.
Returns AvStatus — Encoder status table.
local s = av.status(); print(s.backend, s.hardware, s.recordingAudio)
typed/builtin//modules/api/engine/av/av/stop_live
av.stop_live() -> boolean
Stop any active live-stream session.
Returns boolean — True if a session was stopped, false if none was active.
av.stop_live()
typed/builtin//modules/api/engine/av/av/stop_recording
av.stop_recording(handle: string?) -> (boolean, string?)
Stop the active recording (or the one for the given promise
handle). Returns true when a recording was armed at call time. A
false return carries a second value naming how the most recent
recording already ended — the bound it reached, or the failure
that cut it short — and nil when no recording has run at all. The
engine finalizes the take on its next tick: wait for
av.is_recording() to go false, then read what it produced off
av.status().
Parameters
handlestring(optional) — Promise handle of a specific recording (optional).
Returns (boolean, string?) — True if a recording was stopped. How the most recent recording ended, when nothing was armed.
local stopped, ended = av.stop_recording()