Compare commits

...

14 Commits

Author SHA1 Message Date
Hristo Terezov
4a30a7ef25 feat(pip): add debug logging for better PiP flow observability
Add logger.debug calls throughout the PiP (Picture-in-Picture) feature
to make it easier to trace the PiP lifecycle when investigating issues.
Covers API event dispatching, action invocations with state snapshots,
requestPictureInPicture success/failure paths, and Electron namespace
mutations.
2026-03-06 10:57:39 -06:00
Jaya Allamsetty
28a816665d fix(video-layout) Fixes auto-pinning of SS in large calls. (#16992)
* fix(video-layout) Fixes auto-pinning of SS in large calls.
In calls with 50+ participants, isTopPanelEnabled returns true. This causes isStageFilmstripAvailable(state) (called with no minimum participant count from getPinnedParticipant) to return true even when activeParticipants is empty. When getPinnedParticipant takes that stage filmstrip path, it reads from activeParticipants.find(p => p.pinned) — which is always
  empty for an auto-pinned screenshare.
Added an explicit check in shouldDisplayTileView's auto-mode logic: when there are active screenshares AND the top panel is enabled AND auto-pin is active AND Follow Me isn't controlling the layout, exit tile view. The guards on getAutoPinSetting() and !isFollowMeActive(state) mirror the same conditions checked everywhere else before auto-pinning fires, ensuring
  consistent behavior.

* squash: Fixes linter error
2026-02-27 12:52:58 -05:00
Jaya Allamsetty
09648137fd fix(filmstrip) Fixes an issue with ghost tiles appearing on the filmstrip.
This was a regression of the recent changes to filmstrip reordering.
2026-02-27 12:52:47 -05:00
Jaya Allamsetty
2b7a4026aa fix(filmstrip) Excludes partially visible tiles for dominant speaker slot. 2026-02-27 12:52:35 -05:00
Jaya Allamsetty
b6768e6939 fix(filmstrip) Ensures dominant speaker is always visible in filmstrip (#16901)
* fix(filmstrip) Ensures dominant speaker is always visible in filmstrip
2026-02-27 12:52:23 -05:00
damencho
144d9104d9 fix(tests): Adds a retry to iframe kick test. 2026-02-26 10:32:24 -06:00
Jaya Allamsetty
bb169cd3eb chore(deps) Creates a release branch for lib-jitsi-meet 2026-02-25 12:33:23 -05:00
bgrozev
2937d17ff7 test: Clean-up transcription test. (#16991)
* test: Clean-up transcription test.

Possibly fix race conditions

* fix: Wait until jigasi leaves.

* squash: Linting.

* squash: Fix participant count check.

* squash: Switch to iframe.

* squash: Linting.
2026-02-23 19:18:03 -06:00
bgrozev
945e42a923 test: Fix transcription test. (#16974)
Fix waiting for transcription to be turned off. Re-enabling transcription was sometimes started before jigasi had left the room, resulting in a failure.
2026-02-20 18:12:24 -06:00
Boris Grozev
8b4528e3c5 test: Enable retries for the kick test, it's failing sporadically. 2026-02-20 18:12:22 -06:00
Jaya Allamsetty
5eca22fcb5 Revert "fix(filmstrip) Ensures dominant speaker is always visible in filmstrip (#16901)"
This reverts commit bde8dca825.
2026-02-19 09:26:44 -05:00
Jaya Allamsetty
d35d93900c Revert "fix(filmstrip) Excludes partially visible tiles for dominant speaker slot."
This reverts commit c88bfa1b9a.
2026-02-19 09:26:22 -05:00
Mihaela Dumitru
ece9b75d5e fix(ui): add semantic tokens for svg fills and preview (#16962) 2026-02-19 11:39:40 +02:00
Mihaela Dumitru
3b66476d97 fix(ui): more semantic tokens (#16955) 2026-02-18 16:47:54 +02:00
20 changed files with 144 additions and 98 deletions

View File

@@ -45,7 +45,7 @@ body {
.jitsi-icon {
&-default svg {
fill: var(--icon-default-color, white);
fill: var(--icon-svg-fill, white);
}
}

View File

@@ -2,8 +2,8 @@
.reactions-menu {
width: 330px;
background: #242528;
box-shadow: 0px 3px 16px rgba(0, 0, 0, 0.6), 0px 0px 4px 1px rgba(0, 0, 0, 0.25);
background: var(--reactions-menu-background, #242528);
box-shadow: 0px 3px 16px var(--reactions-menu-box-shadow-1, rgba(0, 0, 0, 0.6)), 0px 0px 4px 1px var(--reactions-menu-box-shadow-2, rgba(0, 0, 0, 0.25));
border-radius: 6px;
padding: 16px;
@@ -14,7 +14,7 @@
top: 3px;
& .toolbox-icon.toggled {
background-color: #000000;
background-color: var(--reactions-menu-button-toggled, #000000);
}
}
}

View File

@@ -106,7 +106,7 @@
}
#preview {
background: #040404;
background: var(--prejoin-preview-background, #040404);
display: flex;
align-items: center;
justify-content: center;

View File

@@ -2325,6 +2325,7 @@ class API {
* @returns {void}
*/
notifyPictureInPictureRequested() {
logger.debug('Sending _pip-requested event to External API');
this._sendEvent({
name: '_pip-requested'
});
@@ -2336,6 +2337,7 @@ class API {
* @returns {void}
*/
notifyPictureInPictureEntered() {
logger.debug('Sending pip-entered event to External API');
this._sendEvent({
name: 'pip-entered'
});
@@ -2347,6 +2349,7 @@ class API {
* @returns {void}
*/
notifyPictureInPictureLeft() {
logger.debug('Sending pip-left event to External API');
this._sendEvent({
name: 'pip-left'
});

9
package-lock.json generated
View File

@@ -66,7 +66,7 @@
"js-md5": "0.6.1",
"js-sha512": "0.8.0",
"jwt-decode": "2.2.0",
"lib-jitsi-meet": "https://github.com/jitsi/lib-jitsi-meet/releases/download/v2124.0.0+80df84a1/lib-jitsi-meet.tgz",
"lib-jitsi-meet": "https://github.com/jitsi/lib-jitsi-meet#release-9036",
"lodash-es": "4.17.21",
"null-loader": "4.0.1",
"optional-require": "1.0.3",
@@ -19049,8 +19049,7 @@
},
"node_modules/lib-jitsi-meet": {
"version": "0.0.0",
"resolved": "https://github.com/jitsi/lib-jitsi-meet/releases/download/v2124.0.0+80df84a1/lib-jitsi-meet.tgz",
"integrity": "sha512-wNfil8xxSjcrT3oNA5Lil0qETqR6W2XxpXNewYYjFiM2kSuWgH8fcVxgcQYzvGH+sOqEjdUEk1V81oNo/rB6tQ==",
"resolved": "git+ssh://git@github.com/jitsi/lib-jitsi-meet.git#80df84a1ad34d806cad54cd7dd051e031782c739",
"license": "Apache-2.0",
"dependencies": {
"@jitsi/js-utils": "^2.6.7",
@@ -40848,8 +40847,8 @@
}
},
"lib-jitsi-meet": {
"version": "https://github.com/jitsi/lib-jitsi-meet/releases/download/v2124.0.0+80df84a1/lib-jitsi-meet.tgz",
"integrity": "sha512-wNfil8xxSjcrT3oNA5Lil0qETqR6W2XxpXNewYYjFiM2kSuWgH8fcVxgcQYzvGH+sOqEjdUEk1V81oNo/rB6tQ==",
"version": "git+ssh://git@github.com/jitsi/lib-jitsi-meet.git#80df84a1ad34d806cad54cd7dd051e031782c739",
"from": "lib-jitsi-meet@https://github.com/jitsi/lib-jitsi-meet#release-9036",
"requires": {
"@jitsi/js-utils": "^2.6.7",
"@jitsi/logger": "2.1.1",

View File

@@ -72,7 +72,7 @@
"js-md5": "0.6.1",
"js-sha512": "0.8.0",
"jwt-decode": "2.2.0",
"lib-jitsi-meet": "https://github.com/jitsi/lib-jitsi-meet/releases/download/v2124.0.0+80df84a1/lib-jitsi-meet.tgz",
"lib-jitsi-meet": "https://github.com/jitsi/lib-jitsi-meet#release-9036",
"lodash-es": "4.17.21",
"null-loader": "4.0.1",
"optional-require": "1.0.3",

View File

@@ -157,7 +157,8 @@ ReducerRegistry.register<IParticipantsState>('features/base/participants',
const { participant } = action;
const { id, previousSpeakers = [] } = participant;
const { dominantSpeaker, local } = state;
const activeSpeakers = new Set(previousSpeakers.filter((speakerId: string) => speakerId !== local?.id));
const activeSpeakers = new Set(previousSpeakers
.filter((speakerId: string) => state.remote.has(speakerId) && (speakerId !== local?.id)));
// Only one dominant speaker is allowed.
if (dominantSpeaker) {

View File

@@ -155,6 +155,7 @@ export const colorMap = {
preMeetingBackground: 'surface02', // Pre-meeting screen container background
preMeetingPreview: 'ui01', // Video preview in pre-meeting
prejoinDialogBackground: 'uiBackground', // Prejoin dialog background
prejoinPreviewBackground: 'uiBackground', // Prejoin video preview background (#040404)
prejoinDialogDelimiter: 'ui03', // Prejoin dialog delimiter line
prejoinDialogDelimiterText: 'text01', // Prejoin dialog delimiter text
prejoinTitleText: 'text01', // Prejoin title text color
@@ -356,6 +357,9 @@ export const colorMap = {
// Reactions
reactionsMenuBackground: 'ui01', // Reactions menu background
reactionsMenuBorder: 'ui02', // Reactions menu border
reactionsMenuButtonToggled: 'surface01', // Reactions menu button toggled state background
reactionsMenuBoxShadow1: 'ui09', // Reactions menu box shadow primary
reactionsMenuBoxShadow2: 'ui08', // Reactions menu box shadow secondary
// Recording / Live Stream
recordingBackground: 'ui01', // Recording panel background
@@ -428,7 +432,7 @@ export const colorMap = {
securityDialogBorder: 'ui07', // Security dialog border color
// Deep Linking
deepLinkingBackground: 'ui01', // Deep linking page content pane background
deepLinkingBackground: 'uiBackground', // Deep linking page content pane background (#1e1e1e)
deepLinkingBorder: 'ui03', // Deep linking page content pane border
deepLinkingText: 'text01', // Deep linking page text
deepLinkingSeparator: 'ui03', // Deep linking separator line
@@ -505,6 +509,9 @@ export const colorMap = {
// High-contrast
icon04: 'surface01',
// SVG fill color
iconSvgFill: 'icon01',
// Error
iconError: 'action03',

View File

@@ -13,19 +13,25 @@ export const commonStyles = (theme: Theme) => {
return {
':root': {
// Inject semantic tokens as CSS custom properties for use in SCSS
'--toolbox-background-color': theme.palette.toolboxBackground,
'--drawer-background-color': theme.palette.drawerBackground,
'--icon-svg-fill': theme.palette.iconSvgFill,
'--overflow-menu-background-color': theme.palette.overflowMenuBackground,
'--overflow-menu-item-disabled-color': theme.palette.overflowMenuItemDisabled,
'--overflow-menu-item-hover-color': theme.palette.overflowMenuItemHover,
'--overflow-menu-item-icon-color': theme.palette.overflowMenuItemIcon,
'--overflow-menu-item-text-color': theme.palette.overflowMenuItemText,
'--prejoin-preview-background': theme.palette.prejoinPreviewBackground,
'--reactions-menu-background': theme.palette.reactionsMenuBackground,
'--reactions-menu-box-shadow-1': theme.palette.reactionsMenuBoxShadow1,
'--reactions-menu-box-shadow-2': theme.palette.reactionsMenuBoxShadow2,
'--reactions-menu-button-toggled': theme.palette.reactionsMenuButtonToggled,
'--toolbar-button-active-color': theme.palette.toolbarButtonActive,
'--toolbar-button-color': theme.palette.toolbarButton,
'--toolbar-button-hover-color': theme.palette.toolbarButtonHover,
'--toolbar-button-active-color': theme.palette.toolbarButtonActive,
'--toolbar-icon-active-color': theme.palette.toolbarIconActive,
'--toolbar-icon-color': theme.palette.toolbarIcon,
'--toolbar-icon-hover-color': theme.palette.toolbarIconHover,
'--toolbar-icon-active-color': theme.palette.toolbarIconActive,
'--overflow-menu-background-color': theme.palette.overflowMenuBackground,
'--overflow-menu-item-text-color': theme.palette.overflowMenuItemText,
'--overflow-menu-item-icon-color': theme.palette.overflowMenuItemIcon,
'--overflow-menu-item-hover-color': theme.palette.overflowMenuItemHover,
'--overflow-menu-item-disabled-color': theme.palette.overflowMenuItemDisabled
'--toolbox-background-color': theme.palette.toolboxBackground
},
'.empty-list': {

View File

@@ -26,6 +26,7 @@ export interface IPalette {
icon02: string;
icon03: string;
icon04: string;
iconSvgFill: string;
iconError: string;
link01: string;
link01Active: string;
@@ -153,6 +154,7 @@ export interface IPalette {
prejoinDialogBackground: string;
prejoinDialogDelimiter: string;
prejoinDialogDelimiterText: string;
prejoinPreviewBackground: string;
prejoinRecordingWarningText: string;
prejoinRoomNameText: string;
prejoinTitleText: string;
@@ -313,6 +315,9 @@ export interface IPalette {
pollsVotersText: string;
reactionsMenuBackground: string;
reactionsMenuBorder: string;
reactionsMenuButtonToggled: string;
reactionsMenuBoxShadow1: string;
reactionsMenuBoxShadow2: string;
recordingBackground: string;
recordingHighlightButton: string;
recordingHighlightButtonDisabled: string;

View File

@@ -353,7 +353,7 @@ const defaultStyles = (theme: Theme) => {
tintBackground: {
position: 'absolute' as const,
zIndex: 1,
zIndex: 0,
width: '100%',
height: '100%',
backgroundColor: theme.palette.thumbnailTintBackground,

View File

@@ -68,6 +68,8 @@ export function toggleVideoFromPiP() {
*/
export function exitPiP() {
return (dispatch: IStore['dispatch']) => {
logger.debug('exitPiP called');
if (document.pictureInPictureElement) {
document.exitPictureInPicture()
.then(() => {
@@ -95,6 +97,8 @@ export function handleWindowBlur(videoElement: HTMLVideoElement) {
const state = getState();
const isPiPActive = state['features/pip']?.isPiPActive;
logger.debug(`Window blur detected, isPiPActive=${isPiPActive}`);
if (!isPiPActive) {
enterPiP(videoElement);
}
@@ -112,6 +116,8 @@ export function handleWindowFocus() {
const state = getState();
const isPiPActive = state['features/pip']?.isPiPActive;
logger.debug(`Window focus detected, isPiPActive=${isPiPActive}`);
if (isPiPActive) {
dispatch(exitPiP());
}
@@ -160,17 +166,24 @@ export function showPiP() {
return (_dispatch: IStore['dispatch'], getState: IStore['getState']) => {
const state = getState();
const isPiPActive = state['features/pip']?.isPiPActive;
const _shouldShowPip = shouldShowPiP(state);
if (!shouldShowPiP(state)) {
logger.debug(`showPiP called, shouldShow=${_shouldShowPip}, isPiPActive=${isPiPActive}`);
if (!_shouldShowPip) {
return;
}
if (!isPiPActive) {
const videoElement = document.getElementById('pipVideo') as HTMLVideoElement;
if (videoElement) {
enterPiP(videoElement);
if (!videoElement) {
logger.warn('showPiP: pipVideo element not found');
return;
}
enterPiP(videoElement);
}
};
}
@@ -186,6 +199,8 @@ export function hidePiP() {
const state = getState();
const isPiPActive = state['features/pip']?.isPiPActive;
logger.debug(`hidePiP called, isPiPActive=${isPiPActive}`);
if (isPiPActive) {
dispatch(exitPiP());
}

View File

@@ -311,8 +311,12 @@ export function requestPictureInPicture() {
// Wait for metadata to load before requesting PiP.
video.addEventListener('loadedmetadata', () => {
logger.debug(`Calling video.requestPictureInPicture(), readyState=${video.readyState}`);
// @ts-ignore - requestPictureInPicture is not yet in all TypeScript definitions.
video.requestPictureInPicture().catch((err: Error) => {
video.requestPictureInPicture().then(() => {
logger.debug('video.requestPictureInPicture() succeeded');
}).catch((err: Error) => {
logger.error(`Error while requesting PiP after metadata loaded: ${err.message}`);
}).finally(() => {
// Currently Electron will only pass the requests and execute requestPictureInPicture but
@@ -326,8 +330,12 @@ export function requestPictureInPicture() {
return;
}
logger.debug(`Calling video.requestPictureInPicture(), readyState=${video.readyState}`);
// @ts-ignore - requestPictureInPicture is not yet in all TypeScript definitions.
video.requestPictureInPicture().catch((err: Error) => {
video.requestPictureInPicture().then(() => {
logger.debug('video.requestPictureInPicture() succeeded');
}).catch((err: Error) => {
logger.error(`Error while requesting PiP: ${err.message}`);
}).finally(() => {
// Currently Electron will only pass the requests and execute requestPictureInPicture but

View File

@@ -5,6 +5,7 @@ import { isLocalTrackMuted } from '../base/tracks/functions.any';
import { getElectronGlobalNS } from '../base/util/helpers';
import { requestPictureInPicture, shouldShowPiP, updateMediaSessionState } from './functions';
import logger from './logger';
/**
* Listens to audio and video mute state changes when PiP is active
@@ -51,9 +52,11 @@ StateListenerRegistry.register(
if (_shouldShowPiP) {
// Expose requestPictureInPicture for Electron main process.
if (!electronNS.requestPictureInPicture) {
logger.debug('Exposing requestPictureInPicture to Electron namespace');
electronNS.requestPictureInPicture = requestPictureInPicture;
}
} else if (typeof electronNS.requestPictureInPicture === 'function') {
logger.debug('Removing requestPictureInPicture from Electron namespace (PiP disabled)');
delete electronNS.requestPictureInPicture;
}
}

View File

@@ -4,7 +4,8 @@ import { getFeatureFlag } from '../base/flags/functions';
import { pinParticipant } from '../base/participants/actions';
import { getParticipantCount, getPinnedParticipant } from '../base/participants/functions';
import { FakeParticipant } from '../base/participants/types';
import { isStageFilmstripAvailable, isTileViewModeDisabled } from '../filmstrip/functions';
import { isStageFilmstripAvailable, isTileViewModeDisabled, isTopPanelEnabled } from '../filmstrip/functions';
import { isFollowMeActive } from '../follow-me/functions';
import { isVideoPlaying } from '../shared-video/functions';
import { VIDEO_QUALITY_LEVELS } from '../video-quality/constants';
import { getReceiverVideoQualityLevel } from '../video-quality/functions';
@@ -105,6 +106,15 @@ export function shouldDisplayTileView(state: IReduxState) {
// We want jibri to use stage view by default
|| iAmRecorder
// In large calls (top panel enabled), a screenshare should exit tile view automatically
// when auto-pin is active and Follow Me is not controlling the layout.
// getPinnedParticipant() uses the stage filmstrip path in large calls and ignores
// features/base/participants.pinnedParticipant, so we need this explicit check.
|| (Boolean(state['features/video-layout'].remoteScreenShares?.length)
&& isTopPanelEnabled(state)
&& getAutoPinSetting()
&& !isFollowMeActive(state))
);
return !shouldDisplayNormalMode;

View File

@@ -512,7 +512,7 @@ export class Participant {
}
/**
* Waits until the number of participants is exactly the given number.
* Waits until the number of remote participants is exactly the given number.
*
* @param {number} number - The number of participant to wait for.
* @param {string} msg - A custom message to use.
@@ -520,7 +520,7 @@ export class Participant {
*/
waitForParticipants(number: number, msg?: string): Promise<boolean> {
return this.driver.waitUntil(
() => this.execute(count => (APP?.conference?.listMembers()?.length ?? -1) === count, number),
() => this.execute(count => (window.APP?.conference?.listMembers()?.length ?? -1) === count, number),
{
timeout: 15_000,
timeoutMsg: msg || `not the expected participants ${number} in 15s for ${this.name}`

View File

@@ -38,8 +38,9 @@ export default class WebhookProxy {
this.ws.on('error', console.error);
this.ws.on('open', function open() {
this.ws.on('open', () => {
console.log('WebhookProxy connected');
this.logInfo('connected');
});
this.ws.on('message', (data: any) => {

View File

@@ -6,6 +6,7 @@ import { ensureTwoParticipants } from '../../helpers/participants';
import { checkIframeApi } from './util';
setTestProperties(__filename, {
retry: true,
usesBrowsers: [ 'p1', 'p2' ]
});

View File

@@ -8,7 +8,6 @@ import { joinJaasMuc, generateJaasToken as t } from '../../helpers/jaas';
setTestProperties(__filename, {
requireWebhookProxy: true,
retry: true,
useJaas: true,
usesBrowsers: [ 'p1', 'p2' ]
});
@@ -20,6 +19,40 @@ for (const asyncTranscriptions of asyncTranscriptionValues) {
let p1: Participant, p2: Participant;
let webhooksProxy: WebhookProxy;
async function clearTranscriptionStatusChange() {
await p1.getIframeAPI().clearEventResults('transcribingStatusChanged');
await p2.getIframeAPI().clearEventResults('transcribingStatusChanged');
}
/**
* Wait until a transcribingStatusChanged iFrame event is received for both p1 and p2, with a specific value
* for the "on" field.
* Note that addEventListener for 'transcriptionChunkReceived' should have been called prior to this function.
*/
async function waitForTranscriptionStatusChange(expectedOn: boolean) {
for (const p of [ p1, p2 ]) {
const event = await p.driver.waitUntil(
() => p.getIframeAPI().getEventResult('transcribingStatusChanged'),
{
timeout: 10000,
timeoutMsg: `transcribingStatusChanged event not received on ${p.name}`
});
if (event.on !== expectedOn) {
throw new Error(`Expected transcribing to be ${expectedOn} for ${p.name}, got ${event.on}`);
}
if (!expectedOn && !asyncTranscriptions) {
// The "stopped" event is sometimes fired before the jigasi participant leaves. If we re-start
// transcription before jigasi has left, jicofo will reject the request.
await p.switchToIFrame();
await p.waitForParticipants(
1,
'Unexpected number of participants. Jigasi failed to leave?');
await p.switchToMainFrame();
}
}
}
it('setup', async () => {
const room = ctx.roomName;
@@ -61,107 +94,60 @@ for (const asyncTranscriptions of asyncTranscriptionValues) {
expect(await p1.getIframeAPI().getEventResult('isModerator')).toBe(true);
expect(await p1.getIframeAPI().getEventResult('videoConferenceJoined')).toBeDefined();
await p1.getIframeAPI().addEventListener('transcribingStatusChanged');
await p2.getIframeAPI().addEventListener('transcribingStatusChanged');
await p1.getIframeAPI().addEventListener('transcriptionChunkReceived');
await p2.getIframeAPI().addEventListener('transcriptionChunkReceived');
});
it('toggle subtitles', async () => {
await p1.getIframeAPI().addEventListener('transcriptionChunkReceived');
await p2.getIframeAPI().addEventListener('transcriptionChunkReceived');
await clearTranscriptionStatusChange();
await p1.getIframeAPI().executeCommand('toggleSubtitles');
await waitForTranscriptionStatusChange(true);
await checkReceivingChunks(p1, p2, webhooksProxy, asyncTranscriptions);
await p1.getIframeAPI().clearEventResults('transcribingStatusChanged');
await p1.getIframeAPI().addEventListener('transcribingStatusChanged');
await clearTranscriptionStatusChange();
await p1.getIframeAPI().executeCommand('toggleSubtitles');
await p1.driver.waitUntil(() => p1.getIframeAPI()
.getEventResult('transcribingStatusChanged'), {
timeout: 15000,
timeoutMsg: 'transcribingStatusChanged event not received by p1'
});
await waitForTranscriptionStatusChange(false);
});
it('set subtitles on and off', async () => {
// we need to clear results or the last one will be used, from the previous time subtitles were on
await clearTranscriptionStatusChange();
await p1.getIframeAPI().clearEventResults('transcriptionChunkReceived');
await p2.getIframeAPI().clearEventResults('transcriptionChunkReceived');
webhooksProxy.clearCache();
await p1.getIframeAPI().executeCommand('setSubtitles', true, true);
await waitForTranscriptionStatusChange(true);
await checkReceivingChunks(p1, p2, webhooksProxy, asyncTranscriptions);
await p1.getIframeAPI().clearEventResults('transcribingStatusChanged');
await clearTranscriptionStatusChange();
await p1.getIframeAPI().executeCommand('setSubtitles', false);
await p1.driver.waitUntil(() => p1.getIframeAPI()
.getEventResult('transcribingStatusChanged'), {
timeout: 15000,
timeoutMsg: 'transcribingStatusChanged event not received by p1'
});
await waitForTranscriptionStatusChange(false);
});
it('start/stop transcriptions via recording', async () => {
// we need to clear results or the last one will be used, from the previous time subtitles were on
await p1.getIframeAPI().clearEventResults('transcribingStatusChanged');
await p1.getIframeAPI().clearEventResults('transcriptionChunkReceived');
await p2.getIframeAPI().clearEventResults('transcriptionChunkReceived');
await p2.getIframeAPI().addEventListener('transcribingStatusChanged');
await clearTranscriptionStatusChange();
await p1.getIframeAPI().executeCommand('startRecording', { transcription: true });
let allTranscriptionStatusChanged: Promise<any>[] = [];
allTranscriptionStatusChanged.push(await p1.driver.waitUntil(() => p1.getIframeAPI()
.getEventResult('transcribingStatusChanged'), {
timeout: 10000,
timeoutMsg: 'transcribingStatusChanged event not received on p1'
}));
allTranscriptionStatusChanged.push(await p2.driver.waitUntil(() => p2.getIframeAPI()
.getEventResult('transcribingStatusChanged'), {
timeout: 10000,
timeoutMsg: 'transcribingStatusChanged event not received on p2'
}));
let result = await Promise.allSettled(allTranscriptionStatusChanged);
expect(result.length).toBe(2);
result.forEach(e => {
// @ts-ignore
expect(e.value.on).toBe(true);
});
await waitForTranscriptionStatusChange(true);
await checkReceivingChunks(p1, p2, webhooksProxy, asyncTranscriptions);
await p1.getIframeAPI().clearEventResults('transcribingStatusChanged');
await p2.getIframeAPI().clearEventResults('transcribingStatusChanged');
await clearTranscriptionStatusChange();
await p1.getIframeAPI().executeCommand('stopRecording', 'file', true);
allTranscriptionStatusChanged = [];
allTranscriptionStatusChanged.push(await p1.driver.waitUntil(() => p1.getIframeAPI()
.getEventResult('transcribingStatusChanged'), {
timeout: 10000,
timeoutMsg: 'transcribingStatusChanged event not received on p1'
}));
allTranscriptionStatusChanged.push(await p2.driver.waitUntil(() => p2.getIframeAPI()
.getEventResult('transcribingStatusChanged'), {
timeout: 10000,
timeoutMsg: 'transcribingStatusChanged event not received on p2'
}));
result = await Promise.allSettled(allTranscriptionStatusChanged);
expect(result.length).toBe(2);
result.forEach(e => {
// @ts-ignore
expect(e.value.on).toBe(false);
});
await waitForTranscriptionStatusChange(false);
await p1.getIframeAPI().executeCommand('hangup');
await p2.getIframeAPI().executeCommand('hangup');

View File

@@ -4,6 +4,7 @@ import { expectations } from '../../helpers/expectations';
import { ensureTwoParticipants } from '../../helpers/participants';
setTestProperties(__filename, {
retry: true,
usesBrowsers: [ 'p1', 'p2' ]
});