Store meet recordings on Ladill servers via browser upload.
Deploy Ladill Meet / deploy (push) Successful in 53s
Deploy Ladill Meet / deploy (push) Successful in 53s
Capture the meeting in the host browser, upload to Ladill storage on stop, and remove the LiveKit egress placeholder job. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
+153
-5
@@ -17,6 +17,8 @@ function meetRoom() {
|
||||
let room = null;
|
||||
let connectPromise = null;
|
||||
let initialized = false;
|
||||
let recordingMediaRecorder = null;
|
||||
let recordingChunks = [];
|
||||
|
||||
return {
|
||||
isMuted: true,
|
||||
@@ -50,6 +52,7 @@ function meetRoom() {
|
||||
breakoutModalOpen: false,
|
||||
breakoutCount: '4',
|
||||
endMeetingConfirmOpen: false,
|
||||
activeRecordingUuid: null,
|
||||
joinUrl: '',
|
||||
passcode: '',
|
||||
roomTitle: '',
|
||||
@@ -692,13 +695,152 @@ function meetRoom() {
|
||||
|
||||
async toggleRecording() {
|
||||
if (!this.isHost) return;
|
||||
const url = this.isRecording ? this.config.recordingStopUrl : this.config.recordingStartUrl;
|
||||
const res = await fetch(url, { method: 'POST', headers: this.headers() });
|
||||
if (res.ok) {
|
||||
this.isRecording = !this.isRecording;
|
||||
|
||||
if (this.isRecording) {
|
||||
await this.stopServerAndUploadRecording();
|
||||
} else {
|
||||
await this.startServerRecording();
|
||||
}
|
||||
},
|
||||
|
||||
async startServerRecording() {
|
||||
const res = await fetch(this.config.recordingStartUrl, { method: 'POST', headers: this.headers() });
|
||||
if (!res.ok) return;
|
||||
|
||||
const data = await res.json();
|
||||
this.activeRecordingUuid = data.recording_uuid;
|
||||
if (!this.startClientRecording()) {
|
||||
this.isRecording = false;
|
||||
this.activeRecordingUuid = null;
|
||||
return;
|
||||
}
|
||||
|
||||
this.isRecording = true;
|
||||
},
|
||||
|
||||
startClientRecording() {
|
||||
const stream = this.buildRecordingStream();
|
||||
if (!stream || typeof MediaRecorder === 'undefined') {
|
||||
return false;
|
||||
}
|
||||
|
||||
recordingChunks = [];
|
||||
const mimeType = MediaRecorder.isTypeSupported('video/webm;codecs=vp9,opus')
|
||||
? 'video/webm;codecs=vp9,opus'
|
||||
: (MediaRecorder.isTypeSupported('video/webm') ? 'video/webm' : '');
|
||||
|
||||
try {
|
||||
recordingMediaRecorder = mimeType
|
||||
? new MediaRecorder(stream, { mimeType, videoBitsPerSecond: 2_500_000 })
|
||||
: new MediaRecorder(stream, { videoBitsPerSecond: 2_500_000 });
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
|
||||
recordingMediaRecorder.ondataavailable = (event) => {
|
||||
if (event.data?.size > 0) {
|
||||
recordingChunks.push(event.data);
|
||||
}
|
||||
};
|
||||
recordingMediaRecorder.start(1000);
|
||||
|
||||
return true;
|
||||
},
|
||||
|
||||
buildRecordingStream() {
|
||||
if (!room) return null;
|
||||
|
||||
const screenEl = document.getElementById('screen-share');
|
||||
const screenVideo = screenEl && !screenEl.classList.contains('hidden')
|
||||
? screenEl.querySelector('video')
|
||||
: null;
|
||||
const videoSource = screenVideo || document.getElementById('video-grid');
|
||||
|
||||
if (!videoSource?.captureStream) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const videoStream = videoSource.captureStream(25);
|
||||
const tracks = [...videoStream.getVideoTracks(), ...this.collectMeetingAudioTracks()];
|
||||
|
||||
return tracks.length ? new MediaStream(tracks) : null;
|
||||
},
|
||||
|
||||
collectMeetingAudioTracks() {
|
||||
const tracks = [];
|
||||
const addFromParticipant = (participant) => {
|
||||
participant?.audioTrackPublications?.forEach((publication) => {
|
||||
const mediaTrack = publication.track?.mediaStreamTrack;
|
||||
if (mediaTrack && mediaTrack.readyState === 'live') {
|
||||
tracks.push(mediaTrack);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
if (room?.localParticipant) {
|
||||
addFromParticipant(room.localParticipant);
|
||||
}
|
||||
|
||||
room?.remoteParticipants?.forEach(addFromParticipant);
|
||||
|
||||
return tracks;
|
||||
},
|
||||
|
||||
async stopServerAndUploadRecording() {
|
||||
const res = await fetch(this.config.recordingStopUrl, { method: 'POST', headers: this.headers() });
|
||||
const blob = await this.stopClientRecording();
|
||||
this.isRecording = false;
|
||||
|
||||
if (!res.ok) {
|
||||
this.activeRecordingUuid = null;
|
||||
return;
|
||||
}
|
||||
|
||||
const data = await res.json().catch(() => ({}));
|
||||
const recordingUuid = data.recording_uuid || this.activeRecordingUuid;
|
||||
|
||||
if (blob && recordingUuid) {
|
||||
await this.uploadRecording(blob, recordingUuid);
|
||||
}
|
||||
|
||||
this.activeRecordingUuid = null;
|
||||
},
|
||||
|
||||
stopClientRecording() {
|
||||
return new Promise((resolve) => {
|
||||
const recorder = recordingMediaRecorder;
|
||||
if (!recorder || recorder.state === 'inactive') {
|
||||
recordingMediaRecorder = null;
|
||||
recordingChunks = [];
|
||||
resolve(null);
|
||||
return;
|
||||
}
|
||||
|
||||
recorder.onstop = () => {
|
||||
const blob = new Blob(recordingChunks, {
|
||||
type: recorder.mimeType || 'video/webm',
|
||||
});
|
||||
recordingMediaRecorder = null;
|
||||
recordingChunks = [];
|
||||
recorder.stream?.getTracks().forEach((track) => track.stop());
|
||||
resolve(blob.size > 0 ? blob : null);
|
||||
};
|
||||
|
||||
recorder.stop();
|
||||
});
|
||||
},
|
||||
|
||||
async uploadRecording(blob, recordingUuid) {
|
||||
const form = new FormData();
|
||||
form.append('recording', blob, `${recordingUuid}.webm`);
|
||||
|
||||
await fetch(`/room/${this.config.sessionUuid}/recordings/${recordingUuid}/upload`, {
|
||||
method: 'POST',
|
||||
headers: { 'X-CSRF-TOKEN': this.config.csrf, Accept: 'application/json' },
|
||||
body: form,
|
||||
});
|
||||
},
|
||||
|
||||
async toggleLock() {
|
||||
if (!this.isHost) return;
|
||||
const url = this.isLocked ? this.config.unlockUrl : this.config.lockUrl;
|
||||
@@ -767,7 +909,13 @@ function meetRoom() {
|
||||
},
|
||||
|
||||
submitEndMeeting() {
|
||||
document.getElementById('meet-end-form')?.requestSubmit();
|
||||
if (this.isRecording) {
|
||||
this.stopServerAndUploadRecording().finally(() => {
|
||||
document.getElementById('meet-end-form')?.requestSubmit();
|
||||
});
|
||||
} else {
|
||||
document.getElementById('meet-end-form')?.requestSubmit();
|
||||
}
|
||||
this.endMeetingConfirmOpen = false;
|
||||
},
|
||||
|
||||
|
||||
Reference in New Issue
Block a user