diff --git a/src/lotus/lotusScreenshareWatch.test.ts b/src/lotus/lotusScreenshareWatch.test.ts index 64cbee40..d7670b61 100644 --- a/src/lotus/lotusScreenshareWatch.test.ts +++ b/src/lotus/lotusScreenshareWatch.test.ts @@ -132,6 +132,23 @@ describe("startLotusScreenshareWatch", () => { stop(); }); + it("reports ended from the unpublish when LiveKit's handler ran first", () => { + const { emit, stop } = start(); + const share = new FakeTrack() as FakeTrack & { readyState: string }; + share.readyState = "ended"; + const pub = { + source: Track.Source.ScreenShare, + track: { mediaStreamTrack: share }, + }; + emit(RoomEvent.LocalTrackPublished, pub); + emit(RoomEvent.LocalTrackUnpublished, pub); + share.dispatchEvent(new Event("ended")); + expect(sent).toEqual([ + { action: "io.lotus.screenshare_notice", data: { kind: "ended" } }, + ]); + stop(); + }); + it("ignores non-screenshare publications and stops watching on unpublish", () => { const { emit, stop } = start(); const cam = new FakeTrack(); diff --git a/src/lotus/lotusScreenshareWatch.ts b/src/lotus/lotusScreenshareWatch.ts index 53a7e056..ee31dc9a 100644 --- a/src/lotus/lotusScreenshareWatch.ts +++ b/src/lotus/lotusScreenshareWatch.ts @@ -45,6 +45,7 @@ export function startLotusScreenshareWatch( const attach = (room: LivekitRoom): void => { let cleanupTrack: (() => void) | undefined; + let endedHook: (() => void) | undefined; const watchPublication = (pub: LocalTrackPublication): void => { if (pub.source !== Track.Source.ScreenShare) return; @@ -55,7 +56,12 @@ export function startLotusScreenshareWatch( let sentNoFrames = false; let sentAlone = false; - const onEnded = (): void => notify("ended"); + let sentEnded = false; + const onEnded = (): void => { + if (sentEnded) return; + sentEnded = true; + notify("ended"); + }; const onMute = (): void => { if (sentNoFrames) return; noFramesTimer = setTimeout(() => { @@ -67,6 +73,7 @@ export function startLotusScreenshareWatch( if (noFramesTimer !== undefined) clearTimeout(noFramesTimer); noFramesTimer = undefined; }; + endedHook = onEnded; mst?.addEventListener("ended", onEnded); mst?.addEventListener("mute", onMute); mst?.addEventListener("unmute", onUnmute); @@ -86,11 +93,17 @@ export function startLotusScreenshareWatch( if (noFramesTimer !== undefined) clearTimeout(noFramesTimer); clearInterval(aloneTimer); cleanupTrack = undefined; + endedHook = undefined; }; }; + // LiveKit's own `ended` handler runs first and unpublishes the share; a + // listener removed during that dispatch never fires, so decide from the + // track's state here as well. const onUnpublished = (pub: LocalTrackPublication): void => { - if (pub.source === Track.Source.ScreenShare) cleanupTrack?.(); + if (pub.source !== Track.Source.ScreenShare) return; + if (pub.track?.mediaStreamTrack?.readyState === "ended") endedHook?.(); + cleanupTrack?.(); }; room.on(RoomEvent.LocalTrackPublished, watchPublication);