Skip to content

Commit e094320

Browse files
authored
add e2e tests (#591)
* add e2e tests * fixup * use logger * fix formatting * fix tests * check out submodules * run test after linux builds * match uid/gid * pass uid/gid * pnpm ci true * fix * enable more modern c++ toolchain * clang compile * try * another attempt * correct probe * fix * install deps in host runner * ignore examples * build and test in docker * combined build & test * tweak * fix env vars * increase timeout
1 parent 0efe517 commit e094320

File tree

11 files changed

+650
-42
lines changed

11 files changed

+650
-42
lines changed

.github/workflows/ci.yml

Lines changed: 60 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ jobs:
5353
runs-on: ubuntu-latest
5454
steps:
5555
- name: Checkout Repo
56-
uses: actions/checkout@v5
56+
uses: actions/checkout@v6
5757
- uses: pnpm/action-setup@v4
5858
- name: Use Node.js 24
5959
uses: actions/setup-node@v6
@@ -81,7 +81,7 @@ jobs:
8181
node-version: [20, 22, 24, latest]
8282
runs-on: ubuntu-latest
8383
steps:
84-
- uses: actions/checkout@v4
84+
- uses: actions/checkout@v6
8585
- uses: pnpm/action-setup@v4
8686
- name: Setup Node.js
8787
uses: actions/setup-node@v6
@@ -90,8 +90,7 @@ jobs:
9090
cache: pnpm
9191
- name: Install dependencies
9292
run: pnpm install
93-
- name: Test livekit-rtc
94-
run: pnpm --filter="livekit-rtc" test
93+
# RTC tests will be ran after they are built, because builds are so slow
9594
- name: Test livekit-server-sdk (Node)
9695
run: pnpm --filter="livekit-server-sdk" test
9796
- name: Test livekit-server-sdk (Browser)
@@ -131,7 +130,7 @@ jobs:
131130
RUST_BACKTRACE: full
132131
needs: check-changes
133132
steps:
134-
- uses: actions/checkout@v5
133+
- uses: actions/checkout@v6
135134
with:
136135
submodules: recursive
137136

@@ -169,26 +168,70 @@ jobs:
169168
- name: Install dependencies
170169
run: pnpm install
171170

172-
- name: Build (Linux)
171+
# on linux, we'll also run tests after building. for the e2e suite, this would
172+
# only run on the main repo, and not forks
173+
- name: Build & test (Linux)
173174
if: ${{ matrix.platform == 'linux' }}
175+
env:
176+
LIVEKIT_URL: ${{ secrets.LIVEKIT_URL }}
177+
LIVEKIT_API_KEY: ${{ secrets.LIVEKIT_API_KEY }}
178+
LIVEKIT_API_SECRET: ${{ secrets.LIVEKIT_API_SECRET }}
174179
run: |
175180
PROTOC_PATH=$(which protoc)
181+
HOST_UID=$(id -u)
182+
HOST_GID=$(id -g)
176183
docker run --rm \
184+
-e HOST_UID=$HOST_UID \
185+
-e HOST_GID=$HOST_GID \
186+
-e TARGET=${{ matrix.target }} \
177187
-v $PWD:/workspace \
178188
-v $PROTOC_PATH:/tmp/protoc:ro \
179189
-w /workspace \
180190
${{ matrix.build_image }} \
181-
bash -c "\
182-
uname -a; \
183-
cp /tmp/protoc /usr/local/bin/protoc && chmod +x /usr/local/bin/protoc; \
184-
export PATH=/root/.cargo/bin:\$PATH; \
185-
export RUST_BACKTRACE=full; \
186-
yum install -y openssl-devel libX11-devel mesa-libGL-devel libXext-devel libva-devel libdrm-devel clang-devel; \
187-
curl --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y; \
188-
curl -fsSL https://rpm.nodesource.com/setup_20.x | bash -; \
189-
yum install -y nodejs --setopt=nodesource-nodejs.module_hotfixes=1; \
190-
npm install --global pnpm && pnpm install; \
191-
cd packages/livekit-rtc && pnpm build --target ${{ matrix.target }}"
191+
bash -lc '
192+
set -euo pipefail
193+
194+
uname -a
195+
cp /tmp/protoc /usr/local/bin/protoc
196+
chmod +x /usr/local/bin/protoc
197+
198+
yum install -y openssl-devel libX11-devel mesa-libGL-devel libXext-devel libva-devel libdrm-devel clang clang-devel
199+
yum install -y gcc-toolset-12-gcc gcc-toolset-12-gcc-c++ gcc-toolset-12-libstdc++-devel libstdc++-devel
200+
source /opt/rh/gcc-toolset-12/enable
201+
gcc --version
202+
g++ --version
203+
clang --version
204+
clang++ --version
205+
curl -fsSL https://rpm.nodesource.com/setup_20.x | bash -
206+
yum install -y nodejs --setopt=nodesource-nodejs.module_hotfixes=1
207+
npm install --global pnpm
208+
209+
groupadd -g "$HOST_GID" hostgroup 2>/dev/null || true
210+
useradd -m -u "$HOST_UID" -g "$HOST_GID" hostuser 2>/dev/null || true
211+
212+
su - hostuser -c "
213+
set -euo pipefail
214+
export RUST_BACKTRACE=full
215+
curl --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y
216+
export PATH=\$HOME/.cargo/bin:\$PATH
217+
source /opt/rh/gcc-toolset-12/enable
218+
export CC=clang
219+
export CXX=clang++
220+
TOOLCHAIN_ROOT=/opt/rh/gcc-toolset-12/root/usr
221+
export CFLAGS=\"--gcc-toolchain=\$TOOLCHAIN_ROOT\"
222+
export CXXFLAGS=\"--gcc-toolchain=\$TOOLCHAIN_ROOT\"
223+
cd /workspace
224+
CI=true pnpm install
225+
cd packages/livekit-rtc
226+
pnpm build --target $TARGET
227+
cd ../..
228+
pnpm --filter "./packages/livekit-server-sdk" build
229+
export LIVEKIT_URL=${{ secrets.LIVEKIT_URL }}
230+
export LIVEKIT_API_KEY=${{ secrets.LIVEKIT_API_KEY }}
231+
export LIVEKIT_API_SECRET=${{ secrets.LIVEKIT_API_SECRET }}
232+
pnpm --filter "@livekit/rtc-node" test
233+
"
234+
'
192235
193236
- name: Build (macOS)
194237
if: ${{ matrix.platform == 'macos' }}

examples/agent-dispatch/package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,8 @@
1313
},
1414
"dependencies": {
1515
"dotenv": "^16.4.5",
16-
"livekit-server-sdk": "workspace:*"
16+
"livekit-server-sdk": "workspace:*",
17+
"@livekit/protocol": "^1.43.4"
1718
},
1819
"devDependencies": {
1920
"@types/node": "^20.10.4",

packages/livekit-rtc/package.json

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,9 +57,11 @@
5757
"@bufbuild/protoc-gen-es": "^1.10.1",
5858
"@napi-rs/cli": "^2.18.0",
5959
"@types/node": "^22.13.10",
60+
"vitest": "^3.0.0",
6061
"prettier": "^3.0.3",
6162
"tsup": "^8.3.5",
62-
"typescript": "5.8.2"
63+
"typescript": "5.8.2",
64+
"livekit-server-sdk": "workspace:*"
6365
},
6466
"optionalDependencies": {
6567
"@livekit/rtc-node-darwin-arm64": "workspace:*",
@@ -78,6 +80,8 @@
7880
"artifacts": "pnpm build:ts && napi artifacts",
7981
"build:debug": "napi build --platform",
8082
"lint": "eslint -f unix \"src/**/*.ts\" --ignore-pattern \"src/proto/*\"",
83+
"test": "vitest run",
84+
"test:e2e": "vitest run src/tests/e2e.test.ts",
8185
"universal": "napi universal",
8286
"version": "napi version"
8387
}

