feat: Add TURN server support
All checks were successful
Queue Release Build / prepare (push) Successful in 15s
Deploy Web Apps / deploy (push) Successful in 5m35s
Queue Release Build / build-linux (push) Successful in 24m45s
Queue Release Build / build-windows (push) Successful in 13m52s
Queue Release Build / finalize (push) Successful in 23s

This commit is contained in:
2026-04-18 21:27:04 +02:00
parent 167c45ba8d
commit 44588e8789
60 changed files with 2404 additions and 365 deletions

View File

@@ -10,6 +10,7 @@ import { LatencyProfile } from '../realtime.constants';
import { PeerData } from '../realtime.types';
import { WebRTCLogger } from '../logging/webrtc-logger';
import { NoiseReductionManager } from './noise-reduction.manager';
import { loadVoiceSettingsFromStorage } from '../../../domains/voice-session/infrastructure/util/voice-settings-storage.util';
import {
TRACK_KIND_AUDIO,
TRACK_KIND_VIDEO,
@@ -105,6 +106,12 @@ export class MediaManager {
private callbacks: MediaManagerCallbacks
) {
this.noiseReduction = new NoiseReductionManager(logger);
// Read the persisted noise-reduction preference so enableVoice()
// uses the correct value even before voice-controls loads.
try {
this._noiseReductionDesired = loadVoiceSettingsFromStorage().noiseReduction;
} catch { /* keep default */ }
}
/**
@@ -226,7 +233,7 @@ export class MediaManager {
: stream;
// Apply input gain (mic volume) before sending to peers
this.applyInputGainToCurrentStream();
await this.applyInputGainToCurrentStream();
this.logger.logStream('localVoice', this.localMediaStream);
@@ -296,7 +303,7 @@ export class MediaManager {
}
// Apply input gain (mic volume) before sending to peers
this.applyInputGainToCurrentStream();
await this.applyInputGainToCurrentStream();
this.bindLocalTracksToAllPeers();
this.isVoiceActive = true;
@@ -447,7 +454,7 @@ export class MediaManager {
}
// Re-apply input gain to the (possibly new) stream
this.applyInputGainToCurrentStream();
await this.applyInputGainToCurrentStream();
// Propagate the new audio track to every peer connection
this.bindLocalTracksToAllPeers();
@@ -479,8 +486,7 @@ export class MediaManager {
}
if (this.localMediaStream) {
this.applyInputGainToCurrentStream();
this.bindLocalTracksToAllPeers();
void this.applyInputGainToCurrentStream().then(() => this.bindLocalTracksToAllPeers());
}
}
@@ -840,12 +846,22 @@ export class MediaManager {
* If a gain pipeline already exists for the same source stream the gain
* value is simply updated. Otherwise a new pipeline is created.
*/
private applyInputGainToCurrentStream(): void {
private async applyInputGainToCurrentStream(): Promise<void> {
const stream = this.localMediaStream;
if (!stream)
return;
// When gain is unity (1.0) skip the Web Audio pipeline entirely and
// use the raw microphone stream. This avoids unnecessary AudioContext
// overhead when no volume adjustment is needed.
if (this.inputGainVolume === 1.0) {
this.teardownInputGain();
this.preGainStream = stream;
this.applyCurrentMuteState();
return;
}
// If the source stream hasn't changed, just update gain
if (this.preGainStream === stream && this.inputGainNode && this.inputGainCtx) {
this.inputGainNode.gain.value = this.inputGainVolume;
@@ -855,9 +871,15 @@ export class MediaManager {
// Tear down the old pipeline (if any)
this.teardownInputGain();
// Build new pipeline: source gain destination
// Build new pipeline: source -> gain -> destination
this.preGainStream = stream;
this.inputGainCtx = new AudioContext();
// Ensure the AudioContext is running before connecting nodes.
if (this.inputGainCtx.state !== 'running') {
await this.inputGainCtx.resume();
}
this.inputGainSourceNode = this.inputGainCtx.createMediaStreamSource(stream);
this.inputGainNode = this.inputGainCtx.createGain();
this.inputGainNode.gain.value = this.inputGainVolume;

View File

@@ -7,9 +7,9 @@
* a clean output stream that can be sent to peers instead.
*
* Architecture:
* raw mic AudioContext.createMediaStreamSource
* NoiseSuppressorWorklet (AudioWorkletNode)
* MediaStreamDestination clean MediaStream
* raw mic -> AudioContext.createMediaStreamSource
* -> NoiseSuppressorWorklet (AudioWorkletNode)
* -> MediaStreamDestination -> clean MediaStream
*
* The manager is intentionally stateless w.r.t. Angular signals;
* the owning MediaManager / WebRTCService drives signals.
@@ -138,7 +138,7 @@ export class NoiseReductionManager {
/**
* Build the AudioWorklet processing graph:
* rawStream source workletNode destination
* rawStream -> source -> workletNode -> destination
*/
private async buildProcessingGraph(rawStream: MediaStream): Promise<void> {
// Reuse or create the AudioContext (must be 48 kHz for RNNoise)