Log inGet started

cameraInput

Updated September 27, 2026

Physical cameras as live textures and pixel sources: list the capture devices (a webcam, a phone's front and back cameras), open one as a feed, show its live picture anywhere a texture goes (a material, a UI image, a recording), and read its pixels.

require("@builtin::modules.api.engine.cameraInput") -- cameraInput (also available as global 'cameraInput')

The cameraInput namespace: 19 functions.

globals/cameraInput/devices

cameraInput.devices() -> { CameraDevice }

Every capture device the platform offers: id is what cameraInput.open takes as device, name the label a person recognises, facing which way it points. Until camera access has been granted, id and name read as empty strings and facing as "unknown": the list is part of what the permission protects, so a first open takes the default camera or a facing, and the list names the devices after that.

Returns { CameraDevice } — Array of { id, name, facing, default }.

for _, d in ipairs(cameraInput.devices()) do print(d.name, d.facing) end

globals/cameraInput/feeds

cameraInput.feeds() -> { CameraFeedEntry }

Every open feed, with the owning context that opened it (scope, the same tag scopes.list() shows), whose end stops the feed.

Returns { CameraFeedEntry } — Array of { id, scope, state, device, texture }.

for _, f in ipairs(cameraInput.feeds()) do print(f.id, f.state, f.scope) end

globals/cameraInput/onDeviceChange

cameraInput.onDeviceChange(callback: (devices: { CameraDevice }) -> ()) -> () -> ()

Call callback with the new device list every time a camera is added or removed, or the platform names its devices after access is granted. Each listener is watched by a task of the context that registered it, so a listener a component registered ends when that component is removed, and every other listener goes on hearing changes.

Parameters

  • callback (devices: { CameraDevice }) -> () — Receives the device list, as cameraInput.devices() returns it.

Returns () -> () — A function that removes this listener.

local off = cameraInput.onDeviceChange(function(devices) print(#devices) end)

globals/cameraInput/open

cameraInput.open(opts: CameraOpenOpts?) -> (CameraFeed?, string?)

Open a camera as a feed. Returns the feed at once; it reads permissionPending while the platform asks for access, then running (or denied / failed with a reason). Wait with feed:awaitRunning().

feed.texture is a texture handle that shows the camera's current frame: put it on a surface as a session material's base_color_texture (renderer.material.create, worn with Model:applySessionMaterial) or in a slot of a material that exists (renderer.material.setTexture(key, "base_color_texture", feed.texture)), in UI as { type = "image", props = { src = feed.texture } }, and av.record films it like any other scene content.

device names a camera by the id or name cameraInput.devices() lists; facing asks for the "front" or "back" camera instead; with neither, the platform's default camera opens. width, height and fps are hints: the device delivers the nearest it can, and feed:status() reports what it delivers. A second feed on a camera already open shares it.

Returns nil and the reason for a request refused before any camera is touched: a hint outside its range, a facing other than front or back, a device the platform does not offer.

Parameters

  • opts CameraOpenOpts (optional) — { device, facing, width, height, fps }, every field optional.

Returns (CameraFeed?, string?) — The feed, or nil and the reason.

local feed = cameraInput.open({ facing = "front", width = 1280, height = 720 })
local screen = feed and renderer.material.create({ shader = "unlit", textures = { base_color_texture = feed.texture } }, "tv_screen")

globals/cameraInput/stopAll

cameraInput.stopAll() -> number

Stop every open feed and release every camera, whatever opened them.

Returns number — How many feeds stopped.

cameraInput.stopAll()

modules/cameraInput/awaitRunning

awaitRunning(self: CameraFeed, timeoutSeconds: number?): (boolean, string?)

Wait until the feed is running, and report whether it got there. Returns as soon as the feed leaves starting and permissionPending, or when timeoutSeconds have passed, whichever comes first: a permission prompt nobody answers never settles, so the wait is always bounded.

Parameters

  • self CameraFeed
  • timeoutSeconds number? (optional) — Seconds to wait at most. Defaults to 10.
local ok, why = feed:awaitRunning(15); if not ok then print(why) end

modules/cameraInput/devices

devices(): { CameraDevice }

Every capture device the platform offers: id is what cameraInput.open takes as device, name the label a person recognises, facing which way it points. Until camera access has been granted, id and name read as empty strings and facing as "unknown": the list is part of what the permission protects, so a first open takes the default camera or a facing, and the list names the devices after that.

for _, d in ipairs(cameraInput.devices()) do print(d.name, d.facing) end

modules/cameraInput/feeds

feeds(): { CameraFeedEntry }

Every open feed, with the owning context that opened it (scope, the same tag scopes.list() shows), whose end stops the feed.

for _, f in ipairs(cameraInput.feeds()) do print(f.id, f.state, f.scope) end

modules/cameraInput/onDeviceChange

onDeviceChange(callback: (devices: { CameraDevice }) -> ()): () -> ()

Call callback with the new device list every time a camera is added or removed, or the platform names its devices after access is granted. Each listener is watched by a task of the context that registered it, so a listener a component registered ends when that component is removed, and every other listener goes on hearing changes.

Parameters

  • callback (devices: { CameraDevice }) -> () — Receives the device list, as cameraInput.devices() returns it.

Returns () — A function that removes this listener.

local off = cameraInput.onDeviceChange(function(devices) print(#devices) end)

modules/cameraInput/open

open(opts: CameraOpenOpts?): (CameraFeed?, string?)

Open a camera as a feed. Returns the feed at once; it reads permissionPending while the platform asks for access, then running (or denied / failed with a reason). Wait with feed:awaitRunning().

feed.texture is a texture handle that shows the camera's current frame: put it on a surface as a session material's base_color_texture (renderer.material.create, worn with Model:applySessionMaterial) or in a slot of a material that exists (renderer.material.setTexture(key, "base_color_texture", feed.texture)), in UI as { type = "image", props = { src = feed.texture } }, and av.record films it like any other scene content.

device names a camera by the id or name cameraInput.devices() lists; facing asks for the "front" or "back" camera instead; with neither, the platform's default camera opens. width, height and fps are hints: the device delivers the nearest it can, and feed:status() reports what it delivers. A second feed on a camera already open shares it.

Returns nil and the reason for a request refused before any camera is touched: a hint outside its range, a facing other than front or back, a device the platform does not offer.

Parameters

  • opts CameraOpenOpts? (optional) — { device, facing, width, height, fps }, every field optional.
local feed = cameraInput.open({ facing = "front", width = 1280, height = 720 })
local screen = feed and renderer.material.create({ shader = "unlit", textures = { base_color_texture = feed.texture } }, "tv_screen")

modules/cameraInput/readPixels

readPixels(self: CameraFeed, rect: CameraRect?): (any, string?)

Read the pixels of the frame the feed is showing now, without stopping it: the whole frame, or the rectangle { x, y, w, h } of it in pixels from the top-left corner. The answer is a TextureCpuHandle holding RGBA8 pixels (:readPixel(x, y), :rgba(), :encodePng()); :unload() it once done. Returns nil and the reason while no frame has arrived, after the feed stopped, or for a rectangle outside the frame.

Parameters

  • self CameraFeed
  • rect CameraRect? (optional) — Optional { x, y, w, h }.
local cpu = feed:readPixels(); if cpu then print(cpu:readPixel(0, 0)); cpu:unload() end

modules/cameraInput/status

status(self: CameraFeed): CameraStatus

Where the feed stands. width, height and fps are what the device delivers, whatever the open asked for. frames advances once per new frame, so two reads that show the same count saw the same picture. rotation is the degrees clockwise the picture turns to stand upright on the display, and follows a phone's screen as it turns; mirrored says the picture, once turned, reads as the person expects only when shown mirrored, which is how a front camera is shown. reason carries the platform's own message for denied and failed, and what is being waited on for permissionPending.

Parameters

  • self CameraFeed
local s = feed:status(); print(s.state, s.width, s.height, s.frames)

modules/cameraInput/stop

stop(self: CameraFeed): boolean

Stop the feed. The camera is released once no other feed is reading it, which is what turns the platform's camera indicator off. True when the feed was open.

Parameters

  • self CameraFeed
feed:stop()

modules/cameraInput/stopAll

stopAll(): number

Stop every open feed and release every camera, whatever opened them.

cameraInput.stopAll()

typed/builtin//modules/api/engine/cameraInput/cameraInput/devices

cameraInput.devices() -> { CameraDevice }

Every capture device the platform offers: id is what cameraInput.open takes as device, name the label a person recognises, facing which way it points. Until camera access has been granted, id and name read as empty strings and facing as "unknown": the list is part of what the permission protects, so a first open takes the default camera or a facing, and the list names the devices after that.

Returns { CameraDevice } — Array of { id, name, facing, default }.

for _, d in ipairs(cameraInput.devices()) do print(d.name, d.facing) end

typed/builtin//modules/api/engine/cameraInput/cameraInput/feeds

cameraInput.feeds() -> { CameraFeedEntry }

Every open feed, with the owning context that opened it (scope, the same tag scopes.list() shows), whose end stops the feed.

Returns { CameraFeedEntry } — Array of { id, scope, state, device, texture }.

for _, f in ipairs(cameraInput.feeds()) do print(f.id, f.state, f.scope) end

typed/builtin//modules/api/engine/cameraInput/cameraInput/onDeviceChange

cameraInput.onDeviceChange(callback: (devices: { CameraDevice }) -> ()) -> () -> ()

Call callback with the new device list every time a camera is added or removed, or the platform names its devices after access is granted. Each listener is watched by a task of the context that registered it, so a listener a component registered ends when that component is removed, and every other listener goes on hearing changes.

Parameters

  • callback (devices: { CameraDevice }) -> () — Receives the device list, as cameraInput.devices() returns it.

Returns () -> () — A function that removes this listener.

local off = cameraInput.onDeviceChange(function(devices) print(#devices) end)

typed/builtin//modules/api/engine/cameraInput/cameraInput/open

cameraInput.open(opts: CameraOpenOpts?) -> (CameraFeed?, string?)

Open a camera as a feed. Returns the feed at once; it reads permissionPending while the platform asks for access, then running (or denied / failed with a reason). Wait with feed:awaitRunning().

feed.texture is a texture handle that shows the camera's current frame: put it on a surface as a session material's base_color_texture (renderer.material.create, worn with Model:applySessionMaterial) or in a slot of a material that exists (renderer.material.setTexture(key, "base_color_texture", feed.texture)), in UI as { type = "image", props = { src = feed.texture } }, and av.record films it like any other scene content.

device names a camera by the id or name cameraInput.devices() lists; facing asks for the "front" or "back" camera instead; with neither, the platform's default camera opens. width, height and fps are hints: the device delivers the nearest it can, and feed:status() reports what it delivers. A second feed on a camera already open shares it.

Returns nil and the reason for a request refused before any camera is touched: a hint outside its range, a facing other than front or back, a device the platform does not offer.

Parameters

  • opts CameraOpenOpts (optional) — { device, facing, width, height, fps }, every field optional.

Returns (CameraFeed?, string?) — The feed, or nil and the reason.

local feed = cameraInput.open({ facing = "front", width = 1280, height = 720 })
local screen = feed and renderer.material.create({ shader = "unlit", textures = { base_color_texture = feed.texture } }, "tv_screen")

typed/builtin//modules/api/engine/cameraInput/cameraInput/stopAll

cameraInput.stopAll() -> number

Stop every open feed and release every camera, whatever opened them.

Returns number — How many feeds stopped.

cameraInput.stopAll()
  • api
  • reference