Skip to content

fix(rtc): respect byteOffset when passing frame data to native code - #745

Open
aozmekik wants to merge 2 commits into
livekit:mainfrom
aozmekik:fix/679-frame-byteoffset
Open

aozmekik wants to merge 2 commits into
livekit:mainfrom
aozmekik:fix/679-frame-byteoffset

Conversation

@aozmekik

Copy link
Copy Markdown

Fixes #679

AudioFrame.protoInfo() and VideoFrame.dataPtr pass retrievePtr(new Uint8Array(this.data.buffer)) to native code. That view starts at byte 0 of the backing ArrayBuffer, so data.byteOffset is dropped. When a frame is built from a subarray() (which the AudioFrame constructor comment suggests) or from a pooled Node Buffer, native code reads from the start of the backing buffer instead of the frame's own samples.

AudioResampler.push() still passes the right size, so the output length looks correct, but every push resamples the head of the buffer. That explains the issue: 10 ms frames sliced from a clip come out near-silent (presumably the clip starts quiet), while 1 s pushes look roughly fine. The same pointer is used by AudioSource.captureFrame, VideoSource.captureFrame, VideoFrame.convert() and VideoFrame.getPlane().

Fix

Create the view over exactly the frame's bytes:

new Uint8Array(this.data.buffer, this.data.byteOffset, this.data.byteLength)

The native side returns the typed array's as_ptr(), which already accounts for the view's offset (on Node 22, buf.subarray(n) gives base + n), so no extra pointer arithmetic is needed. Frames that start at offset 0 get the same pointer as before.

Tests

  • audio_frame.test.ts
    • reads a subarray()-backed frame back through native code (copyBuffer(protoInfo().dataPtr, byteLength)) and expects the frame's own samples
    • runs the issue scenario: 10 ms subarray() frames vs. slice() copies through AudioResampler(16000, 24000), asserting the residual is below 1% of the signal RMS. It's a tolerance rather than exact equality because soxr dithers 16-bit output, so two runs over the same input can differ by 1–2 LSB.
  • video_frame.test.ts: an I420 frame that starts halfway into its backing buffer; getPlane(0..2) must return its own Y/U/V bytes.

All three fail on main and pass with this change. With the issue's repro on a synthetic 16 kHz clip that starts with 300 ms of near-silence (input RMS ≈ 1531):

push size output RMS on main with this PR
160 samples (10 ms) 11.7 1531.1
1,600 samples (100 ms) 11.6 1531.1
16,000 samples (1 s) 1291.3 1531.1

pnpm --filter @livekit/rtc-node test passes (66 passed, 30 e2e tests skipped without credentials), and lint and format:check are clean. A patch changeset for @livekit/rtc-node is included.

AudioFrame.protoInfo() and VideoFrame.dataPtr built the pointer from
`new Uint8Array(this.data.buffer)`, which drops the view's byteOffset.
Any frame whose data is a subarray() of a larger buffer, or a view into
a pooled Node Buffer, handed native code the start of the whole
ArrayBuffer. AudioResampler, AudioSource/VideoSource.captureFrame and
VideoFrame.convert/getPlane then read the wrong bytes: the sample count
was right but the content was the head of the buffer, so 10ms frames
sliced from a clip resampled to near-silence.

Pass a Uint8Array over exactly the view's bytes instead. The native
binding already returns the view's own data pointer, offset included.

Fixes livekit#679
@changeset-bot

changeset-bot Bot commented Oct 17, 2026 •

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: b94a173

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 1 package
Name Type
@livekit/rtc-node Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@CLAassistant

CLAassistant commented Oct 17, 2026 •

Copy link
Copy Markdown

CLA assistant check
All committers have signed the CLA.

@devin-ai-integration devin-ai-integration Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ Devin Review: No Issues Found

Devin Review analyzed this PR and found no bugs or issues to report.

Devin Review

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

AudioResampler 16kHz→24kHz outputs near-silence when fed 10ms frames (realtime framing)

2 participants