diff --git a/.gitignore b/.gitignore
index 130871f7..eaf43f60 100644
--- a/.gitignore
+++ b/.gitignore
@@ -37,7 +37,7 @@ pnpm-debug.log*
**/**/test-results
-**/**/public/
+**/backend/public/
**/*/coverage
**/**/test-results
diff --git a/meet-ce/frontend/projects/shared-meet-components/src/lib/guards/extract-query-params.guard.ts b/meet-ce/frontend/projects/shared-meet-components/src/lib/guards/extract-query-params.guard.ts
index 0d461e0c..25a02d50 100644
--- a/meet-ce/frontend/projects/shared-meet-components/src/lib/guards/extract-query-params.guard.ts
+++ b/meet-ce/frontend/projects/shared-meet-components/src/lib/guards/extract-query-params.guard.ts
@@ -1,13 +1,7 @@
import { inject } from '@angular/core';
import { ActivatedRouteSnapshot, CanActivateFn } from '@angular/router';
import { ErrorReason } from '../models';
-import {
- AppDataService,
- NavigationService,
- ParticipantService,
- RoomService,
- SessionStorageService
-} from '../services';
+import { AppDataService, NavigationService, ParticipantService, RoomService, SessionStorageService } from '../services';
import { WebComponentProperty } from '@openvidu-meet/typings';
export const extractRoomQueryParamsGuard: CanActivateFn = (route: ActivatedRouteSnapshot) => {
@@ -16,7 +10,14 @@ export const extractRoomQueryParamsGuard: CanActivateFn = (route: ActivatedRoute
const participantService = inject(ParticipantService);
const sessionStorageService = inject(SessionStorageService);
- const { roomId, secret: querySecret, participantName, leaveRedirectUrl, showOnlyRecordings } = extractParams(route);
+ const {
+ roomId,
+ secret: querySecret,
+ participantName,
+ leaveRedirectUrl,
+ showOnlyRecordings,
+ e2eeKey
+ } = extractParams(route);
const secret = querySecret || sessionStorageService.getRoomSecret();
// Handle leave redirect URL logic
@@ -29,6 +30,7 @@ export const extractRoomQueryParamsGuard: CanActivateFn = (route: ActivatedRoute
roomService.setRoomId(roomId);
roomService.setRoomSecret(secret);
+ roomService.setE2EEKey(e2eeKey);
if (participantName) {
participantService.setParticipantName(participantName);
@@ -66,7 +68,8 @@ const extractParams = ({ params, queryParams }: ActivatedRouteSnapshot) => ({
secret: queryParams['secret'] as string,
participantName: queryParams[WebComponentProperty.PARTICIPANT_NAME] as string,
leaveRedirectUrl: queryParams[WebComponentProperty.LEAVE_REDIRECT_URL] as string,
- showOnlyRecordings: (queryParams[WebComponentProperty.SHOW_ONLY_RECORDINGS] as string) || 'false'
+ showOnlyRecordings: (queryParams[WebComponentProperty.SHOW_ONLY_RECORDINGS] as string) || 'false',
+ e2eeKey: queryParams[WebComponentProperty.E2EE_KEY] as string
});
/**
diff --git a/meet-ce/frontend/projects/shared-meet-components/src/lib/guards/index.ts b/meet-ce/frontend/projects/shared-meet-components/src/lib/guards/index.ts
index 5d5b2404..435d7e29 100644
--- a/meet-ce/frontend/projects/shared-meet-components/src/lib/guards/index.ts
+++ b/meet-ce/frontend/projects/shared-meet-components/src/lib/guards/index.ts
@@ -1,5 +1,5 @@
export * from './auth.guard';
export * from './extract-query-params.guard';
-export * from './remove-secret.guard';
+export * from './remove-query-params.guard';
export * from './run-serially.guard';
export * from './validate-access.guard';
diff --git a/meet-ce/frontend/projects/shared-meet-components/src/lib/guards/remove-query-params.guard.ts b/meet-ce/frontend/projects/shared-meet-components/src/lib/guards/remove-query-params.guard.ts
new file mode 100644
index 00000000..61524f79
--- /dev/null
+++ b/meet-ce/frontend/projects/shared-meet-components/src/lib/guards/remove-query-params.guard.ts
@@ -0,0 +1,49 @@
+import { inject } from '@angular/core';
+import { ActivatedRouteSnapshot, CanActivateFn, NavigationEnd, Router } from '@angular/router';
+import { filter, take } from 'rxjs';
+import { NavigationService } from '../services';
+
+/**
+ * Guard that removes specified query parameters from the URL after the navigation completes.
+ *
+ * @param params - Array of query parameter names to remove from the URL
+ * @returns A guard function that schedules removal of the specified query parameters after navigation
+ *
+ */
+export const removeQueryParamsGuard = (params: string[]): CanActivateFn => {
+ return (route: ActivatedRouteSnapshot) => {
+ const router = inject(Router);
+ const navigationService = inject(NavigationService);
+
+ // Only proceed if there are params to remove
+ if (!params || params.length === 0) {
+ return true;
+ }
+
+ // Check if any of the specified params exist in the current query params
+ const hasParamsToRemove = params.some((param) => route.queryParams[param] !== undefined);
+
+ if (!hasParamsToRemove) {
+ // No params to remove, continue navigation immediately
+ return true;
+ }
+
+ // Schedule param removal AFTER navigation completes
+ // This prevents conflicts with the ongoing navigation
+ router.events
+ .pipe(
+ filter((event) => event instanceof NavigationEnd),
+ take(1)
+ )
+ .subscribe(async () => {
+ try {
+ await navigationService.removeQueryParamsFromUrl(route.queryParams, params);
+ } catch (error) {
+ console.error('Error removing query params:', error);
+ }
+ });
+
+ // Allow the current navigation to proceed
+ return true;
+ };
+};
diff --git a/meet-ce/frontend/projects/shared-meet-components/src/lib/guards/remove-secret.guard.ts b/meet-ce/frontend/projects/shared-meet-components/src/lib/guards/remove-secret.guard.ts
deleted file mode 100644
index b7d8bcf3..00000000
--- a/meet-ce/frontend/projects/shared-meet-components/src/lib/guards/remove-secret.guard.ts
+++ /dev/null
@@ -1,25 +0,0 @@
-import { inject } from '@angular/core';
-import { CanActivateFn, NavigationEnd, Router } from '@angular/router';
-import { NavigationService } from '../services';
-import { filter, take } from 'rxjs';
-
-/**
- * Guard that intercepts navigation to remove the 'secret' query parameter from the URL
- * that determine the role of a participant when joining a room or accessing its recordings,
- * in order to enhance security.
- */
-export const removeRoomSecretGuard: CanActivateFn = (route, _state) => {
- const router = inject(Router);
- const navigationService = inject(NavigationService);
-
- router.events
- .pipe(
- filter((event) => event instanceof NavigationEnd),
- take(1)
- )
- .subscribe(async () => {
- await navigationService.removeQueryParamFromUrl(route.queryParams, 'secret');
- });
-
- return true;
-};
diff --git a/meet-ce/frontend/projects/shared-meet-components/src/lib/pages/console/rooms/room-wizard/steps/room-config/room-config.component.html b/meet-ce/frontend/projects/shared-meet-components/src/lib/pages/console/rooms/room-wizard/steps/room-config/room-config.component.html
index d48d2669..d117c2eb 100644
--- a/meet-ce/frontend/projects/shared-meet-components/src/lib/pages/console/rooms/room-wizard/steps/room-config/room-config.component.html
+++ b/meet-ce/frontend/projects/shared-meet-components/src/lib/pages/console/rooms/room-wizard/steps/room-config/room-config.component.html
@@ -52,8 +52,7 @@
message but will be unable to see or hear others.
-
Recording is unavailable while encryption is enabled.
- Chat messages are not protected by end-to-end encryption.
+ Recording is unavailable while encryption is enabled.
diff --git a/meet-ce/frontend/projects/shared-meet-components/src/lib/routes/base-routes.ts b/meet-ce/frontend/projects/shared-meet-components/src/lib/routes/base-routes.ts
index d9b42d91..386d4d01 100644
--- a/meet-ce/frontend/projects/shared-meet-components/src/lib/routes/base-routes.ts
+++ b/meet-ce/frontend/projects/shared-meet-components/src/lib/routes/base-routes.ts
@@ -6,7 +6,7 @@ import {
checkUserNotAuthenticatedGuard,
extractRecordingQueryParamsGuard,
extractRoomQueryParamsGuard,
- removeRoomSecretGuard,
+ removeQueryParamsGuard,
runGuardsSerially,
validateRecordingAccessGuard,
validateRoomAccessGuard
@@ -27,6 +27,7 @@ import {
ViewRecordingComponent,
ConfigComponent
} from '../pages';
+import { WebComponentProperty } from '@openvidu-meet/typings';
export const baseRoutes: Routes = [
{
@@ -40,9 +41,9 @@ export const baseRoutes: Routes = [
canActivate: [
runGuardsSerially(
extractRoomQueryParamsGuard,
+ removeQueryParamsGuard(['secret', WebComponentProperty.E2EE_KEY]),
checkParticipantRoleAndAuthGuard,
- validateRoomAccessGuard,
- removeRoomSecretGuard
+ validateRoomAccessGuard
)
]
},
@@ -52,9 +53,9 @@ export const baseRoutes: Routes = [
canActivate: [
runGuardsSerially(
extractRecordingQueryParamsGuard,
+ removeQueryParamsGuard(['secret', WebComponentProperty.E2EE_KEY]),
checkParticipantRoleAndAuthGuard,
- validateRecordingAccessGuard,
- removeRoomSecretGuard
+ validateRecordingAccessGuard
)
]
},
diff --git a/meet-ce/frontend/projects/shared-meet-components/src/lib/services/meeting/meeting-event-handler.service.ts b/meet-ce/frontend/projects/shared-meet-components/src/lib/services/meeting/meeting-event-handler.service.ts
index dd56aae6..74a2e3c3 100644
--- a/meet-ce/frontend/projects/shared-meet-components/src/lib/services/meeting/meeting-event-handler.service.ts
+++ b/meet-ce/frontend/projects/shared-meet-components/src/lib/services/meeting/meeting-event-handler.service.ts
@@ -92,38 +92,48 @@ export class MeetingEventHandlerService {
): void {
room.on(
RoomEvent.DataReceived,
- async (
- payload: Uint8Array,
- _participant?: RemoteParticipant,
- _kind?: DataPacket_Kind,
- topic?: string
- ) => {
- const event = JSON.parse(new TextDecoder().decode(payload));
+ async (payload: Uint8Array, _participant?: RemoteParticipant, _kind?: DataPacket_Kind, topic?: string) => {
+ // Only process topics that this handler is responsible for
+ const relevantTopics = [
+ 'recordingStopped',
+ MeetSignalType.MEET_ROOM_CONFIG_UPDATED,
+ MeetSignalType.MEET_PARTICIPANT_ROLE_UPDATED
+ ];
- switch (topic) {
- case 'recordingStopped':
- await this.handleRecordingStopped(
- context.roomId,
- context.roomSecret,
- context.onHasRecordingsChanged
- );
- break;
+ if (!topic || !relevantTopics.includes(topic)) {
+ return;
+ }
- case MeetSignalType.MEET_ROOM_CONFIG_UPDATED:
- await this.handleRoomConfigUpdated(event, context.roomId, context.roomSecret);
- break;
+ try {
+ const event = JSON.parse(new TextDecoder().decode(payload));
- case MeetSignalType.MEET_PARTICIPANT_ROLE_UPDATED:
- await this.handleParticipantRoleUpdated(
- event,
- context.roomId,
- context.participantName,
- context.localParticipant,
- context.remoteParticipants,
- context.onRoomSecretChanged,
- context.onParticipantRoleUpdated
- );
- break;
+ switch (topic) {
+ case 'recordingStopped':
+ await this.handleRecordingStopped(
+ context.roomId,
+ context.roomSecret,
+ context.onHasRecordingsChanged
+ );
+ break;
+
+ case MeetSignalType.MEET_ROOM_CONFIG_UPDATED:
+ await this.handleRoomConfigUpdated(event, context.roomId, context.roomSecret);
+ break;
+
+ case MeetSignalType.MEET_PARTICIPANT_ROLE_UPDATED:
+ await this.handleParticipantRoleUpdated(
+ event,
+ context.roomId,
+ context.participantName,
+ context.localParticipant,
+ context.remoteParticipants,
+ context.onRoomSecretChanged,
+ context.onParticipantRoleUpdated
+ );
+ break;
+ }
+ } catch (error) {
+ console.warn(`Failed to parse data message for topic: ${topic}`, error);
}
}
);
@@ -203,7 +213,7 @@ export class MeetingEventHandlerService {
if (error.status === 503) {
console.error(
'No egress service available. Check CPU usage or Media Node capacity. ' +
- 'By default, a recording uses 2 CPUs per room.'
+ 'By default, a recording uses 2 CPUs per room.'
);
} else {
console.error('Error starting recording:', error);
diff --git a/meet-ce/frontend/projects/shared-meet-components/src/lib/services/meeting/meeting-lobby.service.ts b/meet-ce/frontend/projects/shared-meet-components/src/lib/services/meeting/meeting-lobby.service.ts
index bede6ce6..f16d9918 100644
--- a/meet-ce/frontend/projects/shared-meet-components/src/lib/services/meeting/meeting-lobby.service.ts
+++ b/meet-ce/frontend/projects/shared-meet-components/src/lib/services/meeting/meeting-lobby.service.ts
@@ -72,6 +72,10 @@ export class MeetingLobbyService {
return value.name.trim();
}
+ set e2eeKey(key: string) {
+ this.state.participantForm.get('e2eeKey')?.setValue(key);
+ }
+
get e2eeKey(): string {
const { valid, value } = this.state.participantForm;
if (!valid || !value.e2eeKey?.trim()) {
@@ -93,6 +97,12 @@ export class MeetingLobbyService {
// If E2EE is enabled, require e2eeKey
if (this.state.isE2EEEnabled) {
this.state.participantForm.get('e2eeKey')?.setValidators([Validators.required]);
+ this.e2eeKey = this.roomService.getE2EEKey();
+
+ if (this.e2eeKey) {
+ // when e2eeKey is already set (e.g., from URL or webcomponent), populate and disable field
+ this.state.participantForm.get('e2eeKey')?.disable();
+ }
this.state.participantForm.get('e2eeKey')?.updateValueAndValidity();
}
@@ -145,10 +155,16 @@ export class MeetingLobbyService {
}
async submitAccess(): Promise {
- if (!this.participantName) {
+ const sanitized = this.participantName
+ .replace(/[^a-zA-Z0-9 _-]/g, '') // remove invalid chars
+ .replace(/\s+/g, ' ') // normalize spaces
+ .trim(); // remove leading/trailing spaces
+
+ if (!sanitized) {
console.error('Participant form is invalid. Cannot access meeting.');
throw new Error('Participant form is invalid');
}
+ this.participantName = sanitized;
// For E2EE rooms, validate passkey
if (this.state.isE2EEEnabled && !this.e2eeKey) {
@@ -243,11 +259,14 @@ export class MeetingLobbyService {
*/
protected async generateParticipantToken() {
try {
- this.state.participantToken = await this.participantService.generateToken({
- roomId: this.state.roomId,
- secret: this.state.roomSecret,
- participantName: this.participantName
- });
+ this.state.participantToken = await this.participantService.generateToken(
+ {
+ roomId: this.state.roomId,
+ secret: this.state.roomSecret,
+ participantName: this.participantName
+ },
+ this.e2eeKey
+ );
this.participantName = this.participantService.getParticipantName()!;
} catch (error: any) {
console.error('Error generating participant token:', error);
diff --git a/meet-ce/frontend/projects/shared-meet-components/src/lib/services/navigation.service.ts b/meet-ce/frontend/projects/shared-meet-components/src/lib/services/navigation.service.ts
index e0f55712..2c905b7c 100644
--- a/meet-ce/frontend/projects/shared-meet-components/src/lib/services/navigation.service.ts
+++ b/meet-ce/frontend/projects/shared-meet-components/src/lib/services/navigation.service.ts
@@ -177,8 +177,25 @@ export class NavigationService {
* @param param - The parameter to remove
*/
async removeQueryParamFromUrl(queryParams: Params, param: string): Promise {
+ await this.removeQueryParamsFromUrl(queryParams, [param]);
+ }
+
+ /**
+ * Removes multiple query parameters from the URL in a single navigation operation.
+ * This is more efficient than removing params one by one, as it only triggers one navigation.
+ *
+ * @param queryParams - The current query parameters
+ * @param params - Array of parameter names to remove
+ */
+ async removeQueryParamsFromUrl(queryParams: Params, params: string[]): Promise {
+ if (!params || params.length === 0) {
+ return;
+ }
+
const updatedParams = { ...queryParams };
- delete updatedParams[param];
+ params.forEach((param) => {
+ delete updatedParams[param];
+ });
await this.router.navigate([], {
queryParams: updatedParams,
diff --git a/meet-ce/frontend/projects/shared-meet-components/src/lib/services/participant.service.ts b/meet-ce/frontend/projects/shared-meet-components/src/lib/services/participant.service.ts
index b46dff7e..d4f2bc54 100644
--- a/meet-ce/frontend/projects/shared-meet-components/src/lib/services/participant.service.ts
+++ b/meet-ce/frontend/projects/shared-meet-components/src/lib/services/participant.service.ts
@@ -8,7 +8,7 @@ import {
ParticipantRole
} from '@openvidu-meet/typings';
import { getValidDecodedToken } from '../utils';
-import { LoggerService } from 'openvidu-components-angular';
+import { E2eeService, LoggerService } from 'openvidu-components-angular';
@Injectable({
providedIn: 'root'
@@ -29,7 +29,8 @@ export class ParticipantService {
protected httpService: HttpService,
protected featureConfService: FeatureConfigurationService,
protected globalConfigService: GlobalConfigService,
- protected tokenStorageService: TokenStorageService
+ protected tokenStorageService: TokenStorageService,
+ protected e2eeService: E2eeService
) {
this.log = this.loggerService.get('OpenVidu Meet - ParticipantTokenService');
}
@@ -53,8 +54,15 @@ export class ParticipantService {
* @param participantOptions - The options for the participant, including room ID, participant name, and secret
* @return A promise that resolves to the participant token
*/
- async generateToken(participantOptions: ParticipantOptions): Promise {
+ async generateToken(participantOptions: ParticipantOptions, e2EEKey = ''): Promise {
const path = `${this.PARTICIPANTS_API}/token`;
+
+ if (participantOptions.participantName && !!e2EEKey) {
+ // Asign E2EE key and encrypt participant name
+ await this.e2eeService.setE2EEKey(e2EEKey);
+ participantOptions.participantName = await this.e2eeService.encrypt(participantOptions.participantName);
+ }
+
const { token } = await this.httpService.postRequest<{ token: string }>(path, participantOptions);
// Store token in sessionStorage for header mode
@@ -63,7 +71,7 @@ export class ParticipantService {
this.tokenStorageService.setParticipantToken(token);
}
- this.updateParticipantTokenInfo(token);
+ await this.updateParticipantTokenInfo(token);
return token;
}
@@ -83,7 +91,7 @@ export class ParticipantService {
this.tokenStorageService.setParticipantToken(token);
}
- this.updateParticipantTokenInfo(token);
+ await this.updateParticipantTokenInfo(token);
return token;
}
@@ -93,13 +101,14 @@ export class ParticipantService {
* @param token - The JWT token to set.
* @throws Error if the token is invalid or expired.
*/
- protected updateParticipantTokenInfo(token: string): void {
+ protected async updateParticipantTokenInfo(token: string): Promise {
try {
const decodedToken = getValidDecodedToken(token);
const metadata = decodedToken.metadata as MeetTokenMetadata;
if (decodedToken.sub && decodedToken.name) {
- this.setParticipantName(decodedToken.name);
+ const decryptedName = await this.e2eeService.decryptOrMask(decodedToken.name);
+ this.setParticipantName(decryptedName);
this.participantIdentity = decodedToken.sub;
}
diff --git a/meet-ce/frontend/projects/shared-meet-components/src/lib/services/room.service.ts b/meet-ce/frontend/projects/shared-meet-components/src/lib/services/room.service.ts
index ce2da058..6e9d2787 100644
--- a/meet-ce/frontend/projects/shared-meet-components/src/lib/services/room.service.ts
+++ b/meet-ce/frontend/projects/shared-meet-components/src/lib/services/room.service.ts
@@ -22,6 +22,7 @@ export class RoomService {
protected roomId: string = '';
protected roomSecret: string = '';
+ protected e2eeKey: string = '';
protected log;
@@ -50,6 +51,14 @@ export class RoomService {
}
}
+ setE2EEKey(e2eeKey: string) {
+ this.e2eeKey = e2eeKey;
+ }
+
+ getE2EEKey(): string {
+ return this.e2eeKey;
+ }
+
getRoomSecret(): string {
return this.roomSecret;
}
diff --git a/meet-ce/frontend/webcomponent/.vscode/settings.json b/meet-ce/frontend/webcomponent/.vscode/settings.json
index 2c2f71ed..8470f353 100644
--- a/meet-ce/frontend/webcomponent/.vscode/settings.json
+++ b/meet-ce/frontend/webcomponent/.vscode/settings.json
@@ -1,8 +1,5 @@
{
- "jest.jestCommandLine": "node --experimental-vm-modules ../../../node_modules/.bin/jest --config jest.config.mjs",
+ "jest.jestCommandLine": "pnpm run test:unit",
"jest.rootPath": ".",
- "jest.nodeEnv": {
- "NODE_OPTIONS": "--experimental-vm-modules"
- },
"jest.runMode": "on-demand"
}
diff --git a/meet-ce/frontend/webcomponent/jest.config.mjs b/meet-ce/frontend/webcomponent/jest.config.mjs
index 378901d0..19e6a9af 100644
--- a/meet-ce/frontend/webcomponent/jest.config.mjs
+++ b/meet-ce/frontend/webcomponent/jest.config.mjs
@@ -1,25 +1,21 @@
-import { createDefaultEsmPreset } from 'ts-jest'
-
-/** @type {import('ts-jest').JestConfigWithTsJest} */
-const jestConfig = {
+/** @type {import('jest').Config} */
+const config = {
displayName: 'webcomponent',
- ...createDefaultEsmPreset({
- tsconfig: 'tsconfig.json'
- }),
- // Set the root directory to the webcomponent folder
- rootDir: './',
+ preset: 'ts-jest/presets/default-esm',
+ extensionsToTreatAsEsm: ['.ts'],
+ globals: {
+ 'ts-jest': {
+ useESM: true,
+ tsconfig: 'tsconfig.json'
+ }
+ },
resolver: 'ts-jest-resolver',
testEnvironment: 'jsdom',
testMatch: ['**/?(*.)+(spec|test).[tj]s?(x)'],
moduleFileExtensions: ['js', 'ts', 'json', 'node'],
testPathIgnorePatterns: ['/node_modules/', '/dist/', '/tests/e2e/'],
transform: {
- '^.+\\.tsx?$': ['ts-jest', { tsconfig: 'tsconfig.json' }]
- },
- globals: {
- 'ts-jest': {
- tsconfig: 'tsconfig.json'
- }
+ '^.+\\.tsx?$': ['ts-jest', { useESM: true }]
},
moduleNameMapper: {
'^@openvidu-meet/typings$': '/../../typings/src/index.ts',
@@ -27,4 +23,4 @@ const jestConfig = {
}
}
-export default jestConfig
+export default config
diff --git a/meet-ce/frontend/webcomponent/src/components/OpenViduMeet.ts b/meet-ce/frontend/webcomponent/src/components/OpenViduMeet.ts
index 981fd3fd..4a0361ea 100644
--- a/meet-ce/frontend/webcomponent/src/components/OpenViduMeet.ts
+++ b/meet-ce/frontend/webcomponent/src/components/OpenViduMeet.ts
@@ -143,19 +143,24 @@ export class OpenViduMeet extends HTMLElement {
return;
}
- const url = new URL(baseUrl);
- this.targetIframeOrigin = url.origin;
- this.commandsManager.setTargetOrigin(this.targetIframeOrigin);
- this.eventsManager.setTargetOrigin(this.targetIframeOrigin);
+ try {
+ const url = new URL(baseUrl);
+ this.targetIframeOrigin = url.origin;
+ this.commandsManager.setTargetOrigin(this.targetIframeOrigin);
+ this.eventsManager.setTargetOrigin(this.targetIframeOrigin);
- // Update query params
- Array.from(this.attributes).forEach((attr) => {
- if (attr.name !== WebComponentProperty.ROOM_URL && attr.name !== WebComponentProperty.RECORDING_URL) {
- url.searchParams.set(attr.name, attr.value);
- }
- });
+ // Update query params
+ Array.from(this.attributes).forEach((attr) => {
+ if (attr.name !== WebComponentProperty.ROOM_URL && attr.name !== WebComponentProperty.RECORDING_URL) {
+ url.searchParams.set(attr.name, attr.value);
+ }
+ });
- this.iframe.src = url.toString();
+ this.iframe.src = url.toString();
+ } catch (error) {
+ console.error(`Invalid URL provided: ${baseUrl}`, error);
+ alert(`Invalid URL provided: ${baseUrl}`);
+ }
}
/**
diff --git a/meet-ce/frontend/webcomponent/tests/e2e/e2ee-ui.test.ts b/meet-ce/frontend/webcomponent/tests/e2e/e2ee-ui.test.ts
index e9f37700..522d571f 100644
--- a/meet-ce/frontend/webcomponent/tests/e2e/e2ee-ui.test.ts
+++ b/meet-ce/frontend/webcomponent/tests/e2e/e2ee-ui.test.ts
@@ -124,10 +124,10 @@ test.describe('E2EE UI Tests', () => {
});
// ==========================================
- // E2EE SESSION TESTS
+ // E2EE MEETING TESTS
// ==========================================
- test.describe('E2EE in Session', () => {
+ test.describe('E2EE in Meeting', () => {
test.afterEach(async ({ page }) => {
try {
await leaveRoom(page);
@@ -152,7 +152,6 @@ test.describe('E2EE UI Tests', () => {
const page2 = await context.newPage();
// Participant 1 joins with E2EE key
- await page.goto(MEET_TESTAPP_URL);
await prepareForJoiningRoom(page, MEET_TESTAPP_URL, roomId);
await page.click('#join-as-speaker');
@@ -178,7 +177,6 @@ test.describe('E2EE UI Tests', () => {
// Participant 2 joins with same E2EE key
const participant2Name = `P2-${Math.random().toString(36).substring(2, 9)}`;
- await page2.goto(MEET_TESTAPP_URL);
await prepareForJoiningRoom(page2, MEET_TESTAPP_URL, roomId);
await page2.click('#join-as-speaker');
@@ -222,6 +220,16 @@ test.describe('E2EE UI Tests', () => {
});
await expect(encryptionError2).toBeHidden();
+ // Expect video to be flowing (by checking the video element has video tracks)
+ const videoElements = await waitForElementInIframe(page, '.OV_video-element', {
+ state: 'visible',
+ all: true
+ });
+ for (const videoElement of videoElements) {
+ const videoTracks = await videoElement.evaluate((el) => (el as any).srcObject?.getVideoTracks());
+ expect(videoTracks.length).toBeGreaterThan(0);
+ }
+
// Cleanup participant 2
await leaveRoom(page2);
await page2.close();
@@ -342,6 +350,463 @@ test.describe('E2EE UI Tests', () => {
await Promise.all([leaveRoom(page2), leaveRoom(page3)]);
await Promise.all([page2.close(), page3.close()]);
});
+
+ test('should decrypt participant names and chat messages with correct E2EE key', async ({ page, context }) => {
+ // Enable E2EE
+ await updateRoomConfig(roomId, {
+ chat: { enabled: true },
+ recording: { enabled: false, allowAccessTo: MeetRecordingAccess.ADMIN_MODERATOR_SPEAKER },
+ virtualBackground: { enabled: true },
+ e2ee: { enabled: true }
+ });
+
+ const e2eeKey = 'shared-encryption-key-456';
+ const participant1Name = `Alice-${Math.random().toString(36).substring(2, 9)}`;
+ const participant2Name = `Bob-${Math.random().toString(36).substring(2, 9)}`;
+
+ // Participant 1 joins with E2EE key
+ await prepareForJoiningRoom(page, MEET_TESTAPP_URL, roomId);
+ await page.click('#join-as-speaker');
+
+ await waitForElementInIframe(page, '#participant-name-input', { state: 'visible' });
+ await interactWithElementInIframe(page, '#participant-name-input', {
+ action: 'fill',
+ value: participant1Name
+ });
+
+ await interactWithElementInIframe(page, '#participant-e2eekey-input', {
+ action: 'fill',
+ value: e2eeKey
+ });
+
+ await interactWithElementInIframe(page, '#participant-name-submit', { action: 'click' });
+ await waitForElementInIframe(page, 'ov-pre-join', { state: 'visible' });
+ await interactWithElementInIframe(page, '#join-button', { action: 'click' });
+ await waitForElementInIframe(page, 'ov-session', { state: 'visible' });
+
+ // Participant 2 joins with same E2EE key
+ const page2 = await context.newPage();
+ await prepareForJoiningRoom(page2, MEET_TESTAPP_URL, roomId);
+ await page2.click('#join-as-speaker');
+
+ await waitForElementInIframe(page2, '#participant-name-input', { state: 'visible' });
+ await interactWithElementInIframe(page2, '#participant-name-input', {
+ action: 'fill',
+ value: participant2Name
+ });
+
+ await interactWithElementInIframe(page2, '#participant-e2eekey-input', {
+ action: 'fill',
+ value: e2eeKey
+ });
+
+ await interactWithElementInIframe(page2, '#participant-name-submit', { action: 'click' });
+ await waitForElementInIframe(page2, 'ov-pre-join', { state: 'visible' });
+ await interactWithElementInIframe(page2, '#join-button', { action: 'click' });
+ await waitForElementInIframe(page2, 'ov-session', { state: 'visible' });
+
+ // Wait for participants to connect
+ await page.waitForTimeout(2000);
+
+ // ===== CHECK PARTICIPANT NAMES IN VIDEO GRID =====
+ // Participant 1 should see Participant 2's name decrypted
+ const participantNameElements = await Promise.all([
+ waitForElementInIframe(page, '#participant-name', {
+ state: 'attached',
+ all: true
+ }),
+ waitForElementInIframe(page2, '#participant-name', {
+ state: 'attached',
+ all: true
+ })
+ ]);
+
+ for (const participantNameElement of participantNameElements.flat()) {
+ const name = await participantNameElement.evaluate((el) => el.textContent);
+ expect(name.includes(participant1Name) || name.includes(participant2Name)).toBeTruthy();
+ expect(name).not.toContain('*');
+ }
+
+ // ===== CHECK NAMES IN PARTICIPANTS PANEL =====
+ // Open participants panel
+ await Promise.all([
+ interactWithElementInIframe(page, '#participants-panel-btn', { action: 'click' }),
+ interactWithElementInIframe(page2, '#participants-panel-btn', { action: 'click' })
+ ]);
+ await Promise.all([
+ waitForElementInIframe(page, 'ov-participants-panel', { state: 'visible' }),
+ waitForElementInIframe(page2, 'ov-participants-panel', { state: 'visible' })
+ ]);
+ // Check that both names are visible and decrypted in the panel
+ const participantsPanelNames = await Promise.all([
+ waitForElementInIframe(page, '.participant-item-name span', {
+ state: 'attached',
+ all: true
+ }),
+ waitForElementInIframe(page2, '.participant-item-name span', {
+ state: 'attached',
+ all: true
+ })
+ ]);
+
+ for (const participantPanelName of participantsPanelNames.flat()) {
+ const name = await participantPanelName.evaluate((el) => el.textContent);
+ expect(name.includes(participant1Name) || name.includes(participant2Name)).toBeTruthy();
+ expect(name).not.toContain('*');
+ }
+
+ // Close participants panel
+ await Promise.all([
+ interactWithElementInIframe(page, '#participants-panel-btn', { action: 'click' }),
+ interactWithElementInIframe(page2, '#participants-panel-btn', { action: 'click' })
+ ]);
+ await Promise.all([
+ waitForElementInIframe(page, 'ov-participants-panel', { state: 'hidden' }),
+ waitForElementInIframe(page2, 'ov-participants-panel', { state: 'hidden' })
+ ]);
+
+ // ===== CHECK OWN NAME IN SETTINGS PANEL =====
+ // Open settings panel
+ await Promise.all([openMoreOptionsMenu(page), openMoreOptionsMenu(page2)]);
+ await Promise.all([
+ interactWithElementInIframe(page, '#toolbar-settings-btn', { action: 'click' }),
+ interactWithElementInIframe(page2, '#toolbar-settings-btn', { action: 'click' })
+ ]);
+ await Promise.all([
+ waitForElementInIframe(page, 'ov-settings-panel', { state: 'visible' }),
+ waitForElementInIframe(page2, 'ov-settings-panel', { state: 'visible' })
+ ]);
+
+ // Check that own name is visible and decrypted
+ const ownNameInputs = await Promise.all([
+ waitForElementInIframe(page, '#participant-name-input', {
+ state: 'visible'
+ }),
+ waitForElementInIframe(page2, '#participant-name-input', { state: 'visible' })
+ ]);
+
+ const ownName1 = await ownNameInputs[0].evaluate((el: HTMLInputElement) => el.value);
+ const ownName2 = await ownNameInputs[1].evaluate((el: HTMLInputElement) => el.value);
+ expect(ownName1).toBe(participant1Name);
+ expect(ownName1).not.toContain('*');
+ expect(ownName2).toBe(participant2Name);
+ expect(ownName2).not.toContain('*');
+
+ // Close settings panel
+ await Promise.all([
+ interactWithElementInIframe(page, '.panel-close-button', { action: 'click' }),
+ interactWithElementInIframe(page2, '.panel-close-button', { action: 'click' })
+ ]);
+ await Promise.all([
+ waitForElementInIframe(page, 'ov-settings-panel', { state: 'hidden' }),
+ waitForElementInIframe(page2, 'ov-settings-panel', { state: 'hidden' })
+ ]);
+ await Promise.all([closeMoreOptionsMenu(page), closeMoreOptionsMenu(page2)]);
+
+ // ===== CHECK CHAT MESSAGES =====
+ // Open chat
+ await Promise.all([
+ interactWithElementInIframe(page, '#chat-panel-btn', { action: 'click' }),
+ interactWithElementInIframe(page2, '#chat-panel-btn', { action: 'click' })
+ ]);
+ await Promise.all([
+ waitForElementInIframe(page, 'ov-chat-panel', { state: 'visible' }),
+ waitForElementInIframe(page2, 'ov-chat-panel', { state: 'visible' })
+ ]);
+
+ // ===== MESSAGE: PARTICIPANT 1 → PARTICIPANT 2 =====
+ const testMessage1 = `Hello from ${participant1Name}!`;
+ await Promise.all([
+ interactWithElementInIframe(page, '#chat-input', { action: 'fill', value: testMessage1 }),
+ waitForElementInIframe(page2, 'ov-chat-panel', { state: 'visible' })
+ ]);
+
+ await interactWithElementInIframe(page, '#send-btn', { action: 'click' });
+
+ // Wait for message to be sent
+ await page.waitForTimeout(1000);
+
+ // Open chat on page 2
+ const chatMessages2 = await waitForElementInIframe(page2, '.chat-message', { state: 'visible' });
+
+ // Verify message content
+ const messageText2 = await chatMessages2.evaluate((el) => el.textContent || '');
+ expect(messageText2).toContain(testMessage1);
+ expect(messageText2).not.toContain('*');
+
+ // ===== MESSAGE: PARTICIPANT 2 → PARTICIPANT 1 =====
+ const testMessage2 = `Hi from ${participant2Name}!`;
+
+ // Send message in page2 iframe
+ await interactWithElementInIframe(page2, '#chat-input', { action: 'fill', value: testMessage2 });
+ await interactWithElementInIframe(page2, '#send-btn', { action: 'click' });
+
+ // Wait briefly for message delivery
+ await page.waitForTimeout(1000);
+
+ // Wait for message on participant 1’s side
+ const chatMessages1 = await waitForElementInIframe(page, '.chat-message', { state: 'visible' });
+
+ // Collect all chat messages in the chat panel
+ const allMessages1 = await chatMessages1.evaluate((el) =>
+ Array.from(el.closest('ov-chat-panel')?.querySelectorAll('.chat-message') || []).map(
+ (e) => e.textContent || ''
+ )
+ );
+
+ // Verify received message
+ expect(allMessages1.join(' ')).toContain(testMessage2);
+ expect(allMessages1.join(' ')).not.toContain('*');
+
+ // Cleanup
+ await leaveRoom(page2);
+ await page2.close();
+ });
+
+ test('should show masked names and unreadable messages for participant with wrong E2EE key', async ({
+ page,
+ context
+ }) => {
+ // Enable E2EE
+ await updateRoomConfig(roomId, {
+ chat: { enabled: true },
+ recording: { enabled: false, allowAccessTo: MeetRecordingAccess.ADMIN_MODERATOR_SPEAKER },
+ virtualBackground: { enabled: true },
+ e2ee: { enabled: true }
+ });
+
+ const correctKey = 'correct-shared-key-789';
+ const wrongKey = 'wrong-key-999';
+ const participant1Name = `Charlie-${Math.random().toString(36).substring(2, 9)}`;
+ const participant2Name = `David-${Math.random().toString(36).substring(2, 9)}`;
+ const participant3Name = `Eve-${Math.random().toString(36).substring(2, 9)}`;
+ const [page2, page3] = await Promise.all([context.newPage(), context.newPage()]);
+
+ // Prepare for all participants to join the room
+ await Promise.all([
+ prepareForJoiningRoom(page, MEET_TESTAPP_URL, roomId),
+ prepareForJoiningRoom(page2, MEET_TESTAPP_URL, roomId),
+ prepareForJoiningRoom(page3, MEET_TESTAPP_URL, roomId)
+ ]);
+
+ // Join as speaker in all pages
+ await Promise.all([
+ page.click('#join-as-speaker'),
+ page2.click('#join-as-speaker'),
+ page3.click('#join-as-speaker')
+ ]);
+
+ // Wait for name and E2EE key inputs to be visible in all pages
+ await Promise.all([
+ waitForElementInIframe(page, '#participant-name-input', { state: 'visible' }),
+ waitForElementInIframe(page, '#participant-e2eekey-input', { state: 'visible' }),
+ waitForElementInIframe(page2, '#participant-name-input', { state: 'visible' }),
+ waitForElementInIframe(page2, '#participant-e2eekey-input', { state: 'visible' }),
+ waitForElementInIframe(page3, '#participant-name-input', { state: 'visible' }),
+ waitForElementInIframe(page3, '#participant-e2eekey-input', { state: 'visible' })
+ ]);
+
+ // Fill participant names
+ await Promise.all([
+ interactWithElementInIframe(page, '#participant-name-input', {
+ action: 'fill',
+ value: participant1Name
+ }),
+ interactWithElementInIframe(page2, '#participant-name-input', {
+ action: 'fill',
+ value: participant2Name
+ }),
+ interactWithElementInIframe(page3, '#participant-name-input', {
+ action: 'fill',
+ value: participant3Name
+ })
+ ]);
+
+ // Fill E2EE keys (two correct, one wrong)
+ await Promise.all([
+ interactWithElementInIframe(page, '#participant-e2eekey-input', { action: 'fill', value: correctKey }),
+ interactWithElementInIframe(page2, '#participant-e2eekey-input', { action: 'fill', value: correctKey }),
+ interactWithElementInIframe(page3, '#participant-e2eekey-input', { action: 'fill', value: wrongKey })
+ ]);
+
+ // Join all participants
+ await Promise.all([
+ interactWithElementInIframe(page, '#participant-name-submit', { action: 'click' }),
+ interactWithElementInIframe(page2, '#participant-name-submit', { action: 'click' }),
+ interactWithElementInIframe(page3, '#participant-name-submit', { action: 'click' })
+ ]);
+
+ // Wait for prejoin page in all pages
+ await Promise.all([
+ waitForElementInIframe(page, 'ov-pre-join', { state: 'visible' }),
+ waitForElementInIframe(page2, 'ov-pre-join', { state: 'visible' }),
+ waitForElementInIframe(page3, 'ov-pre-join', { state: 'visible' })
+ ]);
+
+ // Click join button in all pages
+ await Promise.all([
+ interactWithElementInIframe(page, '#join-button', { action: 'click' }),
+ interactWithElementInIframe(page2, '#join-button', { action: 'click' }),
+ interactWithElementInIframe(page3, '#join-button', { action: 'click' })
+ ]);
+
+ // Wait for session to be visible in all pages
+ await Promise.all([
+ waitForElementInIframe(page, 'ov-session', { state: 'visible' }),
+ waitForElementInIframe(page2, 'ov-session', { state: 'visible' }),
+ waitForElementInIframe(page3, 'ov-session', { state: 'visible' })
+ ]);
+
+ // Wait for participants to connect
+ await page.waitForTimeout(1000);
+
+ // Check that participant 3 sees encryption error posters for others
+ // ===== CHECK MASKED NAMES IN VIDEO GRID FOR PARTICIPANT 3 =====
+ const participantNameElements3 = await waitForElementInIframe(
+ page3,
+ '#layout .participant-name-container #participant-name',
+ {
+ state: 'attached',
+ all: true
+ }
+ );
+ const participantNames3 = await Promise.all(
+ participantNameElements3.map((el) => el.evaluate((e) => e.textContent))
+ );
+
+ console.log('Participant Names Seen by Participant 3:', participantNames3);
+ console.log('Expected: 3 names (own + 2 masked), got:', participantNames3.length);
+
+ // Should have exactly 3 participants
+ expect(participantNames3.length).toBe(3);
+
+ // Should NOT all be masked (own name should be visible)
+ expect(participantNames3.every((name) => name?.includes('******'))).toBeFalsy();
+
+ // Should have exactly 2 masked names
+ const maskedNames = participantNames3.filter((name) => name?.includes('******'));
+ expect(maskedNames.length).toBe(2);
+
+ // Should see own name
+ expect(participantNames3).toContain(participant3Name);
+
+ // Should NOT see the actual names of P1 and P2
+ expect(participantNames3.join(' ')).not.toContain(participant1Name);
+ expect(participantNames3.join(' ')).not.toContain(participant2Name);
+
+ // ===== CHECK MASKED NAMES IN PARTICIPANTS PANEL =====
+ await interactWithElementInIframe(page3, '#participants-panel-btn', { action: 'click' });
+ await waitForElementInIframe(page3, 'ov-participants-panel', { state: 'visible' });
+
+ const participantsPanelNames3 = await waitForElementInIframe(page3, '.participant-name-text', {
+ state: 'visible',
+ all: true
+ });
+ const panelNamesText3 = await Promise.all(
+ participantsPanelNames3.map((el) => el.evaluate((e) => e.textContent))
+ );
+
+ console.log('Panel Names Seen by Participant 3:', panelNamesText3);
+ console.log('Expected: 3 names (own + 2 masked), got:', panelNamesText3.length);
+
+ // Should have exactly 3 participants in panel
+ expect(panelNamesText3.length).toBe(3);
+
+ // Should NOT all be masked (own name should be visible)
+ expect(panelNamesText3.every((name) => name?.includes('******'))).toBeFalsy();
+
+ // Should have exactly 2 masked names
+ const maskedPanelNames = panelNamesText3.filter((name) => name?.includes('******'));
+ expect(maskedPanelNames.length).toBe(2);
+
+ // Should see own name
+ expect(panelNamesText3).toContain(participant3Name);
+
+ // Should NOT see the actual names of P1 and P2
+ expect(panelNamesText3.join(' ')).not.toContain(participant1Name);
+ expect(panelNamesText3.join(' ')).not.toContain(participant2Name);
+
+ await interactWithElementInIframe(page3, '#participants-panel-btn', { action: 'click' });
+ await waitForElementInIframe(page3, 'ov-participants-panel', { state: 'hidden' });
+
+ // ===== CHECK OWN NAME IN SETTINGS PANEL =====
+ await openMoreOptionsMenu(page3);
+ await interactWithElementInIframe(page3, '#toolbar-settings-btn', { action: 'click' });
+ await waitForElementInIframe(page3, 'ov-settings-panel', { state: 'visible' });
+
+ const ownNameInput3 = await waitForElementInIframe(page3, '#participant-name-input', { state: 'visible' });
+ const ownName3 = await ownNameInput3.evaluate((el: HTMLInputElement) => el.value);
+ expect(ownName3).toBe(participant3Name);
+ expect(ownName3).not.toContain('******');
+
+ await interactWithElementInIframe(page3, '.panel-close-button', { action: 'click' });
+ await waitForElementInIframe(page3, 'ov-settings-panel', { state: 'hidden' });
+ await closeMoreOptionsMenu(page3);
+
+ // ===== SEND MESSAGE FROM PARTICIPANT 1 =====
+ const secretMessage = `Secret message from ${participant1Name}`;
+ await Promise.all([
+ interactWithElementInIframe(page, '#chat-panel-btn', { action: 'click' }),
+ waitForElementInIframe(page, 'ov-chat-panel', { state: 'visible' })
+ ]);
+
+ // Send message
+ await interactWithElementInIframe(page, '#chat-input', { action: 'fill', value: secretMessage });
+ await interactWithElementInIframe(page, '#send-btn', { action: 'click' });
+
+ // Wait for message to be sent and received
+ await Promise.all([
+ waitForElementInIframe(page2, '#chat-panel-btn .mat-badge-content', { state: 'visible' }),
+ waitForElementInIframe(page3, '#chat-panel-btn .mat-badge-content', { state: 'visible' })
+ ]);
+
+ // ===== CHECK CHAT MESSAGES ARE UNREADABLE =====
+ await interactWithElementInIframe(page3, '#chat-panel-btn', { action: 'click' });
+ await waitForElementInIframe(page3, 'ov-chat-panel', { state: 'visible' });
+
+ await page3.waitForTimeout(1000);
+
+ const chatMessagesCount = await countElementsInIframe(page3, '.chat-message');
+ expect(chatMessagesCount).toBeGreaterThan(0);
+
+ const chatMessages3 = await waitForElementInIframe(page3, '.chat-message', {
+ state: 'visible',
+ all: true
+ });
+ const messagesText3 = await Promise.all(chatMessages3.map((el) => el.evaluate((e) => e.textContent)));
+
+ console.log('Chat Messages Seen by Participant 3:', messagesText3);
+ console.log('Expected: All messages masked, got:', messagesText3.length, 'messages');
+
+ // All messages should contain the mask
+ expect(messagesText3.every((text) => text?.includes('******'))).toBeTruthy();
+
+ // Should NOT contain the actual secret message
+ expect(messagesText3.join(' ')).not.toContain(secretMessage);
+
+ // ===== VERIFY PARTICIPANTS 1 AND 2 CAN STILL SEE EACH OTHER =====
+ const participantNameElements1 = await waitForElementInIframe(page, '.participant-name', {
+ state: 'visible',
+ all: true
+ });
+ const participantNames1 = await Promise.all(
+ participantNameElements1.map((el) => el.evaluate((e) => e.textContent))
+ );
+ expect(participantNames1.join(' ')).toContain(participant2Name);
+
+ const participantNameElements2 = await waitForElementInIframe(page2, '.participant-name', {
+ state: 'visible',
+ all: true
+ });
+ const participantNames2 = await Promise.all(
+ participantNameElements2.map((el) => el.evaluate((e) => e.textContent))
+ );
+ expect(participantNames2.join(' ')).toContain(participant1Name);
+
+ // Cleanup
+ await Promise.all([leaveRoom(page2), leaveRoom(page3)]);
+ await Promise.all([page2.close(), page3.close()]);
+ });
});
// ==========================================
diff --git a/meet-ce/frontend/webcomponent/tests/helpers/function-helpers.ts b/meet-ce/frontend/webcomponent/tests/helpers/function-helpers.ts
index a60448ad..512b76a8 100644
--- a/meet-ce/frontend/webcomponent/tests/helpers/function-helpers.ts
+++ b/meet-ce/frontend/webcomponent/tests/helpers/function-helpers.ts
@@ -24,12 +24,43 @@ export async function getIframeInShadowDom(
}
/**
- * Waits for an element inside an iframe within Shadow DOM
- * @param page - Playwright page object
- * @param elementSelector - Selector for the element inside the iframe
- * @param options - Optional configuration
- * @returns Locator for the found element
+ * Waits for one or more elements inside an iframe within a Shadow DOM.
+ *
+ * By default, waits for the first matching element.
+ * If `options.all` is set to `true`, waits for all matching elements and returns an array.
+ *
+ * @param page - Playwright `Page` instance.
+ * @param elementSelector - CSS selector for the target element(s) inside the iframe.
+ * @param options - Optional configuration object.
+ * @param options.componentSelector - Selector for the shadow DOM component that contains the iframe. Defaults to `'openvidu-meet'`.
+ * @param options.iframeSelector - Selector for the iframe inside the shadow DOM. Defaults to `'iframe'`.
+ * @param options.timeout - Maximum time in milliseconds to wait. Defaults to `30000`.
+ * @param options.state - Wait condition: `'attached' | 'detached' | 'visible' | 'hidden'`. Defaults to `'visible'`.
+ * @param options.index - Element index to return when multiple elements match. Defaults to `0`.
+ * @param options.all - If `true`, waits for all matching elements and returns an array of locators. Defaults to `false`.
+ *
+ * @returns A single `Locator` by default, or an array of `Locator[]` when `options.all` is `true`.
+ *
+ * @example
+ * // Wait for the first visible element
+ * const element = await waitForElementInIframe(page, '.participant');
+ *
+ * @example
+ * // Wait for all visible elements
+ * const elements = await waitForElementInIframe(page, '.participant', { all: true });
*/
+export async function waitForElementInIframe(
+ page: Page,
+ elementSelector: string,
+ options?: {
+ componentSelector?: string;
+ iframeSelector?: string;
+ timeout?: number;
+ state?: 'attached' | 'detached' | 'visible' | 'hidden';
+ index?: number;
+ all?: false;
+ }
+): Promise;
export async function waitForElementInIframe(
page: Page,
elementSelector: string,
@@ -38,24 +69,42 @@ export async function waitForElementInIframe(
iframeSelector?: string;
timeout?: number;
state?: 'attached' | 'detached' | 'visible' | 'hidden';
+ all: true;
+ }
+): Promise;
+export async function waitForElementInIframe(
+ page: Page,
+ elementSelector: string,
+ options: {
+ componentSelector?: string;
+ iframeSelector?: string;
+ timeout?: number;
+ state?: 'attached' | 'detached' | 'visible' | 'hidden';
+ index?: number;
+ all?: boolean;
} = {}
-): Promise {
+): Promise {
const {
componentSelector = 'openvidu-meet',
iframeSelector = 'iframe',
timeout = 30000,
- state = 'visible'
+ state = 'visible',
+ index = 0,
+ all = false
} = options;
- // Get the iframe
const frameLocator = await getIframeInShadowDom(page, componentSelector, iframeSelector);
+ const baseLocator = frameLocator.locator(elementSelector);
- // Get element locator
- const elementLocator = frameLocator.locator(elementSelector);
+ if (all) {
+ const locators = await baseLocator.all();
+ await Promise.all(locators.map((l) => l.waitFor({ state, timeout })));
+ return locators;
+ }
- // Wait for the element with the specified state
- await elementLocator.waitFor({ state, timeout });
- return elementLocator;
+ const target = baseLocator.nth(index);
+ await target.waitFor({ state, timeout });
+ return target;
}
export async function countElementsInIframe(
diff --git a/meet-ce/frontend/webcomponent/tests/unit/attributes.test.ts b/meet-ce/frontend/webcomponent/tests/unit/attributes.test.ts
index 48799d61..ef07cb11 100644
--- a/meet-ce/frontend/webcomponent/tests/unit/attributes.test.ts
+++ b/meet-ce/frontend/webcomponent/tests/unit/attributes.test.ts
@@ -1,5 +1,6 @@
import { afterEach, beforeEach, describe, expect, it, jest } from '@jest/globals';
import { OpenViduMeet } from '../../src/components/OpenViduMeet';
+import { WebComponentProperty } from '@openvidu-meet/typings';
import '../../src/index';
describe('OpenViduMeet WebComponent Attributes', () => {
@@ -15,55 +16,391 @@ describe('OpenViduMeet WebComponent Attributes', () => {
document.body.innerHTML = '';
});
- it('should render iframe with correct attributes', () => {
- const iframe = component.shadowRoot?.querySelector('iframe');
- expect(iframe).not.toBeNull();
- expect(iframe?.getAttribute('allow')).toContain('camera');
- expect(iframe?.getAttribute('allow')).toContain('microphone');
- expect(iframe?.getAttribute('allow')).toContain('display-capture');
- expect(iframe?.getAttribute('allow')).toContain('fullscreen');
- expect(iframe?.getAttribute('allow')).toContain('autoplay');
- expect(iframe?.getAttribute('allow')).toContain('compute-pressure');
+ // ==========================================
+ // IFRAME SETUP
+ // ==========================================
+ describe('Iframe Configuration', () => {
+ it('should render iframe with correct media permissions', () => {
+ const iframe = component.shadowRoot?.querySelector('iframe');
+ expect(iframe).not.toBeNull();
+
+ const allowAttribute = iframe?.getAttribute('allow');
+ expect(allowAttribute).toContain('camera');
+ expect(allowAttribute).toContain('microphone');
+ expect(allowAttribute).toContain('display-capture');
+ expect(allowAttribute).toContain('fullscreen');
+ expect(allowAttribute).toContain('autoplay');
+ expect(allowAttribute).toContain('compute-pressure');
+ });
+
+ it('should have iframe ready in shadow DOM', () => {
+ const iframe = component.shadowRoot?.querySelector('iframe');
+ expect(iframe).toBeInstanceOf(HTMLIFrameElement);
+ });
});
- it('should reject rendering iframe when "room-url" attribute is missing', () => {
- const consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation(() => {});
+ // ==========================================
+ // REQUIRED ATTRIBUTES (room-url | recording-url)
+ // ==========================================
+ describe('Required Attributes', () => {
+ it('should reject iframe src when both room-url and recording-url are missing', () => {
+ const consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation(() => {});
- // Trigger updateIframeSrc manually
- (component as any).updateIframeSrc();
+ // Trigger updateIframeSrc manually
+ (component as any).updateIframeSrc();
- const iframe = component.shadowRoot?.querySelector('iframe');
+ const iframe = component.shadowRoot?.querySelector('iframe');
- expect(iframe).toBeDefined();
- expect(iframe?.src).toBeFalsy();
- expect(consoleErrorSpy).toHaveBeenCalledWith('The "room-url" or "recording-url" attribute is required.');
+ expect(iframe).toBeDefined();
+ expect(iframe?.src).toBeFalsy();
+ expect(consoleErrorSpy).toHaveBeenCalledWith('The "room-url" or "recording-url" attribute is required.');
- consoleErrorSpy.mockRestore();
+ consoleErrorSpy.mockRestore();
+ });
+
+ it('should set iframe src when room-url attribute is provided', () => {
+ const roomUrl = 'https://example.com/room/testRoom-123';
+ component.setAttribute(WebComponentProperty.ROOM_URL, roomUrl);
+
+ (component as any).updateIframeSrc();
+
+ const iframe = component.shadowRoot?.querySelector('iframe');
+ expect(iframe?.src).toBe(roomUrl);
+ });
+
+ it('should set iframe src when recording-url attribute is provided', () => {
+ const recordingUrl = 'https://example.com/recordings/recording-abc-123';
+ component.setAttribute(WebComponentProperty.RECORDING_URL, recordingUrl);
+
+ (component as any).updateIframeSrc();
+
+ const iframe = component.shadowRoot?.querySelector('iframe');
+ expect(iframe?.src).toBe(recordingUrl);
+ });
+
+ it('should prefer room-url over recording-url when both are provided', () => {
+ const roomUrl = 'https://example.com/room/testRoom-123';
+ const recordingUrl = 'https://example.com/recordings/recording-abc-123';
+
+ component.setAttribute(WebComponentProperty.ROOM_URL, roomUrl);
+ component.setAttribute(WebComponentProperty.RECORDING_URL, recordingUrl);
+
+ (component as any).updateIframeSrc();
+
+ const iframe = component.shadowRoot?.querySelector('iframe');
+ expect(iframe?.src).toBe(roomUrl);
+ });
+
+ it('should extract origin from room-url and set as target origin', () => {
+ const domain = 'https://example.com';
+ const roomUrl = `${domain}/room/testRoom-123?secret=123456`;
+ component.setAttribute(WebComponentProperty.ROOM_URL, roomUrl);
+
+ (component as any).updateIframeSrc();
+
+ expect((component as any).targetIframeOrigin).toBe(domain);
+ expect((component as any).commandsManager.targetIframeOrigin).toBe(domain);
+ expect((component as any).eventsManager.targetIframeOrigin).toBe(domain);
+ });
+
+ it('should extract origin from recording-url and set as target origin', () => {
+ const domain = 'https://recordings.example.com';
+ const recordingUrl = `${domain}/recordings/recording-abc-123`;
+ component.setAttribute(WebComponentProperty.RECORDING_URL, recordingUrl);
+
+ (component as any).updateIframeSrc();
+
+ expect((component as any).targetIframeOrigin).toBe(domain);
+ expect((component as any).commandsManager.targetIframeOrigin).toBe(domain);
+ expect((component as any).eventsManager.targetIframeOrigin).toBe(domain);
+ });
+
+ it('should update iframe src when room-url attribute changes', () => {
+ const roomUrl1 = 'https://example.com/room/room-1';
+ const roomUrl2 = 'https://example.com/room/room-2';
+
+ component.setAttribute(WebComponentProperty.ROOM_URL, roomUrl1);
+ (component as any).updateIframeSrc();
+
+ let iframe = component.shadowRoot?.querySelector('iframe');
+ expect(iframe?.src).toBe(roomUrl1);
+
+ component.setAttribute(WebComponentProperty.ROOM_URL, roomUrl2);
+ (component as any).updateIframeSrc();
+
+ iframe = component.shadowRoot?.querySelector('iframe');
+ expect(iframe?.src).toBe(roomUrl2);
+ });
});
- it('should update iframe src when "room-url" attribute changes', () => {
- const roomUrl = 'https://example.com/room/testRoom-123?secret=123456';
- component.setAttribute('room-url', roomUrl);
- component.setAttribute('user', 'testUser');
+ // ==========================================
+ // OPTIONAL ATTRIBUTES AS QUERY PARAMETERS
+ // ==========================================
+ describe('Optional Attributes as Query Parameters', () => {
+ const baseRoomUrl = 'https://example.com/room/testRoom';
- // Manually trigger the update (MutationObserver doesn't always trigger in tests)
- (component as any).updateIframeSrc();
+ it('should add participant-name as query parameter', () => {
+ const participantName = 'John Doe';
+ component.setAttribute(WebComponentProperty.ROOM_URL, baseRoomUrl);
+ component.setAttribute(WebComponentProperty.PARTICIPANT_NAME, participantName);
- const iframe = component.shadowRoot?.querySelector('iframe');
- expect(iframe?.src).toEqual(`${roomUrl}&user=testUser`);
+ (component as any).updateIframeSrc();
+
+ const iframe = component.shadowRoot?.querySelector('iframe');
+ const url = new URL(iframe?.src || '');
+ expect(url.searchParams.get(WebComponentProperty.PARTICIPANT_NAME)).toBe(participantName);
+ });
+
+ it('should add e2ee-key as query parameter', () => {
+ const e2eeKey = 'secret-encryption-key-123';
+ component.setAttribute(WebComponentProperty.ROOM_URL, baseRoomUrl);
+ component.setAttribute(WebComponentProperty.E2EE_KEY, e2eeKey);
+
+ (component as any).updateIframeSrc();
+
+ const iframe = component.shadowRoot?.querySelector('iframe');
+ const url = new URL(iframe?.src || '');
+ expect(url.searchParams.get(WebComponentProperty.E2EE_KEY)).toBe(e2eeKey);
+ });
+
+ it('should add leave-redirect-url as query parameter', () => {
+ const redirectUrl = 'https://example.com/goodbye';
+ component.setAttribute(WebComponentProperty.ROOM_URL, baseRoomUrl);
+ component.setAttribute(WebComponentProperty.LEAVE_REDIRECT_URL, redirectUrl);
+
+ (component as any).updateIframeSrc();
+
+ const iframe = component.shadowRoot?.querySelector('iframe');
+ const url = new URL(iframe?.src || '');
+ expect(url.searchParams.get(WebComponentProperty.LEAVE_REDIRECT_URL)).toBe(redirectUrl);
+ });
+
+ it('should add show-only-recordings as query parameter', () => {
+ component.setAttribute(WebComponentProperty.ROOM_URL, baseRoomUrl);
+ component.setAttribute(WebComponentProperty.SHOW_ONLY_RECORDINGS, 'true');
+
+ (component as any).updateIframeSrc();
+
+ const iframe = component.shadowRoot?.querySelector('iframe');
+ const url = new URL(iframe?.src || '');
+ expect(url.searchParams.get(WebComponentProperty.SHOW_ONLY_RECORDINGS)).toBe('true');
+ });
+
+ it('should add multiple optional attributes as query parameters', () => {
+ const participantName = 'Jane Smith';
+ const e2eeKey = 'encryption-key-456';
+ const redirectUrl = 'https://example.com/thanks';
+
+ component.setAttribute(WebComponentProperty.ROOM_URL, baseRoomUrl);
+ component.setAttribute(WebComponentProperty.PARTICIPANT_NAME, participantName);
+ component.setAttribute(WebComponentProperty.E2EE_KEY, e2eeKey);
+ component.setAttribute(WebComponentProperty.LEAVE_REDIRECT_URL, redirectUrl);
+ component.setAttribute(WebComponentProperty.SHOW_ONLY_RECORDINGS, 'false');
+
+ (component as any).updateIframeSrc();
+
+ const iframe = component.shadowRoot?.querySelector('iframe');
+ const url = new URL(iframe?.src || '');
+
+ expect(url.searchParams.get(WebComponentProperty.PARTICIPANT_NAME)).toBe(participantName);
+ expect(url.searchParams.get(WebComponentProperty.E2EE_KEY)).toBe(e2eeKey);
+ expect(url.searchParams.get(WebComponentProperty.LEAVE_REDIRECT_URL)).toBe(redirectUrl);
+ expect(url.searchParams.get(WebComponentProperty.SHOW_ONLY_RECORDINGS)).toBe('false');
+ });
+
+ it('should NOT add room-url or recording-url as query parameters', () => {
+ const roomUrl = 'https://example.com/room/testRoom?secret=abc';
+ component.setAttribute(WebComponentProperty.ROOM_URL, roomUrl);
+
+ (component as any).updateIframeSrc();
+
+ const iframe = component.shadowRoot?.querySelector('iframe');
+ const url = new URL(iframe?.src || '');
+
+ // room-url should not be in query params (it's the base URL)
+ expect(url.searchParams.has(WebComponentProperty.ROOM_URL)).toBe(false);
+ expect(url.searchParams.has(WebComponentProperty.RECORDING_URL)).toBe(false);
+ });
+
+ it('should preserve existing query parameters in room-url', () => {
+ const roomUrl = 'https://example.com/room/testRoom?secret=abc123&role=moderator';
+ const participantName = 'Alice';
+
+ component.setAttribute(WebComponentProperty.ROOM_URL, roomUrl);
+ component.setAttribute(WebComponentProperty.PARTICIPANT_NAME, participantName);
+
+ (component as any).updateIframeSrc();
+
+ const iframe = component.shadowRoot?.querySelector('iframe');
+ const url = new URL(iframe?.src || '');
+
+ // Original query params should be preserved
+ expect(url.searchParams.get('secret')).toBe('abc123');
+ expect(url.searchParams.get('role')).toBe('moderator');
+ // New param should be added
+ expect(url.searchParams.get(WebComponentProperty.PARTICIPANT_NAME)).toBe(participantName);
+ });
});
- it('should extract origin from room-url and set as allowed origin', () => {
- const domain = 'https://example.com';
- const roomUrl = `${domain}/room/testRoom-123?secret=123456`;
- component.setAttribute('room-url', roomUrl);
+ // ==========================================
+ // CUSTOM/UNKNOWN ATTRIBUTES
+ // ==========================================
+ describe('Custom Attributes as Query Parameters', () => {
+ it('should add custom attributes as query parameters', () => {
+ const baseRoomUrl = 'https://example.com/room/testRoom';
+ component.setAttribute(WebComponentProperty.ROOM_URL, baseRoomUrl);
+ component.setAttribute('custom-attr', 'custom-value');
+ component.setAttribute('another-param', 'another-value');
- // Trigger update
- (component as any).updateIframeSrc();
+ (component as any).updateIframeSrc();
- // Check if origin was extracted and set
- expect((component as any).targetIframeOrigin).toBe(domain);
- expect((component as any).commandsManager.targetIframeOrigin).toBe(domain);
- expect((component as any).eventsManager.targetIframeOrigin).toBe(domain);
+ const iframe = component.shadowRoot?.querySelector('iframe');
+ const url = new URL(iframe?.src || '');
+
+ expect(url.searchParams.get('custom-attr')).toBe('custom-value');
+ expect(url.searchParams.get('another-param')).toBe('another-value');
+ });
+
+ it('should handle attribute names with special characters', () => {
+ const baseRoomUrl = 'https://example.com/room/testRoom';
+ component.setAttribute(WebComponentProperty.ROOM_URL, baseRoomUrl);
+ component.setAttribute('data-test-id', '12345');
+
+ (component as any).updateIframeSrc();
+
+ const iframe = component.shadowRoot?.querySelector('iframe');
+ const url = new URL(iframe?.src || '');
+
+ expect(url.searchParams.get('data-test-id')).toBe('12345');
+ });
+ });
+
+ // ==========================================
+ // EDGE CASES
+ // ==========================================
+ describe('Edge Cases', () => {
+ it('should handle empty string attributes', () => {
+ const baseRoomUrl = 'https://example.com/room/testRoom';
+ component.setAttribute(WebComponentProperty.ROOM_URL, baseRoomUrl);
+ component.setAttribute(WebComponentProperty.PARTICIPANT_NAME, '');
+
+ (component as any).updateIframeSrc();
+
+ const iframe = component.shadowRoot?.querySelector('iframe');
+ const url = new URL(iframe?.src || '');
+
+ // Empty string should still be added as query param
+ expect(url.searchParams.has(WebComponentProperty.PARTICIPANT_NAME)).toBe(true);
+ expect(url.searchParams.get(WebComponentProperty.PARTICIPANT_NAME)).toBe('');
+ });
+
+ it('should handle special characters in attribute values', () => {
+ const baseRoomUrl = 'https://example.com/room/testRoom';
+ const specialName = 'User Name With Spaces & Special=Chars';
+ component.setAttribute(WebComponentProperty.ROOM_URL, baseRoomUrl);
+ component.setAttribute(WebComponentProperty.PARTICIPANT_NAME, specialName);
+
+ (component as any).updateIframeSrc();
+
+ const iframe = component.shadowRoot?.querySelector('iframe');
+ const url = new URL(iframe?.src || '');
+
+ // Should be URL-encoded properly
+ expect(url.searchParams.get(WebComponentProperty.PARTICIPANT_NAME)).toBe(specialName);
+ });
+
+ it('should handle updating attributes after initial render', () => {
+ const baseRoomUrl = 'https://example.com/room/testRoom';
+ component.setAttribute(WebComponentProperty.ROOM_URL, baseRoomUrl);
+ (component as any).updateIframeSrc();
+
+ const initialSrc = component.shadowRoot?.querySelector('iframe')?.src;
+
+ // Update an attribute
+ component.setAttribute(WebComponentProperty.PARTICIPANT_NAME, 'Updated Name');
+ (component as any).updateIframeSrc();
+
+ const updatedSrc = component.shadowRoot?.querySelector('iframe')?.src;
+
+ expect(initialSrc).not.toBe(updatedSrc);
+
+ const url = new URL(updatedSrc || '');
+ expect(url.searchParams.get(WebComponentProperty.PARTICIPANT_NAME)).toBe('Updated Name');
+ });
+
+ it('should handle invalid URL gracefully', () => {
+ const consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation(() => {});
+
+ // Set an invalid URL
+ component.setAttribute(WebComponentProperty.ROOM_URL, 'not-a-valid-url');
+
+ // Call updateIframeSrc directly - it should catch the error and log it
+ (component as any).updateIframeSrc();
+
+ // Verify error was logged with the invalid URL
+ expect(consoleErrorSpy).toHaveBeenCalledWith('Invalid URL provided: not-a-valid-url', expect.anything());
+
+ consoleErrorSpy.mockRestore();
+ });
+ });
+
+ // ==========================================
+ // INTEGRATION TESTS
+ // ==========================================
+ describe('Integration Tests', () => {
+ it('should handle complete real-world scenario with room-url and multiple attributes', () => {
+ const roomUrl = 'https://meet.example.com/room/team-standup?secret=xyz789';
+ const participantName = 'John Doe';
+ const e2eeKey = 'my-secure-key';
+ const redirectUrl = 'https://example.com/dashboard';
+
+ component.setAttribute(WebComponentProperty.ROOM_URL, roomUrl);
+ component.setAttribute(WebComponentProperty.PARTICIPANT_NAME, participantName);
+ component.setAttribute(WebComponentProperty.E2EE_KEY, e2eeKey);
+ component.setAttribute(WebComponentProperty.LEAVE_REDIRECT_URL, redirectUrl);
+
+ (component as any).updateIframeSrc();
+
+ const iframe = component.shadowRoot?.querySelector('iframe');
+ const url = new URL(iframe?.src || '');
+
+ // Verify base URL
+ expect(url.origin).toBe('https://meet.example.com');
+ expect(url.pathname).toBe('/room/team-standup');
+
+ // Verify all query parameters
+ expect(url.searchParams.get('secret')).toBe('xyz789');
+ expect(url.searchParams.get(WebComponentProperty.PARTICIPANT_NAME)).toBe(participantName);
+ expect(url.searchParams.get(WebComponentProperty.E2EE_KEY)).toBe(e2eeKey);
+ expect(url.searchParams.get(WebComponentProperty.LEAVE_REDIRECT_URL)).toBe(redirectUrl);
+
+ // Verify origin was set correctly
+ expect((component as any).targetIframeOrigin).toBe('https://meet.example.com');
+ });
+
+ it('should handle complete real-world scenario with recording-url', () => {
+ const recordingUrl = 'https://recordings.example.com/view/rec-20231115-abc123';
+ const participantName = 'Viewer';
+
+ component.setAttribute(WebComponentProperty.RECORDING_URL, recordingUrl);
+ component.setAttribute(WebComponentProperty.PARTICIPANT_NAME, participantName);
+ component.setAttribute(WebComponentProperty.SHOW_ONLY_RECORDINGS, 'true');
+
+ (component as any).updateIframeSrc();
+
+ const iframe = component.shadowRoot?.querySelector('iframe');
+ const url = new URL(iframe?.src || '');
+
+ // Verify base URL
+ expect(url.origin).toBe('https://recordings.example.com');
+ expect(url.pathname).toBe('/view/rec-20231115-abc123');
+
+ // Verify query parameters
+ expect(url.searchParams.get(WebComponentProperty.PARTICIPANT_NAME)).toBe(participantName);
+ expect(url.searchParams.get(WebComponentProperty.SHOW_ONLY_RECORDINGS)).toBe('true');
+
+ // Verify origin was set correctly
+ expect((component as any).targetIframeOrigin).toBe('https://recordings.example.com');
+ });
});
});
diff --git a/meet-ce/typings/src/webcomponent/properties.model.ts b/meet-ce/typings/src/webcomponent/properties.model.ts
index 7b39c898..f573ff3b 100644
--- a/meet-ce/typings/src/webcomponent/properties.model.ts
+++ b/meet-ce/typings/src/webcomponent/properties.model.ts
@@ -14,6 +14,12 @@ export enum WebComponentProperty {
*/
PARTICIPANT_NAME = 'participant-name',
+ /**
+ * Secret key for end-to-end encryption (E2EE).
+ * If provided, the participant will join the meeting using E2EE key.
+ */
+ E2EE_KEY = 'e2ee-key',
+
/**
* URL to redirect to when leaving the meeting.
* Redirection occurs after the **`CLOSED` event** fires.
diff --git a/meet.sh b/meet.sh
index b339a76a..7e9c6972 100755
--- a/meet.sh
+++ b/meet.sh
@@ -622,30 +622,42 @@ build_webcomponent_doc() {
mkdir -p "$output_dir"
fi
- if [ -f "docs/webcomponent-events.md" ] && [ -f "docs/webcomponent-commands.md" ] && [ -f "docs/webcomponent-attributes.md" ]; then
+ if [ -f "docs/webcomponent-events.md" ] && \
+ [ -f "docs/webcomponent-commands.md" ] && \
+ [ -f "docs/webcomponent-attributes.md" ]; then
echo -e "${GREEN}Copying documentation to: $output_dir${NC}"
- cp docs/webcomponent-events.md "$output_dir/webcomponent-events.md"
- cp docs/webcomponent-commands.md "$output_dir/webcomponent-commands.md"
- cp docs/webcomponent-attributes.md "$output_dir/webcomponent-attributes.md"
+ cp docs/webcomponent-{events,commands,attributes}.md "$output_dir"/
echo -e "${GREEN}✓ Documentation copied successfully!${NC}"
+ rm -f docs/webcomponent-{events,commands,attributes}.md
else
echo -e "${RED}Error: Documentation files not found in docs/ directory${NC}"
exit 1
fi
else
echo -e "${YELLOW}No output directory specified. Documentation remains in docs/ directory.${NC}"
+ output_dir="docs"
+ fi
+
+ local abs_path
+ if command -v realpath >/dev/null 2>&1; then
+ abs_path=$(realpath "$output_dir")
+ elif command -v readlink >/dev/null 2>&1; then
+ abs_path=$(readlink -f "$output_dir" 2>/dev/null || (cd "$output_dir" && pwd))
+ else
+ abs_path=$(cd "$output_dir" && pwd)
fi
echo
echo -e "${GREEN}✓ Webcomponent documentation generated successfully!${NC}"
- echo -e "${YELLOW}Output directory: $output_dir${NC}"
- rm -f docs/webcomponent-events.md docs/webcomponent-commands.md docs/webcomponent-attributes.md
+ echo -e "${YELLOW}Output directory: ${abs_path}${NC}"
}
+
# Build REST API documentation
build_rest_api_doc() {
- local output_dir="$1"
+ local output_target="$1"
CE_REST_API_DOC_PATH="meet-ce/backend/public/openapi/"
+
echo -e "${BLUE}=====================================${NC}"
echo -e "${BLUE} Building REST API Docs${NC}"
echo -e "${BLUE}=====================================${NC}"
@@ -653,31 +665,56 @@ build_rest_api_doc() {
check_pnpm
+ # Solo instalar si no existen dependencias locales del backend
+ if [ ! -d "node_modules" ] || [ ! -d "meet-ce/backend/node_modules" ]; then
+ echo -e "${YELLOW}Backend dependencies not found. Installing minimal backend deps...${NC}"
+ pnpm --filter @openvidu-meet/backend install
+ else
+ echo -e "${GREEN}Backend dependencies already present. Skipping install.${NC}"
+ fi
+
echo -e "${GREEN}Generating REST API documentation...${NC}"
pnpm run build:rest-api-docs
- if [ -n "$output_dir" ]; then
- output_dir="${output_dir%/}"
+ # Determinar si el parámetro es archivo o directorio
+ local output_dir output_file
+ if [[ "$output_target" =~ \.html$ ]]; then
+ output_dir=$(dirname "$output_target")
+ output_file="$output_target"
+ else
+ output_dir="${output_target%/}"
+ output_file="$output_dir/public.html"
+ fi
- if [ ! -d "$output_dir" ]; then
- echo -e "${YELLOW}Creating output directory: $output_dir${NC}"
- mkdir -p "$output_dir"
- fi
+ # Crear carpeta contenedora si no existe
+ if [ ! -d "$output_dir" ]; then
+ echo -e "${YELLOW}Creating output directory: $output_dir${NC}"
+ mkdir -p "$output_dir"
+ fi
- if [ -f "$CE_REST_API_DOC_PATH/public.html" ]; then
- echo -e "${GREEN}Copying REST API documentation to: $output_dir${NC}"
- cp "$CE_REST_API_DOC_PATH/public.html" "$output_dir/public.html"
- echo -e "${GREEN}✓ Documentation copied successfully!${NC}"
- else
- echo -e "${RED}Error: REST API documentation files not found${NC}"
+ # Preguntar si el archivo ya existe
+ if [ -f "$output_file" ]; then
+ echo -e "${YELLOW}Warning: '$output_file' already exists.${NC}"
+ read -rp "Do you want to overwrite it? [y/N]: " confirm
+ if [[ ! "$confirm" =~ ^[Yy]$ ]]; then
+ echo -e "${RED}Operation cancelled by user.${NC}"
exit 1
fi
+ fi
+
+ # Copiar documentación
+ if [ -f "$CE_REST_API_DOC_PATH/public.html" ]; then
+ echo -e "${GREEN}Copying REST API documentation to: $output_file${NC}"
+ cp "$CE_REST_API_DOC_PATH/public.html" "$output_file"
+ echo -e "${GREEN}✓ Documentation copied successfully!${NC}"
else
- echo -e "${YELLOW}No output directory specified. Documentation remains in backend/ directory.${NC}"
+ echo -e "${RED}Error: REST API documentation files not found${NC}"
+ exit 1
fi
echo
echo -e "${GREEN}✓ REST API documentation generated successfully!${NC}"
+ echo -e "${YELLOW}Output file: $(cd "$(dirname "$output_file")" && pwd)/$(basename "$output_file")${NC}"
}
# Clone private meet-pro repository into repository root
diff --git a/openvidu-meet.code-workspace b/openvidu-meet.code-workspace
index ef85e143..d21368f2 100644
--- a/openvidu-meet.code-workspace
+++ b/openvidu-meet.code-workspace
@@ -2,42 +2,42 @@
"folders": [
{
"name": "openvidu-components-angular",
- "path": "../openvidu/openvidu-components-angular",
+ "path": "../openvidu/openvidu-components-angular"
},
{
"name": "openvidu-meet (root)",
- "path": ".",
+ "path": "."
},
{
"name": "openvidu-meet (CE)",
- "path": "meet-ce",
+ "path": "meet-ce"
},
{
"name": "openvidu-meet (PRO)",
- "path": "meet-pro",
+ "path": "meet-pro"
},
{
"name": "shared-meet-components",
- "path": "meet-ce/frontend/projects/shared-meet-components",
+ "path": "meet-ce/frontend/projects/shared-meet-components"
},
{
"name": "meet-testapp",
- "path": "testapp",
+ "path": "testapp"
},
{
"name": "meet-webcomponent",
- "path": "meet-ce/frontend/webcomponent",
- },
+ "path": "meet-ce/frontend/webcomponent"
+ }
],
"settings": {
"files.exclude": {
"**/meet-ce": true,
"**/meet-pro": true,
- "**/webcomponent": true,
+ "**/frontend/webcomponent": true,
"**/webhooks-snippets": false,
"**/testapp": true,
"**/.angular": true,
- "**/public": true,
+ "**/public": false,
"**/dist": false,
"**/node_modules": true,
"**/test-results": true,
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index bc826602..05914562 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -501,6 +501,365 @@ importers:
specifier: 5.9.2
version: 5.9.2
+ meet-pro/backend:
+ dependencies:
+ '@aws-sdk/client-s3':
+ specifier: 3.846.0
+ version: 3.846.0
+ '@azure/storage-blob':
+ specifier: 12.27.0
+ version: 12.27.0
+ '@google-cloud/storage':
+ specifier: 7.17.1
+ version: 7.17.1(encoding@0.1.13)
+ '@openvidu-meet-pro/typings':
+ specifier: workspace:*
+ version: link:../typings
+ '@openvidu-meet/backend':
+ specifier: workspace:*
+ version: link:../../meet-ce/backend
+ '@sesamecare-oss/redlock':
+ specifier: 1.4.0
+ version: 1.4.0(ioredis@5.6.1)
+ archiver:
+ specifier: 7.0.1
+ version: 7.0.1
+ bcrypt:
+ specifier: 5.1.1
+ version: 5.1.1(encoding@0.1.13)
+ body-parser:
+ specifier: 2.2.0
+ version: 2.2.0
+ chalk:
+ specifier: 5.6.2
+ version: 5.6.2
+ cookie-parser:
+ specifier: 1.4.7
+ version: 1.4.7
+ cors:
+ specifier: 2.8.5
+ version: 2.8.5
+ cron:
+ specifier: 4.3.3
+ version: 4.3.3
+ dotenv:
+ specifier: 16.6.1
+ version: 16.6.1
+ express:
+ specifier: 4.21.2
+ version: 4.21.2
+ express-rate-limit:
+ specifier: 7.5.1
+ version: 7.5.1(express@4.21.2)
+ inversify:
+ specifier: 6.2.2
+ version: 6.2.2(reflect-metadata@0.2.2)
+ ioredis:
+ specifier: 5.6.1
+ version: 5.6.1
+ jwt-decode:
+ specifier: 4.0.0
+ version: 4.0.0
+ livekit-server-sdk:
+ specifier: 2.13.1
+ version: 2.13.1
+ ms:
+ specifier: 2.1.3
+ version: 2.1.3
+ uid:
+ specifier: 2.0.2
+ version: 2.0.2
+ winston:
+ specifier: 3.18.3
+ version: 3.18.3
+ yamljs:
+ specifier: 0.3.0
+ version: 0.3.0
+ zod:
+ specifier: 3.25.76
+ version: 3.25.76
+ devDependencies:
+ '@types/archiver':
+ specifier: 6.0.3
+ version: 6.0.3
+ '@types/bcrypt':
+ specifier: 5.0.2
+ version: 5.0.2
+ '@types/cookie-parser':
+ specifier: 1.4.9
+ version: 1.4.9(@types/express@4.17.23)
+ '@types/cors':
+ specifier: 2.8.19
+ version: 2.8.19
+ '@types/express':
+ specifier: 4.17.23
+ version: 4.17.23
+ '@types/jest':
+ specifier: 29.5.14
+ version: 29.5.14
+ '@types/ms':
+ specifier: 2.1.0
+ version: 2.1.0
+ '@types/node':
+ specifier: 22.16.4
+ version: 22.16.4
+ '@types/supertest':
+ specifier: 6.0.3
+ version: 6.0.3
+ '@types/unzipper':
+ specifier: 0.10.11
+ version: 0.10.11
+ '@types/validator':
+ specifier: 13.15.2
+ version: 13.15.2
+ '@types/yamljs':
+ specifier: 0.2.34
+ version: 0.2.34
+ '@typescript-eslint/eslint-plugin':
+ specifier: 6.21.0
+ version: 6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.9.2))(eslint@8.57.1)(typescript@5.9.2)
+ '@typescript-eslint/parser':
+ specifier: 6.21.0
+ version: 6.21.0(eslint@8.57.1)(typescript@5.9.2)
+ cross-env:
+ specifier: 7.0.3
+ version: 7.0.3
+ eslint:
+ specifier: 8.57.1
+ version: 8.57.1
+ eslint-config-prettier:
+ specifier: 9.1.0
+ version: 9.1.0(eslint@8.57.1)
+ jest:
+ specifier: 29.7.0
+ version: 29.7.0(@types/node@22.16.4)(ts-node@10.9.2(@types/node@22.16.4)(typescript@5.9.2))
+ jest-fetch-mock:
+ specifier: 3.0.3
+ version: 3.0.3(encoding@0.1.13)
+ jest-junit:
+ specifier: 16.0.0
+ version: 16.0.0
+ nodemon:
+ specifier: 3.1.10
+ version: 3.1.10
+ openapi-generate-html:
+ specifier: 0.5.3
+ version: 0.5.3(@types/node@22.16.4)
+ prettier:
+ specifier: 3.6.2
+ version: 3.6.2
+ supertest:
+ specifier: 7.1.3
+ version: 7.1.3
+ ts-jest:
+ specifier: 29.4.0
+ version: 29.4.0(@babel/core@7.28.4)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.28.4))(jest-util@29.7.0)(jest@29.7.0(@types/node@22.16.4)(ts-node@10.9.2(@types/node@22.16.4)(typescript@5.9.2)))(typescript@5.9.2)
+ ts-jest-resolver:
+ specifier: 2.0.1
+ version: 2.0.1
+ tsx:
+ specifier: 4.20.3
+ version: 4.20.3
+ typescript:
+ specifier: 5.9.2
+ version: 5.9.2
+ unzipper:
+ specifier: 0.12.3
+ version: 0.12.3
+
+ meet-pro/frontend:
+ dependencies:
+ '@angular/animations':
+ specifier: 20.3.4
+ version: 20.3.4(@angular/core@20.3.4(@angular/compiler@20.3.4)(rxjs@7.8.2)(zone.js@0.15.1))
+ '@angular/cdk':
+ specifier: 20.2.9
+ version: 20.2.9(@angular/common@20.3.4(@angular/core@20.3.4(@angular/compiler@20.3.4)(rxjs@7.8.2)(zone.js@0.15.1))(rxjs@7.8.2))(@angular/core@20.3.4(@angular/compiler@20.3.4)(rxjs@7.8.2)(zone.js@0.15.1))(rxjs@7.8.2)
+ '@angular/common':
+ specifier: 20.3.4
+ version: 20.3.4(@angular/core@20.3.4(@angular/compiler@20.3.4)(rxjs@7.8.2)(zone.js@0.15.1))(rxjs@7.8.2)
+ '@angular/compiler':
+ specifier: 20.3.4
+ version: 20.3.4
+ '@angular/core':
+ specifier: 20.3.4
+ version: 20.3.4(@angular/compiler@20.3.4)(rxjs@7.8.2)(zone.js@0.15.1)
+ '@angular/forms':
+ specifier: 20.3.4
+ version: 20.3.4(@angular/common@20.3.4(@angular/core@20.3.4(@angular/compiler@20.3.4)(rxjs@7.8.2)(zone.js@0.15.1))(rxjs@7.8.2))(@angular/core@20.3.4(@angular/compiler@20.3.4)(rxjs@7.8.2)(zone.js@0.15.1))(@angular/platform-browser@20.3.4(@angular/animations@20.3.4(@angular/core@20.3.4(@angular/compiler@20.3.4)(rxjs@7.8.2)(zone.js@0.15.1)))(@angular/common@20.3.4(@angular/core@20.3.4(@angular/compiler@20.3.4)(rxjs@7.8.2)(zone.js@0.15.1))(rxjs@7.8.2))(@angular/core@20.3.4(@angular/compiler@20.3.4)(rxjs@7.8.2)(zone.js@0.15.1)))(rxjs@7.8.2)
+ '@angular/material':
+ specifier: 20.2.9
+ version: 20.2.9(b517547b325ffc8400ae4cda6a618bfd)
+ '@angular/platform-browser':
+ specifier: 20.3.4
+ version: 20.3.4(@angular/animations@20.3.4(@angular/core@20.3.4(@angular/compiler@20.3.4)(rxjs@7.8.2)(zone.js@0.15.1)))(@angular/common@20.3.4(@angular/core@20.3.4(@angular/compiler@20.3.4)(rxjs@7.8.2)(zone.js@0.15.1))(rxjs@7.8.2))(@angular/core@20.3.4(@angular/compiler@20.3.4)(rxjs@7.8.2)(zone.js@0.15.1))
+ '@angular/platform-browser-dynamic':
+ specifier: 20.3.4
+ version: 20.3.4(@angular/common@20.3.4(@angular/core@20.3.4(@angular/compiler@20.3.4)(rxjs@7.8.2)(zone.js@0.15.1))(rxjs@7.8.2))(@angular/compiler@20.3.4)(@angular/core@20.3.4(@angular/compiler@20.3.4)(rxjs@7.8.2)(zone.js@0.15.1))(@angular/platform-browser@20.3.4(@angular/animations@20.3.4(@angular/core@20.3.4(@angular/compiler@20.3.4)(rxjs@7.8.2)(zone.js@0.15.1)))(@angular/common@20.3.4(@angular/core@20.3.4(@angular/compiler@20.3.4)(rxjs@7.8.2)(zone.js@0.15.1))(rxjs@7.8.2))(@angular/core@20.3.4(@angular/compiler@20.3.4)(rxjs@7.8.2)(zone.js@0.15.1)))
+ '@angular/router':
+ specifier: 20.3.4
+ version: 20.3.4(@angular/common@20.3.4(@angular/core@20.3.4(@angular/compiler@20.3.4)(rxjs@7.8.2)(zone.js@0.15.1))(rxjs@7.8.2))(@angular/core@20.3.4(@angular/compiler@20.3.4)(rxjs@7.8.2)(zone.js@0.15.1))(@angular/platform-browser@20.3.4(@angular/animations@20.3.4(@angular/core@20.3.4(@angular/compiler@20.3.4)(rxjs@7.8.2)(zone.js@0.15.1)))(@angular/common@20.3.4(@angular/core@20.3.4(@angular/compiler@20.3.4)(rxjs@7.8.2)(zone.js@0.15.1))(rxjs@7.8.2))(@angular/core@20.3.4(@angular/compiler@20.3.4)(rxjs@7.8.2)(zone.js@0.15.1)))(rxjs@7.8.2)
+ '@livekit/track-processors':
+ specifier: 0.6.1
+ version: 0.6.1(@types/dom-mediacapture-transform@0.1.11)(livekit-client@2.15.11(@types/dom-mediacapture-record@1.0.22))
+ '@openvidu-meet/shared-components':
+ specifier: workspace:*
+ version: link:../../meet-ce/frontend/projects/shared-meet-components
+ '@openvidu-meet/typings':
+ specifier: workspace:*
+ version: link:../../meet-ce/typings
+ autolinker:
+ specifier: 4.1.5
+ version: 4.1.5
+ core-js:
+ specifier: 3.45.1
+ version: 3.45.1
+ jwt-decode:
+ specifier: 4.0.0
+ version: 4.0.0
+ livekit-client:
+ specifier: 2.15.11
+ version: 2.15.11(@types/dom-mediacapture-record@1.0.22)
+ openvidu-components-angular:
+ specifier: workspace:*
+ version: link:../../../openvidu/openvidu-components-angular/projects/openvidu-components-angular
+ rxjs:
+ specifier: 7.8.2
+ version: 7.8.2
+ tslib:
+ specifier: 2.8.1
+ version: 2.8.1
+ unique-names-generator:
+ specifier: 4.7.1
+ version: 4.7.1
+ zone.js:
+ specifier: 0.15.1
+ version: 0.15.1
+ devDependencies:
+ '@angular-builders/custom-webpack':
+ specifier: 20.0.0
+ version: 20.0.0(@angular/compiler-cli@20.3.4(@angular/compiler@20.3.4)(typescript@5.9.2))(@angular/compiler@20.3.4)(@angular/core@20.3.4(@angular/compiler@20.3.4)(rxjs@7.8.2)(zone.js@0.15.1))(@angular/platform-browser@20.3.4(@angular/animations@20.3.4(@angular/core@20.3.4(@angular/compiler@20.3.4)(rxjs@7.8.2)(zone.js@0.15.1)))(@angular/common@20.3.4(@angular/core@20.3.4(@angular/compiler@20.3.4)(rxjs@7.8.2)(zone.js@0.15.1))(rxjs@7.8.2))(@angular/core@20.3.4(@angular/compiler@20.3.4)(rxjs@7.8.2)(zone.js@0.15.1)))(@types/node@22.18.8)(browser-sync@3.0.4)(chokidar@4.0.3)(jest-environment-jsdom@29.7.0)(jest@29.7.0(@types/node@22.18.8)(ts-node@10.9.2(@types/node@22.18.8)(typescript@5.9.2)))(jiti@1.21.7)(karma@6.4.4)(less@4.4.2)(ng-packagr@20.3.0(@angular/compiler-cli@20.3.4(@angular/compiler@20.3.4)(typescript@5.9.2))(tslib@2.8.1)(typescript@5.9.2))(postcss@8.5.6)(terser@5.44.0)(tslib@2.8.1)(tsx@4.20.3)(typescript@5.9.2)
+ '@angular-devkit/build-angular':
+ specifier: 20.3.4
+ version: 20.3.4(@angular/compiler-cli@20.3.4(@angular/compiler@20.3.4)(typescript@5.9.2))(@angular/compiler@20.3.4)(@angular/core@20.3.4(@angular/compiler@20.3.4)(rxjs@7.8.2)(zone.js@0.15.1))(@angular/platform-browser@20.3.4(@angular/animations@20.3.4(@angular/core@20.3.4(@angular/compiler@20.3.4)(rxjs@7.8.2)(zone.js@0.15.1)))(@angular/common@20.3.4(@angular/core@20.3.4(@angular/compiler@20.3.4)(rxjs@7.8.2)(zone.js@0.15.1))(rxjs@7.8.2))(@angular/core@20.3.4(@angular/compiler@20.3.4)(rxjs@7.8.2)(zone.js@0.15.1)))(@types/node@22.18.8)(browser-sync@3.0.4)(chokidar@4.0.3)(jest-environment-jsdom@29.7.0)(jest@29.7.0(@types/node@22.18.8)(ts-node@10.9.2(@types/node@22.18.8)(typescript@5.9.2)))(jiti@1.21.7)(karma@6.4.4)(ng-packagr@20.3.0(@angular/compiler-cli@20.3.4(@angular/compiler@20.3.4)(typescript@5.9.2))(tslib@2.8.1)(typescript@5.9.2))(tsx@4.20.3)(typescript@5.9.2)
+ '@angular-eslint/builder':
+ specifier: 20.3.0
+ version: 20.3.0(chokidar@4.0.3)(eslint@8.57.1)(typescript@5.9.2)
+ '@angular-eslint/eslint-plugin':
+ specifier: 20.3.0
+ version: 20.3.0(@typescript-eslint/utils@8.46.1(eslint@8.57.1)(typescript@5.9.2))(eslint@8.57.1)(typescript@5.9.2)
+ '@angular-eslint/eslint-plugin-template':
+ specifier: 20.3.0
+ version: 20.3.0(@angular-eslint/template-parser@20.3.0(eslint@8.57.1)(typescript@5.9.2))(@typescript-eslint/types@8.46.1)(@typescript-eslint/utils@8.46.1(eslint@8.57.1)(typescript@5.9.2))(eslint@8.57.1)(typescript@5.9.2)
+ '@angular-eslint/schematics':
+ specifier: 20.3.0
+ version: 20.3.0(@angular-eslint/template-parser@20.3.0(eslint@8.57.1)(typescript@5.9.2))(@typescript-eslint/types@8.46.1)(@typescript-eslint/utils@8.46.1(eslint@8.57.1)(typescript@5.9.2))(chokidar@4.0.3)(eslint@8.57.1)(typescript@5.9.2)
+ '@angular-eslint/template-parser':
+ specifier: 20.3.0
+ version: 20.3.0(eslint@8.57.1)(typescript@5.9.2)
+ '@angular/cli':
+ specifier: 20.3.4
+ version: 20.3.4(@types/node@22.18.8)(chokidar@4.0.3)
+ '@angular/compiler-cli':
+ specifier: 20.3.4
+ version: 20.3.4(@angular/compiler@20.3.4)(typescript@5.9.2)
+ '@types/chai':
+ specifier: 4.3.20
+ version: 4.3.20
+ '@types/fluent-ffmpeg':
+ specifier: 2.1.27
+ version: 2.1.27
+ '@types/jasmine':
+ specifier: 5.1.9
+ version: 5.1.9
+ '@types/mocha':
+ specifier: 9.1.1
+ version: 9.1.1
+ '@types/node':
+ specifier: 22.18.8
+ version: 22.18.8
+ '@types/pixelmatch':
+ specifier: 5.2.6
+ version: 5.2.6
+ '@types/pngjs':
+ specifier: 6.0.5
+ version: 6.0.5
+ '@types/selenium-webdriver':
+ specifier: 4.35.1
+ version: 4.35.1
+ '@typescript-eslint/eslint-plugin':
+ specifier: 8.46.1
+ version: 8.46.1(@typescript-eslint/parser@8.46.1(eslint@8.57.1)(typescript@5.9.2))(eslint@8.57.1)(typescript@5.9.2)
+ '@typescript-eslint/parser':
+ specifier: 8.46.1
+ version: 8.46.1(eslint@8.57.1)(typescript@5.9.2)
+ chai:
+ specifier: 4.5.0
+ version: 4.5.0
+ chromedriver:
+ specifier: 141.0.0
+ version: 141.0.0
+ cross-env:
+ specifier: 7.0.3
+ version: 7.0.3
+ eslint:
+ specifier: 8.57.1
+ version: 8.57.1
+ eslint-config-prettier:
+ specifier: 9.1.0
+ version: 9.1.0(eslint@8.57.1)
+ fluent-ffmpeg:
+ specifier: 2.1.3
+ version: 2.1.3
+ jasmine-core:
+ specifier: 5.6.0
+ version: 5.6.0
+ jasmine-spec-reporter:
+ specifier: 7.0.0
+ version: 7.0.0
+ karma:
+ specifier: 6.4.4
+ version: 6.4.4
+ karma-chrome-launcher:
+ specifier: 3.2.0
+ version: 3.2.0
+ karma-coverage:
+ specifier: 2.2.1
+ version: 2.2.1
+ karma-jasmine:
+ specifier: 5.1.0
+ version: 5.1.0(karma@6.4.4)
+ karma-jasmine-html-reporter:
+ specifier: 2.1.0
+ version: 2.1.0(jasmine-core@5.6.0)(karma-jasmine@5.1.0(karma@6.4.4))(karma@6.4.4)
+ mocha:
+ specifier: 10.7.3
+ version: 10.7.3
+ ng-packagr:
+ specifier: 20.3.0
+ version: 20.3.0(@angular/compiler-cli@20.3.4(@angular/compiler@20.3.4)(typescript@5.9.2))(tslib@2.8.1)(typescript@5.9.2)
+ prettier:
+ specifier: 3.3.3
+ version: 3.3.3
+ selenium-webdriver:
+ specifier: 4.25.0
+ version: 4.25.0
+ ts-node:
+ specifier: 10.9.2
+ version: 10.9.2(@types/node@22.18.8)(typescript@5.9.2)
+ typescript:
+ specifier: 5.9.2
+ version: 5.9.2
+
+ meet-pro/typings:
+ devDependencies:
+ '@openvidu-meet/typings':
+ specifier: workspace:*
+ version: link:../../meet-ce/typings
+ typescript:
+ specifier: 5.9.2
+ version: 5.9.2
+
testapp:
dependencies:
'@openvidu-meet/typings':
@@ -6570,6 +6929,7 @@ packages:
livekit-client@2.15.11:
resolution: {integrity: sha512-9cHdAbSibPGyt7wWM+GAUswIOuklQHF9y561Oruzh0nNFNvRzMsE10oqJvjs0k6s2Jl+j/Z5Ar90bzVwLpu1yg==}
+ deprecated: Compatibility issue around AbortSignal.any usage, use >=2.15.12 instead
peerDependencies:
'@types/dom-mediacapture-record': ^1
@@ -9322,6 +9682,59 @@ snapshots:
- webpack-cli
- yaml
+ '@angular-builders/custom-webpack@20.0.0(@angular/compiler-cli@20.3.4(@angular/compiler@20.3.4)(typescript@5.9.2))(@angular/compiler@20.3.4)(@angular/core@20.3.4(@angular/compiler@20.3.4)(rxjs@7.8.2)(zone.js@0.15.1))(@angular/platform-browser@20.3.4(@angular/animations@20.3.4(@angular/core@20.3.4(@angular/compiler@20.3.4)(rxjs@7.8.2)(zone.js@0.15.1)))(@angular/common@20.3.4(@angular/core@20.3.4(@angular/compiler@20.3.4)(rxjs@7.8.2)(zone.js@0.15.1))(rxjs@7.8.2))(@angular/core@20.3.4(@angular/compiler@20.3.4)(rxjs@7.8.2)(zone.js@0.15.1)))(@types/node@22.18.8)(browser-sync@3.0.4)(chokidar@4.0.3)(jest-environment-jsdom@29.7.0)(jest@29.7.0(@types/node@22.18.8)(ts-node@10.9.2(@types/node@22.18.8)(typescript@5.9.2)))(jiti@1.21.7)(karma@6.4.4)(less@4.4.2)(ng-packagr@20.3.0(@angular/compiler-cli@20.3.4(@angular/compiler@20.3.4)(typescript@5.9.2))(tslib@2.8.1)(typescript@5.9.2))(postcss@8.5.6)(terser@5.44.0)(tslib@2.8.1)(tsx@4.20.3)(typescript@5.9.2)':
+ dependencies:
+ '@angular-builders/common': 4.0.0(@types/node@22.18.8)(chokidar@4.0.3)(typescript@5.9.2)
+ '@angular-devkit/architect': 0.2003.5(chokidar@4.0.3)
+ '@angular-devkit/build-angular': 20.3.4(@angular/compiler-cli@20.3.4(@angular/compiler@20.3.4)(typescript@5.9.2))(@angular/compiler@20.3.4)(@angular/core@20.3.4(@angular/compiler@20.3.4)(rxjs@7.8.2)(zone.js@0.15.1))(@angular/platform-browser@20.3.4(@angular/animations@20.3.4(@angular/core@20.3.4(@angular/compiler@20.3.4)(rxjs@7.8.2)(zone.js@0.15.1)))(@angular/common@20.3.4(@angular/core@20.3.4(@angular/compiler@20.3.4)(rxjs@7.8.2)(zone.js@0.15.1))(rxjs@7.8.2))(@angular/core@20.3.4(@angular/compiler@20.3.4)(rxjs@7.8.2)(zone.js@0.15.1)))(@types/node@22.18.8)(browser-sync@3.0.4)(chokidar@4.0.3)(jest-environment-jsdom@29.7.0)(jest@29.7.0(@types/node@22.18.8)(ts-node@10.9.2(@types/node@22.18.8)(typescript@5.9.2)))(jiti@1.21.7)(karma@6.4.4)(ng-packagr@20.3.0(@angular/compiler-cli@20.3.4(@angular/compiler@20.3.4)(typescript@5.9.2))(tslib@2.8.1)(typescript@5.9.2))(tsx@4.20.3)(typescript@5.9.2)
+ '@angular-devkit/core': 20.3.5(chokidar@4.0.3)
+ '@angular/build': 20.3.4(@angular/compiler-cli@20.3.4(@angular/compiler@20.3.4)(typescript@5.9.2))(@angular/compiler@20.3.4)(@angular/core@20.3.4(@angular/compiler@20.3.4)(rxjs@7.8.2)(zone.js@0.15.1))(@angular/platform-browser@20.3.4(@angular/animations@20.3.4(@angular/core@20.3.4(@angular/compiler@20.3.4)(rxjs@7.8.2)(zone.js@0.15.1)))(@angular/common@20.3.4(@angular/core@20.3.4(@angular/compiler@20.3.4)(rxjs@7.8.2)(zone.js@0.15.1))(rxjs@7.8.2))(@angular/core@20.3.4(@angular/compiler@20.3.4)(rxjs@7.8.2)(zone.js@0.15.1)))(@types/node@22.18.8)(chokidar@4.0.3)(jiti@1.21.7)(karma@6.4.4)(less@4.4.2)(ng-packagr@20.3.0(@angular/compiler-cli@20.3.4(@angular/compiler@20.3.4)(typescript@5.9.2))(tslib@2.8.1)(typescript@5.9.2))(postcss@8.5.6)(terser@5.44.0)(tslib@2.8.1)(tsx@4.20.3)(typescript@5.9.2)
+ '@angular/compiler-cli': 20.3.4(@angular/compiler@20.3.4)(typescript@5.9.2)
+ lodash: 4.17.21
+ webpack-merge: 6.0.1
+ transitivePeerDependencies:
+ - '@angular/compiler'
+ - '@angular/core'
+ - '@angular/localize'
+ - '@angular/platform-browser'
+ - '@angular/platform-server'
+ - '@angular/service-worker'
+ - '@angular/ssr'
+ - '@rspack/core'
+ - '@swc/core'
+ - '@swc/wasm'
+ - '@types/node'
+ - '@web/test-runner'
+ - browser-sync
+ - bufferutil
+ - chokidar
+ - debug
+ - html-webpack-plugin
+ - jest
+ - jest-environment-jsdom
+ - jiti
+ - karma
+ - less
+ - lightningcss
+ - ng-packagr
+ - node-sass
+ - postcss
+ - protractor
+ - sass-embedded
+ - stylus
+ - sugarss
+ - supports-color
+ - tailwindcss
+ - terser
+ - tslib
+ - tsx
+ - typescript
+ - uglify-js
+ - utf-8-validate
+ - vitest
+ - webpack-cli
+ - yaml
+
'@angular-devkit/architect@0.2003.4(chokidar@4.0.3)':
dependencies:
'@angular-devkit/core': 20.3.4(chokidar@4.0.3)
@@ -9340,7 +9753,7 @@ snapshots:
dependencies:
'@ampproject/remapping': 2.3.0
'@angular-devkit/architect': 0.2003.4(chokidar@4.0.3)
- '@angular-devkit/build-webpack': 0.2003.4(chokidar@4.0.3)(webpack-dev-server@5.2.2(webpack@5.101.2))(webpack@5.101.2)
+ '@angular-devkit/build-webpack': 0.2003.4(chokidar@4.0.3)(webpack-dev-server@5.2.2(webpack@5.101.2(esbuild@0.25.9)))(webpack@5.101.2(esbuild@0.25.9))
'@angular-devkit/core': 20.3.4(chokidar@4.0.3)
'@angular/build': 20.3.4(@angular/compiler-cli@20.3.4(@angular/compiler@20.3.4)(typescript@5.9.2))(@angular/compiler@20.3.4)(@angular/core@20.3.4(@angular/compiler@20.3.4)(rxjs@7.8.2)(zone.js@0.15.1))(@angular/platform-browser@20.3.4(@angular/animations@20.3.4(@angular/core@20.3.4(@angular/compiler@20.3.4)(rxjs@7.8.2)(zone.js@0.15.1)))(@angular/common@20.3.4(@angular/core@20.3.4(@angular/compiler@20.3.4)(rxjs@7.8.2)(zone.js@0.15.1))(rxjs@7.8.2))(@angular/core@20.3.4(@angular/compiler@20.3.4)(rxjs@7.8.2)(zone.js@0.15.1)))(@types/node@22.18.8)(chokidar@4.0.3)(jiti@1.21.7)(karma@6.4.4)(less@4.4.0)(ng-packagr@20.3.0(@angular/compiler-cli@20.3.4(@angular/compiler@20.3.4)(typescript@5.9.2))(tslib@2.8.1)(typescript@5.9.2))(postcss@8.5.6)(terser@5.43.1)(tslib@2.8.1)(tsx@4.20.3)(typescript@5.9.2)
'@angular/compiler-cli': 20.3.4(@angular/compiler@20.3.4)(typescript@5.9.2)
@@ -9354,13 +9767,13 @@ snapshots:
'@babel/preset-env': 7.28.3(@babel/core@7.28.3)
'@babel/runtime': 7.28.3
'@discoveryjs/json-ext': 0.6.3
- '@ngtools/webpack': 20.3.4(@angular/compiler-cli@20.3.4(@angular/compiler@20.3.4)(typescript@5.9.2))(typescript@5.9.2)(webpack@5.101.2)
+ '@ngtools/webpack': 20.3.4(@angular/compiler-cli@20.3.4(@angular/compiler@20.3.4)(typescript@5.9.2))(typescript@5.9.2)(webpack@5.101.2(esbuild@0.25.9))
ansi-colors: 4.1.3
autoprefixer: 10.4.21(postcss@8.5.6)
- babel-loader: 10.0.0(@babel/core@7.28.3)(webpack@5.101.2)
+ babel-loader: 10.0.0(@babel/core@7.28.3)(webpack@5.101.2(esbuild@0.25.9))
browserslist: 4.26.3
- copy-webpack-plugin: 13.0.1(webpack@5.101.2)
- css-loader: 7.1.2(webpack@5.101.2)
+ copy-webpack-plugin: 13.0.1(webpack@5.101.2(esbuild@0.25.9))
+ css-loader: 7.1.2(webpack@5.101.2(esbuild@0.25.9))
esbuild-wasm: 0.25.9
fast-glob: 3.3.3
http-proxy-middleware: 3.0.5
@@ -9368,32 +9781,32 @@ snapshots:
jsonc-parser: 3.3.1
karma-source-map-support: 1.4.0
less: 4.4.0
- less-loader: 12.3.0(less@4.4.0)(webpack@5.101.2)
- license-webpack-plugin: 4.0.2(webpack@5.101.2)
+ less-loader: 12.3.0(less@4.4.0)(webpack@5.101.2(esbuild@0.25.9))
+ license-webpack-plugin: 4.0.2(webpack@5.101.2(esbuild@0.25.9))
loader-utils: 3.3.1
- mini-css-extract-plugin: 2.9.4(webpack@5.101.2)
+ mini-css-extract-plugin: 2.9.4(webpack@5.101.2(esbuild@0.25.9))
open: 10.2.0
ora: 8.2.0
picomatch: 4.0.3
piscina: 5.1.3
postcss: 8.5.6
- postcss-loader: 8.1.1(postcss@8.5.6)(typescript@5.9.2)(webpack@5.101.2)
+ postcss-loader: 8.1.1(postcss@8.5.6)(typescript@5.9.2)(webpack@5.101.2(esbuild@0.25.9))
resolve-url-loader: 5.0.0
rxjs: 7.8.2
sass: 1.90.0
- sass-loader: 16.0.5(sass@1.90.0)(webpack@5.101.2)
+ sass-loader: 16.0.5(sass@1.90.0)(webpack@5.101.2(esbuild@0.25.9))
semver: 7.7.2
- source-map-loader: 5.0.0(webpack@5.101.2)
+ source-map-loader: 5.0.0(webpack@5.101.2(esbuild@0.25.9))
source-map-support: 0.5.21
terser: 5.43.1
tree-kill: 1.2.2
tslib: 2.8.1
typescript: 5.9.2
webpack: 5.101.2(esbuild@0.25.9)
- webpack-dev-middleware: 7.4.2(webpack@5.101.2)
- webpack-dev-server: 5.2.2(webpack@5.101.2)
+ webpack-dev-middleware: 7.4.2(webpack@5.101.2(esbuild@0.25.9))
+ webpack-dev-server: 5.2.2(webpack@5.101.2(esbuild@0.25.9))
webpack-merge: 6.0.1
- webpack-subresource-integrity: 5.1.0(webpack@5.101.2)
+ webpack-subresource-integrity: 5.1.0(webpack@5.101.2(esbuild@0.25.9))
optionalDependencies:
'@angular/core': 20.3.4(@angular/compiler@20.3.4)(rxjs@7.8.2)(zone.js@0.15.1)
'@angular/platform-browser': 20.3.4(@angular/animations@20.3.4(@angular/core@20.3.4(@angular/compiler@20.3.4)(rxjs@7.8.2)(zone.js@0.15.1)))(@angular/common@20.3.4(@angular/core@20.3.4(@angular/compiler@20.3.4)(rxjs@7.8.2)(zone.js@0.15.1))(rxjs@7.8.2))(@angular/core@20.3.4(@angular/compiler@20.3.4)(rxjs@7.8.2)(zone.js@0.15.1))
@@ -9426,12 +9839,12 @@ snapshots:
- webpack-cli
- yaml
- '@angular-devkit/build-webpack@0.2003.4(chokidar@4.0.3)(webpack-dev-server@5.2.2(webpack@5.101.2))(webpack@5.101.2)':
+ '@angular-devkit/build-webpack@0.2003.4(chokidar@4.0.3)(webpack-dev-server@5.2.2(webpack@5.101.2(esbuild@0.25.9)))(webpack@5.101.2(esbuild@0.25.9))':
dependencies:
'@angular-devkit/architect': 0.2003.4(chokidar@4.0.3)
rxjs: 7.8.2
webpack: 5.101.2(esbuild@0.25.9)
- webpack-dev-server: 5.2.2(webpack@5.101.2)
+ webpack-dev-server: 5.2.2(webpack@5.101.2(esbuild@0.25.9))
transitivePeerDependencies:
- chokidar
@@ -12197,7 +12610,7 @@ snapshots:
'@napi-rs/nice-win32-x64-msvc': 1.1.1
optional: true
- '@ngtools/webpack@20.3.4(@angular/compiler-cli@20.3.4(@angular/compiler@20.3.4)(typescript@5.9.2))(typescript@5.9.2)(webpack@5.101.2)':
+ '@ngtools/webpack@20.3.4(@angular/compiler-cli@20.3.4(@angular/compiler@20.3.4)(typescript@5.9.2))(typescript@5.9.2)(webpack@5.101.2(esbuild@0.25.9))':
dependencies:
'@angular/compiler-cli': 20.3.4(@angular/compiler@20.3.4)(typescript@5.9.2)
typescript: 5.9.2
@@ -13528,6 +13941,10 @@ snapshots:
dependencies:
vite: 7.1.5(@types/node@22.18.8)(jiti@1.21.7)(less@4.4.0)(sass@1.90.0)(terser@5.44.0)(tsx@4.20.3)
+ '@vitejs/plugin-basic-ssl@2.1.0(vite@7.1.5(@types/node@22.18.8)(jiti@1.21.7)(less@4.4.2)(sass@1.90.0)(terser@5.44.0)(tsx@4.20.3))':
+ dependencies:
+ vite: 7.1.5(@types/node@22.18.8)(jiti@1.21.7)(less@4.4.2)(sass@1.90.0)(terser@5.44.0)(tsx@4.20.3)
+
'@webassemblyjs/ast@1.14.1':
dependencies:
'@webassemblyjs/helper-numbers': 1.13.2
@@ -13899,7 +14316,7 @@ snapshots:
transitivePeerDependencies:
- supports-color
- babel-loader@10.0.0(@babel/core@7.28.3)(webpack@5.101.2):
+ babel-loader@10.0.0(@babel/core@7.28.3)(webpack@5.101.2(esbuild@0.25.9)):
dependencies:
'@babel/core': 7.28.3
find-up: 5.0.0
@@ -14486,7 +14903,7 @@ snapshots:
dependencies:
is-what: 3.14.1
- copy-webpack-plugin@13.0.1(webpack@5.101.2):
+ copy-webpack-plugin@13.0.1(webpack@5.101.2(esbuild@0.25.9)):
dependencies:
glob-parent: 6.0.2
normalize-path: 3.0.0
@@ -14597,7 +15014,7 @@ snapshots:
dependencies:
postcss: 8.5.6
- css-loader@7.1.2(webpack@5.101.2):
+ css-loader@7.1.2(webpack@5.101.2(esbuild@0.25.9)):
dependencies:
icss-utils: 5.1.0(postcss@8.5.6)
postcss: 8.5.6
@@ -17148,7 +17565,7 @@ snapshots:
dependencies:
readable-stream: 2.3.8
- less-loader@12.3.0(less@4.4.0)(webpack@5.101.2):
+ less-loader@12.3.0(less@4.4.0)(webpack@5.101.2(esbuild@0.25.9)):
dependencies:
less: 4.4.0
optionalDependencies:
@@ -17189,7 +17606,7 @@ snapshots:
prelude-ls: 1.2.1
type-check: 0.4.0
- license-webpack-plugin@4.0.2(webpack@5.101.2):
+ license-webpack-plugin@4.0.2(webpack@5.101.2(esbuild@0.25.9)):
dependencies:
webpack-sources: 3.3.3
optionalDependencies:
@@ -17445,7 +17862,7 @@ snapshots:
mimic-function@5.0.1: {}
- mini-css-extract-plugin@2.9.4(webpack@5.101.2):
+ mini-css-extract-plugin@2.9.4(webpack@5.101.2(esbuild@0.25.9)):
dependencies:
schema-utils: 4.3.3
tapable: 2.3.0
@@ -18153,7 +18570,7 @@ snapshots:
postcss: 8.5.6
ts-node: 10.9.2(@types/node@22.18.8)(typescript@5.7.3)
- postcss-loader@8.1.1(postcss@8.5.6)(typescript@5.9.2)(webpack@5.101.2):
+ postcss-loader@8.1.1(postcss@8.5.6)(typescript@5.9.2)(webpack@5.101.2(esbuild@0.25.9)):
dependencies:
cosmiconfig: 9.0.0(typescript@5.9.2)
jiti: 1.21.7
@@ -18730,7 +19147,7 @@ snapshots:
safer-buffer@2.1.2: {}
- sass-loader@16.0.5(sass@1.90.0)(webpack@5.101.2):
+ sass-loader@16.0.5(sass@1.90.0)(webpack@5.101.2(esbuild@0.25.9)):
dependencies:
neo-async: 2.6.2
optionalDependencies:
@@ -19038,7 +19455,7 @@ snapshots:
source-map-js@1.2.1: {}
- source-map-loader@5.0.0(webpack@5.101.2):
+ source-map-loader@5.0.0(webpack@5.101.2(esbuild@0.25.9)):
dependencies:
iconv-lite: 0.6.3
source-map-js: 1.2.1
@@ -19812,6 +20229,23 @@ snapshots:
terser: 5.44.0
tsx: 4.20.3
+ vite@7.1.5(@types/node@22.18.8)(jiti@1.21.7)(less@4.4.2)(sass@1.90.0)(terser@5.44.0)(tsx@4.20.3):
+ dependencies:
+ esbuild: 0.25.10
+ fdir: 6.5.0(picomatch@4.0.3)
+ picomatch: 4.0.3
+ postcss: 8.5.6
+ rollup: 4.52.3
+ tinyglobby: 0.2.15
+ optionalDependencies:
+ '@types/node': 22.18.8
+ fsevents: 2.3.3
+ jiti: 1.21.7
+ less: 4.4.2
+ sass: 1.90.0
+ terser: 5.44.0
+ tsx: 4.20.3
+
void-elements@2.0.1: {}
w3c-xmlserializer@4.0.0:
@@ -19848,7 +20282,7 @@ snapshots:
webidl-conversions@7.0.0: {}
- webpack-dev-middleware@7.4.2(webpack@5.101.2):
+ webpack-dev-middleware@7.4.2(webpack@5.101.2(esbuild@0.25.9)):
dependencies:
colorette: 2.0.20
memfs: 4.49.0
@@ -19859,7 +20293,7 @@ snapshots:
optionalDependencies:
webpack: 5.101.2(esbuild@0.25.9)
- webpack-dev-server@5.2.2(webpack@5.101.2):
+ webpack-dev-server@5.2.2(webpack@5.101.2(esbuild@0.25.9)):
dependencies:
'@types/bonjour': 3.5.13
'@types/connect-history-api-fallback': 1.5.4
@@ -19887,7 +20321,7 @@ snapshots:
serve-index: 1.9.1
sockjs: 0.3.24
spdy: 4.0.2
- webpack-dev-middleware: 7.4.2(webpack@5.101.2)
+ webpack-dev-middleware: 7.4.2(webpack@5.101.2(esbuild@0.25.9))
ws: 8.18.3
optionalDependencies:
webpack: 5.101.2(esbuild@0.25.9)
@@ -19905,7 +20339,7 @@ snapshots:
webpack-sources@3.3.3: {}
- webpack-subresource-integrity@5.1.0(webpack@5.101.2):
+ webpack-subresource-integrity@5.1.0(webpack@5.101.2(esbuild@0.25.9)):
dependencies:
typed-assert: 1.0.9
webpack: 5.101.2(esbuild@0.25.9)
diff --git a/testapp/public/js/webcomponent.js b/testapp/public/js/webcomponent.js
new file mode 100644
index 00000000..ae059311
--- /dev/null
+++ b/testapp/public/js/webcomponent.js
@@ -0,0 +1,219 @@
+"use strict";
+const socket = window.io();
+let meet;
+let roomId;
+let showAllWebhooksCheckbox;
+/**
+ * Add a component event to the events log
+ */
+const addEventToLog = (eventType, eventMessage) => {
+ const eventsList = document.getElementById('events-list');
+ if (eventsList) {
+ const li = document.createElement('li');
+ li.className = `event-${eventType}`;
+ li.textContent = `[ ${eventType} ] : ${eventMessage}`;
+ eventsList.insertBefore(li, eventsList.firstChild);
+ }
+};
+const escapeHtml = (unsafe) => {
+ return unsafe
+ .replace(/&/g, '&')
+ .replace(//g, '>')
+ .replace(/"/g, '"')
+ .replace(/'/g, ''');
+};
+const getWebhookEventsFromStorage = (roomId) => {
+ const data = localStorage.getItem('webhookEventsByRoom');
+ if (!data) {
+ return [];
+ }
+ const map = JSON.parse(data);
+ return map[roomId] || [];
+};
+const saveWebhookEventToStorage = (roomId, event) => {
+ const data = localStorage.getItem('webhookEventsByRoom');
+ const map = data ? JSON.parse(data) : {};
+ if (!map[roomId]) {
+ map[roomId] = [];
+ }
+ map[roomId].push(event);
+ localStorage.setItem('webhookEventsByRoom', JSON.stringify(map));
+};
+const clearWebhookEventsByRoom = (roomId) => {
+ const data = localStorage.getItem('webhookEventsByRoom');
+ if (!data)
+ return;
+ const map = JSON.parse(data);
+ if (map[roomId]) {
+ map[roomId] = [];
+ localStorage.setItem('webhookEventsByRoom', JSON.stringify(map));
+ }
+};
+const shouldShowWebhook = (event) => {
+ return (showAllWebhooksCheckbox === null || showAllWebhooksCheckbox === void 0 ? void 0 : showAllWebhooksCheckbox.checked) || event.data.roomId === roomId;
+};
+const listenWebhookServerEvents = () => {
+ socket.on('webhookEvent', (event) => {
+ console.log('Webhook received:', event);
+ const webhookRoomId = event.data.roomId;
+ if (webhookRoomId) {
+ saveWebhookEventToStorage(webhookRoomId, event);
+ }
+ if (!shouldShowWebhook(event)) {
+ console.log('Ignoring webhook event:', event);
+ return;
+ }
+ addWebhookEventElement(event);
+ // Clean up the previous events
+ const isMeetingEnded = event.event === 'meetingEnded';
+ if (isMeetingEnded)
+ clearWebhookEventsByRoom(webhookRoomId);
+ });
+};
+const renderStoredWebhookEvents = (roomId) => {
+ const webhookLogList = document.getElementById('webhook-log-list');
+ if (webhookLogList) {
+ while (webhookLogList.firstChild) {
+ webhookLogList.removeChild(webhookLogList.firstChild);
+ }
+ }
+ const events = getWebhookEventsFromStorage(roomId);
+ events.forEach((event) => addWebhookEventElement(event));
+};
+const addWebhookEventElement = (event) => {
+ const webhookLogList = document.getElementById('webhook-log-list');
+ if (webhookLogList) {
+ // Create unique IDs for this accordion item
+ const itemId = event.creationDate;
+ const headerClassName = `webhook-${event.event}`;
+ const collapseId = `collapse-${itemId}`;
+ // Create accordion item container
+ const accordionItem = document.createElement('div');
+ accordionItem.className = 'accordion-item';
+ // Create header
+ const header = document.createElement('h2');
+ header.classList.add(headerClassName, 'accordion-header');
+ // Create header button
+ const button = document.createElement('button');
+ button.className = 'accordion-button';
+ button.type = 'button';
+ button.setAttribute('data-bs-toggle', 'collapse');
+ button.setAttribute('data-bs-target', `#${collapseId}`);
+ button.setAttribute('aria-expanded', 'true');
+ button.setAttribute('aria-controls', collapseId);
+ button.style.padding = '10px';
+ if (event.event === 'meetingStarted') {
+ button.classList.add('bg-success');
+ }
+ if (event.event === 'meetingEnded') {
+ button.classList.add('bg-danger');
+ }
+ if (event.event.includes('recording')) {
+ button.classList.add('bg-warning');
+ }
+ // Format the header text with event name and timestamp
+ const date = new Date(event.creationDate);
+ const formattedDate = date.toLocaleString('es-ES', {
+ // year: 'numeric',
+ // month: '2-digit',
+ // day: '2-digit',
+ hour: '2-digit',
+ minute: '2-digit',
+ second: '2-digit',
+ hour12: false
+ });
+ button.innerHTML = `[${formattedDate}] ${event.event}`;
+ // Create collapsible content container
+ const collapseDiv = document.createElement('div');
+ collapseDiv.id = collapseId;
+ collapseDiv.className = 'accordion-collapse collapse';
+ collapseDiv.setAttribute('aria-labelledby', headerClassName);
+ collapseDiv.setAttribute('data-bs-parent', '#webhook-log-list');
+ // Create body content
+ const bodyDiv = document.createElement('div');
+ bodyDiv.className = 'accordion-body';
+ // Format JSON with syntax highlighting if possible
+ const formattedJson = JSON.stringify(event, null, 2);
+ bodyDiv.innerHTML = `${escapeHtml(formattedJson)}
`;
+ // Assemble the components
+ header.appendChild(button);
+ collapseDiv.appendChild(bodyDiv);
+ accordionItem.appendChild(header);
+ accordionItem.appendChild(collapseDiv);
+ // Insert at the top of the list (latest events first)
+ if (webhookLogList.firstChild) {
+ webhookLogList.insertBefore(accordionItem, webhookLogList.firstChild);
+ }
+ else {
+ webhookLogList.appendChild(accordionItem);
+ }
+ // Limit the number of items to prevent performance issues
+ const maxItems = 50;
+ while (webhookLogList.children.length > maxItems) {
+ webhookLogList.removeChild(webhookLogList.lastChild);
+ }
+ }
+};
+// Listen to events from openvidu-meet
+const listenWebComponentEvents = () => {
+ const meet = document.querySelector('openvidu-meet');
+ if (!meet) {
+ console.error('openvidu-meet component not found');
+ alert('openvidu-meet component not found in the DOM');
+ return;
+ }
+ meet.on('joined', (event) => {
+ console.log('"joined" event received:', event);
+ addEventToLog('joined', JSON.stringify(event));
+ });
+ meet.on('left', (event) => {
+ console.log('"left" event received:', event);
+ addEventToLog('left', JSON.stringify(event));
+ });
+ meet.on('closed', (event) => {
+ console.log('"closed" event received:', event);
+ addEventToLog('closed', JSON.stringify(event));
+ // Redirect to home page
+ // window.location.href = '/';
+ });
+};
+// Set up commands for the web component
+const setUpWebComponentCommands = () => {
+ var _a, _b, _c;
+ if (!meet) {
+ console.error('openvidu-meet component not found');
+ alert('openvidu-meet component not found in the DOM');
+ return;
+ }
+ // End meeting button click handler
+ (_a = document.getElementById('end-meeting-btn')) === null || _a === void 0 ? void 0 : _a.addEventListener('click', () => meet.endMeeting());
+ // Leave room button click handler
+ (_b = document.getElementById('leave-room-btn')) === null || _b === void 0 ? void 0 : _b.addEventListener('click', () => meet.leaveRoom());
+ // Kick participant button click handler
+ (_c = document.getElementById('kick-participant-btn')) === null || _c === void 0 ? void 0 : _c.addEventListener('click', () => {
+ const participantIdentity = document.getElementById('participant-identity-input').value.trim();
+ if (participantIdentity) {
+ meet.kickParticipant(participantIdentity);
+ }
+ });
+};
+document.addEventListener('DOMContentLoaded', () => {
+ var _a, _b;
+ roomId = (_b = (_a = document.getElementById('room-id')) === null || _a === void 0 ? void 0 : _a.textContent) === null || _b === void 0 ? void 0 : _b.trim();
+ showAllWebhooksCheckbox = document.getElementById('show-all-webhooks');
+ meet = document.querySelector('openvidu-meet');
+ if (!roomId) {
+ console.error('Room ID not found in the DOM');
+ alert('Room ID not found in the DOM');
+ return;
+ }
+ renderStoredWebhookEvents(roomId);
+ listenWebhookServerEvents();
+ listenWebComponentEvents();
+ setUpWebComponentCommands();
+ showAllWebhooksCheckbox === null || showAllWebhooksCheckbox === void 0 ? void 0 : showAllWebhooksCheckbox.addEventListener('change', () => {
+ if (roomId)
+ renderStoredWebhookEvents(roomId);
+ });
+});