packages/livekit-rtc/src/audio_mixer.ts

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
// SPDX-License-Identifier: Apache-2.0
44
import { AsyncQueue } from './async_queue.js';
55
import { AudioFrame } from './audio_frame.js';
6+
import { log } from './log.js';
67

78
// Re-export AsyncQueue for backward compatibility
89
export { AsyncQueue } from './async_queue.js';
@@ -238,7 +239,7 @@ export class AudioMixer {
238239

239240
for (const result of results) {
240241
if (result.status !== 'fulfilled') {
241-
console.warn('AudioMixer: Stream contribution failed:', result.reason);
242+
log.warn('AudioMixer: Stream contribution failed:', result.reason);
242243
continue;
243244
}
244245

@@ -261,6 +262,12 @@ export class AudioMixer {
261262
this.removeStream(stream);
262263
}
263264

265+
// If all streams are exhausted, end the mixer automatically.
266+
// This keeps `for await...of` consumers from hanging indefinitely when inputs complete.
267+
if (!this.ending && removals.length > 0 && this.streams.size === 0) {
268+
this.ending = true;
269+
}
270+
264271
if (!anyData) {
265272
// No data available from any stream, wait briefly before trying again
266273
await this.sleep(1);
@@ -372,25 +379,29 @@ export class AudioMixer {
372379
}
373380

374381
const length = this.chunkSize * this.numChannels;
375-
const mixed = new Int16Array(length);
382+
// Use a wider accumulator to avoid int16 overflow while summing.
383+
const acc = new Int32Array(length);
376384

377385
// Sum all contributions
378386
for (const contrib of contributions) {
379387
for (let i = 0; i < length; i++) {
380388
const val = contrib[i];
381389
if (val !== undefined) {
382-
mixed[i] = (mixed[i] ?? 0) + val;
390+
acc[i] = (acc[i] ?? 0) + val;
383391
}
384392
}
385393
}
386394

387395
// Clip to Int16 range
396+
const mixed = new Int16Array(length);
388397
for (let i = 0; i < length; i++) {
389-
const val = mixed[i] ?? 0;
398+
const val = acc[i] ?? 0;
390399
if (val > 32767) {
391400
mixed[i] = 32767;
392401
} else if (val < -32768) {
393402
mixed[i] = -32768;
403+
} else {
404+
mixed[i] = val;
394405
}
395406
}
396407

packages/livekit-rtc/src/room.ts

Lines changed: 23 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -155,7 +155,14 @@ export class Room extends (EventEmitter as new () => TypedEmitter<RoomCallbacks>
155155
}
156156

157157
get creationTime(): Date {
158-
return new Date(Number(this.info?.creationTime ?? 0));
158+
// TODO: workaround for Rust SDK bug, remove after updating to:
159+
// https://github.com/livekit/rust-sdks/pull/822
160+
// check if creationTime looks like seconds (less than year 3000 in ms), convert to ms if needed
161+
let creationTimeMs = Number(this.info?.creationTime ?? 0);
162+
if (creationTimeMs > 0 && creationTimeMs < 1e12) {
163+
creationTimeMs *= 1000;
164+
}
165+
return new Date(creationTimeMs);
159166
}
160167

161168
get isRecording(): boolean {
@@ -362,7 +369,7 @@ export class Room extends (EventEmitter as new () => TypedEmitter<RoomCallbacks>
362369
participant.info.disconnectReason = ev.value.disconnectReason;
363370
this.emit(RoomEvent.ParticipantDisconnected, participant);
364371
} else {
365-
console.log(`RoomEvent.ParticipantDisconnected: Could not find participant`);
372+
log.warn(`RoomEvent.ParticipantDisconnected: Could not find participant`);
366373
}
367374
} else if (ev.case == 'localTrackPublished') {
368375
const publication = this.localParticipant.trackPublications.get(ev.value.trackSid!);
@@ -377,15 +384,15 @@ export class Room extends (EventEmitter as new () => TypedEmitter<RoomCallbacks>
377384
publication.resolveFirstSubscription();
378385
this.emit(RoomEvent.LocalTrackSubscribed, publication!.track!);
379386
} else {
380-
console.warn(`RoomEvent.LocalTrackSubscribed: Publication not found: ${ev.value.trackSid}`);
387+
log.warn(`RoomEvent.LocalTrackSubscribed: Publication not found: ${ev.value.trackSid}`);
381388
}
382389
} else if (ev.case == 'trackPublished') {
383390
const participant = this.remoteParticipants.get(ev.value.participantIdentity!);
384391
const publication = new RemoteTrackPublication(ev.value.publication!);
385392
if (participant) {
386393
participant.trackPublications.set(publication.sid!, publication);
387394
} else {
388-
console.warn(
395+
log.warn(
389396
`RoomEvent.TrackPublished: Could not find participant: ${ev.value.participantIdentity}`,
390397
);
391398
}
@@ -397,7 +404,7 @@ export class Room extends (EventEmitter as new () => TypedEmitter<RoomCallbacks>
397404
if (publication) {
398405
this.emit(RoomEvent.TrackUnpublished, publication, participant);
399406
} else {
400-
console.warn(`RoomEvent.TrackUnpublished: Could not find publication`);
407+
log.warn(`RoomEvent.TrackUnpublished: Could not find publication`);
401408
}
402409
} else if (ev.case == 'trackSubscribed') {
403410
const ownedTrack = ev.value.track!;
@@ -416,7 +423,7 @@ export class Room extends (EventEmitter as new () => TypedEmitter<RoomCallbacks>
416423

417424
this.emit(RoomEvent.TrackSubscribed, publication.track!, publication, participant);
418425
} catch (e: unknown) {
419-
console.warn(`RoomEvent.TrackSubscribed: ${(e as Error).message}`);
426+
log.warn(`RoomEvent.TrackSubscribed: ${(e as Error).message}`);
420427
}
421428
} else if (ev.case == 'trackUnsubscribed') {
422429
try {
@@ -429,7 +436,7 @@ export class Room extends (EventEmitter as new () => TypedEmitter<RoomCallbacks>
429436
publication.subscribed = false;
430437
this.emit(RoomEvent.TrackUnsubscribed, track, publication, participant);
431438
} catch (e: unknown) {
432-
console.warn(`RoomEvent.TrackUnsubscribed: ${(e as Error).message}`);
439+
log.warn(`RoomEvent.TrackUnsubscribed: ${(e as Error).message}`);
433440
}
434441
} else if (ev.case == 'trackSubscriptionFailed') {
435442
try {
@@ -441,7 +448,7 @@ export class Room extends (EventEmitter as new () => TypedEmitter<RoomCallbacks>
441448
ev.value.error,
442449
);
443450
} catch (e: unknown) {
444-
console.warn(`RoomEvent.TrackSubscriptionFailed: ${(e as Error).message}`);
451+
log.warn(`RoomEvent.TrackSubscriptionFailed: ${(e as Error).message}`);
445452
}
446453
} else if (ev.case == 'trackMuted') {
447454
try {
@@ -455,7 +462,7 @@ export class Room extends (EventEmitter as new () => TypedEmitter<RoomCallbacks>
455462
}
456463
this.emit(RoomEvent.TrackMuted, publication, participant);
457464
} catch (e: unknown) {
458-
console.warn(`RoomEvent.TrackMuted: ${(e as Error).message}`);
465+
log.warn(`RoomEvent.TrackMuted: ${(e as Error).message}`);
459466
}
460467
} else if (ev.case == 'trackUnmuted') {
461468
try {
@@ -469,7 +476,7 @@ export class Room extends (EventEmitter as new () => TypedEmitter<RoomCallbacks>
469476
}
470477
this.emit(RoomEvent.TrackUnmuted, publication, participant);
471478
} catch (e: unknown) {
472-
console.warn(`RoomEvent.TrackUnmuted: ${(e as Error).message}`);
479+
log.warn(`RoomEvent.TrackUnmuted: ${(e as Error).message}`);
473480
}
474481
} else if (ev.case == 'activeSpeakersChanged') {
475482
try {
@@ -478,7 +485,7 @@ export class Room extends (EventEmitter as new () => TypedEmitter<RoomCallbacks>
478485
);
479486
this.emit(RoomEvent.ActiveSpeakersChanged, activeSpeakers);
480487
} catch (e: unknown) {
481-
console.warn(`RoomEvent.ActiveSpeakersChanged: ${(e as Error).message}`);
488+
log.warn(`RoomEvent.ActiveSpeakersChanged: ${(e as Error).message}`);
482489
}
483490
} else if (ev.case == 'roomMetadataChanged') {
484491
this.info.metadata = ev.value.metadata ?? '';
@@ -489,15 +496,15 @@ export class Room extends (EventEmitter as new () => TypedEmitter<RoomCallbacks>
489496
participant.info.metadata = ev.value.metadata;
490497
this.emit(RoomEvent.ParticipantMetadataChanged, participant.metadata, participant);
491498
} catch (e: unknown) {
492-
console.warn(`RoomEvent.ParticipantMetadataChanged: ${(e as Error).message}`);
499+
log.warn(`RoomEvent.ParticipantMetadataChanged: ${(e as Error).message}`);
493500
}
494501
} else if (ev.case == 'participantNameChanged') {
495502
try {
496503
const participant = this.requireParticipantByIdentity(ev.value.participantIdentity!);
497504
participant.info.name = ev.value.name;
498505
this.emit(RoomEvent.ParticipantNameChanged, participant.name!, participant);
499506
} catch (e: unknown) {
500-
console.warn(`RoomEvent.ParticipantNameChanged: ${(e as Error).message}`);
507+
log.warn(`RoomEvent.ParticipantNameChanged: ${(e as Error).message}`);
501508
}
502509
} else if (ev.case == 'participantAttributesChanged') {
503510
try {
@@ -520,14 +527,14 @@ export class Room extends (EventEmitter as new () => TypedEmitter<RoomCallbacks>
520527
this.emit(RoomEvent.ParticipantAttributesChanged, changedAttributes, participant);
521528
}
522529
} catch (e: unknown) {
523-
console.warn(`RoomEvent.ParticipantAttributesChanged: ${(e as Error).message}`);
530+
log.warn(`RoomEvent.ParticipantAttributesChanged: ${(e as Error).message}`);
524531
}
525532
} else if (ev.case == 'connectionQualityChanged') {
526533
try {
527534
const participant = this.requireParticipantByIdentity(ev.value.participantIdentity!);
528535
this.emit(RoomEvent.ConnectionQualityChanged, ev.value.quality!, participant);
529536
} catch (e: unknown) {
530-
console.warn(`RoomEvent.ConnectionQualityChanged: ${(e as Error).message}`);
537+
log.warn(`RoomEvent.ConnectionQualityChanged: ${(e as Error).message}`);
531538
}
532539
} else if (ev.case == 'chatMessage') {
533540
const participant = this.retrieveParticipantByIdentity(ev.value.participantIdentity!);
@@ -612,7 +619,7 @@ export class Room extends (EventEmitter as new () => TypedEmitter<RoomCallbacks>
612619
participant,
613620
);
614621
} catch (e: unknown) {
615-
console.warn(`RoomEvent.ParticipantEncryptionStatusChanged: ${(e as Error).message}`);
622+
log.warn(`RoomEvent.ParticipantEncryptionStatusChanged: ${(e as Error).message}`);
616623
}
617624
}
618625
};

0 commit comments

Comments
 (0)