diff --git a/resources/css/app.css b/resources/css/app.css index 633d7f8..af0e60b 100644 --- a/resources/css/app.css +++ b/resources/css/app.css @@ -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; diff --git a/resources/js/meet-room.js b/resources/js/meet-room.js index 400944f..f27f1ae 100644 --- a/resources/js/meet-room.js +++ b/resources/js/meet-room.js @@ -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(); diff --git a/resources/views/meet/room/show.blade.php b/resources/views/meet/room/show.blade.php index 1afecd8..aed7cc3 100644 --- a/resources/views/meet/room/show.blade.php +++ b/resources/views/meet/room/show.blade.php @@ -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' }}" diff --git a/resources/views/meet/rooms/show.blade.php b/resources/views/meet/rooms/show.blade.php index 4c293e0..2280ba3 100644 --- a/resources/views/meet/rooms/show.blade.php +++ b/resources/views/meet/rooms/show.blade.php @@ -23,19 +23,19 @@

Join link

- +
-
- Download iCal - QR code +
+ Download iCal + QR code @if ($room->sessions->isNotEmpty()) - Attendance CSV + Attendance CSV @endif - Manage invitations + Manage invitations @if ($room->isWebinar()) - Registrations - Registration page + Registrations + Registration page @endif