Files
EchoChat/media-server/src/app.ts
2026-05-18 19:42:55 +08:00

159 lines
4.4 KiB
TypeScript

import * as mediasoup from 'mediasoup';
import sensible from '@fastify/sensible';
import Fastify from 'fastify';
import { config } from './config.js';
import { registerErrorHandler } from './middlewares/error-handler.js';
import { internalAuthPlugin } from './middlewares/internal-auth.js';
import { closeWorker, getWorkerSnapshot, startWorker } from './mediasoup/worker.js';
import { consumerRoutes } from './routes/consumer.route.js';
import { producerRoutes } from './routes/producer.route.js';
import { recordingRoutes } from './routes/recording.route.js';
import { routerRoutes } from './routes/router.route.js';
import { transportRoutes } from './routes/transport.route.js';
import { getRouterStats } from './services/router.service.js';
import { logger } from './utils/logger.js';
export async function buildApp() {
const app = Fastify({
loggerInstance: logger,
disableRequestLogging: false,
trustProxy: false,
bodyLimit: 256 * 1024,
});
await app.register(sensible);
registerErrorHandler(app);
app.get('/healthz', async () => {
const snapshot = getWorkerSnapshot();
return {
ok: snapshot.ready,
service: 'media-server',
mediasoupVersion: mediasoup.version,
workerPid: snapshot.pid,
workerRestartAttempts: snapshot.restartAttempts,
uptimeSec: Math.round(process.uptime()),
timestamp: new Date().toISOString(),
};
});
app.get('/readyz', async (_request, reply) => {
const snapshot = getWorkerSnapshot();
if (!snapshot.ready) {
reply.code(503);
return {
ready: false,
reason: 'mediasoup worker not ready',
};
}
return { ready: true };
});
await app.register(internalAuthPlugin);
app.get('/internal/info', async () => {
const snapshot = getWorkerSnapshot();
const routerStats = getRouterStats();
return {
service: 'media-server',
version: '0.1.0',
mediasoupVersion: mediasoup.version,
worker: snapshot,
listen: {
ip: config.mediasoup.listenIp,
announcedIp: config.mediasoup.announcedIp ?? null,
rtcMinPort: config.mediasoup.rtcMinPort,
rtcMaxPort: config.mediasoup.rtcMaxPort,
},
stats: {
routers: routerStats.total,
},
routers: routerStats.rooms,
};
});
await app.register(
async (scope) => {
await scope.register(routerRoutes);
await scope.register(transportRoutes);
await scope.register(producerRoutes);
await scope.register(consumerRoutes);
await scope.register(recordingRoutes);
},
{ prefix: '/internal/v1' },
);
return app;
}
async function bootstrap(): Promise<void> {
const app = await buildApp();
try {
await startWorker();
} catch (err) {
logger.fatal(
{ err: err instanceof Error ? err.message : String(err) },
'failed to start mediasoup worker, exiting',
);
process.exit(1);
}
try {
await app.listen({ host: config.http.host, port: config.http.port });
logger.info(
{
host: config.http.host,
port: config.http.port,
env: config.nodeEnv,
mediasoup: {
listenIp: config.mediasoup.listenIp,
announcedIp: config.mediasoup.announcedIp ?? '(none)',
rtcPortRange: `${config.mediasoup.rtcMinPort}-${config.mediasoup.rtcMaxPort}`,
},
},
'media-server listening',
);
} catch (err) {
logger.fatal(
{ err: err instanceof Error ? err.message : String(err) },
'failed to start HTTP server',
);
process.exit(1);
}
const shutdown = async (signal: string): Promise<void> => {
logger.info({ signal }, 'received shutdown signal');
try {
await app.close();
} catch (err) {
logger.warn(
{ err: err instanceof Error ? err.message : String(err) },
'error while closing fastify',
);
}
await closeWorker();
process.exit(0);
};
process.on('SIGINT', () => void shutdown('SIGINT'));
process.on('SIGTERM', () => void shutdown('SIGTERM'));
process.on('unhandledRejection', (reason) => {
logger.error({ reason }, 'unhandled promise rejection');
});
process.on('uncaughtException', (err) => {
logger.fatal({ err: err.message, stack: err.stack }, 'uncaught exception');
process.exit(1);
});
}
const isEntryPoint =
import.meta.url === `file://${process.argv[1]}` ||
process.argv[1]?.endsWith('app.ts') ||
process.argv[1]?.endsWith('app.js');
if (isEntryPoint) {
void bootstrap();
}