服务器录屏1

This commit is contained in:
2026-06-02 02:49:47 +08:00
parent 66d6f92d1e
commit 59fc4be5cc
7 changed files with 248 additions and 27 deletions

View File

@@ -10,6 +10,7 @@ import { RecordingSession, listRecordingSessions, stopRecordingSession } from '.
import { registerRecordingPeerCandidate, registerRecordingPeerOffer, stopRecordingAgent } from '../recording/agent';
import { startRecordingCompositionJob } from '../recording/composer';
import { acceptRecordingOffer, addRecordingIceCandidate, stopRecordingPeer } from '../recording/werift-adapter';
import { RecordingPerson } from '../recording/storage';
/**
* 是否为私有模式
@@ -363,7 +364,42 @@ function stopRecordingPeersForSocket(ws: WebSocket, connectionId: string): void
});
}
function roomMemberToRecordingPerson(member: RoomMemberInfo | undefined, fallbackRole: string): RecordingPerson | undefined {
if (!member) {
return undefined;
}
return {
participantId: member.participantId || '',
userId: member.userId || '',
id: member.userId || member.participantId || '',
name: member.name || member.userId || member.participantId || '',
avatar: member.avatar || '',
role: member.role || fallbackRole,
status: 'online'
};
}
function getRecordingRoomPeople(connectionId: string): { host?: RecordingPerson; participants: RecordingPerson[] } {
const room = rooms.get(connectionId);
if (!room) {
return { participants: [] };
}
const members = Array.from(room.members.values());
const hostMember = members.find((member) => member.role === 'host')
|| members.find((member) => member.socketId === room.hostSocketId);
const host = roomMemberToRecordingPerson(hostMember, 'host');
const participants = members
.filter((member) => member !== hostMember && member.role === 'participant')
.map((member) => roomMemberToRecordingPerson(member, 'participant'))
.filter((member) => Boolean(member)) as RecordingPerson[];
return { host, participants };
}
function stopActiveRecordingSessions(connectionId: string): void {
const roomPeople = getRecordingRoomPeople(connectionId);
getActiveRecordingSessions(connectionId).forEach((session) => {
const stoppedSession = stopRecordingSession(session.id);
if (stoppedSession) {
@@ -376,7 +412,9 @@ function stopActiveRecordingSessions(connectionId: string): void {
meetingId: session.connectionId,
recordingId: session.id,
layout: session.layout,
format: session.format
format: session.format,
host: roomPeople.host,
participants: roomPeople.participants
});
})
.catch((error) => {

View File

@@ -1,6 +1,7 @@
import { spawn } from 'child_process';
import { v4 as uuid } from 'uuid';
import {
RecordingPerson,
ServerTrackRecordingFile,
ServerTrackRecordingTarget,
createComposedRecordingTarget,
@@ -25,6 +26,8 @@ export type RecordingCompositionJob = {
failedAt?: string;
error?: string;
inputFiles: string[];
host?: RecordingPerson;
participants?: RecordingPerson[];
deletedInputFiles?: string[];
output?: {
meetingId: string;
@@ -41,6 +44,8 @@ type StartCompositionInput = {
recordingId: string;
layout?: string;
format?: string;
host?: RecordingPerson;
participants?: RecordingPerson[];
};
type CompositionInputSets = {
@@ -267,7 +272,9 @@ async function runRecordingCompositionJob(job: RecordingCompositionJob): Promise
recordingId: job.recordingId,
inputs: compositionInputs,
layout: job.layout,
format: job.format
format: job.format,
host: job.host,
participants: job.participants
});
const deletedInputFiles = deleteServerTrackRecordingFiles(compositionInputs);
@@ -301,7 +308,9 @@ export function startRecordingCompositionJob(input: StartCompositionInput): Reco
format: normalizeFormat(input.format),
createdAt: timestamp,
updatedAt: timestamp,
inputFiles: inputSets.videoInputs.concat(inputSets.audioInputs).map((file) => file.filename)
inputFiles: inputSets.videoInputs.concat(inputSets.audioInputs).map((file) => file.filename),
host: input.host,
participants: input.participants
};
jobs.set(job.id, job);
runRecordingCompositionJob(job);

View File

@@ -18,6 +18,17 @@ export type ServerTrackRecordingFile = ServerTrackRecordingTarget & {
metadata: any;
};
export type RecordingPerson = {
participantId?: string;
userId?: string;
id?: string;
name?: string;
avatar?: string;
role?: string;
status?: string;
mediaState?: any;
};
type CreateTargetInput = {
recordingId: string;
connectionId: string;
@@ -43,6 +54,8 @@ type WriteComposedMetadataInput = {
inputs: ServerTrackRecordingFile[];
layout: string;
format: string;
host?: RecordingPerson;
participants?: RecordingPerson[];
};
export function getRecordingRoot(): string {
@@ -216,19 +229,90 @@ export function deleteServerTrackRecordingFiles(files: ServerTrackRecordingFile[
return deletedFiles;
}
function getPersonKey(person: RecordingPerson | undefined): string {
return person?.participantId || person?.userId || person?.id || '';
}
function normalizeRecordingPerson(person: RecordingPerson | undefined, fallbackRole: string): RecordingPerson | undefined {
if (!person || typeof person !== 'object') {
return undefined;
}
return {
participantId: person.participantId || '',
userId: person.userId || person.id || '',
id: person.id || person.userId || person.participantId || '',
name: person.name || person.userId || person.id || person.participantId || '',
avatar: person.avatar || '',
role: person.role || fallbackRole,
status: person.status || '',
mediaState: person.mediaState
};
}
function collectInputPeople(inputs: ServerTrackRecordingFile[]): {
host?: RecordingPerson;
participants: RecordingPerson[];
} {
const participantsByKey: { [key: string]: RecordingPerson } = {};
let host: RecordingPerson | undefined;
inputs.forEach((file) => {
const metadata = file.metadata || {};
const fileRole = metadata.role === 'host' ? 'host' : 'participant';
const metadataHost = normalizeRecordingPerson(metadata.host, 'host');
if (metadataHost && metadataHost.role === 'host' && !host) {
host = metadataHost;
}
const people = Array.isArray(metadata.participants) ? metadata.participants : [];
people.forEach((person: RecordingPerson) => {
const normalized = normalizeRecordingPerson(person, person.role || fileRole);
const key = getPersonKey(normalized);
if (!normalized || !key) {
return;
}
if (normalized.role === 'host' && !host) {
host = { ...normalized, role: 'host' };
return;
}
if (normalized.role !== 'host' && !participantsByKey[key]) {
participantsByKey[key] = { ...normalized, role: 'participant' };
}
});
if (file.participantId) {
const person = normalizeRecordingPerson({
participantId: file.participantId,
id: file.participantId,
role: fileRole
}, fileRole);
const key = getPersonKey(person);
if (person && key) {
if (fileRole === 'host' && !host) {
host = { ...person, role: 'host' };
} else if (fileRole !== 'host' && !participantsByKey[key]) {
participantsByKey[key] = { ...person, role: 'participant' };
}
}
}
});
return {
host,
participants: Object.keys(participantsByKey).map((key) => participantsByKey[key])
};
}
export function writeComposedRecordingMetadata(input: WriteComposedMetadataInput): void {
const now = new Date().toISOString();
const participantsById: { [participantId: string]: any } = {};
input.inputs.forEach((file) => {
if (!file.participantId || participantsById[file.participantId]) {
return;
}
participantsById[file.participantId] = {
participantId: file.participantId,
id: file.participantId,
role: 'participant'
};
});
const inputPeople = collectInputPeople(input.inputs);
const host = normalizeRecordingPerson(input.host, 'host') || inputPeople.host;
const participants = Array.isArray(input.participants) && input.participants.length > 0
? input.participants
.map((participant) => normalizeRecordingPerson(participant, 'participant'))
.filter((participant) => Boolean(participant)) as RecordingPerson[]
: inputPeople.participants;
const metadata = {
id: `${input.recordingId}-composed`,
@@ -237,14 +321,9 @@ export function writeComposedRecordingMetadata(input: WriteComposedMetadataInput
originalFilename: `server-recording-${input.recordingId}-composed.${input.format}`,
mimetype: input.format === 'mp4' ? 'video/mp4' : 'video/webm',
size: fs.existsSync(input.target.filePath) ? fs.statSync(input.target.filePath).size : 0,
userId: 'server-recorder',
host: {
userId: 'server-recorder',
id: 'server-recorder',
name: 'Server Recorder',
role: 'recorder'
},
participants: Object.keys(participantsById).map((participantId) => participantsById[participantId]),
userId: host?.userId || host?.id || '',
host,
participants,
uploadedAt: now,
updatedAt: now,
recordingSource: 'server-composed',

View File

@@ -326,6 +326,37 @@ function getActiveRecordingSession(connectionId: string) {
return null;
}
function roomMemberToRecordingPerson(member: any, fallbackRole: string): RecordingPerson | undefined {
if (!member || typeof member !== 'object') {
return undefined;
}
return sanitizeRecordingPerson({
participantId: member.participantId,
userId: member.userId || member.id,
id: member.id || member.userId,
name: member.name,
avatar: member.avatar,
role: member.role || fallbackRole,
status: member.status,
mediaState: member.mediaState
}, fallbackRole);
}
function getRecordingRoomPeople(connectionId: string): { host?: RecordingPerson; participants: RecordingPerson[] } {
const room = getWebSocketRooms(connectionId)[0];
const members = Array.isArray(room?.members) ? room.members : [];
const hostMember = members.find((member: any) => member.role === 'host')
|| members.find((member: any) => member.socketId && member.socketId === room?.hostSocketId);
const host = roomMemberToRecordingPerson(hostMember, 'host');
const participants = members
.filter((member: any) => member !== hostMember && member.role === 'participant')
.map((member: any) => roomMemberToRecordingPerson(member, 'participant'))
.filter((member: RecordingPerson | undefined) => Boolean(member)) as RecordingPerson[];
return { host, participants };
}
export const createServer = (config: Options): express.Express => {
const app: express.Express = express();
resetHandler(config.mode);
@@ -561,12 +592,15 @@ export const createServer = (config: Options): express.Express => {
}
const shouldCompose = req.query.compose !== 'false';
const roomPeople = getRecordingRoomPeople(session.connectionId);
const compositionJob = shouldCompose
? startRecordingCompositionJob({
meetingId: session.connectionId,
recordingId: session.id,
layout: session.layout,
format: session.format
format: session.format,
host: roomPeople.host,
participants: roomPeople.participants
})
: null;
res.json({ success: true, session, agent, notified, compositionJob });
@@ -602,7 +636,8 @@ export const createServer = (config: Options): express.Express => {
meetingId,
recordingId,
layout: req.body.layout,
format: req.body.format
format: req.body.format,
...getRecordingRoomPeople(meetingId)
});
res.status(202).json({ success: true, job });
});