mirror of
https://github.com/fluxerapp/fluxer
synced 2026-10-07 19:22:14 +09:00
fix(voice): keep the mic graph on the real audio clock (#3197)
This commit is contained in:
@@ -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';
|
||||
|
||||
Reference in New Issue
Block a user