feat(livekit): add basic room and service endpoint

pull/105/merge
moonrailgun 2 years ago
parent 79a5b76ba4
commit 78407f04d5

@ -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==}

@ -15,6 +15,7 @@
"mini-star": "*"
},
"dependencies": {
"livekit-server-sdk": "^1.2.5",
"tailchat-server-sdk": "*"
}
}

@ -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,
};
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 323 B

@ -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",

@ -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<ActiveRoomProps> = 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 ? (
<LiveKitRoom
token={token}
serverUrl={liveKitUrl}
options={roomOptions}
video={userChoices.videoEnabled}
audio={userChoices.audioEnabled}
onDisconnected={onLeave}
>
<VideoConference chatMessageFormatter={formatChatMessageLinks} />
</LiveKitRoom>
) : (
<LoadingSpinner />
)}
</>
);
});
ActiveRoom.displayName = 'ActiveRoom';

@ -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,10 +28,23 @@ export const LivekitPanel: React.FC = React.memo(() => {
console.log('error while setting up prejoin', err);
});
const roomName = `${groupId}#${panelId}`;
return (
<GroupPanelContainer groupId={groupId} panelId={panelId}>
<LivekitContainer>
<div style={{ display: 'grid', placeItems: 'center', height: '100%' }}>
{roomName && preJoinChoices ? (
<ActiveRoom
roomName={roomName}
userChoices={preJoinChoices}
onLeave={() => {
setPreJoinChoices(undefined);
}}
/>
) : (
<div
style={{ display: 'grid', placeItems: 'center', height: '100%' }}
>
<PreJoinView
onError={handleError}
defaults={{
@ -43,6 +57,7 @@ export const LivekitPanel: React.FC = React.memo(() => {
}}
/>
</div>
)}
</LivekitContainer>
</GroupPanelContainer>
);

@ -0,0 +1,14 @@
import { postRequest } from '@capital/common';
import { useEffect, useState } from 'react';
export function useServerUrl() {
const [serverUrl, setServerUrl] = useState<string | undefined>();
useEffect(() => {
postRequest('/plugin:com.msgbyte.livekit/url').then(({ data }) => {
setServerUrl(data.url);
});
}, []);
return serverUrl;
}

@ -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<string | undefined>(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;
}
Loading…
Cancel
Save