diff --git a/src/bot/audioCaptureProcedure.ts b/src/bot/audioCaptureProcedure.ts index b1e35d0..819b6b5 100644 --- a/src/bot/audioCaptureProcedure.ts +++ b/src/bot/audioCaptureProcedure.ts @@ -63,12 +63,19 @@ export class AudioCaptureProcedure { (window as any).__audioCaptureChunks = [] as any[]; (window as any).__audioCaptureProcessors = {} as Record; (window as any).__audioCaptureContexts = {} as Record; + (window as any).__audioCapturePeerConnections = [] as RTCPeerConnection[]; const OrigRTC = window.RTCPeerConnection; // @ts-ignore — wrapping constructor window.RTCPeerConnection = function (this: RTCPeerConnection, ...args: any[]) { const pc = new OrigRTC(...args); + try { + const pcs = (window as any).__audioCapturePeerConnections as RTCPeerConnection[]; + pcs.push(pc); + } catch { + // ignore + } pc.addEventListener('track', (event: RTCTrackEvent) => { if (event.track.kind !== 'audio') return; diff --git a/src/bot/audioProcedure.ts b/src/bot/audioProcedure.ts index 53bfb74..038bd18 100644 --- a/src/bot/audioProcedure.ts +++ b/src/bot/audioProcedure.ts @@ -207,14 +207,56 @@ export class AudioProcedure { this._logger.info(`Playing audio (format: ${format}, size: ${audioData.length} bytes base64)`); 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 streamDest = (window as any).__ttsStreamDest as MediaStreamAudioDestinationNode; + const pcs = ((window as any).__audioCapturePeerConnections || []) as RTCPeerConnection[]; if (!ctx || !streamDest) { throw new Error('Audio context not initialized'); } + const collectWebRtcAudioStats = async () => { + let senderCount = 0; + let bytesSentTotal = 0; + let packetsSentTotal = 0; + const tracks: Array> = []; + + 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 if (ctx.state === 'suspended') { await ctx.resume(); @@ -242,24 +284,38 @@ export class AudioProcedure { audioBuffer = await ctx.decodeAudioData(bytes.buffer.slice(0)); } + const before = await collectWebRtcAudioStats(); + // Play through the MediaStreamDestination -> Teams mic input const source = ctx.createBufferSource(); source.buffer = audioBuffer; source.connect(streamDest); source.start(0); - return new Promise((resolve) => { + return new Promise((resolve) => { source.onended = () => { try { source.disconnect(); } catch { // 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 }); + 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'); } catch (error) { this._logger.error('Error playing audio:', error); diff --git a/src/bot/orchestrator.ts b/src/bot/orchestrator.ts index 763db62..61a209f 100644 --- a/src/bot/orchestrator.ts +++ b/src/bot/orchestrator.ts @@ -302,6 +302,7 @@ export class BotOrchestrator { } catch { /* keep as-is */ } 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, { waitUntil: 'domcontentloaded', timeout: 30000,