mirror of
https://github.com/vector-im/element-web.git
synced 2026-05-06 04:36:21 +02:00
feat(docs): LiveKit data channel transport for real-time delta sync
Replace per-keystroke Matrix timeline events with a LiveKit data channel for real-time Automerge delta delivery: - Add livekit-client dependency - New useDocumentRTC hook: - Calls client._unstable_getRTCTransports() to get SFU URL - Exchanges Matrix OpenID token with SFU (/sfu/get) for LiveKit JWT - Connects to LiveKit room in data-only mode (autoSubscribe: false) - Exposes publishDelta(bytes) and onDeltaRef callback - Falls back gracefully if no LiveKit transport configured - DocumentView wiring: - useDocumentSync now accepts optional rtc transport - When LiveKit connected: send deltas via data channel (50ms debounce) - When LiveKit not available: fall back to Matrix timeline events (500ms) - Matrix event listener disabled when LiveKit is connected - Snapshots still go via Matrix state events (every 20 deltas + on close) - __docDebug() now shows rtcConnected status Benefits: no homeserver rate limiting on deltas, ~50ms latency vs ~500ms+sync, no delta events cluttering the room timeline. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
parent
8c45fe3736
commit
0e535724cb
@ -82,6 +82,7 @@
|
||||
"linkify-react": "4.3.2",
|
||||
"linkify-string": "4.3.2",
|
||||
"linkifyjs": "4.3.2",
|
||||
"livekit-client": "^2.17.2",
|
||||
"lodash": "npm:lodash-es@^4.17.21",
|
||||
"maplibre-gl": "^5.0.0",
|
||||
"matrix-encrypt-attachment": "^1.0.3",
|
||||
|
||||
@ -15,6 +15,7 @@ import { FormattingButtons } from "./components/FormattingButtons.tsx";
|
||||
import { Editor } from "./components/Editor.tsx";
|
||||
import { ComposerContext, getDefaultContextValue } from "./ComposerContext.ts";
|
||||
import { useSetCursorPosition } from "./hooks/useSetCursorPosition.ts";
|
||||
import { useDocumentRTC } from "./useDocumentRTC.ts";
|
||||
|
||||
/**
|
||||
* Matrix event type for incremental Automerge deltas sent as timeline events.
|
||||
@ -31,6 +32,12 @@ const DOC_STATE_EVENT_TYPE = "org.element.doc.automerge";
|
||||
/** Debounce delay (ms) before sending an incremental delta after a keystroke. */
|
||||
const DELTA_DEBOUNCE_MS = 500;
|
||||
|
||||
/**
|
||||
* Debounce when sending over LiveKit (much lower latency, no rate limits).
|
||||
* Kept small to batch rapid keystrokes without perceptible delay.
|
||||
*/
|
||||
const DELTA_DEBOUNCE_RTC_MS = 50;
|
||||
|
||||
/** Save a full snapshot to room state every N deltas (not every send). */
|
||||
const SNAPSHOT_EVERY_N_DELTAS = 20;
|
||||
|
||||
@ -153,6 +160,12 @@ function useDocumentSync(
|
||||
composerModel: unknown,
|
||||
editorRef: React.RefObject<HTMLDivElement | null>,
|
||||
onContentChanged: () => void,
|
||||
/** If provided, send deltas via LiveKit instead of Matrix events. */
|
||||
rtc?: {
|
||||
publishDelta: (bytes: Uint8Array) => void;
|
||||
onDeltaRef: React.MutableRefObject<((bytes: Uint8Array) => void) | null>;
|
||||
isConnected: boolean;
|
||||
},
|
||||
): {
|
||||
isLoaded: boolean;
|
||||
scheduleDeltaSend: () => void;
|
||||
@ -240,85 +253,109 @@ function useDocumentSync(
|
||||
composerModelRef.current = composerModel;
|
||||
});
|
||||
|
||||
/** Apply raw Automerge delta bytes to the model and update the DOM. */
|
||||
const applyDeltaBytes = useCallback((deltaBytes: Uint8Array): void => {
|
||||
const model = composerModelRef.current;
|
||||
if (!isCollaborative(model)) {
|
||||
logger.warn("[DocumentView] Model not collaborative yet, dropping delta");
|
||||
return;
|
||||
}
|
||||
try {
|
||||
model.receive_changes(deltaBytes);
|
||||
if (editorRef.current) {
|
||||
suppressMutations.current = true;
|
||||
const caretOffset = saveCaretOffset(editorRef.current);
|
||||
editorRef.current.innerHTML = model.get_content_as_html();
|
||||
restoreCaretOffset(editorRef.current, caretOffset);
|
||||
requestAnimationFrame(() => { suppressMutations.current = false; });
|
||||
onContentChanged();
|
||||
}
|
||||
} catch (e) {
|
||||
logger.warn("[DocumentView] Failed to apply remote delta", e);
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [editorRef, onContentChanged]);
|
||||
|
||||
// Wire the LiveKit onDeltaRef callback when RTC is provided.
|
||||
useEffect(() => {
|
||||
logger.info("[DocumentView] Registering delta listeners for room", room.roomId);
|
||||
if (!rtc) return;
|
||||
rtc.onDeltaRef.current = applyDeltaBytes;
|
||||
return () => { rtc.onDeltaRef.current = null; };
|
||||
}, [rtc, applyDeltaBytes]);
|
||||
|
||||
// Matrix event fallback: only used when LiveKit is NOT connected.
|
||||
useEffect(() => {
|
||||
// If LiveKit is connected, deltas come through the data channel — skip Matrix listener.
|
||||
if (rtc?.isConnected) {
|
||||
logger.info("[DocumentView] LiveKit connected — skipping Matrix delta listener");
|
||||
return;
|
||||
}
|
||||
|
||||
logger.info("[DocumentView] Registering Matrix delta listener (no LiveKit)");
|
||||
|
||||
const applyDeltaEvent = (event: import("matrix-js-sdk/src/matrix").MatrixEvent): void => {
|
||||
if (event.getRoomId() !== room.roomId) return;
|
||||
if (event.getType() !== DOC_DELTA_EVENT_TYPE) return;
|
||||
|
||||
// Skip our own events. In encrypted rooms device_id may not be in
|
||||
// unsigned, so also skip by sender alone — the local model already
|
||||
// has our own changes via save_incremental().
|
||||
// Skip our own events — local model already has our changes.
|
||||
if (event.getSender() === client.getUserId()) return;
|
||||
|
||||
const model = composerModelRef.current;
|
||||
if (!isCollaborative(model)) { logger.warn("[DocumentView] Model not collaborative yet, dropping delta"); return; }
|
||||
|
||||
const data = event.getContent<{ data?: string }>().data;
|
||||
if (!data) { logger.warn("[DocumentView] Delta event has no data"); return; }
|
||||
try {
|
||||
model.receive_changes(base64Decode(data));
|
||||
if (editorRef.current) {
|
||||
// Suppress MutationObserver during DOM update. Use
|
||||
// requestAnimationFrame to reset the flag AFTER the observer's
|
||||
// microtask has fired so it doesn't schedule a spurious delta send.
|
||||
suppressMutations.current = true;
|
||||
const caretOffset = saveCaretOffset(editorRef.current);
|
||||
editorRef.current.innerHTML = model.get_content_as_html();
|
||||
restoreCaretOffset(editorRef.current, caretOffset);
|
||||
requestAnimationFrame(() => { suppressMutations.current = false; });
|
||||
onContentChanged();
|
||||
}
|
||||
logger.info("[DocumentView] Applied remote delta successfully");
|
||||
} catch (e) {
|
||||
logger.warn("[DocumentView] Failed to apply remote delta", e);
|
||||
}
|
||||
if (!data) return;
|
||||
applyDeltaBytes(base64Decode(data));
|
||||
};
|
||||
|
||||
// For unencrypted rooms: events arrive ready to use on Room.timeline.
|
||||
// For encrypted rooms: events arrive as m.room.encrypted on Room.timeline
|
||||
// and are only usable after MatrixEventEvent.Decrypted fires on the client.
|
||||
// For unencrypted rooms: events arrive ready on Room.timeline.
|
||||
// For encrypted rooms: decrypt fires MatrixEventEvent.Decrypted.
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
room.on("Room.timeline" as any, applyDeltaEvent);
|
||||
client.on(MatrixEventEvent.Decrypted, applyDeltaEvent);
|
||||
|
||||
return () => {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
room.off("Room.timeline" as any, applyDeltaEvent);
|
||||
client.off(MatrixEventEvent.Decrypted, applyDeltaEvent);
|
||||
};
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [room, client, editorRef, onContentChanged]);
|
||||
}, [room, client, rtc?.isConnected, applyDeltaBytes]);
|
||||
|
||||
// Debounced delta send triggered after each keystroke.
|
||||
// Saves a full snapshot to room state every SNAPSHOT_EVERY_N_DELTAS sends
|
||||
// so the document persists on refresh without rate-limit pressure.
|
||||
// When LiveKit is connected: sends via data channel (50ms debounce, no rate limits).
|
||||
// When falling back: sends as Matrix timeline event (500ms debounce).
|
||||
// Saves a full snapshot to room state every SNAPSHOT_EVERY_N_DELTAS sends.
|
||||
const deltaSendCount = useRef(0);
|
||||
const rtcRef = useRef(rtc);
|
||||
useEffect(() => { rtcRef.current = rtc; });
|
||||
|
||||
const scheduleDeltaSend = useCallback(() => {
|
||||
if (!isCollaborative(composerModel)) return;
|
||||
|
||||
if (debounceTimer.current !== null) clearTimeout(debounceTimer.current);
|
||||
|
||||
const useRTC = rtcRef.current?.isConnected ?? false;
|
||||
const debounceMs = useRTC ? DELTA_DEBOUNCE_RTC_MS : DELTA_DEBOUNCE_MS;
|
||||
|
||||
debounceTimer.current = setTimeout(async () => {
|
||||
debounceTimer.current = null;
|
||||
if (!isCollaborative(composerModel)) return;
|
||||
try {
|
||||
const delta = composerModel.save_incremental();
|
||||
if (delta.length === 0) return; // Nothing new to send.
|
||||
if (delta.length === 0) return;
|
||||
|
||||
const heads = composerModel.get_heads();
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
await client.sendEvent(room.roomId, DOC_DELTA_EVENT_TYPE as any, {
|
||||
data: base64Encode(delta),
|
||||
heads,
|
||||
});
|
||||
|
||||
if (rtcRef.current?.isConnected) {
|
||||
// Fast path: send over LiveKit data channel.
|
||||
rtcRef.current.publishDelta(delta);
|
||||
} else {
|
||||
// Fallback: send as Matrix timeline event.
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
await client.sendEvent(room.roomId, DOC_DELTA_EVENT_TYPE as any, {
|
||||
data: base64Encode(delta),
|
||||
heads,
|
||||
});
|
||||
}
|
||||
|
||||
deltaSendCount.current++;
|
||||
|
||||
// Persist a full snapshot periodically so the document survives
|
||||
// refresh. Includes heads so loaders can skip already-merged deltas.
|
||||
// Persist a full snapshot periodically regardless of transport.
|
||||
if (deltaSendCount.current % SNAPSHOT_EVERY_N_DELTAS === 0) {
|
||||
const snapshot = composerModel.save_document();
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
@ -331,7 +368,7 @@ function useDocumentSync(
|
||||
} catch (e) {
|
||||
logger.warn("[DocumentView] Failed to send delta", e);
|
||||
}
|
||||
}, DELTA_DEBOUNCE_MS);
|
||||
}, debounceMs);
|
||||
}, [client, composerModel, room.roomId]);
|
||||
|
||||
// Flush pending delta and save a final snapshot when the document view
|
||||
@ -395,6 +432,9 @@ export const DocumentView = memo(function DocumentView({ room }: DocumentViewPro
|
||||
const { ref, isWysiwygReady, wysiwyg, actionStates } = wysiwygResult;
|
||||
const composerModel = wysiwygResult.composerModel;
|
||||
|
||||
// LiveKit real-time transport (falls back gracefully if unavailable).
|
||||
const rtc = useDocumentRTC(room, client);
|
||||
|
||||
// Place the cursor at the end and focus the editor once the WASM model is
|
||||
// ready. Without this the editor is enabled but has no selection, so no
|
||||
// cursor appears even after the element receives focus.
|
||||
@ -415,6 +455,7 @@ export const DocumentView = memo(function DocumentView({ room }: DocumentViewPro
|
||||
composerModel,
|
||||
ref,
|
||||
notifyContentChangedRef.current,
|
||||
rtc,
|
||||
);
|
||||
|
||||
const handleInput = useCallback(() => {
|
||||
@ -470,6 +511,7 @@ export const DocumentView = memo(function DocumentView({ room }: DocumentViewPro
|
||||
deviceId: client.getDeviceId(),
|
||||
roomId: room.roomId,
|
||||
modelReady: collab,
|
||||
rtcConnected: rtc.isConnected,
|
||||
heads: collab ? model.get_heads() : null,
|
||||
html: collab ? model.get_content_as_html() : null,
|
||||
docHash: docBytes ? simpleHash(docBytes) : null,
|
||||
@ -505,7 +547,7 @@ export const DocumentView = memo(function DocumentView({ room }: DocumentViewPro
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
delete (window as any).__docDebug;
|
||||
};
|
||||
}, [composerModel, room, client, ref]);
|
||||
}, [composerModel, room, client, ref, rtc]);
|
||||
|
||||
// Always render the Editor so that `ref.current` is attached before
|
||||
// useComposerModel's effect runs and calls initModel(). The loading
|
||||
|
||||
@ -0,0 +1,192 @@
|
||||
/*
|
||||
Copyright 2026 New Vector Ltd.
|
||||
|
||||
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
/**
|
||||
* useDocumentRTC — real-time Automerge delta transport via LiveKit data channel.
|
||||
*
|
||||
* Joins a MatrixRTC session (intent: "document") in the room, exchanges the
|
||||
* user's OpenID token with the LiveKit SFU service for a JWT, connects to the
|
||||
* LiveKit room in data-only mode, and exposes:
|
||||
* - `publishDelta(bytes)` — send an Automerge incremental delta to all peers
|
||||
* - `onDeltaRef` — callback ref invoked on every received delta
|
||||
* - `isConnected` — whether the LiveKit room is currently connected
|
||||
*
|
||||
* Falls back gracefully: if the homeserver has no LiveKit transport configured,
|
||||
* `isConnected` stays false and the caller should fall back to Matrix events.
|
||||
*/
|
||||
|
||||
import { useCallback, useEffect, useRef, useState } from "react";
|
||||
import { type Room as MatrixRoom, type MatrixClient } from "matrix-js-sdk/src/matrix";
|
||||
import { isLivekitTransportConfig } from "matrix-js-sdk/src/matrixrtc";
|
||||
import { logger } from "matrix-js-sdk/src/logger";
|
||||
import { Room as LivekitRoom, RoomEvent, ConnectionState } from "livekit-client";
|
||||
|
||||
const LIVEKIT_DATA_TOPIC = "org.element.doc.delta";
|
||||
|
||||
export interface UseDocumentRTCResult {
|
||||
/** Send an Automerge incremental delta to all connected peers. */
|
||||
publishDelta: (bytes: Uint8Array) => void;
|
||||
/** Set this to receive deltas from peers. Updated value is used without re-registering listeners. */
|
||||
onDeltaRef: React.MutableRefObject<((bytes: Uint8Array) => void) | null>;
|
||||
/** True when connected to the LiveKit room. */
|
||||
isConnected: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Exchange a Matrix OpenID token for a LiveKit JWT from the SFU service.
|
||||
* The SFU service follows MSC4143: POST {sfu_url}/sfu/get with the openid token.
|
||||
*/
|
||||
async function getLivekitJwt(
|
||||
sfuServiceUrl: string,
|
||||
roomId: string,
|
||||
openidToken: {
|
||||
access_token: string;
|
||||
token_type: string;
|
||||
matrix_server_name: string;
|
||||
expires_in: number;
|
||||
},
|
||||
deviceId: string,
|
||||
): Promise<{ url: string; jwt: string }> {
|
||||
const response = await fetch(`${sfuServiceUrl}/sfu/get`, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
room: roomId,
|
||||
openid_token: openidToken,
|
||||
device_id: deviceId,
|
||||
}),
|
||||
});
|
||||
if (!response.ok) {
|
||||
throw new Error(`SFU JWT exchange failed: ${response.status} ${response.statusText}`);
|
||||
}
|
||||
const body = await response.json();
|
||||
if (!body.jwt || !body.url) {
|
||||
throw new Error("SFU response missing jwt or url fields");
|
||||
}
|
||||
return { url: body.url, jwt: body.jwt };
|
||||
}
|
||||
|
||||
export function useDocumentRTC(
|
||||
matrixRoom: MatrixRoom,
|
||||
client: MatrixClient,
|
||||
): UseDocumentRTCResult {
|
||||
const [isConnected, setIsConnected] = useState(false);
|
||||
const livekitRoomRef = useRef<LivekitRoom | null>(null);
|
||||
const onDeltaRef = useRef<((bytes: Uint8Array) => void) | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
let cancelled = false;
|
||||
let livekitRoom: LivekitRoom | null = null;
|
||||
|
||||
const connect = async (): Promise<void> => {
|
||||
// 1. Get available transports from the homeserver.
|
||||
let transports;
|
||||
try {
|
||||
transports = await client._unstable_getRTCTransports();
|
||||
} catch (e) {
|
||||
logger.info("[DocumentRTC] Homeserver does not support /rtc/transports, falling back to Matrix events", e);
|
||||
return;
|
||||
}
|
||||
|
||||
const livekitTransport = transports.find(isLivekitTransportConfig);
|
||||
if (!livekitTransport) {
|
||||
logger.info("[DocumentRTC] No LiveKit transport in /rtc/transports, falling back to Matrix events");
|
||||
return;
|
||||
}
|
||||
|
||||
const sfuServiceUrl = livekitTransport.livekit_service_url;
|
||||
|
||||
// 2. Get an OpenID token and exchange it for a LiveKit JWT.
|
||||
let livekitCreds: { url: string; jwt: string };
|
||||
try {
|
||||
const openidToken = await client.getOpenIdToken();
|
||||
livekitCreds = await getLivekitJwt(
|
||||
sfuServiceUrl,
|
||||
matrixRoom.roomId,
|
||||
openidToken as Parameters<typeof getLivekitJwt>[2],
|
||||
client.getDeviceId() ?? "unknown",
|
||||
);
|
||||
} catch (e) {
|
||||
logger.warn("[DocumentRTC] Failed to get LiveKit credentials", e);
|
||||
return;
|
||||
}
|
||||
|
||||
if (cancelled) return;
|
||||
|
||||
// 3. Connect to LiveKit room (data-only: no camera/mic).
|
||||
livekitRoom = new LivekitRoom();
|
||||
livekitRoomRef.current = livekitRoom;
|
||||
|
||||
livekitRoom.on(RoomEvent.Connected, () => {
|
||||
if (!cancelled) {
|
||||
logger.info("[DocumentRTC] Connected to LiveKit room");
|
||||
setIsConnected(true);
|
||||
}
|
||||
});
|
||||
|
||||
livekitRoom.on(RoomEvent.Disconnected, () => {
|
||||
logger.info("[DocumentRTC] Disconnected from LiveKit room");
|
||||
setIsConnected(false);
|
||||
});
|
||||
|
||||
livekitRoom.on(RoomEvent.Reconnecting, () => {
|
||||
logger.info("[DocumentRTC] Reconnecting to LiveKit room…");
|
||||
setIsConnected(false);
|
||||
});
|
||||
|
||||
livekitRoom.on(RoomEvent.Reconnected, () => {
|
||||
if (!cancelled) {
|
||||
logger.info("[DocumentRTC] Reconnected to LiveKit room");
|
||||
setIsConnected(true);
|
||||
}
|
||||
});
|
||||
|
||||
// 4. Receive Automerge deltas from peers.
|
||||
livekitRoom.on(
|
||||
RoomEvent.DataReceived,
|
||||
(payload: Uint8Array, _participant: unknown, _kind: unknown, topic?: string) => {
|
||||
if (topic !== LIVEKIT_DATA_TOPIC) return;
|
||||
onDeltaRef.current?.(payload);
|
||||
},
|
||||
);
|
||||
|
||||
try {
|
||||
await livekitRoom.connect(livekitCreds.url, livekitCreds.jwt, {
|
||||
// Data-only: don't auto-subscribe to audio/video tracks.
|
||||
autoSubscribe: false,
|
||||
});
|
||||
logger.info("[DocumentRTC] LiveKit connect() resolved");
|
||||
} catch (e) {
|
||||
logger.warn("[DocumentRTC] Failed to connect to LiveKit room", e);
|
||||
setIsConnected(false);
|
||||
}
|
||||
};
|
||||
|
||||
connect().catch((e) => logger.error("[DocumentRTC] Unexpected error in connect()", e));
|
||||
|
||||
return () => {
|
||||
cancelled = true;
|
||||
if (livekitRoomRef.current) {
|
||||
logger.info("[DocumentRTC] Disconnecting from LiveKit room (unmount)");
|
||||
livekitRoomRef.current.disconnect().catch(() => {});
|
||||
livekitRoomRef.current = null;
|
||||
}
|
||||
setIsConnected(false);
|
||||
};
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [matrixRoom.roomId]); // reconnect only if room changes
|
||||
|
||||
const publishDelta = useCallback((bytes: Uint8Array): void => {
|
||||
const room = livekitRoomRef.current;
|
||||
if (!room || room.state !== ConnectionState.Connected) return;
|
||||
room.localParticipant
|
||||
.publishData(bytes, { reliable: true, topic: LIVEKIT_DATA_TOPIC })
|
||||
.catch((e: unknown) => logger.warn("[DocumentRTC] Failed to publish delta", e));
|
||||
}, []);
|
||||
|
||||
return { publishDelta, onDeltaRef, isConnected };
|
||||
}
|
||||
80
pnpm-lock.yaml
generated
80
pnpm-lock.yaml
generated
@ -272,6 +272,9 @@ importers:
|
||||
linkifyjs:
|
||||
specifier: 4.3.2
|
||||
version: 4.3.2
|
||||
livekit-client:
|
||||
specifier: ^2.17.2
|
||||
version: 2.17.2(@types/dom-mediacapture-record@1.0.22)
|
||||
lodash:
|
||||
specifier: npm:lodash-es@^4.17.21
|
||||
version: lodash-es@4.17.23
|
||||
@ -1675,6 +1678,9 @@ packages:
|
||||
resolution: {integrity: sha512-6zABk/ECA/QYSCQ1NGiVwwbQerUCZ+TQbp64Q3AgmfNvurHH0j8TtXa1qbShXA6qqkpAj4V5W8pP6mLe1mcMqA==}
|
||||
engines: {node: '>=18'}
|
||||
|
||||
'@bufbuild/protobuf@1.10.1':
|
||||
resolution: {integrity: sha512-wJ8ReQbHxsAfXhrf9ixl0aYbZorRuOWpBNzm8pL8ftmSxQx/wnJD5Eg861NwJU/czy2VXFIebCeZnZrI9rktIQ==}
|
||||
|
||||
'@cacheable/memory@2.0.8':
|
||||
resolution: {integrity: sha512-FvEb29x5wVwu/Kf93IWwsOOEuhHh6dYCJF3vcKLzXc0KXIW181AOzv6ceT4ZpBHDvAfG60eqb+ekmrnLHIy+jw==}
|
||||
|
||||
@ -2643,6 +2649,12 @@ packages:
|
||||
peerDependencies:
|
||||
'@types/react': ^19.2.10
|
||||
|
||||
'@livekit/mutex@1.1.1':
|
||||
resolution: {integrity: sha512-EsshAucklmpuUAfkABPxJNhzj9v2sG7JuzFDL4ML1oJQSV14sqrpTYnsaOudMAw9yOaW53NU3QQTlUQoRs4czw==}
|
||||
|
||||
'@livekit/protocol@1.44.0':
|
||||
resolution: {integrity: sha512-/vfhDUGcUKO8Q43r6i+5FrDhl5oZjm/X3U4x2Iciqvgn5C8qbj+57YPcWSJ1kyIZm5Cm6AV2nAPjMm3ETD/iyg==}
|
||||
|
||||
'@mapbox/geojson-rewind@0.5.2':
|
||||
resolution: {integrity: sha512-tJaT+RbYGJYStt7wI3cq4Nl4SXxG8W7JDG5DMJu97V25RnbNg3QtQtf+KD+VLjNpWKYsRvXDNmNrBgEETr1ifA==}
|
||||
hasBin: true
|
||||
@ -4165,6 +4177,9 @@ packages:
|
||||
'@types/doctrine@0.0.9':
|
||||
resolution: {integrity: sha512-eOIHzCUSH7SMfonMG1LsC2f8vxBFtho6NGBznK41R84YzPuvSBzrhEps33IsQiOW9+VL6NQ9DbjQJznk/S4uRA==}
|
||||
|
||||
'@types/dom-mediacapture-record@1.0.22':
|
||||
resolution: {integrity: sha512-mUMZLK3NvwRLcAAT9qmcK+9p7tpU2FHdDsntR3YI4+GY88XrgG4XiE7u1Q2LAN2/FZOz/tdMDC3GQCR4T8nFuw==}
|
||||
|
||||
'@types/escape-html@1.0.4':
|
||||
resolution: {integrity: sha512-qZ72SFTgUAZ5a7Tj6kf2SHLetiH5S6f8G5frB2SPQ3EyF02kxdyBFf4Tz4banE3xCgGnKgWLt//a6VuYHKYJTg==}
|
||||
|
||||
@ -7525,6 +7540,9 @@ packages:
|
||||
resolution: {integrity: sha512-RuCOQMIt78LWnktPoeBL0GErkNaJPTBGcYuyaBvUOQSpcpcLfWrHPPihYdOGbV5pam9VTWbeoF7TsGiHugcjGA==}
|
||||
engines: {node: '>= 20'}
|
||||
|
||||
jose@6.1.3:
|
||||
resolution: {integrity: sha512-0TpaTfihd4QMNwrz/ob2Bp7X04yuxJkjRGi4aKmOqwhov54i6u79oCv7T+C7lo70MKH6BesI3vscD1yb/yzKXQ==}
|
||||
|
||||
js-tokens@10.0.0:
|
||||
resolution: {integrity: sha512-lM/UBzQmfJRo9ABXbPWemivdCW8V2G8FHaHdypQaIy523snUjog0W71ayWXTjiR+ixeMyVHN2XcpnTd/liPg/Q==}
|
||||
|
||||
@ -7709,6 +7727,11 @@ packages:
|
||||
resolution: {integrity: sha512-ME4Fb83LgEgwNw96RKNvKV4VTLuXfoKudAmm2lP8Kk87KaMK0/Xrx/aAkMWmT8mDb+3MlFDspfbCs7adjRxA2g==}
|
||||
engines: {node: '>=20.0.0'}
|
||||
|
||||
livekit-client@2.17.2:
|
||||
resolution: {integrity: sha512-+67y2EtAWZabARlY7kANl/VT1Uu1EJYR5a8qwpT2ub/uBCltsEgEDOxCIMwE9HFR5w+z41HR6GL9hyEvW/y6CQ==}
|
||||
peerDependencies:
|
||||
'@types/dom-mediacapture-record': ^1
|
||||
|
||||
loader-runner@4.3.1:
|
||||
resolution: {integrity: sha512-IWqP2SCPhyVFTBtRcgMHdzlf9ul25NwaFx4wCEH/KjAXuuHY4yNjvPXsBokp8jCB936PyWRaPKUNh8NvylLp2Q==}
|
||||
engines: {node: '>=6.11.5'}
|
||||
@ -9406,10 +9429,17 @@ packages:
|
||||
resolution: {integrity: sha512-eflK8wEtyOE6+hsaRVPxvUKYCpRgzLqDTb8krvAsRIwOGlHoSgYLgBXoubGgLd2fT41/OUYdb48v4k4WWHQurA==}
|
||||
engines: {node: '>= 10.13.0'}
|
||||
|
||||
sdp-transform@2.15.0:
|
||||
resolution: {integrity: sha512-KrOH82c/W+GYQ0LHqtr3caRpM3ITglq3ljGUIb8LTki7ByacJZ9z+piSGiwZDsRyhQbYBOBJgr2k6X4BZXi3Kw==}
|
||||
hasBin: true
|
||||
|
||||
sdp-transform@3.0.0:
|
||||
resolution: {integrity: sha512-gfYVRGxjHkGF2NPeUWHw5u6T/KGFtS5/drPms73gaSuMaVHKCY3lpLnGDfswVQO0kddeePoti09AwhYP4zA8dQ==}
|
||||
hasBin: true
|
||||
|
||||
sdp@3.2.1:
|
||||
resolution: {integrity: sha512-lwsAIzOPlH8/7IIjjz3K0zYBk7aBVVcvjMwt3M4fLxpjMYyy7i3I97SLHebgn4YBjirkzfp3RvRDWSKsh/+WFw==}
|
||||
|
||||
seedrandom@3.0.5:
|
||||
resolution: {integrity: sha512-8OwmbklUNzwezjGInmZ+2clQmExQPvomqjL7LFqOYqtmuxRgQYqOD3mHaU+MvZn5FLUeVxVfQjwLZW/n/JFuqg==}
|
||||
|
||||
@ -10031,6 +10061,9 @@ packages:
|
||||
peerDependencies:
|
||||
typescript: '>=4.8.4'
|
||||
|
||||
ts-debounce@4.0.0:
|
||||
resolution: {integrity: sha512-+1iDGY6NmOGidq7i7xZGA4cm8DAa6fqdYcvO5Z6yBevH++Bdo9Qt/mN0TzHUgcCcKv1gmh9+W5dHqz8pMWbCbg==}
|
||||
|
||||
ts-dedent@2.2.0:
|
||||
resolution: {integrity: sha512-q5W7tVM71e2xjHZTlgfTDoPF/SmqKG5hddq9SzR49CH2hayqRKJtQ4mtRlSxKaJlR/+9rEM+mnBHf7I2/BQcpQ==}
|
||||
engines: {node: '>=6.10'}
|
||||
@ -10118,6 +10151,9 @@ packages:
|
||||
resolution: {integrity: sha512-3KS2b+kL7fsuk/eJZ7EQdnEmQoaho/r6KUef7hxvltNA5DR8NAUM+8wJMbJyZ4G9/7i3v5zPBIMN5aybAh2/Jg==}
|
||||
engines: {node: '>= 0.4'}
|
||||
|
||||
typed-emitter@2.1.0:
|
||||
resolution: {integrity: sha512-g/KzbYKbH5C2vPkaXGu8DJlHrGKHLsM25Zg9WuC9pMGfuvT+X25tZQWo5fK1BjBm8+UrVE9LDCvaY0CQk+fXDA==}
|
||||
|
||||
typedoc-plugin-markdown@4.10.0:
|
||||
resolution: {integrity: sha512-psrg8Rtnv4HPWCsoxId+MzEN8TVK5jeKCnTbnGAbTBqcDapR9hM41bJT/9eAyKn9C2MDG9Qjh3MkltAYuLDoXg==}
|
||||
engines: {node: '>= 18'}
|
||||
@ -10567,6 +10603,10 @@ packages:
|
||||
webpack-cli:
|
||||
optional: true
|
||||
|
||||
webrtc-adapter@9.0.4:
|
||||
resolution: {integrity: sha512-5ZZY1+lGq8LEKuDlg9M2RPJHlH3R7OVwyHqMcUsLKCgd9Wvf+QrFTCItkXXYPmrJn8H6gRLXbSgxLLdexiqHxw==}
|
||||
engines: {node: '>=6.0.0', npm: '>=3.10.0'}
|
||||
|
||||
websocket-driver@0.7.4:
|
||||
resolution: {integrity: sha512-b17KeDIQVjvb0ssuSDF2cYXSg2iztliJ4B9WdsuB6J952qCPKmnVq4DyW5motImXHDC1cBT/1UezrJVsKw5zjg==}
|
||||
engines: {node: '>=0.8.0'}
|
||||
@ -11666,6 +11706,8 @@ snapshots:
|
||||
|
||||
'@bcoe/v8-coverage@1.0.2': {}
|
||||
|
||||
'@bufbuild/protobuf@1.10.1': {}
|
||||
|
||||
'@cacheable/memory@2.0.8':
|
||||
dependencies:
|
||||
'@cacheable/utils': 2.4.0
|
||||
@ -12705,6 +12747,12 @@ snapshots:
|
||||
dependencies:
|
||||
'@types/react': 19.2.10
|
||||
|
||||
'@livekit/mutex@1.1.1': {}
|
||||
|
||||
'@livekit/protocol@1.44.0':
|
||||
dependencies:
|
||||
'@bufbuild/protobuf': 1.10.1
|
||||
|
||||
'@mapbox/geojson-rewind@0.5.2':
|
||||
dependencies:
|
||||
get-stream: 6.0.1
|
||||
@ -14324,6 +14372,8 @@ snapshots:
|
||||
|
||||
'@types/doctrine@0.0.9': {}
|
||||
|
||||
'@types/dom-mediacapture-record@1.0.22': {}
|
||||
|
||||
'@types/escape-html@1.0.4': {}
|
||||
|
||||
'@types/eslint-scope@3.7.7':
|
||||
@ -18417,6 +18467,8 @@ snapshots:
|
||||
'@hapi/topo': 6.0.2
|
||||
'@standard-schema/spec': 1.1.0
|
||||
|
||||
jose@6.1.3: {}
|
||||
|
||||
js-tokens@10.0.0: {}
|
||||
|
||||
js-tokens@4.0.0: {}
|
||||
@ -18616,6 +18668,20 @@ snapshots:
|
||||
rfdc: 1.4.1
|
||||
wrap-ansi: 10.0.0
|
||||
|
||||
livekit-client@2.17.2(@types/dom-mediacapture-record@1.0.22):
|
||||
dependencies:
|
||||
'@livekit/mutex': 1.1.1
|
||||
'@livekit/protocol': 1.44.0
|
||||
'@types/dom-mediacapture-record': 1.0.22
|
||||
events: 3.3.0
|
||||
jose: 6.1.3
|
||||
loglevel: 1.9.2
|
||||
sdp-transform: 2.15.0
|
||||
ts-debounce: 4.0.0
|
||||
tslib: 2.8.1
|
||||
typed-emitter: 2.1.0
|
||||
webrtc-adapter: 9.0.4
|
||||
|
||||
loader-runner@4.3.1: {}
|
||||
|
||||
loader-utils@2.0.4:
|
||||
@ -20544,8 +20610,12 @@ snapshots:
|
||||
ajv-formats: 2.1.1(ajv@8.18.0)
|
||||
ajv-keywords: 5.1.0(ajv@8.18.0)
|
||||
|
||||
sdp-transform@2.15.0: {}
|
||||
|
||||
sdp-transform@3.0.0: {}
|
||||
|
||||
sdp@3.2.1: {}
|
||||
|
||||
seedrandom@3.0.5: {}
|
||||
|
||||
select-hose@2.0.0: {}
|
||||
@ -21378,6 +21448,8 @@ snapshots:
|
||||
dependencies:
|
||||
typescript: 5.9.3
|
||||
|
||||
ts-debounce@4.0.0: {}
|
||||
|
||||
ts-dedent@2.2.0: {}
|
||||
|
||||
tsconfig-paths@3.15.0:
|
||||
@ -21478,6 +21550,10 @@ snapshots:
|
||||
possible-typed-array-names: 1.1.0
|
||||
reflect.getprototypeof: 1.0.10
|
||||
|
||||
typed-emitter@2.1.0:
|
||||
optionalDependencies:
|
||||
rxjs: 7.8.2
|
||||
|
||||
typedoc-plugin-markdown@4.10.0(typedoc@0.28.17(typescript@5.9.3)):
|
||||
dependencies:
|
||||
typedoc: 0.28.17(typescript@5.9.3)
|
||||
@ -22021,6 +22097,10 @@ snapshots:
|
||||
- esbuild
|
||||
- uglify-js
|
||||
|
||||
webrtc-adapter@9.0.4:
|
||||
dependencies:
|
||||
sdp: 3.2.1
|
||||
|
||||
websocket-driver@0.7.4:
|
||||
dependencies:
|
||||
http-parser-js: 0.5.10
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user