mirror of
https://github.com/vector-im/element-call.git
synced 2026-07-09 18:29:21 +00:00
WIP
This commit is contained in:
@@ -67,7 +67,7 @@ import {
|
|||||||
ScreenShareViewModel,
|
ScreenShareViewModel,
|
||||||
type UserMediaViewModel,
|
type UserMediaViewModel,
|
||||||
} from "./MediaViewModel";
|
} from "./MediaViewModel";
|
||||||
import { accumulate, generateKeyed$, pauseWhen } from "../utils/observable";
|
import { accumulate, generateMap$, pauseWhen } from "../utils/observable";
|
||||||
import {
|
import {
|
||||||
duplicateTiles,
|
duplicateTiles,
|
||||||
MatrixRTCMode,
|
MatrixRTCMode,
|
||||||
@@ -110,7 +110,7 @@ import {
|
|||||||
} from "./layout-types.ts";
|
} from "./layout-types.ts";
|
||||||
import { type ElementCallError } from "../utils/errors.ts";
|
import { type ElementCallError } from "../utils/errors.ts";
|
||||||
import { type ObservableScope } from "./ObservableScope.ts";
|
import { type ObservableScope } from "./ObservableScope.ts";
|
||||||
import { createMatrixLivekitMembers$ } from "./remoteMembers/matrixLivekitMerger.ts";
|
import { createMatrixLivekitMembers$ } from "./remoteMembers/MatrixLivekitMembers.ts";
|
||||||
import { createLocalMembership$ } from "./localMember/LocalMembership.ts";
|
import { createLocalMembership$ } from "./localMember/LocalMembership.ts";
|
||||||
import { createLocalTransport$ } from "./localMember/LocalTransport.ts";
|
import { createLocalTransport$ } from "./localMember/LocalTransport.ts";
|
||||||
import { createSessionMembershipsAndTransports$ } from "./SessionBehaviors.ts";
|
import { createSessionMembershipsAndTransports$ } from "./SessionBehaviors.ts";
|
||||||
@@ -335,24 +335,17 @@ export class CallViewModel {
|
|||||||
*/
|
*/
|
||||||
// TODO KEEP THIS!! and adapt it to what our membershipManger returns
|
// TODO KEEP THIS!! and adapt it to what our membershipManger returns
|
||||||
private readonly mediaItems$ = this.scope.behavior<MediaItem[]>(
|
private readonly mediaItems$ = this.scope.behavior<MediaItem[]>(
|
||||||
generateKeyed$<
|
generateMap$(
|
||||||
[typeof this.matrixLivekitMembers$.value, number],
|
|
||||||
MediaItem,
|
|
||||||
MediaItem[]
|
|
||||||
>(
|
|
||||||
// Generate a collection of MediaItems from the list of expected (whether
|
// Generate a collection of MediaItems from the list of expected (whether
|
||||||
// present or missing) LiveKit participants.
|
// present or missing) LiveKit participants.
|
||||||
combineLatest([this.matrixLivekitMembers$, duplicateTiles.value$]),
|
combineLatest([this.matrixLivekitMembers$, duplicateTiles.value$]),
|
||||||
([matrixLivekitMembers, duplicateTiles], createOrGet) => {
|
function* ([matrixLivekitMembers, duplicateTiles]) {
|
||||||
const items: MediaItem[] = [];
|
const items: MediaItem[] = [];
|
||||||
|
|
||||||
for (const {
|
for (const [
|
||||||
connection,
|
|
||||||
participant,
|
|
||||||
member,
|
|
||||||
displayName$,
|
|
||||||
participantId,
|
participantId,
|
||||||
} of matrixLivekitMembers) {
|
{ connection$, participant$, member$, displayName$ },
|
||||||
|
] of matrixLivekitMembers) {
|
||||||
if (connection === undefined) {
|
if (connection === undefined) {
|
||||||
logger.warn("connection is not yet initialised.");
|
logger.warn("connection is not yet initialised.");
|
||||||
continue;
|
continue;
|
||||||
@@ -361,7 +354,6 @@ export class CallViewModel {
|
|||||||
const mediaId = `${participantId}:${i}`;
|
const mediaId = `${participantId}:${i}`;
|
||||||
const lkRoom = connection?.livekitRoom;
|
const lkRoom = connection?.livekitRoom;
|
||||||
const url = connection?.transport.livekit_service_url;
|
const url = connection?.transport.livekit_service_url;
|
||||||
const dpName$ = displayName$.pipe(map((n) => n ?? "[👻]"));
|
|
||||||
const item = createOrGet(
|
const item = createOrGet(
|
||||||
mediaId,
|
mediaId,
|
||||||
(scope) =>
|
(scope) =>
|
||||||
@@ -378,7 +370,7 @@ export class CallViewModel {
|
|||||||
url,
|
url,
|
||||||
this.mediaDevices,
|
this.mediaDevices,
|
||||||
this.pretendToBeDisconnected$,
|
this.pretendToBeDisconnected$,
|
||||||
dpName$,
|
displayName$,
|
||||||
this.handsRaised$.pipe(
|
this.handsRaised$.pipe(
|
||||||
map((v) => v[participantId]?.time ?? null),
|
map((v) => v[participantId]?.time ?? null),
|
||||||
),
|
),
|
||||||
@@ -405,7 +397,7 @@ export class CallViewModel {
|
|||||||
lkRoom,
|
lkRoom,
|
||||||
url,
|
url,
|
||||||
this.pretendToBeDisconnected$,
|
this.pretendToBeDisconnected$,
|
||||||
dpName$,
|
displayName$,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -20,8 +20,8 @@ import { type LocalParticipant, type RemoteParticipant } from "livekit-client";
|
|||||||
import { type Behavior } from "../Behavior";
|
import { type Behavior } from "../Behavior";
|
||||||
import { type Connection } from "./Connection";
|
import { type Connection } from "./Connection";
|
||||||
import { type ObservableScope } from "../ObservableScope";
|
import { type ObservableScope } from "../ObservableScope";
|
||||||
import { generateKeyed$ } from "../../utils/observable";
|
import { generateItems$ } from "../../utils/observable";
|
||||||
import { areLivekitTransportsEqual } from "./matrixLivekitMerger";
|
import { areLivekitTransportsEqual } from "./MatrixLivekitMembers.ts";
|
||||||
import { type ConnectionFactory } from "./ConnectionFactory.ts";
|
import { type ConnectionFactory } from "./ConnectionFactory.ts";
|
||||||
|
|
||||||
export class ConnectionManagerData {
|
export class ConnectionManagerData {
|
||||||
@@ -142,31 +142,28 @@ export function createConnectionManager$({
|
|||||||
* Connections for each transport in use by one or more session members.
|
* Connections for each transport in use by one or more session members.
|
||||||
*/
|
*/
|
||||||
const connections$ = scope.behavior(
|
const connections$ = scope.behavior(
|
||||||
generateKeyed$<LivekitTransport[], Connection, Connection[]>(
|
generateItems$(
|
||||||
transports$,
|
transports$,
|
||||||
(transports, createOrGet) => {
|
function* (transports) {
|
||||||
const createConnection =
|
for (const transport of transports)
|
||||||
(
|
yield {
|
||||||
transport: LivekitTransport,
|
// We need to serialize the transport to a string to properly use it
|
||||||
): ((scope: ObservableScope) => Connection) =>
|
// as a Map key, but we also need the real transport object in order
|
||||||
(scope) => {
|
// to construct the connection; pass it through as the item's data.
|
||||||
const connection = connectionFactory.createConnection(
|
key: `${transport.livekit_service_url}|${transport.livekit_alias}`,
|
||||||
transport,
|
data: transport,
|
||||||
scope,
|
|
||||||
logger,
|
|
||||||
);
|
|
||||||
// Start the connection immediately
|
|
||||||
// Use connection state to track connection progress
|
|
||||||
void connection.start();
|
|
||||||
// TODO subscribe to connection state to retry or log issues?
|
|
||||||
return connection;
|
|
||||||
};
|
};
|
||||||
|
},
|
||||||
return transports.map((transport) => {
|
(scope, key, transport$) => {
|
||||||
const key =
|
const connection = connectionFactory.createConnection(
|
||||||
transport.livekit_service_url + "|" + transport.livekit_alias;
|
transport$.value,
|
||||||
return createOrGet(key, createConnection(transport));
|
scope,
|
||||||
});
|
logger,
|
||||||
|
);
|
||||||
|
// Start the connection immediately
|
||||||
|
// Use connection state to track connection progress
|
||||||
|
void connection.start();
|
||||||
|
return connection;
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -13,10 +13,23 @@ import {
|
|||||||
type LivekitTransport,
|
type LivekitTransport,
|
||||||
type CallMembership,
|
type CallMembership,
|
||||||
} from "matrix-js-sdk/lib/matrixrtc";
|
} from "matrix-js-sdk/lib/matrixrtc";
|
||||||
import { combineLatest, map, startWith, type Observable } from "rxjs";
|
import { logger } from "matrix-js-sdk/lib/logger";
|
||||||
|
import {
|
||||||
|
combineLatest,
|
||||||
|
filter,
|
||||||
|
fromEvent,
|
||||||
|
map,
|
||||||
|
startWith,
|
||||||
|
switchMap,
|
||||||
|
type Observable,
|
||||||
|
} from "rxjs";
|
||||||
// eslint-disable-next-line rxjs/no-internal
|
// eslint-disable-next-line rxjs/no-internal
|
||||||
import { type NodeStyleEventEmitter } from "rxjs/internal/observable/fromEvent";
|
import { type NodeStyleEventEmitter } from "rxjs/internal/observable/fromEvent";
|
||||||
import { type Room as MatrixRoom, type RoomMember } from "matrix-js-sdk";
|
import {
|
||||||
|
RoomStateEvent,
|
||||||
|
type Room as MatrixRoom,
|
||||||
|
type RoomMember,
|
||||||
|
} from "matrix-js-sdk";
|
||||||
|
|
||||||
// import type { Logger } from "matrix-js-sdk/lib/logger";
|
// import type { Logger } from "matrix-js-sdk/lib/logger";
|
||||||
import { type Behavior } from "../Behavior";
|
import { type Behavior } from "../Behavior";
|
||||||
@@ -24,6 +37,7 @@ import { type ObservableScope } from "../ObservableScope";
|
|||||||
import { type createConnectionManager$ } from "./ConnectionManager";
|
import { type createConnectionManager$ } from "./ConnectionManager";
|
||||||
import { getRoomMemberFromRtcMember, memberDisplaynames$ } from "./displayname";
|
import { getRoomMemberFromRtcMember, memberDisplaynames$ } from "./displayname";
|
||||||
import { type Connection } from "./Connection";
|
import { type Connection } from "./Connection";
|
||||||
|
import { generateItems$ } from "../../utils/observable";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Represent a matrix call member and his associated livekit participation.
|
* Represent a matrix call member and his associated livekit participation.
|
||||||
@@ -31,18 +45,20 @@ import { type Connection } from "./Connection";
|
|||||||
* or if it has no livekit transport at all.
|
* or if it has no livekit transport at all.
|
||||||
*/
|
*/
|
||||||
export interface MatrixLivekitMember {
|
export interface MatrixLivekitMember {
|
||||||
membership: CallMembership;
|
participantId: string;
|
||||||
|
membership$: Behavior<CallMembership>;
|
||||||
displayName$: Behavior<string>;
|
displayName$: Behavior<string>;
|
||||||
participant?: LocalLivekitParticipant | RemoteLivekitParticipant;
|
participant$:
|
||||||
connection?: Connection;
|
| Behavior<LocalLivekitParticipant | undefined>
|
||||||
|
| Behavior<RemoteLivekitParticipant | undefined>;
|
||||||
|
connection$: Behavior<Connection | undefined>;
|
||||||
|
mxcAvatarUrl$: Behavior<string | undefined>;
|
||||||
/**
|
/**
|
||||||
* TODO Try to remove this! Its waaay to much information.
|
* TODO Try to remove this! Its waaay to much information.
|
||||||
* Just get the member's avatar
|
* Just get the member's avatar
|
||||||
* @deprecated
|
* @deprecated
|
||||||
*/
|
*/
|
||||||
member: RoomMember;
|
member$: Behavior<RoomMember>;
|
||||||
mxcAvatarUrl?: string;
|
|
||||||
participantId: string;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
@@ -85,7 +101,7 @@ export function createMatrixLivekitMembers$({
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
function createMatrixLivekitMember$(): Observable<MatrixLivekitMember[]> {
|
function createMatrixLivekitMember$(): Observable<MatrixLivekitMember[]> {
|
||||||
const displaynameMap$ = memberDisplaynames$(
|
const displayNameMap$ = memberDisplaynames$(
|
||||||
scope,
|
scope,
|
||||||
matrixRoom,
|
matrixRoom,
|
||||||
membershipsWithTransport$.pipe(map((v) => v.map((v) => v.membership))),
|
membershipsWithTransport$.pipe(map((v) => v.map((v) => v.membership))),
|
||||||
@@ -93,51 +109,75 @@ export function createMatrixLivekitMembers$({
|
|||||||
deviceId,
|
deviceId,
|
||||||
);
|
);
|
||||||
|
|
||||||
return combineLatest([
|
return generateItems$(
|
||||||
membershipsWithTransport$,
|
combineLatest([
|
||||||
connectionManager.connectionManagerData$,
|
membershipsWithTransport$,
|
||||||
]).pipe(
|
connectionManager.connectionManagerData$,
|
||||||
map(([memberships, managerData]) => {
|
]),
|
||||||
const items: MatrixLivekitMember[] = memberships.map(
|
function* ([memberships, managerData]) {
|
||||||
({ membership, transport }) => {
|
for (const { membership, transport } of memberships) {
|
||||||
// TODO! cannot use membership.membershipID yet, Currently its hardcoded by the jwt service to
|
// TODO! cannot use membership.membershipID yet, Currently its hardcoded by the jwt service to
|
||||||
const participantId = /*membership.membershipID*/ `${membership.userId}:${membership.deviceId}`;
|
const participantId = /*membership.membershipID*/ `${membership.userId}:${membership.deviceId}`;
|
||||||
|
|
||||||
const participants = transport
|
const participants = transport
|
||||||
? managerData.getParticipantForTransport(transport)
|
? managerData.getParticipantForTransport(transport)
|
||||||
: [];
|
: [];
|
||||||
const participant = participants.find(
|
const participant = participants.find(
|
||||||
(p) => p.identity == participantId,
|
(p) => p.identity == participantId,
|
||||||
);
|
);
|
||||||
const member = getRoomMemberFromRtcMember(
|
const member = getRoomMemberFromRtcMember(
|
||||||
membership,
|
membership,
|
||||||
matrixRoom,
|
matrixRoom,
|
||||||
)?.member;
|
)?.member;
|
||||||
const connection = transport
|
if (member === undefined) {
|
||||||
? managerData.getConnectionForTransport(transport)
|
logger.warn(`No room member for participant ${participantId}`);
|
||||||
: undefined;
|
continue;
|
||||||
const displayName$ = scope.behavior(
|
}
|
||||||
displaynameMap$.pipe(
|
|
||||||
map(
|
const connection = transport
|
||||||
(displayNameMap) =>
|
? managerData.getConnectionForTransport(transport)
|
||||||
displayNameMap.get(membership.membershipID) ?? "---",
|
: undefined;
|
||||||
),
|
|
||||||
),
|
yield {
|
||||||
);
|
key: participantId,
|
||||||
return {
|
data: {
|
||||||
participant,
|
participant,
|
||||||
membership,
|
membership,
|
||||||
connection,
|
connection,
|
||||||
// This makes sense to add the the js-sdk callMembership (we only need the avatar so probably the call memberhsip just should aquire the avatar)
|
// This makes sense to add the the js-sdk callMembership (we only need the avatar so probably the call memberhsip just should aquire the avatar)
|
||||||
// TODO Ugh this is hidign that it might be undefined!! best we remove the member entirely.
|
member,
|
||||||
member: member as RoomMember,
|
},
|
||||||
displayName$,
|
};
|
||||||
mxcAvatarUrl: member?.getMxcAvatarUrl(),
|
}
|
||||||
participantId,
|
},
|
||||||
};
|
(scope, participantId, data$): MatrixLivekitMember => ({
|
||||||
},
|
participantId,
|
||||||
);
|
membership$: scope.behavior(data$.pipe(map((data) => data.membership))),
|
||||||
return items;
|
displayName$: scope.behavior(
|
||||||
|
displayNameMap$.pipe(
|
||||||
|
map((displayNames) => displayNames.get(participantId)),
|
||||||
|
filter((name) => name !== undefined),
|
||||||
|
),
|
||||||
|
"",
|
||||||
|
),
|
||||||
|
participant$: scope.behavior(
|
||||||
|
data$.pipe(map((data) => data.participant)),
|
||||||
|
// Assert that a local participant will never become a remote
|
||||||
|
// participant or vice versa
|
||||||
|
) as
|
||||||
|
| Behavior<LocalLivekitParticipant | undefined>
|
||||||
|
| Behavior<RemoteLivekitParticipant | undefined>,
|
||||||
|
connection$: scope.behavior(data$.pipe(map((data) => data.connection))),
|
||||||
|
mxcAvatarUrl$: scope.behavior(
|
||||||
|
// React to avatar changes
|
||||||
|
fromEvent(matrixRoom, RoomStateEvent.Members).pipe(
|
||||||
|
startWith(null),
|
||||||
|
switchMap(() =>
|
||||||
|
data$.pipe(map((data) => data.member.getMxcAvatarUrl())),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
member$: scope.behavior(data$.pipe(map((data) => data.member))),
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ import {
|
|||||||
takeWhile,
|
takeWhile,
|
||||||
tap,
|
tap,
|
||||||
withLatestFrom,
|
withLatestFrom,
|
||||||
|
BehaviorSubject,
|
||||||
} from "rxjs";
|
} from "rxjs";
|
||||||
|
|
||||||
import { type Behavior } from "../state/Behavior";
|
import { type Behavior } from "../state/Behavior";
|
||||||
@@ -120,69 +121,54 @@ export function pauseWhen<T>(pause$: Behavior<boolean>) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Maps a changing input value to an output value consisting of items that have
|
* Maps a changing input value to a collection of items that each capture some
|
||||||
* automatically generated ObservableScopes tied to a key. Items will be
|
* dynamic data and are tied to a key. Items will be automatically created when
|
||||||
* automatically created when their key is requested for the first time, reused
|
* their key is requested for the first time, reused when the same key is
|
||||||
* when the same key is requested at a later time, and destroyed (have their
|
* requested acy later time, and destroyed (have their scope ended) when the key
|
||||||
* scope ended) when the key is no longer requested.
|
* is no longer requested.
|
||||||
*
|
*
|
||||||
* @param input$ The input value to be mapped.
|
* @param input$ The input value to be mapped.
|
||||||
* @param project A function mapping input values to output values. This
|
* @param generator A generator function yielding a key and the currently
|
||||||
* function receives an additional callback `createOrGet` which can be used
|
* associated data for each item that it wants to exist.
|
||||||
* within the function body to request that an item be generated for a certain
|
* @param factory A function constructing an actual item, given the item's key,
|
||||||
* key. The caller provides a factory which will be used to create the item if
|
* dynamic data, and an automatically managed ObservableScope for the item.
|
||||||
* it is being requested for the first time. Otherwise, the item previously
|
|
||||||
* existing under that key will be returned.
|
|
||||||
*/
|
*/
|
||||||
export function generateKeyed$<In, Item, Out>(
|
export function generateItems$<Input, Key, Data, Item>(
|
||||||
input$: Observable<In>,
|
input$: Observable<Input>,
|
||||||
project: (
|
generator: (input: Input) => Generator<{ key: Key; data: Data }, void, void>,
|
||||||
input: In,
|
factory: (scope: ObservableScope, key: Key, data$: Behavior<Data>) => Item,
|
||||||
createOrGet: (
|
): Observable<Item[]> {
|
||||||
key: string,
|
|
||||||
factory: (scope: ObservableScope) => Item,
|
|
||||||
) => Item,
|
|
||||||
) => Out,
|
|
||||||
): Observable<Out> {
|
|
||||||
return input$.pipe(
|
return input$.pipe(
|
||||||
// Keep track of the existing items over time, so we can reuse them
|
// Keep track of the existing items over time, so we can reuse them
|
||||||
scan<
|
scan((prevItems, input) => {
|
||||||
In,
|
const nextItems = new Map<
|
||||||
{
|
Key,
|
||||||
items: Map<string, { item: Item; scope: ObservableScope }>;
|
{ scope: ObservableScope; data$: BehaviorSubject<Data>; item: Item }
|
||||||
output: Out;
|
>();
|
||||||
},
|
|
||||||
{ items: Map<string, { item: Item; scope: ObservableScope }> }
|
|
||||||
>(
|
|
||||||
(state, data) => {
|
|
||||||
const nextItems = new Map<
|
|
||||||
string,
|
|
||||||
{ item: Item; scope: ObservableScope }
|
|
||||||
>();
|
|
||||||
|
|
||||||
const output = project(data, (key, factory) => {
|
for (const { key, data } of generator(input)) {
|
||||||
let item = state.items.get(key);
|
let item = prevItems.get(key);
|
||||||
if (item === undefined) {
|
if (item === undefined) {
|
||||||
// First time requesting the key; create the item
|
// First time requesting the key; create the item
|
||||||
const scope = new ObservableScope();
|
const scope = new ObservableScope();
|
||||||
item = { item: factory(scope), scope };
|
const data$ = new BehaviorSubject(data);
|
||||||
}
|
item = { scope, data$, item: factory(scope, key, data$) };
|
||||||
nextItems.set(key, item);
|
} else {
|
||||||
return item.item;
|
item.data$.next(data);
|
||||||
});
|
}
|
||||||
|
nextItems.set(key, item);
|
||||||
|
}
|
||||||
|
|
||||||
// Destroy all items that are no longer being requested
|
// Destroy all items that are no longer being requested
|
||||||
for (const [key, { scope }] of state.items)
|
for (const [key, { scope }] of prevItems)
|
||||||
if (!nextItems.has(key)) scope.end();
|
if (!nextItems.has(key)) scope.end();
|
||||||
|
|
||||||
return { items: nextItems, output };
|
return nextItems;
|
||||||
},
|
}, new Map<Key, { scope: ObservableScope; data$: BehaviorSubject<Data>; item: Item }>()),
|
||||||
{ items: new Map() },
|
finalizeValue((items) => {
|
||||||
),
|
|
||||||
finalizeValue((state) => {
|
|
||||||
// Destroy all remaining items when no longer subscribed
|
// Destroy all remaining items when no longer subscribed
|
||||||
for (const { scope } of state.items.values()) scope.end();
|
for (const { scope } of items.values()) scope.end();
|
||||||
}),
|
}),
|
||||||
map(({ output }) => output),
|
map((items) => [...items.values()].map(({ item }) => item)),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user