Skip to content

Commit

Permalink
Browse files Browse the repository at this point in the history
…amon into Fix/336
  • Loading branch information
g00hyun committed Dec 2, 2024
2 parents e093584 + 31ccb9f commit a7a5954
Show file tree
Hide file tree
Showing 17 changed files with 110 additions and 76 deletions.
10 changes: 10 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,11 +43,15 @@
> 1️⃣ 캠퍼들은 코어타임 시간에 실시간 방송을 키면서 부스트 캠프 활동에 참여할 수 있습니다.<br>
> 2️⃣ 화면공유 on/off, 캠 on/off, 마이크 on/off 기능으로 캠퍼들이 보다 자유로운 방송을 할 수 있도록 돕습니다.<br>
> 3️⃣ 별도의 송출 소프트웨어 없이 서비스 내에서 방송 송출과 화면 배치 과정이 자동으로 이루어져 캠퍼들이 부담없이 방송할 수 있는 환경을 제공합니다.
>
![화면 송출 데모](https://github.com/user-attachments/assets/aaf18b1f-9192-4c3e-8059-0d9c0603184d)

### 👀 실시간 방송 시청
> 1️⃣ 캠퍼들은 서로의 방송을 시청하면서 실시간으로 서로의 학습 경험을 공유할 수 있습니다.<br>
> 2️⃣ 시청화면 하단에 방송중인 캠퍼의 정보를 제공하여 온라인 네트워킹 환경을 제공합니다.<br>
![방송 시청 데모](https://github.com/user-attachments/assets/c63cd77a-cc14-49e4-b3ed-36bc6ec26582)

### 💬 채팅
> 1️⃣ 캠퍼들은 채팅을 통해 실시간으로 소통할 수 있습니다.<br>
> 2️⃣ 방송 송출창과 시청창 모두 채팅 기능을 제공하여 방송중인 캠퍼와 시청하는 캠퍼 모두 자유롭게 지식을 공유하고 유대감을 쌓을 수 있습니다.<br>
Expand All @@ -56,14 +60,20 @@
> 1️⃣ 실시간 녹화 기능을 제공하여 코어타임 학습 중 기억하고 싶은 순간을 기록할 수 있습니다.<br>
> 2️⃣ 방송 중 기록한 녹화본들은 출석 내역에서 확인하며 스스로의 학습 경험을 돌아볼 수 있습니다.<br>
![녹화 데모](https://github.com/user-attachments/assets/905fa5b5-3531-4dbc-b92d-1dcf94d5fcc9)

### ✏️ 출석
> 1️⃣ 캠퍼는 마이페이지에서 본인의 출석 내역을 한 눈에 확인할 수 있습니다.<br>
> 2️⃣ 코어타임 시간 내에 송출되는 방송 시간을 기반으로 자동으로 캠퍼들의 출석이 관리됩니다.<br>
![출석 데모](https://github.com/user-attachments/assets/63adc867-a159-4bb4-a7f2-fea5edd892ab)

### 📚 아카이브
> 1️⃣ 캠퍼들은 메인페이지에서 여러개로 나누어진 베이스캠프를 한 번에 모아서 관리할 수 있습니다.<br>
> 2️⃣ 자유롭게 하이퍼링크를 등록하여 맞춤형 온라인 베이스 캠프를 구성할 수 있습니다.<br>
![아카이브 데모](https://github.com/user-attachments/assets/dafcd2ca-df14-4720-8f54-0ae4eb90be50)

# 핵심 개발 일지
| **핵심 기능** | **설명** |
|:---|:---|
Expand Down
12 changes: 6 additions & 6 deletions apps/api/src/auth/auth.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,11 +24,11 @@ export class AuthController {

@Get('/github/callback')
@UseGuards(GithubAuthGuard)
signinGithubCallback(@UserReq() member: Member, @Res() res: Response) {
const accessToken = this.authService.login(member);
async signinGithubCallback(@UserReq() member: Member, @Res() res: Response) {
const { accessToken, isNecessaryInfo } = this.authService.login(member);
const CALLBACK_URI = this.configService.get('CALLBACK_URI');

res.redirect(`${CALLBACK_URI}/auth?accessToken=${accessToken}`);
res.redirect(`${CALLBACK_URI}/auth?accessToken=${accessToken}&isNecessaryInfo=${isNecessaryInfo}`);
}

@Get('/signin/google')
Expand All @@ -39,10 +39,10 @@ export class AuthController {

@Get('/google/callback')
@UseGuards(GoogleAuthGuard)
signinGoogleCallback(@UserReq() member: Member, @Res() res: Response) {
const accessToken = this.authService.login(member);
async signinGoogleCallback(@UserReq() member: Member, @Res() res: Response) {
const { accessToken, isNecessaryInfo } = this.authService.login(member);
const CALLBACK_URI = this.configService.get('CALLBACK_URI');

res.redirect(`${CALLBACK_URI}/auth?accessToken=${accessToken}`);
res.redirect(`${CALLBACK_URI}/auth?accessToken=${accessToken}&isNecessaryInfo=${isNecessaryInfo}`);
}
}
3 changes: 2 additions & 1 deletion apps/api/src/auth/auth.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,9 @@ export class AuthService {
login(member: Member) {
const payload = { id: member.id, camperId: member.camperId };
const accessToken = this.jwtService.sign(payload);
const isNecessaryInfo = Boolean(member.field && member.name && member.camperId);

return accessToken;
return { accessToken, isNecessaryInfo };
}

async validateMember(id: number) {
Expand Down
4 changes: 2 additions & 2 deletions apps/api/src/member/dto/update-member-info.dto.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,15 +19,15 @@ export class UpdateMemberInfoDto {
@ApiProperty({ example: 'J000' })
camperId: string;
@ApiProperty({ example: 'WEB' })
type: FieldEnum;
field: FieldEnum;
@ApiProperty({ type: Contacts })
contacts: Contacts;

toMember() {
const member = new Member();
member.name = this.name;
member.camperId = this.camperId;
member.field = this.type;
member.field = this.field;
member.email = this.contacts.email;
member.github = this.contacts.github;
member.blog = this.contacts.blog;
Expand Down
3 changes: 2 additions & 1 deletion apps/chat/src/chat/chat.gateway.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,9 +32,10 @@ export class ChatGateway implements OnGatewayDisconnect {
};
}
//룸입장
@UseGuards(JWTAuthGuard)
@SubscribeMessage('joinRoom')
async handleJoinRoom(@MessageBody('roomId') roomId: string, @ConnectedSocket() client: Socket) {
this.chatService.joinRoom(roomId, client);
await this.chatService.joinRoom(roomId, client);
}
//룸나가기
@SubscribeMessage('leaveRoom')
Expand Down
4 changes: 4 additions & 0 deletions apps/chat/src/chat/chat.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ export class ChatService {
async createRoom(roomId: string, client: Socket) {
const member = await this.memberService.getMemberInfo(client.token);

console.log(member);
this.logger.log('createRoom... ', member.camperId, member.name);
const newClient = new Client(member.camperId, member.name, client);

const room = {
Expand All @@ -39,11 +41,13 @@ export class ChatService {
}

async joinRoom(roomId: string, client: Socket) {
this.logger.log('client Token is.. ', client.token);
const member = await this.memberService.getMemberInfo(client.token);

const room = this.rooms.get(roomId);
if (!room) new CustomWsException(ErrorStatus.ROOM_NOT_FOUND);

this.logger.log('joinRoom... ', member.camperId, member.name);
const newClient = new Client(member.camperId, member.name, client);

room.clients.set(client.id, newClient);
Expand Down
24 changes: 6 additions & 18 deletions apps/client/src/components/ChatContainer/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -32,26 +32,14 @@ const ChatContainer = ({ roomId, isProducer }: { roomId: string; isProducer: boo
const setUpRoom = async (isProducer: boolean) => {
try {
if (isProducer) {
console.log('채팅룸 생성할거임');
await new Promise(resolve =>
socket?.emit(
'createRoom',
{ name: '송출자', camperId: 'J111', roomId: roomId },
(response: { roomId: string }) => {
console.log(`채팅룸 생성 응답: ${JSON.stringify(response)}`);
console.log(`채팅룸 생성: ${response.roomId}`);
resolve;
},
),
);
socket?.emit('createRoom', { roomId: roomId }, (response: { roomId: string }) => {
console.log(`채팅룸 생성: ${response.roomId}`);
});
} else {
// 채팅방 입장
await new Promise(resolve =>
socket?.emit('joinRoom', { roomId: roomId, name: '김부캠', camperId: 'J999' }, () => {
console.log('채팅방 입장');
resolve;
}),
);
socket?.emit('joinRoom', { roomId: roomId }, () => {
console.log('채팅방 입장');
});
}
} catch (err) {
console.error(`방 생성/입장 실패: ${err}`);
Expand Down
4 changes: 3 additions & 1 deletion apps/client/src/pages/Auth/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,14 @@ function Auth() {
useEffect(() => {
try {
const accessToken = searchParams.get('accessToken');
const isNecessaryInfo = searchParams.get('isNecessaryInfo');
if (!accessToken) {
throw new Error('액세스 토큰을 받지 못했습니다.');
}

setLogIn(accessToken);
navigate('/', { replace: true });
if (isNecessaryInfo === 'true') navigate('/', { replace: true });
else navigate('/profile', { replace: true });
} catch (err) {
setError(err instanceof Error ? err : new Error('로그인 처리 중 오류'));
setTimeout(() => {
Expand Down
1 change: 0 additions & 1 deletion apps/client/src/pages/Broadcast/BroadcastTitle.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,6 @@ function BroadcastTitle({ currentTitle, onTitleChange }: BroadcastTitleProps) {
};

const onSubmit: SubmitHandler<Inputs> = data => {
// TODO: 요청 헤더에 Authorization 설정
axiosInstance.patch('/v1/broadcasts/title', { title: data.title }).then(response => {
if (!response.data.success) {
alert('제목 변경에 실패했습니다!');
Expand Down
13 changes: 10 additions & 3 deletions apps/client/src/pages/Broadcast/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import useScreenShare from '@/hooks/useScreenShare';
import BroadcastPlayer from './BroadcastPlayer';
import { Tracks } from '@/types/mediasoupTypes';
import RecordButton from './RecordButton';
import axiosInstance from '@/services/axios';

const mediaServerUrl = import.meta.env.VITE_MEDIASERVER_URL;

Expand Down Expand Up @@ -52,7 +53,7 @@ function Broadcast() {
roomId,
});
// 방송 정보
const [title, setTitle] = useState<string>('J000님의 방송');
const [title, setTitle] = useState<string>('');

const stopBroadcast = (e?: BeforeUnloadEvent) => {
if (e) {
Expand All @@ -76,9 +77,15 @@ function Broadcast() {
};

useEffect(() => {
window.addEventListener('beforeunload', stopBroadcast);

tracksRef.current['mediaAudio'] = mediaStream?.getAudioTracks()[0];

axiosInstance.get('/v1/members/info').then(response => {
if (response.data.success) {
setTitle(`${response.data.data.camperId}님의 방송`);
}
});

window.addEventListener('beforeunload', stopBroadcast);
return () => {
window.removeEventListener('beforeunload', stopBroadcast);
};
Expand Down
1 change: 0 additions & 1 deletion apps/client/src/pages/Home/LiveList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,6 @@ function LiveList() {
{liveList ? (
liveList.map(data => {
const { broadcastId, broadcastTitle, camperId, profileImage, thumbnail } = data;
console.log(data);
return (
<div key={broadcastId} className="flex justify-center">
<LiveCard
Expand Down
2 changes: 1 addition & 1 deletion apps/client/src/pages/Home/Search.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ function Search({ onSearch }: SearchProps) {
className="flex flex-row h-full w-full border border-1 border-border-bold rounded-circle pl-5 pr-2"
>
<input
{...register('keyword', { required: true })}
{...register('keyword')}
className="flex-1 bg-transparent focus-visible:outline-none"
placeholder="검색할 방송 제목을 입력해주세요"
/>
Expand Down
10 changes: 6 additions & 4 deletions apps/client/src/pages/Profile/EditUserInfo.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ interface EditUserInfoProps {
interface FormInput {
camperId: string | undefined;
name: string | undefined;
type: Field | undefined;
field: Field | undefined;
email: string | undefined;
github: string | undefined;
blog: string | undefined;
Expand All @@ -31,7 +31,7 @@ function EditUserInfo({ userData, toggleEditing }: EditUserInfoProps) {
defaultValues: {
camperId: userData?.camperId,
name: userData?.name,
type: userData?.field,
field: userData?.field,
email: userData?.contacts.email,
github: userData?.contacts.github,
blog: userData?.contacts.blog,
Expand All @@ -47,7 +47,7 @@ function EditUserInfo({ userData, toggleEditing }: EditUserInfoProps) {
const formData = {
name: data.name,
camperId: data.camperId,
type: selectedField,
field: selectedField,
contacts: {
email: data.email ? data.email : '',
github: data.github ? data.github : '',
Expand All @@ -56,6 +56,8 @@ function EditUserInfo({ userData, toggleEditing }: EditUserInfoProps) {
},
};

if (!formData.field) return;

axiosInstance.patch('/v1/members/info', formData).then(response => {
if (response.data.success) {
toggleEditing();
Expand All @@ -73,7 +75,7 @@ function EditUserInfo({ userData, toggleEditing }: EditUserInfoProps) {
<AvatarFallback>MY</AvatarFallback>
</Avatar>
<form onSubmit={handleSubmit(handlePatchUserInfo)} className="flex flex-col w-1/2 gap-5">
{(errors.camperId || errors.name) && (
{(errors.camperId || errors.name || !selectedField) && (
<p className="flex justify-end text-text-danger text-display-medium12">
{errors.camperId ? errors.camperId.message : errors.name ? errors.name.message : '분야를 선택해주세요'}
</p>
Expand Down
20 changes: 20 additions & 0 deletions apps/client/src/utils/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,23 @@ export const checkDependencies = (functionName: string, dependencies: { [key: st
if (missing.length === 0) return null;
return new Error(`${functionName} Error: ${missing.join(',')}이(가) 없습니다.`);
};

export const getPayloadFromJWT = () => {
const token = localStorage.getItem('accessToken');
if (!token) return undefined;
const base64Payload = token.split('.')[1];
const base64 = base64Payload.replace(/-/g, '+').replace(/_/g, '/');

const decodedJWT = JSON.parse(
decodeURIComponent(
window
.atob(base64)
.split('')
.map(c => {
return '%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2);
})
.join(''),
),
);
return decodedJWT;
};
34 changes: 18 additions & 16 deletions apps/media/src/sfu/services/record.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ export class RecordService {
private readonly serverPrivateIp: string;
private readonly announcedIp: string;

private transports = new Map<string, mediasoup.types.Transport>();
private transports = new Map<string, mediasoup.types.Transport[]>();

constructor(private readonly httpService: HttpService, private readonly configService: ConfigService) {
this.recordServerUrl = this.configService.get<string>('RECORD_SERVER_URL', 'http://localhost:3003');
Expand Down Expand Up @@ -54,12 +54,15 @@ export class RecordService {
}

async sendStreamForRecord(room: mediasoup.types.Router, producers: mediasoup.types.Producer[]) {
const recordTransport = await this.createPlainTransport(room);

const ports = { video: null, audio: null };
const consumers = await Promise.all(
producers.map(async producer => {
this.transports.set(room.id, recordTransport);

const recordTransport = await this.createPlainTransport(room);
if (this.transports.get(room.id)) {
this.transports.get(room.id).push(recordTransport);
} else {
this.transports.set(room.id, [recordTransport]);
}
const rtpCapabilities = this.getRtpCapabilities(room, producer.kind);
const recordConsumer = await recordTransport.consume({
producerId: producer.id,
Expand All @@ -70,33 +73,32 @@ export class RecordService {
temporalLayer: 1,
},
});

const { port } = await this.getAvailablePort();
ports[producer.kind] = port;
this.setUpRecordConsumerListeners(recordConsumer);
await recordTransport.connect({ ip: this.serverPrivateIp, port });
this.setUpRecordTransportListeners(recordTransport, port);
return recordConsumer;
}),
);

await this.sendStreamRequest(room.id, ports.video, STREAM_TYPE.RECORD, ports.audio);
setTimeout(async () => {
for (const consumer of consumers) {
await consumer.resume();
await consumer.requestKeyFrame();
}
}, 1000);

const { port } = await this.getAvailablePort();

await recordTransport.connect({ ip: this.serverPrivateIp, port });
this.setUpRecordTransportListeners(recordTransport, port);

await this.sendStreamRequest(room.id, port, STREAM_TYPE.RECORD);
}

async stopStreamFromRecord(room: mediasoup.types.Router, title: string) {
const recordTransport = this.transports.get(room.id);
if (!recordTransport) {
const recordTransports = this.transports.get(room.id);
if (!recordTransports) {
return;
}
recordTransport.close();
for (const recordTransport of recordTransports) {
recordTransport.close();
}
this.transports.delete(room.id);

await lastValueFrom(this.httpService.post(`${this.apiServerUrl}/v1/records`, { title, roomId: room.id }));
Expand Down
Loading

0 comments on commit a7a5954

Please sign in to comment.