Skip to content
Open
Show file tree
Hide file tree
Changes from 5 commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
bcfa1ba
fix: add handleConstraintsRequired handler for audio constraints fix
antsukanova Apr 2, 2026
e903658
fix: update handleConstraintsRequired
antsukanova Apr 3, 2026
54f77fd
fix: update recoveryStream flow
antsukanova Apr 3, 2026
0769169
fix: update newTrack.enabled
antsukanova Apr 3, 2026
ed4b739
fix: update logic, namings, tests
antsukanova Apr 7, 2026
45268c4
fix: update logic with readyState
antsukanova Apr 7, 2026
5c0479d
chore: renaming for handleConstraintsRequired
antsukanova Apr 9, 2026
160e7ed
chore: small refactor changes
antsukanova Apr 9, 2026
abe135c
chore: update path for catch (err: unknown)
antsukanova Apr 9, 2026
5e0bf72
fix: update with effect was disposed check
antsukanova Apr 10, 2026
ed3d0a6
fix: update local stream with constraints-released
antsukanova Apr 14, 2026
8510c5c
fix: update removeTrackHandlers order and logs
antsukanova Apr 15, 2026
1551b08
refactor: put reacquireInputTrack to local audio stream
antsukanova Apr 16, 2026
3ec0623
chore: fix type
antsukanova Apr 16, 2026
fe0b727
chore: update addEffect
antsukanova Apr 16, 2026
20ec267
chore: update ordering
antsukanova Apr 16, 2026
f09c510
refactor: update logic for filterToSupportedConstraints
antsukanova Apr 20, 2026
08c970b
refactor: update for savedTrackSettings and as never for tests
antsukanova Apr 20, 2026
34689a2
refactor: update disposeEffects fir live readyState catch
antsukanova Apr 20, 2026
ea33c2d
test: add additional tests and logic for effectsToDispose
antsukanova Apr 20, 2026
69e7aad
test: improve tests
antsukanova Apr 20, 2026
908a40b
chore: improve edge cases
antsukanova Apr 20, 2026
a7786f2
chore: update tests
antsukanova May 8, 2026
f5ab883
chore: simplify logic in a file
antsukanova May 8, 2026
4c6a038
chore: add try catch for effect.replaceInputTrack(newTrack)
antsukanova May 11, 2026
df4388a
chore: update the mute state
antsukanova May 11, 2026
a772121
fix: update order for stopping track to fix issue with getSettings
antsukanova May 13, 2026
0981ddc
chore: return log forapplyConstraints
antsukanova May 14, 2026
734d374
chore: merge main
antsukanova Jun 23, 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
250 changes: 249 additions & 1 deletion src/media/local-stream.spec.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import { WebrtcCoreError } from '../errors';
import { createMockedStream } from '../util/test-utils';
import * as media from '.';
import { createMockedAudioStream, createMockedStream } from '../util/test-utils';
import { LocalStream, LocalStreamEventNames, TrackEffect } from './local-stream';
import { StreamEventNames } from './stream';

