---
"@context": https://schema.org
"@type": TechArticle
"@id": https://www.twilio.com/docs/video/node-working-with-media-frames#article
headline: Work with media frames in Node.js
description: Learn the I420 video and PCM audio frame formats the Video Media SDK for Node.js uses, and how to send and receive raw media frames in a Video Room.
url: https://www.twilio.com/docs/video/node-working-with-media-frames
inLanguage: en
dateModified: 2026-10-08T15:07:09.000Z
author:
  "@type": Organization
  name: Twilio Developer Education Team
publisher:
  "@type": Organization
  name: Twilio
---

# Work with media frames in Node.js

> \[!IMPORTANT]
>
> The Video Media SDK for Node.js is currently available as a Public Beta product and the information contained in this document is subject to change. This means that some features are not yet implemented and others may be changed before the product is declared as Generally Available. Public Beta products are not covered by the Twilio Support Terms or [Twilio Service Level Agreement][sla].
>
> The Video Media SDK for Node.js is not a US [*Health Insurance Portability and Accountability Act* (HIPAA)][hipaa] Eligible Service or [*Payment Card Industry Data Security Standard* (PCI DSS)][pci] compliant and should not be enabled in workflows that are subject to HIPAA or PCI.

[hipaa]: https://en.wikipedia.org/wiki/Health_Insurance_Portability_and_Accountability_Act

[pci]: https://en.wikipedia.org/wiki/Payment_Card_Industry_Data_Security_Standard

[sla]: https://help.twilio.com/articles/115002413087-Twilio-Beta-product-support

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][pcm].

Using the SDK, write code that sends frames into a Room with the `write()` method and receives frames with the `frames()` async iterator.

## Video frames

Video frames use the [I420][] format. Each frame stores video data in three *planes*: luminance (`Y`) at full resolution, and two chrominance planes (`U`, `V`) at half resolution in each dimension. Each plane is an object that holds the plane's `data` as a `Buffer`, its `stride`, and its own `width` and `height`. The stride is the number of bytes per row. It's at least the plane width, and it can exceed the width when rows are padded for alignment.

### Video frame data parameters

Each video frame consists of the following parameters:

| Parameter   | Type         | Necessity | Accepted values                                                               |
| ----------- | ------------ | --------- | ----------------------------------------------------------------------------- |
| `width`     | integer      | Required  | Frame width in pixels. Must be positive and even.                             |
| `height`    | integer      | Required  | Frame height in pixels. Must be positive and even.                            |
| `y`         | plane object | Required  | [Luminance][luma] plane                                                       |
| `u`         | plane object | Required  | Blue-difference [chrominance][] (`Cb`) plane                                  |
| `v`         | plane object | Required  | Red-difference chrominance (`Cr`) plane                                       |
| `format`    | string       | Optional  | `'I420'`, the only accepted value                                             |
| `timestamp` | number       | Optional  | Presentation time in microseconds. Defaults to the current time.              |
| `rotation`  | integer      | Optional  | Degrees of rotation expressed as one of four values: `0`, `90`, `180`, `270`. |

Each plane object has the following fields:

| Field    | Type    | Description                                                         |
| -------- | ------- | ------------------------------------------------------------------- |
| `data`   | Buffer  | The plane's samples, at least `stride` × `height` bytes long.       |
| `stride` | integer | Bytes per row. At least `width`, and possibly padded for alignment. |
| `width`  | integer | Plane width in samples. Half the frame width for `U` and `V`.       |
| `height` | integer | Plane height in samples. Half the frame height for `U` and `V`.     |

