【m】修改为服务器录屏

This commit is contained in:
2026-06-02 02:34:40 +08:00
parent d74a0c8121
commit 66d6f92d1e
21 changed files with 4053 additions and 32 deletions

View File

@@ -124,6 +124,11 @@ export class MeetingRecorder {
this.audioSources = [];
this.recordingStream = null;
this.connectionId = '';
this.layout = 'grid';
this.onChunk = null;
this.storeChunks = true;
this.mixedAudioDestination = null;
this.mixedAudioTrackIds = new Set();
}
isSupported() {
@@ -137,7 +142,7 @@ export class MeetingRecorder {
return Boolean(this.mediaRecorder && this.mediaRecorder.state !== 'inactive');
}
async start({ localStream, remoteStream, remoteStreams, connectionId } = {}) {
async start({ localStream, remoteStream, remoteStreams, connectionId, layout, onChunk, storeChunks } = {}) {
if (this.isRecording()) {
throw new Error('会议正在录制中');
}
@@ -156,7 +161,11 @@ export class MeetingRecorder {
}
this.connectionId = connectionId || '';
this.layout = layout || 'grid';
this.onChunk = typeof onChunk === 'function' ? onChunk : null;
this.storeChunks = storeChunks !== false;
this.chunks = [];
this.mixedAudioTrackIds = new Set();
this.canvas = canvas;
this.context = context;
@@ -179,6 +188,16 @@ export class MeetingRecorder {
}
}
syncAudio({ localStream, remoteStream, remoteStreams } = {}) {
if (!this.isRecording() || !this.audioContext || !this.mixedAudioDestination) {
return;
}
const streams = collectStreams({ localStream, remoteStream, remoteStreams });
const audioTracks = collectLiveAudioTracks(streams);
audioTracks.forEach(track => this._connectAudioTrack(track));
}
stop() {
if (!this.isRecording()) {
return Promise.resolve(null);
@@ -203,16 +222,26 @@ export class MeetingRecorder {
}
this.audioContext = new AudioContextCtor();
const destination = this.audioContext.createMediaStreamDestination();
this.mixedAudioDestination = this.audioContext.createMediaStreamDestination();
audioTracks.forEach(track => this._connectAudioTrack(track));
audioTracks.forEach(track => {
const sourceStream = new this.window.MediaStream([track]);
const source = this.audioContext.createMediaStreamSource(sourceStream);
source.connect(destination);
this.audioSources.push(source);
});
return this.mixedAudioDestination.stream.getAudioTracks()[0] || null;
}
return destination.stream.getAudioTracks()[0] || null;
_connectAudioTrack(track) {
if (!track || track.readyState === 'ended') {
return;
}
const trackId = track.id || `${track.kind}-${Date.now()}`;
if (this.mixedAudioTrackIds.has(trackId)) {
return;
}
this.mixedAudioTrackIds.add(trackId);
const sourceStream = new this.window.MediaStream([track]);
const source = this.audioContext.createMediaStreamSource(sourceStream);
source.connect(this.mixedAudioDestination);
this.audioSources.push(source);
}
startMediaRecorder(stream) {
@@ -224,7 +253,17 @@ export class MeetingRecorder {
this.mediaRecorder = new MediaRecorderCtor(stream, options);
this.mediaRecorder.ondataavailable = (event) => {
if (event.data && event.data.size > 0) {
this.chunks.push(event.data);
if (this.storeChunks) {
this.chunks.push(event.data);
}
if (this.onChunk) {
try {
this.onChunk(event.data);
}
catch (_error) {
// Ignore chunk callback failures so recording can continue.
}
}
}
};
this.mediaRecorder.onerror = (event) => {
@@ -235,9 +274,9 @@ export class MeetingRecorder {
this.cleanup();
};
this.mediaRecorder.onstop = () => {
const blob = new Blob(this.chunks, { type: this.mediaRecorder.mimeType || 'video/webm' });
const filename = this.buildFilename();
const mimeType = blob.type || this.mediaRecorder.mimeType || 'video/webm';
const mimeType = this.mediaRecorder.mimeType || 'video/webm';
const blob = this.storeChunks ? new Blob(this.chunks, { type: mimeType }) : null;
this.cleanup();
if (this.pendingStop) {
this.pendingStop.resolve({ blob, filename, mimeType });
@@ -267,6 +306,15 @@ export class MeetingRecorder {
context.fillStyle = '#020617';
context.fillRect(0, 0, canvas.width, canvas.height);
if (this.layout === 'host-only') {
if (localVideo) {
drawVideoCover(context, localVideo, 0, 0, canvas.width, canvas.height);
return;
}
drawEmptyFrame(context, canvas);
return;
}
if (remoteVideos.length > 0) {
drawGrid(context, remoteVideos, canvas);
if (localVideo) {
@@ -328,9 +376,13 @@ export class MeetingRecorder {
}
this.audioSources = [];
this.mixedAudioDestination = null;
this.mixedAudioTrackIds = new Set();
this.mediaRecorder = null;
this.canvas = null;
this.context = null;
this.chunks = [];
this.onChunk = null;
this.storeChunks = true;
}
}