/**
* A dummy LocalStream implementation, so we can instantiate it for testing.
Expand Down Expand Up @@ -187,6 +189,252 @@ describe('LocalStream', () => {
});
});

describe('handleConstraintsRequired', () => {
const audioSettings: MediaTrackSettings = {
deviceId: 'test-device-id',
sampleRate: 48000,
channelCount: 1,
sampleSize: 16,
echoCancellation: true,
autoGainControl: true,
noiseSuppression: true,
};

let audioStream: MediaStream;
let audioLocalStream: LocalStream;
let effect: TrackEffect;
let constraintsHandler: (constraints: MediaTrackConstraints) => Promise<void>;
let getUserMediaSpy: jest.SpyInstance;
let newAudioTrack: MediaStreamTrack;

beforeEach(async () => {
audioStream = createMockedAudioStream();
audioLocalStream = new TestLocalStream(audioStream);

const inputTrack = audioStream.getTracks()[0];
jest.spyOn(inputTrack, 'getSettings').mockReturnValue(audioSettings);

const eventHandlers = new Map<string, (...args: unknown[]) => void>();
effect = {
id: 'nr-effect',
kind: 'noise-reduction',
isEnabled: false,
dispose: jest.fn().mockResolvedValue(undefined),
load: jest.fn().mockResolvedValue(undefined),
replaceInputTrack: jest.fn().mockResolvedValue(undefined),
on: jest.fn().mockImplementation((event: string, handler: (...args: unknown[]) => void) => {
eventHandlers.set(event, handler);
}),
off: jest.fn(),
} as unknown as TrackEffect;

const newMockStream = createMockedAudioStream();
[newAudioTrack] = newMockStream.getTracks();
(newMockStream.getAudioTracks as jest.Mock).mockReturnValue([newAudioTrack]);

getUserMediaSpy = jest.spyOn(media, 'getUserMedia').mockResolvedValue(newMockStream);

await audioLocalStream.addEffect(effect);
constraintsHandler = eventHandlers.get('constraints-required') as (
constraints: MediaTrackConstraints
) => Promise<void>;
});

afterEach(() => {
getUserMediaSpy.mockRestore();
});

it('should call getUserMedia with current settings and effect constraints', async () => {
expect.hasAssertions();

await constraintsHandler({ autoGainControl: false, noiseSuppression: false });

expect(getUserMediaSpy).toHaveBeenCalledWith({
audio: {
deviceId: { exact: 'test-device-id' },
sampleRate: 48000,
channelCount: 1,
sampleSize: 16,
echoCancellation: true,
autoGainControl: false,
noiseSuppression: false,
},
});
});

it('should skip re-acquisition when constraints are empty and nothing saved', async () => {
expect.hasAssertions();

await constraintsHandler({});

expect(getUserMediaSpy).not.toHaveBeenCalled();
});

it('should skip re-acquisition when constraints are already satisfied', async () => {
expect.hasAssertions();

await constraintsHandler({ autoGainControl: true, noiseSuppression: true });

expect(getUserMediaSpy).not.toHaveBeenCalled();
});

it('should restore saved user constraints when empty constraints are received', async () => {
expect.hasAssertions();

await constraintsHandler({ autoGainControl: false, noiseSuppression: false });
getUserMediaSpy.mockClear();

(audioStream.getTracks as jest.Mock).mockReturnValue([newAudioTrack]);
jest.spyOn(newAudioTrack, 'getSettings').mockReturnValue({
...audioSettings,
autoGainControl: false,
noiseSuppression: false,
});

await constraintsHandler({});

expect(getUserMediaSpy).toHaveBeenCalledWith({
audio: expect.objectContaining({
autoGainControl: true,
noiseSuppression: true,
}),
});
});

it('should not restore a second time after saved constraints are cleared', async () => {
expect.hasAssertions();

await constraintsHandler({ autoGainControl: false });
getUserMediaSpy.mockClear();

(audioStream.getTracks as jest.Mock).mockReturnValue([newAudioTrack]);
jest.spyOn(newAudioTrack, 'getSettings').mockReturnValue({
...audioSettings,
autoGainControl: false,
});

await constraintsHandler({});
getUserMediaSpy.mockClear();

await constraintsHandler({});

expect(getUserMediaSpy).not.toHaveBeenCalled();
});

it('should replace the input track on the first effect', async () => {
expect.hasAssertions();

await constraintsHandler({ autoGainControl: false });

expect(effect.replaceInputTrack).toHaveBeenCalledWith(newAudioTrack);
});

it('should remove track handlers before stopping the current track', async () => {
expect.hasAssertions();

const currentTrack = audioStream.getTracks()[0];
const callOrder: string[] = [];

jest.spyOn(currentTrack, 'removeEventListener').mockImplementation(() => {
callOrder.push('removeEventListener');
});
jest.spyOn(currentTrack, 'stop').mockImplementation(() => {
callOrder.push('stop');
});

await constraintsHandler({ autoGainControl: false });

const firstRemove = callOrder.indexOf('removeEventListener');
const firstStop = callOrder.indexOf('stop');
expect(firstRemove).toBeGreaterThanOrEqual(0);
expect(firstStop).toBeGreaterThan(firstRemove);
});

it('should stop the current track before calling getUserMedia', async () => {
expect.hasAssertions();

const currentTrack = audioStream.getTracks()[0];
const callOrder: string[] = [];

jest.spyOn(currentTrack, 'stop').mockImplementation(() => {
callOrder.push('stop');
});
getUserMediaSpy.mockImplementation(async () => {
callOrder.push('getUserMedia');
return createMockedAudioStream();
});

await constraintsHandler({ autoGainControl: false });

expect(callOrder).toStrictEqual(['stop', 'getUserMedia']);
});

it('should fall back to getUserMedia without effect constraints when first call fails', async () => {
expect.hasAssertions();

const fallbackStream = createMockedAudioStream();
getUserMediaSpy
.mockRejectedValueOnce(new Error('OverconstrainedError'))
.mockResolvedValueOnce(fallbackStream);

await constraintsHandler({ autoGainControl: false });

expect(getUserMediaSpy).toHaveBeenCalledTimes(2);
expect(getUserMediaSpy).toHaveBeenLastCalledWith({
audio: {
deviceId: { exact: 'test-device-id' },
sampleRate: 48000,
channelCount: 1,
sampleSize: 16,
echoCancellation: true,
autoGainControl: true,
noiseSuppression: true,
},
});
});

it('should emit Ended when both getUserMedia calls fail', async () => {
expect.hasAssertions();

const endedSpy = jest.spyOn(audioLocalStream[StreamEventNames.Ended], 'emit');

getUserMediaSpy
.mockRejectedValueOnce(new Error('OverconstrainedError'))
.mockRejectedValueOnce(new Error('NotFoundError'));

await constraintsHandler({ autoGainControl: false });

expect(getUserMediaSpy).toHaveBeenCalledTimes(2);
expect(endedSpy).toHaveBeenCalledWith();
});

it('should not register constraints-required handler for video tracks', async () => {
expect.hasAssertions();

const videoStream = createMockedStream();
const videoLocalStream = new TestLocalStream(videoStream);

const videoEventHandlers = new Map<string, (...args: unknown[]) => void>();
const videoEffect = {
id: 'video-effect',
kind: 'video-kind',
isEnabled: false,
dispose: jest.fn().mockResolvedValue(undefined),
load: jest.fn().mockResolvedValue(undefined),
on: jest.fn().mockImplementation((event: string, handler: (...args: unknown[]) => void) => {
videoEventHandlers.set(event, handler);
}),
off: jest.fn(),
} as unknown as TrackEffect;

await videoLocalStream.addEffect(videoEffect);

expect(videoEventHandlers.has('constraints-required')).toBe(false);
expect(videoEventHandlers.has('track-updated')).toBe(true);
expect(videoEventHandlers.has('disposed')).toBe(true);
});
});

describe('toJSON', () => {
it('should correctly serialize data', () => {
expect.assertions(1);
Expand Down
Loading
Loading