Skip to content

๐Ÿ’พ WebRTC โ€ Nest.js์™€ React๋กœ ํ•˜๋Š” ๊ฐ„๋‹จ ํ™”์ƒ์ฑ„ํŒ… ์˜ˆ์ œ

๊น€์˜ํ˜„ edited this page Nov 3, 2024 · 3 revisions

image

ํ•™์Šตํ•˜๊ธฐ ์•ž์„œ...

  • ์˜ˆ์ œ๋ฅผ ํ•˜๊ธฐ์— ์•ž์„œ ๋จผ์ € WebRTC ๊ฐœ๋…์ •๋ฆฌ๋ถ€ํ„ฐ ํ•™์Šตํ•˜๋Š”๊ฑธ ๊ฐ•์ถ”๋“œ๋ฆฝ๋‹ˆ๋‹ค.
  • ๋กœ์ปฌํ™˜๊ฒฝ์—์„œ ๋‹ค๋ฃจ๊ธฐ์— ๊ฐ€์žฅ ์–ด๋ ค์šด ์ ์€ SSL ์ธ์ฆ์„œ์ธ ๊ฒƒ ๊ฐ™๋„ค์š”...
  • WebRTC ํ†ต์‹ ์„ ํ†ตํ•œ ํ™”์ƒ์ฑ„ํŒ…์„ ์ง„ํ–‰ํ•˜๊ธฐ ์œ„ํ•ด์„œ๋Š” HTTPS๊ฐ€ ํ•„์ˆ˜
  • React, Nest, OpenVidu 3๊ฐœ ์ „๋ถ€ SSL ์ธ์ฆ์„œ๊ฐ€ ์žˆ์–ด์•ผ ํ•จ
  • Self Signed๋ฅผ ํ†ตํ•ด SSL ์ธ์ฆ์„œ ๋ฐœ๊ธ‰

1. OpenVidu 2.30.0 ์„ค์น˜

  • 3๋ฒ„์ „๋Œ€๊ฐ€ ๋ฒ ํƒ€ ๋ฒ„์ „์ด๊ธฐ ๋•Œ๋ฌธ์— ์•ˆ์ •์ ์ธ ํ”„๋กœ๋•์…˜ ํ™˜๊ฒฝ์„ ์œ„ํ•ด 2.30.0 ์„ค์น˜
curl https://s3-eu-west-1.amazonaws.com/aws.openvidu.io/install_openvidu_latest.sh | bash

2. .env ์ˆ˜์ •

cd openvidu
vi .env

DOMAIN_OR_PUBLIC_IP=localhost
OPENVIDU_SECRET=MAFIA_GAME //์•„๋ฌด๊ฑฐ๋‚˜ ๋„ฃ์–ด๋„ ๋จ
CERTIFICATE_TYPE=selfsigned
HTTPS_PORT=4443 //443 ํฌํŠธ๋ฅผ HTTPS ํŠธ๋ž˜ํ”ฝ์„ ๋ฐ›๊ธฐ ๋•Œ๋ฌธ์— ๊ด€๋ฆฌ์šฉ ํฌํŠธ๋กœ ๋‹ค๋ฅธ ํฌํŠธ๋ฅผ ์‚ฌ์šฉ
HTTP_PORT=80

3. ssl ์ธ์ฆ์„ ์œ„ํ•œ nginx ์„ค์ •

mkdir -p docker/nginx/ssl
mkdir -p docker/nginx/conf.d

4. ๊ฐœ๋ฐœ ํ™˜๊ฒฝ์šฉ SSL ์ธ์ฆ์„œ ์ƒ์„ฑ

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"

5. OpenVidu ์‹คํ–‰/๊ด€๋ฆฌ ๋ช…๋ น

  • Docker ์‹คํ–‰ ์Šคํฌ๋ฆฝํŠธ
# OpenVidu ์‹œ์ž‘
./openvidu start

# ์ƒํƒœ ํ™•์ธ
./openvidu logs

# ์ค‘์ง€
./openvidu stop

# ์žฌ์‹œ์ž‘
./openvidu restart

6. ์‹คํ–‰ ์—๋Ÿฌ ํ•ธ๋“ค๋ง(MacOS ํ•ด๋‹น)

  • 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

6. Volume ์—๋Ÿฌ ์ฒ˜๋ฆฌ

(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
  1. ๊ธฐ์กด ์„ค์ • ์ œ๊ฑฐ
./openvidu stop
docker-compose down -v
docker system prune -a
  1. ๋ณผ๋ฅจ ๋””๋ ‰ํ† ๋ฆฌ ์ƒ์„ฑ
sudo mkdir -p /opt/openvidu
sudo mkdir -p /opt/openvidu/recordings
sudo mkdir -p /opt/openvidu/kms-crashes

sudo chmod -R 777 /opt/openvidu
  1. 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}"
  1. ์žฌ์‹œ์ž‘

7. Nginx ์žฌ์„ค์ •

  1. 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;
    }
}
  1. ์ธ์ฆ์„œ ์ƒ์„ฑ ๋ฐ ๋ฐฐ์น˜
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
  1. 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
  1. ์ปจํ…Œ์ด๋„ˆ ์žฌ์‹œ์ž‘
./openvidu stop
docker-compose down
docker system prune -a
./openvidu start

8. OpenVidu ๋Œ€์‹œ๋ณด๋“œ ์ ‘์†

image

9. nest ์—ฐ๋™

  1. NestJS ํ”„๋กœ์ ํŠธ ์ƒ์„ฑ
nest new openvidu-server
cd openvidu-server

npm install openvidu-node-client @nestjs/config dotenv
  1. .env ์„ค์ •
OPENVIDU_URL=https://localhost:4443
OPENVIDU_SECRET=MAFIA_GAME
NODE_TLS_REJECT_UNAUTHORIZED=0
  1. OpenVidu ๋ชจ๋“ˆ ์ƒ์„ฑ

image

  1. 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;
  }
}
  1. 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);
    }
  }
}
  1. 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 {}
  1. 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 {}
  1. 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"
  1. 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();

10. React ์—ฐ๋™

  1. React ํ”„๋กœ์ ํŠธ ์ƒ์„ฑ
npx create-react-app openvidu-react --template typescript
cd openvidu-react

# ํ•„์š”ํ•œ ํŒจํ‚ค์ง€ ์„ค์น˜
npm install openvidu-browser axios styled-components @types/styled-components
  1. SSL ์„ค์ •
mkdir ssl
cd ssl

openssl req -x509 -nodes -days 365 -newkey rsa:2048 \
  -keyout localhost.key \
  -out localhost.crt \
  -subj "/CN=localhost"
  1. .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
  1. 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 {}
  1. 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;
  1. 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;
  1. 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;
  1. 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"
  }
}
  1. ์‹คํ–‰
npm start

MafiaCamp

๐Ÿ“”์†Œ๊ฐœ
๐ŸŽฏํ”„๋กœ์ ํŠธ ๊ทœ์น™
๐Ÿ’ปํ”„๋กœ์ ํŠธ ๊ธฐํš
๐Ÿ€๊ธฐ์ˆ  ์Šคํƒ
๐Ÿ“š๊ทธ๋ฃน ํšŒ๊ณ 
๐ŸŒˆ๊ฐœ๋ฐœ ์ผ์ง€
๐Ÿ€๋ฌธ์ œ ํ•ด๊ฒฐ ๊ฒฝํ—˜
๐Ÿ”งํŠธ๋Ÿฌ๋ธ” ์ŠˆํŒ…
Clone this wiki locally