├── LICENSE ├── README.md ├── index.html ├── js └── plainwebrtc.js └── snap.png /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Sasivarunan 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ### Serverless-webrtc 2 | --- 3 | * Is it possible to create a p2p connection without a signalling server? 4 | Yes! 5 | * How?? copy paste the SDPs between local and remote that's it! 6 | * Are you kidding?? Nope, Please open `index.html` in a localhost and go through the steps. 7 | * This Serverless webRTC concept is only for learning javascript webRTC APIs 8 | 9 | ### Setup 10 | on macOS/Linux just clone this repo and start a http server 11 | 12 | ` 13 | cd serverless-webrtc 14 | 15 | python -m SimpleHTTPServer 8080 16 | 17 | if you have node http-server 18 | 19 | http-server -p 8080 . 20 | 21 | http://localhost:8080/index.html 22 | ` 23 | ### Online demo 24 | https://svarunan.github.io/serverless-webrtc/ 25 | 26 | ### Usage 27 | * open this url in two tabs lets say A and B. 28 | * click on createOffer in A, copy paste the sdp in B's Remote text box, click "answer" button and this will add sdp to local text box. 29 | * Of B's local text box, copy paste sdp text in to A's Remote text box and click on "answer" button, then you should be able to see p2p connection working 30 | 31 | ### Features 32 | * peer to peer video calling 33 | * chat 34 | * file transfer 35 | 36 | ### Snap 37 | ![serverless-webrtc](snap.png) 38 | 39 | ### Contribute 40 | Let's share the Joy together. Let's keep this project super simple. Raise PR if you see something deprecated or not working. 41 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Plain webRTC 5 | 10 | 11 | 12 |
13 |

Start by:

14 | Enable Audio only 15 | Enable Audio Video 16 | Enable chat 17 | 18 |
19 |
20 |

LocalStream

21 | Audio File 22 | 23 |
24 | 25 |
26 |
27 |

28 | 29 | Local 30 | 31 | 32 |

33 |
34 |
35 |

36 | 37 | Remote 38 | 39 | 40 |

41 |
42 |
43 |
44 |

45 |
46 | 47 | 48 |
49 |
50 |
51 |

52 | File transfer: 53 |

54 |
55 | 56 | 57 | 58 |
59 |
60 |

61 | 62 | 63 |

