More testing and cleaning up

This commit is contained in:
Timo K
2025-11-27 14:42:23 +01:00
parent d22d7460fe
commit e5117f705d
4 changed files with 321 additions and 127 deletions

View File

@@ -22,10 +22,12 @@ import {
catchError,
combineLatest,
distinctUntilChanged,
from,
map,
type Observable,
of,
scan,
startWith,
switchMap,
tap,
} from "rxjs";
@@ -54,13 +56,13 @@ export enum LivekitState {
Error = "error",
/** Not even a transport is available to the LocalMembership */
WaitingForTransport = "waiting_for_transport",
/** A transport is and we are loading the connection based on the transport */
Connecting = "connecting",
InitialisingPublisher = "uninitialized",
/** A connection appeared so we can initialise the publisher */
WaitingForConnection = "waiting_for_connection",
/** Connection and transport arrived, publisher Initialized */
Initialized = "Initialized",
CreatingTracks = "creating_tracks",
ReadyToPublish = "ready_to_publish",
WaitingToPublish = "publishing",
WaitingToPublish = "waiting_to_publish",
Connected = "connected",
Disconnected = "disconnected",
Disconnecting = "disconnecting",
@@ -69,8 +71,7 @@ export enum LivekitState {
type LocalMemberLivekitState =
| { state: LivekitState.Error; error: ElementCallError }
| { state: LivekitState.WaitingForTransport }
| { state: LivekitState.Connecting }
| { state: LivekitState.InitialisingPublisher }
| { state: LivekitState.WaitingForConnection }
| { state: LivekitState.Initialized }
| { state: LivekitState.CreatingTracks }
| { state: LivekitState.ReadyToPublish }
@@ -163,12 +164,10 @@ export const createLocalMembership$ = ({
* Callback to toggle screen sharing. If null, screen sharing is not possible.
*/
toggleScreenSharing: (() => void) | null;
tracks$: Behavior<LocalTrack[]>;
participant$: Behavior<LocalParticipant | null>;
connection$: Behavior<Connection | null>;
homeserverConnected$: Behavior<boolean>;
// deprecated fields
/** @deprecated use state instead*/
connected$: Behavior<boolean>;
// this needs to be discussed
/** @deprecated use state instead*/
reconnecting$: Behavior<boolean>;
@@ -217,20 +216,19 @@ export const createLocalMembership$ = ({
),
);
const localConnectionState$ = localConnection$.pipe(
switchMap((connection) => (connection ? connection.state$ : of(null))),
);
// /**
// * Whether we are "fully" connected to the call. Accounts for both the
// * connection to the MatrixRTC session and the LiveKit publish connection.
// */
// // TODO use this in combination with the MemberState.
const connected$ = scope.behavior(
and$(
homeserverConnected$,
localConnection$.pipe(
switchMap((c) =>
c
? c.state$.pipe(map((state) => state.state === "ConnectedToLkRoom"))
: of(false),
),
localConnectionState$.pipe(
map((state) => (state ? state.state === "ConnectedToLkRoom" : false)),
),
),
);
@@ -259,7 +257,7 @@ export const createLocalMembership$ = ({
// This should be used in a combineLatest with publisher$ to connect.
// to make it possible to call startTracks before the preferredTransport$ has resolved.
const trackStartRequested$ = new BehaviorSubject(false);
const trackStartRequested = Promise.withResolvers<void>();
// This should be used in a combineLatest with publisher$ to connect.
// to make it possible to call startTracks before the preferredTransport$ has resolved.
@@ -273,19 +271,21 @@ export const createLocalMembership$ = ({
* Extract the tracks from the published. Also reacts to changing publishers.
*/
const tracks$ = scope.behavior(
publisher$.pipe(switchMap((p) => (p ? p.tracks$ : constant([])))),
publisher$.pipe(switchMap((p) => (p?.tracks$ ? p.tracks$ : constant([])))),
);
const publishing$ = scope.behavior(
publisher$.pipe(switchMap((p) => (p ? p.publishing$ : constant(false)))),
publisher$.pipe(
switchMap((p) => (p?.publishing$ ? p.publishing$ : constant(false))),
),
);
const startTracks = (): Behavior<LocalTrack[]> => {
trackStartRequested$.next(true);
trackStartRequested.resolve();
return tracks$;
};
const requestConnect = (): void => {
trackStartRequested$.next(true);
trackStartRequested.resolve();
connectRequested$.next(true);
};
@@ -310,37 +310,18 @@ export const createLocalMembership$ = ({
});
});
// const mutestate= publisher$.pipe(switchMap((publisher) => {
// return publisher.muteState$
// });
// For each publisher create the descired tracks
// If we recreate a new publisher we remember the trackStartRequested$ value and immediately create the tracks
// THIS might be fine without a reconcile. There is no cleanup needed. We always get a working publisher
// track start request can than just toggle the tracks.
// TODO does this need `reconcile` to make sure we wait for createAndSetupTracks before we stop tracks?
combineLatest([publisher$, trackStartRequested$]).subscribe(
([publisher, shouldStartTracks]) => {
if (publisher && shouldStartTracks) {
publisher.createAndSetupTracks().catch(
// TODO make this set some error state
(e) => logger.error(e),
);
} else if (publisher) {
publisher.stopTracks();
}
},
);
// Use reconcile here to not run concurrent createAndSetupTracks calls
// `tracks$` will update once they are ready.
scope.reconcile(
scope.behavior(combineLatest([publisher$, trackStartRequested$])),
async ([publisher, shouldStartTracks]) => {
if (publisher && shouldStartTracks) {
scope.behavior(
combineLatest([publisher$, tracks$, from(trackStartRequested.promise)]),
null,
),
async (valueIfReady) => {
if (!valueIfReady) return;
const [publisher, tracks] = valueIfReady;
if (publisher && tracks.length === 0) {
await publisher.createAndSetupTracks().catch((e) => logger.error(e));
} else if (publisher) {
publisher.stopTracks();
}
},
);
@@ -349,8 +330,7 @@ export const createLocalMembership$ = ({
scope.reconcile(
scope.behavior(combineLatest([publisher$, tracks$, connectRequested$])),
async ([publisher, tracks, shouldConnect]) => {
if (shouldConnect === publisher?.publishing$.value)
return Promise.resolve();
if (shouldConnect === publisher?.publishing$.value) return;
if (tracks.length !== 0 && shouldConnect) {
try {
await publisher?.startPublishing();
@@ -374,46 +354,53 @@ export const createLocalMembership$ = ({
logger.error("Multiple Livkit Errors:", e);
else fatalLivekitError$.next(e);
};
const livekitState$: Observable<LocalMemberLivekitState> = combineLatest([
publisher$,
localTransport$,
localConnection$,
tracks$,
publishing$,
connectRequested$,
trackStartRequested$,
fatalLivekitError$,
]).pipe(
map(
([
publisher,
localTransport,
localConnection,
tracks,
publishing,
shouldConnect,
shouldStartTracks,
error,
]) => {
// read this:
// if(!<A>) return {state: ...}
// if(!<B>) return {state: <MyState>}
//
// as:
// We do have <A> but not yet <B> so we are in <MyState>
if (error !== null) return { state: LivekitState.Error, error };
const hasTracks = tracks.length > 0;
if (!localTransport) return { state: LivekitState.WaitingForTransport };
if (!localConnection) return { state: LivekitState.Connecting };
if (!publisher) return { state: LivekitState.InitialisingPublisher };
if (!shouldStartTracks) return { state: LivekitState.Initialized };
if (!hasTracks) return { state: LivekitState.CreatingTracks };
if (!shouldConnect) return { state: LivekitState.ReadyToPublish };
if (!publishing) return { state: LivekitState.WaitingToPublish };
return { state: LivekitState.Connected };
},
const livekitState$: Behavior<LocalMemberLivekitState> = scope.behavior(
combineLatest([
publisher$,
localTransport$,
tracks$.pipe(
tap((t) => {
logger.info("tracks$: ", t);
}),
),
publishing$,
connectRequested$,
from(trackStartRequested.promise).pipe(
map(() => true),
startWith(false),
),
fatalLivekitError$,
]).pipe(
map(
([
publisher,
localTransport,
tracks,
publishing,
shouldConnect,
shouldStartTracks,
error,
]) => {
// read this:
// if(!<A>) return {state: ...}
// if(!<B>) return {state: <MyState>}
//
// as:
// We do have <A> but not yet <B> so we are in <MyState>
if (error !== null) return { state: LivekitState.Error, error };
const hasTracks = tracks.length > 0;
if (!localTransport)
return { state: LivekitState.WaitingForTransport };
if (!publisher) return { state: LivekitState.WaitingForConnection };
if (!shouldStartTracks) return { state: LivekitState.Initialized };
if (!hasTracks) return { state: LivekitState.CreatingTracks };
if (!shouldConnect) return { state: LivekitState.ReadyToPublish };
if (!publishing) return { state: LivekitState.WaitingToPublish };
return { state: LivekitState.Connected };
},
),
distinctUntilChanged(deepCompare),
),
distinctUntilChanged(deepCompare),
);
const fatalMatrixError$ = new BehaviorSubject<ElementCallError | null>(null);
@@ -577,15 +564,15 @@ export const createLocalMembership$ = ({
requestConnect,
requestDisconnect,
connectionState: {
livekit$: scope.behavior(livekitState$),
livekit$: livekitState$,
matrix$: matrixState$,
},
tracks$,
participant$,
homeserverConnected$,
connected$,
reconnecting$,
sharingScreen$,
toggleScreenSharing,
participant$,
connection$: localConnection$,
};
};