fix(lotus): report an ended screenshare even when LiveKit unpublishes first (#39)
LiveKit's own ended handler runs before ours and unpublishes the share; a listener removed during that dispatch never fires. Decide from the track's readyState on LocalTrackUnpublished as well. Verified end to end: closing the shared source now toasts in the host. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PPmy3tPq869XDW4njjVaKA
This commit is contained in:
co-authored by
Claude Opus 5
parent
e7c27dd8a3
commit
9f472fd710
@@ -132,6 +132,23 @@ describe("startLotusScreenshareWatch", () => {
|
|||||||
stop();
|
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", () => {
|
it("ignores non-screenshare publications and stops watching on unpublish", () => {
|
||||||
const { emit, stop } = start();
|
const { emit, stop } = start();
|
||||||
const cam = new FakeTrack();
|
const cam = new FakeTrack();
|
||||||
|
|||||||
@@ -45,6 +45,7 @@ export function startLotusScreenshareWatch(
|
|||||||
|
|
||||||
const attach = (room: LivekitRoom): void => {
|
const attach = (room: LivekitRoom): void => {
|
||||||
let cleanupTrack: (() => void) | undefined;
|
let cleanupTrack: (() => void) | undefined;
|
||||||
|
let endedHook: (() => void) | undefined;
|
||||||
|
|
||||||
const watchPublication = (pub: LocalTrackPublication): void => {
|
const watchPublication = (pub: LocalTrackPublication): void => {
|
||||||
if (pub.source !== Track.Source.ScreenShare) return;
|
if (pub.source !== Track.Source.ScreenShare) return;
|
||||||
@@ -55,7 +56,12 @@ export function startLotusScreenshareWatch(
|
|||||||
let sentNoFrames = false;
|
let sentNoFrames = false;
|
||||||
let sentAlone = 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 => {
|
const onMute = (): void => {
|
||||||
if (sentNoFrames) return;
|
if (sentNoFrames) return;
|
||||||
noFramesTimer = setTimeout(() => {
|
noFramesTimer = setTimeout(() => {
|
||||||
@@ -67,6 +73,7 @@ export function startLotusScreenshareWatch(
|
|||||||
if (noFramesTimer !== undefined) clearTimeout(noFramesTimer);
|
if (noFramesTimer !== undefined) clearTimeout(noFramesTimer);
|
||||||
noFramesTimer = undefined;
|
noFramesTimer = undefined;
|
||||||
};
|
};
|
||||||
|
endedHook = onEnded;
|
||||||
mst?.addEventListener("ended", onEnded);
|
mst?.addEventListener("ended", onEnded);
|
||||||
mst?.addEventListener("mute", onMute);
|
mst?.addEventListener("mute", onMute);
|
||||||
mst?.addEventListener("unmute", onUnmute);
|
mst?.addEventListener("unmute", onUnmute);
|
||||||
@@ -86,11 +93,17 @@ export function startLotusScreenshareWatch(
|
|||||||
if (noFramesTimer !== undefined) clearTimeout(noFramesTimer);
|
if (noFramesTimer !== undefined) clearTimeout(noFramesTimer);
|
||||||
clearInterval(aloneTimer);
|
clearInterval(aloneTimer);
|
||||||
cleanupTrack = undefined;
|
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 => {
|
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);
|
room.on(RoomEvent.LocalTrackPublished, watchPublication);
|
||||||
|
|||||||
Reference in New Issue
Block a user