(fix): sharing screen and then stopping screenshare causes empty gray box to remain

This commit is contained in:
Alois 2026-04-30 22:51:07 +02:00
commit 1df48c55c1
3 changed files with 61 additions and 28 deletions

View file

@ -7,7 +7,7 @@ import {
ContextMenuTrigger, ContextMenuTrigger,
} from "@tensamin/ui"; } from "@tensamin/ui";
import { focusParticipant, setCallView, useCall } from "../../store"; import { focusParticipant, setCallView, useCall } from "../../store";
import { type Participant } from "livekit-client"; import { Track, type Participant } from "livekit-client";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { useUser, type User } from "@tensamin/user/context"; import { useUser, type User } from "@tensamin/user/context";
import VideoViewer from "../videoViewer"; import VideoViewer from "../videoViewer";
@ -67,6 +67,9 @@ export default function Base({
const focusedParticipantId = useCall((state) => state.focusedParticipantId); const focusedParticipantId = useCall((state) => state.focusedParticipantId);
const view = useCall((state) => state.view); const view = useCall((state) => state.view);
const [user, setUser] = useState<User | null>(null); const [user, setUser] = useState<User | null>(null);
const screenSharePublication = participant?.getTrackPublication(
Track.Source.ScreenShare,
);
useEffect(() => { useEffect(() => {
const participantId = Number(participant?.identity); const participantId = Number(participant?.identity);
@ -139,18 +142,15 @@ export default function Base({
{/* Detect video / user and place here */} {/* Detect video / user and place here */}
{type === "stream" && {type === "stream" &&
Array.from(participant.videoTrackPublications.values()).map( (screenSharePublication?.isSubscribed && screenSharePublication.track ? (
(publication) => <VideoViewer
publication.isSubscribed && publication.track ? ( flush={flush}
<VideoViewer participantId={participant.identity}
flush={flush} publication={screenSharePublication}
participantId={participant.identity} />
publication={publication} ) : (
/> <Loader2 className="animate-spin" />
) : ( ))}
<Loader2 className="animate-spin" />
),
)}
{type === "user" && <p>{user.display}</p>} {type === "user" && <p>{user.display}</p>}
</div> </div>

View file

@ -157,7 +157,7 @@ function getActiveScreenShareParticipantIds(): number[] {
.map((participant) => ({ .map((participant) => ({
participantId: getParticipantId(participant.identity), participantId: getParticipantId(participant.identity),
hasScreenShare: hasScreenShare:
participant.getTrackPublication(Track.Source.ScreenShare) != null, participant.getTrackPublication(Track.Source.ScreenShare)?.track != null,
})) }))
.filter( .filter(
(entry): entry is { participantId: number; hasScreenShare: true } => (entry): entry is { participantId: number; hasScreenShare: true } =>
@ -841,6 +841,8 @@ export function useInitializeCall() {
room.on(RoomEvent.Disconnected, onDisconnected); room.on(RoomEvent.Disconnected, onDisconnected);
room.on(RoomEvent.TrackSubscribed, onTrackSubscribed); room.on(RoomEvent.TrackSubscribed, onTrackSubscribed);
room.on(RoomEvent.TrackUnsubscribed, onTrackUnsubscribed); room.on(RoomEvent.TrackUnsubscribed, onTrackUnsubscribed);
room.on(RoomEvent.TrackPublished, onParticipantStateChange);
room.on(RoomEvent.TrackUnpublished, onParticipantStateChange);
room.on(RoomEvent.ParticipantConnected, onParticipantConnected); room.on(RoomEvent.ParticipantConnected, onParticipantConnected);
room.on(RoomEvent.ParticipantDisconnected, onParticipantDisconnected); room.on(RoomEvent.ParticipantDisconnected, onParticipantDisconnected);
room.on(RoomEvent.TrackMuted, onParticipantStateChange); room.on(RoomEvent.TrackMuted, onParticipantStateChange);
@ -860,6 +862,8 @@ export function useInitializeCall() {
room.off(RoomEvent.Disconnected, onDisconnected); room.off(RoomEvent.Disconnected, onDisconnected);
room.off(RoomEvent.TrackSubscribed, onTrackSubscribed); room.off(RoomEvent.TrackSubscribed, onTrackSubscribed);
room.off(RoomEvent.TrackUnsubscribed, onTrackUnsubscribed); room.off(RoomEvent.TrackUnsubscribed, onTrackUnsubscribed);
room.off(RoomEvent.TrackPublished, onParticipantStateChange);
room.off(RoomEvent.TrackUnpublished, onParticipantStateChange);
room.off(RoomEvent.ParticipantConnected, onParticipantConnected); room.off(RoomEvent.ParticipantConnected, onParticipantConnected);
room.off(RoomEvent.ParticipantDisconnected, onParticipantDisconnected); room.off(RoomEvent.ParticipantDisconnected, onParticipantDisconnected);
room.off(RoomEvent.TrackMuted, onParticipantStateChange); room.off(RoomEvent.TrackMuted, onParticipantStateChange);

View file

@ -1,4 +1,5 @@
import { useLayoutEffect, useMemo, useRef, useState } from "react"; import { RoomEvent } from "livekit-client";
import { useEffect, useLayoutEffect, useMemo, useRef, useState } from "react";
import { useCall } from "../../store"; import { useCall } from "../../store";
import Base from "../../components/modals/base"; import Base from "../../components/modals/base";
@ -16,6 +17,20 @@ export default function View() {
const containerRef = useRef<HTMLDivElement | null>(null); const containerRef = useRef<HTMLDivElement | null>(null);
const [focusedTileSize, setFocusedTileSize] = useState({ width: 0, height: 0 }); const [focusedTileSize, setFocusedTileSize] = useState({ width: 0, height: 0 });
const [isFocusedTileFlush, setIsFocusedTileFlush] = useState(false); const [isFocusedTileFlush, setIsFocusedTileFlush] = useState(false);
const [participantVersion, setParticipantVersion] = useState(0);
useEffect(() => {
const syncParticipants = () => {
setParticipantVersion((version) => version + 1);
};
room.on(RoomEvent.ParticipantConnected, syncParticipants);
room.on(RoomEvent.ParticipantDisconnected, syncParticipants);
return () => {
room.off(RoomEvent.ParticipantConnected, syncParticipants);
room.off(RoomEvent.ParticipantDisconnected, syncParticipants);
};
}, [room]);
const users = useMemo(() => { const users = useMemo(() => {
const participants = [ const participants = [
@ -27,12 +42,17 @@ export default function View() {
const participantId = Number(participant.identity); const participantId = Number(participant.identity);
return Number.isInteger(participantId) && participantId > 0; return Number.isInteger(participantId) && participantId > 0;
}); });
}, [room]); // eslint-disable-next-line
}, [participantVersion, room]);
const userIds = useMemo( const userIds = useMemo(
() => users.map((participant) => Number(participant.identity)), () => users.map((participant) => Number(participant.identity)),
[users], [users],
); );
const activeScreenShareParticipantIdSet = useMemo(
() => new Set(activeScreenShareParticipantIds),
[activeScreenShareParticipantIds],
);
const tiles = useMemo( const tiles = useMemo(
() => [ () => [
@ -45,14 +65,19 @@ export default function View() {
})), })),
...userIds ...userIds
.filter((id) => id !== focusedParticipantId) .filter((id) => id !== focusedParticipantId)
.filter((id) => !activeScreenShareParticipantIds.includes(id)) .filter((id) => !activeScreenShareParticipantIdSet.has(id))
.map((participantId) => ({ .map((participantId) => ({
key: `user:${participantId}`, key: `user:${participantId}`,
kind: "user" as const, kind: "user" as const,
participantId, participantId,
})), })),
], ],
[activeScreenShareParticipantIds, userIds, focusedParticipantId], [
activeScreenShareParticipantIds,
activeScreenShareParticipantIdSet,
userIds,
focusedParticipantId,
],
); );
useLayoutEffect(() => { useLayoutEffect(() => {
@ -103,9 +128,17 @@ export default function View() {
return null; return null;
} }
const focusedParticipant = room.getParticipantByIdentity( function getParticipantById(participantId: number) {
String(focusedParticipantId), if (Number(room.localParticipant.identity) === participantId) {
); return room.localParticipant;
}
return room.getParticipantByIdentity(String(participantId));
}
const focusedParticipant = getParticipantById(focusedParticipantId);
const focusedParticipantHasActiveScreenShare =
activeScreenShareParticipantIdSet.has(focusedParticipantId);
return ( return (
<div ref={containerRef} className="flex h-full w-full items-center justify-center overflow-hidden"> <div ref={containerRef} className="flex h-full w-full items-center justify-center overflow-hidden">
@ -117,10 +150,8 @@ export default function View() {
<div className="overflow-hidden" style={focusedTileSize}> <div className="overflow-hidden" style={focusedTileSize}>
<Base <Base
flush={isFocusedTileFlush} flush={isFocusedTileFlush}
type={focusedParticipant?.isScreenShareEnabled ? "stream" : "user"} type={focusedParticipantHasActiveScreenShare ? "stream" : "user"}
participant={room.getParticipantByIdentity( participant={focusedParticipant}
String(focusedParticipantId),
)}
/> />
</div> </div>
</div> </div>
@ -133,9 +164,7 @@ export default function View() {
<div key={tile.key} className="h-full shrink-0 aspect-video"> <div key={tile.key} className="h-full shrink-0 aspect-video">
<Base <Base
type={tile.kind} type={tile.kind}
participant={room.getParticipantByIdentity( participant={getParticipantById(tile.participantId)}
String(tile.participantId),
)}
/> />
</div> </div>
))} ))}