Skip to content

Commit

Permalink
feat: Merge branch 'feat/#356/ticle-participate' of https://github.co…
Browse files Browse the repository at this point in the history
…m/boostcampwm-2024/web21-TICLE into feat/#356/ticle-participate
  • Loading branch information
simeunseo committed Dec 4, 2024
2 parents 78da51d + f86cc1e commit 4064cb3
Show file tree
Hide file tree
Showing 41 changed files with 636 additions and 266 deletions.
2 changes: 1 addition & 1 deletion apps/api/src/entity/summary.entity.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ export class Summary {
@Column('varchar')
audioUrl: string;

@Column('json')
@Column('json', { nullable: true })
summaryText: string[];

@CreateDateColumn({ type: 'timestamp', name: 'created_at' })
Expand Down
4 changes: 3 additions & 1 deletion apps/media/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,11 +28,12 @@
"@nestjs/platform-socket.io": "^10.4.7",
"@nestjs/websockets": "^10.4.7",
"@repo/lint": "workspace:*",
"@repo/tsconfig": "workspace:*",
"@repo/mediasoup": "workspace:*",
"@repo/tsconfig": "workspace:*",
"@repo/types": "workspace:*",
"fluent-ffmpeg": "^2.1.3",
"mediasoup": "^3.15.0",
"node-fetch": "^3.3.2",
"reflect-metadata": "^0.2.0",
"rxjs": "^7.8.1",
"socket.io": "^4.8.1"
Expand All @@ -44,6 +45,7 @@
"@types/express": "^5.0.0",
"@types/jest": "^29.5.2",
"@types/node": "^20.3.1",
"@types/node-fetch": "^2.6.12",
"@types/supertest": "^6.0.0",
"@typescript-eslint/eslint-plugin": "^8.0.0",
"@typescript-eslint/parser": "^8.0.0",
Expand Down
10 changes: 10 additions & 0 deletions apps/media/src/mediasoup/config.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { Injectable } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import {
PlainTransportOptions,
RouterOptions,
RtpCodecCapability,
WebRtcTransportOptions,
Expand Down Expand Up @@ -61,4 +62,13 @@ export class MediasoupConfig {
enableTcp: true,
preferUdp: true,
};

plainTransport: PlainTransportOptions = {
listenIp: {
ip: '127.0.0.1',
announcedIp: '127.0.0.1',
},
rtcpMux: true,
comedia: false,
};
}
29 changes: 29 additions & 0 deletions apps/media/src/mediasoup/mediasoup.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -295,4 +295,33 @@ export class MediasoupService implements OnModuleInit {
closeRoom(roomId: string) {
this.roomService.closeRoom(roomId);
}

async createPlainTransport(router: types.Router) {
return router.createPlainTransport(this.mediasoupConfig.plainTransport);
}

async createRecordConsumer(
transport: types.Transport,
producerId: string,
rtpCapabilities: types.RtpCapabilities,
producerPaused: boolean
) {
const consumer = await transport.consume({
producerId,
rtpCapabilities,
paused: producerPaused,
});
consumer.on('producerpause', () => {
consumer.pause();
});

consumer.on('producerresume', () => {
if (consumer.kind !== 'audio') {
return;
}
consumer.resume();
});

return consumer;
}
}
2 changes: 1 addition & 1 deletion apps/media/src/ncp/ncp.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,4 @@ import { NcpService } from './ncp.service';
providers: [NcpService, NcpConfig],
exports: [NcpService],
})
export class AppModule {}
export class NcpModule {}
25 changes: 17 additions & 8 deletions apps/media/src/ncp/ncp.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import * as fs from 'fs';
import { S3Client, PutObjectCommand } from '@aws-sdk/client-s3';
import { Injectable } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import fetch from 'node-fetch';
import { ErrorMessage } from '@repo/types';

import { NcpConfig } from './ncp.config';
Expand All @@ -18,9 +19,12 @@ export class NcpService {
this.s3 = ncpConfig.s3Client;
}