64 |
65 |
66 | 67 | 68 | -------------------------------------------------------------------------------- /js/plainwebrtc.js: -------------------------------------------------------------------------------- 1 | var conf = { iceServers: [{ "urls": "stun:stun.l.google.com:19302" }] }; 2 | var pc = new RTCPeerConnection(conf); 3 | var localStream, _fileChannel, chatEnabled, context, source, 4 | _chatChannel, sendFileDom = {}, 5 | recFileDom = {}, 6 | receiveBuffer = [], 7 | receivedSize = 0, 8 | file, 9 | bytesPrev = 0; 10 | 11 | function errHandler(err) { 12 | console.log(err); 13 | } 14 | 15 | function enableChat() { 16 | enable_chat.checked ? (chatEnabled = true) : (chatEnabled = false); 17 | } 18 | 19 | function getMedia(mediaType) { 20 | var constraints = { audio: true, video: true } 21 | if (mediaType == 'audio') { 22 | constraints.video = false; 23 | } else { 24 | constraints.video = true 25 | } 26 | navigator.mediaDevices.getUserMedia(constraints).then(stream => { 27 | localStream = stream; 28 | local.srcObject = stream; 29 | local.muted = true; 30 | stream.getTracks().forEach((track) => { 31 | pc.addTrack(track, stream); 32 | }); 33 | }).catch(errHandler); 34 | } 35 | 36 | enableChat(); 37 | getMedia("both") 38 | 39 | function sendMsg() { 40 | var text = sendTxt.value; 41 | chat.innerHTML = chat.innerHTML + "
" + text + "
"; 42 | _chatChannel.send(text); 43 | sendTxt.value = ""; 44 | return false; 45 | } 46 | pc.ondatachannel = function (e) { 47 | if (e.channel.label == "fileChannel") { 48 | console.log('fileChannel Received -', e); 49 | _fileChannel = e.channel; 50 | fileChannel(e.channel); 51 | } 52 | if (e.channel.label == "chatChannel") { 53 | console.log('chatChannel Received -', e); 54 | _chatChannel = e.channel; 55 | chatChannel(e.channel); 56 | } 57 | }; 58 | 59 | pc.onicecandidate = function (e) { 60 | var cand = e.candidate; 61 | if (!cand) { 62 | console.log('iceGatheringState complete\n', pc.localDescription.sdp); 63 | localOffer.value = JSON.stringify(pc.localDescription); 64 | } else { 65 | console.log(cand.candidate); 66 | } 67 | } 68 | pc.oniceconnectionstatechange = function () { 69 | console.log('iceconnectionstatechange: ', pc.iceConnectionState); 70 | } 71 | 72 | pc.ontrack = function (e) { 73 | console.log('remote ontrack', e.streams); 74 | remote.srcObject = e.streams[0]; 75 | } 76 | pc.onconnection = function (e) { 77 | console.log('onconnection ', e); 78 | } 79 | 80 | remoteOfferGot.onclick = function () { 81 | var _remoteOffer = new RTCSessionDescription(JSON.parse(remoteOffer.value)); 82 | console.log('remoteOffer \n', _remoteOffer); 83 | pc.setRemoteDescription(_remoteOffer).then(function () { 84 | console.log('setRemoteDescription ok'); 85 | if (_remoteOffer.type == "offer") { 86 | pc.createAnswer().then(function (description) { 87 | console.log('createAnswer 200 ok \n', description); 88 | pc.setLocalDescription(description).then(function () { }).catch(errHandler); 89 | }).catch(errHandler); 90 | } 91 | }).catch(errHandler); 92 | } 93 | localOfferSet.onclick = function () { 94 | if (chatEnabled) { 95 | _chatChannel = pc.createDataChannel('chatChannel'); 96 | _fileChannel = pc.createDataChannel('fileChannel'); 97 | // _fileChannel.binaryType = 'arraybuffer'; 98 | chatChannel(_chatChannel); 99 | fileChannel(_fileChannel); 100 | } 101 | pc.createOffer().then(des => { 102 | console.log('createOffer ok '); 103 | pc.setLocalDescription(des).then(() => { 104 | setTimeout(function () { 105 | if (pc.iceGatheringState == "complete") { 106 | return; 107 | } else { 108 | console.log('after GetherTimeout'); 109 | localOffer.value = JSON.stringify(pc.localDescription); 110 | } 111 | }, 2000); 112 | console.log('setLocalDescription ok'); 113 | }).catch(errHandler); 114 | // For chat 115 | }).catch(errHandler); 116 | } 117 | 118 | //File transfer 119 | fileTransfer.onchange = function (e) { 120 | var files = fileTransfer.files; 121 | if (files.length > 0) { 122 | file = files[0]; 123 | sendFileDom.name = file.name; 124 | sendFileDom.size = file.size; 125 | sendFileDom.type = file.type; 126 | sendFileDom.fileInfo = "areYouReady"; 127 | console.log(sendFileDom); 128 | } else { 129 | console.log('No file selected'); 130 | } 131 | } 132 | 133 | function sendFile() { 134 | if (!fileTransfer.value) return; 135 | var fileInfo = JSON.stringify(sendFileDom); 136 | _fileChannel.send(fileInfo); 137 | console.log('file info sent'); 138 | } 139 | 140 | 141 | function fileChannel(e) { 142 | _fileChannel.onopen = function (e) { 143 | console.log('file channel is open', e); 144 | } 145 | _fileChannel.onmessage = function (e) { 146 | // Figure out data type 147 | var type = Object.prototype.toString.call(e.data), 148 | data; 149 | if (type == "[object ArrayBuffer]") { 150 | data = e.data; 151 | receiveBuffer.push(data); 152 | receivedSize += data.byteLength; 153 | recFileProg.value = receivedSize; 154 | if (receivedSize == recFileDom.size) { 155 | var received = new window.Blob(receiveBuffer); 156 | file_download.href = URL.createObjectURL(received); 157 | file_download.innerHTML = "download"; 158 | file_download.download = recFileDom.name; 159 | // rest 160 | receiveBuffer = []; 161 | receivedSize = 0; 162 | // clearInterval(window.timer); 163 | } 164 | } else if (type == "[object String]") { 165 | data = JSON.parse(e.data); 166 | } else if (type == "[object Blob]") { 167 | data = e.data; 168 | file_download.href = URL.createObjectURL(data); 169 | file_download.innerHTML = "download"; 170 | file_download.download = recFileDom.name; 171 | } 172 | 173 | // Handle initial msg exchange 174 | if (data.fileInfo) { 175 | if (data.fileInfo == "areYouReady") { 176 | recFileDom = data; 177 | recFileProg.max = data.size; 178 | var sendData = JSON.stringify({ fileInfo: "readyToReceive" }); 179 | _fileChannel.send(sendData); 180 | // window.timer = setInterval(function(){ 181 | // Stats(); 182 | // },1000) 183 | } else if (data.fileInfo == "readyToReceive") { 184 | sendFileProg.max = sendFileDom.size; 185 | sendFileinChannel(); // Start sending the file 186 | } 187 | console.log('_fileChannel: ', data.fileInfo); 188 | } 189 | } 190 | _fileChannel.onclose = function () { 191 | console.log('file channel closed'); 192 | } 193 | } 194 | 195 | function chatChannel(e) { 196 | _chatChannel.onopen = function (e) { 197 | console.log('chat channel is open', e); 198 | } 199 | _chatChannel.onmessage = function (e) { 200 | chat.innerHTML = chat.innerHTML + "
" + e.data + "
" 201 | } 202 | _chatChannel.onclose = function () { 203 | console.log('chat channel closed'); 204 | } 205 | } 206 | 207 | function sendFileinChannel() { 208 | var chunkSize = 16384; 209 | var sliceFile = function (offset) { 210 | var reader = new window.FileReader(); 211 | reader.onload = (function () { 212 | return function (e) { 213 | _fileChannel.send(e.target.result); 214 | if (file.size > offset + e.target.result.byteLength) { 215 | window.setTimeout(sliceFile, 0, offset + chunkSize); 216 | } 217 | sendFileProg.value = offset + e.target.result.byteLength 218 | }; 219 | })(file); 220 | var slice = file.slice(offset, offset + chunkSize); 221 | reader.readAsArrayBuffer(slice); 222 | }; 223 | sliceFile(0); 224 | } 225 | 226 | function Stats() { 227 | pc.getStats(null, function (stats) { 228 | for (var key in stats) { 229 | var res = stats[key]; 230 | console.log(res.type, res.googActiveConnection); 231 | if (res.type === 'googCandidatePair' && 232 | res.googActiveConnection === 'true') { 233 | // calculate current bitrate 234 | var bytesNow = res.bytesReceived; 235 | console.log('bit rate', (bytesNow - bytesPrev)); 236 | bytesPrev = bytesNow; 237 | } 238 | } 239 | }); 240 | } 241 | 242 | streamAudioFile.onchange = function () { 243 | console.log('streamAudioFile'); 244 | context = new AudioContext(); 245 | var file = streamAudioFile.files[0]; 246 | if (file) { 247 | if (file.type.match('audio*')) { 248 | var reader = new FileReader(); 249 | reader.onload = (function (readEvent) { 250 | context.decodeAudioData(readEvent.target.result, function (buffer) { 251 | // create an audio source and connect it to the file buffer 252 | source = context.createBufferSource(); 253 | source.buffer = buffer; 254 | source.start(0); 255 | 256 | // connect the audio stream to the audio hardware 257 | source.connect(context.destination); 258 | 259 | // create a destination for the remote browser 260 | var remote = context.createMediaStreamDestination(); 261 | 262 | // connect the remote destination to the source 263 | source.connect(remote); 264 | 265 | local.srcObject = remote.stream 266 | local.muted = true; 267 | // add the stream to the peer connection 268 | remote.stream.getTracks().forEach((track) => { 269 | pc.addTrack(track, stream); 270 | }); 271 | 272 | // create a SDP offer for the new stream 273 | // pc.createOffer(setLocalAndSendMessage); 274 | }); 275 | }); 276 | 277 | reader.readAsArrayBuffer(file); 278 | } 279 | } 280 | } 281 | 282 | var audioRTC = function (cb) { 283 | console.log('streamAudioFile'); 284 | window.context = new AudioContext(); 285 | var file = streamAudioFile.files[0]; 286 | if (file) { 287 | if (file.type.match('audio*')) { 288 | var reader = new FileReader(); 289 | reader.onload = (function (readEvent) { 290 | context.decodeAudioData(readEvent.target.result, function (buffer) { 291 | // create an audio source and connect it to the file buffer 292 | var source = context.createBufferSource(); 293 | source.buffer = buffer; 294 | source.start(0); 295 | 296 | // connect the audio stream to the audio hardware 297 | source.connect(context.destination); 298 | 299 | // create a destination for the remote browser 300 | var remote = context.createMediaStreamDestination(); 301 | 302 | // connect the remote destination to the source 303 | source.connect(remote); 304 | window.localStream = remote.stream; 305 | cb({ 'status': 'success', 'stream': true }); 306 | }); 307 | }); 308 | 309 | reader.readAsArrayBuffer(file); 310 | } 311 | } 312 | } 313 | 314 | /* Summary 315 | //setup your video 316 | pc = new RTCPeerConnection 317 | navigator.mediaDevices.getUserMedia({ audio: true, video: true }) 318 | pc.addStream(stream) 319 | 320 | //prepare your sdp1 321 | pc.createOffer() - des 322 | pc.setLocalDescription(des) 323 | pc.onicecandidate 324 | pc.localDescription 325 | 326 | //create sdp from sdp1 327 | _remoteOffer = new RTCSessionDescription sdp 328 | pc.setRemoteDescription(_remoteOffer) 329 | _remoteOffer.type == "offer" && pc.createAnswer() - desc 330 | pc.setLocalDescription(description) 331 | pc.onaddstream 332 | */ -------------------------------------------------------------------------------- /snap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/svarunan/serverless-webrtc/211c8fc7036e9ab92cb9e967a0bcafc7e0ca1567/snap.png --------------------------------------------------------------------------------