almost mvp

This commit is contained in:
Timo K
2025-11-30 20:31:21 +01:00
parent 28158bfc23
commit 2d8ffc0ccd
11 changed files with 1144 additions and 100 deletions

View File

@@ -0,0 +1,14 @@
## url parameters
widgetId = $matrix_widget_id
perParticipantE2EE = true
userId = $matrix_user_id
deviceId = $org.matrix.msc3819.matrix_device_id
baseUrl = $org.matrix.msc4039.matrix_base_url
parentUrl = // will be inserted automatically
http://localhost?widgetId=&perParticipantE2EE=true&userId=&deviceId=&baseUrl=&roomId=
->
http://localhost:3000?widgetId=$matrix_widget_id&perParticipantE2EE=true&userId=$matrix_user_id&deviceId=$org.matrix.msc3819.matrix_device_id&baseUrl=$org.matrix.msc4039.matrix_base_url&roomId=$matrix_room_id

BIN
godot/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

51
godot/helper.ts Normal file
View File

@@ -0,0 +1,51 @@
/*
Copyright 2025 New Vector Ltd.
SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
Please see LICENSE in the repository root for full details.
*/
import { logger as rootLogger } from "matrix-js-sdk/lib/logger";
import { scan } from "rxjs";
import { widget as _widget } from "../src/widget";
import { type LivekitRoomItem } from "../src/state/CallViewModel/CallViewModel";
export const logger = rootLogger.getChild("[MatrixRTCSdk]");
if (!_widget) throw Error("No widget. This webapp can only start as a widget");
export const widget = _widget;
export const tryMakeSticky = (): void => {
logger.info("try making sticky MatrixRTCSdk");
void widget.api
.setAlwaysOnScreen(true)
.then(() => {
logger.info("sticky MatrixRTCSdk");
})
.catch((error) => {
logger.error("failed to make sticky MatrixRTCSdk", error);
});
};
export const TEXT_LK_TOPIC = "matrixRTC";
/**
* simple helper operator to combine the last emitted and the current emitted value of a rxjs observable
*
* I think there should be a builtin for this but i did not find it...
*/
export const currentAndPrev = scan<
LivekitRoomItem[],
{
prev: LivekitRoomItem[];
current: LivekitRoomItem[];
}
>(
({ current: lastCurrentVal }, items) => ({
prev: lastCurrentVal,
current: items,
}),
{
prev: [],
current: [],
},
);

45
godot/index.html Normal file
View File

@@ -0,0 +1,45 @@
<!doctype html>
<html>
<head>
<title>Godot MatrixRTC Widget</title>
<meta charset="utf-8" />
<script type="module">
import { createMatrixRTCSdk } from "http://localhost:8123/matrixrtc-ec-godot.js";
try {
console.log("Hello from index.html");
try {
window.matrixRTCSdk = await createMatrixRTCSdk();
console.info("createMatrixRTCSdk was created!");
} catch (e) {
console.error("createMatrixRTCSdk", e);
}
// const sdk = window.matrixRTCSdk;
console.info("matrixRTCSdk join ", window.matrixRTCSdk);
await window.matrixRTCSdk.join();
console.info("matrixRTCSdk joined ");
// sdk.data$.subscribe((data) => {
// console.log(data);
// const div = document.getElementById("data");
// div.appendChild(document.createTextNode(data));
// // TODO forward to godot
// });
// var engine = new Engine($GODOT_CONFIG);
// engine.startGame();
} catch (e) {
console.error("catchALL,", e);
}
</script>
<!--<script src="$GODOT_URL"></script>-->
</head>
<body>
<button onclick="window.matrixRTCSdk.leave();">Leave</button>
<!--<button onclick="window.matrixRTCSdk.sendData({prop: 'Hello, world!'});">
Send Text
</button>-->
<div id="data"></div>
<canvas id="canvas"></canvas>
</body>
</html>

View File

