Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
47 changes: 47 additions & 0 deletions src/state/CallViewModel/localMember/Publisher.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -286,6 +286,53 @@
expect(track!.isUpstreamPaused).toBe(true);
});

it("Resumes upstream for tracks published after startPublishing was called (slow camera)", async () => {
videoEnabled$.next(true);

// Simulate that the upstream ended up paused by the time the track gets
// published (e.g. paused while it was still unpublished).
const originalPublishTrack = localParticipant.publishTrack;
vi.mocked(localParticipant).publishTrack = vi
.fn()
.mockImplementation(async (track: LocalTrack) => {
await track.pauseUpstream();
return originalPublishTrack(track);
});

const resolvers = Promise.withResolvers<void>();
createTrackLock = resolvers.promise;

// Track creation is slow (e.g. camera hardware takes time to open)
await publisher.createAndSetupTracks();
// startPublishing runs before the camera track exists, so its
// resumeUpstreams call finds nothing to resume
await publisher.startPublishing();
expect(
localParticipant.getTrackPublication(Track.Source.Camera),
).toBeUndefined();

// The camera opens and the track gets published
resolvers.resolve();
await flushPromises();

const track = localParticipant.getTrackPublication(
Track.Source.Camera,
)?.track;
expect(track).toBeDefined();
expect(track!.resumeUpstream).toHaveBeenCalled();

Check failure on line 322 in src/state/CallViewModel/localMember/Publisher.test.ts

View workflow job for this annotation

GitHub Actions / Run unit tests

[unit] src/state/CallViewModel/localMember/Publisher.test.ts > Publisher > Resumes upstream for tracks published after startPublishing was called (slow camera)

AssertionError: expected "vi.fn()" to be called at least once ❯ src/state/CallViewModel/localMember/Publisher.test.ts:322:35
expect(track!.isUpstreamPaused).toBe(false);
});

it("Does not pause tracks published after the publisher was destroyed", async () => {
await publisher.destroy();

const track = createMockLocalTrack(Track.Source.Camera);
await localParticipant.publishTrack(track);
await flushPromises();

expect(track.pauseUpstream).not.toHaveBeenCalled();
});

it("Ensure resume upstream when published is called", async () => {
videoEnabled$.next(true);
await publisher.createAndSetupTracks();
Expand Down Expand Up @@ -391,7 +438,7 @@
try {
expect(localParticipant.publishTrack).not.toHaveBeenCalled();
} catch {
expect(track!.mute).toHaveBeenCalled();

Check failure on line 441 in src/state/CallViewModel/localMember/Publisher.test.ts

View workflow job for this annotation

GitHub Actions / Run unit tests

[unit] src/state/CallViewModel/localMember/Publisher.test.ts > Bug fix > wrongly publish tracks while muted

AssertionError: expected "vi.fn()" to be called at least once ❯ src/state/CallViewModel/localMember/Publisher.test.ts:441:27
expect(track!.isMuted).toBe(true);
}
await publisher.destroy();
Expand Down
22 changes: 18 additions & 4 deletions src/state/CallViewModel/localMember/Publisher.ts
Original file line number Diff line number Diff line change
Expand Up @@ -80,11 +80,15 @@ export class Publisher {

this.connection.livekitRoom.localParticipant.on(
ParticipantEvent.LocalTrackPublished,
this.onLocalTrackPublished.bind(this),
this.onLocalTrackPublished,
);
}

public async destroy(): Promise<void> {
this.connection.livekitRoom.localParticipant.off(
ParticipantEvent.LocalTrackPublished,
this.onLocalTrackPublished,
);
this.scope.end();
this.logger.info("Scope ended -> unset handler");
this.muteStates.audio.unsetHandler();
Expand All @@ -106,16 +110,26 @@ export class Publisher {
// So for that we use pauseUpStream(): Stops sending media to the server by replacing
// the sender track with null, but keeps the local MediaStreamTrack active.
// The user can still see/hear themselves locally, but remote participants see nothing.
private onLocalTrackPublished(
private onLocalTrackPublished = (
localTrackPublication: LocalTrackPublication,
): void {
): void => {
this.logger.info("Local track published", localTrackPublication);
const lkRoom = this.connection.livekitRoom;
if (!this.shouldPublish) {
this.logger.debug("Not publishing, pausing upstream");
this.pauseUpstreams(lkRoom, [localTrackPublication.source]).catch((e) => {
this.logger.error(`Failed to pause upstreams`, e);
});
} else {
this.logger.info(`resumeUpstream onLocalTrackPublished`);
// If startPublishing() ran before this track existed (track creation is
// not awaited and e.g. camera hardware can take a while to open), its
// resumeUpstreams() call found no track and did nothing. Resume here so
// that a track published after startPublishing() actually sends media.
// This is a no-op if the upstream is not paused.
localTrackPublication.resumeUpstream().catch((e) => {
this.logger.error(`Failed to resume upstreams`, e);
});
}
if (localTrackPublication.source === Track.Source.Microphone) {
const muteState = this.muteStates.audio;
Expand Down Expand Up @@ -156,7 +170,7 @@ export class Publisher {
}
}
}
}
};
/**
* Create and setup local audio and video tracks based on the current mute states.
* It creates the tracks only if audio and/or video is enabled, to avoid unnecessary
Expand Down
Loading