fix: captions enabling for authenticated Teams UI - add submenu path, reduce timeouts, add debug logging
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
parent
04abaf9402
commit
50f1f1977e
1 changed files with 193 additions and 54 deletions
|
|
@ -58,37 +58,24 @@ export class CaptionsProcedure {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Open the "More actions" (...) menu in the call controls.
|
* 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<void> {
|
private async _openMoreMenu(): Promise<void> {
|
||||||
// Primary selector - confirmed by Recall.ai (Jan 2025)
|
const allSelectors = [
|
||||||
const primarySelector = 'button[id="callingButtons-showMoreBtn"]';
|
'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 = [
|
|
||||||
'[data-tid="callingButtons-showMoreBtn"]',
|
'[data-tid="callingButtons-showMoreBtn"]',
|
||||||
'button[aria-label*="More actions"]',
|
'button[aria-label*="More actions"]',
|
||||||
'button[aria-label*="More"]',
|
'button[aria-label*="More"]',
|
||||||
'[data-tid="more-button"]',
|
'[data-tid="more-button"]',
|
||||||
];
|
];
|
||||||
|
|
||||||
for (const selector of fallbackSelectors) {
|
for (const selector of allSelectors) {
|
||||||
try {
|
try {
|
||||||
const button = await this._page.$(selector);
|
const button = await this._page.$(selector);
|
||||||
if (button) {
|
if (button) {
|
||||||
await button.click();
|
await button.click();
|
||||||
|
this._logger.info(`Clicked "More" button: ${selector}`);
|
||||||
await this._page.waitForTimeout(1000);
|
await this._page.waitForTimeout(1000);
|
||||||
this._logger.info(`Opened more menu (fallback: ${selector})`);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
} catch {
|
} 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');
|
throw new Error('Could not find More actions menu');
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Click the captions button in the 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<void> {
|
private async _clickEnableCaptions(): Promise<void> {
|
||||||
// Primary selector - confirmed by Recall.ai (Jan 2025)
|
// Log visible menu items for debugging
|
||||||
const primarySelector = 'div[id="closed-captions-button"]';
|
await this._logVisibleMenuItems();
|
||||||
|
|
||||||
try {
|
// Strategy 1: Direct captions button (anonymous/light-meetings UI)
|
||||||
await this._page.waitForSelector(primarySelector, { timeout: 120000 });
|
const directSelectors = [
|
||||||
this._logger.info('Found "Captions" button');
|
'div[id="closed-captions-button"]',
|
||||||
await this._page.click(primarySelector);
|
'[data-tid="closed-captions-button"]',
|
||||||
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")',
|
|
||||||
'[data-tid="captions-toggle"]',
|
'[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 {
|
try {
|
||||||
const button = await this._page.$(selector);
|
const button = await this._page.$(selector);
|
||||||
if (button) {
|
if (button) {
|
||||||
await button.click();
|
await button.click();
|
||||||
|
this._logger.info(`Clicked direct captions button: ${selector}`);
|
||||||
await this._page.waitForTimeout(1000);
|
await this._page.waitForTimeout(1000);
|
||||||
this._logger.info(`Clicked enable captions (fallback: ${selector})`);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
|
|
@ -141,26 +128,165 @@ 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');
|
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<void> {
|
||||||
|
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.
|
* 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<void> {
|
private async _waitForCaptionsContainer(): Promise<void> {
|
||||||
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"]',
|
||||||
|
];
|
||||||
|
|
||||||
|
for (const selector of containerSelectors) {
|
||||||
try {
|
try {
|
||||||
await this._page.waitForSelector(containerSelector, { timeout: 120000 });
|
await this._page.waitForSelector(selector, { timeout: 15000 });
|
||||||
this._logger.info('Found captions container');
|
this._logger.info(`Found captions container: ${selector}`);
|
||||||
|
return;
|
||||||
} catch {
|
} catch {
|
||||||
this._logger.warn('Could not find captions container - captions may not have enabled');
|
// Try next
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this._logger.warn('Could not find captions container - captions may not have enabled or may use a different selector');
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Set the spoken language for captions.
|
* Set the spoken language for captions.
|
||||||
*
|
*
|
||||||
|
|
@ -553,11 +679,24 @@ export class CaptionsProcedure {
|
||||||
});
|
});
|
||||||
|
|
||||||
// Verify captions container is present
|
// Verify captions container is present
|
||||||
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"]',
|
||||||
|
];
|
||||||
|
let containerFound = false;
|
||||||
|
for (const sel of containerSelectors) {
|
||||||
try {
|
try {
|
||||||
await this._page.waitForSelector(containerSelector, { timeout: 120000 });
|
await this._page.waitForSelector(sel, { timeout: 10000 });
|
||||||
|
containerFound = true;
|
||||||
|
this._logger.info(`Captions container found: ${sel}`);
|
||||||
|
break;
|
||||||
} catch {
|
} catch {
|
||||||
this._logger.warn('Captions container not found, subscribing anyway');
|
// Try next
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!containerFound) {
|
||||||
|
this._logger.warn('Captions container not found with known selectors, subscribing anyway');
|
||||||
}
|
}
|
||||||
|
|
||||||
this._logger.info('Setting up MutationObserver for captions...');
|
this._logger.info('Setting up MutationObserver for captions...');
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue