feat: Voice UI overhaul — fix black tiles, focus mode, track lifecycle

- Fix black screen when streams end by checking track readyState === 'live'
- Add track 'ended' event listeners in VoiceUser for immediate fallback to avatar
- Add visible "Grid" button to exit focus mode (replaces undiscoverable double-click)
- Auto-focus screen sharers, auto-unfocus when they stop sharing
- Add LIVE badge, speaking glow shadow, screen share object-contain
- Add TrackMuted/TrackUnmuted/ActiveSpeakersChanged Room events
- Fix PiP video attachment with shouldShow dependency
This commit is contained in:
Jannis Braun
2026-02-19 05:45:31 +01:00
parent 2e309bf959
commit cd48261f54
8 changed files with 218 additions and 88 deletions
+12 -3
View File
@@ -81,12 +81,15 @@ export function useLiveKit() {
const track = pub.track;
if (!track)
return;
const mt = track.mediaStreamTrack;
if (!mt || mt.readyState !== 'live')
return;
if (pub.source === Track.Source.Microphone)
audioTrack = track.mediaStreamTrack;
audioTrack = mt;
else if (pub.source === Track.Source.Camera)
videoTrack = track.mediaStreamTrack;
videoTrack = mt;
else if (pub.source === Track.Source.ScreenShare)
screenTrack = track.mediaStreamTrack;
screenTrack = mt;
});
allParticipants.push({ identity: p.identity, userId, username, isSpeaking: p.isSpeaking, isMuted: !p.isMicrophoneEnabled, isCameraOn: p.isCameraEnabled, isScreenSharing: p.isScreenShareEnabled, isLocal, audioTrack, videoTrack, screenTrack });
};
@@ -122,6 +125,9 @@ export function useLiveKit() {
newRoom.on(RoomEvent.TrackUnsubscribed, guardedUpdate);
newRoom.on(RoomEvent.LocalTrackPublished, guardedUpdate);
newRoom.on(RoomEvent.LocalTrackUnpublished, guardedUpdate);
newRoom.on(RoomEvent.TrackMuted, guardedUpdate);
newRoom.on(RoomEvent.TrackUnmuted, guardedUpdate);
newRoom.on(RoomEvent.ActiveSpeakersChanged, guardedUpdate);
newRoom.on(RoomEvent.ConnectionStateChanged, (state) => {
if (roomRef.current === newRoom) {
const connected = state === ConnectionState.Connected;
@@ -192,6 +198,9 @@ export function useLiveKit() {
newRoom.on(RoomEvent.TrackUnsubscribed, guardedUpdate);
newRoom.on(RoomEvent.LocalTrackPublished, guardedUpdate);
newRoom.on(RoomEvent.LocalTrackUnpublished, guardedUpdate);
newRoom.on(RoomEvent.TrackMuted, guardedUpdate);
newRoom.on(RoomEvent.TrackUnmuted, guardedUpdate);
newRoom.on(RoomEvent.ActiveSpeakersChanged, guardedUpdate);
newRoom.on(RoomEvent.ConnectionStateChanged, (state) => {
if (roomRef.current === newRoom) {
const connected = state === ConnectionState.Connected;
+11 -3
View File
@@ -115,9 +115,11 @@ export function useLiveKit() {
p.trackPublications.forEach((pub) => {
const track = pub.track;
if (!track) return;
if (pub.source === Track.Source.Microphone) audioTrack = track.mediaStreamTrack;
else if (pub.source === Track.Source.Camera) videoTrack = track.mediaStreamTrack;
else if (pub.source === Track.Source.ScreenShare) screenTrack = track.mediaStreamTrack;
const mt = track.mediaStreamTrack;
if (!mt || mt.readyState !== 'live') return;
if (pub.source === Track.Source.Microphone) audioTrack = mt;
else if (pub.source === Track.Source.Camera) videoTrack = mt;
else if (pub.source === Track.Source.ScreenShare) screenTrack = mt;
});
allParticipants.push({ identity: p.identity, userId, username, isSpeaking: p.isSpeaking, isMuted: !p.isMicrophoneEnabled, isCameraOn: p.isCameraEnabled, isScreenSharing: p.isScreenShareEnabled, isLocal, audioTrack, videoTrack, screenTrack });
};
@@ -145,6 +147,9 @@ export function useLiveKit() {
newRoom.on(RoomEvent.TrackUnsubscribed, guardedUpdate);
newRoom.on(RoomEvent.LocalTrackPublished, guardedUpdate);
newRoom.on(RoomEvent.LocalTrackUnpublished, guardedUpdate);
newRoom.on(RoomEvent.TrackMuted, guardedUpdate);
newRoom.on(RoomEvent.TrackUnmuted, guardedUpdate);
newRoom.on(RoomEvent.ActiveSpeakersChanged, guardedUpdate);
newRoom.on(RoomEvent.ConnectionStateChanged, (state) => {
if (roomRef.current === newRoom) {
const connected = state === ConnectionState.Connected;
@@ -184,6 +189,9 @@ export function useLiveKit() {
newRoom.on(RoomEvent.TrackUnsubscribed, guardedUpdate);
newRoom.on(RoomEvent.LocalTrackPublished, guardedUpdate);
newRoom.on(RoomEvent.LocalTrackUnpublished, guardedUpdate);
newRoom.on(RoomEvent.TrackMuted, guardedUpdate);
newRoom.on(RoomEvent.TrackUnmuted, guardedUpdate);
newRoom.on(RoomEvent.ActiveSpeakersChanged, guardedUpdate);
newRoom.on(RoomEvent.ConnectionStateChanged, (state) => {
if (roomRef.current === newRoom) {
const connected = state === ConnectionState.Connected;