### Video frame plane sizes

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`         | `y.stride` × `height`     | Luminance                          |
| `U`   | `⌈width/2⌉` × `⌈height/2⌉` | `u.stride` × `⌈height/2⌉` | Blue-difference chrominance (`Cb`) |
| `V`   | `⌈width/2⌉` × `⌈height/2⌉` | `v.stride` × `⌈height/2⌉` | Red-difference chrominance (`Cr`)  |

Sending and receiving use the same shape. Each of `y`, `u`, and `v` is a plane object, so you can write a received frame straight back out without reshaping it.

## Audio frames

The input [audio frames][aframes] 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*][pcm]: 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 `timestamp` in microseconds.

## Send frames into a Room

The `write()` method returns `false` when the SDK doesn't send a frame. For video, that most often means you wrote the frame before `connect()` resolved. For audio, it means the frame didn't fit in the publish queue. The method throws a `TypeError` or `RangeError` exception on invalid input. Start your send loop after `connect()` resolves.

### Send video frames

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

```javascript title="Send one video frame"
const { createLocalVideoTrack } = require('@twilio/video-node-sdk');

const videoTrack = createLocalVideoTrack('virtual-camera');
// Pass videoTrack to connect() or publish it later.

videoTrack.write({
  format: 'I420',
  width: 1280,
  height: 720,
  y: { data: yPlane, stride: 1280, width: 1280, height: 720 },
  u: { data: uPlane, stride: 640, width: 640, height: 360 },
  v: { data: vPlane, stride: 640, width: 640, height: 360 },
  // timestamp is optional; it defaults to the current time in microseconds.
});
```

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

### Send audio frames

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:

```javascript title="Send a series of audio frames"
const { createLocalAudioTrack } = require('@twilio/video-node-sdk');

const audioTrack = createLocalAudioTrack('mic');

audioTrack.write({
  pcm: pcmBuffer, // interleaved int16 samples
  frames: 480, // samples in this buffer
});
```

## Receive frames from a Room

Read frames from a subscribed remote track with the `frames()` async iterator.

* With video, each plane arrives as a plane object with `data`, `stride`, `width`, and `height`.
* With audio, each frame carries its `pcm` buffer, `sampleRate`, `channels`, and `frames`, along with its `timestamp`.

```javascript title="Receive video or audio frames"
async function trackSubscribed(track) {
  if (track.kind === 'video') {
    for await (const frame of track.frames()) {
      const { width, height } = frame;
      const yData = frame.y.data;
      const yStride = frame.y.stride;
      // Process the frame.
      frame.close?.();
    }
  } else if (track.kind === 'audio') {
    for await (const frame of track.frames()) {
      // frame.pcm, frame.sampleRate, frame.channels, frame.frames
      frame.close?.();
    }
  }
}
```

The loop ends when the track is unsubscribed or the Room disconnects. To stop receiving sooner, exit the loop with a `break` statement.

The following example comes from the [`video_mirror.js`][] example in the [SDK repository][]. This code receives remote video and sends it back into the Room by passing each received frame's planes to `write()`.

```javascript title="Remote video sent to a room"
for await (const frame of track.frames()) {
  videoTrack.write({
    format: 'I420',
    width: frame.width,
    height: frame.height,
    y: frame.y,
    u: frame.u,
    v: frame.v,
    timestamp: frame.timestamp,
    rotation: frame.rotation,
  });
  frame.close?.();
}
```

## Frame timing and pacing

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][].

## Next steps

* [Best practices][]: Pace frames, manage memory, and troubleshoot common issues.
* [API Reference][]: Browse the full frame and track APIs.

[`video_mirror.js`]: https://github.com/twilio/twilio-video-node/blob/main/examples/video_mirror.js

[`helpers/paced-audio-writer.js`]: https://github.com/twilio/twilio-video-node/blob/main/examples/helpers/paced-audio-writer.js

[`audio_push.js`]: https://github.com/twilio/twilio-video-node/blob/main/examples/audio_push.js

[aframes]: /docs/video/node-working-with-media-frames#audio-frames

[API Reference]: https://twilio.github.io/twilio-video-node/latest/

[Best practices]: /docs/video/node-best-practices

[chrominance]: https://en.wikipedia.org/wiki/Chrominance

[i420]: /docs/glossary/i420

[luma]: https://en.wikipedia.org/wiki/Luma_\(video\)

[pcm]: https://en.wikipedia.org/wiki/Pulse-code_modulation

[SDK repository]: https://github.com/twilio/twilio-video-node/tree/main/examples
