Add root-cause diagnostics for authenticated launch and WebRTC audio uplink.
Log anon flag presence in auth launch URL and record outbound WebRTC sender stats (delta bytes/packets) around each TTS playback to prove whether audio is transmitted to the meeting. Made-with: Cursor
This commit is contained in:
parent
7aaaf10b3b
commit
77ca96e23c
3 changed files with 67 additions and 3 deletions
|
|
@ -63,12 +63,19 @@ export class AudioCaptureProcedure {
|
||||||
(window as any).__audioCaptureChunks = [] as any[];
|
(window as any).__audioCaptureChunks = [] as any[];
|
||||||
(window as any).__audioCaptureProcessors = {} as Record<string, any>;
|
(window as any).__audioCaptureProcessors = {} as Record<string, any>;
|
||||||
(window as any).__audioCaptureContexts = {} as Record<string, AudioContext>;
|
(window as any).__audioCaptureContexts = {} as Record<string, AudioContext>;
|
||||||
|
(window as any).__audioCapturePeerConnections = [] as RTCPeerConnection[];
|
||||||
|
|
||||||
const OrigRTC = window.RTCPeerConnection;
|
const OrigRTC = window.RTCPeerConnection;
|
||||||
|
|
||||||
// @ts-ignore — wrapping constructor
|
// @ts-ignore — wrapping constructor
|
||||||
window.RTCPeerConnection = function (this: RTCPeerConnection, ...args: any[]) {
|
window.RTCPeerConnection = function (this: RTCPeerConnection, ...args: any[]) {
|
||||||
const pc = new OrigRTC(...args);
|
const pc = new OrigRTC(...args);
|
||||||
|
try {
|
||||||
|
const pcs = (window as any).__audioCapturePeerConnections as RTCPeerConnection[];
|
||||||
|
pcs.push(pc);
|
||||||
|
} catch {
|
||||||
|
// ignore
|
||||||
|
}
|
||||||
|
|
||||||
pc.addEventListener('track', (event: RTCTrackEvent) => {
|
pc.addEventListener('track', (event: RTCTrackEvent) => {
|
||||||
if (event.track.kind !== 'audio') return;
|
if (event.track.kind !== 'audio') return;
|
||||||
|
|
|
||||||
|
|
@ -207,14 +207,56 @@ export class AudioProcedure {
|
||||||
this._logger.info(`Playing audio (format: ${format}, size: ${audioData.length} bytes base64)`);
|
this._logger.info(`Playing audio (format: ${format}, size: ${audioData.length} bytes base64)`);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await this._page.evaluate(async ({ audioData, format }) => {
|
const playbackDiag = await this._page.evaluate(async ({ audioData, format }) => {
|
||||||
const ctx = (window as any).__ttsAudioContext as AudioContext;
|
const ctx = (window as any).__ttsAudioContext as AudioContext;
|
||||||
const streamDest = (window as any).__ttsStreamDest as MediaStreamAudioDestinationNode;
|
const streamDest = (window as any).__ttsStreamDest as MediaStreamAudioDestinationNode;
|
||||||
|
const pcs = ((window as any).__audioCapturePeerConnections || []) as RTCPeerConnection[];
|
||||||
|
|
||||||
if (!ctx || !streamDest) {
|
if (!ctx || !streamDest) {
|
||||||
throw new Error('Audio context not initialized');
|
throw new Error('Audio context not initialized');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const collectWebRtcAudioStats = async () => {
|
||||||
|
let senderCount = 0;
|
||||||
|
let bytesSentTotal = 0;
|
||||||
|
let packetsSentTotal = 0;
|
||||||
|
const tracks: Array<Record<string, any>> = [];
|
||||||
|
|
||||||
|
for (const pc of pcs) {
|
||||||
|
const senders = pc.getSenders?.() || [];
|
||||||
|
for (const sender of senders) {
|
||||||
|
if (!sender?.track || sender.track.kind !== 'audio') continue;
|
||||||
|
senderCount++;
|
||||||
|
tracks.push({
|
||||||
|
id: sender.track.id,
|
||||||
|
label: sender.track.label,
|
||||||
|
enabled: sender.track.enabled,
|
||||||
|
muted: sender.track.muted,
|
||||||
|
readyState: sender.track.readyState,
|
||||||
|
});
|
||||||
|
try {
|
||||||
|
const stats = await sender.getStats();
|
||||||
|
stats.forEach((report) => {
|
||||||
|
if (report.type === 'outbound-rtp' && (report as any).kind === 'audio') {
|
||||||
|
bytesSentTotal += Number((report as any).bytesSent || 0);
|
||||||
|
packetsSentTotal += Number((report as any).packetsSent || 0);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} catch {
|
||||||
|
// ignore stats errors per sender
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
pcs: pcs.length,
|
||||||
|
senderCount,
|
||||||
|
bytesSentTotal,
|
||||||
|
packetsSentTotal,
|
||||||
|
tracks,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
// Resume context if suspended
|
// Resume context if suspended
|
||||||
if (ctx.state === 'suspended') {
|
if (ctx.state === 'suspended') {
|
||||||
await ctx.resume();
|
await ctx.resume();
|
||||||
|
|
@ -242,24 +284,38 @@ export class AudioProcedure {
|
||||||
audioBuffer = await ctx.decodeAudioData(bytes.buffer.slice(0));
|
audioBuffer = await ctx.decodeAudioData(bytes.buffer.slice(0));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const before = await collectWebRtcAudioStats();
|
||||||
|
|
||||||
// Play through the MediaStreamDestination -> Teams mic input
|
// Play through the MediaStreamDestination -> Teams mic input
|
||||||
const source = ctx.createBufferSource();
|
const source = ctx.createBufferSource();
|
||||||
source.buffer = audioBuffer;
|
source.buffer = audioBuffer;
|
||||||
source.connect(streamDest);
|
source.connect(streamDest);
|
||||||
source.start(0);
|
source.start(0);
|
||||||
|
|
||||||
return new Promise<void>((resolve) => {
|
return new Promise((resolve) => {
|
||||||
source.onended = () => {
|
source.onended = () => {
|
||||||
try {
|
try {
|
||||||
source.disconnect();
|
source.disconnect();
|
||||||
} catch {
|
} catch {
|
||||||
// already disconnected
|
// already disconnected
|
||||||
}
|
}
|
||||||
resolve();
|
resolve(null);
|
||||||
|
};
|
||||||
|
}).then(async () => {
|
||||||
|
const after = await collectWebRtcAudioStats();
|
||||||
|
return {
|
||||||
|
before,
|
||||||
|
after,
|
||||||
|
deltaBytes: after.bytesSentTotal - before.bytesSentTotal,
|
||||||
|
deltaPackets: after.packetsSentTotal - before.packetsSentTotal,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
}, { audioData, format });
|
}, { audioData, format });
|
||||||
|
|
||||||
|
this._logger.info(
|
||||||
|
`TTS WebRTC diagnostics: pcs=${playbackDiag?.after?.pcs ?? 0}, senders=${playbackDiag?.after?.senderCount ?? 0}, ` +
|
||||||
|
`deltaBytes=${playbackDiag?.deltaBytes ?? 0}, deltaPackets=${playbackDiag?.deltaPackets ?? 0}`,
|
||||||
|
);
|
||||||
this._logger.info('Audio playback completed');
|
this._logger.info('Audio playback completed');
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
this._logger.error('Error playing audio:', error);
|
this._logger.error('Error playing audio:', error);
|
||||||
|
|
|
||||||
|
|
@ -302,6 +302,7 @@ export class BotOrchestrator {
|
||||||
} catch { /* keep as-is */ }
|
} catch { /* keep as-is */ }
|
||||||
|
|
||||||
this._logger.info(`STEP 4: navigating to launch URL: ${launchUrl.substring(0, 120)}...`);
|
this._logger.info(`STEP 4: navigating to launch URL: ${launchUrl.substring(0, 120)}...`);
|
||||||
|
this._logger.info(`STEP 4: launch URL contains anon=true? ${launchUrl.includes('anon=true')}`);
|
||||||
await this._page!.goto(launchUrl, {
|
await this._page!.goto(launchUrl, {
|
||||||
waitUntil: 'domcontentloaded',
|
waitUntil: 'domcontentloaded',
|
||||||
timeout: 30000,
|
timeout: 30000,
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue