frontend: refactor meeting components and services for improved readability and performance

This commit is contained in:
juancarmore 2026-02-12 13:53:20 +01:00
parent 4c864b193f
commit 599a744302
18 changed files with 149 additions and 439 deletions

View File

@ -1,6 +1,6 @@
<div class="captions-container">
<div class="captions-wrapper" [class.single-caption]="captions().length === 1">
@for (caption of captions(); track trackByCaption($index, caption)) {
@for (caption of captions(); track caption.id) {
<div
[ngClass]="getCaptionClasses(caption)"
[attr.data-caption-id]="caption.id"

View File

@ -13,7 +13,7 @@ export class MeetingCaptionsComponent {
captions = input<Caption[]>([]);
// Track animation state for each caption
protected readonly captionAnimationState = signal<Map<string, 'entering' | 'active' | 'leaving'>>(new Map());
captionAnimationState = signal<Map<string, 'entering' | 'active' | 'leaving'>>(new Map());
// ViewChildren to access caption text containers for auto-scroll
@ViewChildren('captionTextContainer')
@ -77,17 +77,6 @@ export class MeetingCaptionsComponent {
return classes.join(' ');
}
/**
* Tracks captions by their ID for optimal Angular rendering.
*
* @param index Item index
* @param caption Caption item
* @returns Unique identifier
*/
protected trackByCaption(index: number, caption: Caption): string {
return caption.id;
}
/**
* Scrolls all caption text containers to the bottom to show the most recent text.
* Called automatically when captions are updated.

View File

@ -1,4 +1,4 @@
@if (meetingContextService.lkRoom()) {
@if (lkRoom()) {
<div class="main-container" [ngClass]="{ withFooter: areCaptionsEnabledByUser() }">
<ov-layout
[ovRemoteParticipants]="visibleRemoteParticipants()"

View File

@ -1,7 +1,6 @@
import { CommonModule } from '@angular/common';
import { Component, computed, effect, inject, signal, untracked } from '@angular/core';
import {
ILogger,
LoggerService,
OpenViduComponentsUiModule,
PanelService,
@ -30,46 +29,42 @@ import { MeetingCaptionsComponent } from '../meeting-captions/meeting-captions.c
styleUrl: './meeting-custom-layout.component.scss'
})
export class MeetingCustomLayoutComponent {
private readonly logger: ILogger = inject(LoggerService).get('MeetingCustomLayoutComponent');
protected readonly layoutService = inject(MeetingLayoutService);
protected readonly meetingContextService = inject(MeetingContextService);
protected readonly meetingService = inject(MeetingService);
protected readonly panelService = inject(PanelService);
protected readonly captionsService = inject(MeetingCaptionsService);
protected readonly linkOverlayConfig = {
protected meetingContextService = inject(MeetingContextService);
protected meetingService = inject(MeetingService);
protected layoutService = inject(MeetingLayoutService);
protected captionsService = inject(MeetingCaptionsService);
protected panelService = inject(PanelService);
protected logger = inject(LoggerService).get('MeetingCustomLayoutComponent');
lkRoom = this.meetingContextService.lkRoom;
meetingUrl = this.meetingContextService.meetingUrl;
shouldShowLinkOverlay = computed(() => {
const hasNoRemotes = this.remoteParticipants().length === 0;
return this.meetingContextService.canModerateRoom() && hasNoRemotes;
});
linkOverlayConfig = {
title: 'Start collaborating',
subtitle: 'Share this link to bring others into the meeting',
titleSize: 'xl' as const,
titleWeight: 'bold' as const
};
protected readonly meetingUrl = computed(() => this.meetingContextService.meetingUrl());
protected readonly remoteParticipants = computed(() => this.meetingContextService.remoteParticipants());
protected readonly shouldShowLinkOverlay = computed(() => {
const hasNoRemotes = this.meetingContextService.remoteParticipants().length === 0;
return this.meetingContextService.canModerateRoom() && hasNoRemotes;
});
protected readonly areCaptionsEnabledByUser = computed(() => this.captionsService.areCaptionsEnabledByUser());
protected readonly captions = computed(() => this.captionsService.captions());
protected readonly isLayoutSwitchingAllowed = this.meetingContextService.allowLayoutSwitching;
private displayedParticipantIds: string[] = [];
private audioElements = new Map<string, HTMLMediaElement>();
private proxyCache = new WeakMap<ParticipantModel, { proxy: ParticipantModel; showCamera: boolean }>();
areCaptionsEnabledByUser = this.captionsService.areCaptionsEnabledByUser;
isLayoutSwitchingAllowed = this.meetingContextService.allowLayoutSwitching;
isSmartMosaicActive = computed(() => this.isLayoutSwitchingAllowed() && this.layoutService.isSmartMosaicEnabled());
captions = this.captionsService.captions;
remoteParticipants = this.meetingContextService.remoteParticipants;
private _visibleRemoteParticipants = signal<ParticipantModel[]>([]);
readonly visibleRemoteParticipants = this._visibleRemoteParticipants.asReadonly();
visibleRemoteParticipants = this._visibleRemoteParticipants.asReadonly();
protected readonly hiddenParticipantsCount = computed(() => {
hiddenParticipantsCount = computed(() => {
const total = this.remoteParticipants().length;
const visible = this.visibleRemoteParticipants().length;
return Math.max(0, total - visible);
});
protected readonly hiddenParticipantNames = computed(() => {
hiddenParticipantNames = computed(() => {
const visibleIds = new Set(this.visibleRemoteParticipants().map((p) => p.identity));
return this.remoteParticipants()
.filter((p) => !visibleIds.has(p.identity))
@ -80,7 +75,7 @@ export class MeetingCustomLayoutComponent {
* Indicates whether to show the hidden participants indicator in the top bar
* when in smart mosaic mode.
*/
protected readonly showTopBarHiddenParticipantsIndicator = computed(() => {
showTopBarHiddenParticipantsIndicator = computed(() => {
const localParticipant = this.meetingContextService.localParticipant()!;
const hasPinnedParticipant =
localParticipant.isPinned || this.remoteParticipants().some((p) => (p as CustomParticipantModel).isPinned);
@ -90,6 +85,10 @@ export class MeetingCustomLayoutComponent {
return showTopBar;
});
private displayedParticipantIds: string[] = [];
private audioElements = new Map<string, HTMLMediaElement>();
private proxyCache = new WeakMap<ParticipantModel, { proxy: ParticipantModel; showCamera: boolean }>();
constructor() {
this.setupSpeakerTrackingEffect();
this.setupParticipantCleanupEffect();
@ -106,17 +105,13 @@ export class MeetingCustomLayoutComponent {
this.meetingService.copyMeetingSpeakerLink(room);
}
protected isSmartMosaicActive(): boolean {
return this.isLayoutSwitchingAllowed() && this.layoutService.isSmartMosaicEnabled();
}
protected toggleParticipantsPanel(): void {
this.panelService.togglePanel(PanelType.PARTICIPANTS);
}
private setupVisibleParticipantsUpdate(): void {
effect(() => {
const allRemotes = this.meetingContextService.remoteParticipants();
const allRemotes = this.remoteParticipants();
if (!this.isSmartMosaicActive()) {
this._visibleRemoteParticipants.set(allRemotes);
@ -164,7 +159,6 @@ export class MeetingCustomLayoutComponent {
* @param targetIds Set of participant IDs that should be displayed.
* @param availableIds Set of participant IDs that are currently available for display.
*/
private syncDisplayedParticipantsWithTarget(targetIds: Set<string>, availableIds: Set<string>): void {
this.displayedParticipantIds = this.displayedParticipantIds.filter((id) => availableIds.has(id));
@ -193,7 +187,7 @@ export class MeetingCustomLayoutComponent {
private setupSpeakerTrackingEffect(): void {
effect(() => {
const room = this.meetingContextService.lkRoom();
const room = this.lkRoom();
if (this.isLayoutSwitchingAllowed() && room) {
this.layoutService.initializeSpeakerTracking(room);
}

View File

@ -1,5 +1,5 @@
import { CommonModule } from '@angular/common';
import { Component, computed, inject } from '@angular/core';
import { Component, inject } from '@angular/core';
import { LoggerService } from 'openvidu-components-angular';
import { ShareMeetingLinkComponent } from '../../components/share-meeting-link/share-meeting-link.component';
import { MeetingContextService } from '../../services/meeting-context.service';
@ -21,19 +21,8 @@ export class MeetingInvitePanelComponent {
protected loggerService = inject(LoggerService);
protected log = this.loggerService.get('OpenVidu Meet - MeetingInvitePanel');
/**
* Computed signal to determine if the share link should be shown
*/
protected showShareLink = computed(() => {
return this.meetingContextService.canModerateRoom();
});
/**
* Computed signal for the meeting URL from context
*/
protected meetingUrl = computed(() => {
return this.meetingContextService.meetingUrl();
});
showShareLink = this.meetingContextService.canModerateRoom;
meetingUrl = this.meetingContextService.meetingUrl;
onCopyClicked(): void {
const room = this.meetingContextService.meetRoom();

View File

@ -22,14 +22,14 @@ export class MeetingParticipantItemComponent {
// Template reference for the component's template
@ViewChild('template', { static: true }) template!: TemplateRef<any>;
protected meetingService: MeetingService = inject(MeetingService);
protected meetingService = inject(MeetingService);
protected loggerService = inject(LoggerService);
protected log = this.loggerService.get('OpenVidu Meet - MeetingParticipantItem');
/**
* Get or compute display properties for a participant
*/
protected getDisplayProperties(
getDisplayProperties(
participant: CustomParticipantModel,
localParticipant: CustomParticipantModel
): ParticipantDisplayProperties {
@ -56,7 +56,6 @@ export class MeetingParticipantItemComponent {
if (!localParticipant.isModerator()) return;
const roomId = localParticipant.roomName;
if (!roomId) {
this.log.e('Cannot change participant role: local participant room name is undefined');
return;
@ -81,7 +80,6 @@ export class MeetingParticipantItemComponent {
if (!localParticipant.isModerator()) return;
const roomId = localParticipant.roomName;
if (!roomId) {
this.log.e('Cannot change participant role: local participant room name is undefined');
return;
@ -106,9 +104,8 @@ export class MeetingParticipantItemComponent {
if (!localParticipant.isModerator()) return;
const roomId = localParticipant.roomName;
if (!roomId) {
this.log.e('Cannot change participant role: local participant room name is undefined');
this.log.e('Cannot kick participant: local participant room name is undefined');
return;
}

View File

@ -1,5 +1,5 @@
import { CommonModule } from '@angular/common';
import { Component, computed, inject } from '@angular/core';
import { Component, inject } from '@angular/core';
import { FormsModule } from '@angular/forms';
import { MatFormFieldModule } from '@angular/material/form-field';
import { MatIconModule } from '@angular/material/icon';
@ -30,43 +30,24 @@ import { MeetingLayoutService } from '../../services/meeting-layout.service';
styleUrl: './meeting-settings-extensions.component.scss'
})
export class MeetingSettingsExtensionsComponent {
private readonly layoutService = inject(MeetingLayoutService);
protected readonly meetingContextService = inject(MeetingContextService);
private readonly layoutService = inject(MeetingLayoutService);
/**
* Expose LayoutMode enum to template
*/
readonly LayoutMode = MeetLayoutMode;
/** Whether the layout switching feature is allowed */
isLayoutSwitchingAllowed = this.meetingContextService.allowLayoutSwitching;
/** Expose LayoutMode enum to template */
LayoutMode = MeetLayoutMode;
/** Current layout mode */
layoutMode = this.layoutService.layoutMode;
/** Whether Smart Mosaic layout is enabled */
isSmartMode = this.layoutService.isSmartMosaicEnabled;
/**
* Whether the layout switching feature is allowed
*/
protected readonly isLayoutSwitchingAllowed = this.meetingContextService.allowLayoutSwitching;
/**
* Current layout mode
*/
protected readonly layoutMode = computed(() => this.layoutService.layoutMode());
/**
* Current participant count
*/
protected readonly participantCount = computed(() => this.layoutService.maxRemoteSpeakers());
/**
* Minimum number of participants that can be shown when Smart Mosaic layout is enabled
*/
protected readonly minParticipants = this.layoutService.MIN_REMOTE_SPEAKERS;
/**
* Maximum number of participants that can be shown
*/
protected readonly maxParticipants = this.layoutService.MAX_REMOTE_SPEAKERS_LIMIT;
/**
* Computed property to check if Smart Mosaic mode is active
*/
readonly isSmartMode = this.layoutService.isSmartMosaicEnabled;
/** Minimum number of participants that can be shown when Smart Mosaic layout is enabled */
minParticipants = this.layoutService.MIN_REMOTE_SPEAKERS;
/** Maximum number of participants that can be shown */
maxParticipants = this.layoutService.MAX_REMOTE_SPEAKERS_LIMIT;
/** Current participant count */
participantCount = this.layoutService.maxRemoteSpeakers;
/**
* Handler for layout mode change

View File

@ -22,38 +22,26 @@ import { MeetingService } from '../../services/meeting.service';
export class MeetingToolbarExtraButtonsComponent {
protected meetingContextService = inject(MeetingContextService);
protected meetingService = inject(MeetingService);
protected loggerService = inject(LoggerService);
protected captionService = inject(MeetingCaptionsService);
protected loggerService = inject(LoggerService);
protected log = this.loggerService.get('OpenVidu Meet - MeetingToolbarExtraButtons');
protected readonly copyLinkTooltip = 'Copy the meeting link';
protected readonly copyLinkText = 'Copy meeting link';
/**
* Whether to show the copy link button
*/
protected showCopyLinkButton = computed(() => this.meetingContextService.canModerateRoom());
/** Whether to show the copy link button (only for moderators) */
showCopyLinkButton = this.meetingContextService.canModerateRoom;
copyLinkTooltip = 'Copy the meeting link';
copyLinkText = 'Copy meeting link';
/**
* Captions status based on room and global configuration
*/
protected captionsStatus = computed(() => this.meetingContextService.getCaptionsStatus());
/** Captions status based on room and global configuration */
captionsStatus = this.meetingContextService.getCaptionsStatus;
/** Whether to show the captions button (visible when not HIDDEN) */
showCaptionsButton = computed(() => this.captionsStatus() !== 'HIDDEN');
/** Whether captions button is disabled (true when DISABLED_WITH_WARNING) */
isCaptionsButtonDisabled = computed(() => this.captionsStatus() === 'DISABLED_WITH_WARNING');
/** Whether captions are currently enabled by the user */
areCaptionsEnabledByUser = this.captionService.areCaptionsEnabledByUser;
/**
* Whether to show the captions button (visible when not HIDDEN)
*/
protected showCaptionsButton = computed(() => this.captionsStatus() !== 'HIDDEN');
/**
* Whether captions button is disabled (true when DISABLED_WITH_WARNING)
*/
protected isCaptionsButtonDisabled = computed(() => this.captionsStatus() === 'DISABLED_WITH_WARNING');
/**
* Whether the device is mobile (affects button style)
*/
protected isMobile = computed(() => this.meetingContextService.isMobile());
protected areCaptionsEnabledByUser = computed(() => this.captionService.areCaptionsEnabledByUser());
/** Whether the device is mobile (affects button style) */
isMobile = this.meetingContextService.isMobile;
onCopyLinkClick(): void {
const room = this.meetingContextService.meetRoom();
@ -71,6 +59,6 @@ export class MeetingToolbarExtraButtonsComponent {
this.log.w('Captions are disabled at system level (MEET_CAPTIONS_ENABLED=false)');
return;
}
this.captionService.areCaptionsEnabledByUser() ? this.captionService.disable() : this.captionService.enable();
this.areCaptionsEnabledByUser() ? this.captionService.disable() : this.captionService.enable();
}
}

View File

@ -1,5 +1,5 @@
import { CommonModule } from '@angular/common';
import { Component, computed, inject } from '@angular/core';
import { Component, inject } from '@angular/core';
import { MatButtonModule } from '@angular/material/button';
import { MatDividerModule } from '@angular/material/divider';
import { MatIconModule } from '@angular/material/icon';
@ -21,26 +21,16 @@ import { MeetingService } from '../../services/meeting.service';
export class MeetingToolbarLeaveButtonComponent {
protected meetingContextService = inject(MeetingContextService);
protected meetingService = inject(MeetingService);
protected openviduService = inject(OpenViduService);
protected loggerService = inject(LoggerService);
protected log = this.loggerService.get('OpenVidu Meet - MeetingToolbarLeaveButtons');
protected openviduService = inject(OpenViduService);
protected readonly leaveMenuTooltip = 'Leave options';
protected readonly leaveOptionText = 'Leave meeting';
protected readonly endMeetingOptionText = 'End meeting for all';
/**
* Whether to show the leave menu with options
*/
protected showLeaveMenu = computed(() => {
return this.meetingContextService.canModerateRoom();
});
showLeaveMenu = this.meetingContextService.canModerateRoom;
isMobile = this.meetingContextService.isMobile;
/**
* Whether the device is mobile (affects button style)
*/
protected isMobile = computed(() => {
return this.meetingContextService.isMobile();
});
leaveMenuTooltip = 'Leave options';
leaveOptionText = 'Leave meeting';
endMeetingOptionText = 'End meeting for all';
async onLeaveMeetingClick(): Promise<void> {
await this.openviduService.disconnectRoom();

View File

@ -1,23 +0,0 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { MeetingToolbarMoreOptionsMenuComponent } from './meeting-toolbar-more-options-menu.component';
describe('MeetingToolbarMoreOptionsButtonsComponent', () => {
let component: MeetingToolbarMoreOptionsMenuComponent;
let fixture: ComponentFixture<MeetingToolbarMoreOptionsMenuComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [MeetingToolbarMoreOptionsMenuComponent]
})
.compileComponents();
fixture = TestBed.createComponent(MeetingToolbarMoreOptionsMenuComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@ -1,5 +1,5 @@
import { CommonModule } from '@angular/common';
import { Component, computed, inject } from '@angular/core';
import { Component, inject } from '@angular/core';
import { MatButtonModule } from '@angular/material/button';
import { MatIconModule } from '@angular/material/icon';
import { MatMenuModule } from '@angular/material/menu';
@ -12,52 +12,23 @@ import { MeetingContextService } from '../../services/meeting-context.service';
* This component handles custom actions like opening the settings panel for grid layout changes.
*/
@Component({
selector: 'ov-meeting-toolbar-more-options-menu',
imports: [
CommonModule,
MatIconModule,
MatButtonModule,
MatMenuModule,
MatTooltipModule
],
templateUrl: './meeting-toolbar-more-options-menu.component.html',
styleUrl: './meeting-toolbar-more-options-menu.component.scss'
selector: 'ov-meeting-toolbar-more-options-menu',
imports: [CommonModule, MatIconModule, MatButtonModule, MatMenuModule, MatTooltipModule],
templateUrl: './meeting-toolbar-more-options-menu.component.html',
styleUrl: './meeting-toolbar-more-options-menu.component.scss'
})
export class MeetingToolbarMoreOptionsMenuComponent {
/**
* Viewport service for responsive behavior detection
* Injected from openvidu-components-angular
*/
private viewportService = inject(ViewportService);
private meetingContextService = inject(MeetingContextService);
private viewportService = inject(ViewportService);
private panelService = inject(PanelService);
/**
* Panel service for opening/closing panels
* Injected from openvidu-components-angular
*/
private panelService = inject(PanelService);
isMobileView = this.viewportService.isMobile;
isLayoutSwitchingAllowed = this.meetingContextService.allowLayoutSwitching;
/**
* Meeting context service for feature flags
*/
private meetingContextService = inject(MeetingContextService);
/**
* Computed properties for responsive button behavior
* These follow the same pattern as toolbar-media-buttons component
*/
readonly isMobileView = computed(() => this.viewportService.isMobile());
readonly isTabletView = computed(() => this.viewportService.isTablet());
readonly isDesktopView = computed(() => this.viewportService.isDesktop());
/**
* Whether the layout switching feature is allowed
*/
readonly isLayoutSwitchingAllowed = computed(() => this.meetingContextService.allowLayoutSwitching());
/**
* Opens the settings panel to allow users to change grid layout
*/
onOpenSettings(): void {
this.panelService.togglePanel(PanelType.SETTINGS);
}
/**
* Opens the settings panel to allow users to change grid layout
*/
onOpenSettings(): void {
this.panelService.togglePanel(PanelType.SETTINGS);
}
}

View File

@ -1,23 +1,15 @@
import { CommonModule } from '@angular/common';
import { Component, computed, ContentChild, effect, inject, OnInit, signal, Signal } from '@angular/core';
import { Component, computed, ContentChild, effect, inject, OnInit, signal } from '@angular/core';
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
import { MatIconModule } from '@angular/material/icon';
import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
import {
OpenViduComponentsUiModule,
OpenViduThemeMode,
OpenViduThemeService,
Room,
Track
} from 'openvidu-components-angular';
import { OpenViduComponentsUiModule, OpenViduThemeMode, OpenViduThemeService, Room } from 'openvidu-components-angular';
import { Subject } from 'rxjs';
import { RoomFeatures } from '../../../../shared/models/app.model';
import { GlobalConfigService } from '../../../../shared/services/global-config.service';
import { NotificationService } from '../../../../shared/services/notification.service';
import { RoomFeatureService } from '../../../../shared/services/room-feature.service';
import { RuntimeConfigService } from '../../../../shared/services/runtime-config.service';
import { SoundService } from '../../../../shared/services/sound.service';
import { RoomMemberContextService } from '../../../room-members/services/room-member-context.service';
import { MeetingLobbyComponent } from '../../components/meeting-lobby/meeting-lobby.component';
import { MeetingParticipantItemComponent } from '../../customization/meeting-participant-item/meeting-participant-item.component';
import { MeetingCaptionsService } from '../../services/meeting-captions.service';
@ -41,7 +33,16 @@ import { MeetingLobbyService } from '../../services/meeting-lobby.service';
providers: [MeetingLobbyService, MeetingEventHandlerService, SoundService]
})
export class MeetingComponent implements OnInit {
protected _participantItem?: MeetingParticipantItemComponent;
protected meetingContextService = inject(MeetingContextService);
protected lobbyService = inject(MeetingLobbyService);
protected eventHandlerService = inject(MeetingEventHandlerService);
protected captionsService = inject(MeetingCaptionsService);
protected configService = inject(GlobalConfigService);
protected roomFeatureService = inject(RoomFeatureService);
protected ovThemeService = inject(OpenViduThemeService);
protected notificationService = inject(NotificationService);
protected soundService = inject(SoundService);
protected runtimeConfigService = inject(RuntimeConfigService);
// Template reference for custom participant panel item
@ContentChild(MeetingParticipantItemComponent)
@ -49,55 +50,28 @@ export class MeetingComponent implements OnInit {
// Store the reference to the custom participant panel item component
this._participantItem = value;
}
protected _participantItem?: MeetingParticipantItemComponent;
protected participantItemTemplate = computed(() => this._participantItem?.template);
/**
* Controls whether to show lobby (true) or meeting view (false)
*/
/** Controls whether to show lobby (true) or meeting view (false) */
showLobby = true;
isLobbyReady = false;
/**
* Controls whether to show the videoconference component
*/
protected isMeetingLeft = signal(false);
/** Controls whether to show the videoconference component */
isMeetingLeft = signal(false);
// Signals for meeting context data
roomName = this.lobbyService.roomName;
roomMemberToken = this.lobbyService.roomMemberToken;
e2eeKey = this.lobbyService.e2eeKeyValue;
localParticipant = this.meetingContextService.localParticipant;
features = this.roomFeatureService.features;
hasRecordings = this.meetingContextService.hasRecordings;
protected features: Signal<RoomFeatures>;
protected roomMemberContextService = inject(RoomMemberContextService);
protected roomFeatureService = inject(RoomFeatureService);
protected ovThemeService = inject(OpenViduThemeService);
protected configService = inject(GlobalConfigService);
protected notificationService = inject(NotificationService);
protected lobbyService = inject(MeetingLobbyService);
protected meetingContextService = inject(MeetingContextService);
protected eventHandlerService = inject(MeetingEventHandlerService);
protected captionsService = inject(MeetingCaptionsService);
protected soundService = inject(SoundService);
protected runtimeConfigService = inject(RuntimeConfigService);
protected destroy$ = new Subject<void>();
// === LOBBY PHASE COMPUTED SIGNALS (when showLobby = true) ===
protected participantName = computed(() => this.lobbyService.participantName());
protected e2eeKey = computed(() => this.lobbyService.e2eeKeyValue());
protected roomName = computed(() => this.lobbyService.roomName());
protected roomMemberToken = computed(() => this.lobbyService.roomMemberToken());
// === MEETING PHASE COMPUTED SIGNALS (when showLobby = false) ===
// These read from MeetingContextService (Single Source of Truth during meeting)
protected localParticipant = computed(() => this.meetingContextService.localParticipant());
protected remoteParticipants = computed(() => this.meetingContextService.remoteParticipants());
protected hasRemoteParticipants = computed(() => this.remoteParticipants().length > 0);
protected participantsVersion = computed(() => this.meetingContextService.participantsVersion());
// === SHARED COMPUTED SIGNALS (used in both phases) ===
// Both lobby and meeting need these, so we read from MeetingContextService (Single Source of Truth)
protected roomId = computed(() => this.meetingContextService.roomId());
protected roomSecret = computed(() => this.meetingContextService.roomSecret());
protected hasRecordings = computed(() => this.meetingContextService.hasRecordings());
constructor() {
this.features = this.roomFeatureService.features;
// Change theme variables when custom theme is enabled
effect(() => {
if (this.features().hasCustomTheme) {
@ -156,28 +130,8 @@ export class MeetingComponent implements OnInit {
this.captionsService.destroy();
}
// async onRoomConnected() {
// try {
// // Suscribirse solo para actualizar el estado de video pin
// // Los participantes se actualizan automáticamente en MeetingContextService
// combineLatest([
// this.ovComponentsParticipantService.remoteParticipants$,
// this.ovComponentsParticipantService.localParticipant$
// ])
// .pipe(takeUntil(this.destroy$))
// .subscribe(() => {
// this.updateVideoPinState();
// });
// } catch (error) {
// console.error('Error accessing meeting:', error);
// }
// }
onRoomCreated(lkRoom: Room) {
// At this point, user has joined the meeting and MeetingContextService becomes the Single Source of Truth
// MeetingContextService has been updated during lobby initialization with roomId, roomSecret, hasRecordings
// All subsequent updates (hasRecordings, roomSecret, participants) go to MeetingContextService
// Store LiveKit room in context
this.meetingContextService.setLkRoom(lkRoom);
@ -193,29 +147,15 @@ export class MeetingComponent implements OnInit {
this.eventHandlerService.setupRoomListeners(lkRoom);
}
// async leaveMeeting() {
// await this.openviduService.disconnectRoom();
// }
// async endMeeting() {
// if (!this.participantService.isModerator()) return;
// this.meetingContextService.setMeetingEndedBy('self');
// try {
// await this.meetingService.endMeeting(this.roomId()!);
// } catch (error) {
// console.error('Error ending meeting:', error);
// }
// }
async onViewRecordingsClicked() {
const basePath = this.runtimeConfigService.basePath;
const basePathForUrl = basePath.endsWith('/') ? basePath.slice(0, -1) : basePath;
let recordingsUrl = `${basePathForUrl}/room/${this.roomId()}/recordings`;
const roomId = this.meetingContextService.roomId();
let recordingsUrl = `${basePathForUrl}/room/${roomId}/recordings`;
// Append room secret as query param if it exists
const secret = this.roomSecret();
const secret = this.meetingContextService.roomSecret();
if (secret) {
recordingsUrl += `?secret=${secret}`;
}
@ -232,27 +172,8 @@ export class MeetingComponent implements OnInit {
/**
* Handles the participant left event and hides the videoconference component
*/
protected onParticipantLeft(event: any): void {
onParticipantLeft(event: any): void {
this.isMeetingLeft.set(true);
this.eventHandlerService.onParticipantLeft(event);
}
/**
* Centralized logic for managing video pinning based on
* remote participants and local screen sharing state.
*/
protected updateVideoPinState(): void {
const localParticipant = this.localParticipant();
if (!localParticipant) return;
const isSharing = localParticipant.isScreenShareEnabled;
if (this.hasRemoteParticipants() && isSharing) {
// Pin the local screen share to appear bigger
localParticipant.setVideoPinnedBySource(Track.Source.ScreenShare, true);
} else {
// Unpin everything if no remote participants or not sharing
localParticipant.setAllVideoPinned(false);
}
}
}

View File

@ -18,9 +18,9 @@ import { CustomParticipantModel } from '../models/custom-participant.model';
providedIn: 'root'
})
export class MeetingCaptionsService {
private readonly participantService = inject(ParticipantService);
private readonly loggerService = inject(LoggerService);
private readonly logger: ILogger;
private readonly participantService = inject(ParticipantService);
// Configuration with defaults
private readonly defaultConfig: Required<CaptionsConfig> = {

View File

@ -28,7 +28,6 @@ export class MeetingContextService {
private readonly _hasRecordings = signal<boolean>(false);
private readonly _meetingEndedBy = signal<'self' | 'other' | null>(null);
private readonly _lkRoom = signal<Room | undefined>(undefined);
private readonly _participantsVersion = signal<number>(0);
private readonly _localParticipant = signal<CustomParticipantModel | undefined>(undefined);
private readonly _remoteParticipants = signal<CustomParticipantModel[]>([]);
@ -53,11 +52,6 @@ export class MeetingContextService {
/** Readonly signal for the current LiveKit room */
readonly lkRoom = this._lkRoom.asReadonly();
/**
* Readonly signal for participants version (increments on role changes)
* Used to trigger reactivity when participant properties change without array reference changes
*/
readonly participantsVersion = this._participantsVersion.asReadonly();
/** Readonly signal for the local participant */
readonly localParticipant = this._localParticipant.asReadonly();
/** Readonly signal for the remote participants */
@ -172,14 +166,6 @@ export class MeetingContextService {
this._lkRoom.set(room);
}
/**
* Increments the participants version counter
* Used to trigger reactivity when participant properties (like role) change
*/
incrementParticipantsVersion(): void {
this._participantsVersion.update((v) => v + 1);
}
/**
* Synchronizes participants from OpenVidu Components ParticipantService using signals.
* Effects are automatically cleaned up when the service is destroyed.
@ -211,7 +197,6 @@ export class MeetingContextService {
this._roomSecret.set(undefined);
this._hasRecordings.set(false);
this._meetingEndedBy.set(null);
this._participantsVersion.set(0);
this._localParticipant.set(undefined);
this._remoteParticipants.set([]);
}

View File

@ -59,23 +59,11 @@ export class MeetingEventHandlerService {
* @param room The LiveKit Room instance
*/
setupRoomListeners(room: Room): void {
this.setupDataReceivedListener(room);
}
/**
* Sets up the DataReceived event listener for handling room signals
* @param room The LiveKit Room instance
*/
private setupDataReceivedListener(room: Room): void {
room.on(
RoomEvent.DataReceived,
async (payload: Uint8Array, _participant?: RemoteParticipant, _kind?: DataPacket_Kind, topic?: string) => {
// Only process topics that this handler is responsible for
const relevantTopics = [
'recordingStopped',
MeetSignalType.MEET_ROOM_CONFIG_UPDATED,
MeetSignalType.MEET_PARTICIPANT_ROLE_UPDATED
];
const relevantTopics = ['recordingStopped', MeetSignalType.MEET_PARTICIPANT_ROLE_UPDATED];
if (!topic || !relevantTopics.includes(topic)) {
return;
@ -90,14 +78,10 @@ export class MeetingEventHandlerService {
this.meetingContext.setHasRecordings(true);
break;
case MeetSignalType.MEET_ROOM_CONFIG_UPDATED:
// Room cannot be updated if a meeting is ongoing
// await this.handleRoomConfigUpdated(event);
break;
case MeetSignalType.MEET_PARTICIPANT_ROLE_UPDATED:
await this.handleParticipantRoleUpdated(event);
this.showParticipantRoleUpdatedNotification(event);
const roleUpdateEvent = event as MeetParticipantRoleUpdatedPayload;
await this.handleParticipantRoleUpdated(roleUpdateEvent);
this.showParticipantRoleUpdatedNotification(roleUpdateEvent.newRole);
break;
}
} catch (error) {
@ -111,8 +95,6 @@ export class MeetingEventHandlerService {
* Handles participant connected event.
* Sends JOINED event to parent window (for web component integration).
*
* Arrow function ensures correct 'this' binding when called from template.
*
* @param event Participant model from OpenVidu
*/
onParticipantConnected = (event: ParticipantModel): void => {
@ -130,19 +112,17 @@ export class MeetingEventHandlerService {
* Handles participant left event.
* - Maps technical reason to user-friendly reason
* - Sends LEFT event to parent window
* - Cleans up session storage (secrets, tokens)
* - Clears participant identity and token from RoomMemberContextService
* - Navigates to disconnected page
*
* Arrow function ensures correct 'this' binding when called from template.
*
* @param event Participant left event from OpenVidu
*/
onParticipantLeft = async (event: ParticipantLeftEvent): Promise<void> => {
let leftReason = this.mapLeftReason(event.reason);
// If meeting was ended by this user, update reason
const meetingEndedBy = this.meetingContext.meetingEndedBy();
if (leftReason === LeftEventReason.MEETING_ENDED && meetingEndedBy === 'self') {
// If meeting was ended by local user, update reason
const meetingEndedBySelf = this.meetingContext.meetingEndedBy() === 'self';
if (leftReason === LeftEventReason.MEETING_ENDED && meetingEndedBySelf) {
leftReason = LeftEventReason.MEETING_ENDED_BY_SELF;
}
@ -167,8 +147,6 @@ export class MeetingEventHandlerService {
/**
* Handles recording start request event.
*
* Arrow function ensures correct 'this' binding when called from template.
*
* @param event Recording start requested event from OpenVidu
*/
onRecordingStartRequested = async (event: RecordingStartRequestedEvent): Promise<void> => {
@ -189,8 +167,6 @@ export class MeetingEventHandlerService {
/**
* Handles recording stop request event.
*
* Arrow function ensures correct 'this' binding when called from template.
*
* @param event Recording stop requested event from OpenVidu
*/
onRecordingStopRequested = async (event: RecordingStopRequestedEvent): Promise<void> => {
@ -205,42 +181,6 @@ export class MeetingEventHandlerService {
// PRIVATE METHODS - Event Handlers
// ============================================
/**
* Handles room config updated event.
* Updates feature config and refreshes room member token if needed.
* Obtains roomId and roomSecret from MeetingContextService.
*/
// private async handleRoomConfigUpdated(event: MeetRoomConfigUpdatedPayload): Promise<void> {
// const { config } = event;
// // Update feature configuration
// this.featureConfService.setRoomConfig(config);
// // Refresh room member token if recording is enabled
// if (config.recording.enabled) {
// try {
// const roomId = this.meetingContext.roomId();
// const roomSecret = this.meetingContext.roomSecret();
// const participantName = this.roomMemberService.getParticipantName();
// const participantIdentity = this.roomMemberService.getParticipantIdentity();
// if (!roomId || !roomSecret) {
// console.error('Room ID or secret not available for token refresh');
// return;
// }
// await this.roomMemberService.generateToken(roomId, {
// secret: roomSecret,
// grantJoinMeetingPermission: true,
// participantName,
// participantIdentity
// });
// } catch (error) {
// console.error('Error refreshing room member token:', error);
// }
// }
// }
/**
* Handles participant role updated event.
* Updates local or remote participant role and refreshes room member token if needed.
@ -256,9 +196,8 @@ export class MeetingEventHandlerService {
if (local && participantIdentity === local.identity) {
if (!secret || !roomId) return;
// Update room secret in context
// Update room secret in context (without updating session storage)
this.meetingContext.setRoomSecret(secret);
this.sessionStorageService.setRoomSecret(secret);
try {
// Refresh participant token with new role
@ -272,9 +211,6 @@ export class MeetingEventHandlerService {
// Update local participant role
local.meetRole = newRole;
console.log(`You have been assigned the role of ${newRole}`);
// Increment version to trigger reactivity
this.meetingContext.incrementParticipantsVersion();
} catch (error) {
console.error('Error refreshing room member token:', error);
}
@ -284,15 +220,11 @@ export class MeetingEventHandlerService {
const participant = remoteParticipants.find((p) => p.identity === participantIdentity);
if (participant) {
participant.meetRole = newRole;
// Increment version to trigger reactivity
this.meetingContext.incrementParticipantsVersion();
}
}
}
private showParticipantRoleUpdatedNotification(event: MeetParticipantRoleUpdatedPayload): void {
const { newRole } = event as MeetParticipantRoleUpdatedPayload;
private showParticipantRoleUpdatedNotification(newRole: MeetRoomMemberRole): void {
this.notificationService.showSnackbar(`You have been assigned the role of ${newRole.toUpperCase()}`);
newRole === MeetRoomMemberRole.MODERATOR
? this.soundService.playParticipantRoleUpgradedSound()

View File

@ -20,20 +20,19 @@ import { MeetingService } from './meeting.service';
providedIn: 'root'
})
export class MeetingWebComponentManagerService {
protected meetingService = inject(MeetingService);
protected meetingContextService = inject(MeetingContextService);
protected roomMemberContextService = inject(RoomMemberContextService);
protected appCtxService = inject(AppContextService);
protected openviduService = inject(OpenViduService);
protected loggerService = inject(LoggerService);
protected log = this.loggerService.get('OpenVidu Meet - WebComponentManagerService');
protected isInitialized = false;
protected parentDomain: string = '';
protected boundHandleMessage: (event: MessageEvent) => Promise<void>;
protected log;
protected readonly meetingContextService = inject(MeetingContextService);
protected readonly roomMemberContextService = inject(RoomMemberContextService);
protected readonly openviduService = inject(OpenViduService);
protected readonly meetingService = inject(MeetingService);
protected readonly loggerService = inject(LoggerService);
protected readonly appCtxService = inject(AppContextService);
constructor() {
this.log = this.loggerService.get('OpenVidu Meet - WebComponentManagerService');
this.boundHandleMessage = this.handleMessage.bind(this);
effect(() => {
if (this.appCtxService.isEmbeddedMode()) {
@ -95,6 +94,7 @@ export class MeetingWebComponentManagerService {
const message: WebComponentInboundCommandMessage = event.data;
const { command, payload } = message;
// If parent domain is not set, only accept INITIALIZE command to set the parent domain
if (!this.parentDomain) {
if (command === WebComponentCommand.INITIALIZE) {
if (!payload || !('domain' in payload)) {
@ -107,6 +107,7 @@ export class MeetingWebComponentManagerService {
return;
}
// For security, only accept messages from the parent domain
if (event.origin !== this.parentDomain) {
console.warn(`Untrusted origin: ${event.origin}`);
return;

View File

@ -13,16 +13,12 @@ export class AppContextService {
private readonly _edition: WritableSignal<Edition> = signal(Edition.CE);
private readonly _version: WritableSignal<string> = signal('');
readonly mode = computed(() => this._mode());
readonly edition = computed(() => this._edition());
readonly version = computed(() => this._version());
readonly mode = this._mode.asReadonly();
readonly edition = this._edition.asReadonly();
readonly version = this._version.asReadonly();
readonly isEmbeddedMode = computed(() => this._mode() === ApplicationMode.EMBEDDED);
readonly isStandaloneMode = computed(() => this._mode() === ApplicationMode.STANDALONE);
readonly appData = computed(() => ({
mode: this._mode(),
edition: this._edition(),
version: this._version()
}));
constructor() {
this.detectMode();