【m】修改为服务器录屏
This commit is contained in:
@@ -10,6 +10,7 @@ import { bindInviteSocketEvents, buildSocketUserInfoPayload, createSignalingInst
|
||||
import { getNetworkQualityFromSummary, summarizeInboundStats } from './media/webrtc-stats.js';
|
||||
import { createLogger } from '../shared/logger.js';
|
||||
import { MeetingRecorder } from './media/meeting-recorder.js';
|
||||
import { ServerRecordingPeer } from './media/server-recording-peer.js';
|
||||
|
||||
const logger = createLogger('store');
|
||||
class CallStateManager {
|
||||
@@ -28,6 +29,9 @@ class CallStateManager {
|
||||
this.listeners = [];
|
||||
this.socketEventHandlers = {};
|
||||
this._inviteEventSignaling = null;
|
||||
this._recordingEventSignaling = null;
|
||||
this.serverRecordingSession = null;
|
||||
this.serverRecordingPeer = null;
|
||||
this.meetingRecorder = new MeetingRecorder();
|
||||
}
|
||||
subscribe(callback) {
|
||||
@@ -112,12 +116,73 @@ class CallStateManager {
|
||||
async toggleRecording() {
|
||||
const isRecording = this.state.session.localUser.mediaState.recording || false;
|
||||
|
||||
if (this.useWebSocket && this.connectionId) {
|
||||
return isRecording ? this.stopServerRecording() : this.startServerRecording();
|
||||
}
|
||||
|
||||
if (isRecording) {
|
||||
return this.stopRecording();
|
||||
}
|
||||
|
||||
return this.startRecording();
|
||||
}
|
||||
async startServerRecording() {
|
||||
if (this.state.session.status !== 'ongoing') {
|
||||
throw new Error('会议连接成功后才能开始录制');
|
||||
}
|
||||
|
||||
const response = await fetch('/api/recording-sessions', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
connectionId: this.connectionId,
|
||||
layout: 'grid',
|
||||
format: 'webm'
|
||||
})
|
||||
});
|
||||
const responseBody = await response.json().catch(() => ({}));
|
||||
if (!response.ok || responseBody.success === false) {
|
||||
throw new Error(responseBody.message || '服务端录制启动失败');
|
||||
}
|
||||
|
||||
this.serverRecordingSession = responseBody.session;
|
||||
this._setRecordingMediaState(true);
|
||||
return {
|
||||
recording: true,
|
||||
message: '服务端录制已开始'
|
||||
};
|
||||
}
|
||||
async stopServerRecording() {
|
||||
const recordingId = this.serverRecordingSession?.id;
|
||||
if (!recordingId) {
|
||||
this._setRecordingMediaState(false);
|
||||
return {
|
||||
recording: false,
|
||||
message: '服务端录制已停止'
|
||||
};
|
||||
}
|
||||
|
||||
const response = await fetch(`/api/recording-sessions/${encodeURIComponent(recordingId)}`, {
|
||||
method: 'DELETE'
|
||||
});
|
||||
const responseBody = await response.json().catch(() => ({}));
|
||||
if (!response.ok || responseBody.success === false) {
|
||||
throw new Error(responseBody.message || '服务端录制停止失败');
|
||||
}
|
||||
|
||||
this.serverRecordingSession = responseBody.session;
|
||||
this._setRecordingMediaState(false);
|
||||
return {
|
||||
recording: false,
|
||||
message: '服务端录制已停止'
|
||||
};
|
||||
}
|
||||
_setRecordingMediaState(value) {
|
||||
this.state.session.localUser.mediaState.recording = value;
|
||||
this._notifyLocalMediaChange('recording', value);
|
||||
this.emitMediaStateChange();
|
||||
this._notifyUserListUpdate();
|
||||
}
|
||||
async startRecording() {
|
||||
if (this.state.session.status !== 'ongoing') {
|
||||
throw new Error('会议连接成功后才能开始录制');
|
||||
@@ -229,6 +294,7 @@ class CallStateManager {
|
||||
async _updateLocalMediaRefactored(mediaType, value) {
|
||||
if (mediaType === 'video' && value) {
|
||||
await this._enableLocalVideo();
|
||||
this._refreshServerRecordingPeer();
|
||||
this._notifyUserListUpdate();
|
||||
return;
|
||||
}
|
||||
@@ -241,6 +307,7 @@ class CallStateManager {
|
||||
if (mediaType === 'audio') {
|
||||
this._setLocalAudioTrackEnabled(value);
|
||||
}
|
||||
this._refreshServerRecordingPeer();
|
||||
this._notifyUserListUpdate();
|
||||
}
|
||||
async _enableLocalVideo() {
|
||||
@@ -441,6 +508,8 @@ class CallStateManager {
|
||||
await this._startConnection(connectionId);
|
||||
}
|
||||
_registerCallbacks() {
|
||||
this._ensureServerRecordingPeer();
|
||||
this._bindRecordingSignalHandlers();
|
||||
this.renderstreaming.onNewPeer = (participantId) => {
|
||||
logger.debug(`New peer created for ${participantId}, adding local tracks`);
|
||||
if (this.state.localStream) {
|
||||
@@ -534,6 +603,46 @@ class CallStateManager {
|
||||
this._handleRenderStreamingMessage(data);
|
||||
};
|
||||
}
|
||||
_ensureServerRecordingPeer() {
|
||||
if (this.serverRecordingPeer) {
|
||||
return this.serverRecordingPeer;
|
||||
}
|
||||
|
||||
this.serverRecordingPeer = new ServerRecordingPeer({
|
||||
rtcConfiguration: getRTCConfiguration(),
|
||||
getLocalStream: () => this.state.localStream,
|
||||
getSignaling: () => this.getActiveSignaling(),
|
||||
getConnectionId: () => this.connectionId,
|
||||
getParticipantId: () => this.selfParticipantId || (this.role === 'host' ? 'host' : '')
|
||||
});
|
||||
return this.serverRecordingPeer;
|
||||
}
|
||||
_bindRecordingSignalHandlers() {
|
||||
const signaling = this.renderstreaming?._signaling;
|
||||
if (!signaling || signaling === this._recordingEventSignaling || typeof signaling.addEventListener !== 'function') {
|
||||
return;
|
||||
}
|
||||
|
||||
signaling.addEventListener('recording-started', (event) => {
|
||||
this._handleRecordingStarted(event.detail);
|
||||
});
|
||||
signaling.addEventListener('recording-peer-request', (event) => {
|
||||
this._handleRecordingPeerRequest(event.detail);
|
||||
});
|
||||
signaling.addEventListener('recording-stopped', (event) => {
|
||||
this._handleRecordingStopped(event.detail);
|
||||
});
|
||||
signaling.addEventListener('recording-status', (event) => {
|
||||
this._handleRecordingStatus(event.detail);
|
||||
});
|
||||
signaling.addEventListener('recording-answer', (event) => {
|
||||
this._handleRecordingAnswer(event.detail);
|
||||
});
|
||||
signaling.addEventListener('recording-candidate', (event) => {
|
||||
this._handleRecordingCandidate(event.detail);
|
||||
});
|
||||
this._recordingEventSignaling = signaling;
|
||||
}
|
||||
async _startConnection(connectionId) {
|
||||
await this.renderstreaming.start();
|
||||
await this.renderstreaming.createConnection(connectionId);
|
||||
@@ -552,6 +661,9 @@ class CallStateManager {
|
||||
}
|
||||
this.clearStatsMessage();
|
||||
this.stopNetworkQualityDetection();
|
||||
if (this.serverRecordingPeer) {
|
||||
this.serverRecordingPeer.stop();
|
||||
}
|
||||
if (this.durationInterval) {
|
||||
clearInterval(this.durationInterval);
|
||||
this.durationInterval = null;
|
||||
@@ -674,6 +786,116 @@ class CallStateManager {
|
||||
break;
|
||||
}
|
||||
}
|
||||
_isCurrentRecordingEvent(data) {
|
||||
return data && (!data.connectionId || data.connectionId === this.connectionId);
|
||||
}
|
||||
_handleRecordingStarted(data) {
|
||||
if (!this._isCurrentRecordingEvent(data)) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.serverRecordingSession = {
|
||||
id: data.recordingId,
|
||||
connectionId: data.connectionId,
|
||||
status: data.status,
|
||||
layout: data.layout,
|
||||
format: data.format,
|
||||
startedAt: data.startedAt
|
||||
};
|
||||
this._setRecordingMediaState(true);
|
||||
showNotification('服务端录制已开始', 'success');
|
||||
}
|
||||
_handleRecordingStopped(data) {
|
||||
if (!this._isCurrentRecordingEvent(data)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.serverRecordingSession && this.serverRecordingSession.id === data.recordingId) {
|
||||
this.serverRecordingSession = {
|
||||
...this.serverRecordingSession,
|
||||
status: data.status,
|
||||
stoppedAt: data.stoppedAt
|
||||
};
|
||||
}
|
||||
if (this.serverRecordingPeer) {
|
||||
this.serverRecordingPeer.stop(data.recordingId);
|
||||
}
|
||||
this._setRecordingMediaState(false);
|
||||
showNotification('服务端录制已停止', 'success');
|
||||
}
|
||||
async _handleRecordingPeerRequest(data) {
|
||||
if (!this._isCurrentRecordingEvent(data)) {
|
||||
return;
|
||||
}
|
||||
|
||||
logger.debug('收到服务端录制媒体请求:', data);
|
||||
this.notify({
|
||||
type: 'RECORDING_PEER_REQUEST',
|
||||
recordingId: data.recordingId,
|
||||
mediaMode: data.mediaMode
|
||||
});
|
||||
try {
|
||||
await this._ensureServerRecordingPeer().start(data);
|
||||
}
|
||||
catch (error) {
|
||||
logger.error('服务端录制 PeerConnection 创建失败:', error);
|
||||
showNotification('服务端录制媒体连接失败', 'error');
|
||||
}
|
||||
}
|
||||
_isServerRecordingActive() {
|
||||
return this.useWebSocket
|
||||
&& this.serverRecordingSession
|
||||
&& this.serverRecordingSession.status === 'recording';
|
||||
}
|
||||
_refreshServerRecordingPeer() {
|
||||
if (!this._isServerRecordingActive() || !this.serverRecordingPeer) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.serverRecordingPeer.start({
|
||||
recordingId: this.serverRecordingSession.id,
|
||||
connectionId: this.connectionId,
|
||||
mediaMode: 'webrtc-sendonly'
|
||||
}).catch((error) => {
|
||||
logger.error('服务端录制媒体重协商失败:', error);
|
||||
});
|
||||
}
|
||||
_handleRecordingStatus(data) {
|
||||
if (!this._isCurrentRecordingEvent(data)) {
|
||||
return;
|
||||
}
|
||||
|
||||
logger.debug('收到服务端录制状态:', data);
|
||||
this.notify({
|
||||
type: 'RECORDING_STATUS',
|
||||
status: data.status,
|
||||
recordingId: data.recordingId
|
||||
});
|
||||
}
|
||||
async _handleRecordingAnswer(data) {
|
||||
if (!this._isCurrentRecordingEvent(data) || !this.serverRecordingPeer) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await this.serverRecordingPeer.applyAnswer(data);
|
||||
}
|
||||
catch (error) {
|
||||
logger.error('服务端录制 answer 处理失败:', error);
|
||||
}
|
||||
}
|
||||
async _handleRecordingCandidate(data) {
|
||||
if (!this._isCurrentRecordingEvent(data) || !this.serverRecordingPeer) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await this.serverRecordingPeer.addIceCandidate(data);
|
||||
}
|
||||
catch (error) {
|
||||
logger.error('服务端录制 candidate 处理失败:', error);
|
||||
}
|
||||
}
|
||||
_handleChatMessage(data) {
|
||||
const chatPayload = data.data || data.message;
|
||||
if (!chatPayload) {
|
||||
|
||||
Reference in New Issue
Block a user