Copy camera or video frames into a texture you own, fitted, turned and mirrored, or sample the newest frame in place without a copy.
kit.video
module
video.create(root, { size, … })→ Feed
video.live(root, options?)→ Live
rootborrows
the root
Feedreturns
a feed you update every frame and destroy
Livereturns
a live view you update every frame and destroy
GPU functions
sample
types
FitRotationImportedLatestSourceCropFeedLive
Optionscreate, live · 2 fields
size
readonly [number, number]
Sets the texture's width and height in texels.
fit?
Fit= 'cover'
Selects how a frame of another aspect fills the texture.
LiveOptionslive · 1 field
crop?
Options= the whole frame
Crops each frame as a feed with this size and fit does, so video.sample(uv) reads the part of the frame the feed's texture holds at uv.
records
update→frame
write→encoder
update and write into a render pass of their own, before the passes that read the texture.
owns
a texture ·a buffer ·a sampler ·a pipeline ·bind groups
A feed's picture, fitting, fit and each written frame's group; a live view's blank frame, orientation and sampler.
allocates
every write
A bind group for the imported frame. For a live view, each new frame: an external texture from the source and a bind group from the share that holds its part; a web video imports again every frame.
lifecycle
✓initAsync()✓destroy()✓[Symbol.dispose]
injects
hook
The frame, sampler and orientation video.sample reads, from a live view's part.
binds
1 uniform
video.sample, the orientation, beside the frame and its sampler.
Your camera as a dark mirror: each new picture is compared with the last, and edges that moved shed embers. Drifting smoke shows until you allow the camera. Raise the sensitivity to shed from fainter changes.
A camera hands over frames at its own pace, in its own size and orientation, and each frame is valid only briefly: on the web an imported video frame expires when its task ends, and a native camera needs its buffer returned. A scene that samples frames directly has to handle all of that. video.create gives the scene a texture of a size it picks instead, and copies each new frame into it, fitted to that size, turned upright and mirrored if asked. The texture keeps the last frame until the next one arrives.
import{
import video
video}from'@typegpu/kit';
import*as
import web
webfrom'@typegpu/kit/web';
const
constelement:HTMLVideoElement
element=
var document:Document
window.document returns a reference to the document contained in the window.
In an HTML document, the document.createElement() method creates the HTML element specified by localName, or an HTMLUnknownElement if localName isn't recognized.
The srcObject property of the HTMLMediaElement interface sets or returns the object which serves as the source of the media associated with the HTMLMediaElement, or null if not assigned.
The Window.navigator read-only property returns a reference to the Navigator object, which has methods and properties about the application running the script.
The mediaDevices read-only property of the Navigator interface returns a MediaDevices object, which provides access to connected media input devices like cameras and microphones, as well as screen sharing.
Available only in secure contexts.
The getUserMedia() method of the MediaDevices interface prompts the user for permission to use a media input which produces a MediaStream with tracks containing the requested types of media.
The HTMLMediaElement play() method attempts to begin playback of the media. It returns a Promise which is resolved when playback has been successfully started.
Creates a texture of size and the pass that fits frames into it.
@allocates ― every write: a bind group for the imported frame. For a live view, each new frame: an
external texture from the source and a bind group from the share that holds its part; a web
video imports again every frame
@owns ― a texture, a buffer, a sampler, a pipeline, bind groups: a feed's picture, fitting, fit and
each written frame's group; a live view's blank frame, orientation and sampler
@injects ― hook the frame, sampler and orientation video.sample reads, from a live view's
part
@binds ― 0 storage, 1 uniform: video.sample, the orientation, beside the frame and its sampler
@records ― update and write into a render pass of their own, before the passes that read
the texture.
@returns ― a feed you update every frame and destroy
create(
constroot:TgpuRoot
root,{
Options.size: readonly [number, number]
Sets the texture's width and height in texels.
size:[640,360]});
constfeed:video.Feed
feed.
Feed.attach(source: video.Source|undefined): void
Sets where update takes frames from, or undefined to stop and keep the last frame.
Reads a playing <video>, such as one showing getUserMedia's camera, as a source for
video.create's feed or video.live. Frames are numbered as the browser presents them. Readers
must submit in the task that imported the frame, as a frame loop does, because an imported video
frame expires when its task ends. A frame's time is its media time.
@param ― element a video that plays, with its stream or file attached
@returns ― a source to attach to feeds and live views and destroy
video(
constelement:HTMLVideoElement
element,{
VideoOptions.mirrored?: boolean |undefined
Flips every frame left to right, as a mirror shows a front camera. Default false.
Records a frame into one encoder and submits once, with a clock. Borrows the root. The
synchronous recorder borrows each frame's encoder.
@submits ― always once a frame after your recorder returns, and again at each frame.submit()
@param ― record your frame, recorded into each frame's encoder
@returns ― a loop you run with each frame's time and destroy
create(
constroot:TgpuRoot
root,(
frame:frames.Frame
frame)=>{
constfeed:video.Feed
feed.
Feed.update(frame: frames.Frame): void
Records the attached source's frame fitted into frame.encoder if it is new.
update(
frame:frames.Frame
frame);// copies a new frame, or does nothing
// ...passes that read feed.texture
});
feed.texture is rgba8unorm, sampled, at the size you gave. It holds sRGB-encoded colour, as the camera does: decode it with srgbToLinear before lighting, and hand it to a model as it is.
feed.frames counts the frames written. Compare it with the last frame’s count to run work only on new pictures, such as a model or a comparison with the previous frame.
feed.frameSize is the last frame’s size after turning, which tells a portrait camera from a landscape one.
feed.time is when the last frame was captured, or where it sits in a video, in seconds on its source’s clock. Match a model’s result to the frame it saw with it.
fit decides what happens when the frame’s shape differs from the texture’s. 'cover', the default, fills the texture and crops the frame’s overhang. 'contain' shows the whole frame and leaves transparent black around it. 'stretch' fills the texture and changes the frame’s aspect.
The fit is one render pass over the texture, which also turns the frame by its rotation and flips it when mirrored. It writes a uniform only when the frame’s size, rotation or mirroring changes.
feed.crop says where the texture lies on the upright frame, as an origin and extent from 0 to 1 across it. 'cover' keeps it within the frame and 'contain' reaches past it.
video.live binds the source’s newest frame where it lies, and shaders read it with video.sample(uv), turned upright and mirrored as the source asks. Nothing is copied. With crop: { size, fit }, video.sample(uv) reads the part of the frame that a feed of that size and fit holds at uv, which lines up work done on a small feed with the full frame. The ember mirror above does both: its reflection samples the camera in place at full resolution, and its embers compare frames in a small feed. Each new frame is a new external texture, so share the live view’s part before creating the pipelines that sample:
Creates a live view that samples each source's newest frame where it lies, without a copy.
Shaders call video.sample(uv) and see the frame upright and cropped. They sample an external
texture, so they read it with filtering and without mips or textureLoad. Use video.create to
keep frames, compare them or read them as an ordinary texture.
@returns ― a live view you update every frame and destroy
live(
constroot:TgpuRoot
root);
constcamera:video.Live
camera.
Live.attach(source: video.Source|undefined): void
Sets the source, or undefined to stop and show transparent black, because a live view holds
no frame of its own.
Reads a playing <video>, such as one showing getUserMedia's camera, as a source for
video.create's feed or video.live. Frames are numbered as the browser presents them. Readers
must submit in the task that imported the frame, as a frame loop does, because an imported video
frame expires when its task ends. A frame's time is its media time.
@param ― element a video that plays, with its stream or file attached
@returns ― a source to attach to feeds and live views and destroy
video(
constelement:HTMLVideoElement
element,{
VideoOptions.mirrored?: boolean |undefined
Flips every frame left to right, as a mirror shows a front camera. Default false.
Combines parts from modules or your own code in one layout. Call it before creating pipelines.
Set its group on each pass with pass.setBindGroup(shared.layout, shared.group()), or give it to
passes.create as shared, which sets it in every pass it begins. Every pipeline in that pass
that reads a part then uses the shared group.
@param ― parts parts from any modules and your own, by key
@returns ― a layout you set on each pass, with the group for the parts' resources
share(
constroot:TgpuRoot
root,{
camera:bindings.Part<bindings.Entries>
camera:
constcamera:video.Live
camera.
Live.part: bindings.Part<bindings.Entries>
Provides the newest frame, its sampler and orientation for video.sample. Share it before
creating the pipelines that sample, bindings.share(root, { camera: live.part }), because each
new frame is a new external texture. Set the shared group on each pass that samples.
Samples a live view's newest frame at uv, from 0 to 1 across its crop of the frame turned
upright and mirrored as its source asks, the whole frame by default. Transparent black before
the first frame and where 'contain' leaves the crop uncovered. Fill it with live.inject().
Records a frame into one encoder and submits once, with a clock. Borrows the root. The
synchronous recorder borrows each frame's encoder.
@submits ― always once a frame after your recorder returns, and again at each frame.submit()
@param ― record your frame, recorded into each frame's encoder
@returns ― a loop you run with each frame's time and destroy
create(
constroot:TgpuRoot
root,(
frame:frames.Frame
frame)=>{
constcamera:video.Live
camera.
Live.update(frame: frames.Frame): boolean
Binds the source's newest frame. Call it every frame before the passes that sample, because an
imported frame is valid only until its frame submits.
@returns ― whether the frame is new
update(
frame:frames.Frame
frame);// binds the newest frame, new or not
const
constpass:TgpuRenderPass
pass=
frame:frames.Frame
frame.
Frame.encoder: TgpuCommandEncoder
Records commands for submission after the recorder returns. submit() replaces this encoder.
Read it at each call instead of keeping it, and let the frame submit it.
Contains every part's entries. Each entry is named key_name using the part's key in share.
layout,
constshared:bindings.Shared
shared.
Shared.group(): TgpuBindGroup
Returns the group for the current resources, for pass.setBindGroup(shared.layout, …).
Creates a group only when a resource has changed. Otherwise, it allocates nothing.
Call update(frame) every frame before the passes that sample. An imported frame is valid only until the frame that read it submits. It returns whether the frame is new, and camera.frames counts new frames.
video.sample reads an external texture, so it samples with filtering, without mips or textureLoad. Copy frames with video.create to keep them, compare them or read them as an ordinary texture.
Before the first frame, after attach(undefined) and where 'contain' leaves the crop uncovered, video.sample returns transparent black.
A web video imports again every frame. A native camera frame is imported once and stays bound until the next arrives, so steady frames create no bind group.
A source hands out its newest frame, numbered, and any number of feeds and live views read it. Each remembers the last number it saw, so a feed copies a frame once and a live view knows when it is new. web.video(element) reads a playing <video>, from a camera or from a file, and numbers frames as the browser presents them. Readers import a frame in the same task as the frame loop’s submit, while it is valid.
On React Native, runtimes.camera(runtime) reads VisionCamera 5. Call its write(frame) from a frame output’s onFrame on the camera thread, and attach it to the feed on the runtime that hosts the scene. The camera thread wraps each frame’s pixels and gives the camera its buffer back at once. The scene’s runtime imports the newest frame once and keeps it until a newer one arrives, so every feed and live view reads the same frame:
The feed turns each frame by its orientation and mirrors it when isMirrored is set, so VisionCamera’s mirrorMode decides which frames show mirrored. Call source.destroy() after detaching it to release the frame it holds.
Any object with latest(device, seen?) is a source. It returns a video.Latest: an external texture imported on that device, the frame’s size, rotation and mirroring, its optional time and its serial number, counting from 1. It returns undefined before the first frame and when the newest is numbered seen, so a feed imports nothing until a new frame arrives. The source keeps each frame until a newer one replaces it. A program with its own frames, such as decoded VideoFrames, can also call feed.write(encoder, imported) directly and close each frame after the submit.
feed.attach(undefined) stops taking frames and keeps the last one. Stop the camera’s tracks yourself, and destroy the web.video source to stop watching the element.
Importing a video frame needs Chrome 113, Safari 26 or Firefox 144 on Windows. A live view binds a texture view in the external texture slot before its first frame, which needs Chrome 137.
Each copied frame creates a bind group, because each imported frame is a new external texture.
runtimes.camera needs VisionCamera 5 and react-native-webgpu 0.17.