update file exclusion patterns in workspace settings
webcomponent: Added missing and necessary js file Update .gitignore to specify backend public directory exclusion webcomponent: Add error handling for invalid base URL in OpenViduMeet component webcomponent: Update Jest configuration for improved testing setup webcomponent: Enhance iframe attribute tests and add support for optional query parameters webcomponent: Refactor documentation copying in build_webcomponent_doc function for improved readability and add absolute path resolution Add E2EE_KEY property to WebComponentProperty enum for end-to-end encryption support meet.sh: Enhance build_rest_api_doc function with output file handling and user confirmation for overwriting frontend: replace removeRoomSecretGuard with removeQueryParamsGuard for enhanced query parameter management frontend: add E2EE key handling in room service and update query params guard Updated pnpm-lock.yaml Enables end-to-end encryption (E2EE) Adds E2EE functionality to meeting rooms. Significant changes: - Allows encryption of the participant name - Introduces setting and getting E2EE keys - Ensures recording is disabled when encryption is enabled webcomponent: Added e2e test for checking the e2ee funcionality frontend: Sanitize participant name before request for a token fix: clean up formatting in openvidu-meet.code-workspace
This commit is contained in:
parent
e6d04aca16
commit
b055ef0333
2
.gitignore
vendored
2
.gitignore
vendored
@ -37,7 +37,7 @@ pnpm-debug.log*
|
|||||||
|
|
||||||
|
|
||||||
**/**/test-results
|
**/**/test-results
|
||||||
**/**/public/
|
**/backend/public/
|
||||||
|
|
||||||
**/*/coverage
|
**/*/coverage
|
||||||
**/**/test-results
|
**/**/test-results
|
||||||
|
|||||||
@ -1,13 +1,7 @@
|
|||||||
import { inject } from '@angular/core';
|
import { inject } from '@angular/core';
|
||||||
import { ActivatedRouteSnapshot, CanActivateFn } from '@angular/router';
|
import { ActivatedRouteSnapshot, CanActivateFn } from '@angular/router';
|
||||||
import { ErrorReason } from '../models';
|
import { ErrorReason } from '../models';
|
||||||
import {
|
import { AppDataService, NavigationService, ParticipantService, RoomService, SessionStorageService } from '../services';
|
||||||
AppDataService,
|
|
||||||
NavigationService,
|
|
||||||
ParticipantService,
|
|
||||||
RoomService,
|
|
||||||
SessionStorageService
|
|
||||||
} from '../services';
|
|
||||||
import { WebComponentProperty } from '@openvidu-meet/typings';
|
import { WebComponentProperty } from '@openvidu-meet/typings';
|
||||||
|
|
||||||
export const extractRoomQueryParamsGuard: CanActivateFn = (route: ActivatedRouteSnapshot) => {
|
export const extractRoomQueryParamsGuard: CanActivateFn = (route: ActivatedRouteSnapshot) => {
|
||||||
@ -16,7 +10,14 @@ export const extractRoomQueryParamsGuard: CanActivateFn = (route: ActivatedRoute
|
|||||||
const participantService = inject(ParticipantService);
|
const participantService = inject(ParticipantService);
|
||||||
const sessionStorageService = inject(SessionStorageService);
|
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();
|
const secret = querySecret || sessionStorageService.getRoomSecret();
|
||||||
|
|
||||||
// Handle leave redirect URL logic
|
// Handle leave redirect URL logic
|
||||||
@ -29,6 +30,7 @@ export const extractRoomQueryParamsGuard: CanActivateFn = (route: ActivatedRoute
|
|||||||
|
|
||||||
roomService.setRoomId(roomId);
|
roomService.setRoomId(roomId);
|
||||||
roomService.setRoomSecret(secret);
|
roomService.setRoomSecret(secret);
|
||||||
|
roomService.setE2EEKey(e2eeKey);
|
||||||
|
|
||||||
if (participantName) {
|
if (participantName) {
|
||||||
participantService.setParticipantName(participantName);
|
participantService.setParticipantName(participantName);
|
||||||
@ -66,7 +68,8 @@ const extractParams = ({ params, queryParams }: ActivatedRouteSnapshot) => ({
|
|||||||
secret: queryParams['secret'] as string,
|
secret: queryParams['secret'] as string,
|
||||||
participantName: queryParams[WebComponentProperty.PARTICIPANT_NAME] as string,
|
participantName: queryParams[WebComponentProperty.PARTICIPANT_NAME] as string,
|
||||||
leaveRedirectUrl: queryParams[WebComponentProperty.LEAVE_REDIRECT_URL] 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
|
||||||
});
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
export * from './auth.guard';
|
export * from './auth.guard';
|
||||||
export * from './extract-query-params.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 './run-serially.guard';
|
||||||
export * from './validate-access.guard';
|
export * from './validate-access.guard';
|
||||||
|
|||||||
@ -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;
|
||||||
|
};
|
||||||
|
};
|
||||||
@ -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;
|
|
||||||
};
|
|
||||||
@ -52,8 +52,7 @@
|
|||||||
message but will be unable to see or hear others.
|
message but will be unable to see or hear others.
|
||||||
</span>
|
</span>
|
||||||
</li>
|
</li>
|
||||||
<li>Recording is unavailable while encryption is enabled.</li>
|
<li>Recording is <b>unavailable</b> while encryption is enabled.</li>
|
||||||
<li>Chat messages are not protected by end-to-end encryption.</li>
|
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -6,7 +6,7 @@ import {
|
|||||||
checkUserNotAuthenticatedGuard,
|
checkUserNotAuthenticatedGuard,
|
||||||
extractRecordingQueryParamsGuard,
|
extractRecordingQueryParamsGuard,
|
||||||
extractRoomQueryParamsGuard,
|
extractRoomQueryParamsGuard,
|
||||||
removeRoomSecretGuard,
|
removeQueryParamsGuard,
|
||||||
runGuardsSerially,
|
runGuardsSerially,
|
||||||
validateRecordingAccessGuard,
|
validateRecordingAccessGuard,
|
||||||
validateRoomAccessGuard
|
validateRoomAccessGuard
|
||||||
@ -27,6 +27,7 @@ import {
|
|||||||
ViewRecordingComponent,
|
ViewRecordingComponent,
|
||||||
ConfigComponent
|
ConfigComponent
|
||||||
} from '../pages';
|
} from '../pages';
|
||||||
|
import { WebComponentProperty } from '@openvidu-meet/typings';
|
||||||
|
|
||||||
export const baseRoutes: Routes = [
|
export const baseRoutes: Routes = [
|
||||||
{
|
{
|
||||||
@ -40,9 +41,9 @@ export const baseRoutes: Routes = [
|
|||||||
canActivate: [
|
canActivate: [
|
||||||
runGuardsSerially(
|
runGuardsSerially(
|
||||||
extractRoomQueryParamsGuard,
|
extractRoomQueryParamsGuard,
|
||||||
|
removeQueryParamsGuard(['secret', WebComponentProperty.E2EE_KEY]),
|
||||||
checkParticipantRoleAndAuthGuard,
|
checkParticipantRoleAndAuthGuard,
|
||||||
validateRoomAccessGuard,
|
validateRoomAccessGuard
|
||||||
removeRoomSecretGuard
|
|
||||||
)
|
)
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
@ -52,9 +53,9 @@ export const baseRoutes: Routes = [
|
|||||||
canActivate: [
|
canActivate: [
|
||||||
runGuardsSerially(
|
runGuardsSerially(
|
||||||
extractRecordingQueryParamsGuard,
|
extractRecordingQueryParamsGuard,
|
||||||
|
removeQueryParamsGuard(['secret', WebComponentProperty.E2EE_KEY]),
|
||||||
checkParticipantRoleAndAuthGuard,
|
checkParticipantRoleAndAuthGuard,
|
||||||
validateRecordingAccessGuard,
|
validateRecordingAccessGuard
|
||||||
removeRoomSecretGuard
|
|
||||||
)
|
)
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
|||||||
@ -92,38 +92,48 @@ export class MeetingEventHandlerService {
|
|||||||
): void {
|
): void {
|
||||||
room.on(
|
room.on(
|
||||||
RoomEvent.DataReceived,
|
RoomEvent.DataReceived,
|
||||||
async (
|
async (payload: Uint8Array, _participant?: RemoteParticipant, _kind?: DataPacket_Kind, topic?: string) => {
|
||||||
payload: Uint8Array,
|
// Only process topics that this handler is responsible for
|
||||||
_participant?: RemoteParticipant,
|
const relevantTopics = [
|
||||||
_kind?: DataPacket_Kind,
|
'recordingStopped',
|
||||||
topic?: string
|
MeetSignalType.MEET_ROOM_CONFIG_UPDATED,
|
||||||
) => {
|
MeetSignalType.MEET_PARTICIPANT_ROLE_UPDATED
|
||||||
const event = JSON.parse(new TextDecoder().decode(payload));
|
];
|
||||||
|
|
||||||
switch (topic) {
|
if (!topic || !relevantTopics.includes(topic)) {
|
||||||
case 'recordingStopped':
|
return;
|
||||||
await this.handleRecordingStopped(
|
}
|
||||||
context.roomId,
|
|
||||||
context.roomSecret,
|
|
||||||
context.onHasRecordingsChanged
|
|
||||||
);
|
|
||||||
break;
|
|
||||||
|
|
||||||
case MeetSignalType.MEET_ROOM_CONFIG_UPDATED:
|
try {
|
||||||
await this.handleRoomConfigUpdated(event, context.roomId, context.roomSecret);
|
const event = JSON.parse(new TextDecoder().decode(payload));
|
||||||
break;
|
|
||||||
|
|
||||||
case MeetSignalType.MEET_PARTICIPANT_ROLE_UPDATED:
|
switch (topic) {
|
||||||
await this.handleParticipantRoleUpdated(
|
case 'recordingStopped':
|
||||||
event,
|
await this.handleRecordingStopped(
|
||||||
context.roomId,
|
context.roomId,
|
||||||
context.participantName,
|
context.roomSecret,
|
||||||
context.localParticipant,
|
context.onHasRecordingsChanged
|
||||||
context.remoteParticipants,
|
);
|
||||||
context.onRoomSecretChanged,
|
break;
|
||||||
context.onParticipantRoleUpdated
|
|
||||||
);
|
case MeetSignalType.MEET_ROOM_CONFIG_UPDATED:
|
||||||
break;
|
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) {
|
if (error.status === 503) {
|
||||||
console.error(
|
console.error(
|
||||||
'No egress service available. Check CPU usage or Media Node capacity. ' +
|
'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 {
|
} else {
|
||||||
console.error('Error starting recording:', error);
|
console.error('Error starting recording:', error);
|
||||||
|
|||||||
@ -72,6 +72,10 @@ export class MeetingLobbyService {
|
|||||||
return value.name.trim();
|
return value.name.trim();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
set e2eeKey(key: string) {
|
||||||
|
this.state.participantForm.get('e2eeKey')?.setValue(key);
|
||||||
|
}
|
||||||
|
|
||||||
get e2eeKey(): string {
|
get e2eeKey(): string {
|
||||||
const { valid, value } = this.state.participantForm;
|
const { valid, value } = this.state.participantForm;
|
||||||
if (!valid || !value.e2eeKey?.trim()) {
|
if (!valid || !value.e2eeKey?.trim()) {
|
||||||
@ -93,6 +97,12 @@ export class MeetingLobbyService {
|
|||||||
// If E2EE is enabled, require e2eeKey
|
// If E2EE is enabled, require e2eeKey
|
||||||
if (this.state.isE2EEEnabled) {
|
if (this.state.isE2EEEnabled) {
|
||||||
this.state.participantForm.get('e2eeKey')?.setValidators([Validators.required]);
|
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();
|
this.state.participantForm.get('e2eeKey')?.updateValueAndValidity();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -145,10 +155,16 @@ export class MeetingLobbyService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async submitAccess(): Promise<void> {
|
async submitAccess(): Promise<void> {
|
||||||
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.');
|
console.error('Participant form is invalid. Cannot access meeting.');
|
||||||
throw new Error('Participant form is invalid');
|
throw new Error('Participant form is invalid');
|
||||||
}
|
}
|
||||||
|
this.participantName = sanitized;
|
||||||
|
|
||||||
// For E2EE rooms, validate passkey
|
// For E2EE rooms, validate passkey
|
||||||
if (this.state.isE2EEEnabled && !this.e2eeKey) {
|
if (this.state.isE2EEEnabled && !this.e2eeKey) {
|
||||||
@ -243,11 +259,14 @@ export class MeetingLobbyService {
|
|||||||
*/
|
*/
|
||||||
protected async generateParticipantToken() {
|
protected async generateParticipantToken() {
|
||||||
try {
|
try {
|
||||||
this.state.participantToken = await this.participantService.generateToken({
|
this.state.participantToken = await this.participantService.generateToken(
|
||||||
roomId: this.state.roomId,
|
{
|
||||||
secret: this.state.roomSecret,
|
roomId: this.state.roomId,
|
||||||
participantName: this.participantName
|
secret: this.state.roomSecret,
|
||||||
});
|
participantName: this.participantName
|
||||||
|
},
|
||||||
|
this.e2eeKey
|
||||||
|
);
|
||||||
this.participantName = this.participantService.getParticipantName()!;
|
this.participantName = this.participantService.getParticipantName()!;
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
console.error('Error generating participant token:', error);
|
console.error('Error generating participant token:', error);
|
||||||
|
|||||||
@ -177,8 +177,25 @@ export class NavigationService {
|
|||||||
* @param param - The parameter to remove
|
* @param param - The parameter to remove
|
||||||
*/
|
*/
|
||||||
async removeQueryParamFromUrl(queryParams: Params, param: string): Promise<void> {
|
async removeQueryParamFromUrl(queryParams: Params, param: string): Promise<void> {
|
||||||
|
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<void> {
|
||||||
|
if (!params || params.length === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const updatedParams = { ...queryParams };
|
const updatedParams = { ...queryParams };
|
||||||
delete updatedParams[param];
|
params.forEach((param) => {
|
||||||
|
delete updatedParams[param];
|
||||||
|
});
|
||||||
|
|
||||||
await this.router.navigate([], {
|
await this.router.navigate([], {
|
||||||
queryParams: updatedParams,
|
queryParams: updatedParams,
|
||||||
|
|||||||
@ -8,7 +8,7 @@ import {
|
|||||||
ParticipantRole
|
ParticipantRole
|
||||||
} from '@openvidu-meet/typings';
|
} from '@openvidu-meet/typings';
|
||||||
import { getValidDecodedToken } from '../utils';
|
import { getValidDecodedToken } from '../utils';
|
||||||
import { LoggerService } from 'openvidu-components-angular';
|
import { E2eeService, LoggerService } from 'openvidu-components-angular';
|
||||||
|
|
||||||
@Injectable({
|
@Injectable({
|
||||||
providedIn: 'root'
|
providedIn: 'root'
|
||||||
@ -29,7 +29,8 @@ export class ParticipantService {
|
|||||||
protected httpService: HttpService,
|
protected httpService: HttpService,
|
||||||
protected featureConfService: FeatureConfigurationService,
|
protected featureConfService: FeatureConfigurationService,
|
||||||
protected globalConfigService: GlobalConfigService,
|
protected globalConfigService: GlobalConfigService,
|
||||||
protected tokenStorageService: TokenStorageService
|
protected tokenStorageService: TokenStorageService,
|
||||||
|
protected e2eeService: E2eeService
|
||||||
) {
|
) {
|
||||||
this.log = this.loggerService.get('OpenVidu Meet - ParticipantTokenService');
|
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
|
* @param participantOptions - The options for the participant, including room ID, participant name, and secret
|
||||||
* @return A promise that resolves to the participant token
|
* @return A promise that resolves to the participant token
|
||||||
*/
|
*/
|
||||||
async generateToken(participantOptions: ParticipantOptions): Promise<string> {
|
async generateToken(participantOptions: ParticipantOptions, e2EEKey = ''): Promise<string> {
|
||||||
const path = `${this.PARTICIPANTS_API}/token`;
|
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);
|
const { token } = await this.httpService.postRequest<{ token: string }>(path, participantOptions);
|
||||||
|
|
||||||
// Store token in sessionStorage for header mode
|
// Store token in sessionStorage for header mode
|
||||||
@ -63,7 +71,7 @@ export class ParticipantService {
|
|||||||
this.tokenStorageService.setParticipantToken(token);
|
this.tokenStorageService.setParticipantToken(token);
|
||||||
}
|
}
|
||||||
|
|
||||||
this.updateParticipantTokenInfo(token);
|
await this.updateParticipantTokenInfo(token);
|
||||||
return token;
|
return token;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -83,7 +91,7 @@ export class ParticipantService {
|
|||||||
this.tokenStorageService.setParticipantToken(token);
|
this.tokenStorageService.setParticipantToken(token);
|
||||||
}
|
}
|
||||||
|
|
||||||
this.updateParticipantTokenInfo(token);
|
await this.updateParticipantTokenInfo(token);
|
||||||
return token;
|
return token;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -93,13 +101,14 @@ export class ParticipantService {
|
|||||||
* @param token - The JWT token to set.
|
* @param token - The JWT token to set.
|
||||||
* @throws Error if the token is invalid or expired.
|
* @throws Error if the token is invalid or expired.
|
||||||
*/
|
*/
|
||||||
protected updateParticipantTokenInfo(token: string): void {
|
protected async updateParticipantTokenInfo(token: string): Promise<void> {
|
||||||
try {
|
try {
|
||||||
const decodedToken = getValidDecodedToken(token);
|
const decodedToken = getValidDecodedToken(token);
|
||||||
const metadata = decodedToken.metadata as MeetTokenMetadata;
|
const metadata = decodedToken.metadata as MeetTokenMetadata;
|
||||||
|
|
||||||
if (decodedToken.sub && decodedToken.name) {
|
if (decodedToken.sub && decodedToken.name) {
|
||||||
this.setParticipantName(decodedToken.name);
|
const decryptedName = await this.e2eeService.decryptOrMask(decodedToken.name);
|
||||||
|
this.setParticipantName(decryptedName);
|
||||||
this.participantIdentity = decodedToken.sub;
|
this.participantIdentity = decodedToken.sub;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -22,6 +22,7 @@ export class RoomService {
|
|||||||
|
|
||||||
protected roomId: string = '';
|
protected roomId: string = '';
|
||||||
protected roomSecret: string = '';
|
protected roomSecret: string = '';
|
||||||
|
protected e2eeKey: string = '';
|
||||||
|
|
||||||
protected log;
|
protected log;
|
||||||
|
|
||||||
@ -50,6 +51,14 @@ export class RoomService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
setE2EEKey(e2eeKey: string) {
|
||||||
|
this.e2eeKey = e2eeKey;
|
||||||
|
}
|
||||||
|
|
||||||
|
getE2EEKey(): string {
|
||||||
|
return this.e2eeKey;
|
||||||
|
}
|
||||||
|
|
||||||
getRoomSecret(): string {
|
getRoomSecret(): string {
|
||||||
return this.roomSecret;
|
return this.roomSecret;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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.rootPath": ".",
|
||||||
"jest.nodeEnv": {
|
|
||||||
"NODE_OPTIONS": "--experimental-vm-modules"
|
|
||||||
},
|
|
||||||
"jest.runMode": "on-demand"
|
"jest.runMode": "on-demand"
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,25 +1,21 @@
|
|||||||
import { createDefaultEsmPreset } from 'ts-jest'
|
/** @type {import('jest').Config} */
|
||||||
|
const config = {
|
||||||
/** @type {import('ts-jest').JestConfigWithTsJest} */
|
|
||||||
const jestConfig = {
|
|
||||||
displayName: 'webcomponent',
|
displayName: 'webcomponent',
|
||||||
...createDefaultEsmPreset({
|
preset: 'ts-jest/presets/default-esm',
|
||||||
tsconfig: 'tsconfig.json'
|
extensionsToTreatAsEsm: ['.ts'],
|
||||||
}),
|
globals: {
|
||||||
// Set the root directory to the webcomponent folder
|
'ts-jest': {
|
||||||
rootDir: './',
|
useESM: true,
|
||||||
|
tsconfig: 'tsconfig.json'
|
||||||
|
}
|
||||||
|
},
|
||||||
resolver: 'ts-jest-resolver',
|
resolver: 'ts-jest-resolver',
|
||||||
testEnvironment: 'jsdom',
|
testEnvironment: 'jsdom',
|
||||||
testMatch: ['**/?(*.)+(spec|test).[tj]s?(x)'],
|
testMatch: ['**/?(*.)+(spec|test).[tj]s?(x)'],
|
||||||
moduleFileExtensions: ['js', 'ts', 'json', 'node'],
|
moduleFileExtensions: ['js', 'ts', 'json', 'node'],
|
||||||
testPathIgnorePatterns: ['/node_modules/', '/dist/', '/tests/e2e/'],
|
testPathIgnorePatterns: ['/node_modules/', '/dist/', '/tests/e2e/'],
|
||||||
transform: {
|
transform: {
|
||||||
'^.+\\.tsx?$': ['ts-jest', { tsconfig: 'tsconfig.json' }]
|
'^.+\\.tsx?$': ['ts-jest', { useESM: true }]
|
||||||
},
|
|
||||||
globals: {
|
|
||||||
'ts-jest': {
|
|
||||||
tsconfig: 'tsconfig.json'
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
moduleNameMapper: {
|
moduleNameMapper: {
|
||||||
'^@openvidu-meet/typings$': '<rootDir>/../../typings/src/index.ts',
|
'^@openvidu-meet/typings$': '<rootDir>/../../typings/src/index.ts',
|
||||||
@ -27,4 +23,4 @@ const jestConfig = {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default jestConfig
|
export default config
|
||||||
|
|||||||
@ -143,19 +143,24 @@ export class OpenViduMeet extends HTMLElement {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const url = new URL(baseUrl);
|
try {
|
||||||
this.targetIframeOrigin = url.origin;
|
const url = new URL(baseUrl);
|
||||||
this.commandsManager.setTargetOrigin(this.targetIframeOrigin);
|
this.targetIframeOrigin = url.origin;
|
||||||
this.eventsManager.setTargetOrigin(this.targetIframeOrigin);
|
this.commandsManager.setTargetOrigin(this.targetIframeOrigin);
|
||||||
|
this.eventsManager.setTargetOrigin(this.targetIframeOrigin);
|
||||||
|
|
||||||
// Update query params
|
// Update query params
|
||||||
Array.from(this.attributes).forEach((attr) => {
|
Array.from(this.attributes).forEach((attr) => {
|
||||||
if (attr.name !== WebComponentProperty.ROOM_URL && attr.name !== WebComponentProperty.RECORDING_URL) {
|
if (attr.name !== WebComponentProperty.ROOM_URL && attr.name !== WebComponentProperty.RECORDING_URL) {
|
||||||
url.searchParams.set(attr.name, attr.value);
|
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}`);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@ -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 }) => {
|
test.afterEach(async ({ page }) => {
|
||||||
try {
|
try {
|
||||||
await leaveRoom(page);
|
await leaveRoom(page);
|
||||||
@ -152,7 +152,6 @@ test.describe('E2EE UI Tests', () => {
|
|||||||
const page2 = await context.newPage();
|
const page2 = await context.newPage();
|
||||||
|
|
||||||
// Participant 1 joins with E2EE key
|
// Participant 1 joins with E2EE key
|
||||||
await page.goto(MEET_TESTAPP_URL);
|
|
||||||
await prepareForJoiningRoom(page, MEET_TESTAPP_URL, roomId);
|
await prepareForJoiningRoom(page, MEET_TESTAPP_URL, roomId);
|
||||||
await page.click('#join-as-speaker');
|
await page.click('#join-as-speaker');
|
||||||
|
|
||||||
@ -178,7 +177,6 @@ test.describe('E2EE UI Tests', () => {
|
|||||||
|
|
||||||
// Participant 2 joins with same E2EE key
|
// Participant 2 joins with same E2EE key
|
||||||
const participant2Name = `P2-${Math.random().toString(36).substring(2, 9)}`;
|
const participant2Name = `P2-${Math.random().toString(36).substring(2, 9)}`;
|
||||||
await page2.goto(MEET_TESTAPP_URL);
|
|
||||||
await prepareForJoiningRoom(page2, MEET_TESTAPP_URL, roomId);
|
await prepareForJoiningRoom(page2, MEET_TESTAPP_URL, roomId);
|
||||||
await page2.click('#join-as-speaker');
|
await page2.click('#join-as-speaker');
|
||||||
|
|
||||||
@ -222,6 +220,16 @@ test.describe('E2EE UI Tests', () => {
|
|||||||
});
|
});
|
||||||
await expect(encryptionError2).toBeHidden();
|
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
|
// Cleanup participant 2
|
||||||
await leaveRoom(page2);
|
await leaveRoom(page2);
|
||||||
await page2.close();
|
await page2.close();
|
||||||
@ -342,6 +350,463 @@ test.describe('E2EE UI Tests', () => {
|
|||||||
await Promise.all([leaveRoom(page2), leaveRoom(page3)]);
|
await Promise.all([leaveRoom(page2), leaveRoom(page3)]);
|
||||||
await Promise.all([page2.close(), page3.close()]);
|
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()]);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// ==========================================
|
// ==========================================
|
||||||
|
|||||||
@ -24,12 +24,43 @@ export async function getIframeInShadowDom(
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Waits for an element inside an iframe within Shadow DOM
|
* Waits for one or more elements inside an iframe within a Shadow DOM.
|
||||||
* @param page - Playwright page object
|
*
|
||||||
* @param elementSelector - Selector for the element inside the iframe
|
* By default, waits for the first matching element.
|
||||||
* @param options - Optional configuration
|
* If `options.all` is set to `true`, waits for all matching elements and returns an array.
|
||||||
* @returns Locator for the found element
|
*
|
||||||
|
* @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<Locator>;
|
||||||
export async function waitForElementInIframe(
|
export async function waitForElementInIframe(
|
||||||
page: Page,
|
page: Page,
|
||||||
elementSelector: string,
|
elementSelector: string,
|
||||||
@ -38,24 +69,42 @@ export async function waitForElementInIframe(
|
|||||||
iframeSelector?: string;
|
iframeSelector?: string;
|
||||||
timeout?: number;
|
timeout?: number;
|
||||||
state?: 'attached' | 'detached' | 'visible' | 'hidden';
|
state?: 'attached' | 'detached' | 'visible' | 'hidden';
|
||||||
|
all: true;
|
||||||
|
}
|
||||||
|
): Promise<Locator[]>;
|
||||||
|
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<Locator> {
|
): Promise<Locator | Locator[]> {
|
||||||
const {
|
const {
|
||||||
componentSelector = 'openvidu-meet',
|
componentSelector = 'openvidu-meet',
|
||||||
iframeSelector = 'iframe',
|
iframeSelector = 'iframe',
|
||||||
timeout = 30000,
|
timeout = 30000,
|
||||||
state = 'visible'
|
state = 'visible',
|
||||||
|
index = 0,
|
||||||
|
all = false
|
||||||
} = options;
|
} = options;
|
||||||
|
|
||||||
// Get the iframe
|
|
||||||
const frameLocator = await getIframeInShadowDom(page, componentSelector, iframeSelector);
|
const frameLocator = await getIframeInShadowDom(page, componentSelector, iframeSelector);
|
||||||
|
const baseLocator = frameLocator.locator(elementSelector);
|
||||||
|
|
||||||
// Get element locator
|
if (all) {
|
||||||
const elementLocator = frameLocator.locator(elementSelector);
|
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
|
const target = baseLocator.nth(index);
|
||||||
await elementLocator.waitFor({ state, timeout });
|
await target.waitFor({ state, timeout });
|
||||||
return elementLocator;
|
return target;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function countElementsInIframe(
|
export async function countElementsInIframe(
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
import { afterEach, beforeEach, describe, expect, it, jest } from '@jest/globals';
|
import { afterEach, beforeEach, describe, expect, it, jest } from '@jest/globals';
|
||||||
import { OpenViduMeet } from '../../src/components/OpenViduMeet';
|
import { OpenViduMeet } from '../../src/components/OpenViduMeet';
|
||||||
|
import { WebComponentProperty } from '@openvidu-meet/typings';
|
||||||
import '../../src/index';
|
import '../../src/index';
|
||||||
|
|
||||||
describe('OpenViduMeet WebComponent Attributes', () => {
|
describe('OpenViduMeet WebComponent Attributes', () => {
|
||||||
@ -15,55 +16,391 @@ describe('OpenViduMeet WebComponent Attributes', () => {
|
|||||||
document.body.innerHTML = '';
|
document.body.innerHTML = '';
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should render iframe with correct attributes', () => {
|
// ==========================================
|
||||||
const iframe = component.shadowRoot?.querySelector('iframe');
|
// IFRAME SETUP
|
||||||
expect(iframe).not.toBeNull();
|
// ==========================================
|
||||||
expect(iframe?.getAttribute('allow')).toContain('camera');
|
describe('Iframe Configuration', () => {
|
||||||
expect(iframe?.getAttribute('allow')).toContain('microphone');
|
it('should render iframe with correct media permissions', () => {
|
||||||
expect(iframe?.getAttribute('allow')).toContain('display-capture');
|
const iframe = component.shadowRoot?.querySelector('iframe');
|
||||||
expect(iframe?.getAttribute('allow')).toContain('fullscreen');
|
expect(iframe).not.toBeNull();
|
||||||
expect(iframe?.getAttribute('allow')).toContain('autoplay');
|
|
||||||
expect(iframe?.getAttribute('allow')).toContain('compute-pressure');
|
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
|
// Trigger updateIframeSrc manually
|
||||||
(component as any).updateIframeSrc();
|
(component as any).updateIframeSrc();
|
||||||
|
|
||||||
const iframe = component.shadowRoot?.querySelector('iframe');
|
const iframe = component.shadowRoot?.querySelector('iframe');
|
||||||
|
|
||||||
expect(iframe).toBeDefined();
|
expect(iframe).toBeDefined();
|
||||||
expect(iframe?.src).toBeFalsy();
|
expect(iframe?.src).toBeFalsy();
|
||||||
expect(consoleErrorSpy).toHaveBeenCalledWith('The "room-url" or "recording-url" attribute is required.');
|
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';
|
// OPTIONAL ATTRIBUTES AS QUERY PARAMETERS
|
||||||
component.setAttribute('room-url', roomUrl);
|
// ==========================================
|
||||||
component.setAttribute('user', 'testUser');
|
describe('Optional Attributes as Query Parameters', () => {
|
||||||
|
const baseRoomUrl = 'https://example.com/room/testRoom';
|
||||||
|
|
||||||
// Manually trigger the update (MutationObserver doesn't always trigger in tests)
|
it('should add participant-name as query parameter', () => {
|
||||||
(component as any).updateIframeSrc();
|
const participantName = 'John Doe';
|
||||||
|
component.setAttribute(WebComponentProperty.ROOM_URL, baseRoomUrl);
|
||||||
|
component.setAttribute(WebComponentProperty.PARTICIPANT_NAME, participantName);
|
||||||
|
|
||||||
const iframe = component.shadowRoot?.querySelector('iframe');
|
(component as any).updateIframeSrc();
|
||||||
expect(iframe?.src).toEqual(`${roomUrl}&user=testUser`);
|
|
||||||
|
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';
|
// CUSTOM/UNKNOWN ATTRIBUTES
|
||||||
const roomUrl = `${domain}/room/testRoom-123?secret=123456`;
|
// ==========================================
|
||||||
component.setAttribute('room-url', roomUrl);
|
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
|
const iframe = component.shadowRoot?.querySelector('iframe');
|
||||||
expect((component as any).targetIframeOrigin).toBe(domain);
|
const url = new URL(iframe?.src || '');
|
||||||
expect((component as any).commandsManager.targetIframeOrigin).toBe(domain);
|
|
||||||
expect((component as any).eventsManager.targetIframeOrigin).toBe(domain);
|
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');
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@ -14,6 +14,12 @@ export enum WebComponentProperty {
|
|||||||
*/
|
*/
|
||||||
PARTICIPANT_NAME = 'participant-name',
|
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.
|
* URL to redirect to when leaving the meeting.
|
||||||
* Redirection occurs after the **`CLOSED` event** fires.
|
* Redirection occurs after the **`CLOSED` event** fires.
|
||||||
|
|||||||
77
meet.sh
77
meet.sh
@ -622,30 +622,42 @@ build_webcomponent_doc() {
|
|||||||
mkdir -p "$output_dir"
|
mkdir -p "$output_dir"
|
||||||
fi
|
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}"
|
echo -e "${GREEN}Copying documentation to: $output_dir${NC}"
|
||||||
cp docs/webcomponent-events.md "$output_dir/webcomponent-events.md"
|
cp docs/webcomponent-{events,commands,attributes}.md "$output_dir"/
|
||||||
cp docs/webcomponent-commands.md "$output_dir/webcomponent-commands.md"
|
|
||||||
cp docs/webcomponent-attributes.md "$output_dir/webcomponent-attributes.md"
|
|
||||||
echo -e "${GREEN}✓ Documentation copied successfully!${NC}"
|
echo -e "${GREEN}✓ Documentation copied successfully!${NC}"
|
||||||
|
rm -f docs/webcomponent-{events,commands,attributes}.md
|
||||||
else
|
else
|
||||||
echo -e "${RED}Error: Documentation files not found in docs/ directory${NC}"
|
echo -e "${RED}Error: Documentation files not found in docs/ directory${NC}"
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
else
|
else
|
||||||
echo -e "${YELLOW}No output directory specified. Documentation remains in docs/ directory.${NC}"
|
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
|
fi
|
||||||
|
|
||||||
echo
|
echo
|
||||||
echo -e "${GREEN}✓ Webcomponent documentation generated successfully!${NC}"
|
echo -e "${GREEN}✓ Webcomponent documentation generated successfully!${NC}"
|
||||||
echo -e "${YELLOW}Output directory: $output_dir${NC}"
|
echo -e "${YELLOW}Output directory: ${abs_path}${NC}"
|
||||||
rm -f docs/webcomponent-events.md docs/webcomponent-commands.md docs/webcomponent-attributes.md
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
# Build REST API documentation
|
# Build REST API documentation
|
||||||
build_rest_api_doc() {
|
build_rest_api_doc() {
|
||||||
local output_dir="$1"
|
local output_target="$1"
|
||||||
CE_REST_API_DOC_PATH="meet-ce/backend/public/openapi/"
|
CE_REST_API_DOC_PATH="meet-ce/backend/public/openapi/"
|
||||||
|
|
||||||
echo -e "${BLUE}=====================================${NC}"
|
echo -e "${BLUE}=====================================${NC}"
|
||||||
echo -e "${BLUE} Building REST API Docs${NC}"
|
echo -e "${BLUE} Building REST API Docs${NC}"
|
||||||
echo -e "${BLUE}=====================================${NC}"
|
echo -e "${BLUE}=====================================${NC}"
|
||||||
@ -653,31 +665,56 @@ build_rest_api_doc() {
|
|||||||
|
|
||||||
check_pnpm
|
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}"
|
echo -e "${GREEN}Generating REST API documentation...${NC}"
|
||||||
pnpm run build:rest-api-docs
|
pnpm run build:rest-api-docs
|
||||||
|
|
||||||
if [ -n "$output_dir" ]; then
|
# Determinar si el parámetro es archivo o directorio
|
||||||
output_dir="${output_dir%/}"
|
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
|
# Crear carpeta contenedora si no existe
|
||||||
echo -e "${YELLOW}Creating output directory: $output_dir${NC}"
|
if [ ! -d "$output_dir" ]; then
|
||||||
mkdir -p "$output_dir"
|
echo -e "${YELLOW}Creating output directory: $output_dir${NC}"
|
||||||
fi
|
mkdir -p "$output_dir"
|
||||||
|
fi
|
||||||
|
|
||||||
if [ -f "$CE_REST_API_DOC_PATH/public.html" ]; then
|
# Preguntar si el archivo ya existe
|
||||||
echo -e "${GREEN}Copying REST API documentation to: $output_dir${NC}"
|
if [ -f "$output_file" ]; then
|
||||||
cp "$CE_REST_API_DOC_PATH/public.html" "$output_dir/public.html"
|
echo -e "${YELLOW}Warning: '$output_file' already exists.${NC}"
|
||||||
echo -e "${GREEN}✓ Documentation copied successfully!${NC}"
|
read -rp "Do you want to overwrite it? [y/N]: " confirm
|
||||||
else
|
if [[ ! "$confirm" =~ ^[Yy]$ ]]; then
|
||||||
echo -e "${RED}Error: REST API documentation files not found${NC}"
|
echo -e "${RED}Operation cancelled by user.${NC}"
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
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
|
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
|
fi
|
||||||
|
|
||||||
echo
|
echo
|
||||||
echo -e "${GREEN}✓ REST API documentation generated successfully!${NC}"
|
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
|
# Clone private meet-pro repository into repository root
|
||||||
|
|||||||
@ -2,42 +2,42 @@
|
|||||||
"folders": [
|
"folders": [
|
||||||
{
|
{
|
||||||
"name": "openvidu-components-angular",
|
"name": "openvidu-components-angular",
|
||||||
"path": "../openvidu/openvidu-components-angular",
|
"path": "../openvidu/openvidu-components-angular"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "openvidu-meet (root)",
|
"name": "openvidu-meet (root)",
|
||||||
"path": ".",
|
"path": "."
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "openvidu-meet (CE)",
|
"name": "openvidu-meet (CE)",
|
||||||
"path": "meet-ce",
|
"path": "meet-ce"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "openvidu-meet (PRO)",
|
"name": "openvidu-meet (PRO)",
|
||||||
"path": "meet-pro",
|
"path": "meet-pro"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "shared-meet-components",
|
"name": "shared-meet-components",
|
||||||
"path": "meet-ce/frontend/projects/shared-meet-components",
|
"path": "meet-ce/frontend/projects/shared-meet-components"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "meet-testapp",
|
"name": "meet-testapp",
|
||||||
"path": "testapp",
|
"path": "testapp"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "meet-webcomponent",
|
"name": "meet-webcomponent",
|
||||||
"path": "meet-ce/frontend/webcomponent",
|
"path": "meet-ce/frontend/webcomponent"
|
||||||
},
|
}
|
||||||
],
|
],
|
||||||
"settings": {
|
"settings": {
|
||||||
"files.exclude": {
|
"files.exclude": {
|
||||||
"**/meet-ce": true,
|
"**/meet-ce": true,
|
||||||
"**/meet-pro": true,
|
"**/meet-pro": true,
|
||||||
"**/webcomponent": true,
|
"**/frontend/webcomponent": true,
|
||||||
"**/webhooks-snippets": false,
|
"**/webhooks-snippets": false,
|
||||||
"**/testapp": true,
|
"**/testapp": true,
|
||||||
"**/.angular": true,
|
"**/.angular": true,
|
||||||
"**/public": true,
|
"**/public": false,
|
||||||
"**/dist": false,
|
"**/dist": false,
|
||||||
"**/node_modules": true,
|
"**/node_modules": true,
|
||||||
"**/test-results": true,
|
"**/test-results": true,
|
||||||
|
|||||||
494
pnpm-lock.yaml
generated
494
pnpm-lock.yaml
generated
@ -501,6 +501,365 @@ importers:
|
|||||||
specifier: 5.9.2
|
specifier: 5.9.2
|
||||||
version: 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:
|
testapp:
|
||||||
dependencies:
|
dependencies:
|
||||||
'@openvidu-meet/typings':
|
'@openvidu-meet/typings':
|
||||||
@ -6570,6 +6929,7 @@ packages:
|
|||||||
|
|
||||||
livekit-client@2.15.11:
|
livekit-client@2.15.11:
|
||||||
resolution: {integrity: sha512-9cHdAbSibPGyt7wWM+GAUswIOuklQHF9y561Oruzh0nNFNvRzMsE10oqJvjs0k6s2Jl+j/Z5Ar90bzVwLpu1yg==}
|
resolution: {integrity: sha512-9cHdAbSibPGyt7wWM+GAUswIOuklQHF9y561Oruzh0nNFNvRzMsE10oqJvjs0k6s2Jl+j/Z5Ar90bzVwLpu1yg==}
|
||||||
|
deprecated: Compatibility issue around AbortSignal.any usage, use >=2.15.12 instead
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
'@types/dom-mediacapture-record': ^1
|
'@types/dom-mediacapture-record': ^1
|
||||||
|
|
||||||
@ -9322,6 +9682,59 @@ snapshots:
|
|||||||
- webpack-cli
|
- webpack-cli
|
||||||
- yaml
|
- 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)':
|
'@angular-devkit/architect@0.2003.4(chokidar@4.0.3)':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@angular-devkit/core': 20.3.4(chokidar@4.0.3)
|
'@angular-devkit/core': 20.3.4(chokidar@4.0.3)
|
||||||
@ -9340,7 +9753,7 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
'@ampproject/remapping': 2.3.0
|
'@ampproject/remapping': 2.3.0
|
||||||
'@angular-devkit/architect': 0.2003.4(chokidar@4.0.3)
|
'@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-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/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)
|
'@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/preset-env': 7.28.3(@babel/core@7.28.3)
|
||||||
'@babel/runtime': 7.28.3
|
'@babel/runtime': 7.28.3
|
||||||
'@discoveryjs/json-ext': 0.6.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
|
ansi-colors: 4.1.3
|
||||||
autoprefixer: 10.4.21(postcss@8.5.6)
|
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
|
browserslist: 4.26.3
|
||||||
copy-webpack-plugin: 13.0.1(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)
|
css-loader: 7.1.2(webpack@5.101.2(esbuild@0.25.9))
|
||||||
esbuild-wasm: 0.25.9
|
esbuild-wasm: 0.25.9
|
||||||
fast-glob: 3.3.3
|
fast-glob: 3.3.3
|
||||||
http-proxy-middleware: 3.0.5
|
http-proxy-middleware: 3.0.5
|
||||||
@ -9368,32 +9781,32 @@ snapshots:
|
|||||||
jsonc-parser: 3.3.1
|
jsonc-parser: 3.3.1
|
||||||
karma-source-map-support: 1.4.0
|
karma-source-map-support: 1.4.0
|
||||||
less: 4.4.0
|
less: 4.4.0
|
||||||
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))
|
||||||
license-webpack-plugin: 4.0.2(webpack@5.101.2)
|
license-webpack-plugin: 4.0.2(webpack@5.101.2(esbuild@0.25.9))
|
||||||
loader-utils: 3.3.1
|
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
|
open: 10.2.0
|
||||||
ora: 8.2.0
|
ora: 8.2.0
|
||||||
picomatch: 4.0.3
|
picomatch: 4.0.3
|
||||||
piscina: 5.1.3
|
piscina: 5.1.3
|
||||||
postcss: 8.5.6
|
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
|
resolve-url-loader: 5.0.0
|
||||||
rxjs: 7.8.2
|
rxjs: 7.8.2
|
||||||
sass: 1.90.0
|
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
|
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
|
source-map-support: 0.5.21
|
||||||
terser: 5.43.1
|
terser: 5.43.1
|
||||||
tree-kill: 1.2.2
|
tree-kill: 1.2.2
|
||||||
tslib: 2.8.1
|
tslib: 2.8.1
|
||||||
typescript: 5.9.2
|
typescript: 5.9.2
|
||||||
webpack: 5.101.2(esbuild@0.25.9)
|
webpack: 5.101.2(esbuild@0.25.9)
|
||||||
webpack-dev-middleware: 7.4.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)
|
webpack-dev-server: 5.2.2(webpack@5.101.2(esbuild@0.25.9))
|
||||||
webpack-merge: 6.0.1
|
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:
|
optionalDependencies:
|
||||||
'@angular/core': 20.3.4(@angular/compiler@20.3.4)(rxjs@7.8.2)(zone.js@0.15.1)
|
'@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/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
|
- webpack-cli
|
||||||
- yaml
|
- 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:
|
dependencies:
|
||||||
'@angular-devkit/architect': 0.2003.4(chokidar@4.0.3)
|
'@angular-devkit/architect': 0.2003.4(chokidar@4.0.3)
|
||||||
rxjs: 7.8.2
|
rxjs: 7.8.2
|
||||||
webpack: 5.101.2(esbuild@0.25.9)
|
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:
|
transitivePeerDependencies:
|
||||||
- chokidar
|
- chokidar
|
||||||
|
|
||||||
@ -12197,7 +12610,7 @@ snapshots:
|
|||||||
'@napi-rs/nice-win32-x64-msvc': 1.1.1
|
'@napi-rs/nice-win32-x64-msvc': 1.1.1
|
||||||
optional: true
|
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:
|
dependencies:
|
||||||
'@angular/compiler-cli': 20.3.4(@angular/compiler@20.3.4)(typescript@5.9.2)
|
'@angular/compiler-cli': 20.3.4(@angular/compiler@20.3.4)(typescript@5.9.2)
|
||||||
typescript: 5.9.2
|
typescript: 5.9.2
|
||||||
@ -13528,6 +13941,10 @@ snapshots:
|
|||||||
dependencies:
|
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)
|
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':
|
'@webassemblyjs/ast@1.14.1':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@webassemblyjs/helper-numbers': 1.13.2
|
'@webassemblyjs/helper-numbers': 1.13.2
|
||||||
@ -13899,7 +14316,7 @@ snapshots:
|
|||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- supports-color
|
- 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:
|
dependencies:
|
||||||
'@babel/core': 7.28.3
|
'@babel/core': 7.28.3
|
||||||
find-up: 5.0.0
|
find-up: 5.0.0
|
||||||
@ -14486,7 +14903,7 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
is-what: 3.14.1
|
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:
|
dependencies:
|
||||||
glob-parent: 6.0.2
|
glob-parent: 6.0.2
|
||||||
normalize-path: 3.0.0
|
normalize-path: 3.0.0
|
||||||
@ -14597,7 +15014,7 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
postcss: 8.5.6
|
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:
|
dependencies:
|
||||||
icss-utils: 5.1.0(postcss@8.5.6)
|
icss-utils: 5.1.0(postcss@8.5.6)
|
||||||
postcss: 8.5.6
|
postcss: 8.5.6
|
||||||
@ -17148,7 +17565,7 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
readable-stream: 2.3.8
|
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:
|
dependencies:
|
||||||
less: 4.4.0
|
less: 4.4.0
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
@ -17189,7 +17606,7 @@ snapshots:
|
|||||||
prelude-ls: 1.2.1
|
prelude-ls: 1.2.1
|
||||||
type-check: 0.4.0
|
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:
|
dependencies:
|
||||||
webpack-sources: 3.3.3
|
webpack-sources: 3.3.3
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
@ -17445,7 +17862,7 @@ snapshots:
|
|||||||
|
|
||||||
mimic-function@5.0.1: {}
|
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:
|
dependencies:
|
||||||
schema-utils: 4.3.3
|
schema-utils: 4.3.3
|
||||||
tapable: 2.3.0
|
tapable: 2.3.0
|
||||||
@ -18153,7 +18570,7 @@ snapshots:
|
|||||||
postcss: 8.5.6
|
postcss: 8.5.6
|
||||||
ts-node: 10.9.2(@types/node@22.18.8)(typescript@5.7.3)
|
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:
|
dependencies:
|
||||||
cosmiconfig: 9.0.0(typescript@5.9.2)
|
cosmiconfig: 9.0.0(typescript@5.9.2)
|
||||||
jiti: 1.21.7
|
jiti: 1.21.7
|
||||||
@ -18730,7 +19147,7 @@ snapshots:
|
|||||||
|
|
||||||
safer-buffer@2.1.2: {}
|
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:
|
dependencies:
|
||||||
neo-async: 2.6.2
|
neo-async: 2.6.2
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
@ -19038,7 +19455,7 @@ snapshots:
|
|||||||
|
|
||||||
source-map-js@1.2.1: {}
|
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:
|
dependencies:
|
||||||
iconv-lite: 0.6.3
|
iconv-lite: 0.6.3
|
||||||
source-map-js: 1.2.1
|
source-map-js: 1.2.1
|
||||||
@ -19812,6 +20229,23 @@ snapshots:
|
|||||||
terser: 5.44.0
|
terser: 5.44.0
|
||||||
tsx: 4.20.3
|
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: {}
|
void-elements@2.0.1: {}
|
||||||
|
|
||||||
w3c-xmlserializer@4.0.0:
|
w3c-xmlserializer@4.0.0:
|
||||||
@ -19848,7 +20282,7 @@ snapshots:
|
|||||||
|
|
||||||
webidl-conversions@7.0.0: {}
|
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:
|
dependencies:
|
||||||
colorette: 2.0.20
|
colorette: 2.0.20
|
||||||
memfs: 4.49.0
|
memfs: 4.49.0
|
||||||
@ -19859,7 +20293,7 @@ snapshots:
|
|||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
webpack: 5.101.2(esbuild@0.25.9)
|
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:
|
dependencies:
|
||||||
'@types/bonjour': 3.5.13
|
'@types/bonjour': 3.5.13
|
||||||
'@types/connect-history-api-fallback': 1.5.4
|
'@types/connect-history-api-fallback': 1.5.4
|
||||||
@ -19887,7 +20321,7 @@ snapshots:
|
|||||||
serve-index: 1.9.1
|
serve-index: 1.9.1
|
||||||
sockjs: 0.3.24
|
sockjs: 0.3.24
|
||||||
spdy: 4.0.2
|
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
|
ws: 8.18.3
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
webpack: 5.101.2(esbuild@0.25.9)
|
webpack: 5.101.2(esbuild@0.25.9)
|
||||||
@ -19905,7 +20339,7 @@ snapshots:
|
|||||||
|
|
||||||
webpack-sources@3.3.3: {}
|
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:
|
dependencies:
|
||||||
typed-assert: 1.0.9
|
typed-assert: 1.0.9
|
||||||
webpack: 5.101.2(esbuild@0.25.9)
|
webpack: 5.101.2(esbuild@0.25.9)
|
||||||
|
|||||||
219
testapp/public/js/webcomponent.js
Normal file
219
testapp/public/js/webcomponent.js
Normal file
@ -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, '"')
|
||||||
|
.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}] <strong>${event.event}</strong>`;
|
||||||
|
// 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 = `<pre class="mb-0"><code>${escapeHtml(formattedJson)}</code></pre>`;
|
||||||
|
// 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);
|
||||||
|
});
|
||||||
|
});
|
||||||
Loading…
x
Reference in New Issue
Block a user