feat: enhance collaboration session and LiveKit integration
- Added LiveKitRoomService to manage meeting room creation and recording functionalities. - Updated CollaborationSessionSchema to create a LiveKit room upon new collaboration session creation. - Introduced meetingRoomJoinInfo field in MeetingRoomSchema to provide join tokens and server URLs for meeting rooms. - Improved LiveKitService to include user metadata in token generation and added a method to retrieve the server URL. - Enhanced error handling and authorization checks across schemas to ensure proper access control for collaboration sessions and meeting rooms.
This commit is contained in:
@@ -2,10 +2,11 @@ import { Module } from '@nestjs/common'
|
|||||||
import { CollaborationSessionSchema } from './collaborationsession.schema'
|
import { CollaborationSessionSchema } from './collaborationsession.schema'
|
||||||
import { LiveKitModule } from 'src/LiveKit/livekit.module'
|
import { LiveKitModule } from 'src/LiveKit/livekit.module'
|
||||||
import { LiveKitService } from 'src/LiveKit/livekit.service'
|
import { LiveKitService } from 'src/LiveKit/livekit.service'
|
||||||
|
import { LiveKitRoomService } from 'src/LiveKit/livekit.room.service'
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [LiveKitModule],
|
imports: [LiveKitModule],
|
||||||
providers: [CollaborationSessionSchema, LiveKitService],
|
providers: [CollaborationSessionSchema, LiveKitService, LiveKitRoomService],
|
||||||
exports: [CollaborationSessionSchema],
|
exports: [CollaborationSessionSchema],
|
||||||
})
|
})
|
||||||
export class CollaborationSessionModule {}
|
export class CollaborationSessionModule {}
|
||||||
|
|||||||
@@ -6,12 +6,14 @@ import { Builder, SchemaContext } from 'src/Graphql/graphql.builder'
|
|||||||
import { PrismaService } from 'src/Prisma/prisma.service'
|
import { PrismaService } from 'src/Prisma/prisma.service'
|
||||||
import { DateTimeUtils } from 'src/common/utils/datetime.utils'
|
import { DateTimeUtils } from 'src/common/utils/datetime.utils'
|
||||||
import { LiveKitService } from 'src/LiveKit/livekit.service'
|
import { LiveKitService } from 'src/LiveKit/livekit.service'
|
||||||
|
import { LiveKitRoomService } from 'src/LiveKit/livekit.room.service'
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class CollaborationSessionSchema extends PothosSchema {
|
export class CollaborationSessionSchema extends PothosSchema {
|
||||||
constructor(
|
constructor(
|
||||||
@Inject(SchemaBuilderToken) private readonly builder: Builder,
|
@Inject(SchemaBuilderToken) private readonly builder: Builder,
|
||||||
private readonly prisma: PrismaService,
|
private readonly prisma: PrismaService,
|
||||||
private readonly liveKitService: LiveKitService,
|
private readonly liveKitService: LiveKitService,
|
||||||
|
private readonly liveKitRoomService: LiveKitRoomService,
|
||||||
) {
|
) {
|
||||||
super()
|
super()
|
||||||
}
|
}
|
||||||
@@ -101,11 +103,7 @@ export class CollaborationSessionSchema extends PothosSchema {
|
|||||||
// }
|
// }
|
||||||
|
|
||||||
// check if user is participant
|
// check if user is participant
|
||||||
if (
|
if (!collaborationSession.collaboratorsIds.includes(ctx.http.me.id)) throw new Error('User not allowed')
|
||||||
!collaborationSession.collaboratorsIds.includes(ctx.http.me.id) ||
|
|
||||||
ctx.http.me.id === 'user_2nkDilSYEiljIraFGF9PENjILPr'
|
|
||||||
)
|
|
||||||
throw new Error('User not allowed')
|
|
||||||
return collaborationSession
|
return collaborationSession
|
||||||
}
|
}
|
||||||
/* ---------- use case 2 : center mentor get collaboration session by schedule date id --------- */
|
/* ---------- use case 2 : center mentor get collaboration session by schedule date id --------- */
|
||||||
@@ -164,6 +162,14 @@ export class CollaborationSessionSchema extends PothosSchema {
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
// create meeting room
|
||||||
|
const meetingRoom = await this.prisma.meetingRoom.create({
|
||||||
|
data: {
|
||||||
|
collaborationSessionId: newCollaborationSession.id,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
// create livekit room
|
||||||
|
await this.liveKitRoomService.createServiceMeetingRoom(meetingRoom.id)
|
||||||
return newCollaborationSession // if not exist use case
|
return newCollaborationSession // if not exist use case
|
||||||
}
|
}
|
||||||
return collaborationSession // if exist use case
|
return collaborationSession // if exist use case
|
||||||
|
|||||||
@@ -352,6 +352,28 @@ export class DocumentSchema extends PothosSchema {
|
|||||||
})
|
})
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
|
editCollaboratorPermission: t.prismaField({
|
||||||
|
type: this.documentCollaborator(),
|
||||||
|
args: {
|
||||||
|
documentId: t.arg({ type: 'String', required: true }),
|
||||||
|
userId: t.arg({ type: 'String', required: true }),
|
||||||
|
readable: t.arg({ type: 'Boolean', required: true }),
|
||||||
|
writable: t.arg({ type: 'Boolean', required: true }),
|
||||||
|
},
|
||||||
|
resolve: async (_, __, args, ctx: SchemaContext) => {
|
||||||
|
if (ctx.isSubscription) throw new Error('Not allowed')
|
||||||
|
// check if ctx user is owner of document
|
||||||
|
const document = await this.prisma.document.findUnique({
|
||||||
|
where: { id: args.documentId },
|
||||||
|
})
|
||||||
|
if (!document) throw new Error('Document not found')
|
||||||
|
if (document.ownerId !== ctx.http?.me?.id) throw new Error('User is not owner of document')
|
||||||
|
return await this.prisma.documentCollaborator.update({
|
||||||
|
where: { documentId_userId: { documentId: args.documentId, userId: args.userId } },
|
||||||
|
data: { readable: args.readable, writable: args.writable },
|
||||||
|
})
|
||||||
|
},
|
||||||
|
}),
|
||||||
}))
|
}))
|
||||||
|
|
||||||
this.builder.subscriptionFields((t) => ({
|
this.builder.subscriptionFields((t) => ({
|
||||||
|
|||||||
40
src/LiveKit/livekit.egress.ts
Normal file
40
src/LiveKit/livekit.egress.ts
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
// @ts-nocheck
|
||||||
|
import { Injectable } from '@nestjs/common'
|
||||||
|
import {
|
||||||
|
EgressClient,
|
||||||
|
EncodedFileOutput,
|
||||||
|
StreamOutput,
|
||||||
|
EncodedFileType,
|
||||||
|
EncodingOptionsPreset,
|
||||||
|
} from 'livekit-server-sdk'
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class LiveKitEgressService {
|
||||||
|
private readonly egressClient = new EgressClient(
|
||||||
|
process.env.LIVEKIT_URL as string,
|
||||||
|
process.env.LIVEKIT_API_KEY as string,
|
||||||
|
process.env.LIVEKIT_API_SECRET as string,
|
||||||
|
)
|
||||||
|
private readonly output = new EncodedFileOutput({
|
||||||
|
fileType: EncodedFileType.MP4,
|
||||||
|
output: {
|
||||||
|
case: 's3',
|
||||||
|
value: {
|
||||||
|
bucket: process.env.RECORDING_BUCKET_NAME as string,
|
||||||
|
accessKey: process.env.MINIO_ACCESS_KEY as string,
|
||||||
|
secret: process.env.MINIO_SECRET_KEY as string,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
constructor() {}
|
||||||
|
|
||||||
|
async startRecording(roomId: string) {
|
||||||
|
const egress = await this.egressClient.startRoomCompositeEgress(roomId, this.output, {
|
||||||
|
layout: 'grid',
|
||||||
|
encodingOptions: EncodingOptionsPreset.H264_1080P_60,
|
||||||
|
audioOnly: false,
|
||||||
|
videoOnly: false,
|
||||||
|
})
|
||||||
|
return egress
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,8 +1,10 @@
|
|||||||
import { Module, Global } from '@nestjs/common'
|
import { Module, Global } from '@nestjs/common'
|
||||||
import { LiveKitService } from './livekit.service'
|
import { LiveKitService } from './livekit.service'
|
||||||
|
import { LiveKitRoomService } from './livekit.room.service'
|
||||||
|
import { LiveKitEgressService } from './livekit.egress'
|
||||||
@Global()
|
@Global()
|
||||||
@Module({
|
@Module({
|
||||||
providers: [LiveKitService],
|
providers: [LiveKitService, LiveKitRoomService, LiveKitEgressService],
|
||||||
exports: [LiveKitService],
|
exports: [LiveKitService, LiveKitRoomService, LiveKitEgressService],
|
||||||
})
|
})
|
||||||
export class LiveKitModule {}
|
export class LiveKitModule {}
|
||||||
|
|||||||
@@ -1,16 +1,24 @@
|
|||||||
import { forwardRef, Injectable } from '@nestjs/common'
|
import { Injectable } from '@nestjs/common'
|
||||||
// @ts-expect-error
|
// @ts-expect-error
|
||||||
import { Room, RoomServiceClient } from 'livekit-server-sdk'
|
import { Room, RoomServiceClient } from 'livekit-server-sdk'
|
||||||
|
import { LiveKitEgressService } from './livekit.egress'
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class LiveKitRoomService {
|
export class LiveKitRoomService {
|
||||||
constructor(private readonly roomServiceClient: RoomServiceClient) {}
|
private readonly roomServiceClient = new RoomServiceClient(
|
||||||
|
process.env.LIVEKIT_URL as string,
|
||||||
|
process.env.LIVEKIT_API_KEY as string,
|
||||||
|
process.env.LIVEKIT_API_SECRET as string,
|
||||||
|
)
|
||||||
|
constructor(private readonly egressService: LiveKitEgressService) {}
|
||||||
|
|
||||||
async createServiceMeetingRoom(roomId: string) {
|
async createServiceMeetingRoom(roomId: string) {
|
||||||
const room = await this.roomServiceClient.createRoom({
|
const room = await this.roomServiceClient.createRoom({
|
||||||
name: roomId,
|
name: roomId,
|
||||||
maxParticipants: 2,
|
maxParticipants: 2,
|
||||||
})
|
})
|
||||||
|
// start recording
|
||||||
|
await this.egressService.startRecording(roomId)
|
||||||
return room
|
return room
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -22,5 +30,3 @@ export class LiveKitRoomService {
|
|||||||
return room
|
return room
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const roomServiceClient = forwardRef(() => LiveKitRoomService)
|
|
||||||
|
|||||||
@@ -15,7 +15,9 @@ export class LiveKitService {
|
|||||||
throw new Error('User must have a name')
|
throw new Error('User must have a name')
|
||||||
}
|
}
|
||||||
const token = new AccessToken(process.env.LIVEKIT_API_KEY as string, process.env.LIVEKIT_API_SECRET as string, {
|
const token = new AccessToken(process.env.LIVEKIT_API_KEY as string, process.env.LIVEKIT_API_SECRET as string, {
|
||||||
identity: me.name,
|
identity: me.id,
|
||||||
|
name: me.name,
|
||||||
|
metadata: me.avatarUrl ?? '',
|
||||||
})
|
})
|
||||||
token.addGrant({
|
token.addGrant({
|
||||||
roomJoin: true,
|
roomJoin: true,
|
||||||
@@ -23,4 +25,8 @@ export class LiveKitService {
|
|||||||
})
|
})
|
||||||
return await token.toJwt()
|
return await token.toJwt()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getServerUrl() {
|
||||||
|
return process.env.LIVEKIT_URL
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,11 +2,13 @@ import { Inject, Injectable } from '@nestjs/common'
|
|||||||
import { Pothos, PothosRef, PothosSchema, SchemaBuilderToken } from '@smatch-corp/nestjs-pothos'
|
import { Pothos, PothosRef, PothosSchema, SchemaBuilderToken } from '@smatch-corp/nestjs-pothos'
|
||||||
import { Builder, SchemaContext } from 'src/Graphql/graphql.builder'
|
import { Builder, SchemaContext } from 'src/Graphql/graphql.builder'
|
||||||
import { PrismaService } from 'src/Prisma/prisma.service'
|
import { PrismaService } from 'src/Prisma/prisma.service'
|
||||||
|
import { LiveKitService } from 'src/LiveKit/livekit.service'
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class MeetingRoomSchema extends PothosSchema {
|
export class MeetingRoomSchema extends PothosSchema {
|
||||||
constructor(
|
constructor(
|
||||||
@Inject(SchemaBuilderToken) private readonly builder: Builder,
|
@Inject(SchemaBuilderToken) private readonly builder: Builder,
|
||||||
private readonly prisma: PrismaService,
|
private readonly prisma: PrismaService,
|
||||||
|
private readonly livekitService: LiveKitService,
|
||||||
) {
|
) {
|
||||||
super()
|
super()
|
||||||
}
|
}
|
||||||
@@ -24,6 +26,23 @@ export class MeetingRoomSchema extends PothosSchema {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@PothosRef()
|
||||||
|
meetingRoomJoinInfo() {
|
||||||
|
return this.builder.simpleObject('MeetingRoomJoinInfo', {
|
||||||
|
fields: (t) => ({
|
||||||
|
id: t.string({
|
||||||
|
description: 'The ID of the meeting room.',
|
||||||
|
}),
|
||||||
|
token: t.string({
|
||||||
|
description: 'The token to join the meeting room.',
|
||||||
|
}),
|
||||||
|
serverUrl: t.string({
|
||||||
|
description: 'The URL of the server.',
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
@PothosRef()
|
@PothosRef()
|
||||||
meetingRoomCollaborator() {
|
meetingRoomCollaborator() {
|
||||||
return this.builder.prismaObject('MeetingRoomCollaborator', {
|
return this.builder.prismaObject('MeetingRoomCollaborator', {
|
||||||
@@ -69,13 +88,50 @@ export class MeetingRoomSchema extends PothosSchema {
|
|||||||
})
|
})
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
|
// get meeting room info by room id and check if user is collaborator of collaboration session then create new token and return it,
|
||||||
|
// if not collaborator then throw error
|
||||||
|
meetingRoomJoinInfo: t.field({
|
||||||
|
type: this.meetingRoomJoinInfo(),
|
||||||
|
args: {
|
||||||
|
collaborationSessionId: t.arg.string({
|
||||||
|
required: true,
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
resolve: async (_, args, ctx: SchemaContext) => {
|
||||||
|
if (ctx.isSubscription) throw new Error('Not allowed')
|
||||||
|
if (!ctx.http.me) throw new Error('Unauthorized')
|
||||||
|
const meetingRoom = await this.prisma.meetingRoom.findUnique({
|
||||||
|
where: { collaborationSessionId: args.collaborationSessionId },
|
||||||
|
})
|
||||||
|
if (!meetingRoom) throw new Error('Meeting room not found')
|
||||||
|
// check if user is collaborator of collaboration session
|
||||||
|
const collaborationSession = await this.prisma.collaborationSession.findUnique({
|
||||||
|
where: { id: meetingRoom.collaborationSessionId },
|
||||||
|
})
|
||||||
|
if (!collaborationSession) throw new Error('Collaboration session not found')
|
||||||
|
if (!collaborationSession.collaboratorsIds.includes(ctx.http.me.id))
|
||||||
|
throw new Error('User is not collaborator')
|
||||||
|
// create new token
|
||||||
|
const token = await this.livekitService.createToken(ctx.http.me, meetingRoom.id)
|
||||||
|
return {
|
||||||
|
id: meetingRoom.id,
|
||||||
|
token,
|
||||||
|
serverUrl: this.livekitService.getServerUrl(),
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}),
|
||||||
}))
|
}))
|
||||||
this.builder.mutationFields((t) => ({
|
this.builder.mutationFields((t) => ({
|
||||||
createMeetingRoom: t.prismaField({
|
createMeetingRoom: t.prismaField({
|
||||||
type: this.meetingRoom(),
|
type: this.meetingRoom(),
|
||||||
args: {
|
args: {
|
||||||
input: t.arg({
|
input: t.arg({
|
||||||
type: this.builder.generator.getCreateInput('MeetingRoom', ['id', 'createdAt', 'updatedAt']),
|
type: this.builder.generator.getCreateInput('MeetingRoom', [
|
||||||
|
'id',
|
||||||
|
'createdAt',
|
||||||
|
'updatedAt',
|
||||||
|
'collaborators',
|
||||||
|
]),
|
||||||
required: true,
|
required: true,
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
|
|||||||
Reference in New Issue
Block a user