Merge branch 'main' into feat/room-members-users

This commit is contained in:
juancarmore 2026-02-13 12:10:00 +01:00
commit 5ca46e59d8
8 changed files with 71 additions and 13 deletions

View File

@ -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",

View File

@ -2,7 +2,7 @@ import { MeetRoom, MeetRoomField, MeetRoomFilters, MeetRoomStatus } from '@openv
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';

View File

@ -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) =>

View File

@ -97,19 +97,21 @@ export class RecordingService {
status: MeetRecordingStatus.STARTING
});
// Promise that rejects after timeout
const timeoutPromise = new Promise<never>((_, 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<MeetRecordingInfo>((resolve) => {
eventListener = (info: Record<string, unknown>) => {
// Process the event only if it belongs to the current room.
@ -126,6 +128,7 @@ export class RecordingService {
this.systemEventService.on(DistributedEventType.RECORDING_ACTIVE, eventListener);
});
// Promise that starts the recording process
const startRecordingPromise = (async (): Promise<MeetRecordingInfo> => {
try {
const options = this.generateCompositeOptionsFromRequest(room.config, configOverride);
@ -156,6 +159,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);
}

View File

@ -3,6 +3,7 @@ import fs from 'fs';
import { MEET_ENV } from '../environment.js';
let cachedHtml: string | null = null;
const cachedOpenApiHtml = new Map<string, string>();
let configValidated = false;
/**
@ -82,14 +83,14 @@ export function getBasePath(): string {
}
/**
* Injects runtime configuration into the index.html
* Applies runtime base path configuration to the index.html
* - Replaces the <base href="/"> 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;
@ -112,9 +113,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":"<any-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();
}

View File

@ -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.

View File

@ -33,7 +33,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.

View File

@ -2,6 +2,7 @@ import { Injectable } from '@angular/core';
import { Params, Router, UrlTree } from '@angular/router';
import { NavigationErrorReason } from '../models/navigation.model';
import { AppContextService } from './app-context.service';
import { RuntimeConfigService } from './runtime-config.service';
import { SessionStorageService } from './session-storage.service';
@Injectable({
@ -13,7 +14,8 @@ export class NavigationService {
constructor(
private router: Router,
private sessionStorageService: SessionStorageService,
private appCtxService: AppContextService
private appCtxService: AppContextService,
private runtimeConfigService: RuntimeConfigService
) {}
/**
@ -164,6 +166,13 @@ export class NavigationService {
*/
async redirectTo(url: string): Promise<void> {
try {
// Strip basePath prefix if present, since Angular router operates relative to <base href>
const basePath = this.runtimeConfigService.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) {