async uploadFile(localFilePath: string, remoteFileName: string): Promise<string> {
async uploadFile(
localFilePath: string,
remoteFileName: string,
ticleId: string
): Promise<string> {
const bucketName = this.configService.get<string>('NCP_OBJECT_STORAGE_BUCKET');
const endpoint = this.configService.get<string>('NCP_OBJECT_STORAGE_ENDPOINT');

const fileStream = fs.createReadStream(localFilePath);
const params = {
Expand All @@ -30,12 +34,17 @@ export class NcpService {
};

try {
const uploadResponse = await this.s3.send(new PutObjectCommand(params));
// console.log('File uploaded:', uploadResponse);

const url = `${endpoint}/${bucketName}/${remoteFileName}`;
// console.log('Uploaded file URL:', url);

await this.s3.send(new PutObjectCommand(params));
fs.unlink(localFilePath, () => {});
const serverURL = this.configService.get<string>('VITE_API_URL');
//todo 예외처리
fetch(`${serverURL}/stream/audio`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ ticleId, audioUrl: remoteFileName }),
});
return remoteFileName;
} catch (error) {

Check warning on line 49 in apps/media/src/ncp/ncp.service.ts

View workflow job for this annotation

GitHub Actions / check

'error' is defined but never used
throw new Error(ErrorMessage.FILE_UPLOAD_FAILED);
Expand Down
14 changes: 14 additions & 0 deletions apps/media/src/record/record.module.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { Module } from '@nestjs/common';

import { MediasoupModule } from '@/mediasoup/mediasoup.module';
import { NcpModule } from '@/ncp/ncp.module';
import { RoomModule } from '@/room/room.module';

import { RecordService } from './record.service';

@Module({
imports: [RoomModule, MediasoupModule, NcpModule],
providers: [RecordService],
exports: [RecordService],
})
export class RecordModule {}
149 changes: 149 additions & 0 deletions apps/media/src/record/record.service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
import fs from 'fs';

import { Injectable } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { WsException } from '@nestjs/websockets';
import { types } from 'mediasoup';
import { ErrorMessage } from '@repo/types';

import { MediasoupService } from '@/mediasoup/mediasoup.service';
import { NcpService } from '@/ncp/ncp.service';
import { RoomService } from '@/room/room.service';

import { RecordInfo } from './recordInfo';

@Injectable()
export class RecordService {
private recordInfos: Map<string, RecordInfo> = new Map();
private recordPath = './record';
private usedPorts: Set<number> = new Set();

constructor(
private mediasoupService: MediasoupService,
private roomService: RoomService,
private configService: ConfigService,
private ncpService: NcpService
) {
if (!fs.existsSync(this.recordPath)) {
fs.mkdirSync(this.recordPath);
}
}

async startRecord(roomId: string, socketId: string) {
const room = this.roomService.getRoom(roomId);
if (!room) {
return;
}
const router = room.router;
const peer = room.getPeer(socketId);
const audioProducer = peer.getAudioProducer();
if (!audioProducer) {
return;
}

const port = this.getPort();
const recordInfo = this.setRecordInfo(roomId, port, socketId);
const plainTransport = await this.addPlainTransport(recordInfo, router);
plainTransport.connect({
ip: '127.0.0.1',
port,
});
await this.addConsumer(
recordInfo,
router.rtpCapabilities,
audioProducer.id,
audioProducer.paused,
roomId
);
if (!audioProducer.paused) {
recordInfo.createFfmpegProcess(roomId);
}
}

private setRecordInfo(roomId: string, port: number, socketId: string) {
const recordInfo = new RecordInfo(port, socketId, this.ncpService);
this.recordInfos.set(roomId, recordInfo);
return recordInfo;
}

getRecordInfo(roomId: string) {
return this.recordInfos.get(roomId);
}

private async addPlainTransport(recordInfo: RecordInfo, router: types.Router) {
const plainTransport = await this.mediasoupService.createPlainTransport(router);
recordInfo.setPlainTransport(plainTransport);
return plainTransport;
}

private async addConsumer(
recordInfo: RecordInfo,
rtpCapabilities: types.RtpCapabilities,
producerId: string,
producerPaused: boolean,
roomId: string
) {
const plainTransport = recordInfo.plainTransport;
const consumer = await this.mediasoupService.createRecordConsumer(
plainTransport,
producerId,
rtpCapabilities,
producerPaused
);

recordInfo.setRecordConsumer(consumer, roomId);
return consumer;
}

pauseRecord(roomId: string) {
const recordInfo = this.recordInfos.get(roomId);
if (!recordInfo) {
return;
}
recordInfo.pauseRecordProcess();
}

resumeRecord(roomId: string) {
const recordInfo = this.recordInfos.get(roomId);
if (!recordInfo) {
return;
}
recordInfo.resumeRecordProcess();
}

stopRecord(roomId: string) {
const recordInfo = this.recordInfos.get(roomId);
if (!recordInfo) {
return;
}
this.releasePort(recordInfo.port);
recordInfo.stopRecordProcess();
this.recordInfos.delete(roomId);
}

private getPort() {
const minPort = Number(this.configService.get('RECORD_MIN_PORT'));
const maxPort = Number(this.configService.get('RECORD_MAX_PORT'));
const totalPorts = maxPort - minPort + 1;

if (this.usedPorts.size >= totalPorts) {
throw new WsException(ErrorMessage.NO_AVAILABLE_PORT);
}

let port: number;
do {
port = Math.floor(Math.random() * totalPorts) + minPort;
} while (this.usedPorts.has(port));

this.usedPorts.add(port);
return port;
}

private releasePort(port: number): void {
this.usedPorts.delete(port);
}

getIsRecording(roomId: string) {
return this.recordInfos.has(roomId);
}
}
Loading

0 comments on commit 4064cb3

Please sign in to comment.