frontend: implement NavigationService and RecordingManagerService with error handling and navigation methods

This commit is contained in:
Carlos Santos 2025-06-09 13:41:17 +02:00
parent b2f1e2194a
commit 2aa3bc1177
8 changed files with 167 additions and 43 deletions

View File

@ -0,0 +1,5 @@
export interface ErrorRedirectReason {
'invalid-secret': string;
'invalid-room': string;
'internal-error': string;
}

View File

@ -1,11 +1,11 @@
import { Location } from '@angular/common';
// import { Location } from '@angular/common';
import { Component, OnDestroy, OnInit } from '@angular/core';
import { FormControl, FormGroup, FormsModule, ReactiveFormsModule, Validators } from '@angular/forms';
import { MatButtonModule } from '@angular/material/button';
import { MatCardModule } from '@angular/material/card';
import { MatFormFieldModule } from '@angular/material/form-field';
import { MatInputModule } from '@angular/material/input';
import { ActivatedRoute, Router } from '@angular/router';
import { ActivatedRoute } from '@angular/router';
import { MeetRecordingAccess, MeetRoomPreferences, OpenViduMeetPermissions, ParticipantRole } from '@lib/typings/ce';
import {
ApiDirectiveModule,
@ -27,6 +27,8 @@ import {
WebComponentManagerService
} from '../../services';
import { ParticipantTokenService } from '@lib/services/participant-token/participant-token.service';
import { RecordingManagerService } from '@lib/services/recording-manager/recording-manager.service';
import { NavigationService } from '@lib/services/navigation/navigation.service';
@Component({
selector: 'app-video-room',
@ -84,10 +86,10 @@ export class VideoRoomComponent implements OnInit, OnDestroy {
constructor(
protected httpService: HttpService,
protected navigationService: NavigationService,
protected participantTokenService: ParticipantTokenService,
protected router: Router,
protected recManagerService: RecordingManagerService,
protected route: ActivatedRoute,
protected location: Location,
protected authService: AuthService,
protected ctxService: ContextService,
protected roomService: RoomService,
@ -122,7 +124,6 @@ export class VideoRoomComponent implements OnInit, OnDestroy {
await this.generateParticipantToken();
await this.replaceUrlQueryParams();
await this.loadRoomPreferences();
this.updateFeatureConfiguration();
this.showRoom = true;
} catch (error) {
console.error('Error accessing room:', error);
@ -169,7 +170,8 @@ export class VideoRoomComponent implements OnInit, OnDestroy {
this.participantName,
this.roomSecret
);
this.participantToken = token;
// The components library needs the token to be set in the 'onTokenRequested' method
// this.participantToken = token;
this.participantRole = role;
this.participantPermissions = permissions;
} catch (error: any) {
@ -177,11 +179,11 @@ export class VideoRoomComponent implements OnInit, OnDestroy {
switch (error.status) {
case 400:
// Invalid secret
this.redirectToErrorPage('invalid-secret');
await this.navigationService.redirectToErrorPage('invalid-secret');
break;
case 404:
// Room not found
this.redirectToErrorPage('invalid-room');
await this.navigationService.redirectToErrorPage('invalid-room');
break;
case 409:
// Participant already exists.
@ -189,7 +191,7 @@ export class VideoRoomComponent implements OnInit, OnDestroy {
this.participantForm.get('name')?.setErrors({ participantExists: true });
throw new Error('Participant already exists in the room');
default:
this.redirectToErrorPage('internal-error');
await this.navigationService.redirectToErrorPage('internal-error');
}
}
}
@ -210,14 +212,10 @@ export class VideoRoomComponent implements OnInit, OnDestroy {
}
// Replace secret and participant name in the URL query parameters
const queryParams = {
...this.route.snapshot.queryParams,
this.navigationService.updateUrlQueryParams(this.route, {
secret: secretQueryParam,
'participant-name': this.participantName
};
const urlTree = this.router.createUrlTree([], { queryParams, queryParamsHandling: 'merge' });
const newUrl = this.router.serializeUrl(urlTree);
this.location.replaceState(newUrl);
});
}
private async getRoomSecrets(): Promise<{ moderatorSecret: string; publisherSecret: string }> {
@ -230,10 +228,8 @@ export class VideoRoomComponent implements OnInit, OnDestroy {
return { publisherSecret, moderatorSecret };
}
goToRecordings() {
this.router.navigate([`room/${this.roomId}/recordings`], {
queryParams: { secret: this.roomSecret }
});
async goToRecordings() {
await this.navigationService.goToRecordings(this.roomId, this.roomSecret);
}
onParticipantConnected(event: ParticipantModel) {
@ -247,7 +243,7 @@ export class VideoRoomComponent implements OnInit, OnDestroy {
this.wcManagerService.sendMessageToParent(message);
}
onParticipantLeft(event: ParticipantLeftEvent) {
async onParticipantLeft(event: ParticipantLeftEvent) {
console.warn('Participant left the room. Redirecting to:');
const redirectURL = this.ctxService.getLeaveRedirectURL() || '/disconnected';
const isExternalURL = /^https?:\/\//.test(redirectURL);
@ -279,13 +275,12 @@ export class VideoRoomComponent implements OnInit, OnDestroy {
this.sessionStorageService.removeModeratorSecret(event.roomName);
}
this.redirectTo(redirectURL, isExternalURL);
await this.navigationService.redirectTo(redirectURL, isExternalURL);
}
async onRecordingStartRequested(event: RecordingStartRequestedEvent) {
try {
const { roomName: roomId } = event;
await this.httpService.startRecording(roomId);
await this.recManagerService.startRecording(event.roomName);
} catch (error) {
console.error(error);
}
@ -293,11 +288,7 @@ export class VideoRoomComponent implements OnInit, OnDestroy {
async onRecordingStopRequested(event: RecordingStopRequestedEvent) {
try {
const { recordingId } = event;
if (!recordingId) throw new Error('Recording ID not found when stopping recording');
await this.httpService.stopRecording(recordingId);
await this.recManagerService.stopRecording(event.recordingId);
} catch (error) {
console.error(error);
}
@ -335,18 +326,4 @@ export class VideoRoomComponent implements OnInit, OnDestroy {
this.featureFlags.showRecording = this.participantPermissions.canRecord;
}
}
private redirectTo(url: string, isExternal: boolean) {
if (isExternal) {
console.log('Redirecting to external URL:', url);
window.location.href = url;
} else {
console.log('Redirecting to internal route:', url);
this.router.navigate([url], { replaceUrl: true });
}
}
private redirectToErrorPage(reason: string) {
this.router.navigate(['error'], { queryParams: { reason } });
}
}

View File

@ -131,7 +131,7 @@ export class ContextService {
this.context.participantRole = decodedToken.metadata.role;
// Update feature configuration based on the new token
this.updateFeatureConfiguration();
// this.updateFeatureConfiguration();
} catch (error: any) {
this.log.e('Error setting token in context', error);
throw new Error('Error setting token', error);

View File

@ -7,3 +7,5 @@ export * from './notification/notification.service';
export * from './webcomponent-manager/webcomponent-manager.service';
export * from './session-storage/session-storage.service';
export * from './participant-token/participant-token.service';
export * from './recording-manager/recording-manager.service';
export * from './navigation/navigation.service';

View File

@ -0,0 +1,16 @@
import { TestBed } from '@angular/core/testing';
import { NavigationService } from './navigation.service';
describe('NavigationService', () => {
let service: NavigationService;
beforeEach(() => {
TestBed.configureTestingModule({});
service = TestBed.inject(NavigationService);
});
it('should be created', () => {
expect(service).toBeTruthy();
});
});

View File

@ -0,0 +1,71 @@
import { Injectable } from '@angular/core';
import { Location } from '@angular/common';
import { ActivatedRoute, Router } from '@angular/router';
import { ErrorRedirectReason } from '@lib/models/navigation.model';
@Injectable({
providedIn: 'root'
})
export class NavigationService {
constructor(
private router: Router,
private location: Location
) {}
/**
* Redirects to internal or external URLs
*/
async redirectTo(url: string, isExternal: boolean = false): Promise<void> {
if (isExternal) {
console.log('Redirecting to external URL:', url);
window.location.href = url;
} else {
console.log('Redirecting to internal route:', url);
try {
await this.router.navigate([url], { replaceUrl: true });
} catch (error) {
console.error('Error navigating to internal route:', error);
}
}
}
/**
* Redirects to error page with specific reason
*/
async redirectToErrorPage(reason: keyof ErrorRedirectReason): Promise<void> {
try {
await this.router.navigate(['error'], { queryParams: { reason } });
} catch (error) {
console.error('Error redirecting to error page:', error);
}
}
/**
* Updates URL query parameters without navigation
*/
updateUrlQueryParams(route: ActivatedRoute, newParams: Record<string, any>): void {
const queryParams = {
...route.snapshot.queryParams,
...newParams
};
const urlTree = this.router.createUrlTree([], {
queryParams,
queryParamsHandling: 'merge'
});
const newUrl = this.router.serializeUrl(urlTree);
this.location.replaceState(newUrl);
}
/**
* Navigates to recordings page
*/
async goToRecordings(roomId: string, secret: string): Promise<void> {
try {
await this.router.navigate([`room/${roomId}/recordings`], {
queryParams: { secret }
});
} catch (error) {
console.error('Error navigating to recordings:', error);
}
}
}

View File

@ -0,0 +1,16 @@
import { TestBed } from '@angular/core/testing';
import { RecordingManagerService } from './recording-manager.service';
describe('RecordingManagerService', () => {
let service: RecordingManagerService;
beforeEach(() => {
TestBed.configureTestingModule({});
service = TestBed.inject(RecordingManagerService);
});
it('should be created', () => {
expect(service).toBeTruthy();
});
});

View File

@ -0,0 +1,37 @@
import { Injectable } from '@angular/core';
import { HttpService } from '../http/http.service';
@Injectable({
providedIn: 'root'
})
export class RecordingManagerService {
constructor(private httpService: HttpService) {}
/**
* Starts recording for a room
*/
async startRecording(roomId: string): Promise<void> {
try {
await this.httpService.startRecording(roomId);
} catch (error) {
console.error('Error starting recording:', error);
throw error;
}
}
/**
* Stops recording by recording ID
*/
async stopRecording(recordingId: string | undefined): Promise<void> {
if (!recordingId) {
throw new Error('Recording ID not found when stopping recording');
}
try {
await this.httpService.stopRecording(recordingId);
} catch (error) {
console.error('Error stopping recording:', error);
throw error;
}
}
}