├── .browserslistrc ├── .env ├── .eslintrc.js ├── .gitignore ├── Dockerfile ├── README.md ├── aws ├── Dockerfile └── default.conf ├── babel.config.js ├── docker-compose.aws.yml ├── docker-compose.yml ├── package-lock.json ├── package.json ├── postcss.config.js ├── public ├── favicon.ico └── index.html ├── server ├── app.js ├── chat_namespace │ ├── events.js │ └── index.js ├── config │ └── index.js ├── index.js ├── redis │ └── index.js └── routes │ ├── room.js │ └── user.js ├── src ├── App.vue ├── assets │ ├── bck.jpg │ ├── local_env.png │ ├── logo.png │ ├── msg_bck.png │ └── scaling.png ├── components │ ├── ChatArea.vue │ ├── ChatDialog.vue │ ├── MessageArea.vue │ ├── UserList.vue │ ├── VideoArea.vue │ ├── conference │ │ └── Conference.vue │ └── video │ │ ├── AudioVideoControls.vue │ │ └── Video.vue ├── main.js ├── mixins │ └── WebRTC.js ├── router │ └── index.js ├── store │ └── index.js ├── styles │ ├── app.scss │ └── variables.scss ├── utils │ ├── ICEServers.js │ ├── config.js │ └── logging.js └── views │ ├── Chat.vue │ └── Home.vue └── vue.config.js /.browserslistrc: -------------------------------------------------------------------------------- 1 | > 1% 2 | last 2 versions 3 | not ie <= 8 4 | -------------------------------------------------------------------------------- /.env: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adrigardi90/video-chat/757b31d1db67727c2d5b15dd05b95edf78bdc863/.env -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | env: { 4 | node: true 5 | }, 6 | 'extends': [ 7 | 'plugin:vue/essential', 8 | 'eslint:recommended' 9 | ], 10 | rules: { 11 | 'no-console': process.env.NODE_ENV === 'production' ? 'off' : 'off', 12 | 'no-debugger': process.env.NODE_ENV === 'production' ? 'error' : 'off' 13 | }, 14 | parserOptions: { 15 | parser: 'babel-eslint' 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | /dist 4 | 5 | # local env files 6 | .env.local 7 | .env.*.local 8 | 9 | # Log files 10 | npm-debug.log* 11 | yarn-debug.log* 12 | yarn-error.log* 13 | 14 | # Editor directories and files 15 | .idea 16 | .vscode 17 | *.suo 18 | *.ntvs* 19 | *.njsproj 20 | *.sln 21 | *.sw* 22 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:8.6 as build 2 | 3 | WORKDIR /videochat 4 | COPY package.json /videochat/ 5 | RUN npm install 6 | 7 | COPY ./ /videochat 8 | 9 | ARG VUE_APP_SOCKET_HOST=NOT_SET 10 | ARG VUE_APP_SOCKET_PORT=NOT_SET 11 | 12 | RUN export VUE_APP_SOCKET_HOST=${VUE_APP_SOCKET_HOST} VUE_APP_SOCKET_PORT=${VUE_APP_SOCKET_PORT} && npm run build 13 | 14 | CMD ["npm", "run", "run:server"] -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # video-chat 2 | > Chat application with **1 to 1** and **many to many** video functionality using [VueJS](https://vuejs.org), [Vuex](https://vuex.vuejs.org), [WebRTC](https://webrtc.org/start/), [SocketIO](https://socket.io),NodeJS and [Redis](https://github.com/NodeRedis/node_redis) 3 | 4 | ## Quick start 5 | First of all, you need to install and run the redis in your PC. Here there is an [article](https://medium.com/@petehouston/install-and-config-redis-on-mac-os-x-via-homebrew-eb8df9a4f298) for Mac OS X. Once Redis is up and running: 6 | 7 | ```bash 8 | # Clone the repo 9 | git clone https://github.com/adrigardi90/video-chat 10 | 11 | # Change into the repo directory 12 | cd video-chat 13 | 14 | # install 15 | npm install 16 | 17 | # Start the FE in dev mode 18 | npm run serve 19 | 20 | # Start the server 21 | npm run run:server 22 | 23 | ``` 24 | Then visit http://localhost:8080 in your browser 25 | 26 | ## Horizontal scaling 27 | To test out the horizontal scaling we'll create 3 different instances. Each one running a unique nodeJS process serving the FE and exposing the API 28 | 29 |

30 | scaling 31 |

32 | 33 | 34 | ```bash 35 | # Build the images 36 | docker-compose -f docker-compose.yml build 37 | 38 | # Create and run the three instances 39 | docker-compose -f docker-compose.yml up 40 | 41 | ``` 42 | 43 | The webapp will be exposed on http://localhost:3000, http://localhost:3001 and http://localhost:3002, each one with a different socket connection 44 | 45 | -------------------------------------------------------------------------------- /aws/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:8.6 as build 2 | 3 | WORKDIR /videochat 4 | COPY package.json /videochat/ 5 | COPY /aws/default.conf /etc/nginx/conf.d 6 | RUN npm install 7 | 8 | COPY /server /videochat/server 9 | 10 | CMD ["npm", "run", "run:server"] 11 | -------------------------------------------------------------------------------- /aws/default.conf: -------------------------------------------------------------------------------- 1 | server { 2 | listen 80; 3 | 4 | add_header Access-Control-Allow-Origin *; 5 | 6 | location / { 7 | proxy_pass http://localhost:4000; 8 | proxy_http_version 1.1; 9 | proxy_set_header Upgrade $http_upgrade; 10 | proxy_set_header Host $host; 11 | proxy_cache_bypass $http_upgrade; 12 | } 13 | } -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [ 3 | '@vue/app' 4 | ] 5 | } -------------------------------------------------------------------------------- /docker-compose.aws.yml: -------------------------------------------------------------------------------- 1 | version: '3' 2 | 3 | services: 4 | 5 | redis: 6 | image: redis:4.0.5-alpine 7 | networks: 8 | - video-chat 9 | ports: 10 | - 6379:6379 11 | expose: 12 | - "6379" 13 | restart: always 14 | command: ["redis-server", "--appendonly", "yes"] 15 | 16 | chat-service: 17 | build: 18 | context: . 19 | dockerfile: ./aws/Dockerfile 20 | ports: 21 | - 4000:4000 22 | networks: 23 | - video-chat 24 | depends_on: 25 | - redis 26 | environment: 27 | PORT: 4000 28 | REDIS_HOST: redis 29 | REDIS_PORT: 6379 30 | 31 | networks: 32 | video-chat: -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3' 2 | 3 | services: 4 | 5 | redis: 6 | image: redis:4.0.5-alpine 7 | networks: 8 | - video-chat 9 | ports: 10 | - 6379:6379 11 | expose: 12 | - "6379" 13 | restart: always 14 | command: ["redis-server", "--appendonly", "yes"] 15 | 16 | # Copy 1 17 | chat1: 18 | build: 19 | context: . 20 | args: 21 | VUE_APP_SOCKET_HOST: http://localhost 22 | VUE_APP_SOCKET_PORT: 3000 23 | ports: 24 | - 3000:3000 25 | networks: 26 | - video-chat 27 | depends_on: 28 | - redis 29 | environment: 30 | PORT: 3000 31 | REDIS_HOST: redis 32 | REDIS_PORT: 6379 33 | 34 | # Copy 2 35 | chat2: 36 | build: 37 | context: . 38 | args: 39 | VUE_APP_SOCKET_HOST: http://localhost 40 | VUE_APP_SOCKET_PORT: 3001 41 | ports: 42 | - 3001:3001 43 | networks: 44 | - video-chat 45 | depends_on: 46 | - redis 47 | environment: 48 | PORT: 3001 49 | REDIS_HOST: redis 50 | REDIS_PORT: 6379 51 | 52 | # Copy 3 53 | chat3: 54 | build: 55 | context: . 56 | args: 57 | VUE_APP_SOCKET_HOST: http://localhost 58 | VUE_APP_SOCKET_PORT: 3002 59 | ports: 60 | - 3002:3002 61 | networks: 62 | - video-chat 63 | depends_on: 64 | - redis 65 | environment: 66 | PORT: 3002 67 | REDIS_HOST: redis 68 | REDIS_PORT: 6379 69 | 70 | networks: 71 | video-chat: -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "video-chat", 3 | "description" : "Chat with 1 to 1 and many to many video functionality", 4 | "version": "0.1.0", 5 | "private": true, 6 | "scripts": { 7 | "serve": "vue-cli-service serve", 8 | "run:server": "node ./server/index.js", 9 | "debug:server": "node --inspect=0.0.0.0:9229 ./server/index.js", 10 | "build": "vue-cli-service build", 11 | "lint": "vue-cli-service lint" 12 | }, 13 | "dependencies": { 14 | "body-parser": "^1.18.3", 15 | "cors": "^2.8.5", 16 | "express": "^4.16.4", 17 | "http": "0.0.0", 18 | "path": "^0.12.7", 19 | "socket.io": "^2.2.0", 20 | "socket.io-client": "^2.3.0", 21 | "socket.io-redis": "^5.2.0", 22 | "vue": "^2.5.21", 23 | "vue-material": "^1.0.0-beta-10.2", 24 | "vue-resource": "^1.5.1", 25 | "vue-router": "^3.0.1", 26 | "vue-socket.io": "^3.0.5", 27 | "vue-toastr": "^2.1.2", 28 | "vuex": "^3.0.1", 29 | "webrtc-adapter": "^7.5.1" 30 | }, 31 | "devDependencies": { 32 | "@vue/cli-plugin-babel": "^3.3.0", 33 | "@vue/cli-plugin-eslint": "^3.3.0", 34 | "@vue/cli-service": "^3.3.0", 35 | "babel-eslint": "^10.0.1", 36 | "eslint": "^5.8.0", 37 | "eslint-plugin-vue": "^5.0.0", 38 | "node-sass": "^4.9.0", 39 | "sass-loader": "^7.0.1", 40 | "vue-template-compiler": "^2.5.21" 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | autoprefixer: {} 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adrigardi90/video-chat/757b31d1db67727c2d5b15dd05b95edf78bdc863/public/favicon.ico -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | video 12 | 13 | 14 | 17 |
18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /server/app.js: -------------------------------------------------------------------------------- 1 | 2 | const express = require('express') 3 | const app = express() 4 | const io = app.io = require('socket.io')() 5 | const cors = require('cors') 6 | const bodyParser = require('body-parser') 7 | const path = require('path') 8 | 9 | const users = require('./routes/user') 10 | const rooms = require('./routes/room') 11 | const chat = require('./chat_namespace') 12 | 13 | app.use(cors()) 14 | app.use(bodyParser.json()) 15 | 16 | /** 17 | * Middleware 18 | */ 19 | app.use((req, res, next) => { 20 | console.log('Time: ', Date.now()) 21 | next() 22 | }) 23 | 24 | /** 25 | * Routing 26 | */ 27 | app.use('/auth', users) 28 | app.use('/rooms', rooms) 29 | 30 | // Static routing 31 | app.use(express.static(path.join(__dirname, '../dist'))) 32 | 33 | /** 34 | * Chat socket namespace 35 | */ 36 | chat.createNameSpace(io) 37 | 38 | 39 | module.exports = app 40 | -------------------------------------------------------------------------------- /server/chat_namespace/events.js: -------------------------------------------------------------------------------- 1 | 2 | const ChatRedis = require('../redis') 3 | 4 | const changeStatus = (socket, namespace) => async ({ username, status, room }) => { 5 | console.log(`User "${username}" wants to change his status to ${status}`) 6 | 7 | try { 8 | const user = await ChatRedis.getUser(room, username) 9 | await ChatRedis.setUser(room, username, { ...user, status }) 10 | const users = await ChatRedis.getUsers(room) 11 | // Notify all the users in the same room 12 | namespace.in(room).emit('newUser', { users, username }) 13 | } catch (error) { 14 | console.log(error) 15 | } 16 | } 17 | 18 | const publicMessage = (namespace) => ({ room, message, username }) => { 19 | namespace.in(room).emit('newMessage', { message, username }) 20 | } 21 | 22 | const conferenceInvitation = (namespace) => async ({ room, to, from }) => { 23 | console.log(`Conference - Invitation from "${from}" to "${to}" in room ${room}`) 24 | try { 25 | const { privateChat, conference } = await ChatRedis.getUser(room, to) 26 | // User already talking 27 | if (privateChat || conference) { 28 | console.log(`Conference - User "${to}" is already talking. PrivateChat: ${privateChat} - Conference: ${conference}`) 29 | return namespace.to(from).emit('conferenceInvitation', { message: `User ${to} is already talking`, from }) 30 | } 31 | namespace.in(room).emit('conferenceInvitation', { room, to, from }) 32 | } catch (error) { 33 | console.log(error) 34 | } 35 | } 36 | 37 | const joinConference = (socket, namespace) => ({ username, room, to, from }) => { 38 | const admin = username === to 39 | console.log(admin 40 | ? `Conference - User "${username}" wants to open a conference room` 41 | : `Conference - User "${username}" wants to join the "${to}" conference`) 42 | 43 | // Join the room 44 | socket.join(to, async () => { 45 | if (!room) return 46 | 47 | try { 48 | const user = await ChatRedis.getUser(room, username) 49 | await ChatRedis.setUser(room, username, { ...user, conference: to }) 50 | console.log(admin 51 | ? `Conference - User "${username}" opened a conference` 52 | : `Conference - User "${username}" joined the "${to}" conference`) 53 | namespace.in(to).emit('joinConference', { username, to, room, from }) 54 | } catch (error) { 55 | console.log(error) 56 | } 57 | }) 58 | } 59 | 60 | const leaveConference = (socket, namespace) => async ({ room, from, conferenceRoom }) => { 61 | console.log(`Conference - User "${from}" wants to leave the conference room ${room}`) 62 | 63 | try { 64 | const user = await ChatRedis.getUser(room, from) 65 | await ChatRedis.setUser(room, from, { ...user, conference: false }) 66 | socket.leave(conferenceRoom, () => { 67 | namespace.to(conferenceRoom).emit('leaveConference', { room, from }) 68 | console.log(`Conference - User ${from} left the conference room ${room}`) 69 | }) 70 | } catch (error) { 71 | console.log(error) 72 | } 73 | } 74 | 75 | const PCSignalingConference = (namespace) => ({ desc, to, from, room, candidate }) => { 76 | candidate 77 | ? console.log(`Conference - User "${from}" sends a candidate to "${to}"`) 78 | : console.log(`Conference - User "${from}" sends a ${from === room ? 'offer' : 'answer'} to "${to}"`) 79 | namespace.to(room).emit('PCSignalingConference', { desc, to, from, candidate }) 80 | } 81 | 82 | 83 | const leaveRoom = (socket, namespace) => ({ room, username }) => { 84 | console.log(`Room - User "${username}" wants to leave the room ${room}`) 85 | 86 | socket.leave(room, async () => { 87 | console.log(`Room - User "${username}" left the room ${room}`) 88 | 89 | try { 90 | await ChatRedis.delUser(room, username) 91 | const users = await ChatRedis.getUsers(room) 92 | // Notify all the users in the same room 93 | namespace.in(room).emit('newUser', { users, username }) 94 | } catch (error) { 95 | console.log(error) 96 | } 97 | }) 98 | } 99 | 100 | const leaveChat = (socket, namespace) => async ({ room, username }) => { 101 | console.log(`User "${username}" wants to leave the chat`) 102 | 103 | try { 104 | await ChatRedis.delUser(room, username) 105 | const users = await ChatRedis.getUsers(room) 106 | // Leave the socket 107 | socket.leave(room, () => { 108 | console.log(`User "${username}" left the room ${room}`) 109 | // Notify all the users in the same room 110 | namespace.in(room).emit('leaveChat', { users, message: `${username} left the room`}) 111 | }) 112 | } catch (error) { 113 | console.log(error) 114 | } 115 | } 116 | 117 | const joinPrivateRoom = (socket, namespace) => ({ username, room, to, from, joinConfirmation }) => { 118 | console.log(`Private chat - User "${username}" ${!joinConfirmation 119 | ? 'wants to have a' : 'accept the private'} chat with with "${to}"`) 120 | 121 | // Join the room 122 | socket.join(to, async () => { 123 | if (!room) return 124 | 125 | try { 126 | const { privateChat } = await ChatRedis.getUser(room, to) 127 | if (!!privateChat && privateChat !== username) { 128 | namespace.to(to).emit('leavePrivateRoom', { 129 | to, room, 130 | privateMessage: `${to} is already talking`, 131 | from: username, 132 | }) 133 | // Leave the room 134 | socket.leave(to, () => console.log(`Private chat - User "${username}" forced to leave the room "${to}"`)) 135 | return 136 | } 137 | 138 | const user = await ChatRedis.getUser(room, username) 139 | await ChatRedis.setUser(room, username, { ...user, privateChat: to }) 140 | if (!joinConfirmation) namespace.in(room).emit('privateChat', { username, to, room, from }) 141 | } catch (error) { 142 | console.log(error) 143 | } 144 | }) 145 | } 146 | 147 | const leavePrivateRoom = (socket, namespace) => async ({ room, from, to }) => { 148 | console.log(`Private chat - User "${from}" wants to leave the private chat with "${to}"`) 149 | 150 | try { 151 | const user = await ChatRedis.getUser(room, from) 152 | await ChatRedis.setUser(room, from, { ...user, privateChat: false }) 153 | socket.leave(to, () => { 154 | console.log(`Private chat - User "${from}" left the private chat with "${to}"`) 155 | namespace.to(to).emit('leavePrivateRoom', { 156 | to, from, 157 | privateMessage: `${from} has closed the chat`, 158 | }) 159 | }) 160 | } catch (error) { 161 | console.log(error) 162 | } 163 | } 164 | 165 | const privateMessage = (namespace) => ({ privateMessage, to, from, room }) => { 166 | console.log(`Private chat - User "${from}" sends a private message to "${to}"`) 167 | // Private message to the user 168 | namespace.to(room).emit('privateMessage', { to, privateMessage, from, room }) 169 | } 170 | 171 | const privateMessagePCSignaling = (namespace) => ({ desc, to, from, room, candidate }) => { 172 | candidate 173 | ? console.log(`Private chat - User "${from}" sends a candidate to "${to}"`) 174 | : console.log(`Private chat - User "${from}" sends a ${from !== room ? 'offer' : 'answer'} to "${to}"`) 175 | // Private signaling to the user 176 | namespace.to(room).emit('privateMessagePCSignaling', { desc, to, from, candidate }) 177 | } 178 | 179 | module.exports = { 180 | publicMessage, 181 | leaveRoom, 182 | joinPrivateRoom, 183 | leavePrivateRoom, 184 | privateMessage, 185 | privateMessagePCSignaling, 186 | leaveChat, 187 | changeStatus, 188 | conferenceInvitation, 189 | joinConference, 190 | leaveConference, 191 | PCSignalingConference 192 | } -------------------------------------------------------------------------------- /server/chat_namespace/index.js: -------------------------------------------------------------------------------- 1 | const events = require('./events.js') 2 | const config = require('./../config') 3 | const ChatRedis = require('../redis') 4 | 5 | // Socket namespace 6 | let namespace 7 | 8 | // When connecting 9 | const onConnection = (socket) => { 10 | console.log(`Socket connected to port ${config.PORT}`) 11 | let userRoom, userName 12 | 13 | // Listening for joining a room 14 | socket.on('joinRoom', ({ username, room, status }) => { 15 | console.log(`User ${username} wants to join the room ${room}`) 16 | 17 | // Join the room 18 | socket.join(room, async () => { 19 | console.log(`User ${username} joined the room ${room}`) 20 | // We implement here the listener to save the room where the user is 21 | userRoom = room 22 | userName = username 23 | 24 | try { 25 | // add user for the suitable ROOM 26 | await ChatRedis.addUser(room, userName, { username, status, privateChat: false, conference: false }) 27 | const users = await ChatRedis.getUsers(room) 28 | // Notify all the users in the same room 29 | namespace.in(room).emit('newUser', { users, username }) 30 | } catch (error) { 31 | console.log(error) 32 | } 33 | }) 34 | 35 | }) 36 | 37 | // Listening for new public messages 38 | socket.on('publicMessage', events.publicMessage(namespace)) 39 | socket.on('conferenceInvitation', events.conferenceInvitation(namespace)) 40 | // Leave room 41 | socket.on('leaveRoom', events.leaveRoom(socket, namespace)) 42 | // Leave room 43 | socket.on('leaveChat', events.leaveChat(socket, namespace)) 44 | // Listening for private chats 45 | socket.on('joinPrivateRoom', events.joinPrivateRoom(socket, namespace)) 46 | socket.on('joinConference', events.joinConference(socket, namespace)) 47 | // Leave private chat 48 | socket.on('leavePrivateRoom', events.leavePrivateRoom(socket, namespace)) 49 | socket.on('leaveConference', events.leaveConference(socket, namespace)) 50 | // Private message listener 51 | socket.on('privateMessage', events.privateMessage(namespace)) 52 | // Private message for Signaling PeerConnection 53 | socket.on('privateMessagePCSignaling', events.privateMessagePCSignaling(namespace)) 54 | socket.on('PCSignalingConference', events.PCSignalingConference(namespace)) 55 | // Set status 56 | socket.on('changeStatus', events.changeStatus(socket, namespace)) 57 | 58 | // Disconnect 59 | socket.on('disconnect', async () => { 60 | console.log(`User "${userName}" with socket ${socket.id} disconnected`) 61 | try { 62 | await ChatRedis.delUser(userName, config.KEY) 63 | events.leaveChat(socket, namespace)({ 64 | room: userRoom, 65 | username: userName 66 | }) 67 | } catch (error) { 68 | console.log(error) 69 | } 70 | }) 71 | 72 | } 73 | 74 | exports.createNameSpace = (io) => { 75 | exports.io = io 76 | namespace = io 77 | .of(config.CHAT_NAMESPACE) 78 | .on('connection', onConnection) 79 | } -------------------------------------------------------------------------------- /server/config/index.js: -------------------------------------------------------------------------------- 1 | 2 | const CONFIG = { 3 | PORT: process.env.PORT || 3000, 4 | CHAT_NAMESPACE: '/video-chat', 5 | REDIS_HOST: process.env.REDIS_HOST || 'localhost', 6 | REDIS_PORT: process.env.REDIS_PORT || 6379, 7 | ORIGINS: process.env.ORIGINS || '*:*', 8 | KEY: 'unique' 9 | } 10 | 11 | module.exports = CONFIG -------------------------------------------------------------------------------- /server/index.js: -------------------------------------------------------------------------------- 1 | const http = require('http') 2 | const redis = require('socket.io-redis') 3 | 4 | const app = require('./app') 5 | const config = require('./config') 6 | 7 | // Server 8 | const server = http.createServer(app) 9 | 10 | // Atach server to the socket 11 | app.io.attach(server) 12 | 13 | // Origin socket configuration 14 | app.io.origins([config.ORIGINS]) 15 | 16 | // Using the adapter to pass event between nodes 17 | app.io.adapter(redis({ 18 | host: config.REDIS_HOST, 19 | port: config.REDIS_PORT 20 | })) 21 | 22 | server.listen(config.PORT, () => { 23 | console.log(`Server Listening on port ${config.PORT}`) 24 | }) 25 | -------------------------------------------------------------------------------- /server/redis/index.js: -------------------------------------------------------------------------------- 1 | const redis = require('redis') 2 | const bluebird = require('bluebird') 3 | 4 | const config = require('./../config/') 5 | 6 | // Using promises 7 | bluebird.promisifyAll(redis) 8 | 9 | function ChatRedis() { 10 | this.client = redis.createClient({ 11 | host: config.REDIS_HOST 12 | }) 13 | } 14 | 15 | /** 16 | * Add user with hash 17 | * @param {} room 18 | * @param {} socketId 19 | * @param {} userObject 20 | */ 21 | ChatRedis.prototype.addUser = function (room, socketId, userObject) { 22 | this.client 23 | .hsetAsync(room, socketId, JSON.stringify(userObject)) 24 | .then( 25 | () => console.debug('addUser ', userObject.username + ' added to the room ' + room), 26 | err => console.log('addUser', err) 27 | ) 28 | } 29 | 30 | /** 31 | * Get all users by room 32 | * @param {} room 33 | */ 34 | ChatRedis.prototype.getUsers = function (room) { 35 | return this.client 36 | .hgetallAsync(room) 37 | .then(users => { 38 | const userList = [] 39 | for (let user in users) { 40 | userList.push(JSON.parse(users[user])) 41 | } 42 | return userList 43 | }, error => { 44 | console.log('getUsers ', error) 45 | }) 46 | } 47 | 48 | /** 49 | * Delete a user in a room with socketId 50 | * @param {} room 51 | * @param {} socketId 52 | */ 53 | ChatRedis.prototype.delUser = function (room, socketId) { 54 | return this.client 55 | .hdelAsync(room, socketId) 56 | .then( 57 | res => (res), 58 | err => { console.log('delUser ', err) } 59 | ) 60 | } 61 | 62 | /** 63 | * Get user by room and socketId 64 | * @param {} room 65 | * @param {} socketId 66 | */ 67 | ChatRedis.prototype.getUser = function (room, socketId) { 68 | return this.client 69 | .hgetAsync(room, socketId) 70 | .then( 71 | res => JSON.parse(res), 72 | err => { console.log('getUser ', err) } 73 | ) 74 | } 75 | 76 | /** 77 | * Get number of clients connected in a room 78 | */ 79 | ChatRedis.prototype.getClientsInRoom = function (io, namespace, room) { 80 | return new Promise((resolve, reject) => { 81 | io.of(namespace).adapter.clients([room], (err, clients) => { 82 | if (err) reject(err) 83 | resolve(clients.length) 84 | }) 85 | }) 86 | } 87 | 88 | /** 89 | * Set user 90 | * @param {} room 91 | * @param {} socketId 92 | * @param {} newValue 93 | */ 94 | ChatRedis.prototype.setUser = function (room, socketId, newValue) { 95 | return this.client 96 | .hsetAsync(room, socketId, JSON.stringify(newValue)) 97 | .then( 98 | res => res, 99 | err => { console.log('setUser', err) } 100 | ) 101 | } 102 | 103 | module.exports = new ChatRedis() 104 | -------------------------------------------------------------------------------- /server/routes/room.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const roomRouter = express.Router(); 3 | 4 | const rooms = [ 5 | { 6 | id: 1, 7 | name: 'GENERAL' 8 | }, 9 | { 10 | id: 2, 11 | name: 'SPORTS' 12 | }, 13 | { 14 | id: 3, 15 | name: 'GAMES' 16 | }, 17 | ] 18 | 19 | // route for get rooms 20 | roomRouter.get('/', (req,res) => { 21 | res.send(rooms) 22 | }) 23 | 24 | module.exports = roomRouter -------------------------------------------------------------------------------- /server/routes/user.js: -------------------------------------------------------------------------------- 1 | 2 | const express = require('express'); 3 | const userRouter = express.Router() 4 | 5 | const ChatRedis = require('../redis') 6 | const config = require('../config') 7 | 8 | // config.KEY: We just want to store the logged users (username has to be unique) 9 | // so we always use the same key to adapt it to our Redis implementation 10 | 11 | // Login 12 | userRouter.post('/login', (req, res) => { 13 | const newUser = req.body 14 | if (!newUser.username) return res.send({ code: 400, message: 'Data is required' }) 15 | console.log(`Login user ${newUser.username}`) 16 | 17 | ChatRedis 18 | .getUser(newUser.username, config.KEY) 19 | .then(user => { 20 | if (!user) { 21 | ChatRedis.addUser(newUser.username, config.KEY, newUser) 22 | console.log(`User ${newUser.username} logged`) 23 | return res.send({ code: 200, message: 'Logged in succesfully' }) 24 | } 25 | 26 | console.log(`User ${newUser.username} already exists`) 27 | return res.send({ code: 401, message: 'Username already exists' }) 28 | }) 29 | }) 30 | 31 | // Logout 32 | userRouter.post('/logout', (req, res) => { 33 | const user = req.body 34 | console.log(`Logout user ${user.username}`) 35 | 36 | ChatRedis 37 | .delUser(user.username, config.KEY) 38 | .then(data => { 39 | if (!data) 40 | return res.send({ code: 400, message: 'User not found' }) 41 | 42 | return res.send({ code: 200, message: 'Logged in succesfully' }) 43 | }) 44 | }) 45 | 46 | 47 | module.exports = userRouter -------------------------------------------------------------------------------- /src/App.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 26 | -------------------------------------------------------------------------------- /src/assets/bck.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adrigardi90/video-chat/757b31d1db67727c2d5b15dd05b95edf78bdc863/src/assets/bck.jpg -------------------------------------------------------------------------------- /src/assets/local_env.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adrigardi90/video-chat/757b31d1db67727c2d5b15dd05b95edf78bdc863/src/assets/local_env.png -------------------------------------------------------------------------------- /src/assets/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adrigardi90/video-chat/757b31d1db67727c2d5b15dd05b95edf78bdc863/src/assets/logo.png -------------------------------------------------------------------------------- /src/assets/msg_bck.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adrigardi90/video-chat/757b31d1db67727c2d5b15dd05b95edf78bdc863/src/assets/msg_bck.png -------------------------------------------------------------------------------- /src/assets/scaling.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adrigardi90/video-chat/757b31d1db67727c2d5b15dd05b95edf78bdc863/src/assets/scaling.png -------------------------------------------------------------------------------- /src/components/ChatArea.vue: -------------------------------------------------------------------------------- 1 | 17 | 18 | 19 | 64 | 65 | 100 | 101 | -------------------------------------------------------------------------------- /src/components/ChatDialog.vue: -------------------------------------------------------------------------------- 1 | 48 | 49 | 50 | 155 | 156 | 157 | 158 | 203 | 204 | -------------------------------------------------------------------------------- /src/components/MessageArea.vue: -------------------------------------------------------------------------------- 1 | 17 | 18 | 19 | 37 | 38 | 39 | 65 | -------------------------------------------------------------------------------- /src/components/UserList.vue: -------------------------------------------------------------------------------- 1 | 28 | 29 | 30 | 67 | 68 | 121 | 122 | -------------------------------------------------------------------------------- /src/components/VideoArea.vue: -------------------------------------------------------------------------------- 1 | 16 | 17 | 18 | 100 | 101 | 125 | 126 | -------------------------------------------------------------------------------- /src/components/conference/Conference.vue: -------------------------------------------------------------------------------- 1 | 50 | 51 | 149 | 150 | -------------------------------------------------------------------------------- /src/components/video/AudioVideoControls.vue: -------------------------------------------------------------------------------- 1 | 15 | 16 | 39 | 40 | -------------------------------------------------------------------------------- /src/components/video/Video.vue: -------------------------------------------------------------------------------- 1 | 29 | 30 | 48 | 49 | -------------------------------------------------------------------------------- /src/main.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import App from './App.vue' 3 | import router from './router' 4 | import store from './store' 5 | import VueSocketIO from 'vue-socket.io' 6 | import io from 'socket.io-client' 7 | import VueResource from 'vue-resource' 8 | import './styles/app.scss' 9 | import { url } from './utils/config' 10 | import adapter from 'webrtc-adapter' 11 | 12 | console.log(`Browser ${adapter.browserDetails.browser} - version ${adapter.browserDetails.version}`) 13 | 14 | // Socket config 15 | Vue.use(new VueSocketIO({ 16 | debug: true, 17 | connection: io(`${url}/video-chat`, { autoConnect: false }), 18 | vuex: { 19 | store, 20 | actionPrefix: 'SOCKET_', 21 | mutationPrefix: 'SOCKET_' 22 | }, 23 | })) 24 | 25 | // Vue resource for http 26 | Vue.use(VueResource) 27 | 28 | Vue.config.productionTip = false 29 | 30 | new Vue({ 31 | router, 32 | store, 33 | render: h => h(App) 34 | }).$mount('#app') 35 | -------------------------------------------------------------------------------- /src/mixins/WebRTC.js: -------------------------------------------------------------------------------- 1 | import { log } from "../utils/logging" 2 | import { servers } from '../utils/ICEServers' 3 | import { WS_EVENTS, DESCRIPTION_TYPE } from '../utils/config' 4 | 5 | export const videoConfiguration = { 6 | data() { 7 | return { 8 | // Media config 9 | constraints: { 10 | audio: { 11 | echoCancellation: true, 12 | noiseSuppression: true, 13 | autoGainControl: false 14 | }, 15 | video: { 16 | width: 400, 17 | height: 250 18 | }, 19 | }, 20 | // TURN/STUN ice servers 21 | configuration: servers, 22 | // Offer config 23 | offerOptions: { 24 | offerToReceiveAudio: 1, 25 | offerToReceiveVideo: 1 26 | }, 27 | 28 | // Local video 29 | myVideo: undefined, 30 | localStream: undefined, 31 | username: "" 32 | } 33 | }, 34 | 35 | created() { 36 | this.username = this.$store.state.username 37 | }, 38 | beforeDestroy() { 39 | this.localStream.getTracks().forEach(track => track.stop()) 40 | }, 41 | 42 | methods: { 43 | async getUserMedia() { 44 | log(`Requesting ${this.username} video stream`) 45 | 46 | if ("mediaDevices" in navigator) { 47 | try { 48 | const stream = await navigator.mediaDevices.getUserMedia(this.constraints) 49 | this.myVideo.srcObject = stream 50 | this.myVideo.volume = 0 51 | this.localStream = stream 52 | } catch (error) { 53 | log(`getUserMedia error: ${error}`) 54 | } 55 | } 56 | }, 57 | getAudioVideo() { 58 | const video = this.localStream.getVideoTracks() 59 | const audio = this.localStream.getAudioTracks() 60 | 61 | if (video.length > 0) log(`Using video device: ${video[0].label}`) 62 | if (audio.length > 0) log(`Using audio device: ${audio[0].label}`) 63 | }, 64 | async setRemoteDescription(remoteDesc, pc) { 65 | try { 66 | log(`${this.username} setRemoteDescription: start`) 67 | await pc.setRemoteDescription(remoteDesc) 68 | log(`${this.username} setRemoteDescription: finished`) 69 | } catch (error) { 70 | log(`Error setting the RemoteDescription in ${this.username}. Error: ${error}`) 71 | } 72 | }, 73 | async createOffer(pc, to, room, conference = false) { 74 | log(`${this.username} wants to start a call with ${to}`) 75 | try { 76 | const offer = await pc.createOffer(this.offerOptions) 77 | log(`${this.username} setLocalDescription: start`) 78 | await pc.setLocalDescription(offer) 79 | log(`${this.username} setLocalDescription: finished`) 80 | this.sendSignalingMessage(pc.localDescription, true, to, room, conference) 81 | } catch (error) { 82 | log(`Error creating the offer from ${this.username}. Error: ${error}`) 83 | } 84 | }, 85 | async createAnswer(pc, to, room, conference) { 86 | log(`${this.username} create an answer: start`) 87 | try { 88 | const answer = await pc.createAnswer() 89 | log(`${this.username} setLocalDescription: start`) 90 | await pc.setLocalDescription(answer) 91 | log(`${this.username} setLocalDescription: finished`) 92 | this.sendSignalingMessage(pc.localDescription, false, to, room, conference) 93 | } catch (error) { 94 | log(`Error creating the answer from ${this.username}. Error: ${error}`) 95 | } 96 | }, 97 | async handleAnswer(desc, pc, from, room, conference = false) { 98 | log(`${this.username} gets an offer from ${from}`) 99 | await this.setRemoteDescription(desc, pc) 100 | this.createAnswer(pc, from, room, conference) 101 | }, 102 | sendSignalingMessage(desc, offer, to, room, conference) { 103 | const isOffer = offer ? DESCRIPTION_TYPE.offer : DESCRIPTION_TYPE.answer 104 | log(`${this.username} sends the ${isOffer} through the signal channel to ${to} in room ${room}`) 105 | 106 | // send the offer to the other peer 107 | this.$socket.emit(conference ? WS_EVENTS.PCSignalingConference : WS_EVENTS.privateMessagePCSignaling, { 108 | desc: desc, 109 | to: to, 110 | from: this.username, 111 | room: room, 112 | }) 113 | }, 114 | addLocalStream(pc) { 115 | pc.addStream(this.localStream) 116 | }, 117 | addCandidate(pc, candidate) { 118 | try { 119 | log(`${this.username} added a candidate`) 120 | pc.addIceCandidate(candidate) 121 | } catch (error) { 122 | log(`Error adding a candidate in ${this.username}. Error: ${error}`) 123 | } 124 | }, 125 | onIceCandidates(pc, to, room, conference = false) { 126 | pc.onicecandidate = ({ candidate }) => { 127 | if (!candidate) return 128 | setTimeout(() => { 129 | this.$socket.emit(conference ? WS_EVENTS.PCSignalingConference : WS_EVENTS.privateMessagePCSignaling, { 130 | candidate, 131 | to: to, 132 | from: this.username, 133 | room: room, 134 | }) 135 | }, 500) 136 | } 137 | }, 138 | onAddStream(user, video) { 139 | user.pc.onaddstream = event => { 140 | user.peerVideo = user.peerVideo || document.getElementById(video) 141 | if (!user.peerVideo.srcObject && event.stream) { 142 | user.peerStream = event.stream 143 | user.peerVideo.srcObject = user.peerStream 144 | } 145 | } 146 | }, 147 | pauseVideo() { 148 | this.localStream.getVideoTracks().forEach(t => (t.enabled = !t.enabled)) 149 | }, 150 | pauseAudio() { 151 | this.localStream.getAudioTracks().forEach(t => (t.enabled = !t.enabled)) 152 | }, 153 | }, 154 | } -------------------------------------------------------------------------------- /src/router/index.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import Router from 'vue-router' 3 | import VueMaterial from 'vue-material' 4 | import 'vue-material/dist/vue-material.min.css' 5 | import 'vue-material/dist/theme/default.css' // This line here 6 | import VueToastr from "vue-toastr" 7 | 8 | import Home from './../views/Home.vue' 9 | import store from '../store' 10 | 11 | Vue.use(VueMaterial) 12 | Vue.use(VueToastr, { 13 | defaultPosition: "toast-top-left", 14 | defaultTimeout: 3000, 15 | defaultProgressBar: false, 16 | defaultProgressBarValue: 0, 17 | }) 18 | Vue.use(Router) 19 | 20 | export default new Router({ 21 | routes: [ 22 | { 23 | path: '/', 24 | name: 'home', 25 | component: Home, 26 | beforeEnter: (to, from, next) => { 27 | store.state.room && store.state.username ? next('/chat') : next() 28 | } 29 | }, 30 | { 31 | path: '/chat', 32 | name: 'chat', 33 | // route level code-splitting 34 | // this generates a separate chunk (about.[hash].js) for this route 35 | // which is lazy-loaded when the route is visited. 36 | component: () => import(/* webpackChunkName: "about" */ './../views/Chat.vue'), 37 | beforeEnter: (to, from, next) => { 38 | !store.state.room && !store.state.username ? next('/') : next() 39 | } 40 | } 41 | ] 42 | }) 43 | -------------------------------------------------------------------------------- /src/store/index.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import Vuex from 'vuex' 3 | import { STATUS_OPTIONS, url, STORE_ACTIONS } from '../utils/config' 4 | 5 | Vue.use(Vuex) 6 | 7 | export default new Vuex.Store({ 8 | state: { 9 | room: undefined, 10 | username: undefined, 11 | status: STATUS_OPTIONS.available, 12 | rooms: [] 13 | }, 14 | mutations: { 15 | joinRoom(state, { room, username }) { 16 | state.room = room 17 | state.username = username 18 | }, 19 | changeRoom(state, room) { 20 | state.room = room 21 | }, 22 | setRooms(state, rooms) { 23 | state.rooms = rooms 24 | }, 25 | leaveChat(state) { 26 | state.room = undefined 27 | state.username = undefined 28 | }, 29 | changeStatus(state) { 30 | let nextStatus 31 | if (state.status === STATUS_OPTIONS.available) nextStatus = STATUS_OPTIONS.absent 32 | if (state.status === STATUS_OPTIONS.absent) nextStatus = STATUS_OPTIONS.unavailable 33 | if (state.status === STATUS_OPTIONS.unavailable) nextStatus = STATUS_OPTIONS.available 34 | 35 | state.status = nextStatus 36 | } 37 | }, 38 | actions: { 39 | joinRoom({ commit }, data) { 40 | return new Promise(async (resolve, reject) => { 41 | try { 42 | const { body } = await Vue.http.post(`${url}/auth/login`, data) 43 | if (body.code === 400 || body.code === 401 || body.code === 500) { 44 | reject({ message: body.message }) 45 | } 46 | commit(STORE_ACTIONS.joinRoom, data) 47 | resolve() 48 | } catch (error) { 49 | reject(error) 50 | } 51 | }) 52 | }, 53 | changeRoom({ commit }, room) { 54 | commit(STORE_ACTIONS.changeRoom, room) 55 | }, 56 | setRooms({ commit }) { 57 | return new Promise(async (resolve, reject) => { 58 | try { 59 | // const rooms = await Vue.http.get(`http://${url}/rooms`) 60 | const rooms = [{ 61 | id: 1, 62 | name: 'GENERAL' 63 | }, { 64 | id: 2, 65 | name: 'SPORTS' 66 | },{ 67 | id: 3, 68 | name: 'GAMES' 69 | }, 70 | ] 71 | commit(STORE_ACTIONS.setRooms, rooms) 72 | resolve(rooms) 73 | } catch (error) { 74 | reject(error) 75 | } 76 | }) 77 | }, 78 | leaveChat({ commit }, username) { 79 | return new Promise(async (resolve, reject) => { 80 | try { 81 | const { body : { code } } = await Vue.http.post(`${url}/auth/logout`, { username }) 82 | if (code !== 200) reject() 83 | commit(STORE_ACTIONS.leaveChat) 84 | resolve() 85 | } catch (error) { 86 | reject(error) 87 | } 88 | }) 89 | }, 90 | changeStatus({ commit }) { 91 | commit(STORE_ACTIONS.changeStatus) 92 | } 93 | } 94 | }) 95 | -------------------------------------------------------------------------------- /src/styles/app.scss: -------------------------------------------------------------------------------- 1 | body { 2 | min-height: unset; 3 | overflow: hidden; 4 | } 5 | 6 | textarea { 7 | resize: none; 8 | &:disabled { 9 | cursor: not-allowed; 10 | background: #80808021; 11 | } 12 | } 13 | 14 | // Overwrite material styles 15 | .md-overlay { 16 | position: unset; 17 | top: unset; 18 | right: unset; 19 | } 20 | 21 | .md-dialog { 22 | height: 350px; 23 | bottom: 0; 24 | position: fixed; 25 | right: 0; 26 | top: 100vh; 27 | transform: translate(0%, -50%); 28 | left: unset; 29 | &-container { 30 | border: 1px solid gainsboro; 31 | flex-flow: row; 32 | } 33 | &-actions { 34 | padding: 0; 35 | } 36 | &-content { 37 | padding: 1rem; 38 | background: url('./../assets/msg_bck.png'); 39 | background-size: 100% 100%; 40 | } 41 | } 42 | 43 | .md-button.md-theme-default[disabled] .md-icon-font { 44 | color: rgba(0, 0, 0, 0.38) !important; 45 | color: var(--md-theme-default-icon-disabled-on-background, rgba(0, 0, 0, 0.38)) !important; 46 | } -------------------------------------------------------------------------------- /src/styles/variables.scss: -------------------------------------------------------------------------------- 1 | $main_blue: #3961a5; 2 | $secondary_blue: #486ca9; 3 | 4 | $available_status: #05b105; 5 | $absent_status: #f7bb04; 6 | $unavailable_status: #bb0000; -------------------------------------------------------------------------------- /src/utils/ICEServers.js: -------------------------------------------------------------------------------- 1 | export const servers = { 2 | iceServers: [ 3 | { url: 'stun:stun.services.mozilla.org' }, 4 | { url: "stun:stun01.sipphone.com" }, 5 | { url: "stun:stun.ekiga.net" }, 6 | { url: "stun:stun.fwdnet.net" }, 7 | { url: "stun:stun.ideasip.com" }, 8 | { url: "stun:stun.iptel.org" }, 9 | { url: "stun:stun.rixtelecom.se" }, 10 | { url: "stun:stun.schlund.de" }, 11 | { url: "stun:stun.l.google.com:19302" }, 12 | { url: "stun:stun1.l.google.com:19302" }, 13 | { url: "stun:stun2.l.google.com:19302" }, 14 | { url: "stun:stun3.l.google.com:19302" }, 15 | { url: "stun:stun4.l.google.com:19302" }, 16 | { url: "stun:stunserver.org" }, 17 | { url: "stun:stun.softjoys.com" }, 18 | { url: "stun:stun.voiparound.com" }, 19 | { url: "stun:stun.voipbuster.com" }, 20 | { url: "stun:stun.voipstunt.com" }, 21 | { url: "stun:stun.voxgratia.org" }, 22 | { url: "stun:stun.xten.com" }, 23 | { url: "stun:stun.wtfismyip.com" }, 24 | { url: "stun:stun.1und1.de" }, 25 | { url: "stun:stun.gmx.net" }, 26 | { url: "stun:stun.l.google.com:19305" }, 27 | { url: "stun:stun1.l.google.com:19305" }, 28 | { url: "stun:stun2.l.google.com:19305" }, 29 | { url: "stun:stun3.l.google.com:19305" }, 30 | { url: "stun:stun4.l.google.com:19305" }, 31 | { url: "stun:stun.jappix.com:3478" }, 32 | { url: "stun:stun.services.mozilla.com" }, 33 | { url: "stun:stun.counterpath.com" }, 34 | { url: "stun:stun.stunprotocol.prg" }, 35 | { url: "stun:s1.taraba.net" }, 36 | { url: "stun:s2.taraba.net" }, 37 | { url: "stun:s1.voipstation.jp" }, 38 | { url: "stun:s2.voipstation.jp" }, 39 | { url: "stun:stun.sipnet.net:3478" }, 40 | { url: "stun:stun.sipnet.ru:3478" }, 41 | { url: "stun:stun.stunprotocol.org:3478" }, 42 | { url: 'stun:stun.1und1.de:3478' }, 43 | { url: 'stun:stun.gmx.net:3478' }, 44 | { url: 'stun:stun.l.google.com:19302' }, 45 | { url: 'stun:stun1.l.google.com:19302' }, 46 | { url: 'stun:stun2.l.google.com:19302' }, 47 | { url: 'stun:stun3.l.google.com:19302' }, 48 | { url: 'stun:stun4.l.google.com:19302' }, 49 | { url: 'stun:23.21.150.121:3478' }, 50 | { url: 'stun:iphone-stun.strato-iphone.de:3478' }, 51 | { url: 'stun:numb.viagenie.ca:3478' }, 52 | { url: 'stun:stun.12connect.com:3478' }, 53 | { url: 'stun:stun.12voip.com:3478' }, 54 | { url: 'stun:stun.1und1.de:3478' }, 55 | { url: 'stun:stun.2talk.co.nz:3478' }, 56 | { url: 'stun:stun.2talk.com:3478' }, 57 | { url: 'stun:stun.3clogic.com:3478' }, 58 | { url: 'stun:stun.3cx.com:3478' }, 59 | { url: 'stun:stun.a-mm.tv:3478' }, 60 | { url: 'stun:stun.aa.net.uk:3478' }, 61 | { url: 'stun:stun.acrobits.cz:3478' }, 62 | { url: 'stun:stun.actionvoip.com:3478' }, 63 | { url: 'stun:stun.advfn.com:3478' }, 64 | { url: 'stun:stun.aeta-audio.com:3478' }, 65 | { url: 'stun:stun.aeta.com:3478' }, 66 | { url: 'stun:stun.altar.com.pl:3478' }, 67 | { url: 'stun:stun.annatel.net:3478' }, 68 | { url: 'stun:stun.antisip.com:3478' }, 69 | { url: 'stun:stun.arbuz.ru:3478' }, 70 | { url: 'stun:stun.avigora.fr:3478' }, 71 | { url: 'stun:stun.awa-shima.com:3478' }, 72 | { url: 'stun:stun.b2b2c.ca:3478' }, 73 | { url: 'stun:stun.bahnhof.net:3478' }, 74 | { url: 'stun:stun.barracuda.com:3478' }, 75 | { url: 'stun:stun.bluesip.net:3478' }, 76 | { url: 'stun:stun.bmwgs.cz:3478' }, 77 | { url: 'stun:stun.botonakis.com:3478' }, 78 | { url: 'stun:stun.budgetsip.com:3478' }, 79 | { url: 'stun:stun.cablenet-as.net:3478' }, 80 | { url: 'stun:stun.callromania.ro:3478' }, 81 | { url: 'stun:stun.callwithus.com:3478' }, 82 | { url: 'stun:stun.chathelp.ru:3478' }, 83 | { url: 'stun:stun.cheapvoip.com:3478' }, 84 | { url: 'stun:stun.ciktel.com:3478' }, 85 | { url: 'stun:stun.cloopen.com:3478' }, 86 | { url: 'stun:stun.comfi.com:3478' }, 87 | { url: 'stun:stun.commpeak.com:3478' }, 88 | { url: 'stun:stun.comtube.com:3478' }, 89 | { url: 'stun:stun.comtube.ru:3478' }, 90 | { url: 'stun:stun.cope.es:3478' }, 91 | { url: 'stun:stun.counterpath.com:3478' }, 92 | { url: 'stun:stun.counterpath.net:3478' }, 93 | { url: 'stun:stun.datamanagement.it:3478' }, 94 | { url: 'stun:stun.dcalling.de:3478' }, 95 | { url: 'stun:stun.demos.ru:3478' }, 96 | { url: 'stun:stun.develz.org:3478' }, 97 | { url: 'stun:stun.dingaling.ca:3478' }, 98 | { url: 'stun:stun.doublerobotics.com:3478' }, 99 | { url: 'stun:stun.dus.net:3478' }, 100 | { url: 'stun:stun.easycall.pl:3478' }, 101 | { url: 'stun:stun.easyvoip.com:3478' }, 102 | { url: 'stun:stun.ekiga.net:3478' }, 103 | { url: 'stun:stun.epygi.com:3478' }, 104 | { url: 'stun:stun.etoilediese.fr:3478' }, 105 | { url: 'stun:stun.faktortel.com.au:3478' }, 106 | { url: 'stun:stun.freecall.com:3478' }, 107 | { url: 'stun:stun.freeswitch.org:3478' }, 108 | { url: 'stun:stun.freevoipdeal.com:3478' }, 109 | { url: 'stun:stun.gmx.de:3478' }, 110 | { url: 'stun:stun.gmx.net:3478' }, 111 | { url: 'stun:stun.gradwell.com:3478' }, 112 | { url: 'stun:stun.halonet.pl:3478' }, 113 | { url: 'stun:stun.hellonanu.com:3478' }, 114 | { url: 'stun:stun.hoiio.com:3478' }, 115 | { url: 'stun:stun.hosteurope.de:3478' }, 116 | { url: 'stun:stun.ideasip.com:3478' }, 117 | { url: 'stun:stun.infra.net:3478' }, 118 | { url: 'stun:stun.internetcalls.com:3478' }, 119 | { url: 'stun:stun.intervoip.com:3478' }, 120 | { url: 'stun:stun.ipcomms.net:3478' }, 121 | { url: 'stun:stun.ipfire.org:3478' }, 122 | { url: 'stun:stun.ippi.fr:3478' }, 123 | { url: 'stun:stun.ipshka.com:3478' }, 124 | { url: 'stun:stun.irian.at:3478' }, 125 | { url: 'stun:stun.it1.hr:3478' }, 126 | { url: 'stun:stun.ivao.aero:3478' }, 127 | { url: 'stun:stun.jumblo.com:3478' }, 128 | { url: 'stun:stun.justvoip.com:3478' }, 129 | { url: 'stun:stun.kanet.ru:3478' }, 130 | { url: 'stun:stun.kiwilink.co.nz:3478' }, 131 | { url: 'stun:stun.l.google.com:19302' }, 132 | { url: 'stun:stun.linea7.net:3478' }, 133 | { url: 'stun:stun.linphone.org:3478' }, 134 | { url: 'stun:stun.liveo.fr:3478' }, 135 | { url: 'stun:stun.lowratevoip.com:3478' }, 136 | { url: 'stun:stun.lugosoft.com:3478' }, 137 | { url: 'stun:stun.lundimatin.fr:3478' }, 138 | { url: 'stun:stun.magnet.ie:3478' }, 139 | { url: 'stun:stun.mgn.ru:3478' }, 140 | { url: 'stun:stun.mit.de:3478' }, 141 | { url: 'stun:stun.mitake.com.tw:3478' }, 142 | { url: 'stun:stun.miwifi.com:3478' }, 143 | { url: 'stun:stun.modulus.gr:3478' }, 144 | { url: 'stun:stun.myvoiptraffic.com:3478' }, 145 | { url: 'stun:stun.mywatson.it:3478' }, 146 | { url: 'stun:stun.nas.net:3478' }, 147 | { url: 'stun:stun.neotel.co.za:3478' }, 148 | { url: 'stun:stun.netappel.com:3478' }, 149 | { url: 'stun:stun.netgsm.com.tr:3478' }, 150 | { url: 'stun:stun.nfon.net:3478' }, 151 | { url: 'stun:stun.noblogs.org:3478' }, 152 | { url: 'stun:stun.noc.ams-ix.net:3478' }, 153 | { url: 'stun:stun.nonoh.net:3478' }, 154 | { url: 'stun:stun.nottingham.ac.uk:3478' }, 155 | { url: 'stun:stun.nova.is:3478' }, 156 | { url: 'stun:stun.on.net.mk:3478' }, 157 | { url: 'stun:stun.ooma.com:3478' }, 158 | { url: 'stun:stun.ooonet.ru:3478' }, 159 | { url: 'stun:stun.oriontelekom.rs:3478' }, 160 | { url: 'stun:stun.outland-net.de:3478' }, 161 | { url: 'stun:stun.ozekiphone.com:3478' }, 162 | { url: 'stun:stun.personal-voip.de:3478' }, 163 | { url: 'stun:stun.phone.com:3478' }, 164 | { url: 'stun:stun.pjsip.org:3478' }, 165 | { url: 'stun:stun.poivy.com:3478' }, 166 | { url: 'stun:stun.powerpbx.org:3478' }, 167 | { url: 'stun:stun.powervoip.com:3478' }, 168 | { url: 'stun:stun.ppdi.com:3478' }, 169 | { url: 'stun:stun.qq.com:3478' }, 170 | { url: 'stun:stun.rackco.com:3478' }, 171 | { url: 'stun:stun.rapidnet.de:3478' }, 172 | { url: 'stun:stun.rb-net.com:3478' }, 173 | { url: 'stun:stun.rixtelecom.se:3478' }, 174 | { url: 'stun:stun.rockenstein.de:3478' }, 175 | { url: 'stun:stun.rolmail.net:3478' }, 176 | { url: 'stun:stun.rynga.com:3478' }, 177 | { url: 'stun:stun.schlund.de:3478' }, 178 | { url: 'stun:stun.services.mozilla.com:3478' }, 179 | { url: 'stun:stun.sigmavoip.com:3478' }, 180 | { url: 'stun:stun.sip.us:3478' }, 181 | { url: 'stun:stun.sipdiscount.com:3478' }, 182 | { url: 'stun:stun.sipgate.net:10000' }, 183 | { url: 'stun:stun.sipgate.net:3478' }, 184 | { url: 'stun:stun.siplogin.de:3478' }, 185 | { url: 'stun:stun.sipnet.net:3478' }, 186 | { url: 'stun:stun.sipnet.ru:3478' }, 187 | { url: 'stun:stun.siportal.it:3478' }, 188 | { url: 'stun:stun.sippeer.dk:3478' }, 189 | { url: 'stun:stun.siptraffic.com:3478' }, 190 | { url: 'stun:stun.skylink.ru:3478' }, 191 | { url: 'stun:stun.sma.de:3478' }, 192 | { url: 'stun:stun.smartvoip.com:3478' }, 193 | { url: 'stun:stun.smsdiscount.com:3478' }, 194 | { url: 'stun:stun.snafu.de:3478' }, 195 | { url: 'stun:stun.softjoys.com:3478' }, 196 | { url: 'stun:stun.solcon.nl:3478' }, 197 | { url: 'stun:stun.solnet.ch:3478' }, 198 | { url: 'stun:stun.sonetel.com:3478' }, 199 | { url: 'stun:stun.sonetel.net:3478' }, 200 | { url: 'stun:stun.sovtest.ru:3478' }, 201 | { url: 'stun:stun.speedy.com.ar:3478' }, 202 | { url: 'stun:stun.spokn.com:3478' }, 203 | { url: 'stun:stun.srce.hr:3478' }, 204 | { url: 'stun:stun.ssl7.net:3478' }, 205 | { url: 'stun:stun.stunprotocol.org:3478' }, 206 | { url: 'stun:stun.symform.com:3478' }, 207 | { url: 'stun:stun.symplicity.com:3478' }, 208 | { url: 'stun:stun.t-online.de:3478' }, 209 | { url: 'stun:stun.tagan.ru:3478' }, 210 | { url: 'stun:stun.teachercreated.com:3478' }, 211 | { url: 'stun:stun.tel.lu:3478' }, 212 | { url: 'stun:stun.telbo.com:3478' }, 213 | { url: 'stun:stun.telefacil.com:3478' }, 214 | { url: 'stun:stun.tng.de:3478' }, 215 | { url: 'stun:stun.twt.it:3478' }, 216 | { url: 'stun:stun.u-blox.com:3478' }, 217 | { url: 'stun:stun.ucsb.edu:3478' }, 218 | { url: 'stun:stun.ucw.cz:3478' }, 219 | { url: 'stun:stun.uls.co.za:3478' }, 220 | { url: 'stun:stun.unseen.is:3478' }, 221 | { url: 'stun:stun.usfamily.net:3478' }, 222 | { url: 'stun:stun.veoh.com:3478' }, 223 | { url: 'stun:stun.vidyo.com:3478' }, 224 | { url: 'stun:stun.vipgroup.net:3478' }, 225 | { url: 'stun:stun.viva.gr:3478' }, 226 | { url: 'stun:stun.vivox.com:3478' }, 227 | { url: 'stun:stun.vline.com:3478' }, 228 | { url: 'stun:stun.vo.lu:3478' }, 229 | { url: 'stun:stun.vodafone.ro:3478' }, 230 | { url: 'stun:stun.voicetrading.com:3478' }, 231 | { url: 'stun:stun.voip.aebc.com:3478' }, 232 | { url: 'stun:stun.voip.blackberry.com:3478' }, 233 | { url: 'stun:stun.voip.eutelia.it:3478' }, 234 | { url: 'stun:stun.voiparound.com:3478' }, 235 | { url: 'stun:stun.voipblast.com:3478' }, 236 | { url: 'stun:stun.voipbuster.com:3478' }, 237 | { url: 'stun:stun.voipbusterpro.com:3478' }, 238 | { url: 'stun:stun.voipcheap.co.uk:3478' }, 239 | { url: 'stun:stun.voipcheap.com:3478' }, 240 | { url: 'stun:stun.voipfibre.com:3478' }, 241 | { url: 'stun:stun.voipgain.com:3478' }, 242 | { url: 'stun:stun.voipgate.com:3478' }, 243 | { url: 'stun:stun.voipinfocenter.com:3478' }, 244 | { url: 'stun:stun.voipplanet.nl:3478' }, 245 | { url: 'stun:stun.voippro.com:3478' }, 246 | { url: 'stun:stun.voipraider.com:3478' }, 247 | { url: 'stun:stun.voipstunt.com:3478' }, 248 | { url: 'stun:stun.voipwise.com:3478' }, 249 | { url: 'stun:stun.voipzoom.com:3478' }, 250 | { url: 'stun:stun.vopium.com:3478' }, 251 | { url: 'stun:stun.voxox.com:3478' }, 252 | { url: 'stun:stun.voys.nl:3478' }, 253 | { url: 'stun:stun.voztele.com:3478' }, 254 | { url: 'stun:stun.vyke.com:3478' }, 255 | { url: 'stun:stun.webcalldirect.com:3478' }, 256 | { url: 'stun:stun.whoi.edu:3478' }, 257 | { url: 'stun:stun.wifirst.net:3478' }, 258 | { url: 'stun:stun.wwdl.net:3478' }, 259 | { url: 'stun:stun.xs4all.nl:3478' }, 260 | { url: 'stun:stun.xtratelecom.es:3478' }, 261 | { url: 'stun:stun.yesss.at:3478' }, 262 | { url: 'stun:stun.zadarma.com:3478' }, 263 | { url: 'stun:stun.zadv.com:3478' }, 264 | { url: 'stun:stun.zoiper.com:3478' }, 265 | { url: 'stun:stun1.faktortel.com.au:3478' }, 266 | { url: 'stun:stun1.l.google.com:19302' }, 267 | { url: 'stun:stun1.voiceeclipse.net:3478' }, 268 | { url: 'stun:stun2.l.google.com:19302' }, 269 | { url: 'stun:stun3.l.google.com:19302' }, 270 | { url: 'stun:stun4.l.google.com:19302' }, 271 | { url: 'stun:stunserver.org:3478' }, 272 | { 273 | url: "turn:numb.viagenie.ca", 274 | credential: "muazkh", 275 | username: "webrtc@live.com" 276 | }, 277 | { 278 | url: "turn:192.158.29.39:3478?transport=udp", 279 | credential: "JZEOEt2V3Qb0y27GRntt2u2PAYA=", 280 | username: "28224511:1379330808" 281 | }, 282 | { 283 | url: "turn:192.158.29.39:3478?transport=tcp", 284 | credential: "JZEOEt2V3Qb0y27GRntt2u2PAYA=", 285 | username: "28224511:1379330808" 286 | }, 287 | ], 288 | iceTransportPolicy: "all" 289 | } -------------------------------------------------------------------------------- /src/utils/config.js: -------------------------------------------------------------------------------- 1 | export const url = `${process.env.VUE_APP_SOCKET_HOST || 'http://localhost'}:${process.env.VUE_APP_SOCKET_PORT || '3000'}` 2 | 3 | export const STORE_ACTIONS = { 4 | joinRoom: 'joinRoom', 5 | setRooms: 'setRooms', 6 | changeRoom: 'changeRoom', 7 | leaveChat:'leaveChat', 8 | changeStatus: 'changeStatus' 9 | } 10 | export const WS_EVENTS = { 11 | joinPrivateRoom: 'joinPrivateRoom', 12 | joinRoom: 'joinRoom', 13 | leaveRoom: 'leaveRoom', 14 | publicMessage: 'publicMessage', 15 | leavePrivateRoom: 'leavePrivateRoom', 16 | leaveChat: 'leaveChat', 17 | changeStatus: 'changeStatus', 18 | privateMessage: 'privateMessage', 19 | privateMessagePCSignaling: 'privateMessagePCSignaling', 20 | PCSignalingConference: 'PCSignalingConference', 21 | conferenceInvitation: 'conferenceInvitation', 22 | joinConference: 'joinConference', 23 | leaveConference: 'leaveConference' 24 | } 25 | 26 | export const STATUS_OPTIONS = { 27 | available: 'available', 28 | absent: 'absent', 29 | unavailable: 'unavailable' 30 | } 31 | 32 | export const DESCRIPTION_TYPE = { 33 | offer: 'offer', 34 | answer: 'answer' 35 | } -------------------------------------------------------------------------------- /src/utils/logging.js: -------------------------------------------------------------------------------- 1 | export const log = (arg) => { 2 | var now = (window.performance.now() / 1000).toFixed(3) 3 | console.log(`${now}: ${arg}`) 4 | } -------------------------------------------------------------------------------- /src/views/Chat.vue: -------------------------------------------------------------------------------- 1 | 68 | 69 | 245 | 246 | 247 | 248 | 358 | 359 | 360 | -------------------------------------------------------------------------------- /src/views/Home.vue: -------------------------------------------------------------------------------- 1 | 38 | 39 | 81 | 82 | 107 | -------------------------------------------------------------------------------- /vue.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | productionSourceMap: false 3 | }; --------------------------------------------------------------------------------