From 59d722f882cebaf8586d7ad6056899e6a6b21b5b Mon Sep 17 00:00:00 2001 From: CSantosM <4a.santos@gmail.com> Date: Wed, 4 Feb 2026 15:20:12 +0100 Subject: [PATCH 1/4] backend: enhance recording start process sending FAILED event to client when an error occurs --- meet-ce/backend/src/services/recording.service.ts | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/meet-ce/backend/src/services/recording.service.ts b/meet-ce/backend/src/services/recording.service.ts index 1dd03ff9..5d8bd480 100644 --- a/meet-ce/backend/src/services/recording.service.ts +++ b/meet-ce/backend/src/services/recording.service.ts @@ -85,19 +85,21 @@ export class RecordingService { status: MeetRecordingStatus.STARTING }); + // Promise that rejects after timeout const timeoutPromise = new Promise((_, reject) => { timeoutId = setTimeout(() => { if (isOperationCompleted) return; isOperationCompleted = true; - //Clean up the event listener and timeout + // Clean up the event listener and timeout this.systemEventService.off(DistributedEventType.RECORDING_ACTIVE, eventListener); this.handleRecordingTimeout(recordingId, roomId).catch(() => {}); reject(errorRecordingStartTimeout(roomId)); }, ms(INTERNAL_CONFIG.RECORDING_STARTED_TIMEOUT)); }); + // Promise that resolves when RECORDING_ACTIVE event is received const activeEgressEventPromise = new Promise((resolve) => { eventListener = (info: Record) => { // Process the event only if it belongs to the current room. @@ -114,6 +116,7 @@ export class RecordingService { this.systemEventService.on(DistributedEventType.RECORDING_ACTIVE, eventListener); }); + // Promise that starts the recording process const startRecordingPromise = (async (): Promise => { try { const options = this.generateCompositeOptionsFromRequest(room.config, configOverride); @@ -144,6 +147,16 @@ export class RecordingService { } catch (error) { if (isOperationCompleted) { this.logger.warn(`startRoomComposite failed after timeout: ${error}`); + + // Manually send the recording FAILED signal to OpenVidu Components for avoiding missing event + await this.frontendEventService.sendRecordingSignalToOpenViduComponents(roomId, { + recordingId, + roomId, + roomName: roomId, + status: MeetRecordingStatus.FAILED, + error: (error as Error).message + }); + throw errorRecordingStartTimeout(roomId); } From 1046b5a0dd38daaab928ae7181a212ad4d0af768 Mon Sep 17 00:00:00 2001 From: CSantosM <4a.santos@gmail.com> Date: Thu, 5 Feb 2026 12:40:49 +0100 Subject: [PATCH 2/4] backend: enhance build script to generate API documentation after compilation --- meet-ce/backend/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/meet-ce/backend/package.json b/meet-ce/backend/package.json index 97753f7c..9b541166 100644 --- a/meet-ce/backend/package.json +++ b/meet-ce/backend/package.json @@ -27,7 +27,7 @@ "package.json" ], "scripts": { - "build": "tsc -p tsconfig.prod.json", + "build": "tsc -p tsconfig.prod.json && pnpm run doc:api", "build:watch": "tsc -p tsconfig.prod.json --watch", "doc:api": "mkdir -p public/openapi && cd openapi && openapi-generate-html -i openvidu-meet-api.yaml --ui=stoplight --theme=light --title 'OpenVidu Meet REST API' --description 'OpenVidu Meet REST API' -o ../public/openapi/public.html", "doc:internal-api": "mkdir -p public/openapi && cd openapi && openapi-generate-html -i openvidu-meet-internal-api.yaml --ui=stoplight --theme=dark --title 'OpenVidu Meet Internal REST API' --description 'OpenVidu Meet Internal REST API' -o ../public/openapi/internal.html", From 366632741cd76c457fa67c95a886b789e42a67b9 Mon Sep 17 00:00:00 2001 From: cruizba Date: Thu, 5 Feb 2026 19:39:48 +0100 Subject: [PATCH 3/4] fix: strip basePath prefix in redirectTo method Strip basePath prefix if present, since Angular router operates relative to --- .../src/lib/shared/services/navigation.service.ts | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/meet-ce/frontend/projects/shared-meet-components/src/lib/shared/services/navigation.service.ts b/meet-ce/frontend/projects/shared-meet-components/src/lib/shared/services/navigation.service.ts index 8e60bf76..7afee202 100644 --- a/meet-ce/frontend/projects/shared-meet-components/src/lib/shared/services/navigation.service.ts +++ b/meet-ce/frontend/projects/shared-meet-components/src/lib/shared/services/navigation.service.ts @@ -1,6 +1,7 @@ import { Injectable } from '@angular/core'; import { Params, Router, UrlTree } from '@angular/router'; import { NavigationErrorReason } from '../models/navigation.model'; +import { AppConfigService } from './app-config.service'; import { AppDataService } from './app-data.service'; import { SessionStorageService } from './session-storage.service'; @@ -13,7 +14,8 @@ export class NavigationService { constructor( private router: Router, private sessionStorageService: SessionStorageService, - private appDataService: AppDataService + private appDataService: AppDataService, + private appConfigService: AppConfigService ) {} setLeaveRedirectUrl(leaveRedirectUrl: string): void { @@ -80,6 +82,13 @@ export class NavigationService { */ async redirectTo(url: string): Promise { try { + // Strip basePath prefix if present, since Angular router operates relative to + const basePath = this.appConfigService.basePath; + const basePathPrefix = basePath.endsWith('/') ? basePath.slice(0, -1) : basePath; + if (basePathPrefix && url.startsWith(basePathPrefix)) { + url = url.slice(basePathPrefix.length) || '/'; + } + let urlTree = this.router.parseUrl(url); await this.router.navigateByUrl(urlTree, { replaceUrl: true }); } catch (error) { From a853aa02a25b087dbfdb1b20ce6ed15dc90eb0e0 Mon Sep 17 00:00:00 2001 From: cruizba Date: Wed, 11 Feb 2026 19:13:00 +0100 Subject: [PATCH 4/4] backend: apply dynamic base path to OpenAPI docs server URLs When deployed under a base path (e.g. /meet), the Stoplight "Try It" requests were hitting /api/v1 instead of /meet/api/v1. This applies the base path to the embedded OpenAPI spec's servers array at serve time, following the same pattern used for the frontend index.html. Also renames html-injection.utils to html-dynamic-base-path.utils and updates function names for better wording. --- .../src/repositories/room.repository.ts | 2 +- meet-ce/backend/src/server.ts | 8 ++-- ...ils.ts => html-dynamic-base-path.utils.ts} | 42 +++++++++++++++++-- meet-ce/backend/src/utils/url.utils.ts | 2 +- .../backend/tests/helpers/request-helpers.ts | 2 +- 5 files changed, 46 insertions(+), 10 deletions(-) rename meet-ce/backend/src/utils/{html-injection.utils.ts => html-dynamic-base-path.utils.ts} (67%) diff --git a/meet-ce/backend/src/repositories/room.repository.ts b/meet-ce/backend/src/repositories/room.repository.ts index 7d828bb0..b8ee452c 100644 --- a/meet-ce/backend/src/repositories/room.repository.ts +++ b/meet-ce/backend/src/repositories/room.repository.ts @@ -2,7 +2,7 @@ import { MeetRoom, MeetRoomFilters, MeetRoomStatus } from '@openvidu-meet/typing import { inject, injectable } from 'inversify'; import { MeetRoomDocument, MeetRoomModel } from '../models/mongoose-schemas/room.schema.js'; import { LoggerService } from '../services/logger.service.js'; -import { getBasePath } from '../utils/html-injection.utils.js'; +import { getBasePath } from '../utils/html-dynamic-base-path.utils.js'; import { getBaseUrl } from '../utils/url.utils.js'; import { BaseRepository } from './base.repository.js'; diff --git a/meet-ce/backend/src/server.ts b/meet-ce/backend/src/server.ts index a3a62f31..5e6f2da5 100644 --- a/meet-ce/backend/src/server.ts +++ b/meet-ce/backend/src/server.ts @@ -17,7 +17,7 @@ import { internalMeetingRouter } from './routes/meeting.routes.js'; import { recordingRouter } from './routes/recording.routes.js'; import { internalRoomRouter, roomRouter } from './routes/room.routes.js'; import { userRouter } from './routes/user.routes.js'; -import { getBasePath, getInjectedHtml } from './utils/html-injection.utils.js'; +import { getBasePath, getHtmlWithBasePath, getOpenApiHtmlWithBasePath } from './utils/html-dynamic-base-path.utils.js'; import { frontendDirectoryPath, frontendHtmlPath, @@ -76,7 +76,7 @@ const createApp = () => { // Public API routes appRouter.use(`${INTERNAL_CONFIG.API_BASE_PATH_V1}/docs`, (_req: Request, res: Response) => - res.sendFile(publicApiHtmlFilePath) + res.type('html').send(getOpenApiHtmlWithBasePath(publicApiHtmlFilePath, INTERNAL_CONFIG.API_BASE_PATH_V1)) ); appRouter.use(`${INTERNAL_CONFIG.API_BASE_PATH_V1}/rooms`, /*mediaTypeValidatorMiddleware,*/ roomRouter); appRouter.use(`${INTERNAL_CONFIG.API_BASE_PATH_V1}/recordings`, /*mediaTypeValidatorMiddleware,*/ recordingRouter); @@ -85,7 +85,7 @@ const createApp = () => { if (process.env.NODE_ENV === 'development') { // Serve internal API docs only in development mode appRouter.use(`${INTERNAL_CONFIG.INTERNAL_API_BASE_PATH_V1}/docs`, (_req: Request, res: Response) => - res.sendFile(internalApiHtmlFilePath) + res.type('html').send(getOpenApiHtmlWithBasePath(internalApiHtmlFilePath, INTERNAL_CONFIG.INTERNAL_API_BASE_PATH_V1)) ); } @@ -104,7 +104,7 @@ const createApp = () => { appRouter.get('/v1/openvidu-meet.js', (_req: Request, res: Response) => res.sendFile(webcomponentBundlePath)); // Serve OpenVidu Meet index.html file for all non-API routes (with dynamic base path injection) appRouter.get(/^(?!.*\/(api|internal-api)\/).*$/, (_req: Request, res: Response) => { - res.type('html').send(getInjectedHtml(frontendHtmlPath)); + res.type('html').send(getHtmlWithBasePath(frontendHtmlPath)); }); // Catch all other routes and return 404 appRouter.use((_req: Request, res: Response) => diff --git a/meet-ce/backend/src/utils/html-injection.utils.ts b/meet-ce/backend/src/utils/html-dynamic-base-path.utils.ts similarity index 67% rename from meet-ce/backend/src/utils/html-injection.utils.ts rename to meet-ce/backend/src/utils/html-dynamic-base-path.utils.ts index 200c1489..1bbbe722 100644 --- a/meet-ce/backend/src/utils/html-injection.utils.ts +++ b/meet-ce/backend/src/utils/html-dynamic-base-path.utils.ts @@ -3,6 +3,7 @@ import fs from 'fs'; import { MEET_ENV } from '../environment.js'; let cachedHtml: string | null = null; +const cachedOpenApiHtml = new Map(); let configValidated = false; /** @@ -73,14 +74,14 @@ export function getBasePath(): string { } /** - * Injects runtime configuration into the index.html + * Applies runtime base path configuration to the index.html * - Replaces the tag with the configured base path - * - Injects a script with window.__OPENVIDU_MEET_CONFIG__ for frontend access + * - Adds a script with window.__OPENVIDU_MEET_CONFIG__ for frontend access * * @param htmlPath Path to the index.html file * @returns The modified HTML content */ -export function getInjectedHtml(htmlPath: string): string { +export function getHtmlWithBasePath(htmlPath: string): string { // In production, cache the result for performance if (process.env.NODE_ENV === 'production' && cachedHtml) { return cachedHtml; @@ -103,9 +104,44 @@ export function getInjectedHtml(htmlPath: string): string { return html; } +/** + * Applies the runtime base path to the OpenAPI documentation HTML. + * Replaces the servers URL in the embedded OpenAPI spec so that "Try It" requests + * use the correct path when deployed under a base path (e.g. /meet/api/v1). + * + * @param htmlPath Path to the OpenAPI HTML file + * @param apiBasePath The API base path (e.g. /api/v1 or /internal-api/v1) + * @returns The modified HTML content + */ +export function getOpenApiHtmlWithBasePath(htmlPath: string, apiBasePath: string): string { + if (process.env.NODE_ENV === 'production' && cachedOpenApiHtml.has(htmlPath)) { + return cachedOpenApiHtml.get(htmlPath)!; + } + + const basePath = getBasePath(); + // Build full server URL: strip trailing slash from basePath to avoid double slashes + const fullServerUrl = basePath.replace(/\/$/, '') + apiBasePath; + + let html = fs.readFileSync(htmlPath, 'utf-8'); + + // Replace the servers URL in the embedded OpenAPI JSON + // Matches "servers":[{"url":"" and replaces the URL with the full path + html = html.replace( + /("servers":\[\{"url":")[^"]*(")/, + `$1${fullServerUrl}$2` + ); + + if (process.env.NODE_ENV === 'production') { + cachedOpenApiHtml.set(htmlPath, html); + } + + return html; +} + /** * Clears the cached HTML (useful for testing or config changes) */ export function clearHtmlCache(): void { cachedHtml = null; + cachedOpenApiHtml.clear(); } diff --git a/meet-ce/backend/src/utils/url.utils.ts b/meet-ce/backend/src/utils/url.utils.ts index 1d0ac9cb..86ac3d25 100644 --- a/meet-ce/backend/src/utils/url.utils.ts +++ b/meet-ce/backend/src/utils/url.utils.ts @@ -1,7 +1,7 @@ import { container } from '../config/dependency-injector.config.js'; import { MEET_ENV } from '../environment.js'; import { BaseUrlService } from '../services/base-url.service.js'; -import { getBasePath } from './html-injection.utils.js'; +import { getBasePath } from './html-dynamic-base-path.utils.js'; /** * Returns the base URL for the application, including the configured base path. diff --git a/meet-ce/backend/tests/helpers/request-helpers.ts b/meet-ce/backend/tests/helpers/request-helpers.ts index 3f6106b8..93b1a003 100644 --- a/meet-ce/backend/tests/helpers/request-helpers.ts +++ b/meet-ce/backend/tests/helpers/request-helpers.ts @@ -31,7 +31,7 @@ import { ApiKeyService } from '../../src/services/api-key.service.js'; import { GlobalConfigService } from '../../src/services/global-config.service.js'; import { RecordingService } from '../../src/services/recording.service.js'; import { RoomScheduledTasksService } from '../../src/services/room-scheduled-tasks.service.js'; -import { getBasePath } from '../../src/utils/html-injection.utils.js'; +import { getBasePath } from '../../src/utils/html-dynamic-base-path.utils.js'; /** * Constructs the full API path by prepending the base path.