Merge pull request 'feat(calls): change noise suppression mid-call without rejoining' (#485) from feat/live-denoise-switch into lotus
CI / Secret scan (gitleaks) (push) Successful in 15s
CI / Build & Quality Checks (push) Successful in 1m41s
CI / Docker image build & smoke test (push) Skipped
CI / Trigger Desktop Build (push) Successful in 22s
CI / Playwright smoke (e2e) (push) Successful in 25m24s
CI / Secret scan (gitleaks) (push) Successful in 15s
CI / Build & Quality Checks (push) Successful in 1m41s
CI / Docker image build & smoke test (push) Skipped
CI / Trigger Desktop Build (push) Successful in 22s
CI / Playwright smoke (e2e) (push) Successful in 25m24s
This commit was merged in pull request #485.
This commit is contained in:
Generated
+4
-4
@@ -81,7 +81,7 @@
|
|||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@axe-core/playwright": "4.13.0",
|
"@axe-core/playwright": "4.13.0",
|
||||||
"@lotusguild/element-call-embedded": "0.28.0-lotus.3",
|
"@lotusguild/element-call-embedded": "0.28.0-lotus.4",
|
||||||
"@playwright/test": "1.63.0",
|
"@playwright/test": "1.63.0",
|
||||||
"@rollup/plugin-inject": "5.0.5",
|
"@rollup/plugin-inject": "5.0.5",
|
||||||
"@rollup/plugin-wasm": "6.2.2",
|
"@rollup/plugin-wasm": "6.2.2",
|
||||||
@@ -2695,9 +2695,9 @@
|
|||||||
"integrity": "sha512-dfLbk+PwWvFzSxwk3n5ySL0hfBog779o8h68wK/7/APo/7cgyWp5jcXockbxdk5kFRkbeXWm4Fbi9FrdN381sA=="
|
"integrity": "sha512-dfLbk+PwWvFzSxwk3n5ySL0hfBog779o8h68wK/7/APo/7cgyWp5jcXockbxdk5kFRkbeXWm4Fbi9FrdN381sA=="
|
||||||
},
|
},
|
||||||
"node_modules/@lotusguild/element-call-embedded": {
|
"node_modules/@lotusguild/element-call-embedded": {
|
||||||
"version": "0.28.0-lotus.3",
|
"version": "0.28.0-lotus.4",
|
||||||
"resolved": "https://code.lotusguild.org/api/packages/LotusGuild/npm/%40lotusguild%2Felement-call-embedded/-/0.28.0-lotus.3/element-call-embedded-0.28.0-lotus.3.tgz",
|
"resolved": "https://code.lotusguild.org/api/packages/LotusGuild/npm/%40lotusguild%2Felement-call-embedded/-/0.28.0-lotus.4/element-call-embedded-0.28.0-lotus.4.tgz",
|
||||||
"integrity": "sha512-CPXWXLCZkGMD8MFWgmerTb19UC1aj9QCD1PtSG8AO+IZvT1t1Md2hiCCu/EPDwRyYHBV54n5yWxQGg0hgLSteg==",
|
"integrity": "sha512-g+1mkdpGqUkE7JJo3b2G0XBiwDFwEedWul20hwD0iBpDyW67OafjbhrzRwfROY8Q9bx6BnulJHiR6Ll98KjguA==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"node_modules/@matrix-org/matrix-sdk-crypto-wasm": {
|
"node_modules/@matrix-org/matrix-sdk-crypto-wasm": {
|
||||||
|
|||||||
+1
-1
@@ -108,7 +108,7 @@
|
|||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@axe-core/playwright": "4.13.0",
|
"@axe-core/playwright": "4.13.0",
|
||||||
"@lotusguild/element-call-embedded": "0.28.0-lotus.3",
|
"@lotusguild/element-call-embedded": "0.28.0-lotus.4",
|
||||||
"@playwright/test": "1.63.0",
|
"@playwright/test": "1.63.0",
|
||||||
"@rollup/plugin-inject": "5.0.5",
|
"@rollup/plugin-inject": "5.0.5",
|
||||||
"@rollup/plugin-wasm": "6.2.2",
|
"@rollup/plugin-wasm": "6.2.2",
|
||||||
|
|||||||
@@ -60,6 +60,7 @@ import { SrOnly } from '../features/call-status/styles.css';
|
|||||||
import { useCallHotkeys } from '../hooks/useCallHotkeys';
|
import { useCallHotkeys } from '../hooks/useCallHotkeys';
|
||||||
import { useAfkAutoMute } from '../hooks/useAfkAutoMute';
|
import { useAfkAutoMute } from '../hooks/useAfkAutoMute';
|
||||||
import { useCallQuality } from '../hooks/useCallQuality';
|
import { useCallQuality } from '../hooks/useCallQuality';
|
||||||
|
import { useCallDenoise } from '../hooks/useCallDenoise';
|
||||||
import { useRemoteAllMuted } from '../hooks/useCallSpeakers';
|
import { useRemoteAllMuted } from '../hooks/useCallSpeakers';
|
||||||
import { useRoomAvatar, useRoomName } from '../hooks/useRoomMeta';
|
import { useRoomAvatar, useRoomName } from '../hooks/useRoomMeta';
|
||||||
import { mDirectAtom } from '../state/mDirectList';
|
import { mDirectAtom } from '../state/mDirectList';
|
||||||
@@ -836,6 +837,7 @@ function CallUtils({ embed, joined }: { embed: CallEmbed; joined: boolean }) {
|
|||||||
useMutedTalkWarning(embed, joined);
|
useMutedTalkWarning(embed, joined);
|
||||||
useCallThemeSync(embed);
|
useCallThemeSync(embed);
|
||||||
useCallQuality(embed);
|
useCallQuality(embed);
|
||||||
|
useCallDenoise(embed);
|
||||||
useCallHangupEvent(
|
useCallHangupEvent(
|
||||||
embed,
|
embed,
|
||||||
useCallback(() => {
|
useCallback(() => {
|
||||||
|
|||||||
@@ -2137,7 +2137,7 @@ export function Calls() {
|
|||||||
</details>
|
</details>
|
||||||
|
|
||||||
<Text size="T200" priority="400">
|
<Text size="T200" priority="400">
|
||||||
Note: Applying changes requires rejoining the call.
|
Changes apply straight away, even during a call.
|
||||||
</Text>
|
</Text>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,41 @@
|
|||||||
|
import { useEffect } from 'react';
|
||||||
|
import { useAtomValue } from 'jotai';
|
||||||
|
import { CallEmbed } from '../plugins/call';
|
||||||
|
import { settingsAtom } from '../state/settings';
|
||||||
|
import { buildDenoisePayload } from '../utils/callDenoise';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Apply noise-suppression setting changes to a call that's already running
|
||||||
|
* (the fork's `io.lotus.set_denoise`): switching between off, browser-native
|
||||||
|
* and an ML model, or between models, takes effect without rejoining. The join
|
||||||
|
* itself still carries the settings in the call URL, so the first send is a
|
||||||
|
* no-op fork-side.
|
||||||
|
*/
|
||||||
|
export function useCallDenoise(embed: CallEmbed): void {
|
||||||
|
const {
|
||||||
|
callNoiseSuppression,
|
||||||
|
callDenoiseModel,
|
||||||
|
callDenoiseNativeNS,
|
||||||
|
callDenoiseGate,
|
||||||
|
callDenoiseGateThreshold,
|
||||||
|
} = useAtomValue(settingsAtom);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
embed.control.setDenoise(
|
||||||
|
buildDenoisePayload({
|
||||||
|
callNoiseSuppression,
|
||||||
|
callDenoiseModel,
|
||||||
|
callDenoiseNativeNS,
|
||||||
|
callDenoiseGate,
|
||||||
|
callDenoiseGateThreshold,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}, [
|
||||||
|
embed,
|
||||||
|
callNoiseSuppression,
|
||||||
|
callDenoiseModel,
|
||||||
|
callDenoiseNativeNS,
|
||||||
|
callDenoiseGate,
|
||||||
|
callDenoiseGateThreshold,
|
||||||
|
]);
|
||||||
|
}
|
||||||
@@ -4,6 +4,7 @@ import { CallControlState } from './CallControlState';
|
|||||||
import { ElementMediaStateDetail, ElementMediaStatePayload, ElementWidgetActions } from './types';
|
import { ElementMediaStateDetail, ElementMediaStatePayload, ElementWidgetActions } from './types';
|
||||||
import { canDelegateCapability } from './utils';
|
import { canDelegateCapability } from './utils';
|
||||||
import { CallErrorState, parseCallError } from './callError';
|
import { CallErrorState, parseCallError } from './callError';
|
||||||
|
import type { LotusDenoisePayload } from '../../utils/callDenoise';
|
||||||
|
|
||||||
export enum CallControlEvent {
|
export enum CallControlEvent {
|
||||||
StateUpdate = 'state_update',
|
StateUpdate = 'state_update',
|
||||||
@@ -106,6 +107,11 @@ export class CallControl extends EventEmitter implements CallControlState {
|
|||||||
// pre-join send pends to a 10s widget timeout, mirroring the deafen gate).
|
// pre-join send pends to a 10s widget timeout, mirroring the deafen gate).
|
||||||
private lastQuality: LotusQualityPayload | null = null;
|
private lastQuality: LotusQualityPayload | null = null;
|
||||||
|
|
||||||
|
// Last noise-suppression choice (io.lotus.set_denoise), held like
|
||||||
|
// lastQuality: sent once joined and re-sent after an EC reconnect, whose
|
||||||
|
// remounted handler starts again from the settings in the join URL.
|
||||||
|
private lastDenoise: LotusDenoisePayload | null = null;
|
||||||
|
|
||||||
// C-M5: set true by CallControls while a push-to-talk key is held. A PTT hold
|
// C-M5: set true by CallControls while a push-to-talk key is held. A PTT hold
|
||||||
// unmutes the mic transiently, and onMediaState() must NOT treat that as a
|
// unmutes the mic transiently, and onMediaState() must NOT treat that as a
|
||||||
// user-initiated unmute that auto-undeafens the user.
|
// user-initiated unmute that auto-undeafens the user.
|
||||||
@@ -191,6 +197,7 @@ export class CallControl extends EventEmitter implements CallControlState {
|
|||||||
this.joined = true;
|
this.joined = true;
|
||||||
this.sendDeafenState();
|
this.sendDeafenState();
|
||||||
this.sendQuality();
|
this.sendQuality();
|
||||||
|
this.sendDenoise();
|
||||||
this.sendHotkeyCodes();
|
this.sendHotkeyCodes();
|
||||||
this.sendFrameScreenshareAllowed();
|
this.sendFrameScreenshareAllowed();
|
||||||
}
|
}
|
||||||
@@ -204,6 +211,7 @@ export class CallControl extends EventEmitter implements CallControlState {
|
|||||||
public resendForkState(): void {
|
public resendForkState(): void {
|
||||||
this.sendDeafenState();
|
this.sendDeafenState();
|
||||||
this.sendQuality();
|
this.sendQuality();
|
||||||
|
this.sendDenoise();
|
||||||
if (this._audioOutputId !== undefined) this.sendAudioOutput(this._audioOutputId);
|
if (this._audioOutputId !== undefined) this.sendAudioOutput(this._audioOutputId);
|
||||||
// [Gitea #17] The pin lives fork-side and is dropped on a handler remount.
|
// [Gitea #17] The pin lives fork-side and is dropped on a handler remount.
|
||||||
if (this._focusedUserId !== null) this.sendFocus(this._focusedUserId, this._focusedMediaId);
|
if (this._focusedUserId !== null) this.sendFocus(this._focusedUserId, this._focusedMediaId);
|
||||||
@@ -762,6 +770,21 @@ export class CallControl extends EventEmitter implements CallControlState {
|
|||||||
this.call.transport.send('io.lotus.set_quality', this.lastQuality).catch(() => undefined);
|
this.call.transport.send('io.lotus.set_quality', this.lastQuality).catch(() => undefined);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Switch noise suppression on the live mic (the fork's `io.lotus.set_denoise`):
|
||||||
|
* browser-native on/off and/or the ML model, without rejoining. Join-gated
|
||||||
|
* like setQuality().
|
||||||
|
*/
|
||||||
|
public setDenoise(payload: LotusDenoisePayload): void {
|
||||||
|
this.lastDenoise = payload;
|
||||||
|
this.sendDenoise();
|
||||||
|
}
|
||||||
|
|
||||||
|
private sendDenoise(): void {
|
||||||
|
if (!this.joined || !this.lastDenoise) return;
|
||||||
|
this.call.transport.send('io.lotus.set_denoise', this.lastDenoise).catch(() => undefined);
|
||||||
|
}
|
||||||
|
|
||||||
public dispose() {
|
public dispose() {
|
||||||
// [Gitea #56] Don't let a manual focus pin outlive the call.
|
// [Gitea #56] Don't let a manual focus pin outlive the call.
|
||||||
this.clearFocusParticipant();
|
this.clearFocusParticipant();
|
||||||
|
|||||||
@@ -0,0 +1,27 @@
|
|||||||
|
import { test } from 'node:test';
|
||||||
|
import assert from 'node:assert/strict';
|
||||||
|
|
||||||
|
import { buildDenoisePayload } from './callDenoise';
|
||||||
|
|
||||||
|
test('buildDenoisePayload passes the settings through', () => {
|
||||||
|
assert.deepEqual(
|
||||||
|
buildDenoisePayload({
|
||||||
|
callNoiseSuppression: 'ml',
|
||||||
|
callDenoiseModel: 'dtln',
|
||||||
|
callDenoiseNativeNS: false,
|
||||||
|
callDenoiseGate: true,
|
||||||
|
callDenoiseGateThreshold: -50,
|
||||||
|
}),
|
||||||
|
{ mode: 'ml', model: 'dtln', nativeNS: false, gate: true, gateThreshold: -50 },
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('buildDenoisePayload uses the same defaults as the join URL', () => {
|
||||||
|
assert.deepEqual(buildDenoisePayload({}), {
|
||||||
|
mode: 'browser',
|
||||||
|
model: 'rnnoise',
|
||||||
|
nativeNS: true,
|
||||||
|
gate: false,
|
||||||
|
gateThreshold: -45,
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
import { DenoiseModelId, NoiseSuppressionMode } from '../state/settings';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Payload for the fork's `io.lotus.set_denoise` action, which switches noise
|
||||||
|
* suppression on the live mic mid-call (browser suppressor on/off and/or the
|
||||||
|
* ML model) instead of needing a rejoin.
|
||||||
|
*/
|
||||||
|
export type LotusDenoisePayload = {
|
||||||
|
mode: NoiseSuppressionMode;
|
||||||
|
model: DenoiseModelId;
|
||||||
|
nativeNS: boolean;
|
||||||
|
gate: boolean;
|
||||||
|
gateThreshold: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type DenoiseSettings = {
|
||||||
|
callNoiseSuppression?: NoiseSuppressionMode;
|
||||||
|
callDenoiseModel?: DenoiseModelId;
|
||||||
|
callDenoiseNativeNS?: boolean;
|
||||||
|
callDenoiseGate?: boolean;
|
||||||
|
callDenoiseGateThreshold?: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
/** Same defaults as the call URL built at join (useCallStart/createCallEmbed). */
|
||||||
|
export const buildDenoisePayload = (s: DenoiseSettings): LotusDenoisePayload => ({
|
||||||
|
mode: s.callNoiseSuppression ?? 'browser',
|
||||||
|
model: s.callDenoiseModel ?? 'rnnoise',
|
||||||
|
nativeNS: s.callDenoiseNativeNS ?? true,
|
||||||
|
gate: s.callDenoiseGate ?? false,
|
||||||
|
gateThreshold: s.callDenoiseGateThreshold ?? -45,
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user