fix(voice): keep the mic graph on the real audio clock (#3197)

This commit is contained in:
Hampus
2026-10-04 16:18:34 +02:00
committed by GitHub
parent 14d475df9a
commit 2ac164d5b8
3 changed files with 47 additions and 3 deletions
@@ -10,6 +10,7 @@ const RESUME_GESTURE_EVENTS = ['pointerdown', 'keydown'] as const;
const IDLE_SUSPEND_DELAY_MS = 500;
const IDLE_CLOSE_DELAY_MS = 60_000;
const RATE_KEYED_CANDIDATE_RATES = [48000, 44100, 32000, 24000, 16000];
const INAUDIBLE_OUTPUT_OFFSET = 1e-6;
export interface VoiceInputContextLease {
readonly context: AudioContext;
@@ -54,9 +55,17 @@ function syncEntryResume(entry: VoiceInputContextEntry): void {
resumeEntry(entry);
}
function keepOutputOnAudioDevice(context: AudioContext): void {
if (typeof ConstantSourceNode !== 'function') return;
const source = new ConstantSourceNode(context, {offset: INAUDIBLE_OUTPUT_OFFSET});
source.connect(context.destination);
source.start();
}
function createEntry(options: AudioContextOptions, shared: boolean): VoiceInputContextEntry | null {
const context = createVoiceAudioContext({latencyHint: 'interactive', ...options});
if (!context) return null;
keepOutputOnAudioDevice(context);
const entry: VoiceInputContextEntry = {
context,
holders: 0,
@@ -105,6 +105,8 @@ export interface PerTrackStats {
nackCount?: number;
pliCount?: number;
firCount?: number;
packetsSent?: number;
sourceSamplesDurationMs?: number;
retransmittedPacketsSent?: number;
retransmittedBytesSent?: number;
keyFramesEncoded?: number;
@@ -113,10 +115,14 @@ export interface PerTrackStats {
powerEfficientDecoder?: boolean;
decoderAcceleration?: VideoAccelerationStatus;
totalDecodeTimeMs?: number;
packetsDiscarded?: number;
jitterBufferDelayMs?: number;
jitterBufferTargetDelayMs?: number;
jitterBufferEmittedCount?: number;
concealedSamples?: number;
silentConcealedSamples?: number;
insertedSamplesForDeceleration?: number;
removedSamplesForAcceleration?: number;
totalSamplesReceived?: number;
}
@@ -251,10 +257,15 @@ interface RTCStatsEntry {
decoderImplementation?: string;
powerEfficientDecoder?: boolean;
totalDecodeTime?: number;
packetsDiscarded?: number;
totalSamplesDuration?: number;
jitterBufferDelay?: number;
jitterBufferTargetDelay?: number;
jitterBufferEmittedCount?: number;
concealedSamples?: number;
silentConcealedSamples?: number;
insertedSamplesForDeceleration?: number;
removedSamplesForAcceleration?: number;
totalSamplesReceived?: number;
dtlsCipher?: string;
srtpCipher?: string;
@@ -371,9 +382,17 @@ function hasUsableJitterBuffer(report: RTCStatsEntry): boolean {
return report.jitterBufferEmittedCount > 0;
}
function buildOutboundTrackExtras(report: RTCStatsEntry): Partial<PerTrackStats> {
function buildOutboundTrackExtras(
report: RTCStatsEntry,
mediaSource: RTCStatsEntry | undefined,
): Partial<PerTrackStats> {
return {
active: report.active,
packetsSent: report.packetsSent,
sourceSamplesDurationMs:
typeof mediaSource?.totalSamplesDuration === 'number'
? Math.round(mediaSource.totalSamplesDuration * 1000)
: undefined,
retransmittedPacketsSent: report.retransmittedPacketsSent,
retransmittedBytesSent: report.retransmittedBytesSent,
keyFramesEncoded: report.keyFramesEncoded,
@@ -397,6 +416,13 @@ function buildInboundTrackExtras(
jitterBufferDelayMs: hasUsableJitterBuffer(report)
? Math.round((report.jitterBufferDelay! / report.jitterBufferEmittedCount!) * 1000)
: undefined,
jitterBufferTargetDelayMs:
hasUsableJitterBuffer(report) && typeof report.jitterBufferTargetDelay === 'number'
? Math.round((report.jitterBufferTargetDelay / report.jitterBufferEmittedCount!) * 1000)
: undefined,
packetsDiscarded: report.packetsDiscarded,
insertedSamplesForDeceleration: report.insertedSamplesForDeceleration,
removedSamplesForAcceleration: report.removedSamplesForAcceleration,
jitterBufferEmittedCount: report.jitterBufferEmittedCount,
concealedSamples: report.concealedSamples,
silentConcealedSamples: report.silentConcealedSamples,
@@ -477,7 +503,9 @@ function buildPerTrackStat(args: {
nackCount: report.nackCount,
pliCount: report.pliCount,
firCount: report.firCount,
...(isOutbound ? buildOutboundTrackExtras(report) : buildInboundTrackExtras(report, decoderAcceleration)),
...(isOutbound
? buildOutboundTrackExtras(report, mediaSource)
: buildInboundTrackExtras(report, decoderAcceleration)),
};
}
@@ -202,9 +202,14 @@ export class VoiceInputGraph {
return this.configureRun;
}
loadGate(): Promise<void> {
this.gateLoad ??= this.installGate();
return this.gateLoad;
}
private async drainConfigure(): Promise<void> {
try {
this.gateLoad ??= this.installGate();
void this.loadGate();
while (!this.disposed && this.configureDirty) {
this.configureDirty = false;
const config = this.resolveConfig();
@@ -488,6 +493,8 @@ export class VoiceInputTrackProcessor implements TrackProcessor<Track.Kind.Audio
() => this.reportRuntime(),
);
this.graph.setSourceNode(opts.track, acquired.source);
await this.graph.loadGate();
if (this.state !== 'idle') throw new Error(`Voice input processor cannot start while ${this.state}`);
this.destination = context.createMediaStreamDestination();
this.destination.channelCount = this.channelCount;
this.destination.channelCountMode = 'explicit';