Fix stuck recording processing and add invoice service API test.
Deploy Ladill Meet / deploy (push) Successful in 52s

Capture stage spotlight video for browser recordings, fail incomplete uploads, allow post-session host uploads, and expire stale processing recordings automatically.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
isaacclad
2026-07-03 22:54:26 +00:00
co-authored by Cursor
parent 94a7906188
commit 4af53f9374
11 changed files with 395 additions and 18 deletions
+70 -10
View File
@@ -1486,8 +1486,10 @@ function meetRoom() {
const data = await res.json();
this.activeRecordingUuid = data.recording_uuid;
if (! await this.tryStartClientRecordingWithRetry()) {
await this.abortServerRecording('Could not capture meeting video in this browser.');
this.isRecording = false;
this.activeRecordingUuid = null;
window.alert(`Recording could not start because this browser could not capture the ${this.sessionNoun()} video. Try Chrome on desktop, or disable stage-only layouts temporarily.`);
return;
}
@@ -1530,7 +1532,11 @@ function meetRoom() {
const screenVideo = screenEl && !screenEl.classList.contains('hidden')
? screenEl.querySelector('video')
: null;
const videoSource = screenVideo || document.getElementById('video-grid');
let videoSource = screenVideo;
if (!videoSource) {
videoSource = this.recordingVideoCaptureElement();
}
if (!videoSource?.captureStream) {
return null;
@@ -1542,6 +1548,26 @@ function meetRoom() {
return tracks.length ? new MediaStream(tracks) : null;
},
recordingVideoCaptureElement() {
if (!this.usesStageLayout) {
return document.getElementById('video-grid');
}
const spotlight = document.getElementById('meet-stage-spotlight');
const grid = document.getElementById('video-grid');
const stage = document.getElementById('meet-stage');
if (spotlight?.querySelector('video')) {
return spotlight;
}
if (grid?.querySelector('video')) {
return grid;
}
return stage || spotlight || grid;
},
collectMeetingAudioTracks() {
const tracks = [];
const addFromParticipant = (participant) => {
@@ -1564,30 +1590,56 @@ function meetRoom() {
async stopServerAndUploadRecording() {
const hadClientRecorder = !!recordingMediaRecorder;
const res = await fetch(this.config.recordingStopUrl, { method: 'POST', headers: this.headers() });
const blob = await this.stopClientRecording();
this.isRecording = false;
if (!res.ok) {
let recordingUuid = this.activeRecordingUuid;
const res = await fetch(this.config.recordingStopUrl, { method: 'POST', headers: this.headers() });
if (res.ok) {
const data = await res.json().catch(() => ({}));
recordingUuid = data.recording_uuid || recordingUuid;
} else if (res.status !== 404 || !recordingUuid) {
this.activeRecordingUuid = null;
return;
}
const data = await res.json().catch(() => ({}));
const recordingUuid = data.recording_uuid || this.activeRecordingUuid;
if (blob && recordingUuid) {
const uploaded = await this.uploadRecording(blob, recordingUuid);
if (!uploaded) {
window.alert(`Recording upload failed. Open Recordings and try again, or re-record the ${this.sessionNoun()}.`);
window.alert(`Recording upload failed. Open Recordings and refresh shortly, or re-record the ${this.sessionNoun()}.`);
}
} else if (recordingUuid) {
await this.failServerRecording(
recordingUuid,
hadClientRecorder
? `No recording was captured from this ${this.sessionNoun()}.`
: 'Recording capture stopped before any video was saved.',
);
if (hadClientRecorder) {
window.alert(`No recording was captured from this ${this.sessionNoun()}. Start recording again while the ${this.sessionNoun()} is live.`);
}
} else if (recordingUuid && hadClientRecorder) {
window.alert(`No recording was captured from this ${this.sessionNoun()}. Start recording again while the ${this.sessionNoun()} is live.`);
}
this.activeRecordingUuid = null;
},
async abortServerRecording(reason) {
await fetch(this.config.recordingStopUrl, {
method: 'POST',
headers: this.headers(),
body: JSON.stringify({ failed: true, reason }),
});
},
async failServerRecording(recordingUuid, reason) {
const url = `/room/${this.config.sessionUuid}/recordings/${recordingUuid}/fail`;
await fetch(url, {
method: 'POST',
headers: this.headers(),
body: JSON.stringify({ reason }),
});
},
stopClientRecording() {
return new Promise((resolve) => {
const recorder = recordingMediaRecorder;
@@ -1826,7 +1878,7 @@ function meetRoom() {
},
submitEndMeeting() {
if (this.isRecording) {
if (this.isRecording || this.activeRecordingUuid || recordingMediaRecorder) {
this.stopServerAndUploadRecording().finally(() => {
document.getElementById('meet-end-form')?.requestSubmit();
});
@@ -1836,6 +1888,14 @@ function meetRoom() {
this.endMeetingConfirmOpen = false;
},
async submitLeave() {
if (this.isHost && (this.isRecording || this.activeRecordingUuid || recordingMediaRecorder)) {
await this.stopServerAndUploadRecording();
}
document.getElementById('meet-leave-form')?.submit();
},
async uploadFile(event) {
const file = event.target.files?.[0];
if (!file || !this.config.filesUploadUrl) return;
+1 -1
View File
@@ -427,7 +427,7 @@
<div class="meet-toolbar__divider sm:hidden" aria-hidden="true"></div>
<div class="meet-toolbar__actions sm:contents">
<form method="POST" action="{{ route('meet.room.leave', $session) }}" class="inline">
<form id="meet-leave-form" method="POST" action="{{ route('meet.room.leave', $session) }}" class="inline" @submit.prevent="submitLeave()">
@csrf
<button type="submit"
class="rounded-full bg-red-600 p-3 font-medium hover:bg-red-500 sm:px-5 sm:py-3 sm:text-sm"