From a483aa6def60840a3acb1f5d4441c4104fa4f101 Mon Sep 17 00:00:00 2001 From: ValueOn AG Date: Mon, 16 Feb 2026 09:02:42 +0100 Subject: [PATCH] fix: auth join via direct URL (skip launcher), sequential audio queue, improve AI prompt (less floskel, stricter response rules) Co-authored-by: Cursor --- src/bot/audioProcedure.ts | 40 ++++++++++++++++++++++++++++++++++-- src/bot/joinProcedure.ts | 18 ++++++++++++++++ src/bot/orchestrator.ts | 43 +++++++++++++++++++++++---------------- 3 files changed, 82 insertions(+), 19 deletions(-) diff --git a/src/bot/audioProcedure.ts b/src/bot/audioProcedure.ts index 5b242f9..8f32732 100644 --- a/src/bot/audioProcedure.ts +++ b/src/bot/audioProcedure.ts @@ -20,6 +20,8 @@ export class AudioProcedure { private _logger: Logger; private _audioContext: boolean = false; private _initScriptInjected: boolean = false; + private _audioQueue: Array<{ audioData: string; format: 'mp3' | 'wav' | 'pcm' }> = []; + private _isPlaying: boolean = false; constructor(page: Page, logger: Logger) { this._page = page; @@ -113,13 +115,47 @@ export class AudioProcedure { } /** - * Play audio in the browser. - * Audio is piped into the MediaStreamDestination that Teams uses as mic input. + * Queue audio for sequential playback. + * Audio is never played in parallel -- each clip waits for the previous one to finish. * * @param audioData Base64 encoded audio data * @param format Audio format (mp3, wav, pcm) */ async playAudio(audioData: string, format: 'mp3' | 'wav' | 'pcm'): Promise { + // Add to queue + this._audioQueue.push({ audioData, format }); + this._logger.info(`Audio queued (queue size: ${this._audioQueue.length}, playing: ${this._isPlaying})`); + + // If not currently playing, start processing the queue + if (!this._isPlaying) { + await this._processAudioQueue(); + } + } + + /** + * Process the audio queue sequentially. + */ + private async _processAudioQueue(): Promise { + if (this._isPlaying) return; + this._isPlaying = true; + + while (this._audioQueue.length > 0) { + const item = this._audioQueue.shift()!; + try { + await this._playAudioInternal(item.audioData, item.format); + } catch (error) { + this._logger.error('Error playing queued audio:', error); + } + } + + this._isPlaying = false; + } + + /** + * Internal: Play audio in the browser (single clip, no queuing). + * Audio is piped into the MediaStreamDestination that Teams uses as mic input. + */ + private async _playAudioInternal(audioData: string, format: 'mp3' | 'wav' | 'pcm'): Promise { if (!this._audioContext) { await this.initialize(); } diff --git a/src/bot/joinProcedure.ts b/src/bot/joinProcedure.ts index 7a64fb6..5d59a7d 100644 --- a/src/bot/joinProcedure.ts +++ b/src/bot/joinProcedure.ts @@ -54,6 +54,24 @@ export class JoinProcedure { await this._handleLauncherDialog(); } + /** + * Check if a launcher dialog is present and handle it. + * Used for authenticated joins where we navigate directly to the meeting URL + * but Teams may still show the launcher. + */ + async handleLauncherIfPresent(): Promise { + try { + const launcherButton = await this._page.$('button[data-tid="joinOnWeb"]'); + if (launcherButton) { + this._logger.info('Launcher dialog found after direct navigation, clicking "Continue on this browser"'); + await launcherButton.click(); + await this._page.waitForTimeout(2000); + } + } catch { + // No launcher - that's fine for authenticated joins + } + } + /** * Handle the launcher dialog that asks how to join. * Primary selector: button[data-tid="joinOnWeb"] (confirmed working in Recall.ai). diff --git a/src/bot/orchestrator.ts b/src/bot/orchestrator.ts index 1e20f90..389bf96 100644 --- a/src/bot/orchestrator.ts +++ b/src/bot/orchestrator.ts @@ -168,25 +168,34 @@ export class BotOrchestrator { this._setState('navigating'); - // Navigate to meeting and handle launcher - await this._joinProcedure.startMeetingLauncherFlow(this._meetingUrl); + if (authenticate) { + // AUTHENTICATED JOIN: Navigate directly to the original meeting URL + // within the Teams v2 web app context. The launcher (/dl/launcher/) always + // redirects to the "light-meetings" anonymous experience regardless of auth. + // Instead, navigate directly to the original meeting URL -- Teams v2 will + // recognize the auth session and show the authenticated pre-join screen. + this._logger.info(`Authenticated: navigating directly to meeting URL: ${this._meetingUrl}`); + await this._page!.goto(this._meetingUrl, { + waitUntil: 'domcontentloaded', + timeout: 30000, + }); - // After navigation, check if Teams put us on the anonymous page despite auth - if (authenticate && this._page) { - const currentUrl = this._page.url(); - if (currentUrl.includes('anon=true') || currentUrl.includes('light-meetings/launch')) { - this._logger.warn(`Teams redirected to anonymous mode despite auth. URL: ${currentUrl}`); - // Strip anon params and re-navigate - const cleanUrl = currentUrl - .replace(/[&?]anon=true/gi, '') - .replace(/%26anon%3Dtrue/gi, ''); - this._logger.info(`Re-navigating without anon: ${cleanUrl}`); - await this._page.goto(cleanUrl, { - waitUntil: 'domcontentloaded', - timeout: 30000, - }); - await this._page.waitForTimeout(3000); + // Teams may show the launcher dialog even for direct URLs -- handle it + await this._joinProcedure.handleLauncherIfPresent(); + + // Wait for the pre-join page to stabilize + await this._page!.waitForTimeout(3000); + + // Verify we're on an authenticated page (no "Type your name" input) + const pageText = await this._page!.evaluate(() => document.body?.innerText?.substring(0, 500) || ''); + if (pageText.includes('Enter the name') || pageText.includes('Type your name')) { + this._logger.warn('Still on anonymous page after auth navigation - auth session may not have transferred'); + } else { + this._logger.info('On authenticated pre-join page (no name input required)'); } + } else { + // ANONYMOUS JOIN: Use the launcher flow (resolves URL, adds anon params) + await this._joinProcedure.startMeetingLauncherFlow(this._meetingUrl); } // Set virtual background if configured (must be done on pre-join screen, before "Join now")