Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
9020d5e
feat: add initial support for frame processor usage directly on tracks
1egoman May 27, 2026
3623d33
fix: add optional chaining call to new *Cleared FrameProcessor methods
1egoman May 28, 2026
7fea123
fix: add missing changeset
1egoman May 28, 2026
1b10da8
fix: get rid of AudioStreamLike
1egoman May 28, 2026
9e49683
fix: run prettier
1egoman May 28, 2026
793b649
fix: address devin issue
1egoman May 29, 2026
479f8bc
feat: port in updates from https://github.com/livekit/python-sdks/pul…
1egoman Jun 24, 2026
1fea4b1
feat: add audio stream tests to exercise new frame processor paths
1egoman Jun 24, 2026
f37fa1e
fix: address bad formatting
1egoman Jun 24, 2026
01fda68
fix: hack around ffi free running for fake handles
1egoman Jun 24, 2026
6ba8a8d
fix: ensure track.info.sid is rewritten during full reconnect path
1egoman Jun 24, 2026
1567796
fix: be sure to clean up frame processors when disconnecting the room
1egoman Jun 24, 2026
4f2d86e
fix: swap importActual -> vi.importActual
1egoman Jun 24, 2026
255f712
fix: attempt LLM driven update to get frame processor tests to pass b…
1egoman Jun 24, 2026
83203b1
fix: add missing internal to new export only used in tests
1egoman Jun 26, 2026
052ed6d
fix: address formatting
1egoman Jun 26, 2026
8e83a43
Revert "fix: attempt LLM driven update to get frame processor tests t…
1egoman Jun 26, 2026
2013b96
fix: swap from bun test -> bun --bun run test
1egoman Jun 26, 2026
e4a769c
feat: guard that the bun CI job actually runs on bun
1egoman Jun 26, 2026
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/proud-pianos-joke.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@livekit/rtc-node': patch
---

Add initial support for frame processor usage directly on tracks
13 changes: 12 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,18 @@ jobs:
LIVEKIT_API_SECRET: ${{ secrets.LIVEKIT_API_SECRET }}

- name: Test server sdk + rtc-node (Bun)
run: bun install && bun test --concurrent
# Run vitest under the bun runtime rather than bun's native test runner.
# `--bun` forces bun to execute vitest (overriding its node shebang), so
# test bodies still run on bun while vitest provides full module mocking
# (vi.mock / vi.spyOn) that bun's native runner lacks.
# EXPECT_BUN_RUNTIME=1 arms a guard test (src/bun_runtime.test.ts) that
# fails if test bodies aren't actually executing on bun — i.e. if the
# `--bun` flag ever stops overriding vitest's node shebang and the job
# silently regresses to running under node.
run: |
bun install
(cd packages/livekit-server-sdk && EXPECT_BUN_RUNTIME=1 bun --bun run test)
(cd packages/livekit-rtc && EXPECT_BUN_RUNTIME=1 bun --bun run test)

build_and_release:
if: github.ref == 'refs/heads/main'
Expand Down
32 changes: 28 additions & 4 deletions packages/livekit-rtc/src/audio_stream.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,13 @@ import type { Track } from './track.js';

export interface AudioStreamOptions {
noiseCancellation?: NoiseCancellationOptions | FrameProcessor<AudioFrame>;
/**
* When the audio stream closes, whether to run the {@link FrameProcessor}'s
* `close()` method. If `false`, the processor is left open so it can be
* reused with another {@link AudioStream}. Only relevant when
* `noiseCancellation` is a {@link FrameProcessor}. Defaults to `true`.
*/
autoCloseNoiseCancellation?: boolean;
sampleRate?: number;
numChannels?: number;
frameSizeMs?: number;
Expand All @@ -24,26 +31,31 @@ export interface NoiseCancellationOptions {
options: Record<string, any>;
}

class AudioStreamSource implements UnderlyingSource<AudioFrame> {
/** @internal */
export class AudioStreamSource implements UnderlyingSource<AudioFrame> {
Comment thread
1egoman marked this conversation as resolved.
private controller?: ReadableStreamDefaultController<AudioFrame>;
private ffiHandle: FfiHandle;
private disposed = false;
private sampleRate: number;
private numChannels: number;
private legacyNcOptions?: NoiseCancellationOptions;
private frameProcessor?: FrameProcessor<AudioFrame>;
private frameProcessor: FrameProcessor<AudioFrame> | null = null;
private autoCloseProcessor = true;
private frameSizeMs?: number;
private track: Track;

constructor(
track: Track,
sampleRateOrOptions?: number | AudioStreamOptions,
numChannels?: number,
) {
this.track = track;
if (sampleRateOrOptions !== undefined && typeof sampleRateOrOptions !== 'number') {
this.sampleRate = sampleRateOrOptions.sampleRate ?? 48000;
this.numChannels = sampleRateOrOptions.numChannels ?? 1;
if (isFrameProcessor(sampleRateOrOptions.noiseCancellation)) {
this.frameProcessor = sampleRateOrOptions.noiseCancellation;
this.autoCloseProcessor = sampleRateOrOptions.autoCloseNoiseCancellation ?? true;
} else {
this.legacyNcOptions = sampleRateOrOptions.noiseCancellation;
}
Expand Down Expand Up @@ -77,6 +89,12 @@ class AudioStreamSource implements UnderlyingSource<AudioFrame> {
this.ffiHandle = new FfiHandle(res.stream!.handle!.id!);

FfiClient.instance.on(FfiClientEvent.FfiEvent, this.onEvent);
track.registerAudioStream(this);
}

/** @internal */
get processor(): FrameProcessor<AudioFrame> | null {
return this.frameProcessor;
}

private onEvent = (ev: FfiEvent) => {
Expand Down Expand Up @@ -113,8 +131,11 @@ class AudioStreamSource implements UnderlyingSource<AudioFrame> {
// while buffered frames are still in the ReadableStream queue.
if (!this.disposed) {
this.disposed = true;
this.track.unregisterAudioStream(this);
this.ffiHandle.dispose();
this.frameProcessor?.close();
if (this.frameProcessor && this.autoCloseProcessor) {
this.frameProcessor.close();
}
}
break;
}
Expand All @@ -128,10 +149,13 @@ class AudioStreamSource implements UnderlyingSource<AudioFrame> {
FfiClient.instance.off(FfiClientEvent.FfiEvent, this.onEvent);
if (!this.disposed) {
this.disposed = true;
this.track.unregisterAudioStream(this);
this.ffiHandle.dispose();
// Also close the frame processor on cancel for symmetry with the EOS path,
// so resources are released regardless of how the stream ends.
this.frameProcessor?.close();
if (this.frameProcessor && this.autoCloseProcessor) {
this.frameProcessor.close();
}
}
}
}
Expand Down
Loading
Loading