Skip to content

@typegpu/kitvideo

Camera and video

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
  • updateframe
  • writeencoder

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.

ember-mirrorMove in front of the camera and your outline sheds embers.
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
web
from '@typegpu/kit/web';
const
const element: HTMLVideoElement
element
=
var document: Document

window.document returns a reference to the document contained in the window.

MDN Reference

document
.
Document.createElement<"video">(tagName: "video", options?: ElementCreationOptions): HTMLVideoElement (+2 overloads)

In an HTML document, the document.createElement() method creates the HTML element specified by localName, or an HTMLUnknownElement if localName isn't recognized.

MDN Reference

createElement
('video');
const element: HTMLVideoElement
element
.
HTMLMediaElement.srcObject: MediaProvider | null

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.

MDN Reference

srcObject
= await
var navigator: Navigator

The Window.navigator read-only property returns a reference to the Navigator object, which has methods and properties about the application running the script.

MDN Reference

navigator
.
Navigator.mediaDevices: MediaDevices

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.

MDN Reference

mediaDevices
.
MediaDevices.getUserMedia(constraints?: MediaStreamConstraints): Promise<MediaStream>

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.

MDN Reference

getUserMedia
({
MediaStreamConstraints.video?: boolean | MediaTrackConstraints | undefined
video
: true });
const element: HTMLVideoElement
element
.
HTMLMediaElement.muted: boolean

The HTMLMediaElement.muted property indicates whether the media element is muted.

MDN Reference

muted
= true;
await
const element: HTMLVideoElement
element
.
HTMLMediaElement.play(): Promise<void>

The HTMLMediaElement play() method attempts to begin playback of the media. It returns a Promise which is resolved when playback has been successfully started.

MDN Reference

play
();
const
const feed: video.Feed
feed
=
import video
video
.
function create(root: TgpuRoot, options: video.Options): video.Feed

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
(
const root: TgpuRoot
root
, {
Options.size: readonly [number, number]

Sets the texture's width and height in texels.

size
: [640, 360] });
const feed: 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.

