├── .gitignore ├── LICENSE ├── README.md ├── image_processing_pipeline.html ├── images ├── new_file_arriving.png └── share_new_file.png ├── package.json ├── sdp_session_manager.html ├── sdp_session_manager.js ├── video_call_with_chat_and_file_sharing.html └── webrtc_signal_server.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2013 Rob Manson, http://buildAR.com. All rights reserved. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | 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, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | getting_started_with_webrtc 2 | =========================== 3 | 4 | Example code to help you get started creating your own WebRTC applications. This application is designed to help you quickly and easily work out how WebRTC works under the hood and is written with a focus on readability. It is designed to make the Offer/Answer call setup process less opaque than the example code provided in the WebRTC API specification documents. It is not intended as a WebRTC API wrapper that hides all the complexity under a high level abstraction. Instead it provides a fully working example application with video calls, text chat and drag and drop file sharing. It is the perfect launch point for anyone who wants to start developing their own WebRTC application and in the process really learn how the WebRTC API and signaling works. 5 | 6 | For a full description of how all this code works and other examples described in more details see "Getting started with WebRTC" by Rob Manson, published by Packt Publishing (http://www.packtpub.com/getting-started-with-webrtc/book). 7 | 8 | webrtc_polyfill.js 9 | ------------------ 10 | (included inline within video_call_with_chat_and_file_sharing.html) 11 | This is a simple WebRTC polyfill based loosely on adapter.js by Adam Barth. It is designed to make it easy to write WebRTC code that runs on different browser implementations (e.g. Chrome, Firefox, etc.). 12 | 13 | video_call_with_chat_and_file_sharing.html 14 | ------------------------------------------ 15 | This is a basic web page that connects a Caller and a Callee via a Web Socket signaling server to support a video call and text based chat with file sharing. This utilises the new_file_arriving.png and share_new_file.png files in the images directory. 16 | 17 | webrtc_signal_server.js 18 | ----------------------- 19 | This is a node.js based script that provides Web server and Web Socket server functionality required to support the video_call_with_chat_and_file_sharing.html based WebRTC application. 20 | 21 | This requires the "websocket" package that can be installed from the command line by typing `npm install websocket`. 22 | 23 | or `npm install` from the project root 24 | 25 | To start this server from the command line simply type "node webrtc_signal_server.js" then point your browser at http://localhost:1234 26 | You can replace localhost with any ip address you like and you can replace 1234 with any port you like too. 27 | 28 | IMPORTANT: If you are trying to access this node server from more than one computer you MUST replace "localhost" with an IP address that can be accessed by all of these computers, at least via NAT/STUN (e.g. not 127.0.0.1). Firewalls may also limit access to port 1234 so this may also need to be updated. 29 | 30 | Extras 31 | ====== 32 | 33 | image_processing_pipeline.html 34 | ------------------------------ 35 | This code is designed to help you explore how the Video/Canvas processing pipeline works. The coding style is focused on clearly describing the concepts and is not designed to be used as production code. 36 | 37 | The key concepts covered are: 38 | - the Video/Canvas pipeline 39 | - Array Buffers vs Views 40 | - efficient frame buffer processing using multiple Views 41 | 42 | sdp_session_manager.js & sdp_session_manager.html 43 | ------------------------------------------------- 44 | This is a parser/renderer and example page designed to help you quickly get started exploring how to manipulate SDP based WebRTC sessions through javascript. None of this code deals with the actual sending/adding of SDP to any of the peers. This code simply handles the parsing, manipulation and rendering of SDP. 45 | 46 | All feedback and discussion about the structure of the Javascript Object Model that represents the session is welcome. 47 | 48 | All pull requests that improve the parsing and rendering is very welcome 8) 49 | -------------------------------------------------------------------------------- /image_processing_pipeline.html: -------------------------------------------------------------------------------- 1 | 2 | 3 |
4 | 252 | 260 | 261 | 262 | 263 |TOGGLE FILTER <-- View the source of this page to see what this does (HINT: search for "Key concept:" and "example:" to get started)
264 | 265 | 266 | -------------------------------------------------------------------------------- /images/new_file_arriving.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/buildar/getting_started_with_webrtc/00c357b4bff420c99dc090462063234c5902af13/images/new_file_arriving.png -------------------------------------------------------------------------------- /images/share_new_file.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/buildar/getting_started_with_webrtc/00c357b4bff420c99dc090462063234c5902af13/images/share_new_file.png -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "getting_started_with_webrtc", 3 | "version": "1.0.0", 4 | "description": "getting_started_with_webrtc\r ===========================", 5 | "main": "webrtc_signal_server.js", 6 | "dependencies": { 7 | "websocket": "^1.0.17" 8 | }, 9 | "devDependencies": {}, 10 | "scripts": { 11 | "test": "echo \"Error: no test specified\" && exit 1" 12 | }, 13 | "repository": { 14 | "type": "git", 15 | "url": "https://github.com/buildar/getting_started_with_webrtc.git" 16 | }, 17 | "author": "", 18 | "license": "MIT", 19 | "bugs": { 20 | "url": "https://github.com/buildar/getting_started_with_webrtc/issues" 21 | }, 22 | "homepage": "https://github.com/buildar/getting_started_with_webrtc" 23 | } 24 | -------------------------------------------------------------------------------- /sdp_session_manager.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | View source for the secret sauce! 8) 8 | 9 | 284 | 285 | -------------------------------------------------------------------------------- /sdp_session_manager.js: -------------------------------------------------------------------------------- 1 | function sdp_session_manager() { 2 | /* 3 | 4 | sdp_session_manager.js by Rob Manson (buildAR.com) 5 | 6 | An SDP parsing and rendering process based on rfc4566 and rfc3264. 7 | 8 | basic usage 9 | ----------- 10 | var sdp_sm = new sdp_session_manager(); 11 | var session = sdp_sm.parse_sdp(sdp_in); 12 | var sdp_out = sdp_sm.render_sdp(session); 13 | 14 | NOTE: sdp_out should == sdp_in 15 | 16 | 17 | The MIT License 18 | 19 | Copyright (c) 2013 Rob Manson, http://buildAR.com. All rights reserved. 20 | 21 | Permission is hereby granted, free of charge, to any person obtaining a copy 22 | of this software and associated documentation files (the "Software"), to deal 23 | in the Software without restriction, including without limitation the rights 24 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 25 | copies of the Software, and to permit persons to whom the Software is 26 | furnished to do so, subject to the following conditions: 27 | 28 | The above copyright notice and this permission notice shall be included in 29 | all copies or substantial portions of the Software. 30 | 31 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 32 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 33 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 34 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 35 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 36 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 37 | THE SOFTWARE. 38 | 39 | */ 40 | 41 | var this_session, state, current_media, current_rtpmap; 42 | 43 | var sm = new sdp_session_manager(); 44 | 45 | function sdp_session_manager() { 46 | return this; 47 | } 48 | 49 | sm.constructor.prototype.list_rtp_formats = function(i) { 50 | return this_session.media[i].rtp_formats; 51 | } 52 | 53 | sm.constructor.prototype.hold = function(i) { 54 | var m = this_session.media; 55 | if (i != undefined) { 56 | if (m[i] != undefined) { 57 | m[i].mode = "inactive"; 58 | } else { 59 | console.log("ERROR: invalid media stream - "+i); 60 | } 61 | } else { 62 | if (m.length) { 63 | for (var i in m) { 64 | m[i].mode = "inactive"; 65 | } 66 | } else { 67 | console.log("WARNING: no media streams in this session"); 68 | } 69 | } 70 | } 71 | 72 | sm.constructor.prototype.unhold = function(i, type) { 73 | var m = this_session.media; 74 | if (i != undefined) { 75 | if (m[i] != undefined) { 76 | if (type != undefined) { 77 | m[i].mode = type; 78 | } else { 79 | m[i].mode = "sendrecv"; 80 | } 81 | } else { 82 | console.log("ERROR: invalid media stream - "+i); 83 | } 84 | } else { 85 | if (m.length) { 86 | for (var i in m) { 87 | m[i].mode = "sendrecv"; 88 | } 89 | } else { 90 | console.log("WARNING: no media streams in this session"); 91 | } 92 | } 93 | } 94 | 95 | sm.constructor.prototype.reject = function(i) { 96 | var m = this_session.media; 97 | if (i != undefined) { 98 | if (m[i] != undefined) { 99 | if (m[i].description != undefined) { 100 | m[i].description.port = 0; 101 | } else { 102 | console.log("ERROR: invalid media stream - "+i+" (no description)"); 103 | } 104 | } else { 105 | console.log("ERROR: invalid media stream - "+i); 106 | } 107 | } else { 108 | if (m.length) { 109 | for (var i in m) { 110 | if (m[i].description != undefined) { 111 | m[i].description.port = 0; 112 | } else { 113 | console.log("ERROR: invalid media stream - "+i+" (no description)"); 114 | } 115 | } 116 | } else { 117 | console.log("WARNING: no media streams in this session"); 118 | } 119 | } 120 | } 121 | 122 | sm.constructor.prototype.parse_sdp = function(sdp) { 123 | this_session = { 124 | session:{ 125 | other_attributes:{}, 126 | }, 127 | media:[], 128 | }; 129 | state = "session"; 130 | current_media = undefined; 131 | current_rtpmap = undefined; 132 | 133 | var obj = {}; 134 | var obj_array = []; 135 | var lines = sdp.split("\r\n"); 136 | 137 | for (var line in lines) { 138 | var kv = lines[line].match(/(.)=(.*)/); 139 | if (kv) { 140 | if (state == "session") { 141 | parse_session(kv[1], kv[2]); 142 | } 143 | if (state == "media") { 144 | parse_media(kv[1], kv[2]); 145 | } 146 | } 147 | } 148 | 149 | return this_session; 150 | } 151 | 152 | function split_value(value) { 153 | return value.split(" "); 154 | } 155 | 156 | function split_attribute_value(value) { 157 | var match = value.match(/(.*?):(.*)/); 158 | var key = match[1]; 159 | var value = match[2]; 160 | return { key:key, value:value }; 161 | } 162 | 163 | function parse_session(key, value) { 164 | if (key == "m") { 165 | state = "media"; 166 | } else if (key == "v") { 167 | session_param = true; 168 | this_session.session.version = value; 169 | } else if (key == "o") { 170 | session_param = true; 171 | var origin = split_value(value); 172 | if (origin.length == 6) { 173 | this_session.session.username = origin[0]; 174 | this_session.session.id = origin[1]; 175 | this_session.session.version = origin[2]; 176 | this_session.session.net_type = origin[3]; 177 | this_session.session.address_type = origin[4]; 178 | this_session.session.address = origin[5]; 179 | } else { 180 | console.log("ERROR: invalid origin line ("+value+")"); 181 | } 182 | } else if (key == "s") { 183 | session_param = true; 184 | this_session.session.subject = value; 185 | } else if (key == "t") { 186 | session_param = true; 187 | var time = split_value(value); 188 | if (time.length == 2) { 189 | this_session.session.time = { 190 | start: time[0], 191 | stop: time[1], 192 | }; 193 | } else { 194 | console.log("ERROR: invalid time line ("+value+")"); 195 | } 196 | } else if (key == "a") { 197 | session_param = true; 198 | //value = value.replace(/:\s+/, ":"); 199 | var obj = split_attribute_value(value); 200 | if (value.match("BUNDLE")) { 201 | var values = split_value(obj.value); 202 | this_session.session["group_bundle"] = values.slice(1,values.length); 203 | } else if (this_session.session.other_attributes[obj.key] == undefined) { 204 | this_session.session.other_attributes[obj.key] = obj.value; 205 | } else { 206 | console.log("ERROR: invalid session attribute ("+key+":"+value+")"); 207 | } 208 | } 209 | } 210 | 211 | function parse_media(key, value) { 212 | if (key == "m") { 213 | if (current_media == undefined) { 214 | current_media = 0; 215 | } else { 216 | current_media++; 217 | } 218 | if (this_session.media[current_media] == undefined) { 219 | this_session.media[current_media] = { 220 | other_attributes: {}, 221 | }; 222 | } 223 | var media = split_value(value); 224 | if (media.length > 3) { 225 | this_session.media[current_media].type = media[0]; 226 | this_session.media[current_media].description = { 227 | port: media[1], 228 | protocol: media[2], 229 | rtp_format_preferences: media.slice(3,media.length), 230 | }; 231 | } else { 232 | console.log("ERROR: invalid media line ("+value+")"); 233 | } 234 | } else if (key == "c") { 235 | var connection = split_value(value); 236 | if (connection.length == 3) { 237 | this_session.media[current_media].connection = { 238 | net_type: connection[0], 239 | address_type: connection[1], 240 | address: connection[2], 241 | }; 242 | } else { 243 | console.log("ERROR: invalid media line ("+value+")"); 244 | } 245 | } else if (key == "a") { 246 | value = value.replace(/:\s+/, ":"); 247 | if (value.match(/(recvonly|sendrecv|sendonly|inactive)/)) { 248 | this_session.media[current_media].mode = value; 249 | } else { 250 | var obj = undefined; 251 | try { 252 | obj = split_attribute_value(value); 253 | } catch(e) { }; 254 | if (obj) { 255 | if (obj.key == "rtpmap") { 256 | var values = split_value(obj.value); 257 | var format_id = values[0]; 258 | current_rtpmap = format_id; 259 | var format_params = values[1].split("/"); 260 | var name = format_params[0]; 261 | var rate = format_params[1]; 262 | if (this_session.media[current_media].rtp_formats == undefined) { 263 | this_session.media[current_media].rtp_formats = {}; 264 | } 265 | this_session.media[current_media].rtp_formats[format_id] = { 266 | name: name, 267 | rate: rate, 268 | }; 269 | if (format_params[2] != undefined) { 270 | this_session.media[current_media].rtp_formats[format_id].encoding_parameter = format_params[2]; 271 | } 272 | } else if (obj.key == "fmtp") { 273 | var values = split_value(obj.value); 274 | this_session.media[current_media].rtp_formats[current_rtpmap].format_parameters = values.splice(1,values.length).join(" "); 275 | } else if (obj.key == "candidate") { 276 | if (this_session.media[current_media].candidates == undefined) { 277 | this_session.media[current_media].candidates = []; 278 | } 279 | this_session.media[current_media].candidates.push(obj.value); 280 | } else if (obj.key == "ssrc") { 281 | if (this_session.media[current_media].synchronisation_sources == undefined) { 282 | this_session.media[current_media].synchronisation_sources = []; 283 | } 284 | this_session.media[current_media].synchronisation_sources.push(obj.value); 285 | } else if (this_session.media[current_media].other_attributes[obj.key] == undefined) { 286 | this_session.media[current_media].other_attributes[obj.key] = obj.value; 287 | } else { 288 | var tmp = this_session.media[current_media].other_attributes[obj.key]; 289 | if (tmp instanceof Array) { 290 | tmp.push(obj.value); 291 | } else { 292 | var tmp2 = [tmp]; 293 | tmp = tmp2; 294 | } 295 | } 296 | } else if (this_session.media[current_media].other_attributes[value] == undefined) { 297 | this_session.media[current_media].other_attributes[value] = ""; 298 | } else { 299 | var tmp = this_session.media[current_media].other_attributes[value]; 300 | if (tmp instanceof Array) { 301 | tmp.push(""); 302 | } else { 303 | var tmp2 = [tmp]; 304 | tmp = tmp2; 305 | } 306 | } 307 | } 308 | } 309 | } 310 | 311 | sm.constructor.prototype.render_sdp = function() { 312 | var sdp = ""; 313 | sdp += render_session(this_session.session); 314 | sdp += render_media(this_session.media); 315 | return sdp; 316 | } 317 | 318 | function render_line(key, value) { 319 | return key+"="+value+"\r\n"; 320 | } 321 | 322 | function render_attribute(key, value) { 323 | if (value == undefined || value == "") { 324 | return key; 325 | } else { 326 | return key+":"+value; 327 | } 328 | } 329 | 330 | function render_session(obj) { 331 | var sdp = "v=0\r\n"; 332 | if (obj.username != undefined 333 | && obj.id != undefined 334 | && obj.version != undefined 335 | && obj.net_type != undefined 336 | && obj.address_type != undefined 337 | && obj.address != undefined) { 338 | var o = [obj.username, obj.id, obj.version, obj.net_type, obj.address_type, obj.address].join(" "); 339 | sdp += render_line("o", o); 340 | } 341 | if (obj.subject != undefined) { 342 | sdp += render_line("s", obj.subject); 343 | } 344 | if (obj.time != undefined && obj.time.start != undefined && obj.time.stop != undefined) { 345 | sdp += render_line("t", obj.time.start+" "+obj.time.stop); 346 | } 347 | if (obj.group_bundle != undefined) { 348 | sdp += render_line("a","group:BUNDLE "+obj.group_bundle.join(" ")); 349 | } 350 | if (Object.keys(obj.other_attributes).length) { 351 | for (var key in obj.other_attributes) { 352 | sdp += render_line("a", render_attribute(key, obj.other_attributes[key])); 353 | } 354 | } 355 | return sdp; 356 | } 357 | 358 | function render_media(obj) { 359 | var sdp = ""; 360 | for (var media in obj) { 361 | var m = obj[media]; 362 | if (m.type != undefined 363 | && m.description.port != undefined 364 | && m.description.protocol != undefined 365 | && m.description.rtp_format_preferences.length) { 366 | var d = m.description; 367 | var line = [m.type, d.port, d.protocol].join(" ")+" "+d.rtp_format_preferences.join(" "); 368 | sdp += render_line("m", line); 369 | } 370 | if (m.description.port != 0) { 371 | if (m.connection != undefined 372 | && m.connection.net_type != undefined 373 | && m.connection.address_type != undefined 374 | && m.connection.address != undefined) { 375 | var c = m.connection; 376 | sdp += render_line("c", [c.net_type, c.address_type, c.address].join(" ")); 377 | } 378 | if (m.mode != undefined) { 379 | sdp += render_line("a", render_attribute(m.mode)); 380 | } 381 | if (m.rtp_formats != undefined) { 382 | for (var fid in m.rtp_formats) { 383 | var f = m.rtp_formats[fid]; 384 | var line = "rtpmap:"+fid+" "+f.name+"/"+f.rate; 385 | if (f.encoding_parameters != undefined) { 386 | line += "/"+f.encoding_parameters; 387 | } 388 | sdp += render_line("a", line); 389 | if (f.format_parameters != undefined) { 390 | sdp += render_line("a", "fmtp:"+fid+" "+f.format_parameters); 391 | } 392 | } 393 | } 394 | if (m.other_attributes != undefined) { 395 | if (Object.keys(m.other_attributes).length) { 396 | for (var key in m.other_attributes) { 397 | sdp += render_line("a", render_attribute(key, m.other_attributes[key])); 398 | } 399 | } 400 | } 401 | if (m.candidates != undefined) { 402 | if (Object.keys(m.candidates).length) { 403 | for (var id in m.candidates) { 404 | sdp += render_line("a", "candidate:"+m.candidates[id]); 405 | } 406 | } 407 | } 408 | if (m.synchronisation_sources != undefined) { 409 | if (Object.keys(m.synchronisation_sources).length) { 410 | for (var id in m.synchronisation_sources) { 411 | sdp += render_line("a", "ssrc:"+m.synchronisation_sources[id]); 412 | } 413 | } 414 | } 415 | } 416 | } 417 | return sdp; 418 | } 419 | return sm; 420 | } 421 | -------------------------------------------------------------------------------- /video_call_with_chat_and_file_sharing.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 77 | 553 | 669 | 670 | 671 |