Merge branch 'main' into feat/room-members-users
This commit is contained in:
commit
5ca46e59d8
@ -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",
|
||||
|
||||
@ -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';
|
||||
|
||||
|
||||
@ -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) =>
|
||||
|
||||
@ -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);
|
||||
}
|
||||
|
||||
|
||||
@ -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();
|
||||
}
|
||||
@ -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.
|
||||
|
||||
@ -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.
|
||||
|
||||
@ -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) {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user