diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 5449c54b..60d5d5e2 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1941,6 +1941,9 @@ importers: server/plugins/com.msgbyte.livekit: dependencies: + livekit-server-sdk: + specifier: ^1.2.5 + version: 1.2.5 tailchat-server-sdk: specifier: '*' version: link:../../packages/sdk @@ -14845,6 +14848,16 @@ packages: quick-lru: 4.0.1 dev: true + /camelcase-keys@7.0.2: + resolution: {integrity: sha512-Rjs1H+A9R+Ig+4E/9oyB66UC5Mj9Xq3N//vcLf2WzgdTi/3gUu3Z9KoqmlrEG4VuuLK8wJHofxzdQXz/knhiYg==} + engines: {node: '>=12'} + dependencies: + camelcase: 6.3.0 + map-obj: 4.3.0 + quick-lru: 5.1.1 + type-fest: 1.4.0 + dev: false + /camelcase@2.1.1: resolution: {integrity: sha512-DLIsRzJVBQu72meAKPkWQOLcujdXT32hwdfnkI1frSiSRMK1MofjKHf+MEx0SB6fjEFXL8fBDv1dKymBlOp4Qw==} engines: {node: '>=0.10.0'} @@ -22571,6 +22584,16 @@ packages: semver: 5.7.1 dev: false + /jsonwebtoken@9.0.1: + resolution: {integrity: sha512-K8wx7eJ5TPvEjuiVSkv167EVboBDv9PZdDoF7BgeQnBLVvZWW9clr2PsQHVJDTKaEIH5JBIwHujGcHp7GgI2eg==} + engines: {node: '>=12', npm: '>=6'} + dependencies: + jws: 3.2.2 + lodash: 4.17.21 + ms: 2.1.3 + semver: 7.5.4 + dev: false + /jsprim@1.4.2: resolution: {integrity: sha512-P2bSOMAc/ciLz6DzgjVlGJP9+BrJWu5UDGK70C2iweC5QBIeFf0ZXRvGjEj2uYgrY2MkAAhsSWHDWlFtEroZWw==} engines: {node: '>=0.6.0'} @@ -22928,6 +22951,17 @@ packages: webrtc-adapter: 8.2.3 dev: false + /livekit-server-sdk@1.2.5: + resolution: {integrity: sha512-QYHGEoilSAXUQQBAZE2SXU1oXW8z08VFp2UxcZzXdPt3u4E9xamghTMhgniLMWmpSCl7oqVObQ6XXnK9rkr0Pg==} + dependencies: + axios: 1.4.0 + camelcase-keys: 7.0.2 + jsonwebtoken: 9.0.1 + protobufjs: 7.2.4 + transitivePeerDependencies: + - debug + dev: false + /load-json-file@1.1.0: resolution: {integrity: sha512-cy7ZdNRXdablkXYNI049pthVeXFurRyb9+hA/dZzerZ0pGTx42z+y+ssxBaVV2l70t1muq5IdKhn4UtcoGUY9A==} engines: {node: '>=0.10.0'} @@ -23321,7 +23355,6 @@ packages: /map-obj@4.3.0: resolution: {integrity: sha512-hdN1wVrZbb29eBGiGjJbeP8JbKjq1urkHJ/LIP/NY48MZ1QVXUsQBV1G1zvYFHn1XE06cwjBsOI2K3Ulnj1YXQ==} engines: {node: '>=8'} - dev: true /map-or-similar@1.5.0: resolution: {integrity: sha512-0aF7ZmVon1igznGI4VS30yugpduQW3y3GkcgGJOp7d8x8QrizhigUxjI/m2UojsXXto+jLAH3KSz+xOJTiORjg==} diff --git a/server/plugins/com.msgbyte.livekit/package.json b/server/plugins/com.msgbyte.livekit/package.json index 1904cfed..4791d003 100644 --- a/server/plugins/com.msgbyte.livekit/package.json +++ b/server/plugins/com.msgbyte.livekit/package.json @@ -15,6 +15,7 @@ "mini-star": "*" }, "dependencies": { + "livekit-server-sdk": "^1.2.5", "tailchat-server-sdk": "*" } } diff --git a/server/plugins/com.msgbyte.livekit/services/livekit.service.dev.ts b/server/plugins/com.msgbyte.livekit/services/livekit.service.dev.ts index af716534..98ce7e87 100644 --- a/server/plugins/com.msgbyte.livekit/services/livekit.service.dev.ts +++ b/server/plugins/com.msgbyte.livekit/services/livekit.service.dev.ts @@ -1,5 +1,7 @@ +import type { TcContext } from 'tailchat-server-sdk'; import { TcService, TcDbService } from 'tailchat-server-sdk'; import type { LivekitDocument, LivekitModel } from '../models/livekit'; +import { AccessToken } from 'livekit-server-sdk'; /** * livekit @@ -14,8 +16,79 @@ class LivekitService extends TcService { return 'plugin:com.msgbyte.livekit'; } + get livekitUrl() { + return process.env.LIVEKIT_URL; + } + + get apiKey() { + return process.env.LIVEKIT_API_KEY; + } + + get apiSecret() { + return process.env.LIVEKIT_API_SECRET; + } + + /** + * 返回服务是否可用 + */ + get serverAvailable(): boolean { + if (this.apiKey && this.apiSecret) { + return true; + } + + return false; + } + onInit() { + this.registerAvailableAction(() => this.serverAvailable); + + if (!this.serverAvailable) { + console.warn( + 'Livekit service not available, miss env var: LIVEKIT_API_KEY, LIVEKIT_API_SECRET' + ); + return; + } + this.registerLocalDb(require('../models/livekit').default); + + this.registerAction('url', this.url); + this.registerAction('generateToken', this.generateToken); + } + + async url(ctx: TcContext) { + return { + url: this.livekitUrl, + }; + } + + async generateToken( + ctx: TcContext<{ + roomName: string; + }> + ) { + const { roomName } = ctx.params; + + const { userId, user } = ctx.meta; + const nickname = user.nickname; + const identity = userId; + + const at = new AccessToken(this.apiKey, this.apiSecret, { + identity: userId, + name: nickname, + }); + at.addGrant({ + room: roomName, + roomJoin: true, + canPublish: true, + canPublishData: true, + canSubscribe: true, + }); + const accessToken = at.toJwt(); + + return { + identity, + accessToken, + }; } } diff --git a/server/plugins/com.msgbyte.livekit/web/plugins/com.msgbyte.livekit/assets/icon.png b/server/plugins/com.msgbyte.livekit/web/plugins/com.msgbyte.livekit/assets/icon.png new file mode 100644 index 00000000..f68bf391 Binary files /dev/null and b/server/plugins/com.msgbyte.livekit/web/plugins/com.msgbyte.livekit/assets/icon.png differ diff --git a/server/plugins/com.msgbyte.livekit/web/plugins/com.msgbyte.livekit/manifest.json b/server/plugins/com.msgbyte.livekit/web/plugins/com.msgbyte.livekit/manifest.json index a75188fe..5a29a6b5 100644 --- a/server/plugins/com.msgbyte.livekit/web/plugins/com.msgbyte.livekit/manifest.json +++ b/server/plugins/com.msgbyte.livekit/web/plugins/com.msgbyte.livekit/manifest.json @@ -2,6 +2,7 @@ "label": "livekit", "name": "com.msgbyte.livekit", "url": "{BACKEND}/plugins/com.msgbyte.livekit/index.js", + "icon": "{BACKEND}/plugins/com.msgbyte.livekit/assets/icon.png", "version": "0.0.0", "author": "moonrailgun", "description": "Add livekit to provide meeting and live streaming feature", diff --git a/server/plugins/com.msgbyte.livekit/web/plugins/com.msgbyte.livekit/src/components/ActiveRoom.tsx b/server/plugins/com.msgbyte.livekit/web/plugins/com.msgbyte.livekit/src/components/ActiveRoom.tsx new file mode 100644 index 00000000..6953ddad --- /dev/null +++ b/server/plugins/com.msgbyte.livekit/web/plugins/com.msgbyte.livekit/src/components/ActiveRoom.tsx @@ -0,0 +1,65 @@ +import { LoadingSpinner } from '@capital/component'; +import { + formatChatMessageLinks, + LiveKitRoom, + LocalUserChoices, + VideoConference, +} from '@livekit/components-react'; +import { RoomOptions, VideoPresets } from 'livekit-client'; +import React, { useMemo } from 'react'; +import { useServerUrl } from '../utils/useServerUrl'; +import { useToken } from '../utils/useToken'; + +type ActiveRoomProps = { + userChoices: LocalUserChoices; + roomName: string; + region?: string; + onLeave?: () => void; + hq?: boolean; +}; +export const ActiveRoom: React.FC = React.memo((props) => { + const { roomName, userChoices, onLeave, hq } = props; + + const token = useToken(roomName); + const liveKitUrl = useServerUrl(); + + const roomOptions = useMemo((): RoomOptions => { + return { + videoCaptureDefaults: { + deviceId: userChoices.videoDeviceId ?? undefined, + resolution: hq === true ? VideoPresets.h2160 : VideoPresets.h720, + }, + publishDefaults: { + videoSimulcastLayers: + hq === true + ? [VideoPresets.h1080, VideoPresets.h720] + : [VideoPresets.h540, VideoPresets.h216], + }, + audioCaptureDefaults: { + deviceId: userChoices.audioDeviceId ?? undefined, + }, + adaptiveStream: { pixelDensity: 'screen' }, + dynacast: true, + }; + }, [userChoices, hq]); + + return ( + <> + {token && liveKitUrl ? ( + + + + ) : ( + + )} + + ); +}); +ActiveRoom.displayName = 'ActiveRoom'; diff --git a/server/plugins/com.msgbyte.livekit/web/plugins/com.msgbyte.livekit/src/group/LivekitPanel.tsx b/server/plugins/com.msgbyte.livekit/web/plugins/com.msgbyte.livekit/src/group/LivekitPanel.tsx index fcd26b30..eb12bba0 100644 --- a/server/plugins/com.msgbyte.livekit/web/plugins/com.msgbyte.livekit/src/group/LivekitPanel.tsx +++ b/server/plugins/com.msgbyte.livekit/web/plugins/com.msgbyte.livekit/src/group/LivekitPanel.tsx @@ -15,6 +15,7 @@ import { import { LogLevel, RoomOptions, VideoPresets } from 'livekit-client'; import { PreJoinView } from '../components/PreJoinView'; import { LivekitContainer } from '../components/LivekitContainer'; +import { ActiveRoom } from '../components/ActiveRoom'; export const LivekitPanel: React.FC = React.memo(() => { const { groupId, panelId } = useGroupPanelContext(); @@ -27,22 +28,36 @@ export const LivekitPanel: React.FC = React.memo(() => { console.log('error while setting up prejoin', err); }); + const roomName = `${groupId}#${panelId}`; + return ( -
- { - console.log('Joining with: ', values); - setPreJoinChoices(values); + {roomName && preJoinChoices ? ( + { + setPreJoinChoices(undefined); }} /> -
+ ) : ( +
+ { + console.log('Joining with: ', values); + setPreJoinChoices(values); + }} + /> +
+ )}
); diff --git a/server/plugins/com.msgbyte.livekit/web/plugins/com.msgbyte.livekit/src/utils/useServerUrl.ts b/server/plugins/com.msgbyte.livekit/web/plugins/com.msgbyte.livekit/src/utils/useServerUrl.ts new file mode 100644 index 00000000..7932d5cd --- /dev/null +++ b/server/plugins/com.msgbyte.livekit/web/plugins/com.msgbyte.livekit/src/utils/useServerUrl.ts @@ -0,0 +1,14 @@ +import { postRequest } from '@capital/common'; +import { useEffect, useState } from 'react'; + +export function useServerUrl() { + const [serverUrl, setServerUrl] = useState(); + + useEffect(() => { + postRequest('/plugin:com.msgbyte.livekit/url').then(({ data }) => { + setServerUrl(data.url); + }); + }, []); + + return serverUrl; +} diff --git a/server/plugins/com.msgbyte.livekit/web/plugins/com.msgbyte.livekit/src/utils/useToken.ts b/server/plugins/com.msgbyte.livekit/web/plugins/com.msgbyte.livekit/src/utils/useToken.ts new file mode 100644 index 00000000..fa11f701 --- /dev/null +++ b/server/plugins/com.msgbyte.livekit/web/plugins/com.msgbyte.livekit/src/utils/useToken.ts @@ -0,0 +1,22 @@ +import { postRequest } from '@capital/common'; +import type { UseTokenOptions } from '@livekit/components-react'; +import { useEffect, useState } from 'react'; + +export function useToken(roomName: string, options: UseTokenOptions = {}) { + const [token, setToken] = useState(undefined); + + useEffect(() => { + const tokenFetcher = async () => { + const params = new URLSearchParams({ ...options.userInfo, roomName }); + const { data } = await postRequest( + `/plugin:com.msgbyte.livekit/generateToken?${params.toString()}` + ); + const { accessToken } = data; + setToken(accessToken); + }; + + tokenFetcher(); + }, [roomName, options]); + + return token; +}