@@ -4,42 +4,57 @@ Copyright 2025 New Vector Ltd.
SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
Please see LICENSE in the repository root for full details.
*/
import { of } from "rxjs";
import { loadClient } from "../src/ClientContext.tsx";
import { createCallViewModel$ } from "../src/state/CallViewModel/CallViewModel.ts";
import { MuteStates } from "../src/state/MuteStates.ts";
import { ObservableScope } from "../src/state/ObservableScope.ts";
import { getUrlParams } from "../src/UrlParams.ts";
// import { type InitResult } from "../src/ClientContext";
import { map, type Observable, of, Subject, switchMap } from "rxjs";
import { MatrixRTCSessionEvent } from "matrix-js-sdk/lib/matrixrtc";
import { type TextStreamInfo } from "livekit-client/dist/src/room/types";
import {
type Room as LivekitRoom,
type TextStreamReader,
} from "livekit-client";
import { type Behavior, constant } from "../src/state/Behavior";
import { createCallViewModel$ } from "../src/state/CallViewModel/CallViewModel";
import { ObservableScope } from "../src/state/ObservableScope";
import { getUrlParams } from "../src/UrlParams";
import { MuteStates } from "../src/state/MuteStates";
import { MediaDevices } from "../src/state/MediaDevices";
import { constant } from "../src/state/Behavior.ts";
import { E2eeType } from "../src/e2ee/e2eeType.ts";
import { E2eeType } from "../src/e2ee/e2eeType";
import { type LocalMemberConnectionState } from "../src/state/CallViewModel/localMember/LocalMembership";
import {
currentAndPrev,
logger,
TEXT_LK_TOPIC,
tryMakeSticky,
widget,
} from "./helper";
import { ElementWidgetActions } from "../src/widget";
console.log("test Godot EC export");
export async function start(): Promise<void> {
const initResults = await loadClient();
if (initResults === null) {
console.error("could not init client");
return;
}
const { client } = initResults;
interface MatrixRTCSdk {
join: () => LocalMemberConnectionState;
/** @throws on leave errors */
leave: () => void;
data$: Observable<{ sender: string; data: string }>;
sendData?: (data: Record<string, unknown>) => Promise<void>;
}
export async function createMatrixRTCSdk(): Promise<MatrixRTCSdk> {
logger.info("Hello");
const client = await widget.client;
logger.info("client created");
const scope = new ObservableScope();
const { roomId } = getUrlParams();
if (roomId === null) {
console.error("could not get roomId from url params");
return;
}
if (roomId === null) throw Error("could not get roomId from url params");
const room = client.getRoom(roomId);
if (room === null) {
console.error("could not get room from client");
return;
}
if (room === null) throw Error("could not get room from client");
const mediaDevices = new MediaDevices(scope);
const muteStates = new MuteStates(scope, mediaDevices, constant(true));
const rtcSession = client.matrixRTC.getRoomSession(room);
const callViewModel = createCallViewModel$(
scope,
client.matrixRTC.getRoomSession(room),
rtcSession,
room,
mediaDevices,
muteStates,
@@ -48,29 +63,148 @@ export async function start(): Promise<void> {
of({}),
constant({ supported: false, processor: undefined }),
);
callViewModel.join();
// callViewModel.audioParticipants$.pipe(
// switchMap((lkRooms) => {
// for (const item of lkRooms) {
// item.livekitRoom.registerTextStreamHandler;
// }
// }),
// );
}
// Example default godot export
logger.info("CallViewModelCreated");
// create data listener
const data$ = new Subject<{ sender: string; data: string }>();
// <!DOCTYPE html>
// <html>
// <head>
// <title>My Template</title>
// <meta charset="UTF-8">
// </head>
// <body>
// <canvas id="canvas"></canvas>
// <script src="$GODOT_URL"></script>
// <script>
// var engine = new Engine($GODOT_CONFIG);
// engine.startGame();
// </script>
// </body>
// </html>
// const lkTextStreamHandlerFunction = async (
// reader: TextStreamReader,
// participantInfo: { identity: string },
// livekitRoom: LivekitRoom,
// ): Promise<void> => {
// const info = reader.info;
// console.log(
// `Received text stream from ${participantInfo.identity}\n` +
// ` Topic: ${info.topic}\n` +
// ` Timestamp: ${info.timestamp}\n` +
// ` ID: ${info.id}\n` +
// ` Size: ${info.size}`, // Optional, only available if the stream was sent with `sendText`
// );
// const participants = callViewModel.livekitRoomItems$.value.find(
// (i) => i.livekitRoom === livekitRoom,
// )?.participants;
// if (participants && participants.includes(participantInfo.identity)) {
// const text = await reader.readAll();
// console.log(`Received text: ${text}`);
// data$.next({ sender: participantInfo.identity, data: text });
// } else {
// logger.warn(
// "Received text from unknown participant",
// participantInfo.identity,
// );
// }
// };
// const livekitRoomItemsSub = callViewModel.livekitRoomItems$
// .pipe(currentAndPrev)
// .subscribe({
// next: ({ prev, current }) => {
// const prevRooms = prev.map((i) => i.livekitRoom);
// const currentRooms = current.map((i) => i.livekitRoom);
// const addedRooms = currentRooms.filter((r) => !prevRooms.includes(r));
// const removedRooms = prevRooms.filter((r) => !currentRooms.includes(r));
// addedRooms.forEach((r) =>
// r.registerTextStreamHandler(
// TEXT_LK_TOPIC,
// (reader, participantInfo) =>
// void lkTextStreamHandlerFunction(reader, participantInfo, r),
// ),
// );
// removedRooms.forEach((r) =>
// r.unregisterTextStreamHandler(TEXT_LK_TOPIC),
// );
// },
// complete: () => {
// logger.info("Livekit room items subscription completed");
// for (const item of callViewModel.livekitRoomItems$.value) {
// logger.info("unregistering room item from room", item.url);
// item.livekitRoom.unregisterTextStreamHandler(TEXT_LK_TOPIC);
// }
// },
// });
// create sendData function
// const sendFn: Behavior<(data: string) => Promise<TextStreamInfo>> =
// scope.behavior(
// callViewModel.localMatrixLivekitMember$.pipe(
// switchMap((m) => {
// if (!m)
// return of((data: string): never => {
// throw Error("local membership not yet ready.");
// });
// return m.participant$.pipe(
// map((p) => {
// if (p === null) {
// return (data: string): never => {
// throw Error("local participant not yet ready to send data.");
// };
// } else {
// return async (data: string): Promise<TextStreamInfo> =>
// p.sendText(data, { topic: TEXT_LK_TOPIC });
// }
// }),
// );
// }),
// ),
// );
// const sendData = async (data: Record<string, unknown>): Promise<void> => {
// const dataString = JSON.stringify(data);
// try {
// const info = await sendFn.value(dataString);
// logger.info(`Sent text with stream ID: ${info.id}`);
// } catch (e) {
// console.error("failed sending: ", dataString, e);
// }
// };
// after hangup gets called
const leaveSubs = callViewModel.leave$.subscribe(() => {
const scheduleWidgetCloseOnLeave = async (): Promise<void> => {
const leaveResolver = Promise.withResolvers<void>();
logger.info("waiting for RTC leave");
rtcSession.on(MatrixRTCSessionEvent.JoinStateChanged, (isJoined) => {
logger.info("received RTC join update: ", isJoined);
if (!isJoined) leaveResolver.resolve();
});
await leaveResolver.promise;
logger.info("send Unstick");
await widget.api
.setAlwaysOnScreen(false)
.catch((e) =>
logger.error(
"Failed to set call widget `alwaysOnScreen` to false",
e,
),
);
logger.info("send Close");
await widget.api.transport
.send(ElementWidgetActions.Close, {})
.catch((e) => logger.error("Failed to send close action", e));
};
// schedule close first and then leave (scope.end)
void scheduleWidgetCloseOnLeave();
// actual hangup (ending scope will send the leave event.. its kinda odd. since you might end up closing the widget too fast)
scope.end();
});
logger.info("createMatrixRTCSdk done");
return {
join: (): LocalMemberConnectionState => {
// first lets try making the widget sticky
tryMakeSticky();
return callViewModel.join();
},
leave: (): void => {
callViewModel.hangup();
leaveSubs.unsubscribe();
// livekitRoomItemsSub.unsubscribe();
},
data$,
// sendData,
};
}