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:
+1
-1
@@ -103,7 +103,7 @@
|
|||||||
"livekit-client": "^2.18.1",
|
"livekit-client": "^2.18.1",
|
||||||
"lodash-es": "^4.17.21",
|
"lodash-es": "^4.17.21",
|
||||||
"loglevel": "^1.9.1",
|
"loglevel": "^1.9.1",
|
||||||
"matrix-js-sdk": "github:matrix-org/matrix-js-sdk#develop",
|
"matrix-js-sdk": "github:matrix-org/matrix-js-sdk#robin/livekit-updates",
|
||||||
"matrix-widget-api": "^1.18.0",
|
"matrix-widget-api": "^1.18.0",
|
||||||
"node-stdlib-browser": "^1.3.1",
|
"node-stdlib-browser": "^1.3.1",
|
||||||
"normalize.css": "^8.0.1",
|
"normalize.css": "^8.0.1",
|
||||||
|
|||||||
Generated
+14
-6
@@ -191,8 +191,8 @@ importers:
|
|||||||
specifier: ^1.9.1
|
specifier: ^1.9.1
|
||||||
version: 1.9.2
|
version: 1.9.2
|
||||||
matrix-js-sdk:
|
matrix-js-sdk:
|
||||||
specifier: github:matrix-org/matrix-js-sdk#develop
|
specifier: github:matrix-org/matrix-js-sdk#robin/livekit-updates
|
||||||
version: https://codeload.github.com/matrix-org/matrix-js-sdk/tar.gz/fa4772a4ddd335a5a8bfbf8bb21df49b70f7a999
|
version: https://codeload.github.com/matrix-org/matrix-js-sdk/tar.gz/621b225e227dc6dc2b1c9d53413235d8999e6d54
|
||||||
matrix-widget-api:
|
matrix-widget-api:
|
||||||
specifier: ^1.18.0
|
specifier: ^1.18.0
|
||||||
version: 1.19.0
|
version: 1.19.0
|
||||||
@@ -4673,14 +4673,17 @@ packages:
|
|||||||
matrix-events-sdk@0.0.1:
|
matrix-events-sdk@0.0.1:
|
||||||
resolution: {integrity: sha512-1QEOsXO+bhyCroIe2/A5OwaxHvBm7EsSQ46DEDn8RBIfQwN5HWBpFvyWWR4QY0KHPPnnJdI99wgRiAl7Ad5qaA==}
|
resolution: {integrity: sha512-1QEOsXO+bhyCroIe2/A5OwaxHvBm7EsSQ46DEDn8RBIfQwN5HWBpFvyWWR4QY0KHPPnnJdI99wgRiAl7Ad5qaA==}
|
||||||
|
|
||||||
matrix-js-sdk@https://codeload.github.com/matrix-org/matrix-js-sdk/tar.gz/fa4772a4ddd335a5a8bfbf8bb21df49b70f7a999:
|
matrix-js-sdk@https://codeload.github.com/matrix-org/matrix-js-sdk/tar.gz/621b225e227dc6dc2b1c9d53413235d8999e6d54:
|
||||||
resolution: {gitHosted: true, integrity: sha512-qF8++74MX36KS1KEw6Lc8HHXgZ6nHD1oUU4yBZUj5vVPvZTIspk1lOGztkFmizE5vmLAHd5tdadL9Xu1Asy4bA==, tarball: https://codeload.github.com/matrix-org/matrix-js-sdk/tar.gz/fa4772a4ddd335a5a8bfbf8bb21df49b70f7a999}
|
resolution: {gitHosted: true, integrity: sha512-HeTjG3NPGeCMkfPfGPaYl1QBnn7XYRiC1NpBgOxflujzQmTBNTPshGSTzMsMQKH/hpfi8QtjPE1SddNzzax3vA==, tarball: https://codeload.github.com/matrix-org/matrix-js-sdk/tar.gz/621b225e227dc6dc2b1c9d53413235d8999e6d54}
|
||||||
version: 42.3.0
|
version: 42.3.0
|
||||||
engines: {node: '>=22.0.0'}
|
engines: {node: '>=22.0.0'}
|
||||||
|
|
||||||
matrix-widget-api@1.19.0:
|
matrix-widget-api@1.19.0:
|
||||||
resolution: {integrity: sha512-Qe7L6G1mQJphBC47/Bzp5ljPIK1UARgLiuGgS+/KmEs/eaNHkQ3+ArD0/0kvMV+d6kCGAZ9OKRqsnuPjizI+tw==}
|
resolution: {integrity: sha512-Qe7L6G1mQJphBC47/Bzp5ljPIK1UARgLiuGgS+/KmEs/eaNHkQ3+ArD0/0kvMV+d6kCGAZ9OKRqsnuPjizI+tw==}
|
||||||
|
|
||||||
|
matrix-widget-api@1.20.0:
|
||||||
|
resolution: {integrity: sha512-qwFP37rgns7mPesM5/vvZgknzb+kvyRB5BISv5sCuBxsGMceMr+Kogf7OkjRxBXdMCPBFuf6gIuYRe5S+lpzQw==}
|
||||||
|
|
||||||
md5.js@1.3.5:
|
md5.js@1.3.5:
|
||||||
resolution: {integrity: sha512-xitP+WxNPcTTOgnTJcrhM0xvdPepipPSf3I8EIpGKeFLjt3PlJLIDG3u8EX53ZIubkb+5U2+3rELYpEhHhzdkg==}
|
resolution: {integrity: sha512-xitP+WxNPcTTOgnTJcrhM0xvdPepipPSf3I8EIpGKeFLjt3PlJLIDG3u8EX53ZIubkb+5U2+3rELYpEhHhzdkg==}
|
||||||
|
|
||||||
@@ -10227,7 +10230,7 @@ snapshots:
|
|||||||
|
|
||||||
matrix-events-sdk@0.0.1: {}
|
matrix-events-sdk@0.0.1: {}
|
||||||
|
|
||||||
matrix-js-sdk@https://codeload.github.com/matrix-org/matrix-js-sdk/tar.gz/fa4772a4ddd335a5a8bfbf8bb21df49b70f7a999:
|
matrix-js-sdk@https://codeload.github.com/matrix-org/matrix-js-sdk/tar.gz/621b225e227dc6dc2b1c9d53413235d8999e6d54:
|
||||||
dependencies:
|
dependencies:
|
||||||
'@babel/runtime': 8.0.0
|
'@babel/runtime': 8.0.0
|
||||||
'@matrix-org/matrix-sdk-crypto-wasm': 18.9.0
|
'@matrix-org/matrix-sdk-crypto-wasm': 18.9.0
|
||||||
@@ -10236,7 +10239,7 @@ snapshots:
|
|||||||
content-type: 3.1.1
|
content-type: 3.1.1
|
||||||
loglevel: 1.9.2
|
loglevel: 1.9.2
|
||||||
matrix-events-sdk: 0.0.1
|
matrix-events-sdk: 0.0.1
|
||||||
matrix-widget-api: 1.19.0
|
matrix-widget-api: 1.20.0
|
||||||
p-retry: 8.0.1
|
p-retry: 8.0.1
|
||||||
sdp-transform: 3.0.0
|
sdp-transform: 3.0.0
|
||||||
unhomoglyph: 1.0.6
|
unhomoglyph: 1.0.6
|
||||||
@@ -10246,6 +10249,11 @@ snapshots:
|
|||||||
'@types/events': 3.0.3
|
'@types/events': 3.0.3
|
||||||
events: 3.3.0
|
events: 3.3.0
|
||||||
|
|
||||||
|
matrix-widget-api@1.20.0:
|
||||||
|
dependencies:
|
||||||
|
'@types/events': 3.0.3
|
||||||
|
events: 3.3.0
|
||||||
|
|
||||||
md5.js@1.3.5:
|
md5.js@1.3.5:
|
||||||
dependencies:
|
dependencies:
|
||||||
hash-base: 3.0.5
|
hash-base: 3.0.5
|
||||||
|
|||||||
@@ -0,0 +1,71 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2026 Element Creations Ltd.
|
||||||
|
|
||||||
|
SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
||||||
|
Please see LICENSE in the repository root for full details.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { type MatrixClient } from "matrix-js-sdk";
|
||||||
|
import { type CallMembershipIdentityParts } from "matrix-js-sdk/lib/matrixrtc/EncryptionManager";
|
||||||
|
|
||||||
|
import { type OpenIDClientParts } from "./openID.ts";
|
||||||
|
import { extractFullConfigFromToken, type SFUConfig } from "./types.ts";
|
||||||
|
import { doNetworkOperationWithRetry } from "../../utils/matrix.ts";
|
||||||
|
|
||||||
|
// TODO: This should come from the `MatrixRTCSession`s slot description instead
|
||||||
|
// of being hardcoded here. (the legacy flow hardcodes it as well)
|
||||||
|
const SLOT_ID = "m.call#ROOM";
|
||||||
|
|
||||||
|
export type ClientGetTokenParts = OpenIDClientParts &
|
||||||
|
Pick<MatrixClient, "_unstable_getLivekitToken">;
|
||||||
|
|
||||||
|
export async function getSFUConfig({
|
||||||
|
client,
|
||||||
|
membership,
|
||||||
|
url,
|
||||||
|
roomId,
|
||||||
|
}: {
|
||||||
|
client: ClientGetTokenParts;
|
||||||
|
membership: CallMembershipIdentityParts;
|
||||||
|
url: string;
|
||||||
|
roomId: string;
|
||||||
|
}): Promise<SFUConfig> {
|
||||||
|
const res = await doNetworkOperationWithRetry(async () =>
|
||||||
|
client._unstable_getLivekitToken({
|
||||||
|
url,
|
||||||
|
room_id: roomId,
|
||||||
|
slot_id: "m.call#ROOM",
|
||||||
|
member: { id: membership.memberId },
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
return extractFullConfigFromToken({ url, jwt: res.jwt });
|
||||||
|
}
|
||||||
|
|
||||||
|
export type ClientDelegationParts = Pick<
|
||||||
|
MatrixClient,
|
||||||
|
"_unstable_delegateDelayedLeave" | "baseUrl"
|
||||||
|
>;
|
||||||
|
|
||||||
|
export async function delegateDelayedLeave({
|
||||||
|
client,
|
||||||
|
membership,
|
||||||
|
url,
|
||||||
|
roomId,
|
||||||
|
delayId,
|
||||||
|
}: {
|
||||||
|
client: ClientDelegationParts;
|
||||||
|
membership: CallMembershipIdentityParts;
|
||||||
|
url: string;
|
||||||
|
roomId: string;
|
||||||
|
delayId: string;
|
||||||
|
}): Promise<void> {
|
||||||
|
await doNetworkOperationWithRetry(async () =>
|
||||||
|
client._unstable_delegateDelayedLeave({
|
||||||
|
url,
|
||||||
|
room_id: roomId,
|
||||||
|
slot_id: SLOT_ID,
|
||||||
|
member: { id: membership.memberId },
|
||||||
|
delay_id: delayId,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,148 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2026 Element Creations Ltd.
|
||||||
|
|
||||||
|
SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
||||||
|
Please see LICENSE in the repository root for full details.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { type CallMembershipIdentityParts } from "matrix-js-sdk/lib/matrixrtc/EncryptionManager";
|
||||||
|
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 { type LivekitTransport } from "matrix-js-sdk/lib/matrixrtc";
|
||||||
|
import { type SFUConfig } from "./types";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks whether an error means "this homeserver does not implement the
|
||||||
|
* (unstable) MSC4195 endpoints".
|
||||||
|
*
|
||||||
|
* MSC4195 documents `M_NOT_FOUND` for this, but a homeserver without support
|
||||||
|
* answers with whatever it uses for unknown endpoints (Synapse: 404 with
|
||||||
|
* `M_UNRECOGNIZED`), so we accept any 404 as "unsupported".
|
||||||
|
* @param e The error to check.
|
||||||
|
*/
|
||||||
|
function isEndpointUnsupported(e: unknown): boolean {
|
||||||
|
return (
|
||||||
|
e instanceof MatrixError &&
|
||||||
|
(e.httpStatus === 404 ||
|
||||||
|
e.errcode === "M_NOT_FOUND" ||
|
||||||
|
e.errcode === "M_UNRECOGNIZED")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getSFUConfig({
|
||||||
|
client,
|
||||||
|
membership,
|
||||||
|
transport,
|
||||||
|
roomId,
|
||||||
|
matrixRTCMode,
|
||||||
|
logger,
|
||||||
|
}: {
|
||||||
|
client: ClientGetTokenParts & OpenIDClientParts;
|
||||||
|
membership: CallMembershipIdentityParts;
|
||||||
|
transport: LivekitTransport;
|
||||||
|
roomId: string;
|
||||||
|
matrixRTCMode: MatrixRTCMode;
|
||||||
|
logger: Logger;
|
||||||
|
}): Promise<SFUConfig> {
|
||||||
|
if ("url" in transport) {
|
||||||
|
try {
|
||||||
|
return await getSFUConfigWithCSApi({
|
||||||
|
client,
|
||||||
|
membership,
|
||||||
|
url: transport.url,
|
||||||
|
roomId,
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
if (isEndpointUnsupported(e)) {
|
||||||
|
// Alias transport before checking whether we can fall back, otherwise
|
||||||
|
// TypeScript gets confused about its type
|
||||||
|
const transport_ = transport;
|
||||||
|
if ("livekit_service_url" in transport_) {
|
||||||
|
logger.warn(
|
||||||
|
`Homeserver does not support the MSC4195 get_token endpoint, falling back to the OpenID flow with service ${transport_.livekit_service_url}`,
|
||||||
|
);
|
||||||
|
// Fall through
|
||||||
|
} else {
|
||||||
|
logger.error(
|
||||||
|
`Homeserver does not support the MSC4195 get_token endpoint, cannot get token for transport ${transport.url}`,
|
||||||
|
);
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ("livekit_service_url" in transport) {
|
||||||
|
return await getSFUConfigWithOpenID(
|
||||||
|
client,
|
||||||
|
membership,
|
||||||
|
transport.livekit_service_url,
|
||||||
|
roomId,
|
||||||
|
{ matrixRTCMode },
|
||||||
|
logger,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Error(`Unrecognized transport: ${JSON.stringify(transport)}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function delegateDelayedLeave({
|
||||||
|
client,
|
||||||
|
membership,
|
||||||
|
transport,
|
||||||
|
roomId,
|
||||||
|
delayId,
|
||||||
|
matrixRTCMode,
|
||||||
|
logger,
|
||||||
|
}: {
|
||||||
|
client: ClientDelegationParts & OpenIDClientParts;
|
||||||
|
membership: CallMembershipIdentityParts;
|
||||||
|
transport: LivekitTransport;
|
||||||
|
roomId: string;
|
||||||
|
delayId: string;
|
||||||
|
matrixRTCMode: MatrixRTCMode;
|
||||||
|
logger: Logger;
|
||||||
|
}): Promise<void> {
|
||||||
|
if ("url" in transport) {
|
||||||
|
try {
|
||||||
|
await delegateDelayedLeaveWithCSApi({
|
||||||
|
client,
|
||||||
|
membership,
|
||||||
|
url: transport.url,
|
||||||
|
roomId,
|
||||||
|
delayId,
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
if (isEndpointUnsupported(e))
|
||||||
|
logger.error(
|
||||||
|
"Server misconfigured: transport contains a 'url' field but server does not support the MSC4195 delegate_delayed_leave endpoint",
|
||||||
|
);
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// 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.
|
||||||
|
await getSFUConfigWithOpenID(
|
||||||
|
client,
|
||||||
|
membership,
|
||||||
|
transport.livekit_service_url,
|
||||||
|
roomId,
|
||||||
|
{ matrixRTCMode, delayEndpointBaseUrl: client.baseUrl, delayId },
|
||||||
|
logger,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -16,54 +16,11 @@ import { type Logger } from "matrix-js-sdk/lib/logger";
|
|||||||
import {
|
import {
|
||||||
FailToGetOpenIdToken,
|
FailToGetOpenIdToken,
|
||||||
NoMatrix2AuthorizationService,
|
NoMatrix2AuthorizationService,
|
||||||
} from "../utils/errors";
|
} 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 { MatrixRTCMode } from "../../config/ConfigOptions";
|
||||||
|
import { extractFullConfigFromToken, type SFUConfig } from "./types";
|
||||||
/**
|
|
||||||
* Configuration and access tokens provided by the SFU on successful authentication.
|
|
||||||
*/
|
|
||||||
export interface SFUConfig {
|
|
||||||
url: string;
|
|
||||||
jwt: string;
|
|
||||||
livekitAlias: string;
|
|
||||||
// NOTE: Currently unused.
|
|
||||||
livekitIdentity: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Decoded details from the JWT.
|
|
||||||
*/
|
|
||||||
interface SFUJWTPayload {
|
|
||||||
/**
|
|
||||||
* Expiration time for the JWT.
|
|
||||||
* Note: This value is in seconds since Unix epoch.
|
|
||||||
*/
|
|
||||||
exp: number;
|
|
||||||
/**
|
|
||||||
* Name of the instance which authored the JWT
|
|
||||||
*/
|
|
||||||
iss: string;
|
|
||||||
/**
|
|
||||||
* Time at which the JWT can start to be used.
|
|
||||||
* Note: This value is in seconds since Unix epoch.
|
|
||||||
*/
|
|
||||||
nbf: number;
|
|
||||||
/**
|
|
||||||
* Subject. The Livekit alias in this context.
|
|
||||||
*/
|
|
||||||
sub: string;
|
|
||||||
/**
|
|
||||||
* The set of permissions for the user.
|
|
||||||
*/
|
|
||||||
video: {
|
|
||||||
canPublish: boolean;
|
|
||||||
canSubscribe: boolean;
|
|
||||||
room: string;
|
|
||||||
roomJoin: boolean;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// The bits we need from MatrixClient
|
// The bits we need from MatrixClient
|
||||||
export type OpenIDClientParts = Pick<
|
export type OpenIDClientParts = Pick<
|
||||||
@@ -90,7 +47,7 @@ export type OpenIDClientParts = Pick<
|
|||||||
* @returns Object containing the token information
|
* @returns Object containing the token information
|
||||||
* @throws FailToGetOpenIdToken
|
* @throws FailToGetOpenIdToken
|
||||||
*/
|
*/
|
||||||
export async function getSFUConfigWithOpenID(
|
export async function getSFUConfig(
|
||||||
client: OpenIDClientParts,
|
client: OpenIDClientParts,
|
||||||
membership: CallMembershipIdentityParts,
|
membership: CallMembershipIdentityParts,
|
||||||
serviceUrl: string,
|
serviceUrl: string,
|
||||||
@@ -169,23 +126,6 @@ export async function getSFUConfigWithOpenID(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function extractFullConfigFromToken(sfuConfig: {
|
|
||||||
url: string;
|
|
||||||
jwt: string;
|
|
||||||
}): SFUConfig {
|
|
||||||
const [, payloadStr] = sfuConfig.jwt.split(".");
|
|
||||||
const payload = JSON.parse(global.atob(payloadStr)) as SFUJWTPayload;
|
|
||||||
return {
|
|
||||||
jwt: sfuConfig.jwt,
|
|
||||||
url: sfuConfig.url,
|
|
||||||
livekitAlias: payload.video.room,
|
|
||||||
// NOTE: Currently unused.
|
|
||||||
// Probably also not helpful since we now compute the backendIdentity on joining the call so we can use it for the encryption manager.
|
|
||||||
// The only reason for us to know it locally is to connect the right users with the lk world. (and to set our own keys)
|
|
||||||
livekitIdentity: payload.sub,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
async function getLiveKitJWTLegacy(
|
async function getLiveKitJWTLegacy(
|
||||||
deviceId: string,
|
deviceId: string,
|
||||||
livekitServiceURL: string,
|
livekitServiceURL: string,
|
||||||
@@ -261,7 +201,7 @@ class NotSupportedError extends Error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getLiveKitJWT(
|
async function getLiveKitJWT(
|
||||||
membership: CallMembershipIdentityParts,
|
membership: CallMembershipIdentityParts,
|
||||||
livekitServiceURL: string,
|
livekitServiceURL: string,
|
||||||
matrixRoomId: string,
|
matrixRoomId: string,
|
||||||
@@ -0,0 +1,79 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2026 Element Creations Ltd.
|
||||||
|
|
||||||
|
SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
||||||
|
Please see LICENSE in the repository root for full details.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Configuration and access tokens provided by the SFU on successful authentication.
|
||||||
|
*/
|
||||||
|
export interface SFUConfig {
|
||||||
|
/**
|
||||||
|
* The WebSocket URL of the LiveKit SFU. This is what we connect the LiveKit
|
||||||
|
* room to. Note that this is NOT the JWT service URL
|
||||||
|
* (`livekit_service_url`): the legacy JWT service returns this URL as part of
|
||||||
|
* its response, while the CS-Api based flow requires us to know it upfront.
|
||||||
|
*/
|
||||||
|
url: string;
|
||||||
|
jwt: string;
|
||||||
|
livekitAlias: string;
|
||||||
|
// NOTE: Currently unused.
|
||||||
|
livekitIdentity: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Decoded details from the JWT.
|
||||||
|
*/
|
||||||
|
interface SFUJWTPayload {
|
||||||
|
/**
|
||||||
|
* Expiration time for the JWT.
|
||||||
|
* Note: This value is in seconds since Unix epoch.
|
||||||
|
*/
|
||||||
|
exp: number;
|
||||||
|
/**
|
||||||
|
* Name of the instance which authored the JWT
|
||||||
|
*/
|
||||||
|
iss: string;
|
||||||
|
/**
|
||||||
|
* Time at which the JWT can start to be used.
|
||||||
|
* Note: This value is in seconds since Unix epoch.
|
||||||
|
*/
|
||||||
|
nbf: number;
|
||||||
|
/**
|
||||||
|
* Subject. The Livekit alias in this context.
|
||||||
|
*/
|
||||||
|
sub: string;
|
||||||
|
/**
|
||||||
|
* The set of permissions for the user.
|
||||||
|
*/
|
||||||
|
video: {
|
||||||
|
canPublish: boolean;
|
||||||
|
canSubscribe: boolean;
|
||||||
|
room: string;
|
||||||
|
roomJoin: boolean;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Complements the SFU websocket url and the JWT with the information encoded in
|
||||||
|
* the JWT payload itself.
|
||||||
|
* @param sfuConfig The SFU websocket url and the JWT to connect with.
|
||||||
|
* @returns The full SFU config, including the LiveKit alias and identity.
|
||||||
|
*/
|
||||||
|
export function extractFullConfigFromToken(sfuConfig: {
|
||||||
|
url: string;
|
||||||
|
jwt: string;
|
||||||
|
}): SFUConfig {
|
||||||
|
const [, payloadStr] = sfuConfig.jwt.split(".");
|
||||||
|
const payload = JSON.parse(global.atob(payloadStr)) as SFUJWTPayload;
|
||||||
|
return {
|
||||||
|
jwt: sfuConfig.jwt,
|
||||||
|
url: sfuConfig.url,
|
||||||
|
livekitAlias: payload.video.room,
|
||||||
|
// NOTE: Currently unused.
|
||||||
|
// Probably also not helpful since we now compute the backendIdentity on joining the call so we can use it for the encryption manager.
|
||||||
|
// The only reason for us to know it locally is to connect the right users with the lk world. (and to set our own keys)
|
||||||
|
livekitIdentity: payload.sub,
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -15,7 +15,6 @@ import {
|
|||||||
MediaDeviceFailure,
|
MediaDeviceFailure,
|
||||||
} from "livekit-client";
|
} from "livekit-client";
|
||||||
import { observeParticipantEvents } from "@livekit/components-core";
|
import { observeParticipantEvents } from "@livekit/components-core";
|
||||||
import { type MatrixClient } from "matrix-js-sdk";
|
|
||||||
import {
|
import {
|
||||||
Status as RTCSessionStatus,
|
Status as RTCSessionStatus,
|
||||||
type LivekitTransport,
|
type LivekitTransport,
|
||||||
@@ -80,7 +79,10 @@ import {
|
|||||||
} from "../remoteMembers/Connection.ts";
|
} from "../remoteMembers/Connection.ts";
|
||||||
import { type HomeserverConnected } from "./HomeserverConnected.ts";
|
import { type HomeserverConnected } from "./HomeserverConnected.ts";
|
||||||
import { type LocalTransport } from "./LocalTransport.ts";
|
import { type LocalTransport } from "./LocalTransport.ts";
|
||||||
import { getSFUConfigWithOpenID } from "../../../livekit/openIDSFU.ts";
|
import {
|
||||||
|
type ClientDelegationParts,
|
||||||
|
delegateDelayedLeave,
|
||||||
|
} from "../../../livekit/auth/index.ts";
|
||||||
|
|
||||||
export enum TransportState {
|
export enum TransportState {
|
||||||
/** Not even a transport is available to the LocalMembership */
|
/** Not even a transport is available to the LocalMembership */
|
||||||
@@ -150,7 +152,7 @@ interface Props {
|
|||||||
roomId: string;
|
roomId: string;
|
||||||
ownMembershipIdentity: CallMembershipIdentityParts;
|
ownMembershipIdentity: CallMembershipIdentityParts;
|
||||||
localTransport$: Observable<LocalTransport>;
|
localTransport$: Observable<LocalTransport>;
|
||||||
client: Pick<MatrixClient, "getDeviceId" | "getOpenIdToken">;
|
client: ClientDelegationParts;
|
||||||
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;
|
||||||
@@ -308,15 +310,16 @@ export const createLocalMembership$ = ({
|
|||||||
const joinParams$ = scope.behavior(
|
const joinParams$ = scope.behavior(
|
||||||
localTransport$.pipe(
|
localTransport$.pipe(
|
||||||
switchMap(async ({ transport }) => {
|
switchMap(async ({ transport }) => {
|
||||||
const transportSupportsDelegation = checkDelegationSupport(
|
const transportSupportsDelegation =
|
||||||
transport.livekit_service_url + "/delegate_delayed_leave",
|
"livekit_service_url" in transport &&
|
||||||
`transport ${transport.livekit_service_url}`,
|
(await checkDelegationSupport(
|
||||||
);
|
transport.livekit_service_url + "/delegate_delayed_leave",
|
||||||
|
`transport ${transport.livekit_service_url}`,
|
||||||
|
));
|
||||||
return {
|
return {
|
||||||
transport,
|
transport,
|
||||||
delegationSupported:
|
delegationSupported:
|
||||||
(await homeserverSupportsDelegation) ||
|
transportSupportsDelegation || (await homeserverSupportsDelegation),
|
||||||
(await transportSupportsDelegation),
|
|
||||||
};
|
};
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
@@ -334,7 +337,7 @@ export const createLocalMembership$ = ({
|
|||||||
),
|
),
|
||||||
tap((connection) => {
|
tap((connection) => {
|
||||||
logger.info(
|
logger.info(
|
||||||
`Local connection updated: ${connection?.transport?.livekit_service_url}`,
|
`Local connection updated: ${JSON.stringify(connection?.transport)}`,
|
||||||
);
|
);
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
@@ -406,7 +409,7 @@ export const createLocalMembership$ = ({
|
|||||||
scope.reconcile(localConnection$, async (connection) => {
|
scope.reconcile(localConnection$, async (connection) => {
|
||||||
logger.info(
|
logger.info(
|
||||||
"reconcile based on new localConnection:",
|
"reconcile based on new localConnection:",
|
||||||
connection?.transport.livekit_service_url,
|
JSON.stringify(connection?.transport),
|
||||||
);
|
);
|
||||||
if (connection !== null) {
|
if (connection !== null) {
|
||||||
const publisher = createPublisherFactory(connection);
|
const publisher = createPublisherFactory(connection);
|
||||||
@@ -702,23 +705,22 @@ export const createLocalMembership$ = ({
|
|||||||
async ([joinParams, delayId]) => {
|
async ([joinParams, delayId]) => {
|
||||||
if (joinParams?.delegationSupported && delayId !== null) {
|
if (joinParams?.delegationSupported && delayId !== null) {
|
||||||
try {
|
try {
|
||||||
// This will technically cause the service to issue a new JWT token,
|
logger.info(
|
||||||
// but it's safe to discard. We're only interested in triggering
|
`Delegating delayed leave to ${JSON.stringify(joinParams.transport)}…`,
|
||||||
// delegation.
|
|
||||||
await getSFUConfigWithOpenID(
|
|
||||||
client,
|
|
||||||
ownMembershipIdentity,
|
|
||||||
joinParams.transport.livekit_service_url,
|
|
||||||
roomId,
|
|
||||||
{ matrixRTCMode, delayEndpointBaseUrl: baseUrl, delayId },
|
|
||||||
logger,
|
|
||||||
);
|
);
|
||||||
|
await delegateDelayedLeave({
|
||||||
|
client,
|
||||||
|
membership: ownMembershipIdentity,
|
||||||
|
transport: joinParams.transport,
|
||||||
|
roomId,
|
||||||
|
delayId,
|
||||||
|
matrixRTCMode,
|
||||||
|
logger,
|
||||||
|
});
|
||||||
|
logger.info("Delayed leave successfully delegated");
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
// TODO: Surface this to the user as a service interruption?
|
// TODO: Surface this to the user as a service interruption?
|
||||||
logger.error(
|
logger.error(`Failed to delegate leave`, e);
|
||||||
`Failed to delegate leave to ${joinParams.transport.livekit_service_url}`,
|
|
||||||
e,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -175,6 +175,8 @@ export const initializeWidget = (
|
|||||||
receiveToDevice: sendRecvToDevice,
|
receiveToDevice: sendRecvToDevice,
|
||||||
turnServers: false,
|
turnServers: false,
|
||||||
rtcTransports: true,
|
rtcTransports: true,
|
||||||
|
rtcLivekitGetToken: true,
|
||||||
|
rtcLivekitDelegateDelayedLeave: true,
|
||||||
sendDelayedEvents: true,
|
sendDelayedEvents: true,
|
||||||
updateDelayedEvents: true,
|
updateDelayedEvents: true,
|
||||||
sendSticky: true,
|
sendSticky: true,
|
||||||
|
|||||||
Reference in New Issue
Block a user