Skip to contentSkip to navigationSkip to topbar
Page tools
Useful for sharing or LLM
Accelerate development with AI

On this page
Looking for more inspiration?Visit the

Work with media frames in Node.js


The Video Media SDK for Node.js works with media one frame at a time. A frame represents one unit of decoded media. You can send either one frame of video as one picture in I420 format or one frame of audio frame as a sample of sound(link takes you to an external page).

Using the SDK, write code that sends frames into a Room with the write() method and receives frames with an onFrame() callback.


Video frames

video-frames page anchor

Video frames use the I420 format. Each frame stores video data in three planes or memory blocks of luminance (Y) set per pixel and chrominance (U, V) set per four-pixel block. Each plane serves as a Buffer. Each also has a stride: the number of bytes per row. The stride spans the plane width at minimum. When rows get padded for alignment, the stride can exceed the plane width.

Video frame data parameters

video-frame-data-parameters page anchor

Each video frame consists of the following parameters:

ParameterTypeNecessityAccepted values
yyPlaneRequiredLuminance(link takes you to an external page) frame data buffer
uuPlaneRequiredBlue-difference chrominance(link takes you to an external page) (Cb) frame data buffer
vvPlaneRequiredRed-difference chrominance (Cr) frame data buffer
yStrideintegerRequiredLength of the luminance stride expressed in number of pixels.
uStrideintegerRequiredLength of the blue-difference chrominance stride expressed in number of pixels.
vStrideintegerRequiredLength of the red-difference chrominance stride expressed in number of pixels.
widthintegerRequiredWidth of the frame expressed in number of pixels.
heightintegerRequiredHeight of the frame expressed in number of pixels.
rotationintegerOptionalDegrees of rotation expressed as one of four values: 0, 90, 180, 270.
timestampNsbigintRequiredTimestamp expressed in number of nanoseconds.

The luminance plane dimensions get set to the full size of the frame and the chrominance planes get set to half of the frame size.

PlaneLogical sizeBuffer sizeDescription
Ywidth × heightyStride × heightLuminance
U⌈width/2⌉ × ⌈height/2⌉uStride × ⌈height/2⌉Blue-difference chrominance (Cb)
V⌈width/2⌉ × ⌈height/2⌉vStride × ⌈height/2⌉Red-difference chrominance (Cr)

Input and output use slightly different shapes:

  • When you send a frame with the write() method, you pass a flat object with y, u, and v Buffer objects and their yStride, uStride, and vStride values.
  • When you receive a frame, each plane arrives wrapped in an object with data, stride, width, and height fields.

The input audio frames carry interleaved 48 kHz mono S16LE PCM samples in a single Buffer. You pass only the pcm buffer and the number of frames.

  • S: Use Signed positive or negative integer values.
  • 16: Store 16 bits (or two bytes) of data per audio sample.
  • LE: Use the little-endian method to store data placing the least significant byte in the smallest memory address.
  • PCM: Convert data using Pulse-code modulation(link takes you to an external page): the raw, uncompressed audio wave data.

The output audio frames can vary. Each frame reports its own sampleRate, channels, and frames, along with the pcm buffer and a bigint timestampNs.


Until you connect to a Room, you can't access a track source. When the source isn't ready, the write() method returns false and throws an error on invalid input. After the connect() method returns a response, start your send loop.

To publish video, create a local video track and call the write() method for each I420 frame.

Send one video frame

send-one-video-frame page anchor
1
const { createLocalVideoTrack } = require('@twilio/video-node-sdk');
2
3
const videoTrack = createLocalVideoTrack('virtual-camera');
4
// Pass videoTrack to connect() or publish it later.
5
6
videoTrack.write({
7
y: yPlane,
8
u: uPlane,
9
v: vPlane,
10
yStride: 1280,
11
uStride: 640,
12
vStride: 640,
13
width: 1280,
14
height: 720,
15
timestampNs: process.hrtime.bigint(), // optional; defaults to now
16
});

A real app loops write() method calls at the source frame rate.

To publish audio, create an audio track and call the write() method for the 48 kHz mono PCM buffer of a certain number of frames:

Send a series of audio frames

send-a-series-of-audio-frames page anchor
1
const { createLocalAudioTrack } = require('@twilio/video-node-sdk');
2
3
const audioTrack = createLocalAudioTrack('mic');
4
5
audioTrack.write({
6
pcm: pcmBuffer, // interleaved int16 samples
7
frames: 480, // samples in this buffer
8
});

Receive frames from a Room

receive-frames-from-a-room page anchor

Register a callback with onFrame() on a subscribed remote track.

  • With video, each plane arrives as an object with a data buffer and a stride.
  • With audio, each frame arrives as an object with four properties.

Receive video or audio frames

receive-video-or-audio-frames page anchor
1
function trackSubscribed(track) {
2
if (track.kind === 'video') {
3
track.onFrame(frame => {
4
const { width, height } = frame;
5
const yData = frame.y.data;
6
const yStride = frame.y.stride;
7
// Process the frame, then return.
8
});
9
}
10
11
if (track.kind === 'audio') {
12
track.onFrame(frame => {
13
// frame.pcm, frame.sampleRate, frame.channels, frame.frames
14
});
15
}
16
}

To stop receiving, call removeFrameCallback() on the track.

The following example comes from the video_mirror.js(link takes you to an external page) example in the SDK repository(link takes you to an external page). This code receives remote video and sends it straight back into the Room, mapping each received plane onto a write() input.

Remote video sent to a room

remote-video-sent-to-a-room page anchor
1
track.onFrame(frame => {
2
videoTrack.write({
3
y: frame.y.data,
4
u: frame.u.data,
5
v: frame.v.data,
6
width: frame.width,
7
height: frame.height,
8
yStride: frame.y.stride,
9
uStride: frame.u.stride,
10
vStride: frame.v.stride,
11
timestampNs: frame.timestampNs,
12
rotation: frame.rotation,
13
});
14
});

Send frames at their real frame rate and keep timestamps moving forward.

Audio has extreme time-sensitivity. If you write frames on a plain setInterval, playback might drift and click. Pace the playback using a drift-compensated writer that drains a buffer queue at exactly 48 kHz.

To review a working example, see audio_push.js(link takes you to an external page) and its helpers/paced-audio-writer.js(link takes you to an external page) in the SDK repository(link takes you to an external page).