2026-05-25 20:37:36 +08:00
|
|
|
import { MeetingRecorder } from '../../public/call/media/meeting-recorder.js';
|
2026-05-25 16:39:13 +08:00
|
|
|
|
|
|
|
|
class MediaStreamMock {
|
|
|
|
|
constructor(tracks = []) {
|
|
|
|
|
this.tracks = tracks;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
getTracks() {
|
|
|
|
|
return this.tracks;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
getAudioTracks() {
|
|
|
|
|
return this.tracks.filter(track => track.kind === 'audio');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
getVideoTracks() {
|
|
|
|
|
return this.tracks.filter(track => track.kind === 'video');
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
class MediaRecorderMock {
|
|
|
|
|
static isTypeSupported() {
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
constructor(stream, options) {
|
|
|
|
|
this.stream = stream;
|
|
|
|
|
this.mimeType = options.mimeType;
|
|
|
|
|
this.state = 'inactive';
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
start() {
|
|
|
|
|
this.state = 'recording';
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
stop() {
|
|
|
|
|
this.state = 'inactive';
|
|
|
|
|
this.ondataavailable({ data: new Blob(['recording'], { type: this.mimeType }) });
|
|
|
|
|
this.onstop();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function createTrack(kind) {
|
|
|
|
|
return {
|
|
|
|
|
kind,
|
|
|
|
|
readyState: 'live',
|
|
|
|
|
stop: jest.fn(),
|
|
|
|
|
clone: jest.fn(() => createTrack(kind))
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function createWindowMock({ mediaRecorder = MediaRecorderMock } = {}) {
|
|
|
|
|
return {
|
|
|
|
|
MediaRecorder: mediaRecorder,
|
|
|
|
|
MediaStream: MediaStreamMock,
|
|
|
|
|
URL: {
|
|
|
|
|
createObjectURL: jest.fn(() => 'blob:recording'),
|
|
|
|
|
revokeObjectURL: jest.fn()
|
|
|
|
|
},
|
|
|
|
|
requestAnimationFrame: jest.fn(() => 1),
|
|
|
|
|
cancelAnimationFrame: jest.fn(),
|
|
|
|
|
setTimeout: jest.fn((callback) => {
|
|
|
|
|
callback();
|
|
|
|
|
return 1;
|
|
|
|
|
})
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
describe('MeetingRecorder', () => {
|
|
|
|
|
beforeEach(() => {
|
|
|
|
|
HTMLCanvasElement.prototype.getContext = jest.fn(() => ({
|
|
|
|
|
fillStyle: '',
|
|
|
|
|
font: '',
|
|
|
|
|
textAlign: '',
|
|
|
|
|
textBaseline: '',
|
|
|
|
|
fillRect: jest.fn(),
|
|
|
|
|
fillText: jest.fn(),
|
|
|
|
|
drawImage: jest.fn()
|
|
|
|
|
}));
|
|
|
|
|
HTMLCanvasElement.prototype.captureStream = jest.fn(() => new MediaStreamMock([createTrack('video')]));
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
afterEach(() => {
|
|
|
|
|
jest.restoreAllMocks();
|
|
|
|
|
document.body.innerHTML = '';
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
test('starts and stops recording a meeting file', async () => {
|
|
|
|
|
const windowRef = createWindowMock();
|
|
|
|
|
const recorder = new MeetingRecorder({ documentRef: document, windowRef });
|
|
|
|
|
const localVideo = document.createElement('video');
|
|
|
|
|
localVideo.id = 'localVideo';
|
|
|
|
|
localVideo.srcObject = new MediaStreamMock([createTrack('video')]);
|
|
|
|
|
Object.defineProperty(localVideo, 'readyState', { value: 2 });
|
|
|
|
|
Object.defineProperty(localVideo, 'videoWidth', { value: 640 });
|
|
|
|
|
Object.defineProperty(localVideo, 'videoHeight', { value: 360 });
|
|
|
|
|
localVideo.getBoundingClientRect = () => ({ width: 320, height: 180 });
|
|
|
|
|
document.body.appendChild(localVideo);
|
|
|
|
|
|
|
|
|
|
await recorder.start({
|
|
|
|
|
localStream: new MediaStreamMock([createTrack('audio')]),
|
|
|
|
|
connectionId: '123-456-789'
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
expect(recorder.isRecording()).toBe(true);
|
|
|
|
|
|
|
|
|
|
const result = await recorder.stop();
|
|
|
|
|
|
|
|
|
|
expect(result.filename).toContain('meeting-recording-123-456-789');
|
|
|
|
|
expect(result.mimeType).toBe('video/mp4;codecs=avc1.42E01E,mp4a.40.2');
|
|
|
|
|
expect(result.filename).toMatch(/\.mp4$/);
|
|
|
|
|
expect(windowRef.URL.createObjectURL).not.toHaveBeenCalled();
|
|
|
|
|
expect(recorder.isRecording()).toBe(false);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
test('reports unsupported browsers', async () => {
|
|
|
|
|
HTMLCanvasElement.prototype.captureStream = undefined;
|
|
|
|
|
const windowRef = createWindowMock({ mediaRecorder: undefined });
|
|
|
|
|
const recorder = new MeetingRecorder({ documentRef: document, windowRef });
|
|
|
|
|
|
|
|
|
|
await expect(recorder.start()).rejects.toThrow('当前浏览器不支持会议录制');
|
|
|
|
|
});
|
|
|
|
|
});
|