Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/fix-nv12-i420a-plane-length.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@livekit/rtc-node': patch
---

Size the chroma planes of NV12 and I420A frames from the chroma height when copying them out of native memory
28 changes: 28 additions & 0 deletions packages/livekit-rtc/src/video_frame.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
// SPDX-FileCopyrightText: 2026 LiveKit, Inc.
//
// SPDX-License-Identifier: Apache-2.0
import { VideoBufferType } from '@livekit/rtc-ffi-bindings';
import { describe, expect, it } from 'vitest';
import { getPlaneLength } from './video_frame.js';

describe('getPlaneLength', () => {
it.each([
// 8x4: luma 32, each chroma plane 4x2 = 8
[VideoBufferType.I420, 8, 4, 32 + 8 * 2],
[VideoBufferType.I420A, 8, 4, 32 * 2 + 8 * 2],
[VideoBufferType.NV12, 8, 4, 32 + 8 * 2],
// 4x8: luma 32, each chroma plane 2x4 = 8
[VideoBufferType.I420, 4, 8, 32 + 8 * 2],
[VideoBufferType.I420A, 4, 8, 32 * 2 + 8 * 2],
[VideoBufferType.NV12, 4, 8, 32 + 8 * 2],
// odd dimensions round the chroma planes up: 5x3 -> 3x2
[VideoBufferType.I420A, 5, 3, 15 * 2 + 6 * 2],
[VideoBufferType.NV12, 5, 3, 15 + 6 * 2],
])('sizes buffer type %i at %ix%i as %i bytes', (type, width, height, expected) => {
expect(getPlaneLength(type, width, height)).toBe(expected);
});

it('sizes a 1280x720 NV12 frame from its chroma height', () => {
expect(getPlaneLength(VideoBufferType.NV12, 1280, 720)).toBe(1_382_400);
});
});
7 changes: 4 additions & 3 deletions packages/livekit-rtc/src/video_frame.ts
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,8 @@ export class VideoFrame {
}
}

const getPlaneLength = (type: VideoBufferType, width: number, height: number): number => {
/** @internal */
export const getPlaneLength = (type: VideoBufferType, width: number, height: number): number => {
const chromaWidth = Math.trunc((width + 1) / 2);
const chromaHeight = Math.trunc((height + 1) / 2);
switch (type) {
Expand All @@ -118,13 +119,13 @@ const getPlaneLength = (type: VideoBufferType, width: number, height: number): n
case VideoBufferType.I420:
return width * height + chromaWidth * chromaHeight * 2;
case VideoBufferType.I420A:
return width * height * 2 + chromaWidth * chromaWidth * 2;
return width * height * 2 + chromaWidth * chromaHeight * 2;
case VideoBufferType.I422:
return width * height + chromaWidth * height * 2;
case VideoBufferType.I010:
return width * height * 2 + chromaWidth * chromaHeight * 4;
case VideoBufferType.NV12:
return width * height + chromaWidth * chromaWidth * 2;
return width * height + chromaWidth * chromaHeight * 2;
}
};

Expand Down
Loading