mirror of
https://gitcode.com/GitHub_Trending/ji/jitsi-meet.git
synced 2026-02-14 01:40:18 +00:00
Compare commits
15 Commits
debug-test
...
9026
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
28c72bfa7f | ||
|
|
24c78cf8ff | ||
|
|
147e64106d | ||
|
|
3d53f2e4c5 | ||
|
|
e60bfc573a | ||
|
|
796a7efa7f | ||
|
|
fb075c376d | ||
|
|
c457ed0d3c | ||
|
|
1efc5e40e1 | ||
|
|
dc84826d9c | ||
|
|
b4a64ebc44 | ||
|
|
dbb3ccc274 | ||
|
|
9a6ed65cb1 | ||
|
|
21ea67b29c | ||
|
|
ab4be2366f |
File diff suppressed because it is too large
Load Diff
@@ -227,6 +227,9 @@
|
||||
"video_ssrc": "Video SSRC:",
|
||||
"yes": "yes"
|
||||
},
|
||||
"customPanel": {
|
||||
"close": "Close"
|
||||
},
|
||||
"dateUtils": {
|
||||
"earlier": "Earlier",
|
||||
"today": "Today",
|
||||
@@ -1315,6 +1318,7 @@
|
||||
"chat": "Open / Close chat",
|
||||
"clap": "Clap",
|
||||
"closeChat": "Close chat",
|
||||
"closeCustomPanel": "Close",
|
||||
"closeMoreActions": "Close more actions menu",
|
||||
"closeParticipantsPane": "Close participants pane",
|
||||
"closedCaptions": "Closed captions",
|
||||
@@ -1420,9 +1424,11 @@
|
||||
"chat": "Open / Close chat",
|
||||
"clap": "Clap",
|
||||
"closeChat": "Close chat",
|
||||
"closeCustomPanel": "Close",
|
||||
"closeParticipantsPane": "Close participants pane",
|
||||
"closeReactionsMenu": "Close reactions menu",
|
||||
"closedCaptions": "Closed captions",
|
||||
"copilot": "Copilot",
|
||||
"disableNoiseSuppression": "Disable extra noise suppression",
|
||||
"disableReactionSounds": "You can disable reaction sounds for this meeting",
|
||||
"documentClose": "Close shared document",
|
||||
|
||||
@@ -240,13 +240,27 @@ function initCommands() {
|
||||
APP.store.dispatch(muteAllParticipants(exclude, muteMediaType));
|
||||
},
|
||||
'mute-remote-participant': (participantId, mediaType) => {
|
||||
if (!isLocalParticipantModerator(APP.store.getState())) {
|
||||
logger.error('Missing moderator rights to mute remote participant');
|
||||
const state = APP.store.getState();
|
||||
const muteMediaType = mediaType ? mediaType : MEDIA_TYPE.AUDIO;
|
||||
const localParticipant = getLocalParticipant(state);
|
||||
|
||||
// Check if targeting the local participant
|
||||
if (participantId === localParticipant?.id) {
|
||||
|
||||
if (muteMediaType === MEDIA_TYPE.AUDIO) {
|
||||
APP.conference.toggleAudioMuted(false);
|
||||
} else if (muteMediaType === MEDIA_TYPE.VIDEO) {
|
||||
APP.conference.toggleVideoMuted(false, true);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const muteMediaType = mediaType ? mediaType : MEDIA_TYPE.AUDIO;
|
||||
if (!isLocalParticipantModerator(state)) {
|
||||
logger.error('Missing moderator rights to mute remote participant');
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
APP.store.dispatch(muteRemote(participantId, muteMediaType));
|
||||
},
|
||||
@@ -1417,17 +1431,15 @@ class API {
|
||||
*
|
||||
* @param {string} participantId - The ID of the participant.
|
||||
* @param {boolean} isMuted - True if muted, false if unmuted.
|
||||
* @param {string} mediaType - Media type that was muted ('audio', 'video', or 'desktop').
|
||||
* @param {boolean} isSelfMuted - True if participant muted themselves, false if muted by moderator.
|
||||
* @param {string} mediaType - Media type that was muted ('audio' or 'video').
|
||||
* @returns {void}
|
||||
*/
|
||||
notifyParticipantMuted(participantId, isMuted, mediaType, isSelfMuted = true) {
|
||||
notifyParticipantMuted(participantId, isMuted, mediaType) {
|
||||
this._sendEvent({
|
||||
name: 'participant-muted',
|
||||
id: participantId,
|
||||
isMuted,
|
||||
mediaType,
|
||||
isSelfMuted
|
||||
mediaType
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
2109
package-lock.json
generated
2109
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
14
package.json
14
package.json
@@ -164,12 +164,12 @@
|
||||
"@types/w3c-image-capture": "1.0.6",
|
||||
"@types/w3c-web-hid": "1.0.3",
|
||||
"@types/zxcvbn": "4.4.1",
|
||||
"@wdio/allure-reporter": "9.22.0",
|
||||
"@wdio/cli": "9.22.0",
|
||||
"@wdio/globals": "9.17.0",
|
||||
"@wdio/junit-reporter": "9.21.0",
|
||||
"@wdio/local-runner": "9.22.0",
|
||||
"@wdio/mocha-framework": "9.22.0",
|
||||
"@wdio/allure-reporter": "9.23.2",
|
||||
"@wdio/cli": "9.23.2",
|
||||
"@wdio/globals": "9.23.0",
|
||||
"@wdio/junit-reporter": "9.23.2",
|
||||
"@wdio/local-runner": "9.23.2",
|
||||
"@wdio/mocha-framework": "9.23.2",
|
||||
"babel-loader": "9.1.0",
|
||||
"babel-plugin-optional-require": "0.3.1",
|
||||
"circular-dependency-plugin": "5.2.0",
|
||||
@@ -194,7 +194,7 @@
|
||||
"typescript": "5.7.2",
|
||||
"unorm": "1.6.0",
|
||||
"webdriverio": "9.22.0",
|
||||
"webpack": "5.95.0",
|
||||
"webpack": "5.105.0",
|
||||
"webpack-bundle-analyzer": "4.4.2",
|
||||
"webpack-cli": "5.1.4",
|
||||
"webpack-dev-server": "5.1.0"
|
||||
|
||||
@@ -26,5 +26,6 @@ import '../face-landmarks/middleware';
|
||||
import '../gifs/middleware';
|
||||
import '../whiteboard/middleware.web';
|
||||
import '../file-sharing/middleware.web';
|
||||
import '../custom-panel/middleware.web';
|
||||
|
||||
import './middlewares.any';
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import '../base/devices/reducer';
|
||||
import '../base/premeeting/reducer';
|
||||
import '../custom-panel/reducer';
|
||||
import '../base/tooltip/reducer';
|
||||
import '../e2ee/reducer';
|
||||
import '../face-landmarks/reducer';
|
||||
|
||||
@@ -31,6 +31,7 @@ import { IUserInteractionState } from '../base/user-interaction/reducer';
|
||||
import { IBreakoutRoomsState } from '../breakout-rooms/reducer';
|
||||
import { ICalendarSyncState } from '../calendar-sync/reducer';
|
||||
import { IChatState } from '../chat/reducer';
|
||||
import { ICustomPanelState } from '../custom-panel/reducer';
|
||||
import { IDeepLinkingState } from '../deep-linking/reducer';
|
||||
import { IDropboxState } from '../dropbox/reducer';
|
||||
import { IDynamicBrandingState } from '../dynamic-branding/reducer';
|
||||
@@ -121,6 +122,7 @@ export interface IReduxState {
|
||||
'features/calendar-sync': ICalendarSyncState;
|
||||
'features/call-integration': ICallIntegrationState;
|
||||
'features/chat': IChatState;
|
||||
'features/custom-panel': ICustomPanelState;
|
||||
'features/deep-linking': IDeepLinkingState;
|
||||
'features/dropbox': IDropboxState;
|
||||
'features/dynamic-branding': IDynamicBrandingState;
|
||||
|
||||
@@ -387,7 +387,8 @@ export function setConfigFromURLParams(
|
||||
|
||||
// When not in an iframe, start without media if the pre-join page is not enabled.
|
||||
if (!isEmbedded()
|
||||
&& 'config.prejoinConfig.enabled' in params && config.prejoinConfig?.enabled === false) {
|
||||
&& ('config.prejoinConfig' in params || 'config.prejoinConfig.enabled' in params)
|
||||
&& config.prejoinConfig?.enabled === false) {
|
||||
logger.warn('Using prejoinConfig.enabled config URL overwrite implies starting without media.');
|
||||
config.disableInitialGUM = true;
|
||||
}
|
||||
|
||||
1
react/features/base/icons/svg/AI.svg
Normal file
1
react/features/base/icons/svg/AI.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-sparkles-icon lucide-sparkles"><path d="M11.017 2.814a1 1 0 0 1 1.966 0l1.051 5.558a2 2 0 0 0 1.594 1.594l5.558 1.051a1 1 0 0 1 0 1.966l-5.558 1.051a2 2 0 0 0-1.594 1.594l-1.051 5.558a1 1 0 0 1-1.966 0l-1.051-5.558a2 2 0 0 0-1.594-1.594l-5.558-1.051a1 1 0 0 1 0-1.966l5.558-1.051a2 2 0 0 0 1.594-1.594z"/><path d="M20 2v4"/><path d="M22 4h-4"/><circle cx="4" cy="20" r="2"/></svg>
|
||||
|
After Width: | Height: | Size: 582 B |
@@ -1,3 +1,4 @@
|
||||
import { default as IconAI } from './AI.svg';
|
||||
import { default as IconRecordAccount } from './account-record.svg';
|
||||
import { default as IconAddUser } from './add-user.svg';
|
||||
import { default as IconArrowBack } from './arrow-back.svg';
|
||||
@@ -112,6 +113,7 @@ import { default as IconYahoo } from './yahoo.svg';
|
||||
*/
|
||||
export const DEFAULT_ICON: Record<string, any> = {
|
||||
IconAddUser,
|
||||
IconAI,
|
||||
IconArrowBack,
|
||||
IconArrowDown,
|
||||
IconArrowDownLarge,
|
||||
|
||||
@@ -4,6 +4,7 @@ import { DEFAULT_ICON } from './constants';
|
||||
|
||||
const {
|
||||
IconAddUser,
|
||||
IconAI,
|
||||
IconArrowBack,
|
||||
IconArrowDown,
|
||||
IconArrowDownLarge,
|
||||
@@ -123,6 +124,7 @@ const {
|
||||
|
||||
export {
|
||||
IconAddUser,
|
||||
IconAI,
|
||||
IconArrowBack,
|
||||
IconArrowDown,
|
||||
IconArrowDownLarge,
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { IReduxState, IStore } from '../../app/types';
|
||||
import { getLocalParticipant } from '../participants/functions';
|
||||
import StateListenerRegistry from '../redux/StateListenerRegistry';
|
||||
|
||||
/**
|
||||
@@ -13,6 +14,13 @@ StateListenerRegistry.register(
|
||||
|
||||
if (muted !== previousMuted) {
|
||||
APP.API.notifyAudioMutedStatusChanged(muted);
|
||||
|
||||
// Also fire the participantMuted event for consistency
|
||||
const localParticipant = getLocalParticipant(store.getState());
|
||||
|
||||
if (localParticipant) {
|
||||
APP.API.notifyParticipantMuted(localParticipant.id, muted, 'audio');
|
||||
}
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
@@ -2,6 +2,7 @@ import { batch } from 'react-redux';
|
||||
|
||||
import { IStore } from '../../app/types';
|
||||
import { CHAT_SIZE } from '../../chat/constants';
|
||||
import { getCustomPanelWidth } from '../../custom-panel/functions';
|
||||
import { getParticipantsPaneWidth } from '../../participants-pane/functions';
|
||||
|
||||
import {
|
||||
@@ -51,6 +52,7 @@ export function clientResized(clientWidth: number, clientHeight: number) {
|
||||
}
|
||||
|
||||
availableWidth -= getParticipantsPaneWidth(state);
|
||||
availableWidth -= getCustomPanelWidth(state);
|
||||
|
||||
reducedUIEnabled && dispatch(setReducedUI(availableWidth, clientHeight));
|
||||
}
|
||||
|
||||
@@ -51,7 +51,7 @@ import './subscriber.web';
|
||||
MiddlewareRegistry.register(store => next => action => {
|
||||
switch (action.type) {
|
||||
case TRACK_ADDED: {
|
||||
const { local } = action.track;
|
||||
const { local, jitsiTrack } = action.track;
|
||||
|
||||
// The devices list needs to be refreshed when no initial video permissions
|
||||
// were granted and a local video track is added by umuting the video.
|
||||
@@ -65,6 +65,16 @@ MiddlewareRegistry.register(store => next => action => {
|
||||
|
||||
if (participantId) {
|
||||
logTracksForParticipant(store.getState()['features/base/tracks'], participantId, 'Track added');
|
||||
|
||||
// Fire participantMuted event for initial state of remote tracks
|
||||
if (typeof action.track?.muted !== 'undefined' && jitsiTrack) {
|
||||
const isVideoTrack = jitsiTrack.getType() !== MEDIA_TYPE.AUDIO;
|
||||
const mediaType = isVideoTrack
|
||||
? (jitsiTrack.getVideoType() === VIDEO_TYPE.DESKTOP ? 'desktop' : 'video')
|
||||
: 'audio';
|
||||
|
||||
APP.API.notifyParticipantMuted(participantId, action.track.muted, mediaType);
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
@@ -119,6 +129,16 @@ MiddlewareRegistry.register(store => next => action => {
|
||||
// TODO Remove the following calls to APP.UI once components interested
|
||||
// in track mute changes are moved into React and/or redux.
|
||||
|
||||
const { jitsiTrack } = action.track;
|
||||
const participantID = jitsiTrack.getParticipantId();
|
||||
const isVideoTrack = jitsiTrack.type !== MEDIA_TYPE.AUDIO;
|
||||
const local = jitsiTrack.isLocal();
|
||||
|
||||
// Get old muted state BEFORE updating
|
||||
const tracks = store.getState()['features/base/tracks'];
|
||||
const oldTrack = tracks.find((t: ITrack) => t.jitsiTrack === jitsiTrack);
|
||||
const oldMutedState = oldTrack?.muted;
|
||||
|
||||
const result = next(action);
|
||||
const state = store.getState();
|
||||
|
||||
@@ -126,11 +146,6 @@ MiddlewareRegistry.register(store => next => action => {
|
||||
return result;
|
||||
}
|
||||
|
||||
const { jitsiTrack } = action.track;
|
||||
const participantID = jitsiTrack.getParticipantId();
|
||||
const isVideoTrack = jitsiTrack.type !== MEDIA_TYPE.AUDIO;
|
||||
const local = jitsiTrack.isLocal();
|
||||
|
||||
if (isVideoTrack) {
|
||||
if (local && !(jitsiTrack.getVideoType() === VIDEO_TYPE.DESKTOP)) {
|
||||
APP.conference.setVideoMuteStatus();
|
||||
@@ -144,12 +159,14 @@ MiddlewareRegistry.register(store => next => action => {
|
||||
if (typeof action.track?.muted !== 'undefined' && participantID && !local) {
|
||||
logTracksForParticipant(store.getState()['features/base/tracks'], participantID, 'Track updated');
|
||||
|
||||
// Notify external API when remote participant mutes/unmutes themselves
|
||||
const mediaType = isVideoTrack
|
||||
? (jitsiTrack.getVideoType() === VIDEO_TYPE.DESKTOP ? 'desktop' : 'video')
|
||||
: 'audio';
|
||||
// Fire participantMuted event only if muted state actually changed
|
||||
if (oldMutedState !== action.track.muted) {
|
||||
const mediaType = isVideoTrack
|
||||
? (jitsiTrack.getVideoType() === VIDEO_TYPE.DESKTOP ? 'desktop' : 'video')
|
||||
: 'audio';
|
||||
|
||||
APP.API.notifyParticipantMuted(participantID, action.track.muted, mediaType, true);
|
||||
APP.API.notifyParticipantMuted(participantID, action.track.muted, mediaType);
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
|
||||
@@ -4,7 +4,7 @@ import { isEqual, sortBy } from 'lodash-es';
|
||||
import VideoLayout from '../../../../modules/UI/videolayout/VideoLayout';
|
||||
import { getAutoPinSetting } from '../../video-layout/functions.any';
|
||||
import { MEDIA_TYPE } from '../media/constants';
|
||||
import { getScreenshareParticipantIds } from '../participants/functions';
|
||||
import { getLocalParticipant, getScreenshareParticipantIds } from '../participants/functions';
|
||||
import StateListenerRegistry from '../redux/StateListenerRegistry';
|
||||
|
||||
import { isLocalTrackMuted } from './functions';
|
||||
@@ -47,6 +47,13 @@ StateListenerRegistry.register(
|
||||
/* listener */ (muted, store, previousMuted) => {
|
||||
if (muted !== previousMuted) {
|
||||
APP.API.notifyVideoMutedStatusChanged(muted);
|
||||
|
||||
// Also fire the participantMuted event for consistency
|
||||
const localParticipant = getLocalParticipant(store.getState());
|
||||
|
||||
if (localParticipant) {
|
||||
APP.API.notifyParticipantMuted(localParticipant.id, muted, 'video');
|
||||
}
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
@@ -301,7 +301,7 @@ const DialogWithTabs = ({
|
||||
}
|
||||
|
||||
return null;
|
||||
}, [ selectedTabIndex, tabStates ]);
|
||||
}, [ selectedTabIndex, tabStates, tabs ]);
|
||||
|
||||
const closeIcon = useMemo(() => (
|
||||
<ClickableIcon
|
||||
|
||||
@@ -14,6 +14,7 @@ import { setColorAlpha } from '../../../base/util/helpers';
|
||||
import { openChat, setFocusedTab } from '../../../chat/actions.web';
|
||||
import Chat from '../../../chat/components/web/Chat';
|
||||
import { ChatTabs } from '../../../chat/constants';
|
||||
import CustomPanel from '../../../custom-panel/components/web/CustomPanel';
|
||||
import { isFileUploadingEnabled, processFiles } from '../../../file-sharing/functions.any';
|
||||
import MainFilmstrip from '../../../filmstrip/components/web/MainFilmstrip';
|
||||
import ScreenshareFilmstrip from '../../../filmstrip/components/web/ScreenshareFilmstrip';
|
||||
@@ -326,6 +327,7 @@ class Conference extends AbstractConference<IProps, any> {
|
||||
{ _showVisitorsQueue && <VisitorsQueue />}
|
||||
</div>
|
||||
<ParticipantsPane />
|
||||
<CustomPanel />
|
||||
<ReactionAnimations />
|
||||
</div>
|
||||
);
|
||||
|
||||
14
react/features/custom-panel/actionTypes.ts
Normal file
14
react/features/custom-panel/actionTypes.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
/**
|
||||
* Action type to signal the closing of the custom panel.
|
||||
*/
|
||||
export const CUSTOM_PANEL_CLOSE = 'CUSTOM_PANEL_CLOSE';
|
||||
|
||||
/**
|
||||
* Action type to signal the opening of the custom panel.
|
||||
*/
|
||||
export const CUSTOM_PANEL_OPEN = 'CUSTOM_PANEL_OPEN';
|
||||
|
||||
/**
|
||||
* Action type to enable or disable the custom panel dynamically.
|
||||
*/
|
||||
export const SET_CUSTOM_PANEL_ENABLED = 'SET_CUSTOM_PANEL_ENABLED';
|
||||
40
react/features/custom-panel/actions.web.ts
Normal file
40
react/features/custom-panel/actions.web.ts
Normal file
@@ -0,0 +1,40 @@
|
||||
import {
|
||||
CUSTOM_PANEL_CLOSE,
|
||||
CUSTOM_PANEL_OPEN,
|
||||
SET_CUSTOM_PANEL_ENABLED
|
||||
} from './actionTypes';
|
||||
|
||||
/**
|
||||
* Action to close the custom panel.
|
||||
*
|
||||
* @returns {Object} The action object.
|
||||
*/
|
||||
export function close() {
|
||||
return {
|
||||
type: CUSTOM_PANEL_CLOSE
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Action to open the custom panel.
|
||||
*
|
||||
* @returns {Object} The action object.
|
||||
*/
|
||||
export function open() {
|
||||
return {
|
||||
type: CUSTOM_PANEL_OPEN
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Action to enable or disable the custom panel dynamically.
|
||||
*
|
||||
* @param {boolean} enabled - Whether the custom panel should be enabled.
|
||||
* @returns {Object} The action object.
|
||||
*/
|
||||
export function setCustomPanelEnabled(enabled: boolean) {
|
||||
return {
|
||||
type: SET_CUSTOM_PANEL_ENABLED,
|
||||
enabled
|
||||
};
|
||||
}
|
||||
10
react/features/custom-panel/components/web/CustomPanel.tsx
Normal file
10
react/features/custom-panel/components/web/CustomPanel.tsx
Normal file
@@ -0,0 +1,10 @@
|
||||
/**
|
||||
* Custom panel placeholder component.
|
||||
* This file is overridden by jitsi-meet-branding at build time
|
||||
* to provide the actual panel implementation with iframe content.
|
||||
*
|
||||
* @returns {null} This placeholder renders nothing.
|
||||
*/
|
||||
const CustomPanel = (): null => null;
|
||||
|
||||
export default CustomPanel;
|
||||
@@ -0,0 +1,10 @@
|
||||
/**
|
||||
* Custom panel button placeholder component.
|
||||
* This file is overridden by jitsi-meet-branding at build time
|
||||
* to provide the actual button implementation with custom icon.
|
||||
*
|
||||
* @returns {null} This placeholder renders nothing.
|
||||
*/
|
||||
const CustomPanelButton = (): null => null;
|
||||
|
||||
export default CustomPanelButton;
|
||||
4
react/features/custom-panel/constants.ts
Normal file
4
react/features/custom-panel/constants.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
/**
|
||||
* Default width for the custom panel in pixels.
|
||||
*/
|
||||
export const DEFAULT_CUSTOM_PANEL_WIDTH = 315;
|
||||
67
react/features/custom-panel/functions.ts
Normal file
67
react/features/custom-panel/functions.ts
Normal file
@@ -0,0 +1,67 @@
|
||||
import { IReduxState } from '../app/types';
|
||||
|
||||
import { DEFAULT_CUSTOM_PANEL_WIDTH } from './constants';
|
||||
|
||||
/**
|
||||
* Returns whether the custom panel is enabled based on Redux state.
|
||||
* The feature is disabled by default and can be enabled dynamically via console.
|
||||
*
|
||||
* @param {IReduxState} state - The Redux state.
|
||||
* @returns {boolean} Whether the custom panel is enabled.
|
||||
*/
|
||||
export function isCustomPanelEnabled(state: IReduxState): boolean {
|
||||
return Boolean(state['features/custom-panel']?.enabled);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the custom panel URL.
|
||||
* Override to provide the actual URL.
|
||||
*
|
||||
* @returns {string} The custom panel URL.
|
||||
*/
|
||||
export function getCustomPanelUrl(): string {
|
||||
return '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the custom panel button icon.
|
||||
* Override to provide the actual icon.
|
||||
*
|
||||
* @returns {Function | undefined} The icon component.
|
||||
*/
|
||||
export function getCustomPanelIcon(): Function | undefined {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the configured panel width.
|
||||
*
|
||||
* @returns {number} The panel width in pixels.
|
||||
*/
|
||||
export function getCustomPanelConfiguredWidth(): number {
|
||||
return DEFAULT_CUSTOM_PANEL_WIDTH;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether the custom panel is currently open.
|
||||
*
|
||||
* @param {IReduxState} state - The Redux state.
|
||||
* @returns {boolean} Whether the custom panel is open.
|
||||
*/
|
||||
export function getCustomPanelOpen(state: IReduxState): boolean {
|
||||
return Boolean(state['features/custom-panel']?.isOpen);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the current panel width (0 if closed or disabled).
|
||||
*
|
||||
* @param {IReduxState} state - The Redux state.
|
||||
* @returns {number} The panel width in pixels.
|
||||
*/
|
||||
export function getCustomPanelWidth(state: IReduxState): number {
|
||||
if (!isCustomPanelEnabled(state)) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
return getCustomPanelOpen(state) ? getCustomPanelConfiguredWidth() : 0;
|
||||
}
|
||||
29
react/features/custom-panel/hooks.web.ts
Normal file
29
react/features/custom-panel/hooks.web.ts
Normal file
@@ -0,0 +1,29 @@
|
||||
import { useSelector } from 'react-redux';
|
||||
|
||||
import CustomPanelButton from './components/web/CustomPanelButton';
|
||||
import { isCustomPanelEnabled } from './functions';
|
||||
|
||||
/**
|
||||
* Configuration for the custom panel toolbar button.
|
||||
*/
|
||||
const customPanel = {
|
||||
key: 'custom-panel',
|
||||
Content: CustomPanelButton,
|
||||
group: 2
|
||||
};
|
||||
|
||||
/**
|
||||
* A hook that returns the custom panel button if the feature is enabled.
|
||||
* Uses useSelector for reactive updates when the feature is toggled dynamically.
|
||||
*
|
||||
* @returns {Object | undefined} The button configuration or undefined if disabled.
|
||||
*/
|
||||
export function useCustomPanelButton() {
|
||||
const enabled = useSelector(isCustomPanelEnabled);
|
||||
|
||||
if (enabled) {
|
||||
return customPanel;
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
4
react/features/custom-panel/middleware.web.ts
Normal file
4
react/features/custom-panel/middleware.web.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
/**
|
||||
* Custom panel middleware placeholder.
|
||||
* Override to add custom panel functionality.
|
||||
*/
|
||||
59
react/features/custom-panel/reducer.ts
Normal file
59
react/features/custom-panel/reducer.ts
Normal file
@@ -0,0 +1,59 @@
|
||||
import ReducerRegistry from '../base/redux/ReducerRegistry';
|
||||
|
||||
import {
|
||||
CUSTOM_PANEL_CLOSE,
|
||||
CUSTOM_PANEL_OPEN,
|
||||
SET_CUSTOM_PANEL_ENABLED
|
||||
} from './actionTypes';
|
||||
|
||||
/**
|
||||
* The state of the custom panel feature.
|
||||
*/
|
||||
export interface ICustomPanelState {
|
||||
|
||||
/**
|
||||
* Whether the custom panel feature is enabled.
|
||||
* This can be toggled dynamically via console.
|
||||
*/
|
||||
enabled: boolean;
|
||||
|
||||
/**
|
||||
* Whether the custom panel is currently open.
|
||||
*/
|
||||
isOpen: boolean;
|
||||
}
|
||||
|
||||
const DEFAULT_STATE: ICustomPanelState = {
|
||||
enabled: false,
|
||||
isOpen: false
|
||||
};
|
||||
|
||||
/**
|
||||
* Listen for actions that mutate the custom panel state.
|
||||
*/
|
||||
ReducerRegistry.register(
|
||||
'features/custom-panel', (state: ICustomPanelState = DEFAULT_STATE, action): ICustomPanelState => {
|
||||
switch (action.type) {
|
||||
case CUSTOM_PANEL_CLOSE:
|
||||
return {
|
||||
...state,
|
||||
isOpen: false
|
||||
};
|
||||
|
||||
case CUSTOM_PANEL_OPEN:
|
||||
return {
|
||||
...state,
|
||||
isOpen: true
|
||||
};
|
||||
|
||||
case SET_CUSTOM_PANEL_ENABLED:
|
||||
return {
|
||||
...state,
|
||||
enabled: action.enabled
|
||||
};
|
||||
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
}
|
||||
);
|
||||
@@ -13,6 +13,17 @@ import { isPiPEnabled } from './external-api.shared';
|
||||
import logger from './logger';
|
||||
import { IMediaSessionState } from './types';
|
||||
|
||||
/**
|
||||
* Flag to track if a PiP request is currently pending (requested but not yet entered).
|
||||
*
|
||||
* This prevents duplicate PiP entry requests that can occur on macOS when minimizing
|
||||
* a window. On minimize, both the 'blur' event and 'visibilitychange' event fire in
|
||||
* rapid succession (within ~10ms), each triggering enterPiP(). Without this guard,
|
||||
* Electron receives two PiP requests before the first one completes, causing the
|
||||
* first PiP to immediately exit and triggering a pip leave event that will cause the window to be restored.
|
||||
*/
|
||||
let pipRequestPending = false;
|
||||
|
||||
/**
|
||||
* Gets the appropriate video track for PiP based on prejoin state.
|
||||
* During prejoin, returns local video track. In conference, returns large video participant's track.
|
||||
@@ -303,6 +314,12 @@ export function requestPictureInPicture() {
|
||||
// @ts-ignore - requestPictureInPicture is not yet in all TypeScript definitions.
|
||||
video.requestPictureInPicture().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
|
||||
// if the code there becomes more complicated it is worth considering to change the implementation
|
||||
// to handle errors on the Electron side to prevent the scenario where the code in Electron fails
|
||||
// and the this flag is not reset. This would prevent PiP for ever displaying again.
|
||||
pipRequestPending = false;
|
||||
});
|
||||
}, { once: true });
|
||||
|
||||
@@ -312,6 +329,12 @@ export function requestPictureInPicture() {
|
||||
// @ts-ignore - requestPictureInPicture is not yet in all TypeScript definitions.
|
||||
video.requestPictureInPicture().catch((err: Error) => {
|
||||
logger.error(`Error while requesting PiP: ${err.message}`);
|
||||
}).finally(() => {
|
||||
// Currently Electron will only pass the requests and execute requestPictureInPicture but
|
||||
// if the code there becomes more complicated it is worth considering to change the implementation
|
||||
// to handle errors on the Electron side to prevent the scenario where the code in Electron fails
|
||||
// and the this flag is not reset. This would prevent PiP for ever displaying again.
|
||||
pipRequestPending = false;
|
||||
});
|
||||
}
|
||||
|
||||
@@ -329,6 +352,19 @@ export function enterPiP(videoElement: HTMLVideoElement | undefined | null) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if a PiP request is already pending or PiP is already active.
|
||||
if (pipRequestPending) {
|
||||
logger.debug('PiP request already pending, skipping duplicate request');
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (document.pictureInPictureElement) {
|
||||
logger.debug('PiP already active, skipping request');
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if PiP is supported.
|
||||
if (!('pictureInPictureEnabled' in document)) {
|
||||
logger.error('Picture-in-Picture is not supported in this browser');
|
||||
@@ -348,6 +384,7 @@ export function enterPiP(videoElement: HTMLVideoElement | undefined | null) {
|
||||
// requestPictureInPicture with userGesture: true in the main process.
|
||||
if (browser.isElectron()) {
|
||||
logger.log('Electron detected, sending postMessage to request PiP');
|
||||
pipRequestPending = true;
|
||||
|
||||
APP.API.notifyPictureInPictureRequested();
|
||||
|
||||
|
||||
@@ -52,7 +52,8 @@ export default class PrejoinApp extends BaseApp<Props> {
|
||||
|
||||
dispatch?.(setConfig({
|
||||
prejoinConfig: {
|
||||
enabled: true
|
||||
enabled: true,
|
||||
showHangUp: false
|
||||
},
|
||||
startWithAudioMuted,
|
||||
startWithVideoMuted
|
||||
|
||||
@@ -248,7 +248,7 @@ export function getRecordButtonProps(state: IReduxState) {
|
||||
// a button can be disabled/enabled if enableFeaturesBasedOnToken
|
||||
// is on or if the livestreaming is running.
|
||||
let disabled = false;
|
||||
let tooltip = '';
|
||||
let tooltip = isRecordingRunning(state) ? 'dialog.stopRecording' : 'dialog.startRecording';
|
||||
|
||||
// If the containing component provides the visible prop, that is one
|
||||
// above all, but if not, the button should be autonomus and decide on
|
||||
|
||||
@@ -123,7 +123,8 @@ export const MAIN_TOOLBAR_BUTTONS_PRIORITY = [
|
||||
'embedmeeting',
|
||||
'feedback',
|
||||
'download',
|
||||
'help'
|
||||
'help',
|
||||
'custom-panel'
|
||||
];
|
||||
|
||||
export const TOOLBAR_TIMEOUT = 4000;
|
||||
@@ -148,6 +149,7 @@ export const TOOLBAR_BUTTONS: ToolbarButton[] = [
|
||||
'camera',
|
||||
'chat',
|
||||
'closedcaptions',
|
||||
'custom-panel',
|
||||
'desktop',
|
||||
'download',
|
||||
'embedmeeting',
|
||||
|
||||
@@ -15,6 +15,7 @@ import { isToggleCameraEnabled } from '../base/tracks/functions.web';
|
||||
import { toggleChat } from '../chat/actions.web';
|
||||
import { isChatDisabled } from '../chat/functions';
|
||||
import { useChatButton } from '../chat/hooks.web';
|
||||
import { useCustomPanelButton } from '../custom-panel/hooks.web';
|
||||
import { useEmbedButton } from '../embed-meeting/hooks';
|
||||
import { useEtherpadButton } from '../etherpad/hooks';
|
||||
import { useFeedbackButton } from '../feedback/hooks.web';
|
||||
@@ -292,6 +293,7 @@ export function useToolboxButtons(
|
||||
const feedback = useFeedbackButton();
|
||||
const _download = useDownloadButton();
|
||||
const _help = useHelpButton();
|
||||
const customPanel = useCustomPanelButton();
|
||||
|
||||
const buttons: { [key in ToolbarButton]?: IToolboxButton; } = {
|
||||
microphone,
|
||||
@@ -326,7 +328,8 @@ export function useToolboxButtons(
|
||||
embedmeeting: embed,
|
||||
feedback,
|
||||
download: _download,
|
||||
help: _help
|
||||
help: _help,
|
||||
'custom-panel': customPanel
|
||||
};
|
||||
const buttonKeys = Object.keys(buttons) as ToolbarButton[];
|
||||
|
||||
|
||||
@@ -19,6 +19,7 @@ export interface IToolboxNativeButton {
|
||||
export type ToolbarButton = 'camera' |
|
||||
'chat' |
|
||||
'closedcaptions' |
|
||||
'custom-panel' |
|
||||
'desktop' |
|
||||
'download' |
|
||||
'embedmeeting' |
|
||||
|
||||
@@ -73,11 +73,6 @@ export function muteRemote(participantId: string, mediaType: MediaType) {
|
||||
const muteMediaType = mediaType === MEDIA_TYPE.SCREENSHARE ? 'desktop' : mediaType;
|
||||
|
||||
dispatch(muteRemoteParticipant(participantId, muteMediaType));
|
||||
|
||||
// Notify external API that participant was muted by moderator
|
||||
if (typeof APP !== 'undefined') {
|
||||
APP.API.notifyParticipantMuted(participantId, true, muteMediaType, false);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -11,6 +11,10 @@ local json = require "cjson.safe";
|
||||
local http = require "net.http";
|
||||
local inspect = require 'inspect';
|
||||
|
||||
local util = module:require 'util';
|
||||
local get_room_by_name_and_subdomain = util.get_room_by_name_and_subdomain;
|
||||
local is_vpaas = util.is_vpaas;
|
||||
|
||||
local ban_check_count = module:measure("muc_auth_ban_check", "rate")
|
||||
local ban_check_users_banned_count = module:measure("muc_auth_ban_users_banned", "rate")
|
||||
local ban_check_error_count = module:measure("muc_auth_ban_check_error", "rate")
|
||||
@@ -39,14 +43,20 @@ end);
|
||||
local function shouldAllow(session)
|
||||
local token = session.auth_token;
|
||||
|
||||
if token ~= nil then
|
||||
-- module:log("debug", "Checking whether user should be banned ")
|
||||
|
||||
if token ~= nil and session.jitsi_web_query_room and session.jitsi_web_query_prefix then
|
||||
-- cached tokens are banned
|
||||
if cache:get(token) then
|
||||
return false;
|
||||
end
|
||||
|
||||
local room = get_room_by_name_and_subdomain(session.jitsi_web_query_room, session.jitsi_web_query_prefix);
|
||||
if not room then
|
||||
return nil;
|
||||
end
|
||||
if not is_vpaas(room) then
|
||||
return true;
|
||||
end
|
||||
|
||||
-- TODO: do this only for enabled customers
|
||||
ban_check_count();
|
||||
local function cb(content, code, response, request)
|
||||
@@ -55,7 +65,7 @@ local function shouldAllow(session)
|
||||
local r = json.decode(content)
|
||||
if r['access'] ~= nil and r['access'] == false then
|
||||
module:log("info", "User is banned room:%s tenant:%s user_id:%s group:%s",
|
||||
session.jitsi_meet_room, session.jitsi_web_query_prefix,
|
||||
session.jitsi_web_query_room, session.jitsi_web_query_prefix,
|
||||
inspect(session.jitsi_meet_context_user), session.jitsi_meet_context_group);
|
||||
|
||||
ban_check_users_banned_count();
|
||||
@@ -71,8 +81,9 @@ local function shouldAllow(session)
|
||||
end
|
||||
else
|
||||
ban_check_error_count();
|
||||
module:log("warn", "Error code:%s contacting url:%s response:%s request:%s content:%s",
|
||||
code, ACCESS_MANAGER_URL, response, request, content);
|
||||
module:log("warn", "Error code:%s contacting url:%s content:%s room:%s tenant:%s response:%s request:%s",
|
||||
code, ACCESS_MANAGER_URL, session.jitsi_web_query_room, session.jitsi_web_query_prefix,
|
||||
inspect(response), inspect(request), content);
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
@@ -40,13 +40,18 @@ module:hook('muc-occupant-left', function (event)
|
||||
end
|
||||
|
||||
-- seems the room only has jibri and transcriber, add a timeout to destroy the room
|
||||
if room.empty_destroy_timer then
|
||||
room.empty_destroy_timer:stop();
|
||||
end
|
||||
room.empty_destroy_timer = module:add_timer(EMPTY_TIMEOUT, function()
|
||||
if room.destroying then return end
|
||||
room:destroy(nil, 'Empty room with recording and/or transcribing.');
|
||||
|
||||
module:log('info',
|
||||
'the conference terminated %s as being empty for %s seconds with recording/transcribing enabled',
|
||||
room.jid, EMPTY_TIMEOUT);
|
||||
'the conference terminated %s as being empty for %s seconds with recording/transcribing enabled. By %s',
|
||||
room.jid, EMPTY_TIMEOUT, room.empty_destroy_timer);
|
||||
end)
|
||||
module:log('info', 'Added room destroy timer %s for %s', room.empty_destroy_timer, room.jid);
|
||||
end, -100); -- the last thing to execute
|
||||
|
||||
module:hook('muc-room-destroyed', function (event)
|
||||
|
||||
@@ -212,39 +212,39 @@ local function filterTranscriptionResult(event)
|
||||
-- Do not fire the event, but forward the message
|
||||
return
|
||||
end
|
||||
end
|
||||
|
||||
if msg_obj.transcript ~= nil then
|
||||
local transcription = msg_obj;
|
||||
if msg_obj.transcript ~= nil then
|
||||
local transcription = msg_obj;
|
||||
|
||||
-- in case of the string matching optimization above failed
|
||||
if transcription.is_interim then
|
||||
return;
|
||||
-- in case of the string matching optimization above failed
|
||||
if transcription.is_interim then
|
||||
return;
|
||||
end
|
||||
|
||||
-- TODO what if we have multiple alternative transcriptions not just 1
|
||||
local text_message = transcription.transcript[1].text;
|
||||
--do not send empty messages
|
||||
if text_message == '' then
|
||||
return;
|
||||
end
|
||||
|
||||
local user_id = transcription.participant.id;
|
||||
local who = room:get_occupant_by_nick(jid.bare(room.jid)..'/'..user_id);
|
||||
|
||||
transcription.jid = who and who.jid;
|
||||
transcription.session_id = room._data.meetingId;
|
||||
|
||||
local tenant, conference_name, id = extract_subdomain(jid.node(room.jid));
|
||||
if tenant then
|
||||
transcription.fqn = tenant..'/'..conference_name;
|
||||
else
|
||||
transcription.fqn = conference_name;
|
||||
end
|
||||
transcription.customer_id = id;
|
||||
|
||||
return module:fire_event('jitsi-transcript-received', {
|
||||
room = room, occupant = occupant, transcription = transcription, stanza = stanza });
|
||||
end
|
||||
|
||||
-- TODO what if we have multiple alternative transcriptions not just 1
|
||||
local text_message = transcription.transcript[1].text;
|
||||
--do not send empty messages
|
||||
if text_message == '' then
|
||||
return;
|
||||
end
|
||||
|
||||
local user_id = transcription.participant.id;
|
||||
local who = room:get_occupant_by_nick(jid.bare(room.jid)..'/'..user_id);
|
||||
|
||||
transcription.jid = who and who.jid;
|
||||
transcription.session_id = room._data.meetingId;
|
||||
|
||||
local tenant, conference_name, id = extract_subdomain(jid.node(room.jid));
|
||||
if tenant then
|
||||
transcription.fqn = tenant..'/'..conference_name;
|
||||
else
|
||||
transcription.fqn = conference_name;
|
||||
end
|
||||
transcription.customer_id = id;
|
||||
|
||||
return module:fire_event('jitsi-transcript-received', {
|
||||
room = room, occupant = occupant, transcription = transcription, stanza = stanza });
|
||||
end
|
||||
|
||||
return module:fire_event('jitsi-endpoint-message-received', {
|
||||
|
||||
@@ -112,9 +112,6 @@ export class Participant {
|
||||
useStunTurn: false
|
||||
},
|
||||
pcStatsInterval: 1500,
|
||||
prejoinConfig: {
|
||||
enabled: false
|
||||
},
|
||||
toolbarConfig: {
|
||||
alwaysVisible: true
|
||||
}
|
||||
@@ -252,6 +249,9 @@ export class Participant {
|
||||
// For the iFrame API the tenant is passed in a different way.
|
||||
url = `/${options.tenant}/${url}`;
|
||||
}
|
||||
if (options.urlAppendString) {
|
||||
url = `${url}${options.urlAppendString}`;
|
||||
}
|
||||
|
||||
await this.driver.url(url);
|
||||
|
||||
@@ -261,6 +261,20 @@ export class Participant {
|
||||
await this.switchToIFrame();
|
||||
}
|
||||
|
||||
if (!options.skipPrejoinButtonClick
|
||||
// @ts-ignore
|
||||
&& !Boolean(await this.execute(() => config.prejoinConfig?.enabled === false))) {
|
||||
// if prejoin is enabled we want to click the join button
|
||||
const p1PreJoinScreen = this.getPreJoinScreen();
|
||||
|
||||
await p1PreJoinScreen.waitForLoading();
|
||||
|
||||
const joinButton = p1PreJoinScreen.getJoinButton();
|
||||
|
||||
await joinButton.waitForDisplayed();
|
||||
await joinButton.click();
|
||||
}
|
||||
|
||||
if (!options.skipWaitToJoin) {
|
||||
await this.waitForMucJoinedOrError();
|
||||
}
|
||||
@@ -674,6 +688,10 @@ export class Participant {
|
||||
return new IframeAPI(this);
|
||||
}
|
||||
|
||||
async getRoomMetadata() {
|
||||
return this.execute(() => window.APP?.conference?._room?.getMetadataHandler()?.getMetadata());
|
||||
}
|
||||
|
||||
/**
|
||||
* Hangups the participant by leaving the page. base.html is an empty page on all deployments.
|
||||
*/
|
||||
|
||||
@@ -8,6 +8,8 @@ export type ITestProperties = {
|
||||
description?: string;
|
||||
/** The test requires the webhook proxy to be available. */
|
||||
requireWebhookProxy: boolean;
|
||||
/** Whether the test should be retried. */
|
||||
retry: boolean;
|
||||
/** The test requires jaas, it should be skipped when the jaas configuration is not enabled. */
|
||||
useJaas: boolean;
|
||||
/** The test uses the webhook proxy if available. */
|
||||
@@ -18,6 +20,7 @@ export type ITestProperties = {
|
||||
const defaultProperties: ITestProperties = {
|
||||
useWebhookProxy: false,
|
||||
requireWebhookProxy: false,
|
||||
retry: false,
|
||||
useJaas: false,
|
||||
usesBrowsers: [ 'p1' ]
|
||||
};
|
||||
|
||||
@@ -176,6 +176,7 @@ export default class WebhookProxy {
|
||||
* @param value
|
||||
*/
|
||||
set defaultMeetingSettings(value: {
|
||||
asyncTranscriptions?: boolean;
|
||||
autoAudioRecording?: boolean;
|
||||
autoTranscriptions?: boolean;
|
||||
autoVideoRecording?: boolean;
|
||||
|
||||
@@ -62,6 +62,11 @@ export type IParticipantJoinOptions = {
|
||||
*/
|
||||
skipDisplayName?: boolean;
|
||||
|
||||
/**
|
||||
* Whether to skip the prejoin button 'Join' click when joining.
|
||||
*/
|
||||
skipPrejoinButtonClick?: boolean;
|
||||
|
||||
/**
|
||||
* Whether to skip waiting for the participant to join the room. Cases like lobby where we do not succeed to join
|
||||
* based on the logic of the test.
|
||||
@@ -72,6 +77,11 @@ export type IParticipantJoinOptions = {
|
||||
* An optional tenant to use. If provided it overrides the default.
|
||||
*/
|
||||
tenant?: string;
|
||||
|
||||
/**
|
||||
* An optional string to append to the URL when joining the room.
|
||||
*/
|
||||
urlAppendString?: string;
|
||||
};
|
||||
|
||||
export type IJoinOptions = {
|
||||
@@ -96,6 +106,11 @@ export type IJoinOptions = {
|
||||
*/
|
||||
skipInMeetingChecks?: boolean;
|
||||
|
||||
/**
|
||||
* Whether to skip the prejoin button 'Join' click when joining.
|
||||
*/
|
||||
skipPrejoinButtonClick?: boolean;
|
||||
|
||||
/**
|
||||
* The skip waiting for the participant to join the room setting to pass to IParticipantJoinOptions.
|
||||
*/
|
||||
@@ -110,4 +125,9 @@ export type IJoinOptions = {
|
||||
* Options used when generating a token.
|
||||
*/
|
||||
tokenOptions?: ITokenOptions;
|
||||
|
||||
/**
|
||||
* An optional string to append to the URL when joining the room.
|
||||
*/
|
||||
urlAppendString?: string;
|
||||
};
|
||||
|
||||
@@ -7,6 +7,7 @@ import { joinJaasMuc, generateJaasToken as t } from '../../helpers/jaas';
|
||||
|
||||
setTestProperties(__filename, {
|
||||
requireWebhookProxy: true,
|
||||
retry: true,
|
||||
useJaas: true
|
||||
});
|
||||
|
||||
|
||||
@@ -8,182 +8,201 @@ import { joinJaasMuc, generateJaasToken as t } from '../../helpers/jaas';
|
||||
|
||||
setTestProperties(__filename, {
|
||||
requireWebhookProxy: true,
|
||||
retry: true,
|
||||
useJaas: true,
|
||||
usesBrowsers: [ 'p1', 'p2' ]
|
||||
});
|
||||
|
||||
describe('Transcription', () => {
|
||||
let p1: Participant, p2: Participant;
|
||||
let webhooksProxy: WebhookProxy;
|
||||
for (const asyncTranscriptions of [ false, true ]) {
|
||||
describe(`Transcription (async=${asyncTranscriptions})`, () => {
|
||||
let p1: Participant, p2: Participant;
|
||||
let webhooksProxy: WebhookProxy;
|
||||
|
||||
it('setup', async () => {
|
||||
const room = ctx.roomName;
|
||||
it('setup', async () => {
|
||||
const room = ctx.roomName;
|
||||
|
||||
webhooksProxy = ctx.webhooksProxy;
|
||||
webhooksProxy = ctx.webhooksProxy;
|
||||
webhooksProxy.defaultMeetingSettings = { asyncTranscriptions };
|
||||
|
||||
p1 = await joinJaasMuc({
|
||||
name: 'p1',
|
||||
token: t({ room, moderator: true }),
|
||||
iFrameApi: true });
|
||||
p1 = await joinJaasMuc({
|
||||
name: 'p1',
|
||||
token: t({ room, moderator: true }),
|
||||
iFrameApi: true
|
||||
});
|
||||
|
||||
const transcriptionEnabled = await p1.execute(() => config.transcription?.enabled);
|
||||
const transcriptionEnabled = await p1.execute(() => config.transcription?.enabled);
|
||||
|
||||
expect(transcriptionEnabled).toBe(expectations.jaas.transcriptionEnabled);
|
||||
expect(transcriptionEnabled).toBe(expectations.jaas.transcriptionEnabled);
|
||||
|
||||
p2 = await joinJaasMuc({
|
||||
name: 'p2',
|
||||
token: t({ room }),
|
||||
iFrameApi: true }, {
|
||||
configOverwrite: {
|
||||
startWithAudioMuted: true
|
||||
const roomMetadata = await p1.getRoomMetadata();
|
||||
|
||||
if (asyncTranscriptions) {
|
||||
expect(roomMetadata.asyncTranscription).toBe(true);
|
||||
} else {
|
||||
expect(roomMetadata.asyncTranscription).toBeFalsy();
|
||||
}
|
||||
|
||||
p2 = await joinJaasMuc({
|
||||
name: 'p2',
|
||||
token: t({ room }),
|
||||
iFrameApi: true
|
||||
}, {
|
||||
configOverwrite: {
|
||||
startWithAudioMuted: true
|
||||
}
|
||||
});
|
||||
|
||||
await Promise.all([
|
||||
p1.switchToMainFrame(),
|
||||
p2.switchToMainFrame(),
|
||||
]);
|
||||
|
||||
expect(await p1.getIframeAPI().getEventResult('isModerator')).toBe(true);
|
||||
expect(await p1.getIframeAPI().getEventResult('videoConferenceJoined')).toBeDefined();
|
||||
});
|
||||
|
||||
await Promise.all([
|
||||
p1.switchToMainFrame(),
|
||||
p2.switchToMainFrame(),
|
||||
]);
|
||||
it('toggle subtitles', async () => {
|
||||
await p1.getIframeAPI().addEventListener('transcriptionChunkReceived');
|
||||
await p2.getIframeAPI().addEventListener('transcriptionChunkReceived');
|
||||
await p1.getIframeAPI().executeCommand('toggleSubtitles');
|
||||
|
||||
expect(await p1.getIframeAPI().getEventResult('isModerator')).toBe(true);
|
||||
expect(await p1.getIframeAPI().getEventResult('videoConferenceJoined')).toBeDefined();
|
||||
});
|
||||
await checkReceivingChunks(p1, p2, webhooksProxy, !asyncTranscriptions);
|
||||
|
||||
it('toggle subtitles', async () => {
|
||||
await p1.getIframeAPI().addEventListener('transcriptionChunkReceived');
|
||||
await p2.getIframeAPI().addEventListener('transcriptionChunkReceived');
|
||||
await p1.getIframeAPI().executeCommand('toggleSubtitles');
|
||||
await p1.getIframeAPI().clearEventResults('transcribingStatusChanged');
|
||||
await p1.getIframeAPI().addEventListener('transcribingStatusChanged');
|
||||
|
||||
await checkReceivingChunks(p1, p2, webhooksProxy);
|
||||
await p1.getIframeAPI().executeCommand('toggleSubtitles');
|
||||
|
||||
await p1.getIframeAPI().clearEventResults('transcribingStatusChanged');
|
||||
await p1.getIframeAPI().addEventListener('transcribingStatusChanged');
|
||||
|
||||
await p1.getIframeAPI().executeCommand('toggleSubtitles');
|
||||
|
||||
await p1.driver.waitUntil(() => p1.getIframeAPI()
|
||||
.getEventResult('transcribingStatusChanged'), {
|
||||
timeout: 15000,
|
||||
timeoutMsg: 'transcribingStatusChanged event not received by p1'
|
||||
});
|
||||
});
|
||||
|
||||
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 p1.getIframeAPI().clearEventResults('transcriptionChunkReceived');
|
||||
await p2.getIframeAPI().clearEventResults('transcriptionChunkReceived');
|
||||
|
||||
await p1.getIframeAPI().executeCommand('setSubtitles', true, true);
|
||||
|
||||
await checkReceivingChunks(p1, p2, webhooksProxy);
|
||||
|
||||
await p1.getIframeAPI().clearEventResults('transcribingStatusChanged');
|
||||
|
||||
await p1.getIframeAPI().executeCommand('setSubtitles', false);
|
||||
|
||||
await p1.driver.waitUntil(() => p1.getIframeAPI()
|
||||
.getEventResult('transcribingStatusChanged'), {
|
||||
timeout: 15000,
|
||||
timeoutMsg: 'transcribingStatusChanged event not received by p1'
|
||||
});
|
||||
});
|
||||
|
||||
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 p1.getIframeAPI().executeCommand('startRecording', { transcription: true });
|
||||
|
||||
let allTranscriptionStatusChanged: Promise<any>[] = [];
|
||||
|
||||
allTranscriptionStatusChanged.push(await p1.driver.waitUntil(() => p1.getIframeAPI()
|
||||
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()
|
||||
timeout: 15000,
|
||||
timeoutMsg: 'transcribingStatusChanged event not received by p1'
|
||||
});
|
||||
});
|
||||
|
||||
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 p1.getIframeAPI().clearEventResults('transcriptionChunkReceived');
|
||||
await p2.getIframeAPI().clearEventResults('transcriptionChunkReceived');
|
||||
|
||||
await p1.getIframeAPI().executeCommand('setSubtitles', true, true);
|
||||
|
||||
await checkReceivingChunks(p1, p2, webhooksProxy, !asyncTranscriptions);
|
||||
|
||||
await p1.getIframeAPI().clearEventResults('transcribingStatusChanged');
|
||||
|
||||
await p1.getIframeAPI().executeCommand('setSubtitles', false);
|
||||
|
||||
await p1.driver.waitUntil(() => p1.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);
|
||||
timeout: 15000,
|
||||
timeoutMsg: 'transcribingStatusChanged event not received by p1'
|
||||
});
|
||||
});
|
||||
|
||||
await checkReceivingChunks(p1, p2, webhooksProxy);
|
||||
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 p1.getIframeAPI().clearEventResults('transcribingStatusChanged');
|
||||
await p2.getIframeAPI().clearEventResults('transcribingStatusChanged');
|
||||
await p2.getIframeAPI().addEventListener('transcribingStatusChanged');
|
||||
|
||||
await p1.getIframeAPI().executeCommand('stopRecording', 'file', true);
|
||||
await p1.getIframeAPI().executeCommand('startRecording', { transcription: true });
|
||||
|
||||
allTranscriptionStatusChanged = [];
|
||||
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'
|
||||
}));
|
||||
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);
|
||||
let result = await Promise.allSettled(allTranscriptionStatusChanged);
|
||||
|
||||
expect(result.length).toBe(2);
|
||||
expect(result.length).toBe(2);
|
||||
|
||||
result.forEach(e => {
|
||||
// @ts-ignore
|
||||
expect(e.value.on).toBe(false);
|
||||
result.forEach(e => {
|
||||
// @ts-ignore
|
||||
expect(e.value.on).toBe(true);
|
||||
});
|
||||
|
||||
await checkReceivingChunks(p1, p2, webhooksProxy, !asyncTranscriptions);
|
||||
|
||||
await p1.getIframeAPI().clearEventResults('transcribingStatusChanged');
|
||||
await p2.getIframeAPI().clearEventResults('transcribingStatusChanged');
|
||||
|
||||
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 p1.getIframeAPI().executeCommand('hangup');
|
||||
await p2.getIframeAPI().executeCommand('hangup');
|
||||
|
||||
// sometimes events are not immediately received,
|
||||
// let's wait for destroy event before waiting for those that depends on it
|
||||
await webhooksProxy.waitForEvent('ROOM_DESTROYED');
|
||||
|
||||
const event: {
|
||||
data: {
|
||||
preAuthenticatedLink: string;
|
||||
};
|
||||
eventType: string;
|
||||
} = await webhooksProxy.waitForEvent('TRANSCRIPTION_UPLOADED');
|
||||
|
||||
expect(event.eventType).toBe('TRANSCRIPTION_UPLOADED');
|
||||
expect(event.data.preAuthenticatedLink).toBeDefined();
|
||||
});
|
||||
|
||||
await p1.getIframeAPI().executeCommand('hangup');
|
||||
await p2.getIframeAPI().executeCommand('hangup');
|
||||
|
||||
// sometimes events are not immediately received,
|
||||
// let's wait for destroy event before waiting for those that depends on it
|
||||
await webhooksProxy.waitForEvent('ROOM_DESTROYED');
|
||||
|
||||
const event: {
|
||||
data: {
|
||||
preAuthenticatedLink: string;
|
||||
};
|
||||
eventType: string;
|
||||
} = await webhooksProxy.waitForEvent('TRANSCRIPTION_UPLOADED');
|
||||
|
||||
expect('TRANSCRIPTION_UPLOADED').toBe(event.eventType);
|
||||
expect(event.data.preAuthenticatedLink).toBeDefined();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async function checkReceivingChunks(p1: Participant, p2: Participant, webhooksProxy: WebhookProxy) {
|
||||
const allTranscripts: Promise<any>[] = [];
|
||||
|
||||
allTranscripts.push(await p1.driver.waitUntil(() => p1.getIframeAPI()
|
||||
/**
|
||||
*
|
||||
* @param p1
|
||||
* @param p2
|
||||
* @param webhooksProxy
|
||||
* @param expectName Whether to expect the events to contain the name of the participant. Currently, async
|
||||
* transcriptions do not include the name. TODO: remove this parameter when async transcription events are fixed.
|
||||
*/
|
||||
async function checkReceivingChunks(p1: Participant, p2: Participant, webhooksProxy: WebhookProxy, expectName = true) {
|
||||
const p1Promise = p1.driver.waitUntil(() => p1.getIframeAPI()
|
||||
.getEventResult('transcriptionChunkReceived'), {
|
||||
timeout: 60000,
|
||||
timeoutMsg: 'transcriptionChunkReceived event not received on p1 side'
|
||||
}));
|
||||
});
|
||||
|
||||
allTranscripts.push(await p2.driver.waitUntil(() => p2.getIframeAPI()
|
||||
const p2Promise = p2.driver.waitUntil(() => p2.getIframeAPI()
|
||||
.getEventResult('transcriptionChunkReceived'), {
|
||||
timeout: 60000,
|
||||
timeoutMsg: 'transcriptionChunkReceived event not received on p2 side'
|
||||
}));
|
||||
});
|
||||
|
||||
// TRANSCRIPTION_CHUNK_RECEIVED webhook
|
||||
allTranscripts.push((async () => {
|
||||
const webhookPromise = async () => {
|
||||
const event: {
|
||||
data: {
|
||||
final: string;
|
||||
@@ -198,42 +217,36 @@ async function checkReceivingChunks(p1: Participant, p2: Participant, webhooksPr
|
||||
eventType: string;
|
||||
} = await webhooksProxy.waitForEvent('TRANSCRIPTION_CHUNK_RECEIVED');
|
||||
|
||||
expect('TRANSCRIPTION_CHUNK_RECEIVED').toBe(event.eventType);
|
||||
expect(event.eventType).toBe('TRANSCRIPTION_CHUNK_RECEIVED');
|
||||
|
||||
event.data.stable = event.data.final;
|
||||
|
||||
return event;
|
||||
})());
|
||||
};
|
||||
|
||||
const result = await Promise.allSettled(allTranscripts);
|
||||
|
||||
expect(result.length).toBeGreaterThan(0);
|
||||
|
||||
// @ts-ignore
|
||||
const firstEntryData = result[0].value.data;
|
||||
const stable = firstEntryData.stable || firstEntryData.final;
|
||||
const language = firstEntryData.language;
|
||||
const messageID = firstEntryData.messageID;
|
||||
const [ p1Event, p2Event, webhookEvent ] = await Promise.all([ p1Promise, p2Promise, await webhookPromise() ]);
|
||||
const p1Id = await p1.getEndpointId();
|
||||
|
||||
result.map(r => {
|
||||
// @ts-ignore
|
||||
const v = r.value;
|
||||
const p1Transcript = p1Event.data.stable || p1Event.data.final;
|
||||
const p2Transcript = p2Event.data.stable || p2Event.data.final;
|
||||
const webhookTranscript = webhookEvent.data.stable || webhookEvent.data.final;
|
||||
|
||||
expect(v).toBeDefined();
|
||||
expect(p2Transcript.includes(p1Transcript) || p1Transcript.includes(p2Transcript)).toBe(true);
|
||||
expect(webhookTranscript.includes(p1Transcript) || p1Transcript.includes(webhookTranscript)).toBe(true);
|
||||
|
||||
return v.data;
|
||||
}).forEach(tr => {
|
||||
const checkTranscripts = stable.includes(tr.stable || tr.final) || (tr.stable || tr.final).includes(stable);
|
||||
expect(p2Event.data.language).toBe(p1Event.data.language);
|
||||
expect(webhookEvent.data.language).toBe(p1Event.data.language);
|
||||
|
||||
if (!checkTranscripts) {
|
||||
console.log('received events', JSON.stringify(result));
|
||||
}
|
||||
expect(p2Event.data.messageID).toBe(p1Event.data.messageID);
|
||||
expect(webhookEvent.data.messageID).toBe(p1Event.data.messageID);
|
||||
|
||||
expect(checkTranscripts).toBe(true);
|
||||
expect(tr.language).toBe(language);
|
||||
expect(tr.messageID).toBe(messageID);
|
||||
expect(tr.participant.id).toBe(p1Id);
|
||||
expect(tr.participant.name).toBe(p1.name);
|
||||
});
|
||||
expect(p1Event.data.participant.id).toBe(p1Id);
|
||||
expect(p2Event.data.participant.id).toBe(p1Id);
|
||||
expect(webhookEvent.data.participant.id).toBe(p1Id);
|
||||
|
||||
if (expectName) {
|
||||
expect(p1Event.data.participant.name).toBe(p1.name);
|
||||
expect(p2Event.data.participant.name).toBe(p1.name);
|
||||
expect(webhookEvent.data.participant.name).toBe(p1.name);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -183,7 +183,8 @@ describe('Desktop sharing', () => {
|
||||
p2p: {
|
||||
backToP2PDelay: 3,
|
||||
enabled: true
|
||||
}
|
||||
},
|
||||
startWithAudioMuted: false
|
||||
}
|
||||
});
|
||||
const { p1 } = ctx;
|
||||
@@ -201,7 +202,8 @@ describe('Desktop sharing', () => {
|
||||
p2p: {
|
||||
backToP2PDelay: 3,
|
||||
enabled: true
|
||||
}
|
||||
},
|
||||
startWithAudioMuted: false
|
||||
}
|
||||
});
|
||||
const { p2, p3 } = ctx;
|
||||
@@ -304,11 +306,16 @@ describe('Desktop sharing', () => {
|
||||
|
||||
await ensureTwoParticipants({
|
||||
configOverwrite: {
|
||||
startWithAudioMuted: true
|
||||
startWithAudioMuted: true,
|
||||
startWithVideoMuted: false
|
||||
},
|
||||
skipInMeetingChecks: true
|
||||
});
|
||||
await ensureThreeParticipants({
|
||||
configOverwrite: {
|
||||
startWithAudioMuted: false,
|
||||
startWithVideoMuted: false
|
||||
},
|
||||
skipInMeetingChecks: true
|
||||
});
|
||||
const { p2, p3 } = ctx;
|
||||
@@ -334,7 +341,12 @@ describe('Desktop sharing', () => {
|
||||
it('with lastN', async () => {
|
||||
await hangupAllParticipants();
|
||||
|
||||
await ensureThreeParticipants();
|
||||
await ensureThreeParticipants({
|
||||
configOverwrite: {
|
||||
startWithAudioMuted: false,
|
||||
startWithVideoMuted: false
|
||||
},
|
||||
});
|
||||
const { p1, p2, p3 } = ctx;
|
||||
|
||||
await p3.getToolbar().clickDesktopSharingButton();
|
||||
@@ -345,7 +357,8 @@ describe('Desktop sharing', () => {
|
||||
await ensureFourParticipants({
|
||||
configOverwrite: {
|
||||
channelLastN: 2,
|
||||
startWithAudioMuted: true
|
||||
startWithAudioMuted: true,
|
||||
startWithVideoMuted: false
|
||||
}
|
||||
});
|
||||
const { p4 } = ctx;
|
||||
|
||||
44
tests/specs/media/prejoinDisabled.spec.ts
Normal file
44
tests/specs/media/prejoinDisabled.spec.ts
Normal file
@@ -0,0 +1,44 @@
|
||||
import type { Participant } from '../../helpers/Participant';
|
||||
import { setTestProperties } from '../../helpers/TestProperties';
|
||||
import { ensureTwoParticipants, hangupAllParticipants } from '../../helpers/participants';
|
||||
|
||||
setTestProperties(__filename, {
|
||||
usesBrowsers: [ 'p1', 'p2' ]
|
||||
});
|
||||
|
||||
describe('Prejoin disabled', () => {
|
||||
it('joining with no prejoin - nested object', async () => {
|
||||
await ensureTwoParticipants({
|
||||
configOverwrite: {
|
||||
prejoinConfig: {
|
||||
enabled: false
|
||||
}
|
||||
},
|
||||
skipInMeetingChecks: true,
|
||||
});
|
||||
|
||||
await checkEveryoneMuted(ctx);
|
||||
|
||||
await hangupAllParticipants();
|
||||
});
|
||||
it('joining with no prejoin - direct url param ', async () => {
|
||||
await ensureTwoParticipants({
|
||||
skipInMeetingChecks: true,
|
||||
urlAppendString: '&config.prejoinConfig.enabled=false'
|
||||
});
|
||||
|
||||
await checkEveryoneMuted(ctx);
|
||||
});
|
||||
});
|
||||
|
||||
async function checkEveryoneMuted({ p1, p2 }: { p1: Participant; p2: Participant; }) {
|
||||
await p1.getFilmstrip().assertAudioMuteIconIsDisplayed(p2);
|
||||
await p1.getFilmstrip().assertAudioMuteIconIsDisplayed(p1);
|
||||
await p2.getFilmstrip().assertAudioMuteIconIsDisplayed(p2);
|
||||
await p2.getFilmstrip().assertAudioMuteIconIsDisplayed(p1);
|
||||
|
||||
await p1.getParticipantsPane().assertAudioMuteIconIsDisplayed(p2);
|
||||
await p1.getParticipantsPane().assertAudioMuteIconIsDisplayed(p1);
|
||||
await p2.getParticipantsPane().assertAudioMuteIconIsDisplayed(p2);
|
||||
await p2.getParticipantsPane().assertAudioMuteIconIsDisplayed(p1);
|
||||
}
|
||||
@@ -190,6 +190,7 @@ describe('Start muted', () => {
|
||||
const options = {
|
||||
configOverwrite: {
|
||||
startWithAudioMuted: true,
|
||||
startWithVideoMuted: false,
|
||||
testing: {
|
||||
testMode: true,
|
||||
debugAudioLevels: true
|
||||
@@ -228,7 +229,9 @@ describe('Start muted', () => {
|
||||
},
|
||||
p2p: {
|
||||
enabled: true
|
||||
}
|
||||
},
|
||||
startWithAudioMuted: false,
|
||||
startWithVideoMuted: false
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
} from '../../helpers/participants';
|
||||
|
||||
setTestProperties(__filename, {
|
||||
retry: true,
|
||||
usesBrowsers: [ 'p1', 'p2', 'p3' ]
|
||||
});
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@ describe('XMPP Conference Request', () => {
|
||||
it('join with conferenceRequestUrl disabled', async () => {
|
||||
await ensureOneParticipant({
|
||||
skipWaitToJoin: true,
|
||||
skipPrejoinButtonClick: true,
|
||||
configOverwrite: {
|
||||
prejoinConfig: {
|
||||
enabled: true
|
||||
|
||||
@@ -49,7 +49,7 @@ describe('Lobby', () => {
|
||||
// media is being receiving and there are two remote streams
|
||||
await p3.waitToJoinMUC();
|
||||
await p3.waitForIceConnected();
|
||||
await p3.waitForSendReceiveData();
|
||||
await p3.waitForReceiveMedia();
|
||||
await p3.waitForRemoteStreams(2);
|
||||
|
||||
// now check third one display name in the room, is the one set in the prejoin screen
|
||||
@@ -107,7 +107,7 @@ describe('Lobby', () => {
|
||||
// media is being receiving and there are two remote streams
|
||||
await p3.waitToJoinMUC();
|
||||
await p3.waitForIceConnected();
|
||||
await p3.waitForSendReceiveData();
|
||||
await p3.waitForReceiveMedia();
|
||||
await p3.waitForRemoteStreams(2);
|
||||
|
||||
// now check third one display name in the room, is the one set in the prejoin screen
|
||||
@@ -275,7 +275,7 @@ describe('Lobby', () => {
|
||||
|
||||
await p3.waitToJoinMUC();
|
||||
await p3.waitForIceConnected();
|
||||
await p3.waitForSendReceiveData();
|
||||
await p3.waitForReceiveMedia();
|
||||
});
|
||||
|
||||
it('enable with more than two participants', async () => {
|
||||
@@ -430,19 +430,18 @@ async function enableLobby() {
|
||||
async function enterLobby(participant: Participant, enterDisplayName = false, usePreJoin = false) {
|
||||
const options: IJoinOptions = { };
|
||||
|
||||
if (usePreJoin) {
|
||||
options.configOverwrite = {
|
||||
prejoinConfig: {
|
||||
enabled: true
|
||||
}
|
||||
};
|
||||
}
|
||||
options.configOverwrite = {
|
||||
prejoinConfig: {
|
||||
enabled: usePreJoin
|
||||
}
|
||||
};
|
||||
|
||||
await ensureThreeParticipants({
|
||||
...options,
|
||||
skipDisplayName: true,
|
||||
skipWaitToJoin: true,
|
||||
skipInMeetingChecks: true
|
||||
skipInMeetingChecks: true,
|
||||
skipPrejoinButtonClick: true
|
||||
});
|
||||
|
||||
const { p3 } = ctx;
|
||||
|
||||
@@ -15,7 +15,8 @@ describe('Pre-join screen', () => {
|
||||
requireDisplayName: true
|
||||
},
|
||||
skipDisplayName: true,
|
||||
skipWaitToJoin: true
|
||||
skipWaitToJoin: true,
|
||||
skipPrejoinButtonClick: true
|
||||
});
|
||||
|
||||
const p1PreJoinScreen = ctx.p1.getPreJoinScreen();
|
||||
@@ -42,7 +43,8 @@ describe('Pre-join screen', () => {
|
||||
}
|
||||
},
|
||||
skipDisplayName: true,
|
||||
skipWaitToJoin: true
|
||||
skipWaitToJoin: true,
|
||||
skipPrejoinButtonClick: true
|
||||
});
|
||||
|
||||
const p1PreJoinScreen = ctx.p1.getPreJoinScreen();
|
||||
@@ -64,7 +66,8 @@ describe('Pre-join screen', () => {
|
||||
}
|
||||
},
|
||||
skipDisplayName: true,
|
||||
skipWaitToJoin: true
|
||||
skipWaitToJoin: true,
|
||||
skipPrejoinButtonClick: true
|
||||
});
|
||||
|
||||
const { p1 } = ctx;
|
||||
@@ -111,7 +114,8 @@ describe('Pre-join screen', () => {
|
||||
}
|
||||
},
|
||||
skipDisplayName: true,
|
||||
skipWaitToJoin: true
|
||||
skipWaitToJoin: true,
|
||||
skipPrejoinButtonClick: true
|
||||
});
|
||||
|
||||
const p1PreJoinScreen = ctx.p2.getPreJoinScreen();
|
||||
|
||||
@@ -288,9 +288,23 @@ export const config: WebdriverIO.MultiremoteConfig = {
|
||||
keepAlive.forEach(clearInterval);
|
||||
},
|
||||
|
||||
beforeSession(c, capabilities_, specs_, cid) {
|
||||
async beforeSession(c, capabilities_, spec, cid) {
|
||||
const originalBefore = c.before;
|
||||
|
||||
if (spec && spec.length == 1) {
|
||||
const testFilePath = spec[0].replace(/^file:\/\//, '');
|
||||
const testProperties = await getTestProperties(testFilePath);
|
||||
|
||||
if (testProperties.retry) {
|
||||
c.specFileRetries = 1;
|
||||
c.specFileRetriesDeferred = true;
|
||||
c.specFileRetriesDelay = 1;
|
||||
console.log(`Enabling retry for ${testFilePath}`);
|
||||
}
|
||||
} else {
|
||||
console.log('No test file or multiple test files specified, will not enable retries');
|
||||
}
|
||||
|
||||
if (!originalBefore || !Array.isArray(originalBefore) || originalBefore.length !== 1) {
|
||||
console.warn('No before hook found or more than one found, skipping');
|
||||
|
||||
|
||||
Reference in New Issue
Block a user