From 77ca96e23cd58b91350849473d07037a21fd3fa2 Mon Sep 17 00:00:00 2001
From: ValueOn AG
Date: Fri, 27 Feb 2026 08:23:24 +0100
Subject: [PATCH] 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
---
src/bot/audioCaptureProcedure.ts | 7 ++++
src/bot/audioProcedure.ts | 62 ++++++++++++++++++++++++++++++--
src/bot/orchestrator.ts | 1 +
3 files changed, 67 insertions(+), 3 deletions(-)
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,