attach
(
import web
web
.
function video(element: HTMLVideoElement, options?: web.VideoOptions): web.Video

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
(
const element: HTMLVideoElement
element
, {
VideoOptions.mirrored?: boolean | undefined

Flips every frame left to right, as a mirror shows a front camera. Default false.

mirrored
: true }));
await
const feed: video.Feed
feed
.
Feed.initAsync(): Promise<void>
initAsync
();
const
const loop: frames.Frames
loop
=
import frames
frames
.
function create(root: TgpuRoot, record: Recorder): frames.Frames

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
(
const root: TgpuRoot
root
, (
frame: frames.Frame
frame
) => {
const feed: 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:

import {
import bindings
bindings
,
import video
video
} from '@typegpu/kit';
const
const camera: video.Live
camera
=
import video
video
.
function live(root: TgpuRoot, options?: video.LiveOptions): video.Live

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
(
const root: TgpuRoot
root
);
const camera: 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.

attach
(
import web
web
.
function video(element: HTMLVideoElement, options?: web.VideoOptions): web.Video

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
(
const element: HTMLVideoElement
element
, {
VideoOptions.mirrored?: boolean | undefined

Flips every frame left to right, as a mirror shows a front camera. Default false.

mirrored
: true }));
const
const shared: bindings.Shared
shared
=
import bindings
bindings
.
function share(root: TgpuRoot, parts: Record<string, bindings.Part>): bindings.Shared

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
(
const root: TgpuRoot
root
, {
camera: bindings.Part<bindings.Entries>
camera
:
const camera: 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.

part
}); // before the pipelines that sample
const
const backdrop: TgpuFragmentFn<{
uv: d.Vec2f;
}, d.Vec4f>
backdrop
=
const tgpu: {
const: typeof import("node_modules/typegpu/core/constant/tgpuConstant").constant;
fn: typeof import("node_modules/typegpu/core/function/tgpuFn").fn;
comptime: typeof import("node_modules/typegpu/core/function/comptime").comptime;
resolve: typeof import("node_modules/typegpu/core/resolve/tgpuResolve").resolve;
resolveWithContext: typeof import("node_modules/typegpu/core/resolve/tgpuResolve").resolveWithContext;
init: typeof import("node_modules/typegpu/core/root/init").init;
initFromDevice: typeof import("node_modules/typegpu/core/root/init").initFromDevice;
slot: typeof import("node_modules/typegpu/core/slot/slot").slot;
lazy: typeof import("node_modules/typegpu/core/slot/lazy").lazy;
... 10 more ...;
'~unstable': typeof import("node_modules/typegpu/tgpuUnstable");
}

@module ― typegpu

tgpu
.
fragmentFn: <{
uv: d.Vec2f;
}, d.Vec4f>(options: {
in: {
uv: d.Vec2f;
};
out: d.Vec4f;
}) => TgpuFragmentFnShell<{
uv: d.Vec2f;
}, d.Vec4f> (+1 overload)
fragmentFn
({
in: {
uv: d.Vec2f;
}
in
: {
uv: d.Vec2f
uv
:
import d
d
.
const vec2f: d.Vec2f
vec2f
},
out: d.Vec4f
out
:
import d
d
.
const vec4f: d.Vec4f
vec4f
})(({
uv: d.v2f
uv
}) => {
'use gpu';
return
import video
video
.
const sample: (uv: d.v2f) => d.v4f

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().

sample
(
uv: d.v2f
uv
);
});
const
const drawing: TgpuRenderPipeline<d.Vec4f>
drawing
=
const root: TgpuRoot
root
.
WithBinding.pipe(transform: (cfg: Configurable) => Configurable): WithBinding
pipe
(
const camera: video.Live
camera
.
Live.inject(): Inject

Fills video.sample for pipelines created from the configuration it is piped into.

inject
()).
WithBinding.createRenderPipeline<{}, {}, {
uv: import("node_modules/typegpu/data/wgslTypes").Vec2f;
}, d.Vec4f>(descriptor: TgpuRenderPipeline<in Targets = never>.DescriptorBase & {
attribs?: {} | undefined;
vertex: TgpuVertexFn<{}, {
uv: import("node_modules/typegpu/data/wgslTypes").Vec2f;
}> | ((input: AutoVertexIn<InferGPURecord<AttribRecordToDefaultDataTypes<{}>>>) => AutoVertexOut<AnyAutoCustoms>);
fragment: TgpuFragmentFn<{
uv: import("node_modules/typegpu/data/wgslTypes").Vec2f;
} & Record<string, AnyFragmentInputBuiltin>, d.Vec4f> | ((input: AutoFragmentIn<...>) => d.v4f | (AnyAutoCustoms & Partial<...>));
targets?: TgpuColorTargetState;
}): TgpuRenderPipeline<...> (+2 overloads)
createRenderPipeline
({
vertex: TgpuVertexFn<{}, {
uv: import("node_modules/typegpu/data/wgslTypes").Vec2f;
}> | ((input: AutoVertexIn<InferGPURecord<AttribRecordToDefaultDataTypes<{}>>>) => AutoVertexOut<AnyAutoCustoms>)
vertex
:
import common
common
.
const fullScreenTriangle: TgpuVertexFn<{}, {
uv: import("node_modules/typegpu/data/wgslTypes").Vec2f;
}>
export fullScreenTriangle

A vertex function that defines a single full-screen triangle out of three points.

@example

import { common } from 'typegpu';
const pipeline = root.createRenderPipeline({
vertex: common.fullScreenTriangle,
fragment: yourFragmentShader,
});
pipeline.draw(3);

fullScreenTriangle
,
fragment: TgpuFragmentFn<{
uv: import("node_modules/typegpu/data/wgslTypes").Vec2f;
} & Record<string, AnyFragmentInputBuiltin>, d.Vec4f> | ((input: AutoFragmentIn<InferGPURecord<{
uv: import("node_modules/typegpu/data/wgslTypes").Vec2f;
}>>) => d.v4f | (AnyAutoCustoms & Partial<InferGPURecord<{
readonly $fragDepth: import("node_modules/typegpu/builtin").BuiltinFragDepth;
readonly $sampleMask: import("node_modules/typegpu/builtin").BuiltinSampleMask;
}>>))
fragment
:
const backdrop: TgpuFragmentFn<{
uv: d.Vec2f;
}, d.Vec4f>
backdrop
,
targets?: TgpuColorTargetState
targets
: {
format?: GPUTextureFormat | undefined

The

GPUTextureFormat

of this color target. The pipeline will only be compatible with

GPURenderPassEncoder

s which use a

GPUTextureView

of this format in the corresponding color attachment.

@default ― navigator.gpu.getPreferredCanvasFormat()

format
:
const surface: web.Surface
surface
.
Surface.format: GPUTextureFormat
format
},
});
const
const loop: frames.Frames
loop
=
import frames
frames
.
function create(root: TgpuRoot, record: Recorder): frames.Frames

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
(
const root: TgpuRoot
root
, (
frame: frames.Frame
frame
) => {
const camera: 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
const pass: 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.

encoder
.
TgpuCommandEncoder.beginRenderPass(descriptor: TgpuRenderPassDescriptor): TgpuRenderPass

Begins recording a render pass. Attachment views accept TypeGPU textures, texture views and canvas contexts, next to raw

GPUTextureView

s.

beginRenderPass
({
TgpuRenderPassDescriptor.colorAttachments?: ColorAttachment | readonly (ColorAttachment | null)[] | undefined
colorAttachments
: [{
ColorAttachment.view: GPUTextureView | (ColorTextureConstraint & RenderFlag) | TgpuTextureView<d.WgslTexture<WgslTextureProps>> | TgpuTextureRenderView | GPUCanvasContext

A

GPUTextureView

describing the texture subresource that will be output to for this color attachment.

view
:
const surface: web.Surface
surface
.
Surface.view(frame: frames.Frame): GPUTextureView

Returns a view of the current canvas texture, created once per texture. Applies pending size changes first, which a pass using context would skip.

view
(
frame: frames.Frame
frame
),
ColorAttachment.loadOp?: GPULoadOp | undefined

Indicates the load operation to perform on

GPURenderPassColorAttachment#view

prior to executing the render pass. Note: It is recommended to prefer clearing; see

GPULoadOp# "clear"

for details.

@default ― 'clear'

loadOp
: 'clear',
ColorAttachment.storeOp?: GPUStoreOp | undefined

The store operation to perform on

GPURenderPassColorAttachment#view

after executing the render pass.

@default ― 'store'

storeOp
: 'store' }],
});
const pass: TgpuRenderPass
pass
.
TgpuRenderCommands.setBindGroup<Record<string, TgpuLayoutEntry | null>>(bindGroupLayout: TgpuBindGroupLayout<Record<string, TgpuLayoutEntry | null>>, bindGroup: GPUBindGroup | TgpuBindGroup<Record<string, TgpuLayoutEntry | null>>): void (+1 overload)

Associates a bind group with the given layout

setBindGroup
(
const shared: bindings.Shared
shared
.
Shared.layout: TgpuBindGroupLayout<Record<string, TgpuLayoutEntry | null>>

Contains every part's entries. Each entry is named key_name using the part's key in share.

layout
,
const shared: 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.

group
());
const pass: TgpuRenderPass
pass
.
TgpuRenderCommands.setPipeline(pipeline: TgpuRenderPipeline): void

Sets the current

TgpuRenderPipeline

for subsequent draw calls

setPipeline
(
const drawing: TgpuRenderPipeline<d.Vec4f>
drawing
);
const pass: TgpuRenderPass
pass
.
TgpuRenderCommands.draw(vertexCount: number, instanceCount?: number, firstVertex?: number, firstInstance?: number): void
draw
(3);
const pass: TgpuRenderPass
pass
.
TgpuRenderPass.end(): void

Completes the recording of this render pass

end
();
});
  • 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:

import * as runtimes from '@typegpu/kit/runtimes';
import { useCamera, useFrameOutput } from 'react-native-vision-camera';
const source = runtimes.camera(worker); // the runtime that hosts the scene
hosted.send(({ scene }) => {
'worklet';
scene.feed.attach(source);
});
const output = useFrameOutput({
onFrame: (frame) => {
'worklet';
source.write(frame);
frame.dispose();
},
});
useCamera({ isActive: true, device: 'front', outputs: [output], orientationSource: 'interface' });

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.