diff --git a/src/state/CallViewModel/localMember/LocalMember.ts b/src/state/CallViewModel/localMember/LocalMember.ts index daadbe7c..9ef94fe4 100644 --- a/src/state/CallViewModel/localMember/LocalMember.ts +++ b/src/state/CallViewModel/localMember/LocalMember.ts @@ -37,7 +37,7 @@ import { import { type Logger } from "matrix-js-sdk/lib/logger"; import { deepCompare } from "matrix-js-sdk/lib/utils"; -import { constant, type Behavior } from "../../Behavior.ts"; +import { type Behavior } from "../../Behavior.ts"; import { type IConnectionManager } from "../remoteMembers/ConnectionManager.ts"; import { type ObservableScope } from "../../ObservableScope.ts"; import { type Publisher } from "./Publisher.ts"; @@ -68,6 +68,8 @@ export enum TransportState { export enum PublishState { WaitingForUser = "publish_waiting_for_user", + // XXX: This state is removed for now since we do not have full control over + // track publication anymore with the publisher abstraction, might come back in the future? // /** Implies lk connection is connected */ // Starting = "publish_start_publishing", /** Implies lk connection is connected */ @@ -79,6 +81,8 @@ export enum PublishState { export enum TrackState { /** The track is waiting for user input to create tracks (waiting to call `startTracks()`) */ WaitingForUser = "tracks_waiting_for_user", + // XXX: This state is removed for now since we do not have full control over + // track creation anymore with the publisher abstraction, might come back in the future? // /** Implies lk connection is connected */ // Creating = "tracks_creating", /** Implies lk connection is connected */ @@ -154,9 +158,10 @@ export const createLocalMembership$ = ({ matrixRTCSession, }: Props): { /** - * This starts audio and video tracks. They will be reused when calling `requestPublish`. + * This request to start audio and video tracks. + * Can be called early to pre-emptively get media permissions and start devices. */ - startTracks: () => Behavior; + startTracks: () => void; /** * This sets a inner state (shouldPublish) to true and instructs the js-sdk and livekit to keep the user * connected to matrix and livekit. @@ -265,19 +270,10 @@ export const createLocalMembership$ = ({ * The publisher is stored in here an abstracts creating and publishing tracks. */ const publisher$ = new BehaviorSubject(null); - /** - * Extract the tracks from the published. Also reacts to changing publishers. - */ - // const tracks$ = scope.behavior( - // publisher$.pipe(switchMap((p) => (p?.tracks$ ? p.tracks$ : constant([])))), - // ); - // const publishing$ = scope.behavior( - // publisher$.pipe(switchMap((p) => p?.publishing$ ?? constant(false))), - // ); - const startTracks = (): Behavior => { + const startTracks = (): void => { trackStartRequested.resolve(); - return constant(undefined); + // This used to return the tracks, but now they are only accessible via the publisher. }; const requestJoinAndPublish = (): void => { @@ -348,14 +344,6 @@ export const createLocalMembership$ = ({ setPublishError(new UnknownCallError(error as Error)); } } - // XXX Why is that? - // else { - // try { - // await publisher?.stopPublishing(); - // } catch (error) { - // setLivekitError(new UnknownCallError(error as Error)); - // } - // } }, ); @@ -401,16 +389,10 @@ export const createLocalMembership$ = ({ ([ localConnectionState, localTransport, - // tracks, - // publishing, shouldPublish, shouldStartTracks, ]) => { if (!localTransport) return null; - // const hasTracks = tracks.length > 0; - // let trackState: TrackState = TrackState.WaitingForUser; - // if (hasTracks && shouldStartTracks) trackState = TrackState.Ready; - // if (!hasTracks && shouldStartTracks) trackState = TrackState.Creating; const trackState: TrackState = shouldStartTracks ? TrackState.Ready : TrackState.WaitingForUser; diff --git a/src/state/CallViewModel/localMember/Publisher.test.ts b/src/state/CallViewModel/localMember/Publisher.test.ts index 3cc96bc2..38a80bed 100644 --- a/src/state/CallViewModel/localMember/Publisher.test.ts +++ b/src/state/CallViewModel/localMember/Publisher.test.ts @@ -34,7 +34,7 @@ beforeEach(() => { scope = new ObservableScope(); }); -// afterEach(() => scope.end()); +afterEach(() => scope.end()); function createMockLocalTrack(source: Track.Source): LocalTrack { const track = { diff --git a/src/state/CallViewModel/localMember/Publisher.ts b/src/state/CallViewModel/localMember/Publisher.ts index d4ad656c..4428d845 100644 --- a/src/state/CallViewModel/localMember/Publisher.ts +++ b/src/state/CallViewModel/localMember/Publisher.ts @@ -97,7 +97,7 @@ export class Publisher { // it would also prevent the user from seeing their own video/audio preview. // 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 + // The user can still see/hear themselves locally, but remote participants see nothing. private onLocalTrackPublished( localTrackPublication: LocalTrackPublication, ): void { @@ -128,6 +128,15 @@ 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 + * permission prompts. + * + * It also observes mute state changes to update LiveKit microphone/camera states accordingly. + * If a track is not created initially because disabled, it will be created when unmuting. + * + * This call is not blocking anymore, instead callers can listen to the + * `RoomEvent.MediaDevicesError` event in the LiveKit room to be notified of any errors. * */ public async createAndSetupTracks(): Promise { @@ -141,25 +150,21 @@ export class Publisher { const audio = this.muteStates.audio.enabled$.value; const video = this.muteStates.video.enabled$.value; - const enableTracks = async (): Promise => { - if (audio && video) { - // Enable both at once in order to have a single permission prompt! - await lkRoom.localParticipant.enableCameraAndMicrophone(); - } else if (audio) { - await lkRoom.localParticipant.setMicrophoneEnabled(true); - } else if (video) { - await lkRoom.localParticipant.setCameraEnabled(true); - } - return; - }; - // We don't await enableTracks, because livekit could block until the tracks + // We don't await the creation, because livekit could block until the tracks // are fully published, and not only that they are created. // We don't have control on that, localParticipant creates and publishes the tracks // asap. // We are using the `ParticipantEvent.LocalTrackPublished` to be notified // when tracks are actually published, and at that point // we can pause upstream if needed (depending on if startPublishing has been called). - void enableTracks(); + if (audio && video) { + // Enable both at once in order to have a single permission prompt! + void lkRoom.localParticipant.enableCameraAndMicrophone(); + } else if (audio) { + void lkRoom.localParticipant.setMicrophoneEnabled(true); + } else if (video) { + void lkRoom.localParticipant.setCameraEnabled(true); + } return Promise.resolve(); } @@ -233,6 +238,8 @@ export class Publisher { public async stopPublishing(): Promise { this.logger.debug("stopPublishing called"); this.shouldPublish = false; + // Pause upstream will stop sending media to the server, while keeping + // the local MediaStreamTrack active, so the user can still see themselves. await this.pauseUpstreams(this.connection.livekitRoom, [ Track.Source.Microphone, Track.Source.Camera,