├── README.md ├── src ├── favicon.ico ├── ws │ └── stream.js ├── app.js ├── assets │ ├── js │ │ ├── autolink.js │ │ ├── events.js │ │ ├── helpers.js │ │ └── rtc.js │ └── css │ │ └── app.css └── index.html └── package.json /README.md: -------------------------------------------------------------------------------- 1 | # customize-web-rtc-vcall 2 | -------------------------------------------------------------------------------- /src/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vinika02/customize-web-rtc-vcall/HEAD/src/favicon.ico -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "livestream-server", 3 | "version": "0.0.1", 4 | "description": "A conference call implementation using WebRTC, Socket.io and Node.js.", 5 | "main": "src/app.js", 6 | "dependencies": { 7 | "express": "^4.17.1", 8 | "serve-favicon": "^2.5.0", 9 | "socket.io": "^2.3.0" 10 | }, 11 | "devDependencies": { 12 | "nodemon": "^2.0.3" 13 | }, 14 | "scripts": { 15 | "watch": "nodemon --watch \"src/\" --exec \"node src/app.js\"", 16 | "test": "echo \"Error: no test specified\" && exit 1" 17 | }, 18 | "repository": { 19 | "type": "git", 20 | "url": "git+https://github.com/amirsanni/livestream-server.git" 21 | }, 22 | "keywords": [ 23 | "LiveStream", 24 | "Server" 25 | ], 26 | "author": "Amir Sanni ", 27 | "license": "ISC", 28 | "bugs": { 29 | "url": "https://github.com/amirsanni/livestream-server/issues" 30 | }, 31 | "homepage": "https://github.com/amirsanni/livestream-server#readme" 32 | } 33 | -------------------------------------------------------------------------------- /src/ws/stream.js: -------------------------------------------------------------------------------- 1 | const stream = ( socket ) => { 2 | socket.on( 'subscribe', ( data ) => { 3 | //subscribe/join a room 4 | socket.join( data.room ); 5 | socket.join( data.socketId ); 6 | 7 | //Inform other members in the room of new user's arrival 8 | if ( socket.adapter.rooms[data.room].length > 1 ) { 9 | socket.to( data.room ).emit( 'new user', { socketId: data.socketId } ); 10 | } 11 | } ); 12 | 13 | 14 | socket.on( 'newUserStart', ( data ) => { 15 | socket.to( data.to ).emit( 'newUserStart', { sender: data.sender } ); 16 | } ); 17 | 18 | 19 | socket.on( 'sdp', ( data ) => { 20 | socket.to( data.to ).emit( 'sdp', { description: data.description, sender: data.sender } ); 21 | } ); 22 | 23 | 24 | socket.on( 'ice candidates', ( data ) => { 25 | socket.to( data.to ).emit( 'ice candidates', { candidate: data.candidate, sender: data.sender } ); 26 | } ); 27 | 28 | 29 | socket.on( 'chat', ( data ) => { 30 | socket.to( data.room ).emit( 'chat', { sender: data.sender, msg: data.msg } ); 31 | } ); 32 | }; 33 | 34 | module.exports = stream; 35 | -------------------------------------------------------------------------------- /src/app.js: -------------------------------------------------------------------------------- 1 | let express = require( 'express' ); 2 | let app = express(); 3 | let server = require( 'http' ).Server( app ); 4 | let io = require( 'socket.io' )( server ); 5 | let stream = require( './ws/stream' ); 6 | let path = require( 'path' ); 7 | let favicon = require( 'serve-favicon' ); 8 | 9 | app.use( favicon( path.join( __dirname, 'favicon.ico' ) ) ); 10 | app.use( '/assets', express.static( path.join( __dirname, 'assets' ) ) ); 11 | 12 | app.get( '/', ( req, res ) => { 13 | res.sendFile( __dirname + '/index.html' ); 14 | }); 15 | 16 | 17 | io.on('connection', function(socket) { 18 | socket.on("call_to_user", function (data){ 19 | io.emit('dialling', data); 20 | console.log(data); 21 | }); 22 | socket.on("decline_call", function (data){ 23 | io.emit('decline_call', data); 24 | console.log("decline_call"); 25 | }); 26 | 27 | }); 28 | 29 | 30 | io.of( '/stream' ).on( 'connection', stream ); 31 | 32 | io.of( '/stream' ).on('connection', client => { 33 | client.on('disconnect', () => { 34 | io.emit('calller_disconnected', "1"); 35 | }); 36 | }); 37 | 38 | const port = process.env.PORT || 3000; 39 | 40 | 41 | var listener = server.listen(port, function(){ 42 | console.log('Listening on ' + listener.address().address, + listener.address().port); //Listening on port 8080 43 | }); 44 | -------------------------------------------------------------------------------- /src/assets/js/autolink.js: -------------------------------------------------------------------------------- 1 | // Generated by CoffeeScript 1.10.0 2 | /** 3 | * @author Bryan Woods 4 | * @url https://github.com/bryanwoods/autolink-js 5 | */ 6 | ( function () { 7 | var autoLink, 8 | slice = [].slice; 9 | 10 | autoLink = function () { 11 | var callback, k, linkAttributes, option, options, pattern, v; 12 | options = 1 <= arguments.length ? slice.call( arguments, 0 ) : []; 13 | pattern = /(^|[\s\n]|<[A-Za-z]*\/?>)((?:https?|ftp):\/\/[\-A-Z0-9+\u0026\u2019@#\/%?=()~_|!:,.;]*[\-A-Z0-9+\u0026@#\/%=~()_|])/gi; 14 | if ( !( options.length > 0 ) ) { 15 | return this.replace( pattern, "$1$2" ); 16 | } 17 | option = options[0]; 18 | callback = option["callback"]; 19 | linkAttributes = ( ( function () { 20 | var results; 21 | results = []; 22 | for ( k in option ) { 23 | v = option[k]; 24 | if ( k !== 'callback' ) { 25 | results.push( " " + k + "='" + v + "'" ); 26 | } 27 | } 28 | return results; 29 | } )() ).join( '' ); 30 | return this.replace( pattern, function ( match, space, url ) { 31 | var link; 32 | link = ( typeof callback === "function" ? callback( url ) : void 0 ) || ( "" + url + "" ); 33 | return "" + space + link; 34 | } ); 35 | }; 36 | 37 | String.prototype['autoLink'] = autoLink; 38 | 39 | } ).call( this ); 40 | -------------------------------------------------------------------------------- /src/assets/css/app.css: -------------------------------------------------------------------------------- 1 | .chat-col{ 2 | right: -100vw; 3 | bottom: 0; 4 | top: 40.5px; 5 | z-index: 1000; 6 | position: fixed; 7 | color: #fff; 8 | padding-right: 5px; 9 | padding-left: 5px; 10 | padding-bottom: 40px; 11 | padding-top: 15px; 12 | min-height: 100vh; 13 | } 14 | #user-label{ 15 | font-weight: bold; 16 | } 17 | .bg-pink{ 18 | background: #2740b9; 19 | } 20 | 21 | .chat-col.chat-opened { 22 | right: 0; 23 | overflow-y: auto; 24 | overflow-x: hidden; 25 | transition: all 0.3s ease !important; 26 | -webkit-transition: all 0.3s ease !important; 27 | -moz-transition: all 0.3s ease !important; 28 | } 29 | 30 | #chat-messages{ 31 | height: 70vh; 32 | margin-bottom: 20px; 33 | overflow-x: hidden; 34 | overflow-y: auto; 35 | scrollbar-width: none; /* Firefox */ 36 | -ms-overflow-style: none; /* IE 10+ */ 37 | } 38 | 39 | #chat-messages::-webkit-scrollbar { 40 | width: 0px; /* remove scrollbar space */ 41 | background: transparent; 42 | } 43 | 44 | .chat-box{ 45 | bottom: 30px; 46 | right: 0; 47 | position: absolute; 48 | border: 0; 49 | border-top: 1px groove white; 50 | border-left: 1px groove white; 51 | font-size: small; 52 | } 53 | 54 | .chat-box::placeholder{ 55 | font-size: small; 56 | font-weight: lighter; 57 | font-style: italic; 58 | } 59 | 60 | .chat-box, 61 | .chat-box:focus{ 62 | resize: none !important; 63 | box-shadow: none !important; 64 | } 65 | 66 | .chat-row{ 67 | height: 100%; 68 | overflow-x: scroll; 69 | } 70 | 71 | .main{ 72 | padding-top: 40px; 73 | } 74 | 75 | 76 | .remote-video{ 77 | width:100%; 78 | height:auto; 79 | max-height: 90vh; 80 | } 81 | 82 | 83 | .remote-video-controls{ 84 | position:absolute; 85 | bottom: 0; 86 | background-color:rgba(0, 0, 0, 0.5); 87 | z-index:300000; 88 | padding: 10px; 89 | width: 100%; 90 | text-align: center; 91 | visibility: hidden; 92 | } 93 | 94 | 95 | .remote-video:hover + .remote-video-controls, 96 | .remote-video-controls:hover{ 97 | visibility: visible; 98 | } 99 | 100 | 101 | .local-video{ 102 | bottom: 0; 103 | left: 0; 104 | position: fixed; 105 | width:15vw; 106 | } 107 | 108 | 109 | .mirror-mode{ 110 | -ms-transform: scaleX(-1); 111 | -moz-transform: scaleX(-1); 112 | -webkit-transform: scaleX(-1); 113 | transform: scaleX(-1); 114 | } 115 | 116 | 117 | .sender-info{ 118 | font-size: smaller; 119 | margin-top: 5px; 120 | align-self: flex-end; 121 | } 122 | 123 | 124 | .msg{ 125 | font-weight: 400; 126 | font-size: 12px; 127 | color: black; 128 | background-color: wheat; 129 | } 130 | 131 | 132 | .chat-card{ 133 | border-radius: 6px; 134 | } 135 | 136 | 137 | .btn-no-effect:focus{ 138 | box-shadow: none; 139 | } 140 | 141 | .very-small{ 142 | font-size: 6px !important; 143 | } 144 | 145 | 146 | #close-single-peer-btn { 147 | position: fixed; 148 | top: 0; 149 | text-align: center; 150 | background: rgba(0, 0, 0, 0.5); 151 | color: #f1f1f1; 152 | border-radius: 0%; 153 | z-index: 100; 154 | } 155 | 156 | 157 | .pointer{ 158 | cursor: pointer; 159 | } 160 | 161 | 162 | .record-option{ 163 | height: 200px; 164 | border-radius: 10%; 165 | border: 1px solid #17a2b8; 166 | cursor: pointer; 167 | padding: 10px; 168 | vertical-align: middle; 169 | } 170 | 171 | 172 | .custom-modal { 173 | display: none; 174 | position: fixed; 175 | z-index: 10000; 176 | left: 0; 177 | top: 0; 178 | width: 100%; 179 | height: 100%; 180 | overflow: auto; 181 | } 182 | 183 | 184 | .custom-modal-content { 185 | background-color: #fefefe; 186 | margin: 15% auto; 187 | padding: 20px; 188 | border: 1px solid #17a2b8; 189 | width: 80%; 190 | } 191 | 192 | 193 | @keyframes animatetop { 194 | from {top: -300px; opacity: 0} 195 | to {top: 0; opacity: 1} 196 | } 197 | 198 | 199 | @media only screen and (max-width:767px){ 200 | .chat-col{ 201 | right: -100vw; 202 | width: 100vw; 203 | z-index: 99999; 204 | transition: 0.3s; 205 | top: 47px; 206 | } 207 | 208 | .chat-opened::-webkit-scrollbar { 209 | display: none; 210 | } 211 | 212 | #chat-messages{ 213 | height: 60vh; 214 | } 215 | 216 | .chat-box{ 217 | bottom: 90px; 218 | margin-bottom: 0px; 219 | } 220 | 221 | .card-sm{ 222 | max-width: 100%; 223 | min-width: 50%; 224 | } 225 | 226 | 227 | .local-video{ 228 | width:40vw; 229 | } 230 | } 231 | 232 | 233 | @media (min-width:768px){ 234 | .card{ 235 | width: 50%; 236 | z-index: 1000; 237 | } 238 | } 239 | -------------------------------------------------------------------------------- /src/assets/js/events.js: -------------------------------------------------------------------------------- 1 | import helpers from './helpers.js'; 2 | 3 | window.addEventListener( 'load', () => { 4 | //When the chat icon is clicked 5 | document.querySelector( '#toggle-chat-pane' ).addEventListener( 'click', ( e ) => { 6 | let chatElem = document.querySelector( '#chat-pane' ); 7 | let mainSecElem = document.querySelector( '#main-section' ); 8 | 9 | if ( chatElem.classList.contains( 'chat-opened' ) ) { 10 | chatElem.setAttribute( 'hidden', true ); 11 | mainSecElem.classList.remove( 'col-md-9' ); 12 | mainSecElem.classList.add( 'col-md-12' ); 13 | chatElem.classList.remove( 'chat-opened' ); 14 | } 15 | 16 | else { 17 | chatElem.attributes.removeNamedItem( 'hidden' ); 18 | mainSecElem.classList.remove( 'col-md-12' ); 19 | mainSecElem.classList.add( 'col-md-9' ); 20 | chatElem.classList.add( 'chat-opened' ); 21 | } 22 | 23 | //remove the 'New' badge on chat icon (if any) once chat is opened. 24 | setTimeout( () => { 25 | if ( document.querySelector( '#chat-pane' ).classList.contains( 'chat-opened' ) ) { 26 | helpers.toggleChatNotificationBadge(); 27 | } 28 | }, 300 ); 29 | } ); 30 | 31 | 32 | //When the video frame is clicked. This will enable picture-in-picture 33 | document.getElementById( 'local' ).addEventListener( 'click', () => { 34 | if ( !document.pictureInPictureElement ) { 35 | document.getElementById( 'local' ).requestPictureInPicture() 36 | .catch( error => { 37 | // Video failed to enter Picture-in-Picture mode. 38 | console.error( error ); 39 | } ); 40 | } 41 | 42 | else { 43 | document.exitPictureInPicture() 44 | .catch( error => { 45 | // Video failed to leave Picture-in-Picture mode. 46 | console.error( error ); 47 | } ); 48 | } 49 | } ); 50 | 51 | 52 | //When the 'Create room" is button is clicked 53 | document.getElementById( 'create-room' ).addEventListener( 'click', ( e ) => { 54 | e.preventDefault(); 55 | 56 | let roomName = document.querySelector( '#room-name' ).value; 57 | let yourName = document.querySelector( '#your-name' ).value; 58 | 59 | if ( roomName && yourName ) { 60 | //remove error message, if any 61 | document.querySelector( '#err-msg' ).innerHTML = ""; 62 | 63 | //save the user's name in sessionStorage 64 | sessionStorage.setItem( 'username', yourName ); 65 | 66 | //create room link 67 | let roomLink = `${ location.origin }?room=${ roomName.trim().replace( ' ', '_' ) }_${ helpers.generateRandomString() }`; 68 | 69 | //show message with link to room 70 | document.querySelector( '#room-created' ).innerHTML = `Room successfully created. Click here to enter room. 71 | Share the room link with your partners.`; 72 | 73 | //empty the values 74 | document.querySelector( '#room-name' ).value = ''; 75 | document.querySelector( '#your-name' ).value = ''; 76 | } 77 | 78 | else { 79 | document.querySelector( '#err-msg' ).innerHTML = "All fields are required"; 80 | } 81 | } ); 82 | 83 | 84 | 85 | //When the 'Enter room' button is clicked. 86 | document.getElementById( 'enter-room' ).addEventListener( 'click', ( e ) => { 87 | e.preventDefault(); 88 | 89 | let name = document.querySelector( '#username' ).value; 90 | 91 | if ( name ) { 92 | //remove error message, if any 93 | document.querySelector( '#err-msg-username' ).innerHTML = ""; 94 | 95 | //save the user's name in sessionStorage 96 | sessionStorage.setItem( 'username', name ); 97 | 98 | let data = { 99 | roomlink: "room", 100 | callerId: "", 101 | receiverId: "name", 102 | caller: "caller", 103 | receiver: name 104 | }; 105 | 106 | var socket = io.connect('http://ptbcsi.site:3000'); 107 | 108 | socket.emit('call_to_user', video_call_data); 109 | 110 | //reload room 111 | location.reload(); 112 | } 113 | 114 | else { 115 | document.querySelector( '#err-msg-username' ).innerHTML = "Please input your name"; 116 | } 117 | } ); 118 | 119 | document.getElementById( 'close-call' ).addEventListener( 'click', ( e ) => { 120 | e.preventDefault(); 121 | window.close(); 122 | } ); 123 | 124 | 125 | 126 | 127 | document.addEventListener( 'click', ( e ) => { 128 | if ( e.target && e.target.classList.contains( 'expand-remote-video' ) ) { 129 | helpers.maximiseStream( e ); 130 | } 131 | 132 | else if ( e.target && e.target.classList.contains( 'mute-remote-mic' ) ) { 133 | helpers.singleStreamToggleMute( e ); 134 | } 135 | } ); 136 | 137 | 138 | document.getElementById( 'closeModal' ).addEventListener( 'click', () => { 139 | helpers.toggleModal( 'recording-options-modal', false ); 140 | } ); 141 | } ); 142 | -------------------------------------------------------------------------------- /src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Making a Video Call 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 |
24 |
25 |
26 |
27 | Record video 28 |
29 |
30 | Record screen 31 |
32 |
33 | 34 |
35 |
36 | 37 |
38 |
39 |
40 |
41 | 42 | 43 | 72 | 73 | 101 | 102 | 103 | 123 | 124 | 147 | 148 | 149 | 150 | 173 |
174 | 175 |
176 | 177 | 178 | 179 | 236 | -------------------------------------------------------------------------------- /src/assets/js/helpers.js: -------------------------------------------------------------------------------- 1 | export default { 2 | generateRandomString() { 3 | const crypto = window.crypto || window.msCrypto; 4 | let array = new Uint32Array(1); 5 | 6 | return crypto.getRandomValues(array); 7 | }, 8 | 9 | 10 | closeVideo( elemId ) { 11 | if ( document.getElementById( elemId ) ) { 12 | document.getElementById( elemId ).remove(); 13 | this.adjustVideoElemSize(); 14 | } 15 | }, 16 | 17 | 18 | pageHasFocus() { 19 | return !( document.hidden || document.onfocusout || window.onpagehide || window.onblur ); 20 | }, 21 | 22 | 23 | getQString( url = '', keyToReturn = '' ) { 24 | url = url ? url : location.href; 25 | let queryStrings = decodeURIComponent( url ).split( '#', 2 )[0].split( '?', 2 )[1]; 26 | 27 | if ( queryStrings ) { 28 | let splittedQStrings = queryStrings.split( '&' ); 29 | 30 | if ( splittedQStrings.length ) { 31 | let queryStringObj = {}; 32 | 33 | splittedQStrings.forEach( function ( keyValuePair ) { 34 | let keyValue = keyValuePair.split( '=', 2 ); 35 | 36 | if ( keyValue.length ) { 37 | queryStringObj[keyValue[0]] = keyValue[1]; 38 | } 39 | } ); 40 | 41 | return keyToReturn ? ( queryStringObj[keyToReturn] ? queryStringObj[keyToReturn] : null ) : queryStringObj; 42 | } 43 | 44 | return null; 45 | } 46 | 47 | return null; 48 | }, 49 | 50 | 51 | userMediaAvailable() { 52 | return !!( navigator.getUserMedia || navigator.webkitGetUserMedia || navigator.mozGetUserMedia || navigator.msGetUserMedia ); 53 | }, 54 | 55 | 56 | getUserFullMedia() { 57 | 58 | console.log(navigator); 59 | return false; 60 | if ( this.userMediaAvailable() ) { 61 | return navigator.mediaDevices.getUserMedia( { 62 | video: true, 63 | audio: { 64 | echoCancellation: true, 65 | noiseSuppression: true 66 | } 67 | } ); 68 | } 69 | 70 | else { 71 | 72 | // console.log() 73 | throw new Error( 'User media not available' ); 74 | } 75 | }, 76 | 77 | 78 | getUserAudio() { 79 | if ( this.userMediaAvailable() ) { 80 | return navigator.mediaDevices.getUserMedia( { 81 | audio: { 82 | echoCancellation: true, 83 | noiseSuppression: true 84 | } 85 | } ); 86 | } 87 | 88 | else { 89 | throw new Error( 'User media not available' ); 90 | } 91 | }, 92 | 93 | 94 | 95 | shareScreen() { 96 | if ( this.userMediaAvailable() ) { 97 | return navigator.mediaDevices.getDisplayMedia( { 98 | video: { 99 | cursor: "always" 100 | }, 101 | audio: { 102 | echoCancellation: true, 103 | noiseSuppression: true, 104 | sampleRate: 44100 105 | } 106 | } ); 107 | } 108 | 109 | else { 110 | throw new Error( 'User media not available' ); 111 | } 112 | }, 113 | 114 | 115 | getIceServer() { 116 | return { 117 | iceServers: [ 118 | { 119 | urls: ["stun:eu-turn4.xirsys.com"] 120 | }, 121 | { 122 | username: "ml0jh0qMKZKd9P_9C0UIBY2G0nSQMCFBUXGlk6IXDJf8G2uiCymg9WwbEJTMwVeiAAAAAF2__hNSaW5vbGVl", 123 | credential: "4dd454a6-feee-11e9-b185-6adcafebbb45", 124 | urls: [ 125 | "turn:eu-turn4.xirsys.com:80?transport=udp", 126 | "turn:eu-turn4.xirsys.com:3478?transport=tcp" 127 | ] 128 | } 129 | ] 130 | }; 131 | }, 132 | 133 | 134 | addChat( data, senderType ) { 135 | let chatMsgDiv = document.querySelector( '#chat-messages' ); 136 | let contentAlign = 'justify-content-end'; 137 | let senderName = 'You'; 138 | let msgBg = 'bg-white'; 139 | 140 | if ( senderType === 'remote' ) { 141 | contentAlign = 'justify-content-start'; 142 | senderName = data.sender; 143 | msgBg = ''; 144 | 145 | this.toggleChatNotificationBadge(); 146 | } 147 | 148 | let infoDiv = document.createElement( 'div' ); 149 | infoDiv.className = 'sender-info'; 150 | infoDiv.innerHTML = `${ senderName } - ${ moment().format( 'Do MMMM, YYYY h:mm a' ) }`; 151 | 152 | let colDiv = document.createElement( 'div' ); 153 | colDiv.className = `col-10 card chat-card msg ${ msgBg }`; 154 | colDiv.innerHTML = xssFilters.inHTMLData( data.msg ).autoLink( { target: "_blank", rel: "nofollow"}); 155 | 156 | let rowDiv = document.createElement( 'div' ); 157 | rowDiv.className = `row ${ contentAlign } mb-2`; 158 | 159 | 160 | colDiv.appendChild( infoDiv ); 161 | rowDiv.appendChild( colDiv ); 162 | 163 | chatMsgDiv.appendChild( rowDiv ); 164 | 165 | /** 166 | * Move focus to the newly added message but only if: 167 | * 1. Page has focus 168 | * 2. User has not moved scrollbar upward. This is to prevent moving the scroll position if user is reading previous messages. 169 | */ 170 | if ( this.pageHasFocus ) { 171 | rowDiv.scrollIntoView(); 172 | } 173 | }, 174 | 175 | 176 | toggleChatNotificationBadge() { 177 | if ( document.querySelector( '#chat-pane' ).classList.contains( 'chat-opened' ) ) { 178 | document.querySelector( '#new-chat-notification' ).setAttribute( 'hidden', true ); 179 | } 180 | 181 | else { 182 | document.querySelector( '#new-chat-notification' ).removeAttribute( 'hidden' ); 183 | } 184 | }, 185 | 186 | 187 | 188 | replaceTrack( stream, recipientPeer ) { 189 | let sender = recipientPeer.getSenders ? recipientPeer.getSenders().find( s => s.track && s.track.kind === stream.kind ) : false; 190 | 191 | sender ? sender.replaceTrack( stream ) : ''; 192 | }, 193 | 194 | 195 | 196 | toggleShareIcons( share ) { 197 | let shareIconElem = document.querySelector( '#share-screen' ); 198 | 199 | if ( share ) { 200 | shareIconElem.setAttribute( 'title', 'Stop sharing screen' ); 201 | shareIconElem.children[0].classList.add( 'text-primary' ); 202 | shareIconElem.children[0].classList.remove( 'text-white' ); 203 | } 204 | 205 | else { 206 | shareIconElem.setAttribute( 'title', 'Share screen' ); 207 | shareIconElem.children[0].classList.add( 'text-white' ); 208 | shareIconElem.children[0].classList.remove( 'text-primary' ); 209 | } 210 | }, 211 | 212 | 213 | toggleVideoBtnDisabled( disabled ) { 214 | document.getElementById( 'toggle-video' ).disabled = disabled; 215 | }, 216 | 217 | 218 | maximiseStream( e ) { 219 | let elem = e.target.parentElement.previousElementSibling; 220 | 221 | elem.requestFullscreen() || elem.mozRequestFullScreen() || elem.webkitRequestFullscreen() || elem.msRequestFullscreen(); 222 | }, 223 | 224 | 225 | singleStreamToggleMute( e ) { 226 | if ( e.target.classList.contains( 'fa-microphone' ) ) { 227 | e.target.parentElement.previousElementSibling.muted = true; 228 | e.target.classList.add( 'fa-microphone-slash' ); 229 | e.target.classList.remove( 'fa-microphone' ); 230 | } 231 | 232 | else { 233 | e.target.parentElement.previousElementSibling.muted = false; 234 | e.target.classList.add( 'fa-microphone' ); 235 | e.target.classList.remove( 'fa-microphone-slash' ); 236 | } 237 | }, 238 | 239 | 240 | saveRecordedStream( stream, user ) { 241 | let blob = new Blob( stream, { type: 'video/webm' } ); 242 | 243 | let file = new File( [blob], `${ user }-${ moment().unix() }-record.webm` ); 244 | 245 | saveAs( file ); 246 | }, 247 | 248 | 249 | toggleModal( id, show ) { 250 | let el = document.getElementById( id ); 251 | 252 | if ( show ) { 253 | el.style.display = 'block'; 254 | el.removeAttribute( 'aria-hidden' ); 255 | } 256 | 257 | else { 258 | el.style.display = 'none'; 259 | el.setAttribute( 'aria-hidden', true ); 260 | } 261 | }, 262 | 263 | 264 | 265 | setLocalStream( stream, mirrorMode = true ) { 266 | const localVidElem = document.getElementById( 'local' ); 267 | 268 | localVidElem.srcObject = stream; 269 | mirrorMode ? localVidElem.classList.add( 'mirror-mode' ) : localVidElem.classList.remove( 'mirror-mode' ); 270 | }, 271 | 272 | 273 | adjustVideoElemSize() { 274 | let elem = document.getElementsByClassName( 'card' ); 275 | let totalRemoteVideosDesktop = elem.length; 276 | let newWidth = totalRemoteVideosDesktop <= 2 ? '50%' : ( 277 | totalRemoteVideosDesktop == 3 ? '33.33%' : ( 278 | totalRemoteVideosDesktop <= 8 ? '25%' : ( 279 | totalRemoteVideosDesktop <= 15 ? '20%' : ( 280 | totalRemoteVideosDesktop <= 18 ? '16%' : ( 281 | totalRemoteVideosDesktop <= 23 ? '15%' : ( 282 | totalRemoteVideosDesktop <= 32 ? '12%' : '10%' 283 | ) 284 | ) 285 | ) 286 | ) 287 | ) 288 | ); 289 | 290 | 291 | for ( let i = 0; i < totalRemoteVideosDesktop; i++ ) { 292 | elem[i].style.width = newWidth; 293 | } 294 | }, 295 | 296 | 297 | createDemoRemotes( str, total = 6 ) { 298 | let i = 0; 299 | 300 | let testInterval = setInterval( () => { 301 | let newVid = document.createElement( 'video' ); 302 | newVid.id = `demo-${ i }-video`; 303 | newVid.srcObject = str; 304 | newVid.autoplay = true; 305 | newVid.className = 'remote-video'; 306 | 307 | //video controls elements 308 | let controlDiv = document.createElement( 'div' ); 309 | controlDiv.className = 'remote-video-controls'; 310 | controlDiv.innerHTML = ` 311 | `; 312 | 313 | //create a new div for card 314 | let cardDiv = document.createElement( 'div' ); 315 | cardDiv.className = 'card card-sm'; 316 | cardDiv.id = `demo-${ i }`; 317 | cardDiv.appendChild( newVid ); 318 | cardDiv.appendChild( controlDiv ); 319 | 320 | //put div in main-section elem 321 | document.getElementById( 'videos' ).appendChild( cardDiv ); 322 | 323 | this.adjustVideoElemSize(); 324 | 325 | i++; 326 | 327 | if ( i == total ) { 328 | clearInterval( testInterval ); 329 | } 330 | }, 2000 ); 331 | } 332 | }; 333 | -------------------------------------------------------------------------------- /src/assets/js/rtc.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @author Amir Sanni 3 | * @date 6th January, 2020 4 | */ 5 | import h from './helpers.js'; 6 | 7 | window.addEventListener( 'load', () => { 8 | const room = h.getQString( location.href, 'room' ); 9 | const username = sessionStorage.getItem( 'username' ); 10 | 11 | if ( !room ) { 12 | document.querySelector( '#room-create' ).attributes.removeNamedItem( 'hidden' ); 13 | } 14 | 15 | else if ( !username ) { 16 | document.querySelector( '#username-set' ).attributes.removeNamedItem( 'hidden' ); 17 | } 18 | 19 | else { 20 | let commElem = document.getElementsByClassName( 'room-comm' ); 21 | 22 | for ( let i = 0; i < commElem.length; i++ ) { 23 | commElem[i].attributes.removeNamedItem( 'hidden' ); 24 | } 25 | 26 | var pc = []; 27 | 28 | let socket = io( '/stream' ); 29 | 30 | var socketId = ''; 31 | var myStream = ''; 32 | var screen = ''; 33 | var recordedStream = []; 34 | var mediaRecorder = ''; 35 | 36 | //Get user video by default 37 | getAndSetUserStream(); 38 | 39 | 40 | socket.on( 'connect', () => { 41 | //set socketId 42 | socketId = socket.io.engine.id; 43 | 44 | 45 | socket.emit( 'subscribe', { 46 | room: room, 47 | socketId: socketId 48 | } ); 49 | 50 | 51 | socket.on( 'new user', ( data ) => { 52 | socket.emit( 'newUserStart', { to: data.socketId, sender: socketId } ); 53 | pc.push( data.socketId ); 54 | init( true, data.socketId ); 55 | } ); 56 | 57 | 58 | socket.on( 'newUserStart', ( data ) => { 59 | pc.push( data.sender ); 60 | init( false, data.sender ); 61 | } ); 62 | 63 | 64 | socket.on( 'ice candidates', async ( data ) => { 65 | data.candidate ? await pc[data.sender].addIceCandidate( new RTCIceCandidate( data.candidate ) ) : ''; 66 | } ); 67 | 68 | 69 | socket.on( 'sdp', async ( data ) => { 70 | if ( data.description.type === 'offer' ) { 71 | data.description ? await pc[data.sender].setRemoteDescription( new RTCSessionDescription( data.description ) ) : ''; 72 | 73 | h.getUserFullMedia().then( async ( stream ) => { 74 | if ( !document.getElementById( 'local' ).srcObject ) { 75 | h.setLocalStream( stream ); 76 | } 77 | 78 | //save my stream 79 | myStream = stream; 80 | 81 | stream.getTracks().forEach( ( track ) => { 82 | pc[data.sender].addTrack( track, stream ); 83 | } ); 84 | 85 | let answer = await pc[data.sender].createAnswer(); 86 | 87 | await pc[data.sender].setLocalDescription( answer ); 88 | 89 | socket.emit( 'sdp', { description: pc[data.sender].localDescription, to: data.sender, sender: socketId } ); 90 | } ).catch( ( e ) => { 91 | console.error( e ); 92 | } ); 93 | } 94 | 95 | else if ( data.description.type === 'answer' ) { 96 | await pc[data.sender].setRemoteDescription( new RTCSessionDescription( data.description ) ); 97 | } 98 | } ); 99 | 100 | 101 | socket.on( 'chat', ( data ) => { 102 | h.addChat( data, 'remote' ); 103 | } ); 104 | } ); 105 | 106 | 107 | 108 | function getAndSetUserStream() { 109 | h.getUserFullMedia().then( ( stream ) => { 110 | //save my stream 111 | myStream = stream; 112 | 113 | h.setLocalStream( stream ); 114 | } ).catch( ( e ) => { 115 | console.error( `stream error: ${ e }` ); 116 | } ); 117 | } 118 | 119 | 120 | function sendMsg( msg ) { 121 | let data = { 122 | room: room, 123 | msg: msg, 124 | sender: username 125 | }; 126 | 127 | //emit chat message 128 | socket.emit( 'chat', data ); 129 | 130 | //add localchat 131 | h.addChat( data, 'local' ); 132 | } 133 | 134 | 135 | 136 | function init( createOffer, partnerName ) { 137 | pc[partnerName] = new RTCPeerConnection( h.getIceServer() ); 138 | 139 | if ( screen && screen.getTracks().length ) { 140 | screen.getTracks().forEach( ( track ) => { 141 | pc[partnerName].addTrack( track, screen );//should trigger negotiationneeded event 142 | } ); 143 | } 144 | 145 | else if ( myStream ) { 146 | myStream.getTracks().forEach( ( track ) => { 147 | pc[partnerName].addTrack( track, myStream );//should trigger negotiationneeded event 148 | } ); 149 | } 150 | 151 | else { 152 | h.getUserFullMedia().then( ( stream ) => { 153 | //save my stream 154 | myStream = stream; 155 | 156 | stream.getTracks().forEach( ( track ) => { 157 | pc[partnerName].addTrack( track, stream );//should trigger negotiationneeded event 158 | } ); 159 | 160 | h.setLocalStream( stream ); 161 | } ).catch( ( e ) => { 162 | console.error( `stream error: ${ e }` ); 163 | } ); 164 | } 165 | 166 | 167 | 168 | //create offer 169 | if ( createOffer ) { 170 | pc[partnerName].onnegotiationneeded = async () => { 171 | let offer = await pc[partnerName].createOffer(); 172 | 173 | await pc[partnerName].setLocalDescription( offer ); 174 | 175 | socket.emit( 'sdp', { description: pc[partnerName].localDescription, to: partnerName, sender: socketId } ); 176 | }; 177 | } 178 | 179 | 180 | 181 | //send ice candidate to partnerNames 182 | pc[partnerName].onicecandidate = ( { candidate } ) => { 183 | socket.emit( 'ice candidates', { candidate: candidate, to: partnerName, sender: socketId } ); 184 | }; 185 | 186 | 187 | 188 | //add 189 | pc[partnerName].ontrack = ( e ) => { 190 | let str = e.streams[0]; 191 | if ( document.getElementById( `${ partnerName }-video` ) ) { 192 | document.getElementById( `${ partnerName }-video` ).srcObject = str; 193 | } 194 | 195 | else { 196 | //video elem 197 | let newVid = document.createElement( 'video' ); 198 | newVid.id = `${ partnerName }-video`; 199 | newVid.srcObject = str; 200 | newVid.autoplay = true; 201 | newVid.className = 'remote-video'; 202 | 203 | //video controls elements 204 | let controlDiv = document.createElement( 'div' ); 205 | controlDiv.className = 'remote-video-controls'; 206 | controlDiv.innerHTML = ` 207 | `; 208 | 209 | //create a new div for card 210 | let cardDiv = document.createElement( 'div' ); 211 | cardDiv.className = 'card card-sm'; 212 | cardDiv.id = partnerName; 213 | cardDiv.appendChild( newVid ); 214 | cardDiv.appendChild( controlDiv ); 215 | 216 | //put div in main-section elem 217 | document.getElementById( 'videos' ).appendChild( cardDiv ); 218 | 219 | h.adjustVideoElemSize(); 220 | } 221 | }; 222 | 223 | 224 | 225 | pc[partnerName].onconnectionstatechange = ( d ) => { 226 | switch ( pc[partnerName].iceConnectionState ) { 227 | case 'disconnected': 228 | case 'failed': 229 | h.closeVideo( partnerName ); 230 | break; 231 | 232 | case 'closed': 233 | h.closeVideo( partnerName ); 234 | break; 235 | } 236 | }; 237 | 238 | 239 | 240 | pc[partnerName].onsignalingstatechange = ( d ) => { 241 | switch ( pc[partnerName].signalingState ) { 242 | case 'closed': 243 | console.log( "Signalling state is 'closed'" ); 244 | h.closeVideo( partnerName ); 245 | break; 246 | } 247 | }; 248 | } 249 | 250 | 251 | 252 | function shareScreen() { 253 | h.shareScreen().then( ( stream ) => { 254 | h.toggleShareIcons( true ); 255 | 256 | //disable the video toggle btns while sharing screen. This is to ensure clicking on the btn does not interfere with the screen sharing 257 | //It will be enabled was user stopped sharing screen 258 | h.toggleVideoBtnDisabled( true ); 259 | 260 | //save my screen stream 261 | screen = stream; 262 | 263 | //share the new stream with all partners 264 | broadcastNewTracks( stream, 'video', false ); 265 | 266 | //When the stop sharing button shown by the browser is clicked 267 | screen.getVideoTracks()[0].addEventListener( 'ended', () => { 268 | stopSharingScreen(); 269 | } ); 270 | } ).catch( ( e ) => { 271 | console.error( e ); 272 | } ); 273 | } 274 | 275 | 276 | 277 | function stopSharingScreen() { 278 | //enable video toggle btn 279 | h.toggleVideoBtnDisabled( false ); 280 | 281 | return new Promise( ( res, rej ) => { 282 | screen.getTracks().length ? screen.getTracks().forEach( track => track.stop() ) : ''; 283 | 284 | res(); 285 | } ).then( () => { 286 | h.toggleShareIcons( false ); 287 | broadcastNewTracks( myStream, 'video' ); 288 | } ).catch( ( e ) => { 289 | console.error( e ); 290 | } ); 291 | } 292 | 293 | 294 | 295 | function broadcastNewTracks( stream, type, mirrorMode = true ) { 296 | h.setLocalStream( stream, mirrorMode ); 297 | 298 | let track = type == 'audio' ? stream.getAudioTracks()[0] : stream.getVideoTracks()[0]; 299 | 300 | for ( let p in pc ) { 301 | let pName = pc[p]; 302 | 303 | if ( typeof pc[pName] == 'object' ) { 304 | h.replaceTrack( track, pc[pName] ); 305 | } 306 | } 307 | } 308 | 309 | 310 | function toggleRecordingIcons( isRecording ) { 311 | let e = document.getElementById( 'record' ); 312 | 313 | if ( isRecording ) { 314 | e.setAttribute( 'title', 'Stop recording' ); 315 | e.children[0].classList.add( 'text-danger' ); 316 | e.children[0].classList.remove( 'text-white' ); 317 | } 318 | 319 | else { 320 | e.setAttribute( 'title', 'Record' ); 321 | e.children[0].classList.add( 'text-white' ); 322 | e.children[0].classList.remove( 'text-danger' ); 323 | } 324 | } 325 | 326 | 327 | function startRecording( stream ) { 328 | mediaRecorder = new MediaRecorder( stream, { 329 | mimeType: 'video/webm;codecs=vp9' 330 | } ); 331 | 332 | mediaRecorder.start( 1000 ); 333 | toggleRecordingIcons( true ); 334 | 335 | mediaRecorder.ondataavailable = function ( e ) { 336 | recordedStream.push( e.data ); 337 | }; 338 | 339 | mediaRecorder.onstop = function () { 340 | toggleRecordingIcons( false ); 341 | 342 | h.saveRecordedStream( recordedStream, username ); 343 | 344 | setTimeout( () => { 345 | recordedStream = []; 346 | }, 3000 ); 347 | }; 348 | 349 | mediaRecorder.onerror = function ( e ) { 350 | console.error( e ); 351 | }; 352 | } 353 | 354 | 355 | //Chat textarea 356 | document.getElementById( 'chat-input' ).addEventListener( 'keypress', ( e ) => { 357 | if ( e.which === 13 && ( e.target.value.trim() ) ) { 358 | e.preventDefault(); 359 | 360 | sendMsg( e.target.value ); 361 | 362 | setTimeout( () => { 363 | e.target.value = ''; 364 | }, 50 ); 365 | } 366 | } ); 367 | 368 | 369 | //When the video icon is clicked 370 | document.getElementById( 'toggle-video' ).addEventListener( 'click', ( e ) => { 371 | e.preventDefault(); 372 | 373 | let elem = document.getElementById( 'toggle-video' ); 374 | 375 | if ( myStream.getVideoTracks()[0].enabled ) { 376 | e.target.classList.remove( 'fa-video' ); 377 | e.target.classList.add( 'fa-video-slash' ); 378 | elem.setAttribute( 'title', 'Show Video' ); 379 | 380 | myStream.getVideoTracks()[0].enabled = false; 381 | } 382 | 383 | else { 384 | e.target.classList.remove( 'fa-video-slash' ); 385 | e.target.classList.add( 'fa-video' ); 386 | elem.setAttribute( 'title', 'Hide Video' ); 387 | 388 | myStream.getVideoTracks()[0].enabled = true; 389 | } 390 | 391 | broadcastNewTracks( myStream, 'video' ); 392 | } ); 393 | 394 | 395 | //When the mute icon is clicked 396 | document.getElementById( 'toggle-mute' ).addEventListener( 'click', ( e ) => { 397 | e.preventDefault(); 398 | 399 | let elem = document.getElementById( 'toggle-mute' ); 400 | 401 | if ( myStream.getAudioTracks()[0].enabled ) { 402 | e.target.classList.remove( 'fa-microphone-alt' ); 403 | e.target.classList.add( 'fa-microphone-alt-slash' ); 404 | elem.setAttribute( 'title', 'Unmute' ); 405 | 406 | myStream.getAudioTracks()[0].enabled = false; 407 | } 408 | 409 | else { 410 | e.target.classList.remove( 'fa-microphone-alt-slash' ); 411 | e.target.classList.add( 'fa-microphone-alt' ); 412 | elem.setAttribute( 'title', 'Mute' ); 413 | 414 | myStream.getAudioTracks()[0].enabled = true; 415 | } 416 | 417 | broadcastNewTracks( myStream, 'audio' ); 418 | } ); 419 | 420 | 421 | //When user clicks the 'Share screen' button 422 | document.getElementById( 'share-screen' ).addEventListener( 'click', ( e ) => { 423 | e.preventDefault(); 424 | 425 | if ( screen && screen.getVideoTracks().length && screen.getVideoTracks()[0].readyState != 'ended' ) { 426 | stopSharingScreen(); 427 | } 428 | 429 | else { 430 | shareScreen(); 431 | } 432 | } ); 433 | 434 | 435 | //When record button is clicked 436 | document.getElementById( 'record' ).addEventListener( 'click', ( e ) => { 437 | /** 438 | * Ask user what they want to record. 439 | * Get the stream based on selection and start recording 440 | */ 441 | if ( !mediaRecorder || mediaRecorder.state == 'inactive' ) { 442 | h.toggleModal( 'recording-options-modal', true ); 443 | } 444 | 445 | else if ( mediaRecorder.state == 'paused' ) { 446 | mediaRecorder.resume(); 447 | } 448 | 449 | else if ( mediaRecorder.state == 'recording' ) { 450 | mediaRecorder.stop(); 451 | } 452 | } ); 453 | 454 | 455 | //When user choose to record screen 456 | document.getElementById( 'record-screen' ).addEventListener( 'click', () => { 457 | h.toggleModal( 'recording-options-modal', false ); 458 | 459 | if ( screen && screen.getVideoTracks().length ) { 460 | startRecording( screen ); 461 | } 462 | 463 | else { 464 | h.shareScreen().then( ( screenStream ) => { 465 | startRecording( screenStream ); 466 | } ).catch( () => { } ); 467 | } 468 | } ); 469 | 470 | 471 | //When user choose to record own video 472 | document.getElementById( 'record-video' ).addEventListener( 'click', () => { 473 | h.toggleModal( 'recording-options-modal', false ); 474 | 475 | if ( myStream && myStream.getTracks().length ) { 476 | startRecording( myStream ); 477 | } 478 | 479 | else { 480 | h.getUserFullMedia().then( ( videoStream ) => { 481 | startRecording( videoStream ); 482 | } ).catch( () => { } ); 483 | } 484 | } ); 485 | } 486 | } ); 487 | --------------------------------------------------------------------------------