diff --git a/fluxer_app/src/features/voice/engine/VoiceInputAudioContext.ts b/fluxer_app/src/features/voice/engine/VoiceInputAudioContext.ts index e6ce320ff..abb2a6fdc 100644 --- a/fluxer_app/src/features/voice/engine/VoiceInputAudioContext.ts +++ b/fluxer_app/src/features/voice/engine/VoiceInputAudioContext.ts @@ -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, diff --git a/fluxer_app/src/features/voice/engine/v2/VoiceEngineV2AppStatsHostAdapter.ts b/fluxer_app/src/features/voice/engine/v2/VoiceEngineV2AppStatsHostAdapter.ts index d9875cc4c..e0ed40dfb 100644 --- a/fluxer_app/src/features/voice/engine/v2/VoiceEngineV2AppStatsHostAdapter.ts +++ b/fluxer_app/src/features/voice/engine/v2/VoiceEngineV2AppStatsHostAdapter.ts @@ -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 { +function buildOutboundTrackExtras( + report: RTCStatsEntry, + mediaSource: RTCStatsEntry | undefined, +): Partial { 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)), }; } diff --git a/fluxer_app/src/features/voice/utils/VoiceInputProcessor.ts b/fluxer_app/src/features/voice/utils/VoiceInputProcessor.ts index 2e1da8a5b..83d846543 100644 --- a/fluxer_app/src/features/voice/utils/VoiceInputProcessor.ts +++ b/fluxer_app/src/features/voice/utils/VoiceInputProcessor.ts @@ -202,9 +202,14 @@ export class VoiceInputGraph { return this.configureRun; } + loadGate(): Promise { + this.gateLoad ??= this.installGate(); + return this.gateLoad; + } + private async drainConfigure(): Promise { 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 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';