-
Notifications
You must be signed in to change notification settings - Fork 0
๐พ WebRTC โ Nest.js์ React๋ก ํ๋ ๊ฐ๋จ ํ์์ฑํ ์์
๊น์ํ edited this page Nov 3, 2024
·
3 revisions
- ์์ ๋ฅผ ํ๊ธฐ์ ์์ ๋จผ์ WebRTC ๊ฐ๋ ์ ๋ฆฌ๋ถํฐ ํ์ตํ๋๊ฑธ ๊ฐ์ถ๋๋ฆฝ๋๋ค.
- ๋ก์ปฌํ๊ฒฝ์์ ๋ค๋ฃจ๊ธฐ์ ๊ฐ์ฅ ์ด๋ ค์ด ์ ์ SSL ์ธ์ฆ์์ธ ๊ฒ ๊ฐ๋ค์...
- WebRTC ํต์ ์ ํตํ ํ์์ฑํ ์ ์งํํ๊ธฐ ์ํด์๋ HTTPS๊ฐ ํ์
- React, Nest, OpenVidu 3๊ฐ ์ ๋ถ SSL ์ธ์ฆ์๊ฐ ์์ด์ผ ํจ
- Self Signed๋ฅผ ํตํด SSL ์ธ์ฆ์ ๋ฐ๊ธ
- 3๋ฒ์ ๋๊ฐ ๋ฒ ํ ๋ฒ์ ์ด๊ธฐ ๋๋ฌธ์ ์์ ์ ์ธ ํ๋ก๋์ ํ๊ฒฝ์ ์ํด 2.30.0 ์ค์น
curl https://s3-eu-west-1.amazonaws.com/aws.openvidu.io/install_openvidu_latest.sh | bash
cd openvidu
vi .env
DOMAIN_OR_PUBLIC_IP=localhost
OPENVIDU_SECRET=MAFIA_GAME //์๋ฌด๊ฑฐ๋ ๋ฃ์ด๋ ๋จ
CERTIFICATE_TYPE=selfsigned
HTTPS_PORT=4443 //443 ํฌํธ๋ฅผ HTTPS ํธ๋ํฝ์ ๋ฐ๊ธฐ ๋๋ฌธ์ ๊ด๋ฆฌ์ฉ ํฌํธ๋ก ๋ค๋ฅธ ํฌํธ๋ฅผ ์ฌ์ฉ
HTTP_PORT=80
mkdir -p docker/nginx/ssl
mkdir -p docker/nginx/conf.d
openssl req -x509 -nodes -days 365 -newkey rsa:2048 \
-keyout docker/nginx/ssl/nginx.key \
-out docker/nginx/ssl/nginx.crt \
-subj "/C=KR/ST=Seoul/L=Seoul/O=Dev/CN=localhost"
- Docker ์คํ ์คํฌ๋ฆฝํธ
# OpenVidu ์์
./openvidu start
# ์ํ ํ์ธ
./openvidu logs
# ์ค์ง
./openvidu stop
# ์ฌ์์
./openvidu restart
- ARM64 ํ๊ฒฝ์์ ๋ฐ์ํ๋ ๋ฌธ์
(base) 0chord@0Chord ๎ฐ ~/Desktop/boostcamp-membership/openvidu/openvidu ๎ฐ ./openvidu start
[+] Running 60/5
โ openvidu-server 9 layers [โฃฟโฃฟโฃฟโฃฟโฃฟโฃฟโฃฟโฃฟโฃฟ] 0B/0B Pulled 83.6s
โ coturn 8 layers [โฃฟโฃฟโฃฟโฃฟโฃฟโฃฟโฃฟโฃฟ] 0B/0B Pulled 163.6s
โ kms 13 layers [โฃฟโฃฟโฃฟโฃฟโฃฟโฃฟโฃฟโฃฟโฃฟโฃฟโฃฟโฃฟโฃฟ] 0B/0B Pulled 165.4s
โ nginx 15 layers [โฃฟโฃฟโฃฟโฃฟโฃฟโฃฟโฃฟโฃฟโฃฟโฃฟโฃฟโฃฟโฃฟโฃฟโฃฟ] 0B/0B Pulled 163.4s
โ app 10 layers [โฃฟโฃฟโฃฟโฃฟโฃฟโฃฟโฃฟโฃฟโฃฟโฃฟ] 0B/0B Pulled 129.9s
[+] Running 8/11
โ Network openvidu_default Created 0.1s
โ Container openvidu-app-1 Started 0.5s
โ ฟ Container openvidu-openvidu-server-1 Starting 0.7s
โ Container openvidu-coturn-1 Started 0.7s
โ ฟ Container openvidu-nginx-1 Starting 0.7s
โ ฟ Container openvidu-kms-1 Starting 0.7s
! openvidu-server The requested image's platform (linux/amd64) does not match the detected host platform (linux/arm64/v8) and no specific platform was requested 0.0s
! kms The requested image's platform (linux/amd64) does not match the detected host platform (linux/arm64/v8) and no specific platform was requested 0.0s
! nginx The requested image's platform (linux/amd64) does not match the detected host platform (linux/arm64/v8) and no specific platform was requested 0.0s
! app The requested image's platform (linux/amd64) does not match the detected host platform (linux/arm64/v8) and no specific platform was requested 0.0s
! coturn The requested image's platform (linux/amd64) does not match the detected host platform (linux/arm64/v8) and no specific platform was requested 0.0s
Error response from daemon: Mounts denied:
The path /opt/openvidu/kms-crashes is not shared from the host and is not known to Docker.
You can configure shared paths from Docker -> Preferences... -> Resources -> File Sharing.
See https://docs.docker.com/desktop/mac for more info.
- ํ๋ซํผ ๋ถ์ผ์น ๋ฌธ์ ํด๊ฒฐ
version: '3.1'
services:
openvidu-server:
image: openvidu/openvidu-server:2.30.0
restart: on-failure
platform: linux/amd64
network_mode: host
entrypoint: ['/usr/local/bin/entrypoint.sh']
volumes:
- ./coturn:/run/secrets/coturn
- /var/run/docker.sock:/var/run/docker.sock
- ${OPENVIDU_RECORDING_PATH}:${OPENVIDU_RECORDING_PATH}
- ${OPENVIDU_RECORDING_CUSTOM_LAYOUT}:${OPENVIDU_RECORDING_CUSTOM_LAYOUT}
- ${OPENVIDU_CDR_PATH}:${OPENVIDU_CDR_PATH}
env_file:
- .env
environment:
- SERVER_SSL_ENABLED=false
- SERVER_PORT=5443
- KMS_URIS=["ws://localhost:8888/kurento"]
- COTURN_IP=${COTURN_IP:-auto-ipv4}
- COTURN_PORT=${COTURN_PORT:-3478}
logging:
options:
max-size: "${DOCKER_LOGS_MAX_SIZE:-100M}"
kms:
image: ${KMS_IMAGE:-kurento/kurento-media-server:7.0.1}
restart: always
platform: linux/amd64
network_mode: host
ulimits:
core: -1
volumes:
- /opt/openvidu/kms-crashes:/opt/openvidu/kms-crashes
- ${OPENVIDU_RECORDING_PATH}:${OPENVIDU_RECORDING_PATH}
- /opt/openvidu/kurento-logs:/opt/openvidu/kurento-logs
environment:
- KMS_MIN_PORT=40000
- KMS_MAX_PORT=57000
- GST_DEBUG=${KMS_DOCKER_ENV_GST_DEBUG:-}
- KURENTO_LOG_FILE_SIZE=${KMS_DOCKER_ENV_KURENTO_LOG_FILE_SIZE:-100}
- KURENTO_LOGS_PATH=/opt/openvidu/kurento-logs
logging:
options:
max-size: "${DOCKER_LOGS_MAX_SIZE:-100M}"
coturn:
image: openvidu/openvidu-coturn:2.30.0
restart: on-failure
platform: linux/amd64
ports:
- "${COTURN_PORT:-3478}:${COTURN_PORT:-3478}/tcp"
- "${COTURN_PORT:-3478}:${COTURN_PORT:-3478}/udp"
env_file:
- .env
volumes:
- ./coturn:/run/secrets/coturn
command:
- --log-file=stdout
- --listening-port=${COTURN_PORT:-3478}
- --fingerprint
- --min-port=${COTURN_MIN_PORT:-57001}
- --max-port=${COTURN_MAX_PORT:-65535}
- --realm=openvidu
- --verbose
- --use-auth-secret
- --static-auth-secret=$${COTURN_SHARED_SECRET_KEY}
logging:
options:
max-size: "${DOCKER_LOGS_MAX_SIZE:-100M}"
nginx:
image: openvidu/openvidu-proxy:2.30.0
restart: always
platform: linux/amd64
network_mode: host
volumes:
- ./certificates:/etc/letsencrypt
- ./owncert:/owncert
- ./custom-nginx-vhosts:/etc/nginx/vhost.d/
- ./custom-nginx-locations:/custom-nginx-locations
- ${OPENVIDU_RECORDING_CUSTOM_LAYOUT}:/opt/openvidu/custom-layout
environment:
- DOMAIN_OR_PUBLIC_IP=${DOMAIN_OR_PUBLIC_IP}
- CERTIFICATE_TYPE=${CERTIFICATE_TYPE}
- LETSENCRYPT_EMAIL=${LETSENCRYPT_EMAIL}
- PROXY_HTTP_PORT=${HTTP_PORT:-}
- PROXY_HTTPS_PORT=${HTTPS_PORT:-}
- PROXY_HTTPS_PROTOCOLS=${HTTPS_PROTOCOLS:-}
- PROXY_HTTPS_CIPHERS=${HTTPS_CIPHERS:-}
- PROXY_HTTPS_HSTS=${HTTPS_HSTS:-}
- ALLOWED_ACCESS_TO_DASHBOARD=${ALLOWED_ACCESS_TO_DASHBOARD:-}
- ALLOWED_ACCESS_TO_RESTAPI=${ALLOWED_ACCESS_TO_RESTAPI:-}
- PROXY_MODE=CE
- WITH_APP=true
- SUPPORT_DEPRECATED_API=${SUPPORT_DEPRECATED_API:-false}
- REDIRECT_WWW=${REDIRECT_WWW:-false}
- WORKER_CONNECTIONS=${WORKER_CONNECTIONS:-10240}
- PUBLIC_IP=${PROXY_PUBLIC_IP:-auto-ipv4}
logging:
options:
max-size: "${DOCKER_LOGS_MAX_SIZE:-100M}"
- Docker ํ์ผ ๊ณต์ ์ค์
- Docker Desktop์์ Settings ํด๋ฆญ
- Resource > File Sharing ์ ํ
/opt/openvidu
/opt/openvidu/kms-crashes
- Applay&Restart ํด๋ฆญ
- OpenVidu ์ฌ์์
./openvidu stop
docker system prune -a
./openvidu start
(base) 0chord@0Chord ๎ฐ ~/Desktop/boostcamp-membership/openvidu/openvidu ๎ฐ ./openvidu start
[+] Running 60/5
โ openvidu-server 9 layers [โฃฟโฃฟโฃฟโฃฟโฃฟโฃฟโฃฟโฃฟโฃฟ] 0B/0B Pulled 169.9s
โ kms 13 layers [โฃฟโฃฟโฃฟโฃฟโฃฟโฃฟโฃฟโฃฟโฃฟโฃฟโฃฟโฃฟโฃฟ] 0B/0B Pulled 150.7s
โ nginx 15 layers [โฃฟโฃฟโฃฟโฃฟโฃฟโฃฟโฃฟโฃฟโฃฟโฃฟโฃฟโฃฟโฃฟโฃฟโฃฟ] 0B/0B Pulled 94.7s
โ app 10 layers [โฃฟโฃฟโฃฟโฃฟโฃฟโฃฟโฃฟโฃฟโฃฟโฃฟ] 0B/0B Pulled 35.4s
โ coturn 8 layers [โฃฟโฃฟโฃฟโฃฟโฃฟโฃฟโฃฟโฃฟ] 0B/0B Pulled 110.7s
[+] Running 4/7
โ Network openvidu_default C... 0.1s
โ Container openvidu-coturn-1 Started 2.1s
โ ฟ Container openvidu-kms-1 S... 2.1s
โ Container openvidu-app-1 S... 1.7s
โ ฟ Container openvidu-openvidu-server-1 Starting 2.1s
โ ฟ Container openvidu-nginx-1 Starting 2.1s
! app The requested image's platform (linux/amd64) does not match the detected host platform (linux/arm64/v8) and no specific platform was requested 0.0s
Error response from daemon: error while creating mount source path '/host_mnt/opt/openvidu/recordings': mkdir /host_mnt/opt/openvidu: operation not permitted
- ๊ธฐ์กด ์ค์ ์ ๊ฑฐ
./openvidu stop
docker-compose down -v
docker system prune -a
- ๋ณผ๋ฅจ ๋๋ ํ ๋ฆฌ ์์ฑ
sudo mkdir -p /opt/openvidu
sudo mkdir -p /opt/openvidu/recordings
sudo mkdir -p /opt/openvidu/kms-crashes
sudo chmod -R 777 /opt/openvidu
- docker-compose.yml ์์
version: '3.1'
services:
openvidu-server:
image: openvidu/openvidu-server:2.30.0
restart: on-failure
platform: linux/amd64
network_mode: host
entrypoint: ['/usr/local/bin/entrypoint.sh']
volumes:
- ./recordings:/opt/openvidu/recordings
- ./kms-crashes:/opt/openvidu/kms-crashes
- ./coturn:/run/secrets/coturn
- /var/run/docker.sock:/var/run/docker.sock
- ${OPENVIDU_RECORDING_PATH}:${OPENVIDU_RECORDING_PATH}
- ${OPENVIDU_RECORDING_CUSTOM_LAYOUT}:${OPENVIDU_RECORDING_CUSTOM_LAYOUT}
- ${OPENVIDU_CDR_PATH}:${OPENVIDU_CDR_PATH}
env_file:
- .env
environment:
- SERVER_SSL_ENABLED=false
- SERVER_PORT=5443
- KMS_URIS=["ws://localhost:8888/kurento"]
- COTURN_IP=${COTURN_IP:-auto-ipv4}
- COTURN_PORT=${COTURN_PORT:-3478}
logging:
options:
max-size: "${DOCKER_LOGS_MAX_SIZE:-100M}"
kms:
image: ${KMS_IMAGE:-kurento/kurento-media-server:7.0.1}
restart: always
platform: linux/amd64
network_mode: host
ulimits:
core: -1
volumes:
- ./recordings:/opt/openvidu/recordings
- ./kms-crashes:/opt/openvidu/kms-crashes
- /opt/openvidu/kms-crashes:/opt/openvidu/kms-crashes
- ${OPENVIDU_RECORDING_PATH}:${OPENVIDU_RECORDING_PATH}
- /opt/openvidu/kurento-logs:/opt/openvidu/kurento-logs
environment:
- KMS_MIN_PORT=40000
- KMS_MAX_PORT=57000
- GST_DEBUG=${KMS_DOCKER_ENV_GST_DEBUG:-}
- KURENTO_LOG_FILE_SIZE=${KMS_DOCKER_ENV_KURENTO_LOG_FILE_SIZE:-100}
- KURENTO_LOGS_PATH=/opt/openvidu/kurento-logs
logging:
options:
max-size: "${DOCKER_LOGS_MAX_SIZE:-100M}"
coturn:
image: openvidu/openvidu-coturn:2.30.0
restart: on-failure
platform: linux/amd64
ports:
- "${COTURN_PORT:-3478}:${COTURN_PORT:-3478}/tcp"
- "${COTURN_PORT:-3478}:${COTURN_PORT:-3478}/udp"
env_file:
- .env
volumes:
- ./coturn:/run/secrets/coturn
command:
- --log-file=stdout
- --listening-port=${COTURN_PORT:-3478}
- --fingerprint
- --min-port=${COTURN_MIN_PORT:-57001}
- --max-port=${COTURN_MAX_PORT:-65535}
- --realm=openvidu
- --verbose
- --use-auth-secret
- --static-auth-secret=$${COTURN_SHARED_SECRET_KEY}
logging:
options:
max-size: "${DOCKER_LOGS_MAX_SIZE:-100M}"
nginx:
image: openvidu/openvidu-proxy:2.30.0
restart: always
platform: linux/amd64
network_mode: host
volumes:
- ./certificates:/etc/letsencrypt
- ./owncert:/owncert
- ./custom-nginx-vhosts:/etc/nginx/vhost.d/
- ./custom-nginx-locations:/custom-nginx-locations
- ${OPENVIDU_RECORDING_CUSTOM_LAYOUT}:/opt/openvidu/custom-layout
environment:
- DOMAIN_OR_PUBLIC_IP=${DOMAIN_OR_PUBLIC_IP}
- CERTIFICATE_TYPE=${CERTIFICATE_TYPE}
- LETSENCRYPT_EMAIL=${LETSENCRYPT_EMAIL}
- PROXY_HTTP_PORT=${HTTP_PORT:-}
- PROXY_HTTPS_PORT=${HTTPS_PORT:-}
- PROXY_HTTPS_PROTOCOLS=${HTTPS_PROTOCOLS:-}
- PROXY_HTTPS_CIPHERS=${HTTPS_CIPHERS:-}
- PROXY_HTTPS_HSTS=${HTTPS_HSTS:-}
- ALLOWED_ACCESS_TO_DASHBOARD=${ALLOWED_ACCESS_TO_DASHBOARD:-}
- ALLOWED_ACCESS_TO_RESTAPI=${ALLOWED_ACCESS_TO_RESTAPI:-}
- PROXY_MODE=CE
- WITH_APP=true
- SUPPORT_DEPRECATED_API=${SUPPORT_DEPRECATED_API:-false}
- REDIRECT_WWW=${REDIRECT_WWW:-false}
- WORKER_CONNECTIONS=${WORKER_CONNECTIONS:-10240}
- PUBLIC_IP=${PROXY_PUBLIC_IP:-auto-ipv4}
logging:
options:
max-size: "${DOCKER_LOGS_MAX_SIZE:-100M}"
- ์ฌ์์
- nginx ํ์ผ ์์
vi custom-nginx-vhosts/default.conf
# custom-nginx-vhosts/default.conf ๋ด์ฉ
upstream openvidu {
server openvidu-server:5443;
}
server {
listen 4443 ssl;
server_name localhost;
ssl_certificate /etc/letsencrypt/live/localhost/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/localhost/privkey.pem;
ssl_session_cache shared:SSL:10m;
ssl_session_timeout 10m;
location / {
proxy_pass http://openvidu;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_connect_timeout 300;
proxy_send_timeout 300;
proxy_read_timeout 300;
}
location /dashboard {
proxy_pass http://openvidu/dashboard;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}
- ์ธ์ฆ์ ์์ฑ ๋ฐ ๋ฐฐ์น
mkdir -p certificates/live/localhost
openssl req -x509 -nodes -days 365 -newkey rsa:2048 \
-keyout certificates/live/localhost/privkey.pem \
-out certificates/live/localhost/fullchain.pem \
-subj "/CN=localhost"
chmod -R 755 certificates
- Docker-compose ์ฌ์ค์
version: '3.1'
services:
openvidu-server:
image: openvidu/openvidu-server:2.30.0
restart: on-failure
platform: linux/amd64
ports:
- "5443:5443"
entrypoint: ['/usr/local/bin/entrypoint.sh']
networks:
- openvidu_network
volumes:
- ./recordings:/opt/openvidu/recordings
- ./kms-crashes:/opt/openvidu/kms-crashes
- ./coturn:/run/secrets/coturn
- /var/run/docker.sock:/var/run/docker.sock
- ${OPENVIDU_RECORDING_PATH}:${OPENVIDU_RECORDING_PATH}
- ${OPENVIDU_RECORDING_CUSTOM_LAYOUT}:${OPENVIDU_RECORDING_CUSTOM_LAYOUT}
- ${OPENVIDU_CDR_PATH}:${OPENVIDU_CDR_PATH}
env_file:
- .env
environment:
- SERVER_SSL_ENABLED=false
- SERVER_PORT=5443
- KMS_URIS=["ws://kms:8888/kurento"]
- COTURN_IP=${COTURN_IP:-auto-ipv4}
- COTURN_PORT=${COTURN_PORT:-3478}
logging:
options:
max-size: "${DOCKER_LOGS_MAX_SIZE:-100M}"
kms:
image: ${KMS_IMAGE:-kurento/kurento-media-server:7.0.1}
restart: always
platform: linux/amd64
ports:
- "8888:8888"
networks:
- openvidu_network
ulimits:
core: -1
volumes:
- ./recordings:/opt/openvidu/recordings
- ./kms-crashes:/opt/openvidu/kms-crashes
- /opt/openvidu/kms-crashes:/opt/openvidu/kms-crashes
- ${OPENVIDU_RECORDING_PATH}:${OPENVIDU_RECORDING_PATH}
- /opt/openvidu/kurento-logs:/opt/openvidu/kurento-logs
environment:
- KMS_MIN_PORT=40000
- KMS_MAX_PORT=57000
- GST_DEBUG=${KMS_DOCKER_ENV_GST_DEBUG:-}
- KURENTO_LOG_FILE_SIZE=${KMS_DOCKER_ENV_KURENTO_LOG_FILE_SIZE:-100}
- KURENTO_LOGS_PATH=/opt/openvidu/kurento-logs
logging:
options:
max-size: "${DOCKER_LOGS_MAX_SIZE:-100M}"
coturn:
image: openvidu/openvidu-coturn:2.30.0
restart: on-failure
platform: linux/amd64
networks:
- openvidu_network
ports:
- "${COTURN_PORT:-3478}:${COTURN_PORT:-3478}/tcp"
- "${COTURN_PORT:-3478}:${COTURN_PORT:-3478}/udp"
env_file:
- .env
volumes:
- ./coturn:/run/secrets/coturn
command:
- --log-file=stdout
- --listening-port=${COTURN_PORT:-3478}
- --fingerprint
- --min-port=${COTURN_MIN_PORT:-57001}
- --max-port=${COTURN_MAX_PORT:-65535}
- --realm=openvidu
- --verbose
- --use-auth-secret
- --static-auth-secret=$${COTURN_SHARED_SECRET_KEY}
logging:
options:
max-size: "${DOCKER_LOGS_MAX_SIZE:-100M}"
nginx:
image: openvidu/openvidu-proxy:2.30.0
restart: always
platform: linux/amd64
networks:
- openvidu_network
depends_on:
- openvidu-server
ports:
- "80:80"
- "443:443"
- "4443:4443"
volumes:
- ./certificates:/etc/letsencrypt
- ./owncert:/owncert
- ./custom-nginx-vhosts:/etc/nginx/vhost.d/
- ./custom-nginx-locations:/custom-nginx-locations
- ${OPENVIDU_RECORDING_CUSTOM_LAYOUT}:/opt/openvidu/custom-layout
- ./docker/nginx/ssl:/etc/nginx/ssl
environment:
- DOMAIN_OR_PUBLIC_IP=${DOMAIN_OR_PUBLIC_IP}
- CERTIFICATE_TYPE=${CERTIFICATE_TYPE}
- LETSENCRYPT_EMAIL=${LETSENCRYPT_EMAIL}
- PROXY_HTTP_PORT=${HTTP_PORT:-}
- PROXY_HTTPS_PORT=${HTTPS_PORT:-}
- PROXY_HTTPS_PROTOCOLS=${HTTPS_PROTOCOLS:-}
- PROXY_HTTPS_CIPHERS=${HTTPS_CIPHERS:-}
- PROXY_HTTPS_HSTS=${HTTPS_HSTS:-}
- ALLOWED_ACCESS_TO_DASHBOARD=${ALLOWED_ACCESS_TO_DASHBOARD:-}
- ALLOWED_ACCESS_TO_RESTAPI=${ALLOWED_ACCESS_TO_RESTAPI:-}
- PROXY_MODE=CE
- WITH_APP=true
- SUPPORT_DEPRECATED_API=${SUPPORT_DEPRECATED_API:-false}
- REDIRECT_WWW=${REDIRECT_WWW:-false}
- WORKER_CONNECTIONS=${WORKER_CONNECTIONS:-10240}
- PUBLIC_IP=${PROXY_PUBLIC_IP:-auto-ipv4}
logging:
options:
max-size: "${DOCKER_LOGS_MAX_SIZE:-100M}"
networks:
openvidu_network:
driver: bridge
- ์ปจํ ์ด๋ ์ฌ์์
./openvidu stop
docker-compose down
docker system prune -a
./openvidu start
- NestJS ํ๋ก์ ํธ ์์ฑ
nest new openvidu-server
cd openvidu-server
npm install openvidu-node-client @nestjs/config dotenv
- .env ์ค์
OPENVIDU_URL=https://localhost:4443
OPENVIDU_SECRET=MAFIA_GAME
NODE_TLS_REJECT_UNAUTHORIZED=0
- OpenVidu ๋ชจ๋ ์์ฑ
- OpenViduService ๊ตฌํ
import {Injectable} from "@nestjs/common";
import {OpenVidu, Session} from "openvidu-node-client";
import {ConfigService} from "@nestjs/config";
@Injectable()
export class OpenViduService {
private openVidu: OpenVidu;
constructor(private readonly configService: ConfigService) {
this.openVidu = new OpenVidu(
this.configService.get('OPENVIDU_URL'),
this.configService.get('OPENVIDU_SECRET')
)
}
async createSession(sessionId: string): Promise<Session> {
try {
const sessions = this.openVidu.activeSessions;
const existingSession = sessions.find(s => s.sessionId === sessionId);
if (existingSession) {
return existingSession;
}
return await this.openVidu.createSession({customSessionId: sessionId});
} catch (error){
throw error;
}
}
async createConnection(sessionId: string, metadata?: string): Promise<string> {
try {
const session = await this.openVidu.activeSessions.find(
s => s.sessionId === sessionId
);
if (!session) {
throw new Error('No session exists with that sessionId');
}
const connection = await session.createConnection({
data: metadata,
});
return connection.token;
} catch (error){
throw error;
}
}
getSession(sessionId: string): Session{
const sessions = this.openVidu.activeSessions;
const session = sessions.find(s => s.sessionId === sessionId);
if (!session) {
throw new Error('No session exists with that sessionId');
}
return session;
}
listSessions() {
return this.openVidu.activeSessions;
}
}
- OpenViduController ๊ตฌํ
import { Controller, Post, Body, Get, Param, HttpException, HttpStatus } from '@nestjs/common';
import { OpenViduService } from './openvidu.service';
@Controller('api/sessions')
export class OpenViduController {
constructor(private readonly openViduService: OpenViduService) {}
@Post()
async createSession(@Body() body: { sessionId: string }) {
try {
const session = await this.openViduService.createSession(body.sessionId);
return {sessionId:session.sessionId};
} catch (error) {
throw new HttpException(error.message, HttpStatus.INTERNAL_SERVER_ERROR);
}
}
@Post(':sessionId/connections')
async createConnection(
@Param('sessionId') sessionId: string,
@Body() body: { metadata?: string }
) {
try {
const token = await this.openViduService.createConnection(
sessionId,
body.metadata
);
return { token };
} catch (error) {
throw new HttpException(error.message, HttpStatus.INTERNAL_SERVER_ERROR);
}
}
@Get(':sessionId')
async getSession(@Param('sessionId') sessionId: string) {
try {
const session = await this.openViduService.getSession(sessionId);
return {
sessionId: session.sessionId,
connections: session.activeConnections.length
};
} catch (error) {
throw new HttpException(error.message, HttpStatus.NOT_FOUND);
}
}
@Get()
async listSessions() {
try {
const sessions = this.openViduService.listSessions();
return sessions.map(session => ({
sessionId: session.sessionId,
connections: session.activeConnections.length
}));
} catch (error) {
throw new HttpException(error.message, HttpStatus.INTERNAL_SERVER_ERROR);
}
}
}
- OpenViduModule ๊ตฌํ
import {OpenViduService} from "./openvidu.service";
import {ConfigModule} from "@nestjs/config";
import {OpenViduController} from "./openvidu.controller";
@Module({
imports: [ConfigModule],
controllers: [OpenViduController],
providers: [OpenViduService],
exports: [OpenViduService],
})
export class OpenViduModule {}
- AppModule ๊ตฌํ
import { Module } from '@nestjs/common';
import {ConfigModule} from "@nestjs/config";
import {OpenViduModule} from "./openvidu/openvidu.module";
@Module({
imports: [ConfigModule.forRoot({
isGlobal: true,
}),
OpenViduModule]
})
export class AppModule {}
- ssl ์ธ์ฆ์ ์ค์
- WebRTC ํต์ ์ ํ๋ ค๋ฉด Nest ์๋ฒ๋ SSL ์ธ์ฆ์๊ฐ ์์ด์ผ ํ๋ฏ๋ก ๋ก์ปฌํ๊ฒฝ์์๋ ๊ฐ๋ฐ์ฉ์ผ๋ก ์ธ์ฆ์ ์์ฑ
mkdir -p ssl
cd ssl
openssl req -x509 -nodes -days 365 -newkey rsa:2048 \
-keyout localhost.key \
-out localhost.crt \
-subj "/CN=localhost"
- Nest.js HTTPS ์ค์
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import * as fs from "fs";
import * as path from "path";
import {cwd} from 'process';
async function bootstrap() {
const __dirname = path.resolve(cwd());
const httpsOptions = {
key: await fs.readFileSync(path.join(__dirname, './ssl/localhost.key')),
cert: await fs.readFileSync(path.join(__dirname, './ssl/localhost.crt')),
};
const app = await NestFactory.create(AppModule,{
httpsOptions
});
app.enableCors({
origin: true,
methods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'],
allowedHeaders: ['Content-Type', 'Accept','Authorization'],
credentials: true,
});
await app.listen(3333);
}
bootstrap();
- React ํ๋ก์ ํธ ์์ฑ
npx create-react-app openvidu-react --template typescript
cd openvidu-react
# ํ์ํ ํจํค์ง ์ค์น
npm install openvidu-browser axios styled-components @types/styled-components
- SSL ์ค์
mkdir ssl
cd ssl
openssl req -x509 -nodes -days 365 -newkey rsa:2048 \
-keyout localhost.key \
-out localhost.crt \
-subj "/CN=localhost"
- .env ํ์ผ ์์ฑ (ํ๋ก์ ํธ ๋ฃจํธ)
cd ../
npm i dotenv
vi .env
HTTPS=true
SSL_CRT_FILE=ssl/localhost.crt
SSL_KEY_FILE=ssl/localhost.key
REACT_APP_API_URL=https://localhost:3333
NODE_TLS_REJECT_UNAUTHORIZED=0
REACT_APP_OPENVIDU_URL=https://localhost:4443
REACT_APP_OPENVIDU_SERVER_SECRET=MAFIA_GAME
- src/types/vides.ts ์์ฑ
export interface StreamManager {
stream: {
getMediaStream: () => MediaStream;
};
}
export interface Publisher extends StreamManager {
publishAudio: (value: boolean) => void;
publishVideo: (value: boolean) => void;
}
export interface Subscriber extends StreamManager {}
- src/components/VideoPlayer.tsx ์์ฑ
import React, { useRef, useEffect } from 'react';
import styled from 'styled-components';
const VideoContainer = styled.div`
width: 100%;
height: 100%;
position: relative;
background-color: #000;
border-radius: 8px;
overflow: hidden;
`;
const Video = styled.video`
width: 100%;
height: 100%;
object-fit: cover;
`;
const NoVideo = styled.div`
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
color: white;
font-size: 24px;
background-color: #333;
`;
const MuteIndicator = styled.div`
position: absolute;
bottom: 10px;
left: 10px;
color: white;
padding: 5px;
border-radius: 4px;
background-color: rgba(0, 0, 0, 0.5);
`;
interface VideoPlayerProps {
streamManager: any;
muted: boolean;
audioEnabled?: boolean;
videoEnabled?: boolean;
}
const VideoPlayer: React.FC<VideoPlayerProps> = ({
streamManager,
muted,
audioEnabled = true,
videoEnabled = true
}) => {
const videoRef = useRef<HTMLVideoElement>(null);
useEffect(() => {
if (videoRef.current && streamManager) {
streamManager.addVideoElement(videoRef.current);
}
}, [streamManager]);
return (
<VideoContainer>
{videoEnabled ? (
<Video
ref={videoRef}
autoPlay
playsInline
muted={muted}
/>
) : (
<NoVideo>Video Off</NoVideo>
)}
{!audioEnabled && (
<MuteIndicator>๐</MuteIndicator>
)}
</VideoContainer>
);
};
export default VideoPlayer;
- src/components/VideoRoom.tsx ์์ฑ
import React, { useState, useEffect, useCallback } from 'react';
import { OpenVidu } from 'openvidu-browser';
import axios from 'axios';
import styled from 'styled-components';
import VideoPlayer from './VideoPlayer';
import { Publisher, Subscriber } from '../types/video';
const Container = styled.div`
width: 100%;
height: 100vh;
display: flex;
flex-direction: column;
`;
const VideoGrid = styled.div`
display: grid;
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
gap: 20px;
padding: 20px;
flex: 1;
`;
const Controls = styled.div`
display: flex;
justify-content: center;
padding: 20px;
gap: 10px;
`;
const Button = styled.button`
padding: 10px 20px;
border: none;
border-radius: 5px;
cursor: pointer;
background-color: #007bff;
color: white;
&:hover {
background-color: #0056b3;
}
`;
const APPLICATION_SERVER_URL = 'https://localhost:3333/';
const VideoRoom: React.FC = () => {
const [session, setSession] = useState<any>(undefined);
const [publisher, setPublisher] = useState<Publisher | undefined>(undefined);
const [subscribers, setSubscribers] = useState<Subscriber[]>([]);
const [audioEnabled, setAudioEnabled] = useState(true);
const [videoEnabled, setVideoEnabled] = useState(true);
const joinSession = useCallback(async () => {
try {
const OV = new OpenVidu();
const session = OV.initSession();
session.on('streamCreated', (event) => {
const subscriber = session.subscribe(event.stream, undefined);
setSubscribers((subs) => [...subs, subscriber]);
});
session.on('streamDestroyed', (event) => {
setSubscribers((subs) =>
subs.filter((sub) => sub !== event.stream.streamManager)
);
});
setSession(session);
const response = await axios.post(
`${APPLICATION_SERVER_URL}api/sessions`,
{ sessionId: 'SessionA' },
{
headers: { 'Content-Type': 'application/json' },
}
);
const token = await axios.post(
`${APPLICATION_SERVER_URL}api/sessions/${response.data.sessionId}/connections`,
{},
{
headers: { 'Content-Type': 'application/json' },
}
);
await session.connect(token.data.token);
const publisher = await OV.initPublisherAsync(undefined, {
audioSource: undefined,
videoSource: undefined,
publishAudio: true,
publishVideo: true,
resolution: '640x480',
frameRate: 30,
insertMode: 'APPEND',
mirror: false,
});
await session.publish(publisher);
setPublisher(publisher);
} catch (error) {
console.error('Error joining session:', error);
}
}, []);
const leaveSession = useCallback(() => {
if (session) {
session.disconnect();
setSession(undefined);
setPublisher(undefined);
setSubscribers([]);
}
}, [session]);
const toggleAudio = useCallback(() => {
if (publisher) {
publisher.publishAudio(!audioEnabled);
setAudioEnabled(!audioEnabled);
}
}, [publisher, audioEnabled]);
const toggleVideo = useCallback(() => {
if (publisher) {
publisher.publishVideo(!videoEnabled);
setVideoEnabled(!videoEnabled);
}
}, [publisher, videoEnabled]);
useEffect(() => {
return () => {
leaveSession();
};
}, [leaveSession]);
return (
<Container>
{session === undefined ? (
<Button onClick={joinSession}>Join Session</Button>
) : (
<>
<VideoGrid>
{publisher && <VideoPlayer streamManager={publisher} />}
{subscribers.map((sub, i) => (
<VideoPlayer key={i} streamManager={sub} />
))}
</VideoGrid>
<Controls>
<Button onClick={toggleAudio}>
{audioEnabled ? 'Mute' : 'Unmute'}
</Button>
<Button onClick={toggleVideo}>
{videoEnabled ? 'Stop Video' : 'Start Video'}
</Button>
<Button onClick={leaveSession}>Leave Session</Button>
</Controls>
</>
)}
</Container>
);
};
export default VideoRoom;
- src/App.tsx ์์
import React from 'react';
import VideoRoom from './components/VideoRoom';
import styled from 'styled-components';
const AppContainer = styled.div`
width: 100%;
height: 100vh;
`;
function App() {
return (
<AppContainer>
<VideoRoom />
</AppContainer>
);
}
export default App;
- package.json scripts ์์
{
"scripts": {
"start": "HTTPS=true SSL_CRT_FILE=ssl/localhost.crt SSL_KEY_FILE=ssl/localhost.key react-scripts start",
"build": "react-scripts build",
"test": "react-scripts test",
"eject": "react-scripts eject"
}
}
- ์คํ
npm start
web12-MafiaCamp
๐ฏํ๋ก์ ํธ ๊ท์น
๐ปํ๋ก์ ํธ ๊ธฐํ
๐๊ธฐ์ ์คํ
- ๐ป Next.js 15๋ฅผ ์ ํํ ์ด์
- ๐ NestJS๋ฅผ ์ ํํ ์ด์
- ๐ฅ๏ธ OpenVidu๋ฅผ ์ ํํ ์ด์
- ๐ TypeORM์ ์ ํํ ์ด์
- ๐ฌ ์ฑํ ๊ธฐ๋ฅ ๊ตฌํ์ ์ํด WebSocket์ ์ ํํ ์ด์
- ๐ WebRTC ๊ฐ๋ ์ ๋ฆฌ
- ๐พ WebRTC โ Nest.js์ React๋ก ํ๋ ๊ฐ๋จ ํ์์ฑํ ์์
- ๐ฅ๏ธ GitHub Actions๋ก CI/CD ๊ตฌ์ถ ๋ฐฉ๋ฒ
- ๐ฆ Docker์ ๊ฐ๋ ๊ณผ ์ฌ์ฉ ๋ฐฉ๋ฒ
- ๐ OAuth ๊ธฐ๋ณธ ์ธ์ฆ ๊ณผ์ ๊ณผ ์์
๐๊ทธ๋ฃน ํ๊ณ
๐๊ฐ๋ฐ ์ผ์ง
๐๋ฌธ์ ํด๊ฒฐ ๊ฒฝํ
- ์น์์ผ ๋ฐฉ ๊ด๋ฆฌ ๊ตฌ์กฐ ๊ฐ์
- Pub-Sub ํจํด์ ํตํ ์ค์๊ฐ ๋ฐฉ ๋ชฉ๋ก ์กฐํ ๊ธฐ๋ฅ ๊ฐ๋ฐ
- ์ ํ ์ํ ๊ธฐ๊ณ๋ฅผ ์ด์ฉํ ๊ฒ์ ์งํ ๋ชจ๋ธ๋ง
- ๐ ๋์์ฑ ์ด์๋ฅผ ํด๊ฒฐํ๊ธฐ ์ํ LockManager ๋ง๋ค๊ธฐ
- โฐ RxJS๋ก ์ค์๊ฐ ํ์ด๋จธ ๊ตฌ์ถํ๊ธฐ
- ๐ณNext.js์ Docker๋ฅผ ์ฌ์ฉํ ๋น๋ ์ต์ ํํ๊ธฐ
- ๐ข Redis๋ฅผ ํตํ ์ ์ ์จ๋ผ์ธ ์ํ ๊ด๋ฆฌ ์์คํ ๊ตฌํํ๊ธฐ
- openvidu ์๋ฌ ๋๋ฒ๊น ์ ์ํ Docker ๊ฐ๋ฐํ๊ฒฝ ์ค์
๐งํธ๋ฌ๋ธ ์ํ
- NestJS, mkcert CA ์ธ์ฆ์ ๋ฌธ์ ํด๊ฒฐ ๋ฐฉ๋ฒ
- openvidu ICE ํ๋ณด ๊ด๋ จ ์ค๋ฅ
- Enterํค ์ด๋ฒคํธ ์ค๋ณต ํธ์ถ ๋ฌธ์
- mutex lock ๋ฌธ์
- ํฌํ ๋์์ ์ง์ ์ค๋ฅ
- openvidu ์ธ์ ์ข ๋ฃ ๋ฉ์๋ ์ค๋ฅ
- ์บ์๋ก ์ธํ ๋ฏธ๋ค์จ์ด ๋ฏธํธ์ถ ๋ฐ ํ์ด์ง ์ ํ ์ค๋ฅ
- ๋คํฌ ๋ชจ๋์์ ํ ์คํธ๊ฐ ๋ณด์ด์ง ์๋ ๋ฌธ์
- ๊ฒ์ ๋ฐฉ์์ ์๋ก๊ณ ์นจ ๋๋ ๋ธ๋ผ์ฐ์ ํญ์ ๋ซ์ ๋์ ์์ธ ์ฒ๋ฆฌ