mirror of
https://github.com/vector-im/element-call.git
synced 2026-10-16 23:45:50 +00:00
WIP
This commit is contained in:
@@ -8,7 +8,6 @@ Please see LICENSE in the repository root for full details.
|
|||||||
import { type MatrixClient } from "matrix-js-sdk";
|
import { type MatrixClient } from "matrix-js-sdk";
|
||||||
import { type CallMembershipIdentityParts } from "matrix-js-sdk/lib/matrixrtc/EncryptionManager";
|
import { type CallMembershipIdentityParts } from "matrix-js-sdk/lib/matrixrtc/EncryptionManager";
|
||||||
|
|
||||||
import { type OpenIDClientParts } from "./openID.ts";
|
|
||||||
import { extractFullConfigFromToken, type SFUConfig } from "./types.ts";
|
import { extractFullConfigFromToken, type SFUConfig } from "./types.ts";
|
||||||
import { doNetworkOperationWithRetry } from "../../utils/matrix.ts";
|
import { doNetworkOperationWithRetry } from "../../utils/matrix.ts";
|
||||||
|
|
||||||
@@ -16,8 +15,10 @@ import { doNetworkOperationWithRetry } from "../../utils/matrix.ts";
|
|||||||
// of being hardcoded here. (the legacy flow hardcodes it as well)
|
// of being hardcoded here. (the legacy flow hardcodes it as well)
|
||||||
const SLOT_ID = "m.call#ROOM";
|
const SLOT_ID = "m.call#ROOM";
|
||||||
|
|
||||||
export type ClientGetTokenParts = OpenIDClientParts &
|
export type ClientGetTokenParts = Pick<
|
||||||
Pick<MatrixClient, "_unstable_getLivekitToken">;
|
MatrixClient,
|
||||||
|
"_unstable_getLivekitToken"
|
||||||
|
>;
|
||||||
|
|
||||||
export async function getSFUConfig({
|
export async function getSFUConfig({
|
||||||
client,
|
client,
|
||||||
@@ -35,7 +36,7 @@ export async function getSFUConfig({
|
|||||||
url,
|
url,
|
||||||
room_id: roomId,
|
room_id: roomId,
|
||||||
slot_id: "m.call#ROOM",
|
slot_id: "m.call#ROOM",
|
||||||
member: { id: membership.memberId },
|
member_id: membership.memberId,
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
return extractFullConfigFromToken({ url, jwt: res.jwt });
|
return extractFullConfigFromToken({ url, jwt: res.jwt });
|
||||||
@@ -64,7 +65,7 @@ export async function delegateDelayedLeave({
|
|||||||
url,
|
url,
|
||||||
room_id: roomId,
|
room_id: roomId,
|
||||||
slot_id: SLOT_ID,
|
slot_id: SLOT_ID,
|
||||||
member: { id: membership.memberId },
|
member_id: membership.memberId,
|
||||||
delay_id: delayId,
|
delay_id: delayId,
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|||||||
+35
-44
@@ -8,20 +8,16 @@ Please see LICENSE in the repository root for full details.
|
|||||||
import { type CallMembershipIdentityParts } from "matrix-js-sdk/lib/matrixrtc/EncryptionManager";
|
import { type CallMembershipIdentityParts } from "matrix-js-sdk/lib/matrixrtc/EncryptionManager";
|
||||||
import { type Logger } from "matrix-js-sdk/lib/logger";
|
import { type Logger } from "matrix-js-sdk/lib/logger";
|
||||||
|
|
||||||
import {
|
|
||||||
getSFUConfig as getSFUConfigWithOpenID,
|
|
||||||
type OpenIDClientParts,
|
|
||||||
} from "./openID";
|
|
||||||
import {
|
|
||||||
type ClientDelegationParts,
|
|
||||||
delegateDelayedLeave as delegateDelayedLeaveWithCSApi,
|
|
||||||
type ClientGetTokenParts,
|
|
||||||
getSFUConfig as getSFUConfigWithCSApi,
|
|
||||||
} from "./csApi";
|
|
||||||
import { type MatrixRTCMode } from "../../config/ConfigOptions";
|
|
||||||
import { MatrixError } from "matrix-js-sdk";
|
import { MatrixError } from "matrix-js-sdk";
|
||||||
import { type LivekitTransport } from "matrix-js-sdk/lib/matrixrtc";
|
import { type UnstableLivekitTransport } from "matrix-js-sdk/lib/matrixrtc";
|
||||||
|
import * as openID from "./openID";
|
||||||
|
import * as csApi from "./csApi";
|
||||||
import { type SFUConfig } from "./types";
|
import { type SFUConfig } from "./types";
|
||||||
|
import { NoMatrix2AuthorizationService } from "../../utils/errors";
|
||||||
|
|
||||||
|
export { ClientOpenIDParts } from "./openID";
|
||||||
|
export { ClientGetTokenParts, ClientDelegationParts } from "./csApi";
|
||||||
|
export * from "./types";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Checks whether an error means "this homeserver does not implement the
|
* Checks whether an error means "this homeserver does not implement the
|
||||||
@@ -46,19 +42,19 @@ export async function getSFUConfig({
|
|||||||
membership,
|
membership,
|
||||||
transport,
|
transport,
|
||||||
roomId,
|
roomId,
|
||||||
matrixRTCMode,
|
role,
|
||||||
logger,
|
logger,
|
||||||
}: {
|
}: {
|
||||||
client: ClientGetTokenParts & OpenIDClientParts;
|
client: csApi.ClientGetTokenParts & openID.ClientOpenIDParts;
|
||||||
membership: CallMembershipIdentityParts;
|
membership: CallMembershipIdentityParts;
|
||||||
transport: LivekitTransport;
|
transport: UnstableLivekitTransport;
|
||||||
roomId: string;
|
roomId: string;
|
||||||
matrixRTCMode: MatrixRTCMode;
|
role: "publisher" | "subscriber";
|
||||||
logger: Logger;
|
logger: Logger;
|
||||||
}): Promise<SFUConfig> {
|
}): Promise<SFUConfig> {
|
||||||
if ("url" in transport) {
|
if ("url" in transport) {
|
||||||
try {
|
try {
|
||||||
return await getSFUConfigWithCSApi({
|
return await csApi.getSFUConfig({
|
||||||
client,
|
client,
|
||||||
membership,
|
membership,
|
||||||
url: transport.url,
|
url: transport.url,
|
||||||
@@ -66,19 +62,18 @@ export async function getSFUConfig({
|
|||||||
});
|
});
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (isEndpointUnsupported(e)) {
|
if (isEndpointUnsupported(e)) {
|
||||||
// Alias transport before checking whether we can fall back, otherwise
|
const mayFallBack =
|
||||||
// TypeScript gets confused about its type
|
role === "subscriber" && "livekit_service_url" in transport;
|
||||||
const transport_ = transport;
|
if (mayFallBack) {
|
||||||
if ("livekit_service_url" in transport_) {
|
|
||||||
logger.warn(
|
logger.warn(
|
||||||
`Homeserver does not support the MSC4195 get_token endpoint, falling back to the OpenID flow with service ${transport_.livekit_service_url}`,
|
`Homeserver does not support the MSC4195 get_token endpoint, falling back to the OpenID flow with service ${transport.livekit_service_url}`,
|
||||||
);
|
);
|
||||||
// Fall through
|
// Fall through
|
||||||
} else {
|
} else {
|
||||||
logger.error(
|
logger.error(
|
||||||
`Homeserver does not support the MSC4195 get_token endpoint, cannot get token for transport ${transport.url}`,
|
`Homeserver does not support the MSC4195 get_token endpoint, cannot get token for transport ${transport.url}`,
|
||||||
);
|
);
|
||||||
throw e;
|
throw new NoMatrix2AuthorizationService(e as Error);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
throw e;
|
throw e;
|
||||||
@@ -86,18 +81,14 @@ export async function getSFUConfig({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if ("livekit_service_url" in transport) {
|
return await openID.getSFUConfig({
|
||||||
return await getSFUConfigWithOpenID(
|
client,
|
||||||
client,
|
membership,
|
||||||
membership,
|
serviceUrl: transport.livekit_service_url,
|
||||||
transport.livekit_service_url,
|
roomId,
|
||||||
roomId,
|
role,
|
||||||
{ matrixRTCMode },
|
logger,
|
||||||
logger,
|
});
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
throw new Error(`Unrecognized transport: ${JSON.stringify(transport)}`);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function delegateDelayedLeave({
|
export async function delegateDelayedLeave({
|
||||||
@@ -106,20 +97,18 @@ export async function delegateDelayedLeave({
|
|||||||
transport,
|
transport,
|
||||||
roomId,
|
roomId,
|
||||||
delayId,
|
delayId,
|
||||||
matrixRTCMode,
|
|
||||||
logger,
|
logger,
|
||||||
}: {
|
}: {
|
||||||
client: ClientDelegationParts & OpenIDClientParts;
|
client: csApi.ClientDelegationParts & openID.ClientOpenIDParts;
|
||||||
membership: CallMembershipIdentityParts;
|
membership: CallMembershipIdentityParts;
|
||||||
transport: LivekitTransport;
|
transport: UnstableLivekitTransport;
|
||||||
roomId: string;
|
roomId: string;
|
||||||
delayId: string;
|
delayId: string;
|
||||||
matrixRTCMode: MatrixRTCMode;
|
|
||||||
logger: Logger;
|
logger: Logger;
|
||||||
}): Promise<void> {
|
}): Promise<void> {
|
||||||
if ("url" in transport) {
|
if ("url" in transport) {
|
||||||
try {
|
try {
|
||||||
await delegateDelayedLeaveWithCSApi({
|
await csApi.delegateDelayedLeave({
|
||||||
client,
|
client,
|
||||||
membership,
|
membership,
|
||||||
url: transport.url,
|
url: transport.url,
|
||||||
@@ -136,13 +125,15 @@ export async function delegateDelayedLeave({
|
|||||||
} else {
|
} else {
|
||||||
// This will technically cause the service to issue a new JWT token, but
|
// This will technically cause the service to issue a new JWT token, but
|
||||||
// it's safe to discard. We're only interested in triggering delegation.
|
// it's safe to discard. We're only interested in triggering delegation.
|
||||||
await getSFUConfigWithOpenID(
|
await openID.getSFUConfig({
|
||||||
client,
|
client,
|
||||||
membership,
|
membership,
|
||||||
transport.livekit_service_url,
|
serviceUrl: transport.livekit_service_url,
|
||||||
roomId,
|
roomId,
|
||||||
{ matrixRTCMode, delayEndpointBaseUrl: client.baseUrl, delayId },
|
role: "publisher",
|
||||||
|
delayEndpointBaseUrl: client.baseUrl,
|
||||||
|
delayId,
|
||||||
logger,
|
logger,
|
||||||
);
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ import {
|
|||||||
import fetchMock from "fetch-mock";
|
import fetchMock from "fetch-mock";
|
||||||
import { MatrixError } from "matrix-js-sdk";
|
import { MatrixError } from "matrix-js-sdk";
|
||||||
|
|
||||||
import { getSFUConfigWithOpenID, type OpenIDClientParts } from "./openIDSFU";
|
import { getSFUConfigWithOpenID, type ClientOpenIDParts } from "./openIDSFU";
|
||||||
import { testJWTToken } from "../utils/test-fixtures";
|
import { testJWTToken } from "../utils/test-fixtures";
|
||||||
import { ownMemberMock } from "../utils/test";
|
import { ownMemberMock } from "../utils/test";
|
||||||
import { FailToGetOpenIdToken } from "../utils/errors";
|
import { FailToGetOpenIdToken } from "../utils/errors";
|
||||||
@@ -25,7 +25,7 @@ import { FailToGetOpenIdToken } from "../utils/errors";
|
|||||||
const sfuUrl = "https://sfu.example.org";
|
const sfuUrl = "https://sfu.example.org";
|
||||||
|
|
||||||
describe("getSFUConfigWithOpenID", () => {
|
describe("getSFUConfigWithOpenID", () => {
|
||||||
let matrixClient: MockedObject<OpenIDClientParts>;
|
let matrixClient: MockedObject<ClientOpenIDParts>;
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
fetchMock.catch(404);
|
fetchMock.catch(404);
|
||||||
matrixClient = {
|
matrixClient = {
|
||||||
|
|||||||
+49
-61
@@ -13,17 +13,13 @@ import {
|
|||||||
import { type CallMembershipIdentityParts } from "matrix-js-sdk/lib/matrixrtc/EncryptionManager";
|
import { type CallMembershipIdentityParts } from "matrix-js-sdk/lib/matrixrtc/EncryptionManager";
|
||||||
import { type Logger } from "matrix-js-sdk/lib/logger";
|
import { type Logger } from "matrix-js-sdk/lib/logger";
|
||||||
|
|
||||||
import {
|
import { FailToGetOpenIdToken } from "../../utils/errors";
|
||||||
FailToGetOpenIdToken,
|
|
||||||
NoMatrix2AuthorizationService,
|
|
||||||
} from "../../utils/errors";
|
|
||||||
import { doNetworkOperationWithRetry } from "../../utils/matrix";
|
import { doNetworkOperationWithRetry } from "../../utils/matrix";
|
||||||
import { Config } from "../../config/Config";
|
import { Config } from "../../config/Config";
|
||||||
import { MatrixRTCMode } from "../../config/ConfigOptions";
|
|
||||||
import { extractFullConfigFromToken, type SFUConfig } from "./types";
|
import { extractFullConfigFromToken, type SFUConfig } from "./types";
|
||||||
|
|
||||||
// The bits we need from MatrixClient
|
// The bits we need from MatrixClient
|
||||||
export type OpenIDClientParts = Pick<
|
export type ClientOpenIDParts = Pick<
|
||||||
MatrixClient,
|
MatrixClient,
|
||||||
"getOpenIdToken" | "getDeviceId"
|
"getOpenIdToken" | "getDeviceId"
|
||||||
>;
|
>;
|
||||||
@@ -47,18 +43,25 @@ export type OpenIDClientParts = Pick<
|
|||||||
* @returns Object containing the token information
|
* @returns Object containing the token information
|
||||||
* @throws FailToGetOpenIdToken
|
* @throws FailToGetOpenIdToken
|
||||||
*/
|
*/
|
||||||
export async function getSFUConfig(
|
export async function getSFUConfig({
|
||||||
client: OpenIDClientParts,
|
client,
|
||||||
membership: CallMembershipIdentityParts,
|
membership,
|
||||||
serviceUrl: string,
|
serviceUrl,
|
||||||
roomId: string,
|
roomId,
|
||||||
opts?: {
|
role,
|
||||||
matrixRTCMode?: MatrixRTCMode;
|
delayEndpointBaseUrl,
|
||||||
delayEndpointBaseUrl?: string;
|
delayId,
|
||||||
delayId?: string;
|
logger,
|
||||||
},
|
}: {
|
||||||
logger?: Logger,
|
client: ClientOpenIDParts;
|
||||||
): Promise<SFUConfig> {
|
membership: CallMembershipIdentityParts;
|
||||||
|
serviceUrl: string;
|
||||||
|
roomId: string;
|
||||||
|
role: "publisher" | "subscriber";
|
||||||
|
delayEndpointBaseUrl?: string;
|
||||||
|
delayId?: string;
|
||||||
|
logger: Logger;
|
||||||
|
}): Promise<SFUConfig> {
|
||||||
let openIdToken: IOpenIDToken;
|
let openIdToken: IOpenIDToken;
|
||||||
try {
|
try {
|
||||||
openIdToken = await doNetworkOperationWithRetry(async () =>
|
openIdToken = await doNetworkOperationWithRetry(async () =>
|
||||||
@@ -69,65 +72,50 @@ export async function getSFUConfig(
|
|||||||
error instanceof Error ? error : new Error("Unknown error"),
|
error instanceof Error ? error : new Error("Unknown error"),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
logger?.debug("Got openID token", openIdToken);
|
logger.debug("Got openID token", openIdToken);
|
||||||
let sfuConfig: { url: string; jwt: string } | undefined;
|
let sfuConfig: { url: string; jwt: string } | undefined;
|
||||||
|
|
||||||
const tryBothJwtEndpoints = opts?.matrixRTCMode === undefined; // This is for SFUs where we do not publish.
|
let endpoint: "default" | "legacy";
|
||||||
|
switch (role) {
|
||||||
const forceMatrix2Jwt = opts?.matrixRTCMode === MatrixRTCMode.Matrix_2_0;
|
case "publisher":
|
||||||
|
// When publishing a legacy transport (one with a `livekit_service_url`),
|
||||||
// We want to start using the new endpoint (with optional delay delegation)
|
// subscribers will expect our participant identity to use the legacy
|
||||||
// if we can use both or if we are forced to use the new one.
|
// `@user_id:device_id` format. Only the legacy JWT service endpoint
|
||||||
if (tryBothJwtEndpoints || forceMatrix2Jwt) {
|
// assigns identities in this format, so we must use it.
|
||||||
try {
|
endpoint = "legacy";
|
||||||
logger?.info(
|
break;
|
||||||
`Trying to get JWT via default endpoint for focus ${serviceUrl}...`,
|
case "subscriber":
|
||||||
);
|
// Use the default endpoint as it's more likely to allow remote access.
|
||||||
const sfuConfig = await getLiveKitJWT(
|
endpoint = "default";
|
||||||
membership,
|
break;
|
||||||
serviceUrl,
|
|
||||||
roomId,
|
|
||||||
openIdToken,
|
|
||||||
opts?.delayEndpointBaseUrl,
|
|
||||||
opts?.delayId,
|
|
||||||
);
|
|
||||||
|
|
||||||
return extractFullConfigFromToken(sfuConfig);
|
|
||||||
} catch (e) {
|
|
||||||
logger?.debug(`Failed fetching jwt with matrix 2.0 endpoint:`, e);
|
|
||||||
// Make this throw a hard error in case we force the matrix2.0 endpoint.
|
|
||||||
if (forceMatrix2Jwt) {
|
|
||||||
throw new NoMatrix2AuthorizationService(e as Error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// DEPRECATED
|
|
||||||
// here we either have a sfuConfig or we already exited because of `if (forceMatrix2) throw ...`
|
|
||||||
// The only case we can get into this condition is, if `forceMatrix2` is `false`
|
|
||||||
try {
|
try {
|
||||||
logger?.info(
|
logger.info(
|
||||||
`Trying to get JWT with legacy endpoint for focus ${serviceUrl}...`,
|
`Trying to get JWT with ${endpoint} endpoint for focus ${serviceUrl}...`,
|
||||||
);
|
);
|
||||||
sfuConfig = await getLiveKitJWTLegacy(
|
sfuConfig = await (
|
||||||
membership.deviceId,
|
endpoint === "legacy" ? getLiveKitJWTLegacy : getLiveKitJWT
|
||||||
|
)(
|
||||||
|
membership,
|
||||||
serviceUrl,
|
serviceUrl,
|
||||||
roomId,
|
roomId,
|
||||||
openIdToken,
|
openIdToken,
|
||||||
opts?.delayEndpointBaseUrl,
|
delayEndpointBaseUrl,
|
||||||
opts?.delayId,
|
delayId,
|
||||||
);
|
);
|
||||||
logger?.info(`Got JWT from call's active focus URL.`);
|
logger.info(`Got JWT from call's active focus URL.`);
|
||||||
return extractFullConfigFromToken(sfuConfig);
|
return extractFullConfigFromToken(sfuConfig);
|
||||||
} catch (ex) {
|
} catch (e) {
|
||||||
|
logger.error(`Failed fetching jwt with ${endpoint} endpoint:`, e);
|
||||||
throw new FailToGetOpenIdToken(
|
throw new FailToGetOpenIdToken(
|
||||||
ex instanceof Error ? ex : new Error(`Unknown error ${ex}`),
|
e instanceof Error ? e : new Error(`Unknown error ${e}`),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function getLiveKitJWTLegacy(
|
async function getLiveKitJWTLegacy(
|
||||||
deviceId: string,
|
membership: CallMembershipIdentityParts,
|
||||||
livekitServiceURL: string,
|
livekitServiceURL: string,
|
||||||
matrixRoomId: string,
|
matrixRoomId: string,
|
||||||
openIDToken: IOpenIDToken,
|
openIDToken: IOpenIDToken,
|
||||||
@@ -161,7 +149,7 @@ async function getLiveKitJWTLegacy(
|
|||||||
// However, the livekit room alias is provided as part of the JWT payload.
|
// However, the livekit room alias is provided as part of the JWT payload.
|
||||||
room: matrixRoomId,
|
room: matrixRoomId,
|
||||||
openid_token: openIDToken,
|
openid_token: openIDToken,
|
||||||
device_id: deviceId,
|
device_id: membership.deviceId,
|
||||||
...delayParts,
|
...delayParts,
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ import {
|
|||||||
import { act } from "react";
|
import { act } from "react";
|
||||||
import { type RoomMember } from "matrix-js-sdk";
|
import { type RoomMember } from "matrix-js-sdk";
|
||||||
import {
|
import {
|
||||||
type LivekitTransport,
|
type UnstableLivekitTransport,
|
||||||
type CallMembership,
|
type CallMembership,
|
||||||
} from "matrix-js-sdk/lib/matrixrtc";
|
} from "matrix-js-sdk/lib/matrixrtc";
|
||||||
|
|
||||||
@@ -46,7 +46,7 @@ vitest.mock("../useAudioContext");
|
|||||||
vitest.mock("../soundUtils");
|
vitest.mock("../soundUtils");
|
||||||
vitest.mock("../rtcSessionHelpers", async (importOriginal) => ({
|
vitest.mock("../rtcSessionHelpers", async (importOriginal) => ({
|
||||||
...(await importOriginal()),
|
...(await importOriginal()),
|
||||||
makeTransport: (): [LivekitTransport] => [exampleTransport],
|
makeTransport: (): [UnstableLivekitTransport] => [exampleTransport],
|
||||||
}));
|
}));
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ import {
|
|||||||
type CallMembership,
|
type CallMembership,
|
||||||
type IRTCNotificationContent,
|
type IRTCNotificationContent,
|
||||||
MatrixRTCSessionEvent,
|
MatrixRTCSessionEvent,
|
||||||
type LivekitTransport,
|
type UnstableLivekitTransport,
|
||||||
} from "matrix-js-sdk/lib/matrixrtc";
|
} from "matrix-js-sdk/lib/matrixrtc";
|
||||||
import { deepCompare } from "matrix-js-sdk/lib/utils";
|
import { deepCompare } from "matrix-js-sdk/lib/utils";
|
||||||
|
|
||||||
@@ -97,7 +97,7 @@ vi.mock(
|
|||||||
"../state/CallViewModel/localMember/localTransport",
|
"../state/CallViewModel/localMember/localTransport",
|
||||||
async (importOriginal) => ({
|
async (importOriginal) => ({
|
||||||
...(await importOriginal()),
|
...(await importOriginal()),
|
||||||
makeTransport: async (): Promise<LivekitTransport> =>
|
makeTransport: async (): Promise<UnstableLivekitTransport> =>
|
||||||
Promise.resolve(exampleTransport),
|
Promise.resolve(exampleTransport),
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -43,7 +43,7 @@ import {
|
|||||||
import { type Logger, logger as rootLogger } from "matrix-js-sdk/lib/logger";
|
import { type Logger, logger as rootLogger } from "matrix-js-sdk/lib/logger";
|
||||||
import {
|
import {
|
||||||
MembershipManagerEvent,
|
MembershipManagerEvent,
|
||||||
type LivekitTransport,
|
type UnstableLivekitTransport,
|
||||||
type MatrixRTCSession,
|
type MatrixRTCSession,
|
||||||
type RTCCallIntent,
|
type RTCCallIntent,
|
||||||
type RTCNotificationType,
|
type RTCNotificationType,
|
||||||
@@ -568,6 +568,8 @@ export function createCallViewModel$(
|
|||||||
// membership advertises `${userId}:${deviceId}` as its `membershipID`
|
// membership advertises `${userId}:${deviceId}` as its `membershipID`
|
||||||
// instead, so a uuid there names a member no peer can resolve.
|
// instead, so a uuid there names a member no peer can resolve.
|
||||||
memberId:
|
memberId:
|
||||||
|
// TODO: Make this conditional on the transport instead
|
||||||
|
// or else avoid using it in key messages
|
||||||
matrixRTCMode === MatrixRTCMode.Matrix_2_0
|
matrixRTCMode === MatrixRTCMode.Matrix_2_0
|
||||||
? uuidv4()
|
? uuidv4()
|
||||||
: `${userId}:${deviceId}`,
|
: `${userId}:${deviceId}`,
|
||||||
@@ -580,7 +582,6 @@ export function createCallViewModel$(
|
|||||||
ownMembershipIdentity,
|
ownMembershipIdentity,
|
||||||
client,
|
client,
|
||||||
roomId: matrixRoom.roomId,
|
roomId: matrixRoom.roomId,
|
||||||
matrixRTCMode,
|
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -624,7 +625,7 @@ export function createCallViewModel$(
|
|||||||
),
|
),
|
||||||
muteStates,
|
muteStates,
|
||||||
joinMatrixRTC: (
|
joinMatrixRTC: (
|
||||||
transport: LivekitTransport,
|
transport: UnstableLivekitTransport,
|
||||||
delayedLeaveTimings: ResolvedDelayedLeaveTimings,
|
delayedLeaveTimings: ResolvedDelayedLeaveTimings,
|
||||||
) => {
|
) => {
|
||||||
return enterRTCSession(
|
return enterRTCSession(
|
||||||
@@ -646,9 +647,7 @@ export function createCallViewModel$(
|
|||||||
mediaDevices,
|
mediaDevices,
|
||||||
muteStates,
|
muteStates,
|
||||||
trackProcessorState$,
|
trackProcessorState$,
|
||||||
logger.getChild(
|
logger.getChild(`[Publisher ${JSON.stringify(connection.transport)}]`),
|
||||||
"[Publisher " + connection.transport.livekit_service_url + "]",
|
|
||||||
),
|
|
||||||
controlledAudioDevices,
|
controlledAudioDevices,
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
@@ -671,7 +670,6 @@ export function createCallViewModel$(
|
|||||||
).pipe(map(([delayId]) => delayId ?? null)),
|
).pipe(map(([delayId]) => delayId ?? null)),
|
||||||
matrixRTCSession.delayId ?? null,
|
matrixRTCSession.delayId ?? null,
|
||||||
),
|
),
|
||||||
matrixRTCMode,
|
|
||||||
logger: logger.getChild(`[${Date.now()}]`),
|
logger: logger.getChild(`[${Date.now()}]`),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ Please see LICENSE in the repository root for full details.
|
|||||||
|
|
||||||
import {
|
import {
|
||||||
Status as RTCMemberStatus,
|
Status as RTCMemberStatus,
|
||||||
type LivekitTransport,
|
type UnstableLivekitTransport,
|
||||||
type MatrixRTCSession,
|
type MatrixRTCSession,
|
||||||
} from "matrix-js-sdk/lib/matrixrtc";
|
} from "matrix-js-sdk/lib/matrixrtc";
|
||||||
import {
|
import {
|
||||||
@@ -121,7 +121,7 @@ const mockedClient = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
describe("enterRTCSession", () => {
|
describe("enterRTCSession", () => {
|
||||||
const transport: LivekitTransport = {
|
const transport: UnstableLivekitTransport = {
|
||||||
livekit_service_url: "http://my-livekit-service-url.com",
|
livekit_service_url: "http://my-livekit-service-url.com",
|
||||||
type: "livekit",
|
type: "livekit",
|
||||||
};
|
};
|
||||||
@@ -252,7 +252,7 @@ describe("LocalMembership", () => {
|
|||||||
|
|
||||||
it("throws error on missing RTC config error", () => {
|
it("throws error on missing RTC config error", () => {
|
||||||
withTestScheduler(({ scope, hot, expectObservable }) => {
|
withTestScheduler(({ scope, hot, expectObservable }) => {
|
||||||
const localTransport$ = scope.behavior<null | LivekitTransport>(
|
const localTransport$ = scope.behavior<null | UnstableLivekitTransport>(
|
||||||
hot("1ms #", {}, new MatrixRTCTransportMissingError("domain.com")),
|
hot("1ms #", {}, new MatrixRTCTransportMissingError("domain.com")),
|
||||||
null,
|
null,
|
||||||
);
|
);
|
||||||
@@ -317,7 +317,7 @@ describe("LocalMembership", () => {
|
|||||||
|
|
||||||
const mockTransportConfig = {
|
const mockTransportConfig = {
|
||||||
livekit_service_url: "a",
|
livekit_service_url: "a",
|
||||||
} as LivekitTransport;
|
} as UnstableLivekitTransport;
|
||||||
|
|
||||||
const mockTransport = {
|
const mockTransport = {
|
||||||
transport: mockTransportConfig,
|
transport: mockTransportConfig,
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ import {
|
|||||||
import { observeParticipantEvents } from "@livekit/components-core";
|
import { observeParticipantEvents } from "@livekit/components-core";
|
||||||
import {
|
import {
|
||||||
Status as RTCSessionStatus,
|
Status as RTCSessionStatus,
|
||||||
type LivekitTransport,
|
type UnstableLivekitTransport,
|
||||||
type MatrixRTCSession,
|
type MatrixRTCSession,
|
||||||
type RTCCallIntent,
|
type RTCCallIntent,
|
||||||
type RTCNotificationType,
|
type RTCNotificationType,
|
||||||
@@ -82,7 +82,8 @@ import { type LocalTransport } from "./LocalTransport.ts";
|
|||||||
import {
|
import {
|
||||||
type ClientDelegationParts,
|
type ClientDelegationParts,
|
||||||
delegateDelayedLeave,
|
delegateDelayedLeave,
|
||||||
} from "../../../livekit/auth/index.ts";
|
type ClientOpenIDParts,
|
||||||
|
} from "../../../livekit/auth";
|
||||||
|
|
||||||
export enum TransportState {
|
export enum TransportState {
|
||||||
/** Not even a transport is available to the LocalMembership */
|
/** Not even a transport is available to the LocalMembership */
|
||||||
@@ -145,14 +146,14 @@ interface Props {
|
|||||||
connectionManager: IConnectionManager;
|
connectionManager: IConnectionManager;
|
||||||
createPublisherFactory: (connection: Connection) => Publisher;
|
createPublisherFactory: (connection: Connection) => Publisher;
|
||||||
joinMatrixRTC: (
|
joinMatrixRTC: (
|
||||||
transport: LivekitTransport,
|
transport: UnstableLivekitTransport,
|
||||||
delayedLeaveTimings: ResolvedDelayedLeaveTimings,
|
delayedLeaveTimings: ResolvedDelayedLeaveTimings,
|
||||||
) => void;
|
) => void;
|
||||||
homeserverConnected: HomeserverConnected;
|
homeserverConnected: HomeserverConnected;
|
||||||
roomId: string;
|
roomId: string;
|
||||||
ownMembershipIdentity: CallMembershipIdentityParts;
|
ownMembershipIdentity: CallMembershipIdentityParts;
|
||||||
localTransport$: Observable<LocalTransport>;
|
localTransport$: Observable<LocalTransport>;
|
||||||
client: ClientDelegationParts;
|
client: ClientDelegationParts & ClientOpenIDParts;
|
||||||
matrixRTCSession: Pick<MatrixRTCSession, "updateCallIntent" | "leave">;
|
matrixRTCSession: Pick<MatrixRTCSession, "updateCallIntent" | "leave">;
|
||||||
/** Whether to hide the screen-sharing button. */
|
/** Whether to hide the screen-sharing button. */
|
||||||
hideScreensharing: boolean;
|
hideScreensharing: boolean;
|
||||||
@@ -160,7 +161,6 @@ interface Props {
|
|||||||
hostBridge: HostBridge;
|
hostBridge: HostBridge;
|
||||||
baseUrl: string;
|
baseUrl: string;
|
||||||
delayId$: Behavior<string | null>;
|
delayId$: Behavior<string | null>;
|
||||||
matrixRTCMode: MatrixRTCMode;
|
|
||||||
logger: Logger;
|
logger: Logger;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -208,7 +208,6 @@ export const createLocalMembership$ = ({
|
|||||||
hostBridge,
|
hostBridge,
|
||||||
ownMembershipIdentity,
|
ownMembershipIdentity,
|
||||||
delayId$,
|
delayId$,
|
||||||
matrixRTCMode,
|
|
||||||
}: Props): {
|
}: Props): {
|
||||||
/**
|
/**
|
||||||
* This request to start audio and video tracks.
|
* This request to start audio and video tracks.
|
||||||
@@ -714,7 +713,6 @@ export const createLocalMembership$ = ({
|
|||||||
transport: joinParams.transport,
|
transport: joinParams.transport,
|
||||||
roomId,
|
roomId,
|
||||||
delayId,
|
delayId,
|
||||||
matrixRTCMode,
|
|
||||||
logger,
|
logger,
|
||||||
});
|
});
|
||||||
logger.info("Delayed leave successfully delegated");
|
logger.info("Delayed leave successfully delegated");
|
||||||
@@ -953,7 +951,7 @@ interface EnterRTCSessionOptions {
|
|||||||
*
|
*
|
||||||
* @param rtcSession - The MatrixRTCSession to join.
|
* @param rtcSession - The MatrixRTCSession to join.
|
||||||
* @param ownMembershipIdentity - Options for entering the RTC session.
|
* @param ownMembershipIdentity - Options for entering the RTC session.
|
||||||
* @param transport - The LivekitTransport to use for this session.
|
* @param transport - The UnstableLivekitTransport to use for this session.
|
||||||
* @param options - `encryptMedia`: Whether to encrypt media. `matrixRTCMode`: The
|
* @param options - `encryptMedia`: Whether to encrypt media. `matrixRTCMode`: The
|
||||||
* Matrix RTC mode to use. `delayedLeaveTimings`: The preferred timings for
|
* Matrix RTC mode to use. `delayedLeaveTimings`: The preferred timings for
|
||||||
* delayed leave events. `sendNotificationType`: Whether and what kind of
|
* delayed leave events. `sendNotificationType`: Whether and what kind of
|
||||||
@@ -964,7 +962,7 @@ interface EnterRTCSessionOptions {
|
|||||||
export function enterRTCSession(
|
export function enterRTCSession(
|
||||||
rtcSession: MatrixRTCSession,
|
rtcSession: MatrixRTCSession,
|
||||||
ownMembershipIdentity: CallMembershipIdentityParts,
|
ownMembershipIdentity: CallMembershipIdentityParts,
|
||||||
transport: LivekitTransport,
|
transport: UnstableLivekitTransport,
|
||||||
{
|
{
|
||||||
encryptMedia,
|
encryptMedia,
|
||||||
matrixRTCMode,
|
matrixRTCMode,
|
||||||
|
|||||||
@@ -5,9 +5,9 @@ SPDX-License-IdFentifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
|||||||
Please see LICENSE in the repository root for full details.
|
Please see LICENSE in the repository root for full details.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { type LivekitTransport } from "matrix-js-sdk/lib/matrixrtc";
|
import { type UnstableLivekitTransport } from "matrix-js-sdk/lib/matrixrtc";
|
||||||
import { type MatrixClient } from "matrix-js-sdk";
|
import { type MatrixClient } from "matrix-js-sdk";
|
||||||
import { logger as rootLogger, type Logger } from "matrix-js-sdk/lib/logger";
|
import { logger as rootLogger } from "matrix-js-sdk/lib/logger";
|
||||||
import { type CallMembershipIdentityParts } from "matrix-js-sdk/lib/matrixrtc/EncryptionManager";
|
import { type CallMembershipIdentityParts } from "matrix-js-sdk/lib/matrixrtc/EncryptionManager";
|
||||||
|
|
||||||
import { Config } from "../../../config/Config.ts";
|
import { Config } from "../../../config/Config.ts";
|
||||||
@@ -16,31 +16,31 @@ import {
|
|||||||
MatrixRTCTransportMissingError,
|
MatrixRTCTransportMissingError,
|
||||||
NoMatrix2AuthorizationService,
|
NoMatrix2AuthorizationService,
|
||||||
} from "../../../utils/errors.ts";
|
} from "../../../utils/errors.ts";
|
||||||
import {
|
|
||||||
getSFUConfigWithOpenID,
|
|
||||||
type SFUConfig,
|
|
||||||
type OpenIDClientParts,
|
|
||||||
} from "../../../livekit/openIDSFU.ts";
|
|
||||||
import { customLivekitUrl } from "../../../settings/settings.ts";
|
import { customLivekitUrl } from "../../../settings/settings.ts";
|
||||||
import { RtcTransportAutoDiscovery } from "./RtcTransportAutoDiscovery.ts";
|
import { RtcTransportAutoDiscovery } from "./RtcTransportAutoDiscovery.ts";
|
||||||
import { type MatrixRTCMode } from "../../../config/ConfigOptions.ts";
|
import {
|
||||||
|
getSFUConfig,
|
||||||
|
type ClientGetTokenParts,
|
||||||
|
type ClientOpenIDParts,
|
||||||
|
type SFUConfig,
|
||||||
|
} from "../../../livekit/auth";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
ownMembershipIdentity: CallMembershipIdentityParts;
|
ownMembershipIdentity: CallMembershipIdentityParts;
|
||||||
client: Pick<MatrixClient, "getDomain" | "_unstable_getRTCTransports"> &
|
client: Pick<MatrixClient, "getDomain" | "_unstable_getRTCTransports"> &
|
||||||
OpenIDClientParts;
|
ClientGetTokenParts &
|
||||||
|
ClientOpenIDParts;
|
||||||
// Used by the jwt service to create the livekit room and compute the livekit alias.
|
// Used by the jwt service to create the livekit room and compute the livekit alias.
|
||||||
roomId: string;
|
roomId: string;
|
||||||
matrixRTCMode: MatrixRTCMode;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface LocalTransport {
|
export interface LocalTransport {
|
||||||
transport: LivekitTransport;
|
transport: UnstableLivekitTransport;
|
||||||
sfuConfig: SFUConfig;
|
sfuConfig: SFUConfig;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function isLocalTransport(
|
export function isLocalTransport(
|
||||||
obj: LivekitTransport | LocalTransport,
|
obj: UnstableLivekitTransport | LocalTransport,
|
||||||
): obj is LocalTransport {
|
): obj is LocalTransport {
|
||||||
return "transport" in obj && "sfuConfig" in obj;
|
return "transport" in obj && "sfuConfig" in obj;
|
||||||
}
|
}
|
||||||
@@ -58,7 +58,6 @@ export async function getLocalTransport({
|
|||||||
ownMembershipIdentity,
|
ownMembershipIdentity,
|
||||||
client,
|
client,
|
||||||
roomId,
|
roomId,
|
||||||
matrixRTCMode,
|
|
||||||
}: Props): Promise<LocalTransport> {
|
}: Props): Promise<LocalTransport> {
|
||||||
const logger = rootLogger.getChild("[LocalTransport]");
|
const logger = rootLogger.getChild("[LocalTransport]");
|
||||||
const discovery = new RtcTransportAutoDiscovery({
|
const discovery = new RtcTransportAutoDiscovery({
|
||||||
@@ -69,7 +68,7 @@ export async function getLocalTransport({
|
|||||||
const customUrl = customLivekitUrl.value$.value;
|
const customUrl = customLivekitUrl.value$.value;
|
||||||
|
|
||||||
// Respect the user's custom URL, if set
|
// Respect the user's custom URL, if set
|
||||||
const transport: LivekitTransport | null = customUrl
|
const transport: UnstableLivekitTransport | null = customUrl
|
||||||
? { type: "livekit", livekit_service_url: customUrl }
|
? { type: "livekit", livekit_service_url: customUrl }
|
||||||
: await discovery.discoverPreferredTransport();
|
: await discovery.discoverPreferredTransport();
|
||||||
|
|
||||||
@@ -77,60 +76,26 @@ export async function getLocalTransport({
|
|||||||
throw new MatrixRTCTransportMissingError(client.getDomain() ?? "");
|
throw new MatrixRTCTransportMissingError(client.getDomain() ?? "");
|
||||||
|
|
||||||
try {
|
try {
|
||||||
return await doOpenIdAndJWTFromUrl(
|
return {
|
||||||
transport,
|
transport,
|
||||||
matrixRTCMode,
|
sfuConfig: await getSFUConfig({
|
||||||
ownMembershipIdentity,
|
client,
|
||||||
roomId,
|
membership: ownMembershipIdentity,
|
||||||
client,
|
transport,
|
||||||
logger,
|
roomId,
|
||||||
);
|
role: "publisher",
|
||||||
|
logger,
|
||||||
|
}),
|
||||||
|
};
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
logger.error(
|
logger.error(
|
||||||
`Failed to authenticate to transport ${transport.livekit_service_url}`,
|
`Failed to authenticate to transport ${JSON.stringify(transport)}`,
|
||||||
e,
|
e,
|
||||||
);
|
);
|
||||||
throw mapAuthErrorToUserFriendlyError(e);
|
throw mapAuthErrorToUserFriendlyError(e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Utility to ensure the user can authenticate with the SFU.
|
|
||||||
* We will call `getSFUConfigWithOpenID` once per transport here as it's our
|
|
||||||
* only mechanism of validation. This means we will also ask the
|
|
||||||
* homeserver for a OpenID token a few times. Since OpenID tokens are single
|
|
||||||
* use we don't want to risk any issues by re-using a token.
|
|
||||||
*
|
|
||||||
* @param transport The transport to authenticate with.
|
|
||||||
* @param matrixRTCMode Whether to force the JWT endpoint to be used.
|
|
||||||
* @param membership The identity of the local member.
|
|
||||||
* @param roomId The room ID to use for the JWT.
|
|
||||||
* @param client The client to use for the OpenID token.
|
|
||||||
*
|
|
||||||
* @throws FailToGetOpenIdToken, NoMatrix2AuthorizationService
|
|
||||||
*/
|
|
||||||
async function doOpenIdAndJWTFromUrl(
|
|
||||||
transport: LivekitTransport,
|
|
||||||
matrixRTCMode: MatrixRTCMode,
|
|
||||||
membership: CallMembershipIdentityParts,
|
|
||||||
roomId: string,
|
|
||||||
client: Pick<MatrixClient, "_unstable_getRTCTransports"> & OpenIDClientParts,
|
|
||||||
logger?: Logger,
|
|
||||||
): Promise<LocalTransport> {
|
|
||||||
const sfuConfig = await getSFUConfigWithOpenID(
|
|
||||||
client,
|
|
||||||
membership,
|
|
||||||
transport.livekit_service_url,
|
|
||||||
roomId,
|
|
||||||
{ matrixRTCMode },
|
|
||||||
logger,
|
|
||||||
);
|
|
||||||
return {
|
|
||||||
transport,
|
|
||||||
sfuConfig,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
function mapAuthErrorToUserFriendlyError(e: unknown): Error {
|
function mapAuthErrorToUserFriendlyError(e: unknown): Error {
|
||||||
if (
|
if (
|
||||||
e instanceof FailToGetOpenIdToken ||
|
e instanceof FailToGetOpenIdToken ||
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ import {
|
|||||||
import { MatrixError } from "matrix-js-sdk";
|
import { MatrixError } from "matrix-js-sdk";
|
||||||
import { logger as rootLogger } from "matrix-js-sdk/lib/logger";
|
import { logger as rootLogger } from "matrix-js-sdk/lib/logger";
|
||||||
import {
|
import {
|
||||||
type LivekitTransport,
|
type UnstableLivekitTransport,
|
||||||
type Transport,
|
type Transport,
|
||||||
} from "matrix-js-sdk/lib/matrixrtc";
|
} from "matrix-js-sdk/lib/matrixrtc";
|
||||||
|
|
||||||
@@ -28,12 +28,12 @@ import {
|
|||||||
|
|
||||||
type DiscoveryClient = RtcTransportAutoDiscoveryProps["client"];
|
type DiscoveryClient = RtcTransportAutoDiscoveryProps["client"];
|
||||||
|
|
||||||
const backendTransport: LivekitTransport = {
|
const backendTransport: UnstableLivekitTransport = {
|
||||||
type: "livekit",
|
type: "livekit",
|
||||||
livekit_service_url: "https://backend.example.org",
|
livekit_service_url: "https://backend.example.org",
|
||||||
};
|
};
|
||||||
|
|
||||||
const configTransport: LivekitTransport = {
|
const configTransport: UnstableLivekitTransport = {
|
||||||
type: "livekit",
|
type: "livekit",
|
||||||
livekit_service_url: "https://config.example.org",
|
livekit_service_url: "https://config.example.org",
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -5,8 +5,8 @@ SPDX-License-IdFentifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
|||||||
Please see LICENSE in the repository root for full details.
|
Please see LICENSE in the repository root for full details.
|
||||||
*/
|
*/
|
||||||
import {
|
import {
|
||||||
isLivekitTransport,
|
isUnstableLivekitTransport,
|
||||||
type LivekitTransport,
|
type UnstableLivekitTransport,
|
||||||
} from "matrix-js-sdk/lib/matrixrtc";
|
} from "matrix-js-sdk/lib/matrixrtc";
|
||||||
import { type MatrixClient } from "matrix-js-sdk";
|
import { type MatrixClient } from "matrix-js-sdk";
|
||||||
import { type Logger } from "matrix-js-sdk/lib/logger";
|
import { type Logger } from "matrix-js-sdk/lib/logger";
|
||||||
@@ -40,7 +40,7 @@ export class RtcTransportAutoDiscovery {
|
|||||||
this.logger = logger.getChild("[RtcTransportAutoDiscovery]");
|
this.logger = logger.getChild("[RtcTransportAutoDiscovery]");
|
||||||
}
|
}
|
||||||
|
|
||||||
public async discoverPreferredTransport(): Promise<LivekitTransport | null> {
|
public async discoverPreferredTransport(): Promise<UnstableLivekitTransport | null> {
|
||||||
// 1) backend transports
|
// 1) backend transports
|
||||||
const backendTransport = await this.tryBackendTransports();
|
const backendTransport = await this.tryBackendTransports();
|
||||||
if (backendTransport) {
|
if (backendTransport) {
|
||||||
@@ -67,7 +67,7 @@ export class RtcTransportAutoDiscovery {
|
|||||||
* This will not throw errors, but instead just log them and return null if the expected config is not found or malformed.
|
* This will not throw errors, but instead just log them and return null if the expected config is not found or malformed.
|
||||||
* @private
|
* @private
|
||||||
*/
|
*/
|
||||||
private async tryBackendTransports(): Promise<LivekitTransport | null> {
|
private async tryBackendTransports(): Promise<UnstableLivekitTransport | null> {
|
||||||
const client = this.client;
|
const client = this.client;
|
||||||
// MSC4143: Attempt to fetch transports from backend.
|
// MSC4143: Attempt to fetch transports from backend.
|
||||||
this.logger.info("First try to use getRTCTransports end point ...");
|
this.logger.info("First try to use getRTCTransports end point ...");
|
||||||
@@ -75,7 +75,7 @@ export class RtcTransportAutoDiscovery {
|
|||||||
const transportList = await doNetworkOperationWithRetry(async () =>
|
const transportList = await doNetworkOperationWithRetry(async () =>
|
||||||
client._unstable_getRTCTransports(),
|
client._unstable_getRTCTransports(),
|
||||||
);
|
);
|
||||||
const first = transportList.find(isLivekitTransport);
|
const first = transportList.find(isUnstableLivekitTransport);
|
||||||
if (first) {
|
if (first) {
|
||||||
return first;
|
return first;
|
||||||
} else {
|
} else {
|
||||||
@@ -90,7 +90,7 @@ export class RtcTransportAutoDiscovery {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
private tryConfigTransport(): LivekitTransport | null {
|
private tryConfigTransport(): UnstableLivekitTransport | null {
|
||||||
const url = this.resolvedConfig.livekit?.livekit_service_url;
|
const url = this.resolvedConfig.livekit?.livekit_service_url;
|
||||||
if (url) {
|
if (url) {
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ import fetchMock from "fetch-mock";
|
|||||||
import EventEmitter from "events";
|
import EventEmitter from "events";
|
||||||
import { type IOpenIDToken } from "matrix-js-sdk";
|
import { type IOpenIDToken } from "matrix-js-sdk";
|
||||||
import { logger, type Logger } from "matrix-js-sdk/lib/logger";
|
import { logger, type Logger } from "matrix-js-sdk/lib/logger";
|
||||||
import { type LivekitTransport } from "matrix-js-sdk/lib/matrixrtc";
|
import { type UnstableLivekitTransport } from "matrix-js-sdk/lib/matrixrtc";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
Connection,
|
Connection,
|
||||||
@@ -38,7 +38,7 @@ import {
|
|||||||
type ConnectionOpts,
|
type ConnectionOpts,
|
||||||
} from "./Connection.ts";
|
} from "./Connection.ts";
|
||||||
import { ObservableScope } from "../../ObservableScope.ts";
|
import { ObservableScope } from "../../ObservableScope.ts";
|
||||||
import { type OpenIDClientParts } from "../../../livekit/openIDSFU.ts";
|
import { type ClientOpenIDParts } from "../../../livekit/openIDSFU.ts";
|
||||||
import {
|
import {
|
||||||
ElementCallError,
|
ElementCallError,
|
||||||
FailToGetOpenIdToken,
|
FailToGetOpenIdToken,
|
||||||
@@ -48,7 +48,7 @@ import { mockRemoteParticipant, ownMemberMock } from "../../../utils/test.ts";
|
|||||||
|
|
||||||
let testScope: ObservableScope;
|
let testScope: ObservableScope;
|
||||||
|
|
||||||
let client: MockedObject<OpenIDClientParts>;
|
let client: MockedObject<ClientOpenIDParts>;
|
||||||
|
|
||||||
let fakeLivekitRoom: MockedObject<LivekitRoom>;
|
let fakeLivekitRoom: MockedObject<LivekitRoom>;
|
||||||
|
|
||||||
@@ -57,14 +57,14 @@ let fakeLocalParticipant: MockedObject<LocalParticipant>;
|
|||||||
|
|
||||||
const ROOM_ID = "!roomID:example.org";
|
const ROOM_ID = "!roomID:example.org";
|
||||||
|
|
||||||
const livekitFocus: LivekitTransport = {
|
const livekitFocus: UnstableLivekitTransport = {
|
||||||
livekit_service_url: "https://matrix-rtc.example.org/livekit/jwt",
|
livekit_service_url: "https://matrix-rtc.example.org/livekit/jwt",
|
||||||
type: "livekit",
|
type: "livekit",
|
||||||
};
|
};
|
||||||
|
|
||||||
function setupTest(): void {
|
function setupTest(): void {
|
||||||
testScope = new ObservableScope();
|
testScope = new ObservableScope();
|
||||||
client = vi.mocked<OpenIDClientParts>({
|
client = vi.mocked<ClientOpenIDParts>({
|
||||||
getOpenIdToken: vi.fn().mockResolvedValue({
|
getOpenIdToken: vi.fn().mockResolvedValue({
|
||||||
access_token: "rYsmGUEwNjKgJYyeNUkZseJN",
|
access_token: "rYsmGUEwNjKgJYyeNUkZseJN",
|
||||||
token_type: "Bearer",
|
token_type: "Bearer",
|
||||||
@@ -72,7 +72,7 @@ function setupTest(): void {
|
|||||||
expires_in: 3600,
|
expires_in: 3600,
|
||||||
}),
|
}),
|
||||||
getDeviceId: vi.fn().mockReturnValue("ABCDEF"),
|
getDeviceId: vi.fn().mockReturnValue("ABCDEF"),
|
||||||
} as unknown as OpenIDClientParts);
|
} as unknown as ClientOpenIDParts);
|
||||||
|
|
||||||
localParticipantEventEmiter = new EventEmitter();
|
localParticipantEventEmiter = new EventEmitter();
|
||||||
|
|
||||||
|
|||||||
@@ -22,16 +22,11 @@ import {
|
|||||||
type Track,
|
type Track,
|
||||||
type TrackPublication,
|
type TrackPublication,
|
||||||
} from "livekit-client";
|
} from "livekit-client";
|
||||||
import { type LivekitTransport } from "matrix-js-sdk/lib/matrixrtc";
|
import { type UnstableLivekitTransport } from "matrix-js-sdk/lib/matrixrtc";
|
||||||
import { BehaviorSubject, map } from "rxjs";
|
import { BehaviorSubject, map } from "rxjs";
|
||||||
import { type Logger } from "matrix-js-sdk/lib/logger";
|
import { type Logger } from "matrix-js-sdk/lib/logger";
|
||||||
import { type CallMembershipIdentityParts } from "matrix-js-sdk/lib/matrixrtc/EncryptionManager";
|
import { type CallMembershipIdentityParts } from "matrix-js-sdk/lib/matrixrtc/EncryptionManager";
|
||||||
|
|
||||||
import {
|
|
||||||
getSFUConfigWithOpenID,
|
|
||||||
type OpenIDClientParts,
|
|
||||||
type SFUConfig,
|
|
||||||
} from "../../../livekit/openIDSFU.ts";
|
|
||||||
import { type Behavior } from "../../Behavior.ts";
|
import { type Behavior } from "../../Behavior.ts";
|
||||||
import { type ObservableScope } from "../../ObservableScope.ts";
|
import { type ObservableScope } from "../../ObservableScope.ts";
|
||||||
import {
|
import {
|
||||||
@@ -42,6 +37,12 @@ import {
|
|||||||
SFURoomCreationRestrictedError,
|
SFURoomCreationRestrictedError,
|
||||||
UnknownCallError,
|
UnknownCallError,
|
||||||
} from "../../../utils/errors.ts";
|
} from "../../../utils/errors.ts";
|
||||||
|
import {
|
||||||
|
type ClientGetTokenParts,
|
||||||
|
getSFUConfig,
|
||||||
|
type ClientOpenIDParts,
|
||||||
|
type SFUConfig,
|
||||||
|
} from "../../../livekit/auth";
|
||||||
|
|
||||||
export interface ConnectionOpts {
|
export interface ConnectionOpts {
|
||||||
/**
|
/**
|
||||||
@@ -52,9 +53,9 @@ export interface ConnectionOpts {
|
|||||||
/** The identity parts to use on this connection */
|
/** The identity parts to use on this connection */
|
||||||
ownMembershipIdentity: CallMembershipIdentityParts;
|
ownMembershipIdentity: CallMembershipIdentityParts;
|
||||||
/** The media transport to connect to. */
|
/** The media transport to connect to. */
|
||||||
transport: LivekitTransport;
|
transport: UnstableLivekitTransport;
|
||||||
/** The Matrix client to use for OpenID and SFU config requests. */
|
/** The Matrix client to use for OpenID and SFU config requests. */
|
||||||
client: OpenIDClientParts;
|
client: ClientGetTokenParts & ClientOpenIDParts;
|
||||||
/** The room ID this connection is associated with. */
|
/** The room ID this connection is associated with. */
|
||||||
roomId: string;
|
roomId: string;
|
||||||
/** The observable scope to use for this connection. */
|
/** The observable scope to use for this connection. */
|
||||||
@@ -107,7 +108,7 @@ export class Connection {
|
|||||||
/**
|
/**
|
||||||
* The media transport to connect to.
|
* The media transport to connect to.
|
||||||
*/
|
*/
|
||||||
public readonly transport: LivekitTransport;
|
public readonly transport: UnstableLivekitTransport;
|
||||||
|
|
||||||
public readonly livekitRoom: LivekitRoom;
|
public readonly livekitRoom: LivekitRoom;
|
||||||
|
|
||||||
@@ -137,7 +138,7 @@ export class Connection {
|
|||||||
protected stopped = false;
|
protected stopped = false;
|
||||||
|
|
||||||
// TODO: can we just keep the ConnectionOpts object instead of spreading?
|
// TODO: can we just keep the ConnectionOpts object instead of spreading?
|
||||||
private readonly client: OpenIDClientParts;
|
private readonly client: ClientGetTokenParts & ClientOpenIDParts;
|
||||||
private readonly roomId: string;
|
private readonly roomId: string;
|
||||||
private readonly logger: Logger;
|
private readonly logger: Logger;
|
||||||
private readonly ownMembershipIdentity: CallMembershipIdentityParts;
|
private readonly ownMembershipIdentity: CallMembershipIdentityParts;
|
||||||
@@ -154,10 +155,10 @@ export class Connection {
|
|||||||
this.existingSFUConfig = opts.existingSFUConfig;
|
this.existingSFUConfig = opts.existingSFUConfig;
|
||||||
this.roomId = opts.roomId;
|
this.roomId = opts.roomId;
|
||||||
this.logger = logger.getChild(
|
this.logger = logger.getChild(
|
||||||
"[Connection " + opts.transport.livekit_service_url + "]",
|
`[Connection ${JSON.stringify(opts.transport)}]`,
|
||||||
);
|
);
|
||||||
this.logger.info(
|
this.logger.info(
|
||||||
`constructor: ${opts.transport.livekit_service_url} roomId: ${this.roomId} withSfuConfig?: ${opts.existingSFUConfig ? JSON.stringify(opts.existingSFUConfig) : "undefined"}`,
|
`constructor - roomId: ${this.roomId} withSfuConfig?: ${opts.existingSFUConfig ? JSON.stringify(opts.existingSFUConfig) : "undefined"}`,
|
||||||
);
|
);
|
||||||
const { transport, client, scope } = opts;
|
const { transport, client, scope } = opts;
|
||||||
|
|
||||||
@@ -311,9 +312,7 @@ export class Connection {
|
|||||||
this.existingSFUConfig ??
|
this.existingSFUConfig ??
|
||||||
(await this.getSFUConfigForRemoteConnection());
|
(await this.getSFUConfigForRemoteConnection());
|
||||||
this.logger.debug(
|
this.logger.debug(
|
||||||
"Starting Connection to: ",
|
"Starting Connection - jwt: ",
|
||||||
this.transport.livekit_service_url,
|
|
||||||
"jwt: ",
|
|
||||||
jwt,
|
jwt,
|
||||||
"wss: ",
|
"wss: ",
|
||||||
url,
|
url,
|
||||||
@@ -398,15 +397,14 @@ export class Connection {
|
|||||||
protected async getSFUConfigForRemoteConnection(): Promise<SFUConfig> {
|
protected async getSFUConfigForRemoteConnection(): Promise<SFUConfig> {
|
||||||
// This will only be called for sfu's where we do not publish ourselves.
|
// This will only be called for sfu's where we do not publish ourselves.
|
||||||
// For the local connection we will use the existingJwtTokenData
|
// For the local connection we will use the existingJwtTokenData
|
||||||
return await getSFUConfigWithOpenID(
|
return await getSFUConfig({
|
||||||
this.client,
|
client: this.client,
|
||||||
this.ownMembershipIdentity,
|
membership: this.ownMembershipIdentity,
|
||||||
this.transport.livekit_service_url,
|
transport: this.transport,
|
||||||
this.roomId,
|
roomId: this.roomId,
|
||||||
// dont pass any custom opts for the subscribe only connections
|
role: "subscriber",
|
||||||
{},
|
logger: this.logger,
|
||||||
this.logger,
|
});
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -416,17 +414,13 @@ export class Connection {
|
|||||||
* If the connection is already stopped, this is a no-op.
|
* If the connection is already stopped, this is a no-op.
|
||||||
*/
|
*/
|
||||||
public async stop(): Promise<void> {
|
public async stop(): Promise<void> {
|
||||||
this.logger.debug(
|
this.logger.debug("stop: disconnecing from lk room");
|
||||||
`stop: disconnecing from lk room ${this.transport.livekit_service_url}`,
|
|
||||||
);
|
|
||||||
if (this.stopped) return;
|
if (this.stopped) return;
|
||||||
// Mark as stopped before disconnecting so that a connect() aborted by the
|
// Mark as stopped before disconnecting so that a connect() aborted by the
|
||||||
// disconnect sees the flag and does not report the abort as an error.
|
// disconnect sees the flag and does not report the abort as an error.
|
||||||
this.stopped = true;
|
this.stopped = true;
|
||||||
await this.livekitRoom.disconnect();
|
await this.livekitRoom.disconnect();
|
||||||
this._state$.next(ConnectionState.Stopped);
|
this._state$.next(ConnectionState.Stopped);
|
||||||
this.logger.debug(
|
this.logger.debug("stop: DONE disconnecing from lk room");
|
||||||
`stop: DONE disconnecing from lk room ${this.transport.livekit_service_url}`,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,12 +16,12 @@ import { logger, type Logger } from "matrix-js-sdk/lib/logger";
|
|||||||
// imported as inline to support worker when loaded from a cdn (cross domain)
|
// imported as inline to support worker when loaded from a cdn (cross domain)
|
||||||
import E2EEWorker from "livekit-client/e2ee-worker?worker&inline";
|
import E2EEWorker from "livekit-client/e2ee-worker?worker&inline";
|
||||||
import { type CallMembershipIdentityParts } from "matrix-js-sdk/lib/matrixrtc/EncryptionManager";
|
import { type CallMembershipIdentityParts } from "matrix-js-sdk/lib/matrixrtc/EncryptionManager";
|
||||||
import { type LivekitTransport } from "matrix-js-sdk/lib/matrixrtc";
|
import { type UnstableLivekitTransport } from "matrix-js-sdk/lib/matrixrtc";
|
||||||
|
|
||||||
import { type ObservableScope } from "../../ObservableScope.ts";
|
import { type ObservableScope } from "../../ObservableScope.ts";
|
||||||
import { Connection } from "./Connection.ts";
|
import { Connection } from "./Connection.ts";
|
||||||
import type {
|
import type {
|
||||||
OpenIDClientParts,
|
ClientOpenIDParts,
|
||||||
SFUConfig,
|
SFUConfig,
|
||||||
} from "../../../livekit/openIDSFU.ts";
|
} from "../../../livekit/openIDSFU.ts";
|
||||||
import type { MediaDevices } from "../../MediaDevices.ts";
|
import type { MediaDevices } from "../../MediaDevices.ts";
|
||||||
@@ -44,7 +44,7 @@ import {
|
|||||||
export interface ConnectionFactory {
|
export interface ConnectionFactory {
|
||||||
createConnection(
|
createConnection(
|
||||||
scope: ObservableScope,
|
scope: ObservableScope,
|
||||||
transport: LivekitTransport,
|
transport: UnstableLivekitTransport,
|
||||||
ownMembershipIdentity: CallMembershipIdentityParts,
|
ownMembershipIdentity: CallMembershipIdentityParts,
|
||||||
logger: Logger,
|
logger: Logger,
|
||||||
sfuConfig?: SFUConfig,
|
sfuConfig?: SFUConfig,
|
||||||
@@ -66,7 +66,7 @@ export class ECConnectionFactory implements ConnectionFactory {
|
|||||||
* @param livekitRoomFactory - Optional factory function (for testing) to create LivekitRoom instances. If not provided, a default factory is used.
|
* @param livekitRoomFactory - Optional factory function (for testing) to create LivekitRoom instances. If not provided, a default factory is used.
|
||||||
*/
|
*/
|
||||||
public constructor(
|
public constructor(
|
||||||
private client: OpenIDClientParts,
|
private client: ClientOpenIDParts,
|
||||||
private readonly roomId: string,
|
private readonly roomId: string,
|
||||||
private devices: MediaDevices,
|
private devices: MediaDevices,
|
||||||
private processorState$: Behavior<ProcessorState>,
|
private processorState$: Behavior<ProcessorState>,
|
||||||
@@ -103,7 +103,7 @@ export class ECConnectionFactory implements ConnectionFactory {
|
|||||||
*/
|
*/
|
||||||
public createConnection(
|
public createConnection(
|
||||||
scope: ObservableScope,
|
scope: ObservableScope,
|
||||||
transport: LivekitTransport,
|
transport: UnstableLivekitTransport,
|
||||||
ownMembershipIdentity: CallMembershipIdentityParts,
|
ownMembershipIdentity: CallMembershipIdentityParts,
|
||||||
logger: Logger,
|
logger: Logger,
|
||||||
sfuConfig?: SFUConfig,
|
sfuConfig?: SFUConfig,
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ Please see LICENSE in the repository root for full details.
|
|||||||
|
|
||||||
import { afterEach, beforeEach, describe, expect, test, vi } from "vitest";
|
import { afterEach, beforeEach, describe, expect, test, vi } from "vitest";
|
||||||
import { BehaviorSubject, NEVER } from "rxjs";
|
import { BehaviorSubject, NEVER } from "rxjs";
|
||||||
import { type LivekitTransport } from "matrix-js-sdk/lib/matrixrtc";
|
import { type UnstableLivekitTransport } from "matrix-js-sdk/lib/matrixrtc";
|
||||||
import { type RemoteParticipant } from "livekit-client";
|
import { type RemoteParticipant } from "livekit-client";
|
||||||
import { logger } from "matrix-js-sdk/lib/logger";
|
import { logger } from "matrix-js-sdk/lib/logger";
|
||||||
|
|
||||||
@@ -19,17 +19,17 @@ import {
|
|||||||
import { type ConnectionFactory } from "./ConnectionFactory.ts";
|
import { type ConnectionFactory } from "./ConnectionFactory.ts";
|
||||||
import { type Connection } from "./Connection.ts";
|
import { type Connection } from "./Connection.ts";
|
||||||
import { ownMemberMock, withTestScheduler } from "../../../utils/test.ts";
|
import { ownMemberMock, withTestScheduler } from "../../../utils/test.ts";
|
||||||
import { areLivekitTransportsEqual } from "./MatrixLivekitMembers.ts";
|
import { areUnstableLivekitTransportsEqual } from "./MatrixLivekitMembers.ts";
|
||||||
import { type Behavior } from "../../Behavior.ts";
|
import { type Behavior } from "../../Behavior.ts";
|
||||||
|
|
||||||
// Some test constants
|
// Some test constants
|
||||||
|
|
||||||
const TRANSPORT_1: LivekitTransport = {
|
const TRANSPORT_1: UnstableLivekitTransport = {
|
||||||
type: "livekit",
|
type: "livekit",
|
||||||
livekit_service_url: "https://lk.example.org",
|
livekit_service_url: "https://lk.example.org",
|
||||||
};
|
};
|
||||||
|
|
||||||
const TRANSPORT_2: LivekitTransport = {
|
const TRANSPORT_2: UnstableLivekitTransport = {
|
||||||
type: "livekit",
|
type: "livekit",
|
||||||
livekit_service_url: "https://lk.sample.com",
|
livekit_service_url: "https://lk.sample.com",
|
||||||
};
|
};
|
||||||
@@ -47,7 +47,7 @@ beforeEach(() => {
|
|||||||
vi.mocked(fakeConnectionFactory).createConnection = vi
|
vi.mocked(fakeConnectionFactory).createConnection = vi
|
||||||
.fn()
|
.fn()
|
||||||
.mockImplementation(
|
.mockImplementation(
|
||||||
(scope: ObservableScope, transport: LivekitTransport) => {
|
(scope: ObservableScope, transport: UnstableLivekitTransport) => {
|
||||||
const mockConnection = {
|
const mockConnection = {
|
||||||
transport,
|
transport,
|
||||||
remoteParticipants$: new BehaviorSubject([]),
|
remoteParticipants$: new BehaviorSubject([]),
|
||||||
@@ -94,13 +94,13 @@ describe("connections$ stream", () => {
|
|||||||
).toHaveBeenCalledTimes(2);
|
).toHaveBeenCalledTimes(2);
|
||||||
|
|
||||||
const conn1 = connections.find((c) =>
|
const conn1 = connections.find((c) =>
|
||||||
areLivekitTransportsEqual(c.transport, TRANSPORT_1),
|
areUnstableLivekitTransportsEqual(c.transport, TRANSPORT_1),
|
||||||
);
|
);
|
||||||
expect(conn1).toBeDefined();
|
expect(conn1).toBeDefined();
|
||||||
expect(conn1!.start).toHaveBeenCalled();
|
expect(conn1!.start).toHaveBeenCalled();
|
||||||
|
|
||||||
const conn2 = connections.find((c) =>
|
const conn2 = connections.find((c) =>
|
||||||
areLivekitTransportsEqual(c.transport, TRANSPORT_2),
|
areUnstableLivekitTransportsEqual(c.transport, TRANSPORT_2),
|
||||||
);
|
);
|
||||||
expect(conn2).toBeDefined();
|
expect(conn2).toBeDefined();
|
||||||
expect(conn2!.start).toHaveBeenCalled();
|
expect(conn2!.start).toHaveBeenCalled();
|
||||||
@@ -141,12 +141,12 @@ describe("connections$ stream", () => {
|
|||||||
).toHaveBeenCalledTimes(2);
|
).toHaveBeenCalledTimes(2);
|
||||||
|
|
||||||
const conn2 = connections.find((c) =>
|
const conn2 = connections.find((c) =>
|
||||||
areLivekitTransportsEqual(c.transport, TRANSPORT_2),
|
areUnstableLivekitTransportsEqual(c.transport, TRANSPORT_2),
|
||||||
);
|
);
|
||||||
expect(conn2).toBeDefined();
|
expect(conn2).toBeDefined();
|
||||||
|
|
||||||
const conn1 = connections.find((c) =>
|
const conn1 = connections.find((c) =>
|
||||||
areLivekitTransportsEqual(c.transport, TRANSPORT_1),
|
areUnstableLivekitTransportsEqual(c.transport, TRANSPORT_1),
|
||||||
);
|
);
|
||||||
expect(conn1).toBeDefined();
|
expect(conn1).toBeDefined();
|
||||||
expect(conn1!.start).toHaveBeenCalledOnce();
|
expect(conn1!.start).toHaveBeenCalledOnce();
|
||||||
@@ -187,7 +187,7 @@ describe("connections$ stream", () => {
|
|||||||
expect(connections.length).toBe(1);
|
expect(connections.length).toBe(1);
|
||||||
// The second connection should have been stopped has it is no longer needed.
|
// The second connection should have been stopped has it is no longer needed.
|
||||||
const connection2 = allCreatedConnections.find((c) =>
|
const connection2 = allCreatedConnections.find((c) =>
|
||||||
areLivekitTransportsEqual(c.transport, TRANSPORT_2),
|
areUnstableLivekitTransportsEqual(c.transport, TRANSPORT_2),
|
||||||
);
|
);
|
||||||
expect(connection2).toBeDefined();
|
expect(connection2).toBeDefined();
|
||||||
expect(connection2!.stop).toHaveBeenCalled();
|
expect(connection2!.stop).toHaveBeenCalled();
|
||||||
@@ -225,7 +225,7 @@ describe("connectionManagerData$ stream", () => {
|
|||||||
// Used in test to control fake connections' remoteParticipants$ streams
|
// Used in test to control fake connections' remoteParticipants$ streams
|
||||||
let fakeRemoteParticipantsStreams: Map<string, Behavior<RemoteParticipant[]>>;
|
let fakeRemoteParticipantsStreams: Map<string, Behavior<RemoteParticipant[]>>;
|
||||||
|
|
||||||
function keyForTransport(transport: LivekitTransport): string {
|
function keyForTransport(transport: UnstableLivekitTransport): string {
|
||||||
return `${transport.livekit_service_url}`;
|
return `${transport.livekit_service_url}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -233,7 +233,7 @@ describe("connectionManagerData$ stream", () => {
|
|||||||
fakeRemoteParticipantsStreams = new Map();
|
fakeRemoteParticipantsStreams = new Map();
|
||||||
|
|
||||||
function getRemoteParticipantsFor(
|
function getRemoteParticipantsFor(
|
||||||
transport: LivekitTransport,
|
transport: UnstableLivekitTransport,
|
||||||
): Behavior<RemoteParticipant[]> {
|
): Behavior<RemoteParticipant[]> {
|
||||||
return (
|
return (
|
||||||
fakeRemoteParticipantsStreams.get(keyForTransport(transport)) ??
|
fakeRemoteParticipantsStreams.get(keyForTransport(transport)) ??
|
||||||
@@ -245,7 +245,7 @@ describe("connectionManagerData$ stream", () => {
|
|||||||
vi.mocked(fakeConnectionFactory).createConnection = vi
|
vi.mocked(fakeConnectionFactory).createConnection = vi
|
||||||
.fn()
|
.fn()
|
||||||
.mockImplementation(
|
.mockImplementation(
|
||||||
(scope: ObservableScope, transport: LivekitTransport) => {
|
(scope: ObservableScope, transport: UnstableLivekitTransport) => {
|
||||||
const fakeRemoteParticipants$ = new BehaviorSubject<
|
const fakeRemoteParticipants$ = new BehaviorSubject<
|
||||||
RemoteParticipant[]
|
RemoteParticipant[]
|
||||||
>([]);
|
>([]);
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
|||||||
Please see LICENSE in the repository root for full details.
|
Please see LICENSE in the repository root for full details.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { type LivekitTransport } from "matrix-js-sdk/lib/matrixrtc";
|
import { type UnstableLivekitTransport } from "matrix-js-sdk/lib/matrixrtc";
|
||||||
import {
|
import {
|
||||||
combineLatest,
|
combineLatest,
|
||||||
map,
|
map,
|
||||||
@@ -25,7 +25,7 @@ import { type Behavior } from "../../Behavior.ts";
|
|||||||
import { type Connection } from "./Connection.ts";
|
import { type Connection } from "./Connection.ts";
|
||||||
import { Epoch, type ObservableScope } from "../../ObservableScope.ts";
|
import { Epoch, type ObservableScope } from "../../ObservableScope.ts";
|
||||||
import { generateItemsWithEpoch } from "../../../utils/observable.ts";
|
import { generateItemsWithEpoch } from "../../../utils/observable.ts";
|
||||||
import { areLivekitTransportsEqual } from "./MatrixLivekitMembers.ts";
|
import { areUnstableLivekitTransportsEqual } from "./MatrixLivekitMembers.ts";
|
||||||
import { type ConnectionFactory } from "./ConnectionFactory.ts";
|
import { type ConnectionFactory } from "./ConnectionFactory.ts";
|
||||||
import {
|
import {
|
||||||
isLocalTransport,
|
isLocalTransport,
|
||||||
@@ -56,7 +56,7 @@ export class ConnectionManagerData {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private getKey(transport: LivekitTransport): string {
|
private getKey(transport: UnstableLivekitTransport): string {
|
||||||
// This is enough as a key because the ConnectionManager is already scoped by room.
|
// This is enough as a key because the ConnectionManager is already scoped by room.
|
||||||
// We also do not need to consider the slotId at this point since each `MatrixRTCSession` is already scoped by `slotDescription: {id, application}`.
|
// We also do not need to consider the slotId at this point since each `MatrixRTCSession` is already scoped by `slotDescription: {id, application}`.
|
||||||
return transport.livekit_service_url;
|
return transport.livekit_service_url;
|
||||||
@@ -67,13 +67,13 @@ export class ConnectionManagerData {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public getConnectionForTransport(
|
public getConnectionForTransport(
|
||||||
transport: LivekitTransport,
|
transport: UnstableLivekitTransport,
|
||||||
): Connection | null {
|
): Connection | null {
|
||||||
return this.store.get(this.getKey(transport))?.connection ?? null;
|
return this.store.get(this.getKey(transport))?.connection ?? null;
|
||||||
}
|
}
|
||||||
|
|
||||||
public getParticipantsForTransport(
|
public getParticipantsForTransport(
|
||||||
transport: LivekitTransport,
|
transport: UnstableLivekitTransport,
|
||||||
): RemoteParticipant[] {
|
): RemoteParticipant[] {
|
||||||
const key = this.getKey(transport);
|
const key = this.getKey(transport);
|
||||||
const existing = this.store.get(key);
|
const existing = this.store.get(key);
|
||||||
@@ -88,7 +88,7 @@ interface Props {
|
|||||||
scope: ObservableScope;
|
scope: ObservableScope;
|
||||||
connectionFactory: ConnectionFactory;
|
connectionFactory: ConnectionFactory;
|
||||||
localTransport$: Observable<LocalTransport>;
|
localTransport$: Observable<LocalTransport>;
|
||||||
remoteTransports$: Behavior<Epoch<LivekitTransport[]>>;
|
remoteTransports$: Behavior<Epoch<UnstableLivekitTransport[]>>;
|
||||||
|
|
||||||
logger: Logger;
|
logger: Logger;
|
||||||
ownMembershipIdentity: CallMembershipIdentityParts;
|
ownMembershipIdentity: CallMembershipIdentityParts;
|
||||||
@@ -147,7 +147,7 @@ export function createConnectionManager$({
|
|||||||
* externally this is modified via `registerTransports()`.
|
* externally this is modified via `registerTransports()`.
|
||||||
*/
|
*/
|
||||||
const localAndRemoteTransports$: Behavior<
|
const localAndRemoteTransports$: Behavior<
|
||||||
Epoch<(LivekitTransport | LocalTransport)[]>
|
Epoch<(UnstableLivekitTransport | LocalTransport)[]>
|
||||||
> = scope.behavior(
|
> = scope.behavior(
|
||||||
combineLatest([localTransportAsArray$, remoteTransports$]).pipe(
|
combineLatest([localTransportAsArray$, remoteTransports$]).pipe(
|
||||||
// Combine local and remote transports into one transport array
|
// Combine local and remote transports into one transport array
|
||||||
@@ -157,7 +157,7 @@ export function createConnectionManager$({
|
|||||||
const remoteWithoutLocal = dedupedRemote.filter(
|
const remoteWithoutLocal = dedupedRemote.filter(
|
||||||
(transport) =>
|
(transport) =>
|
||||||
!localTransportAsArray.find((l) =>
|
!localTransportAsArray.find((l) =>
|
||||||
areLivekitTransportsEqual(l.transport, transport),
|
areUnstableLivekitTransportsEqual(l.transport, transport),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
logger.debug(
|
logger.debug(
|
||||||
@@ -271,11 +271,11 @@ export function createConnectionManager$({
|
|||||||
return { connectionManagerData$ };
|
return { connectionManagerData$ };
|
||||||
}
|
}
|
||||||
|
|
||||||
function removeDuplicateTransports<T extends LivekitTransport>(
|
function removeDuplicateTransports<T extends UnstableLivekitTransport>(
|
||||||
transports: T[],
|
transports: T[],
|
||||||
): T[] {
|
): T[] {
|
||||||
return transports.reduce((acc, transport) => {
|
return transports.reduce((acc, transport) => {
|
||||||
if (!acc.some((t) => areLivekitTransportsEqual(t, transport)))
|
if (!acc.some((t) => areUnstableLivekitTransportsEqual(t, transport)))
|
||||||
acc.push(transport);
|
acc.push(transport);
|
||||||
return acc;
|
return acc;
|
||||||
}, [] as T[]);
|
}, [] as T[]);
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ import EventEmitter from "events";
|
|||||||
|
|
||||||
import { ObservableScope } from "../../ObservableScope.ts";
|
import { ObservableScope } from "../../ObservableScope.ts";
|
||||||
import { ECConnectionFactory } from "./ConnectionFactory.ts";
|
import { ECConnectionFactory } from "./ConnectionFactory.ts";
|
||||||
import type { OpenIDClientParts } from "../../../livekit/openIDSFU.ts";
|
import type { ClientOpenIDParts } from "../../../livekit/openIDSFU.ts";
|
||||||
import {
|
import {
|
||||||
exampleTransport,
|
exampleTransport,
|
||||||
mockMediaDevices,
|
mockMediaDevices,
|
||||||
@@ -51,7 +51,7 @@ vi.mock("livekit-client", async (importOriginal) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
let testScope: ObservableScope;
|
let testScope: ObservableScope;
|
||||||
let mockClient: OpenIDClientParts;
|
let mockClient: ClientOpenIDParts;
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
testScope = new ObservableScope();
|
testScope = new ObservableScope();
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import { describe, test, expect, beforeEach, afterEach, vi } from "vitest";
|
|||||||
import { logger } from "matrix-js-sdk/lib/logger";
|
import { logger } from "matrix-js-sdk/lib/logger";
|
||||||
import {
|
import {
|
||||||
type CallMembership,
|
type CallMembership,
|
||||||
type LivekitTransport,
|
type UnstableLivekitTransport,
|
||||||
} from "matrix-js-sdk/lib/matrixrtc";
|
} from "matrix-js-sdk/lib/matrixrtc";
|
||||||
import { BehaviorSubject, combineLatest, map, type Observable } from "rxjs";
|
import { BehaviorSubject, combineLatest, map, type Observable } from "rxjs";
|
||||||
|
|
||||||
@@ -39,12 +39,12 @@ let testScope: ObservableScope;
|
|||||||
const fallbackMemberId = (userId: string, deviceId: string): string =>
|
const fallbackMemberId = (userId: string, deviceId: string): string =>
|
||||||
`${userId}:${deviceId}`;
|
`${userId}:${deviceId}`;
|
||||||
|
|
||||||
const transportA: LivekitTransport = {
|
const transportA: UnstableLivekitTransport = {
|
||||||
type: "livekit",
|
type: "livekit",
|
||||||
livekit_service_url: "https://lk.example.org",
|
livekit_service_url: "https://lk.example.org",
|
||||||
};
|
};
|
||||||
|
|
||||||
const transportB: LivekitTransport = {
|
const transportB: UnstableLivekitTransport = {
|
||||||
type: "livekit",
|
type: "livekit",
|
||||||
livekit_service_url: "https://lk.sample.com",
|
livekit_service_url: "https://lk.sample.com",
|
||||||
};
|
};
|
||||||
@@ -117,7 +117,7 @@ test("should signal participant not yet connected to livekit", async () => {
|
|||||||
function createEpochedMemberships$(m$: Observable<CallMembership[]>): {
|
function createEpochedMemberships$(m$: Observable<CallMembership[]>): {
|
||||||
memberships$: Observable<Epoch<CallMembership[]>>;
|
memberships$: Observable<Epoch<CallMembership[]>>;
|
||||||
membershipsWithTransport$: Observable<
|
membershipsWithTransport$: Observable<
|
||||||
Epoch<{ membership: CallMembership; transport?: LivekitTransport }[]>
|
Epoch<{ membership: CallMembership; transport?: UnstableLivekitTransport }[]>
|
||||||
>;
|
>;
|
||||||
} {
|
} {
|
||||||
const memberships$ = m$.pipe(trackEpoch());
|
const memberships$ = m$.pipe(trackEpoch());
|
||||||
@@ -128,7 +128,7 @@ function createEpochedMemberships$(m$: Observable<CallMembership[]>): {
|
|||||||
return {
|
return {
|
||||||
membership: m,
|
membership: m,
|
||||||
transport:
|
transport:
|
||||||
tr?.type === "livekit" ? (tr as LivekitTransport) : undefined,
|
tr?.type === "livekit" ? (tr as UnstableLivekitTransport) : undefined,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
}),
|
}),
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ Please see LICENSE in the repository root for full details.
|
|||||||
import { type LocalParticipant, type RemoteParticipant } from "livekit-client";
|
import { type LocalParticipant, type RemoteParticipant } from "livekit-client";
|
||||||
import {
|
import {
|
||||||
type CallMembership,
|
type CallMembership,
|
||||||
type LivekitTransport,
|
type UnstableLivekitTransport,
|
||||||
} from "matrix-js-sdk/lib/matrixrtc";
|
} from "matrix-js-sdk/lib/matrixrtc";
|
||||||
import { combineLatest, filter, map } from "rxjs";
|
import { combineLatest, filter, map } from "rxjs";
|
||||||
import { logger } from "matrix-js-sdk/lib/logger";
|
import { logger } from "matrix-js-sdk/lib/logger";
|
||||||
@@ -60,7 +60,7 @@ export interface RemoteMatrixLivekitMember extends MatrixLivekitMember {
|
|||||||
interface Props {
|
interface Props {
|
||||||
scope: ObservableScope;
|
scope: ObservableScope;
|
||||||
membershipsWithTransport$: Behavior<
|
membershipsWithTransport$: Behavior<
|
||||||
Epoch<{ membership: CallMembership; transport?: LivekitTransport }[]>
|
Epoch<{ membership: CallMembership; transport?: UnstableLivekitTransport }[]>
|
||||||
>;
|
>;
|
||||||
connectionManager: IConnectionManager;
|
connectionManager: IConnectionManager;
|
||||||
localUser: { deviceId: string; userId: string };
|
localUser: { deviceId: string; userId: string };
|
||||||
@@ -165,7 +165,7 @@ export function createRemoteMatrixLivekitMembers$({
|
|||||||
// TODO add back in the callviewmodel pauseWhen(this.pretendToBeDisconnected$)
|
// TODO add back in the callviewmodel pauseWhen(this.pretendToBeDisconnected$)
|
||||||
|
|
||||||
// TODO add this to the JS-SDK
|
// TODO add this to the JS-SDK
|
||||||
export function areLivekitTransportsEqual<T extends LivekitTransport>(
|
export function areUnstableLivekitTransportsEqual<T extends UnstableLivekitTransport>(
|
||||||
t1: T | null,
|
t1: T | null,
|
||||||
t2: T | null,
|
t2: T | null,
|
||||||
): boolean {
|
): boolean {
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import { BehaviorSubject, NEVER } from "rxjs";
|
|||||||
import { type Room as LivekitRoom } from "livekit-client";
|
import { type Room as LivekitRoom } from "livekit-client";
|
||||||
import EventEmitter from "events";
|
import EventEmitter from "events";
|
||||||
import fetchMock from "fetch-mock";
|
import fetchMock from "fetch-mock";
|
||||||
import { type LivekitTransport } from "matrix-js-sdk/lib/matrixrtc";
|
import { type UnstableLivekitTransport } from "matrix-js-sdk/lib/matrixrtc";
|
||||||
import { logger } from "matrix-js-sdk/lib/logger";
|
import { logger } from "matrix-js-sdk/lib/logger";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
@@ -19,7 +19,7 @@ import {
|
|||||||
trackEpoch,
|
trackEpoch,
|
||||||
} from "../../ObservableScope.ts";
|
} from "../../ObservableScope.ts";
|
||||||
import { ECConnectionFactory } from "./ConnectionFactory.ts";
|
import { ECConnectionFactory } from "./ConnectionFactory.ts";
|
||||||
import { type OpenIDClientParts } from "../../../livekit/openIDSFU.ts";
|
import { type ClientOpenIDParts } from "../../../livekit/openIDSFU.ts";
|
||||||
import {
|
import {
|
||||||
mockMediaDevices,
|
mockMediaDevices,
|
||||||
mockRtcMembership,
|
mockRtcMembership,
|
||||||
@@ -28,7 +28,7 @@ import {
|
|||||||
} from "../../../utils/test.ts";
|
} from "../../../utils/test.ts";
|
||||||
import { type ProcessorState } from "../../../livekit/TrackProcessorContext.tsx";
|
import { type ProcessorState } from "../../../livekit/TrackProcessorContext.tsx";
|
||||||
import {
|
import {
|
||||||
areLivekitTransportsEqual,
|
areUnstableLivekitTransportsEqual,
|
||||||
createRemoteMatrixLivekitMembers$,
|
createRemoteMatrixLivekitMembers$,
|
||||||
type RemoteMatrixLivekitMember,
|
type RemoteMatrixLivekitMember,
|
||||||
} from "./MatrixLivekitMembers.ts";
|
} from "./MatrixLivekitMembers.ts";
|
||||||
@@ -40,7 +40,7 @@ import { localRtcMember, testJWTToken } from "../../../utils/test-fixtures.ts";
|
|||||||
|
|
||||||
let testScope: ObservableScope;
|
let testScope: ObservableScope;
|
||||||
let ecConnectionFactory: ECConnectionFactory;
|
let ecConnectionFactory: ECConnectionFactory;
|
||||||
let mockClient: OpenIDClientParts;
|
let mockClient: ClientOpenIDParts;
|
||||||
let lkRoomFactory: () => LivekitRoom;
|
let lkRoomFactory: () => LivekitRoom;
|
||||||
|
|
||||||
const createdMockLivekitRooms: Map<string, LivekitRoom> = new Map();
|
const createdMockLivekitRooms: Map<string, LivekitRoom> = new Map();
|
||||||
@@ -147,9 +147,9 @@ test("bob, carl, then bob joining no tracks yet", () => {
|
|||||||
});
|
});
|
||||||
expectObservable(item.connection$).toBe("a", {
|
expectObservable(item.connection$).toBe("a", {
|
||||||
a: expect.toSatisfy((co) =>
|
a: expect.toSatisfy((co) =>
|
||||||
areLivekitTransportsEqual(
|
areUnstableLivekitTransportsEqual(
|
||||||
co.transport,
|
co.transport,
|
||||||
bobMembership.transports[0]! as LivekitTransport,
|
bobMembership.transports[0]! as UnstableLivekitTransport,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
});
|
});
|
||||||
@@ -184,9 +184,9 @@ test("bob, carl, then bob joining no tracks yet", () => {
|
|||||||
expectObservable(item.connection$).toBe("a", {
|
expectObservable(item.connection$).toBe("a", {
|
||||||
a: expect.toSatisfy((connection) => {
|
a: expect.toSatisfy((connection) => {
|
||||||
expect(
|
expect(
|
||||||
areLivekitTransportsEqual(
|
areUnstableLivekitTransportsEqual(
|
||||||
connection.transport,
|
connection.transport,
|
||||||
carlMembership.transports[0]! as LivekitTransport,
|
carlMembership.transports[0]! as UnstableLivekitTransport,
|
||||||
),
|
),
|
||||||
).toBe(true);
|
).toBe(true);
|
||||||
return true;
|
return true;
|
||||||
@@ -214,9 +214,9 @@ test("bob, carl, then bob joining no tracks yet", () => {
|
|||||||
expectObservable(item.connection$).toBe("a", {
|
expectObservable(item.connection$).toBe("a", {
|
||||||
a: expect.toSatisfy((connection) => {
|
a: expect.toSatisfy((connection) => {
|
||||||
expect(
|
expect(
|
||||||
areLivekitTransportsEqual(
|
areUnstableLivekitTransportsEqual(
|
||||||
connection.transport,
|
connection.transport,
|
||||||
daveMembership.transports[0]! as LivekitTransport,
|
daveMembership.transports[0]! as UnstableLivekitTransport,
|
||||||
),
|
),
|
||||||
).toBe(true);
|
).toBe(true);
|
||||||
return true;
|
return true;
|
||||||
|
|||||||
@@ -7,10 +7,10 @@ Please see LICENSE in the repository root for full details.
|
|||||||
|
|
||||||
import {
|
import {
|
||||||
type CallMembership,
|
type CallMembership,
|
||||||
type LivekitTransport,
|
type UnstableLivekitTransport,
|
||||||
type MatrixRTCSession,
|
type MatrixRTCSession,
|
||||||
MatrixRTCSessionEvent,
|
MatrixRTCSessionEvent,
|
||||||
isLivekitTransport,
|
isUnstableLivekitTransport,
|
||||||
} from "matrix-js-sdk/lib/matrixrtc";
|
} from "matrix-js-sdk/lib/matrixrtc";
|
||||||
import { fromEvent } from "rxjs";
|
import { fromEvent } from "rxjs";
|
||||||
|
|
||||||
@@ -31,15 +31,15 @@ export const membershipsAndTransports$ = (
|
|||||||
memberships$: Behavior<Epoch<CallMembership[]>>,
|
memberships$: Behavior<Epoch<CallMembership[]>>,
|
||||||
): {
|
): {
|
||||||
membershipsWithTransport$: Behavior<
|
membershipsWithTransport$: Behavior<
|
||||||
Epoch<{ membership: CallMembership; transport?: LivekitTransport }[]>
|
Epoch<{ membership: CallMembership; transport?: UnstableLivekitTransport }[]>
|
||||||
>;
|
>;
|
||||||
transports$: Behavior<Epoch<LivekitTransport[]>>;
|
transports$: Behavior<Epoch<UnstableLivekitTransport[]>>;
|
||||||
} => {
|
} => {
|
||||||
const membershipsWithTransport$: Behavior<
|
const membershipsWithTransport$: Behavior<
|
||||||
Epoch<
|
Epoch<
|
||||||
{
|
{
|
||||||
membership: CallMembership;
|
membership: CallMembership;
|
||||||
transport: LivekitTransport | undefined;
|
transport: UnstableLivekitTransport | undefined;
|
||||||
}[]
|
}[]
|
||||||
>
|
>
|
||||||
> = scope.behavior(
|
> = scope.behavior(
|
||||||
@@ -49,14 +49,14 @@ export const membershipsAndTransports$ = (
|
|||||||
const transport = membership.getTransport();
|
const transport = membership.getTransport();
|
||||||
return {
|
return {
|
||||||
membership,
|
membership,
|
||||||
transport: isLivekitTransport(transport) ? transport : undefined,
|
transport: isUnstableLivekitTransport(transport) ? transport : undefined,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
const transports$: Behavior<Epoch<LivekitTransport[]>> = scope.behavior(
|
const transports$: Behavior<Epoch<UnstableLivekitTransport[]>> = scope.behavior(
|
||||||
membershipsWithTransport$.pipe(
|
membershipsWithTransport$.pipe(
|
||||||
mapEpoch((mts) => mts.flatMap(({ transport: t }) => (t ? [t] : []))),
|
mapEpoch((mts) => mts.flatMap(({ transport: t }) => (t ? [t] : []))),
|
||||||
),
|
),
|
||||||
|
|||||||
+2
-2
@@ -26,7 +26,7 @@ import {
|
|||||||
import {
|
import {
|
||||||
CallMembership,
|
CallMembership,
|
||||||
type LivekitFocusSelection,
|
type LivekitFocusSelection,
|
||||||
type LivekitTransport,
|
type UnstableLivekitTransport,
|
||||||
type MatrixRTCSession,
|
type MatrixRTCSession,
|
||||||
MatrixRTCSessionEvent,
|
MatrixRTCSessionEvent,
|
||||||
type MatrixRTCSessionEventHandlerMap,
|
type MatrixRTCSessionEventHandlerMap,
|
||||||
@@ -209,7 +209,7 @@ export function mockEmitter<T>(): EmitterMock<T> {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export const exampleTransport: LivekitTransport = {
|
export const exampleTransport: UnstableLivekitTransport = {
|
||||||
type: "livekit",
|
type: "livekit",
|
||||||
livekit_service_url: "https://lk.example.org",
|
livekit_service_url: "https://lk.example.org",
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user