From 50f1f1977e87ccae612bc6b81e3b58362e097b06 Mon Sep 17 00:00:00 2001
From: ValueOn AG
Date: Tue, 17 Feb 2026 20:14:51 +0100
Subject: [PATCH] fix: captions enabling for authenticated Teams UI - add
submenu path, reduce timeouts, add debug logging
Co-authored-by: Cursor
---
src/bot/captionsProcedure.ts | 247 +++++++++++++++++++++++++++--------
1 file changed, 193 insertions(+), 54 deletions(-)
diff --git a/src/bot/captionsProcedure.ts b/src/bot/captionsProcedure.ts
index 20c58b9..5f60c6a 100644
--- a/src/bot/captionsProcedure.ts
+++ b/src/bot/captionsProcedure.ts
@@ -58,37 +58,24 @@ export class CaptionsProcedure {
/**
* Open the "More actions" (...) menu in the call controls.
- * Primary selector: button[id="callingButtons-showMoreBtn"] (confirmed by Recall.ai).
+ * Works for both anonymous (light-meetings) and authenticated (full Teams) UI.
*/
private async _openMoreMenu(): Promise {
- // Primary selector - confirmed by Recall.ai (Jan 2025)
- const primarySelector = 'button[id="callingButtons-showMoreBtn"]';
-
- try {
- await this._page.waitForSelector(primarySelector, { timeout: 120000 });
- this._logger.info('Found "More" button');
- await this._page.click(primarySelector);
- this._logger.info('Clicked "More" button');
- return;
- } catch {
- this._logger.info('Primary more button selector not found, trying fallbacks...');
- }
-
- // Fallback selectors
- const fallbackSelectors = [
+ const allSelectors = [
+ 'button[id="callingButtons-showMoreBtn"]',
'[data-tid="callingButtons-showMoreBtn"]',
'button[aria-label*="More actions"]',
'button[aria-label*="More"]',
'[data-tid="more-button"]',
];
- for (const selector of fallbackSelectors) {
+ for (const selector of allSelectors) {
try {
const button = await this._page.$(selector);
if (button) {
await button.click();
+ this._logger.info(`Clicked "More" button: ${selector}`);
await this._page.waitForTimeout(1000);
- this._logger.info(`Opened more menu (fallback: ${selector})`);
return;
}
} catch {
@@ -96,44 +83,44 @@ export class CaptionsProcedure {
}
}
+ // Last resort: wait for the primary selector with a short timeout
+ try {
+ await this._page.waitForSelector(allSelectors[0], { timeout: 10000 });
+ await this._page.click(allSelectors[0]);
+ this._logger.info('Found "More" button (after wait)');
+ await this._page.waitForTimeout(1000);
+ return;
+ } catch {
+ // Continue
+ }
+
throw new Error('Could not find More actions menu');
}
/**
* Click the captions button in the menu.
- * Primary selector: div[id="closed-captions-button"] (confirmed by Recall.ai).
+ * Handles two UI variants:
+ * - Anonymous (light-meetings): direct div[id="closed-captions-button"]
+ * - Authenticated (full Teams): submenu "Captions & transcripts" → "Turn on live captions"
*/
private async _clickEnableCaptions(): Promise {
- // Primary selector - confirmed by Recall.ai (Jan 2025)
- const primarySelector = 'div[id="closed-captions-button"]';
+ // Log visible menu items for debugging
+ await this._logVisibleMenuItems();
- try {
- await this._page.waitForSelector(primarySelector, { timeout: 120000 });
- this._logger.info('Found "Captions" button');
- await this._page.click(primarySelector);
- this._logger.info('Clicked "Captions" button');
- return;
- } catch {
- this._logger.info('Primary captions button selector not found, trying fallbacks...');
- }
-
- // Fallback selectors
- const fallbackSelectors = [
- 'button:has-text("Turn on live captions")',
- 'button:has-text("Live captions")',
+ // Strategy 1: Direct captions button (anonymous/light-meetings UI)
+ const directSelectors = [
+ 'div[id="closed-captions-button"]',
+ '[data-tid="closed-captions-button"]',
'[data-tid="captions-toggle"]',
- 'button[aria-label*="captions" i]',
- '[role="menuitem"]:has-text("captions")',
- '[role="menuitemcheckbox"]:has-text("captions")',
];
- for (const selector of fallbackSelectors) {
+ for (const selector of directSelectors) {
try {
const button = await this._page.$(selector);
if (button) {
await button.click();
+ this._logger.info(`Clicked direct captions button: ${selector}`);
await this._page.waitForTimeout(1000);
- this._logger.info(`Clicked enable captions (fallback: ${selector})`);
return;
}
} catch {
@@ -141,24 +128,163 @@ export class CaptionsProcedure {
}
}
- // Try clicking away to close menu if captions not found
+ // Strategy 2: Authenticated Teams UI — "Captions & transcripts" submenu first
+ const submenuSelectors = [
+ '[data-tid="captions-and-transcripts-button"]',
+ '[role="menuitem"]:has-text("Captions & transcripts")',
+ '[role="menuitem"]:has-text("Captions and transcripts")',
+ '[role="menuitem"]:has-text("Untertitel und Transkripte")',
+ '[role="menuitem"]:has-text("Untertitel")',
+ 'button:has-text("Captions & transcripts")',
+ 'button:has-text("Captions and transcripts")',
+ ];
+
+ for (const selector of submenuSelectors) {
+ try {
+ const item = await this._page.$(selector);
+ if (item) {
+ await item.click();
+ this._logger.info(`Clicked captions submenu: ${selector}`);
+ await this._page.waitForTimeout(1500);
+
+ // Now look for "Turn on live captions" inside the submenu/panel
+ const enableSelectors = [
+ 'button:has-text("Turn on live captions")',
+ 'button:has-text("Live captions")',
+ 'button:has-text("Live-Untertitel aktivieren")',
+ 'button:has-text("Liveuntertitel")',
+ '[role="menuitem"]:has-text("Turn on live captions")',
+ '[role="menuitem"]:has-text("Live captions")',
+ '[role="menuitemcheckbox"]:has-text("captions")',
+ '[data-tid="toggle-captions"]',
+ ];
+
+ for (const enableSel of enableSelectors) {
+ try {
+ const enableBtn = await this._page.$(enableSel);
+ if (enableBtn) {
+ await enableBtn.click();
+ this._logger.info(`Clicked enable captions: ${enableSel}`);
+ await this._page.waitForTimeout(1000);
+ return;
+ }
+ } catch {
+ // Continue
+ }
+ }
+
+ this._logger.info('Opened captions submenu but could not find enable button');
+ break;
+ }
+ } catch {
+ // Continue
+ }
+ }
+
+ // Strategy 3: Generic text-based fallbacks
+ const textFallbacks = [
+ 'button:has-text("Turn on live captions")',
+ 'button:has-text("Live captions")',
+ 'button[aria-label*="captions" i]',
+ '[role="menuitem"]:has-text("captions")',
+ '[role="menuitemcheckbox"]:has-text("captions")',
+ ];
+
+ for (const selector of textFallbacks) {
+ try {
+ const button = await this._page.$(selector);
+ if (button) {
+ await button.click();
+ this._logger.info(`Clicked captions (text fallback): ${selector}`);
+ await this._page.waitForTimeout(1000);
+ return;
+ }
+ } catch {
+ // Continue
+ }
+ }
+
+ // Strategy 4: DOM scan — find any element mentioning "caption" in the open menu
+ const found = await this._page.evaluate(() => {
+ const keywords = ['caption', 'captions', 'untertitel', 'live caption'];
+ const candidates = document.querySelectorAll(
+ '[role="menuitem"], [role="menuitemcheckbox"], [role="menuitemradio"], button, li, div[role="option"]'
+ );
+ const results: string[] = [];
+ for (let i = 0; i < candidates.length; i++) {
+ const el = candidates[i] as HTMLElement;
+ const text = el.innerText?.toLowerCase()?.trim() || '';
+ if (text && keywords.some(kw => text.includes(kw))) {
+ results.push(text.substring(0, 60));
+ el.click();
+ return { clicked: text.substring(0, 60), allMatches: results };
+ }
+ }
+ return { clicked: null, allMatches: results };
+ });
+
+ if (found.clicked) {
+ this._logger.info(`Clicked captions via DOM scan: "${found.clicked}"`);
+ await this._page.waitForTimeout(1500);
+
+ // Check if this opened a submenu — look for "Turn on" or "enable" inside
+ const turnOnBtn = await this._page.$('button:has-text("Turn on"), [role="menuitem"]:has-text("Turn on")');
+ if (turnOnBtn) {
+ await turnOnBtn.click();
+ this._logger.info('Clicked "Turn on" in captions submenu');
+ await this._page.waitForTimeout(1000);
+ }
+ return;
+ }
+
+ // Nothing found
await this._page.keyboard.press('Escape');
- this._logger.warn('Could not find captions option - may already be enabled or not available');
+ this._logger.warn(`Could not find captions option. DOM scan matches: ${JSON.stringify(found.allMatches)}`);
+ }
+
+ /**
+ * Log visible menu items for debugging when captions button is not found.
+ */
+ private async _logVisibleMenuItems(): Promise {
+ try {
+ const menuItems = await this._page.evaluate(() => {
+ const items = document.querySelectorAll(
+ '[role="menuitem"], [role="menuitemcheckbox"], [role="menuitemradio"]'
+ );
+ return Array.from(items).map(el => {
+ const text = (el as HTMLElement).innerText?.trim()?.substring(0, 50) || '';
+ const tid = el.getAttribute('data-tid') || '';
+ const id = el.id || '';
+ return `[${tid || id || 'no-id'}] ${text}`;
+ }).filter(t => t.length > 5);
+ });
+ this._logger.info(`Visible menu items (${menuItems.length}): ${JSON.stringify(menuItems)}`);
+ } catch {
+ // Not critical
+ }
}
/**
* Wait for the captions container to become visible after enabling.
- * Primary selector: div[data-tid="closed-caption-renderer-wrapper"] (confirmed by Recall.ai).
*/
private async _waitForCaptionsContainer(): Promise {
- const containerSelector = 'div[data-tid="closed-caption-renderer-wrapper"]';
+ const containerSelectors = [
+ 'div[data-tid="closed-caption-renderer-wrapper"]',
+ 'div[data-tid="live-captions-renderer"]',
+ '[data-tid="caption-area"]',
+ ];
- try {
- await this._page.waitForSelector(containerSelector, { timeout: 120000 });
- this._logger.info('Found captions container');
- } catch {
- this._logger.warn('Could not find captions container - captions may not have enabled');
+ for (const selector of containerSelectors) {
+ try {
+ await this._page.waitForSelector(selector, { timeout: 15000 });
+ this._logger.info(`Found captions container: ${selector}`);
+ return;
+ } catch {
+ // Try next
+ }
}
+
+ this._logger.warn('Could not find captions container - captions may not have enabled or may use a different selector');
}
/**
@@ -553,11 +679,24 @@ export class CaptionsProcedure {
});
// Verify captions container is present
- const containerSelector = 'div[data-tid="closed-caption-renderer-wrapper"]';
- try {
- await this._page.waitForSelector(containerSelector, { timeout: 120000 });
- } catch {
- this._logger.warn('Captions container not found, subscribing anyway');
+ const containerSelectors = [
+ 'div[data-tid="closed-caption-renderer-wrapper"]',
+ 'div[data-tid="live-captions-renderer"]',
+ '[data-tid="caption-area"]',
+ ];
+ let containerFound = false;
+ for (const sel of containerSelectors) {
+ try {
+ await this._page.waitForSelector(sel, { timeout: 10000 });
+ containerFound = true;
+ this._logger.info(`Captions container found: ${sel}`);
+ break;
+ } catch {
+ // Try next
+ }
+ }
+ if (!containerFound) {
+ this._logger.warn('Captions container not found with known selectors, subscribing anyway');
}
this._logger.info('Setting up MutationObserver for captions...');