Fix meeting audio playback and restyle room detail actions.
Deploy Ladill Meet / deploy (push) Successful in 39s
Deploy Ladill Meet / deploy (push) Successful in 39s
Unlock LiveKit audio after user gesture, respect join mute/video settings, and replace plain links with btn-secondary pills on the meeting details page. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -153,6 +153,46 @@ html.qr-mobile-page body {
|
||||
padding: 0.625rem 1.25rem;
|
||||
}
|
||||
|
||||
.btn-secondary {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 0.5rem;
|
||||
border-radius: 9999px;
|
||||
border: 1px solid rgb(226 232 240);
|
||||
background-color: #fff;
|
||||
padding: 0.5rem 1rem;
|
||||
font-size: 0.875rem;
|
||||
line-height: 1.25rem;
|
||||
font-weight: 600;
|
||||
color: rgb(51 65 85);
|
||||
box-shadow: 0 1px 2px 0 rgb(0 0 0 / 0.05);
|
||||
transition: background-color 0.15s ease, border-color 0.15s ease;
|
||||
}
|
||||
|
||||
.btn-secondary:hover {
|
||||
background-color: rgb(248 250 252);
|
||||
border-color: rgb(203 213 225);
|
||||
}
|
||||
|
||||
.btn-secondary:disabled {
|
||||
opacity: 0.6;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.btn-secondary-sm {
|
||||
padding: 0.375rem 0.75rem;
|
||||
font-size: 0.75rem;
|
||||
line-height: 1rem;
|
||||
}
|
||||
|
||||
.btn-group {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.btn-fab {
|
||||
display: inline-flex;
|
||||
height: 2.75rem;
|
||||
|
||||
@@ -30,6 +30,9 @@ function meetRoom() {
|
||||
captionText: '',
|
||||
displayName: '',
|
||||
canPublish: true,
|
||||
muteOnJoin: true,
|
||||
videoOffOnJoin: false,
|
||||
audioPlaybackBlocked: false,
|
||||
isWebinar: false,
|
||||
qaItems: [],
|
||||
activePolls: [],
|
||||
@@ -95,6 +98,8 @@ function meetRoom() {
|
||||
this.isHost = el.dataset.isHost === '1';
|
||||
this.canPublish = el.dataset.canPublish !== '0';
|
||||
this.isWebinar = el.dataset.isWebinar === '1';
|
||||
this.muteOnJoin = el.dataset.muteOnJoin === '1';
|
||||
this.videoOffOnJoin = el.dataset.videoOffOnJoin === '1';
|
||||
this.isRecording = el.dataset.recordingActive === '1';
|
||||
this.isLocked = el.dataset.locked === '1';
|
||||
this.watermark = el.dataset.watermark === '1';
|
||||
@@ -170,6 +175,9 @@ function meetRoom() {
|
||||
|
||||
room.on(RoomEvent.TrackSubscribed, (track, publication, participant) => {
|
||||
this.attachTrack(track, publication, participant);
|
||||
if (track.kind === Track.Kind.Audio) {
|
||||
this.ensureAudioPlayback();
|
||||
}
|
||||
});
|
||||
room.on(RoomEvent.TrackUnsubscribed, (track, publication, participant) => {
|
||||
track.detach();
|
||||
@@ -218,6 +226,15 @@ function meetRoom() {
|
||||
room.on(RoomEvent.Disconnected, () => {
|
||||
this.connectionStatus = 'Disconnected';
|
||||
});
|
||||
room.on(RoomEvent.AudioPlaybackStatusChanged, () => {
|
||||
this.audioPlaybackBlocked = room ? !room.canPlaybackAudio : false;
|
||||
if (this.audioPlaybackBlocked) {
|
||||
this.connectionStatus = 'Connected — click anywhere to enable audio';
|
||||
this.registerAudioUnlockGesture();
|
||||
} else if (this.connectionStatus.includes('enable audio')) {
|
||||
this.connectionStatus = 'Connected';
|
||||
}
|
||||
});
|
||||
|
||||
await room.prepareConnection(this.config.url, this.config.token);
|
||||
|
||||
@@ -240,6 +257,8 @@ function meetRoom() {
|
||||
this.isVideoOff = true;
|
||||
}
|
||||
|
||||
await this.ensureAudioPlayback();
|
||||
|
||||
this.setupCaptionPipeline();
|
||||
|
||||
this.updateParticipantTile(room.localParticipant);
|
||||
@@ -275,12 +294,24 @@ function meetRoom() {
|
||||
},
|
||||
|
||||
async enableLocalMedia(activeRoom, attempts = 3) {
|
||||
const wantMic = !this.muteOnJoin || this.isHost;
|
||||
const wantCamera = !this.videoOffOnJoin;
|
||||
|
||||
for (let attempt = 1; attempt <= attempts; attempt += 1) {
|
||||
try {
|
||||
await activeRoom.localParticipant.setMicrophoneEnabled(true);
|
||||
await activeRoom.localParticipant.setCameraEnabled(false);
|
||||
await activeRoom.localParticipant.setMicrophoneEnabled(wantMic);
|
||||
await activeRoom.localParticipant.setCameraEnabled(wantCamera);
|
||||
this.syncLocalMediaState();
|
||||
this.updateParticipantTile(activeRoom.localParticipant);
|
||||
|
||||
if (wantCamera) {
|
||||
activeRoom.localParticipant.videoTrackPublications.forEach((pub) => {
|
||||
if (pub.track && !pub.isMuted && pub.source !== Track.Source.ScreenShare) {
|
||||
this.attachTrack(pub.track, pub, activeRoom.localParticipant);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
this.connectionStatus = 'Connected';
|
||||
return;
|
||||
} catch (mediaError) {
|
||||
@@ -299,6 +330,40 @@ function meetRoom() {
|
||||
}
|
||||
},
|
||||
|
||||
async ensureAudioPlayback() {
|
||||
if (!room || room.canPlaybackAudio) {
|
||||
this.audioPlaybackBlocked = false;
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await room.startAudio();
|
||||
this.audioPlaybackBlocked = false;
|
||||
if (this.connectionStatus.includes('enable audio')) {
|
||||
this.connectionStatus = 'Connected';
|
||||
}
|
||||
} catch (e) {
|
||||
this.audioPlaybackBlocked = true;
|
||||
this.registerAudioUnlockGesture();
|
||||
}
|
||||
},
|
||||
|
||||
registerAudioUnlockGesture() {
|
||||
if (this._audioUnlockRegistered) return;
|
||||
this._audioUnlockRegistered = true;
|
||||
|
||||
const unlock = async () => {
|
||||
await this.ensureAudioPlayback();
|
||||
if (!this.audioPlaybackBlocked) {
|
||||
document.removeEventListener('click', unlock, true);
|
||||
document.removeEventListener('keydown', unlock, true);
|
||||
}
|
||||
};
|
||||
|
||||
document.addEventListener('click', unlock, true);
|
||||
document.addEventListener('keydown', unlock, true);
|
||||
},
|
||||
|
||||
syncLocalMediaState() {
|
||||
if (!room) return;
|
||||
|
||||
@@ -502,6 +567,8 @@ function meetRoom() {
|
||||
|
||||
setupCaptionPipeline() {
|
||||
if (!this.liveCaptions || !this.config.captionUrl) return;
|
||||
if (room?.localParticipant?.isMicrophoneEnabled) return;
|
||||
|
||||
const SpeechRecognition = window.SpeechRecognition || window.webkitSpeechRecognition;
|
||||
if (!SpeechRecognition) return;
|
||||
|
||||
@@ -653,6 +720,7 @@ function meetRoom() {
|
||||
|
||||
async toggleMute() {
|
||||
if (!room) return;
|
||||
await this.ensureAudioPlayback();
|
||||
const enabled = room.localParticipant.isMicrophoneEnabled;
|
||||
await room.localParticipant.setMicrophoneEnabled(!enabled);
|
||||
this.syncLocalMediaState();
|
||||
@@ -661,6 +729,7 @@ function meetRoom() {
|
||||
|
||||
async toggleVideo() {
|
||||
if (!room) return;
|
||||
await this.ensureAudioPlayback();
|
||||
const enabled = room.localParticipant.isCameraEnabled;
|
||||
await room.localParticipant.setCameraEnabled(!enabled);
|
||||
this.syncLocalMediaState();
|
||||
|
||||
@@ -44,6 +44,8 @@
|
||||
data-unlock-url="{{ route('meet.room.unlock', $session) }}"
|
||||
data-caption-url="{{ route('meet.room.caption', $session) }}"
|
||||
data-live-captions="{{ $room->setting('live_captions') ? '1' : '0' }}"
|
||||
data-mute-on-join="{{ $room->setting('mute_on_join', true) ? '1' : '0' }}"
|
||||
data-video-off-on-join="{{ $room->setting('video_off_on_join', false) ? '1' : '0' }}"
|
||||
data-watermark="{{ $watermark ? '1' : '0' }}"
|
||||
data-locked="{{ $session->is_locked ? '1' : '0' }}"
|
||||
data-recording-active="{{ $activeRecording ? '1' : '0' }}"
|
||||
|
||||
@@ -23,19 +23,19 @@
|
||||
<h2 class="text-sm font-medium text-slate-700">Join link</h2>
|
||||
<div class="mt-2 flex gap-2">
|
||||
<input type="text" readonly value="{{ $room->joinUrl() }}" class="flex-1 rounded-lg border-slate-300 bg-slate-50 text-sm font-mono">
|
||||
<button type="button" onclick="navigator.clipboard.writeText('{{ $room->joinUrl() }}')" class="rounded-lg border border-slate-300 px-3 py-2 text-sm hover:bg-slate-50">Copy</button>
|
||||
<button type="button" onclick="navigator.clipboard.writeText('{{ $room->joinUrl() }}')" class="btn-secondary btn-secondary-sm shrink-0">Copy</button>
|
||||
</div>
|
||||
|
||||
<div class="mt-4 flex flex-wrap gap-3 text-sm">
|
||||
<a href="{{ route('meet.rooms.ical', $room) }}" class="text-indigo-600 hover:underline">Download iCal</a>
|
||||
<a href="{{ route('meet.rooms.qr', $room) }}" target="_blank" class="text-indigo-600 hover:underline">QR code</a>
|
||||
<div class="btn-group mt-4">
|
||||
<a href="{{ route('meet.rooms.ical', $room) }}" class="btn-secondary btn-secondary-sm">Download iCal</a>
|
||||
<a href="{{ route('meet.rooms.qr', $room) }}" target="_blank" class="btn-secondary btn-secondary-sm">QR code</a>
|
||||
@if ($room->sessions->isNotEmpty())
|
||||
<a href="{{ route('meet.rooms.attendance', $room) }}" class="text-indigo-600 hover:underline">Attendance CSV</a>
|
||||
<a href="{{ route('meet.rooms.attendance', $room) }}" class="btn-secondary btn-secondary-sm">Attendance CSV</a>
|
||||
@endif
|
||||
<a href="{{ route('meet.invitations.index', $room) }}" class="text-indigo-600 hover:underline">Manage invitations</a>
|
||||
<a href="{{ route('meet.invitations.index', $room) }}" class="btn-secondary btn-secondary-sm">Manage invitations</a>
|
||||
@if ($room->isWebinar())
|
||||
<a href="{{ route('meet.webinar.registrations', $room) }}" class="text-indigo-600 hover:underline">Registrations</a>
|
||||
<a href="{{ route('meet.webinar.register', $room) }}" target="_blank" class="text-indigo-600 hover:underline">Registration page</a>
|
||||
<a href="{{ route('meet.webinar.registrations', $room) }}" class="btn-secondary btn-secondary-sm">Registrations</a>
|
||||
<a href="{{ route('meet.webinar.register', $room) }}" target="_blank" class="btn-secondary btn-secondary-sm">Registration page</a>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user