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.
Using the SDK, write code that sends frames into a Room with the write() method and receives frames with an onFrame() callback.
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.
Each video frame consists of the following parameters:
| Parameter | Type | Necessity | Accepted values |
|---|---|---|---|
y | yPlane | Required | Luminance frame data buffer |
u | uPlane | Required | Blue-difference chrominance (Cb) frame data buffer |
v | vPlane | Required | Red-difference chrominance (Cr) frame data buffer |
yStride | integer | Required | Length of the luminance stride expressed in number of pixels. |
uStride | integer | Required | Length of the blue-difference chrominance stride expressed in number of pixels. |
vStride | integer | Required | Length of the red-difference chrominance stride expressed in number of pixels. |
width | integer | Required | Width of the frame expressed in number of pixels. |
height | integer | Required | Height of the frame expressed in number of pixels. |
rotation | integer | Optional | Degrees of rotation expressed as one of four values: 0, 90, 180, 270. |
timestampNs | bigint | Required | Timestamp 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.
| Plane | Logical size | Buffer size | Description |
|---|---|---|---|
Y | width × height | yStride × height | Luminance |
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 withy,u, andvBufferobjects and theiryStride,uStride, andvStridevalues. - When you receive a frame, each plane arrives wrapped in an object with
data,stride,width, andheightfields.
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: 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.
1const { createLocalVideoTrack } = require('@twilio/video-node-sdk');23const videoTrack = createLocalVideoTrack('virtual-camera');4// Pass videoTrack to connect() or publish it later.56videoTrack.write({7y: yPlane,8u: uPlane,9v: vPlane,10yStride: 1280,11uStride: 640,12vStride: 640,13width: 1280,14height: 720,15timestampNs: process.hrtime.bigint(), // optional; defaults to now16});
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:
1const { createLocalAudioTrack } = require('@twilio/video-node-sdk');23const audioTrack = createLocalAudioTrack('mic');45audioTrack.write({6pcm: pcmBuffer, // interleaved int16 samples7frames: 480, // samples in this buffer8});
Register a callback with onFrame() on a subscribed remote track.
- With video, each plane arrives as an object with a
databuffer and astride. - With audio, each frame arrives as an object with four properties.
1function trackSubscribed(track) {2if (track.kind === 'video') {3track.onFrame(frame => {4const { width, height } = frame;5const yData = frame.y.data;6const yStride = frame.y.stride;7// Process the frame, then return.8});9}1011if (track.kind === 'audio') {12track.onFrame(frame => {13// frame.pcm, frame.sampleRate, frame.channels, frame.frames14});15}16}
To stop receiving, call removeFrameCallback() on the track.
The following example comes from the video_mirror.js example in the SDK repository. This code receives remote video and sends it straight back into the Room, mapping each received plane onto a write() input.
1track.onFrame(frame => {2videoTrack.write({3y: frame.y.data,4u: frame.u.data,5v: frame.v.data,6width: frame.width,7height: frame.height,8yStride: frame.y.stride,9uStride: frame.u.stride,10vStride: frame.v.stride,11timestampNs: frame.timestampNs,12rotation: 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 and its helpers/paced-audio-writer.js in the SDK repository.
- Best practices: Pace frames, manage memory, and troubleshoot common issues.
- API Reference: Browse the full frame and track APIs.