Add eslint
This commit is contained in:
@@ -19,13 +19,12 @@
|
||||
import { Injectable, signal, computed, inject, OnDestroy, Signal } from '@angular/core';
|
||||
import { Subscription } from 'rxjs';
|
||||
import { WebRTCService } from './webrtc.service';
|
||||
/* eslint-disable @typescript-eslint/member-ordering, @typescript-eslint/prefer-for-of, id-length, max-statements-per-line */
|
||||
|
||||
/** RMS volume threshold (0–1) above which a user counts as "speaking". */
|
||||
const SPEAKING_THRESHOLD = 0.015;
|
||||
|
||||
/** How many consecutive silent frames before we flip speaking → false. */
|
||||
const SILENT_FRAME_GRACE = 8;
|
||||
|
||||
/** FFT size for the AnalyserNode (smaller = cheaper). */
|
||||
const FFT_SIZE = 256;
|
||||
|
||||
@@ -73,13 +72,13 @@ export class VoiceActivityService implements OnDestroy {
|
||||
this.subs.push(
|
||||
this.webrtc.onRemoteStream.subscribe(({ peerId, stream }) => {
|
||||
this.trackStream(peerId, stream);
|
||||
}),
|
||||
})
|
||||
);
|
||||
|
||||
this.subs.push(
|
||||
this.webrtc.onPeerDisconnected.subscribe((peerId) => {
|
||||
this.untrackStream(peerId);
|
||||
}),
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
@@ -114,7 +113,9 @@ export class VoiceActivityService implements OnDestroy {
|
||||
*/
|
||||
isSpeaking(userId: string): Signal<boolean> {
|
||||
const entry = this.tracked.get(userId);
|
||||
if (entry) return entry.speakingSignal.asReadonly();
|
||||
|
||||
if (entry)
|
||||
return entry.speakingSignal.asReadonly();
|
||||
|
||||
// Return a computed that re-checks the map so it becomes live
|
||||
// once the stream is tracked.
|
||||
@@ -127,7 +128,10 @@ export class VoiceActivityService implements OnDestroy {
|
||||
*/
|
||||
volume(userId: string): Signal<number> {
|
||||
const entry = this.tracked.get(userId);
|
||||
if (entry) return entry.volumeSignal.asReadonly();
|
||||
|
||||
if (entry)
|
||||
return entry.volumeSignal.asReadonly();
|
||||
|
||||
return computed(() => 0);
|
||||
}
|
||||
|
||||
@@ -141,14 +145,18 @@ export class VoiceActivityService implements OnDestroy {
|
||||
trackStream(id: string, stream: MediaStream): void {
|
||||
// If we already track this exact stream, skip.
|
||||
const existing = this.tracked.get(id);
|
||||
if (existing && existing.stream === stream) return;
|
||||
|
||||
if (existing && existing.stream === stream)
|
||||
return;
|
||||
|
||||
// Clean up any previous entry for this id.
|
||||
if (existing) this.disposeEntry(existing);
|
||||
if (existing)
|
||||
this.disposeEntry(existing);
|
||||
|
||||
const ctx = new AudioContext();
|
||||
const source = ctx.createMediaStreamSource(stream);
|
||||
const analyser = ctx.createAnalyser();
|
||||
|
||||
analyser.fftSize = FFT_SIZE;
|
||||
|
||||
source.connect(analyser);
|
||||
@@ -167,7 +175,7 @@ export class VoiceActivityService implements OnDestroy {
|
||||
volumeSignal,
|
||||
speakingSignal,
|
||||
silentFrames: 0,
|
||||
stream,
|
||||
stream
|
||||
});
|
||||
|
||||
// Ensure the poll loop is running.
|
||||
@@ -177,19 +185,25 @@ export class VoiceActivityService implements OnDestroy {
|
||||
/** Stop tracking and dispose resources for a given ID. */
|
||||
untrackStream(id: string): void {
|
||||
const entry = this.tracked.get(id);
|
||||
if (!entry) return;
|
||||
|
||||
if (!entry)
|
||||
return;
|
||||
|
||||
this.disposeEntry(entry);
|
||||
this.tracked.delete(id);
|
||||
this.publishSpeakingMap();
|
||||
|
||||
// Stop polling when nothing is tracked.
|
||||
if (this.tracked.size === 0) this.stopPolling();
|
||||
if (this.tracked.size === 0)
|
||||
this.stopPolling();
|
||||
}
|
||||
|
||||
// ── Polling loop ────────────────────────────────────────────────
|
||||
|
||||
private ensurePolling(): void {
|
||||
if (this.animFrameId !== null) return;
|
||||
if (this.animFrameId !== null)
|
||||
return;
|
||||
|
||||
this.poll();
|
||||
}
|
||||
|
||||
@@ -214,23 +228,29 @@ export class VoiceActivityService implements OnDestroy {
|
||||
|
||||
// Compute RMS volume from time-domain data (values 0–255, centred at 128).
|
||||
let sumSquares = 0;
|
||||
|
||||
for (let i = 0; i < dataArray.length; i++) {
|
||||
const normalised = (dataArray[i] - 128) / 128;
|
||||
|
||||
sumSquares += normalised * normalised;
|
||||
}
|
||||
|
||||
const rms = Math.sqrt(sumSquares / dataArray.length);
|
||||
|
||||
volumeSignal.set(rms);
|
||||
|
||||
const wasSpeaking = speakingSignal();
|
||||
|
||||
if (rms >= SPEAKING_THRESHOLD) {
|
||||
entry.silentFrames = 0;
|
||||
|
||||
if (!wasSpeaking) {
|
||||
speakingSignal.set(true);
|
||||
mapDirty = true;
|
||||
}
|
||||
} else {
|
||||
entry.silentFrames++;
|
||||
|
||||
if (wasSpeaking && entry.silentFrames >= SILENT_FRAME_GRACE) {
|
||||
speakingSignal.set(false);
|
||||
mapDirty = true;
|
||||
@@ -238,7 +258,8 @@ export class VoiceActivityService implements OnDestroy {
|
||||
}
|
||||
});
|
||||
|
||||
if (mapDirty) this.publishSpeakingMap();
|
||||
if (mapDirty)
|
||||
this.publishSpeakingMap();
|
||||
|
||||
this.animFrameId = requestAnimationFrame(this.poll);
|
||||
};
|
||||
@@ -246,6 +267,7 @@ export class VoiceActivityService implements OnDestroy {
|
||||
/** Rebuild the public speaking-map signal from current entries. */
|
||||
private publishSpeakingMap(): void {
|
||||
const map = new Map<string, boolean>();
|
||||
|
||||
this.tracked.forEach((entry, id) => {
|
||||
map.set(id, entry.speakingSignal());
|
||||
});
|
||||
@@ -256,6 +278,7 @@ export class VoiceActivityService implements OnDestroy {
|
||||
|
||||
private disposeEntry(entry: TrackedStream): void {
|
||||
try { entry.source.disconnect(); } catch { /* already disconnected */ }
|
||||
|
||||
try { entry.ctx.close(); } catch { /* already closed */ }
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user