├── .gitignore ├── config.js.example ├── package.json ├── readme.md ├── receiver.js ├── room.js ├── sender.js ├── server.js └── static ├── bower.json ├── css └── style.css ├── img ├── spinner.gif ├── transparent-1px.png └── webrtc.png ├── index.html └── js ├── adapter-latest.js ├── adapter.js └── index.js /.gitignore: -------------------------------------------------------------------------------- 1 | npm-debug.log 2 | .classpath 3 | .project 4 | .settings/ 5 | .jshintrc 6 | node_modules/ 7 | bower_components/ 8 | *~ 9 | .DS_Store 10 | 11 | /config.js 12 | -------------------------------------------------------------------------------- /config.js.example: -------------------------------------------------------------------------------- 1 | var config = {} 2 | 3 | config.port = '7080'; 4 | config.as_uri = "http://localhost:7080/"; 5 | config.ws_uri = "ws://localhost:8888/kurento"; 6 | config.outputBitrate = 3000000; 7 | //the url of this server, this is where appRTC connects to 8 | config.appRTCUrl = '192.168.111.41:7080'; 9 | //leave uris empty to not use turn 10 | config.turn = { 11 | "username": "", 12 | "password": "", 13 | "uris": [ 14 | "turn:numb.viagenie.ca:3478", 15 | "turn:numb.viagenie.ca:3478?transport=udp", 16 | "turn:numb.viagenie.ca:3478?transport=tcp" 17 | ] 18 | }; 19 | 20 | module.exports = config; 21 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "apprtc-kurento-demo", 3 | "version": "1.0.0", 4 | "description": "Example App to connect Android AppRTC to Kurento", 5 | "main": "index.js", 6 | "private": true, 7 | "scripts": { 8 | "postinstall": "cd static && bower install" 9 | }, 10 | "author": "Max Dörfler ", 11 | "license": "LGPL", 12 | "dependencies": { 13 | "body-parser": "^1.15.0", 14 | "cookie-parser": "^1.4.1", 15 | "express": "^4.13.4", 16 | "express-session": "^1.13.0", 17 | "kurento-client": "^6.4.0", 18 | "shortid": "^2.2.4", 19 | "ws": "^1.0.1" 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | ## AppRTC - Kurento Example 2 | 3 | This is a simple example project in nodejs to demonstrate the compatibility of the [AppRTCDemo](https://github.com/njovy/AppRTCDemo) Android App with the [Kurento Media Server](http://www.kurento.org/). 4 | 5 | This server copies the API of an apprtc server so a mobile device running the AppRTCDemo App can communicate with a Webclient powered by kurento. 6 | 7 | This is a proof of concept and not meant to be stable or complete. 8 | 9 | It is heavily based on the [Kurento Example Apps](https://github.com/Kurento/kurento-tutorial-node.git). 10 | 11 | Tested with AppRTCDemo Version [b73a922be382193ec703c42284c448ff38107f11](https://github.com/njovy/AppRTCDemo/commit/b73a922be382193ec703c42284c448ff38107f11) on a Sony Xperia 12 | 13 | 14 | ## Installation 15 | 16 | 1. Clone this repository 17 | 2. copy config.js.example to config.js and adjust the values 18 | 3. run `npm install` 19 | 20 | ## Execution 21 | 22 | 1. start your kurento server 23 | 2. run `npm start` 24 | 3. on the mobile device start the [AppRTCDemo](https://github.com/njovy/AppRTCDemo) app 25 | 4. in the app settings adjust the Room server URL to your previously started server 26 | 5. connect to a room 27 | 6. on a different device direct a webbrowser to your server url. 28 | 7. enter the room name and hit start 29 | 8. you should be able to see and hear eachother 30 | 31 | ## License 32 | 33 | Licensed under [LGPL v2.1 License] 34 | -------------------------------------------------------------------------------- /receiver.js: -------------------------------------------------------------------------------- 1 | function Receiver(settings) { 2 | settings = settings ? settings : {}; 3 | this.sessionId = settings.sessionId ? settings.sessionId : null; 4 | this.websocket = settings.websocket ? settings.websocket : null; 5 | // this.pipeline = null; 6 | this.videoEndpoint = settings.videoEndpoint ? settings.videoEndpoint : null; 7 | this.audioEndpoint = settings.audioEndpoint ? settings.audioEndpoint : null;; 8 | this.candidateQueueVideo = []; 9 | this.candidateQueueAudio = []; 10 | } 11 | 12 | module.exports = Receiver; 13 | -------------------------------------------------------------------------------- /room.js: -------------------------------------------------------------------------------- 1 | var Receiver = require('./receiver'); 2 | 3 | function Room(settings) { 4 | settings = settings ? settings : {}; 5 | this.roomName = settings.roomName ? settings.roomName : null; 6 | this.sender = settings.sender ? settings.sender : null; 7 | this.receivers = settings.receivers ? settings.receivers : []; 8 | this.pipeline = settings.pipeline ? settings.pipeline : null; 9 | this.senderSdpOffer = settings.senderSdpOffer ? settings.senderSdpOffer : null; 10 | } 11 | 12 | Room.prototype.getOrCreateReceiver = function(settings) { 13 | if (this.receivers[settings.sessionId]) { 14 | return this.receivers[settings.sessionId]; 15 | } else { 16 | var receiver = new Receiver(settings); 17 | this.receivers[settings.sessionId] = receiver; 18 | return receiver; 19 | } 20 | } 21 | 22 | module.exports = Room; 23 | -------------------------------------------------------------------------------- /sender.js: -------------------------------------------------------------------------------- 1 | function Sender(settings) { 2 | settings = settings ? settings : {}; 3 | this.sessionId = settings.sessionId ? settings.sessionId : null; 4 | this.clientId = settings.clientId ? settings.clientId : null; 5 | this.websocket = settings.websocket ? settings.websocket : null; 6 | this.endpoint = settings.endpoint ? settings.endpoint : null; 7 | this.candidateQueue = []; 8 | // this.receivers = []; 9 | } 10 | 11 | Sender.prototype.getPipeline = function() { 12 | return this.pipeline; 13 | }; 14 | 15 | module.exports = Sender; 16 | -------------------------------------------------------------------------------- /server.js: -------------------------------------------------------------------------------- 1 | /* 2 | * (C) Copyright 2016 Ape Unit GmbH (http://apeunit.com/) 3 | * 4 | * All rights reserved. This program and the accompanying materials 5 | * are made available under the terms of the GNU Lesser General Public License 6 | * (LGPL) version 2.1 which accompanies this distribution, and is available at 7 | * http://www.gnu.org/licenses/lgpl-2.1.html 8 | * 9 | * This library is distributed in the hope that it will be useful, 10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 12 | * Lesser General Public License for more details. 13 | * 14 | * Developed by Max Dörfler 15 | */ 16 | 17 | var express = require('express'); 18 | var session = require('express-session'); 19 | var cookieParser = require('cookie-parser'); 20 | var bodyParser = require('body-parser'); 21 | var ws = require('ws'); 22 | var kurento = require('kurento-client'); 23 | var shortid = require('shortid'); 24 | var config = require('./config'); 25 | 26 | var Sender = require('./sender'); 27 | var Receiver = require('./receiver'); 28 | var Room = require('./room'); 29 | 30 | // var as_uri = config.as_uri; 31 | var serverUrl = config.appRTCUrl; 32 | var ws_uri = config.ws_uri; 33 | var port = config.port; 34 | 35 | var kurentoClient = null; 36 | var rooms = []; 37 | 38 | var app = express(); 39 | app.use(bodyParser.json({ 40 | type: 'text' 41 | })); 42 | 43 | app.use(cookieParser()); 44 | 45 | var sessionHandler = session({ 46 | secret: 'none', 47 | rolling: true, 48 | resave: true, 49 | saveUninitialized: true 50 | }); 51 | 52 | app.use(sessionHandler); 53 | 54 | var server = app.listen(port, function() { 55 | console.log('Server started on port', port); 56 | }); 57 | 58 | app.use(function(req, res, next) { 59 | console.log(req.protocol, req.method, req.path, req.get('Content-Type')); 60 | next(); 61 | }); 62 | 63 | 64 | var apprtcWs = new ws.Server({ 65 | server: server, 66 | path: '/ws' 67 | }); 68 | 69 | apprtcWs.on('connection', function(ws) { 70 | var sessionId = null; 71 | var request = ws.upgradeReq; 72 | var response = { 73 | writeHead: {} 74 | }; 75 | 76 | sessionHandler(request, response, function(err) { 77 | sessionId = request.session.id; 78 | console.log('apprtcWs received with sessionId ' + sessionId); 79 | }); 80 | 81 | ws.on('error', function(error) { 82 | console.log('apprtcWs Connection ' + sessionId + ' error', error); 83 | getRoomBySession(sessionId, function(err, room) { 84 | stopReceive(room); 85 | stopSend(room); 86 | }); 87 | }); 88 | 89 | ws.on('close', function() { 90 | console.log('apprtcWs Connection ' + sessionId + ' closed'); 91 | getRoomBySession(sessionId, function(err, room) { 92 | stopReceive(room); 93 | stopSend(room); 94 | }); 95 | }); 96 | 97 | ws.on('message', function(_message) { 98 | var message = JSON.parse(_message); 99 | console.log('apprtcWs ' + sessionId + ' received message ', message); 100 | var clientId = message.clientid ? message.clientid : "empty"; 101 | var roomname = message.roomid ? message.roomid : "emptyID"; 102 | 103 | switch (message.cmd) { 104 | case 'register': 105 | console.log('register called'); 106 | getRoom(roomname, function(err, room) { 107 | if (err) { 108 | console.error(err); 109 | ws.send(JSON.stringify({ 110 | msg: {}, 111 | error: err 112 | })); 113 | } 114 | if (!room) { 115 | console.error("Room not found"); 116 | ws.send(JSON.stringify({ 117 | msg: {}, 118 | error: 'Room not found' 119 | })); 120 | } 121 | if (!room.sender) { 122 | room.sender = new Sender({ 123 | sessionId: sessionId, 124 | clientId: clientId, 125 | websocket: ws 126 | }); 127 | } else { 128 | room.sender.websocket = ws; 129 | room.sender.clientId = clientId; 130 | room.sender.sessionId = sessionId; 131 | } 132 | // console.log('sender created', room.sender); 133 | // console.log(room); 134 | //TODO: what if already offered? 135 | if (room.senderSdpOffer) { 136 | console.log('TODO: got the sdpOffer first'); 137 | } 138 | }); 139 | break; 140 | 141 | case 'startWebRtc': 142 | console.log('startWebRtc'); 143 | var sdpOffer = message.sdpOffer; 144 | var roomName = message.roomName; 145 | getRoom(roomName, function(err, room) { 146 | if (!room) { 147 | return ws.send(JSON.stringify({ 148 | id: 'error', 149 | message: 'Room not found' 150 | })); 151 | } 152 | // sessionId = request.session.id; 153 | startWebRtc(room, sessionId, ws, sdpOffer, function(error, sdpAnswer) { 154 | if (error) { 155 | console.log(error); 156 | return ws.send(JSON.stringify({ 157 | id: 'error', 158 | message: error 159 | })); 160 | } 161 | console.log("startWebRtc response:", sdpAnswer); 162 | ws.send(JSON.stringify({ 163 | id: 'startResponse', 164 | sdpAnswer: sdpAnswer 165 | })); 166 | }); 167 | }); 168 | break; 169 | 170 | case 'onIceCandidate': 171 | var roomName = message.roomName; 172 | getRoom(roomName, function(err, room) { 173 | if (!room) { 174 | return ws.send(JSON.stringify({ 175 | id: 'error', 176 | message: 'Room not found' 177 | })); 178 | } 179 | onIceCandidate(room, sessionId, message.candidate); 180 | }); 181 | break; 182 | 183 | case 'stop': 184 | var roomName = message.roomName; 185 | getRoom(roomName, function(err, room) { 186 | if (room) { 187 | stopReceive(room); 188 | } 189 | }); 190 | break; 191 | 192 | default: 193 | console.log('something else called'); 194 | } 195 | }); 196 | }); 197 | 198 | app.all('/join/:roomname', function(req, res) { 199 | console.log('join called', req.body); 200 | var roomName = req.params.roomname ? req.params.roomname : "empty"; 201 | 202 | //create room 203 | getRoom(roomName, function(err, room) { 204 | if (err) { 205 | console.error(err); 206 | return res.json({ 207 | "result": "ERROR" 208 | }); 209 | } 210 | if (!room) { 211 | room = new Room({ 212 | roomName: roomName 213 | }); 214 | rooms.push(room); 215 | } 216 | console.log(room); 217 | 218 | //generate a client ID 219 | var clientId = shortid.generate(); 220 | 221 | var response = { 222 | "params": { 223 | "is_initiator": "true", 224 | "room_link": "http://" + serverUrl + "/r/" + roomName, 225 | "version_info": "{\"gitHash\": \"029b6dc4742cae3bcb6c5ac6a26d65167c522b9f\", \"branch\": \"master\", \"time\": \"Wed Dec 9 16:08:29 2015 +0100\"}", 226 | "messages": [], 227 | "error_messages": [], 228 | "client_id": clientId, 229 | "bypass_join_confirmation": "false", 230 | "media_constraints": "{\"audio\": true, \"video\": true}", 231 | "include_loopback_js": "", 232 | "turn_url": "http://" + serverUrl + "/turn", 233 | "is_loopback": "false", 234 | "wss_url": "ws://" + serverUrl + "/ws", 235 | "pc_constraints": "{\"optional\": []}", 236 | "pc_config": "{\"rtcpMuxPolicy\": \"require\", \"bundlePolicy\": \"max-bundle\", \"iceServers\": []}", 237 | "wss_post_url": "http://" + serverUrl + "", 238 | "offer_options": "{}", 239 | "warning_messages": [], 240 | "room_id": roomName, 241 | "turn_transports": "" 242 | }, 243 | "result": "SUCCESS" 244 | }; 245 | res.json(response); 246 | }); 247 | }); 248 | 249 | app.all('/leave/:roomname/:clientId', function(req, res) { 250 | console.log('leave called', req.body); 251 | var roomName = req.params.roomname ? req.params.roomname : "empty"; 252 | var clientId = req.params.clientId ? req.params.clientId : "emptyID"; 253 | getRoom(roomName, function(err, room) { 254 | if (room) { 255 | stopSend(room); 256 | } 257 | res.send('todo'); 258 | }); 259 | }); 260 | 261 | app.all('/turn', function(req, res) { 262 | console.log('turn called', req.body); 263 | 264 | var response = config.turn; 265 | res.json(response); 266 | }); 267 | 268 | app.all('/message/:roomname/:clientId', function(req, res) { 269 | console.log('message called', req.body.type); 270 | var roomName = req.params.roomname ? req.params.roomname : "empty"; 271 | var clientId = req.params.clientId ? req.params.clientId : "emptyID"; 272 | var message = req.body; 273 | getRoom(roomName, function(err, room) { 274 | if (err) { 275 | res.json({ 276 | "result": "ERROR", 277 | "error": err 278 | }); 279 | } 280 | if (!room) { 281 | //I dunno 282 | } 283 | 284 | // console.log(message, roomName, id); 285 | switch (message.type) { 286 | case 'candidate': 287 | var sender = room.sender; 288 | console.log('candidate', message.candidate); 289 | var rewrittenCandidate = { 290 | candidate: message.candidate, 291 | sdpMid: 'sdparta_0', 292 | sdpMLineIndex: message.label 293 | }; 294 | // console.log(rewrittenCandidate); 295 | 296 | var candidate = kurento.register.complexTypes.IceCandidate(rewrittenCandidate); 297 | 298 | if (sender.endpoint) { 299 | console.info('appRTC Ice Candidate addIceCandidate', candidate); 300 | sender.endpoint.addIceCandidate(candidate); 301 | } else { 302 | //TODO: 303 | console.info('appRTC Ice Candidate Queueing candidate', sender.candidateQueue); 304 | sender.candidateQueue.push(candidate); 305 | } 306 | break; 307 | case 'offer': 308 | if (room.sender && room.sender.websocket) { 309 | var sender = room.sender; 310 | console.log('yay websocket is present'); 311 | var onCandidate = function(event) { 312 | // console.log("onCandidate"); 313 | var candidate = kurento.register.complexTypes.IceCandidate(event.candidate); 314 | var candidateAnswer = { 315 | msg: { 316 | type: 'candidate', 317 | label: event.candidate.sdpMLineIndex, 318 | id: event.candidate.sdpMid, 319 | candidate: event.candidate.candidate 320 | }, 321 | error: '' 322 | }; 323 | sender.websocket.send(JSON.stringify(candidateAnswer)); 324 | }; 325 | startSendWebRtc(room, message.sdp, onCandidate, function(error, sdpAnswer) { 326 | console.log('started webrtc in POST', error); 327 | var sendSdpAnswer = { 328 | msg: { 329 | type: 'answer', 330 | sdp: sdpAnswer 331 | }, 332 | error: '' 333 | }; 334 | 335 | sender.websocket.send(JSON.stringify(sendSdpAnswer)); 336 | // sendSendStatus(); 337 | }); 338 | } else { 339 | console.log('no websocket is present'); 340 | room.senderSdpOffer = message.sdp; 341 | } 342 | break; 343 | default: 344 | console.log('default'); 345 | } 346 | //just send success 347 | res.json({ 348 | "result": "SUCCESS" 349 | }); 350 | }); 351 | }); 352 | 353 | function getKurentoClient(callback) { 354 | if (kurentoClient !== null) { 355 | return callback(null, kurentoClient); 356 | } 357 | kurento(ws_uri, function(error, _kurentoClient) { 358 | if (error) { 359 | console.log("Could not find media server at address " + ws_uri); 360 | return callback("Could not find media server at address" + ws_uri + ". Exiting with error " + error); 361 | } 362 | 363 | kurentoClient = _kurentoClient; 364 | callback(null, kurentoClient); 365 | }); 366 | } 367 | 368 | function getPipeline(room, callback) { 369 | if (!room) { 370 | return callback('No Room'); 371 | } 372 | 373 | if (room.pipeline !== null) { 374 | console.log('saved pipeline'); 375 | return callback(null, room.pipeline); 376 | } 377 | getKurentoClient(function(error, kurentoClient) { 378 | if (error) { 379 | return callback(error); 380 | } 381 | kurentoClient.create('MediaPipeline', function(error, _pipeline) { 382 | if (error) { 383 | return callback(error); 384 | } 385 | room.pipeline = _pipeline; 386 | return callback(null, room.pipeline); 387 | }); 388 | }); 389 | }; 390 | 391 | function getRoom(roomName, callback) { 392 | console.log("Looking for room:", roomName); 393 | for (var i = 0; i < rooms.length; i++) { 394 | if (rooms[i].roomName == roomName) { 395 | return callback(null, rooms[i]); 396 | } 397 | } 398 | return callback(null, null); 399 | }; 400 | 401 | function getRoomBySession(sessionId, callback) { 402 | console.log("Looking for room with session:", sessionId); 403 | for (var i = 0; i < rooms.length; i++) { 404 | if (rooms[i].sender && rooms[i].sender.sessionId == sessionId) { 405 | return callback(null, rooms[i]); 406 | } 407 | } 408 | return callback(null, null); 409 | }; 410 | 411 | function startSendWebRtc(room, sdpOffer, onCandidate, callback) { 412 | //create webrtc endpoint 413 | getPipeline(room, function(error, pipeline) { 414 | if (error) { 415 | return callback(error); 416 | } 417 | var sender = room.sender; 418 | pipeline.create('WebRtcEndpoint', function(error, _webRtcEndpoint) { 419 | if (error) { 420 | return callback(error); 421 | } 422 | console.log("got send webrtc webRtcEndpoint"); 423 | // rtpEndpoint = _webRtcEndpoint; 424 | sender.endpoint = _webRtcEndpoint; 425 | 426 | console.log("read queue:", sender.candidateQueue); 427 | if (sender.candidateQueue) { 428 | while (sender.candidateQueue.length) { 429 | console.log("adding candidate from queue"); 430 | var candidate = sender.candidateQueue.shift(); 431 | sender.endpoint.addIceCandidate(candidate); 432 | } 433 | } 434 | 435 | sender.endpoint.processOffer(sdpOffer, function(error, sdpAnswer) { 436 | if (error) { 437 | sender.endpoint.release(); 438 | return callback(error); 439 | } 440 | 441 | sender.endpoint.on('OnIceCandidate', function(event) { 442 | onCandidate(event); 443 | }); 444 | 445 | sender.endpoint.gatherCandidates(function(error) { 446 | if (error) { 447 | stopReceive(sessionId); 448 | return callback(error); 449 | } 450 | }); 451 | 452 | console.log("sending sdp answer"); 453 | return callback(null, sdpAnswer); 454 | }); 455 | }); 456 | }); 457 | }; 458 | 459 | function startWebRtc(room, sessionId, ws, sdpOffer, callback) { 460 | if (!room) { 461 | return callback('startWebRtc: No Room'); 462 | } 463 | 464 | var sender = room.sender; 465 | if (!sender || !sender.endpoint) { 466 | return callback('No Sending Endpoint'); 467 | } 468 | 469 | var receiver = room.getOrCreateReceiver({ 470 | sessionId: sessionId, 471 | websocket: ws 472 | }); 473 | if (!receiver) { 474 | return callback('Error getting or creating Receiver'); 475 | } 476 | var pipeline = room.pipeline; 477 | console.log('got pipeline'); 478 | //create webrtc endpoint 479 | console.log("Creating WebRtcEndpoint"); 480 | pipeline.create('WebRtcEndpoint', function(error, _webRtcEndpoint) { 481 | if (error) { 482 | return callback(error); 483 | } 484 | console.log("got webRtcEndpoint"); 485 | // webRtcEndpoint = _webRtcEndpoint; 486 | receiver.videoEndpoint = _webRtcEndpoint; 487 | 488 | if (receiver.candidateQueueVideo) { 489 | while (receiver.candidateQueueVideo.length) { 490 | console.log("adding candidate from queue"); 491 | var candidate = receiver.candidateQueueVideo.shift(); 492 | receiver.videoEndpoint.addIceCandidate(candidate); 493 | } 494 | } 495 | 496 | receiver.videoEndpoint.processOffer(sdpOffer, function(error, sdpAnswer) { 497 | if (error) { 498 | receiver.videoEndpoint.release(); 499 | return callback(error); 500 | } 501 | sender.endpoint.connect(receiver.videoEndpoint, function(error) { 502 | if (error) { 503 | receiver.videoEndpoint.release(); 504 | console.log(error); 505 | return callback(error); 506 | } 507 | 508 | receiver.videoEndpoint.on('OnIceCandidate', function(event) { 509 | var candidate = kurento.register.complexTypes.IceCandidate(event.candidate); 510 | ws.send(JSON.stringify({ 511 | id: 'iceCandidate', 512 | candidate: candidate 513 | })); 514 | }); 515 | 516 | receiver.videoEndpoint.gatherCandidates(function(error) { 517 | if (error) { 518 | stopReceive(sessionId); 519 | return callback(error); 520 | } 521 | }); 522 | 523 | receiver.videoEndpoint.connect(sender.endpoint, function(error) { 524 | if (error) { 525 | receiver.videoEndpoint.release(); 526 | console.log(error); 527 | return callback(error); 528 | } 529 | }); 530 | 531 | return callback(null, sdpAnswer); 532 | }); 533 | }); 534 | }); 535 | } 536 | 537 | function onIceCandidate(room, sessionId, _candidate) { 538 | var candidate = kurento.register.complexTypes.IceCandidate(_candidate); 539 | 540 | console.log('onIceCandidate called'); 541 | var receiver = room.receivers[sessionId]; 542 | if (!receiver) { 543 | return callback('Error getting Receiver'); 544 | } 545 | console.log('onIceCandidate receiver', receiver); 546 | 547 | if (receiver.videoEndpoint) { 548 | console.info('Adding candidate'); 549 | receiver.videoEndpoint.addIceCandidate(candidate); 550 | } else { 551 | console.info('Queueing candidate'); 552 | receiver.candidateQueueVideo.push(candidate); 553 | } 554 | } 555 | 556 | function stopSend(room) { 557 | console.log("stopSend"); 558 | if (!room) { 559 | console.error("no room"); 560 | return; 561 | } 562 | if (room.pipeline) { 563 | room.pipeline.release(); 564 | } 565 | if (room.sender && room.sender.endpoint) { 566 | room.sender.endpoint.release(); 567 | room.sender.endpoint = null; 568 | } 569 | // room.sender = null; 570 | var index = rooms.indexOf(room); 571 | if (index > -1) { 572 | rooms.splice(index, 1); 573 | } 574 | //TODO: release all receivers? 575 | }; 576 | 577 | function stopReceive(room) { 578 | console.log('TODO: stopReceive', room); 579 | if (!room) { 580 | console.error("stopReceive no room"); 581 | return; 582 | } 583 | // var receiver = receivers[sessionId]; 584 | // if (receiver && receiver.videoEndpoint) { 585 | // receiver.videoEndpoint.release(); 586 | // console.log("Released receiving videoEndpoint"); 587 | // } 588 | // if (receiver && receiver.audioEndpoint) { 589 | // receiver.audioEndpoint.release(); 590 | // console.log("Released receiving audioEndpoint"); 591 | // } 592 | } 593 | 594 | app.use(express.static('static')); 595 | -------------------------------------------------------------------------------- /static/bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "apprtc-kurento-demo", 3 | "version": "1.0.0", 4 | "authors": [ 5 | "Max Dörfler " 6 | ], 7 | "description": "Example App to connect Android AppRTC to Kurento", 8 | "main": "index.html", 9 | "license": "LGPL", 10 | "homepage": "http://www.apeunit.com", 11 | "ignore": [ 12 | "**/.*", 13 | "node_modules", 14 | "bower_components", 15 | "test", 16 | "tests" 17 | ], 18 | "dependencies": { 19 | "kurento-utils": "master", 20 | "jquery": "~2.2.1", 21 | "bootstrap": "~3.3.6" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /static/css/style.css: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/apeunit/AppRTC-Kurento-Example/adeec4857c4275810422a27b0878a4e3ce78ffa3/static/css/style.css -------------------------------------------------------------------------------- /static/img/spinner.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/apeunit/AppRTC-Kurento-Example/adeec4857c4275810422a27b0878a4e3ce78ffa3/static/img/spinner.gif -------------------------------------------------------------------------------- /static/img/transparent-1px.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/apeunit/AppRTC-Kurento-Example/adeec4857c4275810422a27b0878a4e3ce78ffa3/static/img/transparent-1px.png -------------------------------------------------------------------------------- /static/img/webrtc.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/apeunit/AppRTC-Kurento-Example/adeec4857c4275810422a27b0878a4e3ce78ffa3/static/img/webrtc.png -------------------------------------------------------------------------------- /static/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | AppRTC to Kurento Demo 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 |
21 |
22 |
23 |

Incoming

24 | 25 |
26 |
27 | 28 | 29 | Start 30 |
31 |
32 | 33 | Stop 34 |
35 |
36 |

Outgoing

37 | 38 | 39 |
40 |
41 |
42 | 43 | 44 | -------------------------------------------------------------------------------- /static/js/adapter-latest.js: -------------------------------------------------------------------------------- 1 | (function(f){if(typeof exports==="object"&&typeof module!=="undefined"){module.exports=f()}else if(typeof define==="function"&&define.amd){define([],f)}else{var g;if(typeof window!=="undefined"){g=window}else if(typeof global!=="undefined"){g=global}else if(typeof self!=="undefined"){g=self}else{g=this}g.adapter = f()}})(function(){var define,module,exports;return (function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o 0 && typeof selector === 'function') { 189 | return origGetStats(selector, successCallback); 190 | } 191 | 192 | var fixChromeStats_ = function(response) { 193 | var standardReport = {}; 194 | var reports = response.result(); 195 | reports.forEach(function(report) { 196 | var standardStats = { 197 | id: report.id, 198 | timestamp: report.timestamp, 199 | type: report.type 200 | }; 201 | report.names().forEach(function(name) { 202 | standardStats[name] = report.stat(name); 203 | }); 204 | standardReport[standardStats.id] = standardStats; 205 | }); 206 | 207 | return standardReport; 208 | }; 209 | 210 | if (arguments.length >= 2) { 211 | var successCallbackWrapper_ = function(response) { 212 | args[1](fixChromeStats_(response)); 213 | }; 214 | 215 | return origGetStats.apply(this, [successCallbackWrapper_, arguments[0]]); 216 | } 217 | 218 | // promise-support 219 | return new Promise(function(resolve, reject) { 220 | if (args.length === 1 && selector === null) { 221 | origGetStats.apply(self, [ 222 | function(response) { 223 | resolve.apply(null, [fixChromeStats_(response)]); 224 | }, reject]); 225 | } else { 226 | origGetStats.apply(self, [resolve, reject]); 227 | } 228 | }); 229 | }; 230 | 231 | return pc; 232 | }; 233 | window.RTCPeerConnection.prototype = webkitRTCPeerConnection.prototype; 234 | 235 | // wrap static methods. Currently just generateCertificate. 236 | if (webkitRTCPeerConnection.generateCertificate) { 237 | Object.defineProperty(window.RTCPeerConnection, 'generateCertificate', { 238 | get: function() { 239 | if (arguments.length) { 240 | return webkitRTCPeerConnection.generateCertificate.apply(null, 241 | arguments); 242 | } else { 243 | return webkitRTCPeerConnection.generateCertificate; 244 | } 245 | } 246 | }); 247 | } 248 | 249 | // add promise support 250 | ['createOffer', 'createAnswer'].forEach(function(method) { 251 | var nativeMethod = webkitRTCPeerConnection.prototype[method]; 252 | webkitRTCPeerConnection.prototype[method] = function() { 253 | var self = this; 254 | if (arguments.length < 1 || (arguments.length === 1 && 255 | typeof(arguments[0]) === 'object')) { 256 | var opts = arguments.length === 1 ? arguments[0] : undefined; 257 | return new Promise(function(resolve, reject) { 258 | nativeMethod.apply(self, [resolve, reject, opts]); 259 | }); 260 | } else { 261 | return nativeMethod.apply(this, arguments); 262 | } 263 | }; 264 | }); 265 | 266 | ['setLocalDescription', 'setRemoteDescription', 267 | 'addIceCandidate'].forEach(function(method) { 268 | var nativeMethod = webkitRTCPeerConnection.prototype[method]; 269 | webkitRTCPeerConnection.prototype[method] = function() { 270 | var args = arguments; 271 | var self = this; 272 | return new Promise(function(resolve, reject) { 273 | nativeMethod.apply(self, [args[0], 274 | function() { 275 | resolve(); 276 | if (args.length >= 2) { 277 | args[1].apply(null, []); 278 | } 279 | }, 280 | function(err) { 281 | reject(err); 282 | if (args.length >= 3) { 283 | args[2].apply(null, [err]); 284 | } 285 | }] 286 | ); 287 | }); 288 | }; 289 | }); 290 | }, 291 | 292 | shimGetUserMedia: function() { 293 | var constraintsToChrome_ = function(c) { 294 | if (typeof c !== 'object' || c.mandatory || c.optional) { 295 | return c; 296 | } 297 | var cc = {}; 298 | Object.keys(c).forEach(function(key) { 299 | if (key === 'require' || key === 'advanced' || key === 'mediaSource') { 300 | return; 301 | } 302 | var r = (typeof c[key] === 'object') ? c[key] : {ideal: c[key]}; 303 | if (r.exact !== undefined && typeof r.exact === 'number') { 304 | r.min = r.max = r.exact; 305 | } 306 | var oldname_ = function(prefix, name) { 307 | if (prefix) { 308 | return prefix + name.charAt(0).toUpperCase() + name.slice(1); 309 | } 310 | return (name === 'deviceId') ? 'sourceId' : name; 311 | }; 312 | if (r.ideal !== undefined) { 313 | cc.optional = cc.optional || []; 314 | var oc = {}; 315 | if (typeof r.ideal === 'number') { 316 | oc[oldname_('min', key)] = r.ideal; 317 | cc.optional.push(oc); 318 | oc = {}; 319 | oc[oldname_('max', key)] = r.ideal; 320 | cc.optional.push(oc); 321 | } else { 322 | oc[oldname_('', key)] = r.ideal; 323 | cc.optional.push(oc); 324 | } 325 | } 326 | if (r.exact !== undefined && typeof r.exact !== 'number') { 327 | cc.mandatory = cc.mandatory || {}; 328 | cc.mandatory[oldname_('', key)] = r.exact; 329 | } else { 330 | ['min', 'max'].forEach(function(mix) { 331 | if (r[mix] !== undefined) { 332 | cc.mandatory = cc.mandatory || {}; 333 | cc.mandatory[oldname_(mix, key)] = r[mix]; 334 | } 335 | }); 336 | } 337 | }); 338 | if (c.advanced) { 339 | cc.optional = (cc.optional || []).concat(c.advanced); 340 | } 341 | return cc; 342 | }; 343 | 344 | var getUserMedia_ = function(constraints, onSuccess, onError) { 345 | if (constraints.audio) { 346 | constraints.audio = constraintsToChrome_(constraints.audio); 347 | } 348 | if (constraints.video) { 349 | constraints.video = constraintsToChrome_(constraints.video); 350 | } 351 | logging('chrome: ' + JSON.stringify(constraints)); 352 | return navigator.webkitGetUserMedia(constraints, onSuccess, onError); 353 | }; 354 | navigator.getUserMedia = getUserMedia_; 355 | 356 | // Returns the result of getUserMedia as a Promise. 357 | var getUserMediaPromise_ = function(constraints) { 358 | return new Promise(function(resolve, reject) { 359 | navigator.getUserMedia(constraints, resolve, reject); 360 | }); 361 | } 362 | 363 | if (!navigator.mediaDevices) { 364 | navigator.mediaDevices = {getUserMedia: getUserMediaPromise_, 365 | enumerateDevices: function() { 366 | return new Promise(function(resolve) { 367 | var kinds = {audio: 'audioinput', video: 'videoinput'}; 368 | return MediaStreamTrack.getSources(function(devices) { 369 | resolve(devices.map(function(device) { 370 | return {label: device.label, 371 | kind: kinds[device.kind], 372 | deviceId: device.id, 373 | groupId: ''}; 374 | })); 375 | }); 376 | }); 377 | }}; 378 | } 379 | 380 | // A shim for getUserMedia method on the mediaDevices object. 381 | // TODO(KaptenJansson) remove once implemented in Chrome stable. 382 | if (!navigator.mediaDevices.getUserMedia) { 383 | navigator.mediaDevices.getUserMedia = function(constraints) { 384 | return getUserMediaPromise_(constraints); 385 | }; 386 | } else { 387 | // Even though Chrome 45 has navigator.mediaDevices and a getUserMedia 388 | // function which returns a Promise, it does not accept spec-style 389 | // constraints. 390 | var origGetUserMedia = navigator.mediaDevices.getUserMedia. 391 | bind(navigator.mediaDevices); 392 | navigator.mediaDevices.getUserMedia = function(c) { 393 | if (c) { 394 | logging('spec: ' + JSON.stringify(c)); // whitespace for alignment 395 | c.audio = constraintsToChrome_(c.audio); 396 | c.video = constraintsToChrome_(c.video); 397 | logging('chrome: ' + JSON.stringify(c)); 398 | } 399 | return origGetUserMedia(c); 400 | }.bind(this); 401 | } 402 | 403 | // Dummy devicechange event methods. 404 | // TODO(KaptenJansson) remove once implemented in Chrome stable. 405 | if (typeof navigator.mediaDevices.addEventListener === 'undefined') { 406 | navigator.mediaDevices.addEventListener = function() { 407 | logging('Dummy mediaDevices.addEventListener called.'); 408 | }; 409 | } 410 | if (typeof navigator.mediaDevices.removeEventListener === 'undefined') { 411 | navigator.mediaDevices.removeEventListener = function() { 412 | logging('Dummy mediaDevices.removeEventListener called.'); 413 | }; 414 | } 415 | }, 416 | 417 | // Attach a media stream to an element. 418 | attachMediaStream: function(element, stream) { 419 | logging('DEPRECATED, attachMediaStream will soon be removed.'); 420 | if (browserDetails.version >= 43) { 421 | element.srcObject = stream; 422 | } else if (typeof element.src !== 'undefined') { 423 | element.src = URL.createObjectURL(stream); 424 | } else { 425 | logging('Error attaching stream to element.'); 426 | } 427 | }, 428 | 429 | reattachMediaStream: function(to, from) { 430 | logging('DEPRECATED, reattachMediaStream will soon be removed.'); 431 | if (browserDetails.version >= 43) { 432 | to.srcObject = from.srcObject; 433 | } else { 434 | to.src = from.src; 435 | } 436 | } 437 | } 438 | 439 | // Expose public methods. 440 | module.exports = { 441 | shimOnTrack: chromeShim.shimOnTrack, 442 | shimSourceObject: chromeShim.shimSourceObject, 443 | shimPeerConnection: chromeShim.shimPeerConnection, 444 | shimGetUserMedia: chromeShim.shimGetUserMedia, 445 | attachMediaStream: chromeShim.attachMediaStream, 446 | reattachMediaStream: chromeShim.reattachMediaStream 447 | }; 448 | 449 | },{"../utils.js":6}],3:[function(require,module,exports){ 450 | /* 451 | * Copyright (c) 2016 The WebRTC project authors. All Rights Reserved. 452 | * 453 | * Use of this source code is governed by a BSD-style license 454 | * that can be found in the LICENSE file in the root of the source 455 | * tree. 456 | */ 457 | 'use strict'; 458 | 459 | // SDP helpers. 460 | var SDPUtils = {}; 461 | 462 | // Generate an alphanumeric identifier for cname or mids. 463 | // TODO: use UUIDs instead? https://gist.github.com/jed/982883 464 | SDPUtils.generateIdentifier = function() { 465 | return Math.random().toString(36).substr(2, 10); 466 | }; 467 | 468 | // The RTCP CNAME used by all peerconnections from the same JS. 469 | SDPUtils.localCName = SDPUtils.generateIdentifier(); 470 | 471 | 472 | // Splits SDP into lines, dealing with both CRLF and LF. 473 | SDPUtils.splitLines = function(blob) { 474 | return blob.trim().split('\n').map(function(line) { 475 | return line.trim(); 476 | }); 477 | }; 478 | // Splits SDP into sessionpart and mediasections. Ensures CRLF. 479 | SDPUtils.splitSections = function(blob) { 480 | var parts = blob.split('\r\nm='); 481 | return parts.map(function(part, index) { 482 | return (index > 0 ? 'm=' + part : part).trim() + '\r\n'; 483 | }); 484 | }; 485 | 486 | // Returns lines that start with a certain prefix. 487 | SDPUtils.matchPrefix = function(blob, prefix) { 488 | return SDPUtils.splitLines(blob).filter(function(line) { 489 | return line.indexOf(prefix) === 0; 490 | }); 491 | }; 492 | 493 | // Parses an ICE candidate line. Sample input: 494 | // candidate:702786350 2 udp 41819902 8.8.8.8 60769 typ relay raddr 8.8.8.8 rport 55996" 495 | SDPUtils.parseCandidate = function(line) { 496 | var parts; 497 | // Parse both variants. 498 | if (line.indexOf('a=candidate:') === 0) { 499 | parts = line.substring(12).split(' '); 500 | } else { 501 | parts = line.substring(10).split(' '); 502 | } 503 | 504 | var candidate = { 505 | foundation: parts[0], 506 | component: parts[1], 507 | protocol: parts[2].toLowerCase(), 508 | priority: parseInt(parts[3], 10), 509 | ip: parts[4], 510 | port: parseInt(parts[5], 10), 511 | // skip parts[6] == 'typ' 512 | type: parts[7] 513 | }; 514 | 515 | for (var i = 8; i < parts.length; i += 2) { 516 | switch (parts[i]) { 517 | case 'raddr': 518 | candidate.relatedAddress = parts[i + 1]; 519 | break; 520 | case 'rport': 521 | candidate.relatedPort = parseInt(parts[i + 1], 10); 522 | break; 523 | case 'tcptype': 524 | candidate.tcpType = parts[i + 1]; 525 | break; 526 | default: // Unknown extensions are silently ignored. 527 | break; 528 | } 529 | } 530 | return candidate; 531 | }; 532 | 533 | // Translates a candidate object into SDP candidate attribute. 534 | SDPUtils.writeCandidate = function(candidate) { 535 | var sdp = []; 536 | sdp.push(candidate.foundation); 537 | sdp.push(candidate.component); 538 | sdp.push(candidate.protocol.toUpperCase()); 539 | sdp.push(candidate.priority); 540 | sdp.push(candidate.ip); 541 | sdp.push(candidate.port); 542 | 543 | var type = candidate.type; 544 | sdp.push('typ'); 545 | sdp.push(type); 546 | if (type !== 'host' && candidate.relatedAddress && 547 | candidate.relatedPort) { 548 | sdp.push('raddr'); 549 | sdp.push(candidate.relatedAddress); // was: relAddr 550 | sdp.push('rport'); 551 | sdp.push(candidate.relatedPort); // was: relPort 552 | } 553 | if (candidate.tcpType && candidate.protocol.toLowerCase() === 'tcp') { 554 | sdp.push('tcptype'); 555 | sdp.push(candidate.tcpType); 556 | } 557 | return 'candidate:' + sdp.join(' '); 558 | }; 559 | 560 | // Parses an rtpmap line, returns RTCRtpCoddecParameters. Sample input: 561 | // a=rtpmap:111 opus/48000/2 562 | SDPUtils.parseRtpMap = function(line) { 563 | var parts = line.substr(9).split(' '); 564 | var parsed = { 565 | payloadType: parseInt(parts.shift(), 10) // was: id 566 | }; 567 | 568 | parts = parts[0].split('/'); 569 | 570 | parsed.name = parts[0]; 571 | parsed.clockRate = parseInt(parts[1], 10); // was: clockrate 572 | parsed.numChannels = parts.length === 3 ? parseInt(parts[2], 10) : 1; // was: channels 573 | return parsed; 574 | }; 575 | 576 | // Generate an a=rtpmap line from RTCRtpCodecCapability or RTCRtpCodecParameters. 577 | SDPUtils.writeRtpMap = function(codec) { 578 | var pt = codec.payloadType; 579 | if (codec.preferredPayloadType !== undefined) { 580 | pt = codec.preferredPayloadType; 581 | } 582 | return 'a=rtpmap:' + pt + ' ' + codec.name + '/' + codec.clockRate + 583 | (codec.numChannels !== 1 ? '/' + codec.numChannels : '') + '\r\n'; 584 | }; 585 | 586 | // Parses an ftmp line, returns dictionary. Sample input: 587 | // a=fmtp:96 vbr=on;cng=on 588 | // Also deals with vbr=on; cng=on 589 | SDPUtils.parseFmtp = function(line) { 590 | var parsed = {}; 591 | var kv; 592 | var parts = line.substr(line.indexOf(' ') + 1).split(';'); 593 | for (var j = 0; j < parts.length; j++) { 594 | kv = parts[j].trim().split('='); 595 | parsed[kv[0].trim()] = kv[1]; 596 | } 597 | return parsed; 598 | }; 599 | 600 | // Generates an a=ftmp line from RTCRtpCodecCapability or RTCRtpCodecParameters. 601 | SDPUtils.writeFtmp = function(codec) { 602 | var line = ''; 603 | var pt = codec.payloadType; 604 | if (codec.preferredPayloadType !== undefined) { 605 | pt = codec.preferredPayloadType; 606 | } 607 | if (codec.parameters && codec.parameters.length) { 608 | var params = []; 609 | Object.keys(codec.parameters).forEach(function(param) { 610 | params.push(param + '=' + codec.parameters[param]); 611 | }); 612 | line += 'a=fmtp:' + pt + ' ' + params.join(';') + '\r\n'; 613 | } 614 | return line; 615 | }; 616 | 617 | // Parses an rtcp-fb line, returns RTCPRtcpFeedback object. Sample input: 618 | // a=rtcp-fb:98 nack rpsi 619 | SDPUtils.parseRtcpFb = function(line) { 620 | var parts = line.substr(line.indexOf(' ') + 1).split(' '); 621 | return { 622 | type: parts.shift(), 623 | parameter: parts.join(' ') 624 | }; 625 | }; 626 | // Generate a=rtcp-fb lines from RTCRtpCodecCapability or RTCRtpCodecParameters. 627 | SDPUtils.writeRtcpFb = function(codec) { 628 | var lines = ''; 629 | var pt = codec.payloadType; 630 | if (codec.preferredPayloadType !== undefined) { 631 | pt = codec.preferredPayloadType; 632 | } 633 | if (codec.rtcpFeedback && codec.rtcpFeedback.length) { 634 | // FIXME: special handling for trr-int? 635 | codec.rtcpFeedback.forEach(function(fb) { 636 | lines += 'a=rtcp-fb:' + pt + ' ' + fb.type + ' ' + fb.parameter + 637 | '\r\n'; 638 | }); 639 | } 640 | return lines; 641 | }; 642 | 643 | // Parses an RFC 5576 ssrc media attribute. Sample input: 644 | // a=ssrc:3735928559 cname:something 645 | SDPUtils.parseSsrcMedia = function(line) { 646 | var sp = line.indexOf(' '); 647 | var parts = { 648 | ssrc: line.substr(7, sp - 7), 649 | }; 650 | var colon = line.indexOf(':', sp); 651 | if (colon > -1) { 652 | parts.attribute = line.substr(sp + 1, colon - sp - 1); 653 | parts.value = line.substr(colon + 1); 654 | } else { 655 | parts.attribute = line.substr(sp + 1); 656 | } 657 | return parts; 658 | }; 659 | 660 | // Extracts DTLS parameters from SDP media section or sessionpart. 661 | // FIXME: for consistency with other functions this should only 662 | // get the fingerprint line as input. See also getIceParameters. 663 | SDPUtils.getDtlsParameters = function(mediaSection, sessionpart) { 664 | var lines = SDPUtils.splitLines(mediaSection); 665 | lines = lines.concat(SDPUtils.splitLines(sessionpart)); // Search in session part, too. 666 | var fpLine = lines.filter(function(line) { 667 | return line.indexOf('a=fingerprint:') === 0; 668 | })[0].substr(14); 669 | // Note: a=setup line is ignored since we use the 'auto' role. 670 | var dtlsParameters = { 671 | role: 'auto', 672 | fingerprints: [{ 673 | algorithm: fpLine.split(' ')[0], 674 | value: fpLine.split(' ')[1] 675 | }] 676 | }; 677 | return dtlsParameters; 678 | }; 679 | 680 | // Serializes DTLS parameters to SDP. 681 | SDPUtils.writeDtlsParameters = function(params, setupType) { 682 | var sdp = 'a=setup:' + setupType + '\r\n'; 683 | params.fingerprints.forEach(function(fp) { 684 | sdp += 'a=fingerprint:' + fp.algorithm + ' ' + fp.value + '\r\n'; 685 | }); 686 | return sdp; 687 | }; 688 | // Parses ICE information from SDP media section or sessionpart. 689 | // FIXME: for consistency with other functions this should only 690 | // get the ice-ufrag and ice-pwd lines as input. 691 | SDPUtils.getIceParameters = function(mediaSection, sessionpart) { 692 | var lines = SDPUtils.splitLines(mediaSection); 693 | lines = lines.concat(SDPUtils.splitLines(sessionpart)); // Search in session part, too. 694 | var iceParameters = { 695 | usernameFragment: lines.filter(function(line) { 696 | return line.indexOf('a=ice-ufrag:') === 0; 697 | })[0].substr(12), 698 | password: lines.filter(function(line) { 699 | return line.indexOf('a=ice-pwd:') === 0; 700 | })[0].substr(10) 701 | }; 702 | return iceParameters; 703 | }; 704 | 705 | // Serializes ICE parameters to SDP. 706 | SDPUtils.writeIceParameters = function(params) { 707 | return 'a=ice-ufrag:' + params.usernameFragment + '\r\n' + 708 | 'a=ice-pwd:' + params.password + '\r\n'; 709 | }; 710 | 711 | // Parses the SDP media section and returns RTCRtpParameters. 712 | SDPUtils.parseRtpParameters = function(mediaSection) { 713 | var description = { 714 | codecs: [], 715 | headerExtensions: [], 716 | fecMechanisms: [], 717 | rtcp: [] 718 | }; 719 | var lines = SDPUtils.splitLines(mediaSection); 720 | var mline = lines[0].split(' '); 721 | for (var i = 3; i < mline.length; i++) { // find all codecs from mline[3..] 722 | var pt = mline[i]; 723 | var rtpmapline = SDPUtils.matchPrefix( 724 | mediaSection, 'a=rtpmap:' + pt + ' ')[0]; 725 | if (rtpmapline) { 726 | var codec = SDPUtils.parseRtpMap(rtpmapline); 727 | var fmtps = SDPUtils.matchPrefix( 728 | mediaSection, 'a=fmtp:' + pt + ' '); 729 | // Only the first a=fmtp: is considered. 730 | codec.parameters = fmtps.length ? SDPUtils.parseFmtp(fmtps[0]) : {}; 731 | codec.rtcpFeedback = SDPUtils.matchPrefix( 732 | mediaSection, 'a=rtcp-fb:' + pt + ' ') 733 | .map(SDPUtils.parseRtcpFb); 734 | description.codecs.push(codec); 735 | } 736 | } 737 | // FIXME: parse headerExtensions, fecMechanisms and rtcp. 738 | return description; 739 | }; 740 | 741 | // Generates parts of the SDP media section describing the capabilities / parameters. 742 | SDPUtils.writeRtpDescription = function(kind, caps) { 743 | var sdp = ''; 744 | 745 | // Build the mline. 746 | sdp += 'm=' + kind + ' '; 747 | sdp += caps.codecs.length > 0 ? '9' : '0'; // reject if no codecs. 748 | sdp += ' UDP/TLS/RTP/SAVPF '; 749 | sdp += caps.codecs.map(function(codec) { 750 | if (codec.preferredPayloadType !== undefined) { 751 | return codec.preferredPayloadType; 752 | } 753 | return codec.payloadType; 754 | }).join(' ') + '\r\n'; 755 | 756 | sdp += 'c=IN IP4 0.0.0.0\r\n'; 757 | sdp += 'a=rtcp:9 IN IP4 0.0.0.0\r\n'; 758 | 759 | // Add a=rtpmap lines for each codec. Also fmtp and rtcp-fb. 760 | caps.codecs.forEach(function(codec) { 761 | sdp += SDPUtils.writeRtpMap(codec); 762 | sdp += SDPUtils.writeFtmp(codec); 763 | sdp += SDPUtils.writeRtcpFb(codec); 764 | }); 765 | // FIXME: add headerExtensions, fecMechanismş and rtcp. 766 | sdp += 'a=rtcp-mux\r\n'; 767 | return sdp; 768 | }; 769 | 770 | SDPUtils.writeSessionBoilerplate = function() { 771 | // FIXME: sess-id should be an NTP timestamp. 772 | return 'v=0\r\n' + 773 | 'o=thisisadapterortc 8169639915646943137 2 IN IP4 127.0.0.1\r\n' + 774 | 's=-\r\n' + 775 | 't=0 0\r\n'; 776 | }; 777 | 778 | SDPUtils.writeMediaSection = function(transceiver, caps, type, stream) { 779 | var sdp = SDPUtils.writeRtpDescription(transceiver.kind, caps); 780 | 781 | // Map ICE parameters (ufrag, pwd) to SDP. 782 | sdp += SDPUtils.writeIceParameters( 783 | transceiver.iceGatherer.getLocalParameters()); 784 | 785 | // Map DTLS parameters to SDP. 786 | sdp += SDPUtils.writeDtlsParameters( 787 | transceiver.dtlsTransport.getLocalParameters(), 788 | type === 'offer' ? 'actpass' : 'active'); 789 | 790 | sdp += 'a=mid:' + transceiver.mid + '\r\n'; 791 | 792 | if (transceiver.rtpSender && transceiver.rtpReceiver) { 793 | sdp += 'a=sendrecv\r\n'; 794 | } else if (transceiver.rtpSender) { 795 | sdp += 'a=sendonly\r\n'; 796 | } else if (transceiver.rtpReceiver) { 797 | sdp += 'a=recvonly\r\n'; 798 | } else { 799 | sdp += 'a=inactive\r\n'; 800 | } 801 | 802 | // FIXME: for RTX there might be multiple SSRCs. Not implemented in Edge yet. 803 | if (transceiver.rtpSender) { 804 | var msid = 'msid:' + stream.id + ' ' + 805 | transceiver.rtpSender.track.id + '\r\n'; 806 | sdp += 'a=' + msid; 807 | sdp += 'a=ssrc:' + transceiver.sendSsrc + ' ' + msid; 808 | } 809 | // FIXME: this should be written by writeRtpDescription. 810 | sdp += 'a=ssrc:' + transceiver.sendSsrc + ' cname:' + 811 | SDPUtils.localCName + '\r\n'; 812 | return sdp; 813 | }; 814 | 815 | // Gets the direction from the mediaSection or the sessionpart. 816 | SDPUtils.getDirection = function(mediaSection, sessionpart) { 817 | // Look for sendrecv, sendonly, recvonly, inactive, default to sendrecv. 818 | var lines = SDPUtils.splitLines(mediaSection); 819 | for (var i = 0; i < lines.length; i++) { 820 | switch (lines[i]) { 821 | case 'a=sendrecv': 822 | case 'a=sendonly': 823 | case 'a=recvonly': 824 | case 'a=inactive': 825 | return lines[i].substr(2); 826 | } 827 | } 828 | if (sessionpart) { 829 | return SDPUtils.getDirection(sessionpart); 830 | } 831 | return 'sendrecv'; 832 | }; 833 | 834 | // Expose public methods. 835 | module.exports = SDPUtils; 836 | 837 | },{}],4:[function(require,module,exports){ 838 | /* 839 | * Copyright (c) 2016 The WebRTC project authors. All Rights Reserved. 840 | * 841 | * Use of this source code is governed by a BSD-style license 842 | * that can be found in the LICENSE file in the root of the source 843 | * tree. 844 | */ 845 | 'use strict'; 846 | 847 | var SDPUtils = require('./edge_sdp'); 848 | var logging = require('../utils').log; 849 | var browserDetails = require('../utils').browserDetails; 850 | 851 | var edgeShim = { 852 | shimPeerConnection: function() { 853 | if (window.RTCIceGatherer) { 854 | // ORTC defines an RTCIceCandidate object but no constructor. 855 | // Not implemented in Edge. 856 | if (!window.RTCIceCandidate) { 857 | window.RTCIceCandidate = function(args) { 858 | return args; 859 | }; 860 | } 861 | // ORTC does not have a session description object but 862 | // other browsers (i.e. Chrome) that will support both PC and ORTC 863 | // in the future might have this defined already. 864 | if (!window.RTCSessionDescription) { 865 | window.RTCSessionDescription = function(args) { 866 | return args; 867 | }; 868 | } 869 | } 870 | 871 | window.RTCPeerConnection = function(config) { 872 | var self = this; 873 | 874 | this.onicecandidate = null; 875 | this.onaddstream = null; 876 | this.onremovestream = null; 877 | this.onsignalingstatechange = null; 878 | this.oniceconnectionstatechange = null; 879 | this.onnegotiationneeded = null; 880 | this.ondatachannel = null; 881 | 882 | this.localStreams = []; 883 | this.remoteStreams = []; 884 | this.getLocalStreams = function() { return self.localStreams; }; 885 | this.getRemoteStreams = function() { return self.remoteStreams; }; 886 | 887 | this.localDescription = new RTCSessionDescription({ 888 | type: '', 889 | sdp: '' 890 | }); 891 | this.remoteDescription = new RTCSessionDescription({ 892 | type: '', 893 | sdp: '' 894 | }); 895 | this.signalingState = 'stable'; 896 | this.iceConnectionState = 'new'; 897 | 898 | this.iceOptions = { 899 | gatherPolicy: 'all', 900 | iceServers: [] 901 | }; 902 | if (config && config.iceTransportPolicy) { 903 | switch (config.iceTransportPolicy) { 904 | case 'all': 905 | case 'relay': 906 | this.iceOptions.gatherPolicy = config.iceTransportPolicy; 907 | break; 908 | case 'none': 909 | // FIXME: remove once implementation and spec have added this. 910 | throw new TypeError('iceTransportPolicy "none" not supported'); 911 | } 912 | } 913 | if (config && config.iceServers) { 914 | // Edge does not like 915 | // 1) stun: 916 | // 2) turn: that does not have all of turn:host:port?transport=udp 917 | this.iceOptions.iceServers = config.iceServers.filter(function(server) { 918 | if (server && server.urls) { 919 | server.urls = server.urls.filter(function(url) { 920 | return url.indexOf('transport=udp') !== -1; 921 | })[0]; 922 | return true; 923 | } 924 | return false; 925 | }); 926 | } 927 | 928 | // per-track iceGathers, iceTransports, dtlsTransports, rtpSenders, ... 929 | // everything that is needed to describe a SDP m-line. 930 | this.transceivers = []; 931 | 932 | // since the iceGatherer is currently created in createOffer but we 933 | // must not emit candidates until after setLocalDescription we buffer 934 | // them in this array. 935 | this._localIceCandidatesBuffer = []; 936 | }; 937 | 938 | window.RTCPeerConnection.prototype._emitBufferedCandidates = function() { 939 | var self = this; 940 | // FIXME: need to apply ice candidates in a way which is async but in-order 941 | this._localIceCandidatesBuffer.forEach(function(event) { 942 | if (self.onicecandidate !== null) { 943 | self.onicecandidate(event); 944 | } 945 | }); 946 | this._localIceCandidatesBuffer = []; 947 | }; 948 | 949 | window.RTCPeerConnection.prototype.addStream = function(stream) { 950 | // Clone is necessary for local demos mostly, attaching directly 951 | // to two different senders does not work (build 10547). 952 | this.localStreams.push(stream.clone()); 953 | this._maybeFireNegotiationNeeded(); 954 | }; 955 | 956 | window.RTCPeerConnection.prototype.removeStream = function(stream) { 957 | var idx = this.localStreams.indexOf(stream); 958 | if (idx > -1) { 959 | this.localStreams.splice(idx, 1); 960 | this._maybeFireNegotiationNeeded(); 961 | } 962 | }; 963 | 964 | // Determines the intersection of local and remote capabilities. 965 | window.RTCPeerConnection.prototype._getCommonCapabilities = 966 | function(localCapabilities, remoteCapabilities) { 967 | var commonCapabilities = { 968 | codecs: [], 969 | headerExtensions: [], 970 | fecMechanisms: [] 971 | }; 972 | localCapabilities.codecs.forEach(function(lCodec) { 973 | for (var i = 0; i < remoteCapabilities.codecs.length; i++) { 974 | var rCodec = remoteCapabilities.codecs[i]; 975 | if (lCodec.name.toLowerCase() === rCodec.name.toLowerCase() && 976 | lCodec.clockRate === rCodec.clockRate && 977 | lCodec.numChannels === rCodec.numChannels) { 978 | // push rCodec so we reply with offerer payload type 979 | commonCapabilities.codecs.push(rCodec); 980 | 981 | // FIXME: also need to determine intersection between 982 | // .rtcpFeedback and .parameters 983 | break; 984 | } 985 | } 986 | }); 987 | 988 | localCapabilities.headerExtensions.forEach(function(lHeaderExtension) { 989 | for (var i = 0; i < remoteCapabilities.headerExtensions.length; i++) { 990 | var rHeaderExtension = remoteCapabilities.headerExtensions[i]; 991 | if (lHeaderExtension.uri === rHeaderExtension.uri) { 992 | commonCapabilities.headerExtensions.push(rHeaderExtension); 993 | break; 994 | } 995 | } 996 | }); 997 | 998 | // FIXME: fecMechanisms 999 | return commonCapabilities; 1000 | }; 1001 | 1002 | // Create ICE gatherer, ICE transport and DTLS transport. 1003 | window.RTCPeerConnection.prototype._createIceAndDtlsTransports = 1004 | function(mid, sdpMLineIndex) { 1005 | var self = this; 1006 | var iceGatherer = new RTCIceGatherer(self.iceOptions); 1007 | var iceTransport = new RTCIceTransport(iceGatherer); 1008 | iceGatherer.onlocalcandidate = function(evt) { 1009 | var event = {}; 1010 | event.candidate = {sdpMid: mid, sdpMLineIndex: sdpMLineIndex}; 1011 | 1012 | var cand = evt.candidate; 1013 | // Edge emits an empty object for RTCIceCandidateComplete‥ 1014 | if (!cand || Object.keys(cand).length === 0) { 1015 | // polyfill since RTCIceGatherer.state is not implemented in Edge 10547 yet. 1016 | if (iceGatherer.state === undefined) { 1017 | iceGatherer.state = 'completed'; 1018 | } 1019 | 1020 | // Emit a candidate with type endOfCandidates to make the samples work. 1021 | // Edge requires addIceCandidate with this empty candidate to start checking. 1022 | // The real solution is to signal end-of-candidates to the other side when 1023 | // getting the null candidate but some apps (like the samples) don't do that. 1024 | event.candidate.candidate = 1025 | 'candidate:1 1 udp 1 0.0.0.0 9 typ endOfCandidates'; 1026 | } else { 1027 | // RTCIceCandidate doesn't have a component, needs to be added 1028 | cand.component = iceTransport.component === 'RTCP' ? 2 : 1; 1029 | event.candidate.candidate = SDPUtils.writeCandidate(cand); 1030 | } 1031 | 1032 | var complete = self.transceivers.every(function(transceiver) { 1033 | return transceiver.iceGatherer && 1034 | transceiver.iceGatherer.state === 'completed'; 1035 | }); 1036 | // FIXME: update .localDescription with candidate and (potentially) end-of-candidates. 1037 | // To make this harder, the gatherer might emit candidates before localdescription 1038 | // is set. To make things worse, gather.getLocalCandidates still errors in 1039 | // Edge 10547 when no candidates have been gathered yet. 1040 | 1041 | if (self.onicecandidate !== null) { 1042 | // Emit candidate if localDescription is set. 1043 | // Also emits null candidate when all gatherers are complete. 1044 | if (self.localDescription && self.localDescription.type === '') { 1045 | self._localIceCandidatesBuffer.push(event); 1046 | if (complete) { 1047 | self._localIceCandidatesBuffer.push({}); 1048 | } 1049 | } else { 1050 | self.onicecandidate(event); 1051 | if (complete) { 1052 | self.onicecandidate({}); 1053 | } 1054 | } 1055 | } 1056 | }; 1057 | iceTransport.onicestatechange = function() { 1058 | self._updateConnectionState(); 1059 | }; 1060 | 1061 | var dtlsTransport = new RTCDtlsTransport(iceTransport); 1062 | dtlsTransport.ondtlsstatechange = function() { 1063 | self._updateConnectionState(); 1064 | }; 1065 | dtlsTransport.onerror = function() { 1066 | // onerror does not set state to failed by itself. 1067 | dtlsTransport.state = 'failed'; 1068 | self._updateConnectionState(); 1069 | }; 1070 | 1071 | return { 1072 | iceGatherer: iceGatherer, 1073 | iceTransport: iceTransport, 1074 | dtlsTransport: dtlsTransport 1075 | }; 1076 | }; 1077 | 1078 | // Start the RTP Sender and Receiver for a transceiver. 1079 | window.RTCPeerConnection.prototype._transceive = function(transceiver, 1080 | send, recv) { 1081 | var params = this._getCommonCapabilities(transceiver.localCapabilities, 1082 | transceiver.remoteCapabilities); 1083 | if (send && transceiver.rtpSender) { 1084 | params.encodings = [{ 1085 | ssrc: transceiver.sendSsrc 1086 | }]; 1087 | params.rtcp = { 1088 | cname: SDPUtils.localCName, 1089 | ssrc: transceiver.recvSsrc 1090 | }; 1091 | transceiver.rtpSender.send(params); 1092 | } 1093 | if (recv && transceiver.rtpReceiver) { 1094 | params.encodings = [{ 1095 | ssrc: transceiver.recvSsrc 1096 | }]; 1097 | params.rtcp = { 1098 | cname: transceiver.cname, 1099 | ssrc: transceiver.sendSsrc 1100 | }; 1101 | transceiver.rtpReceiver.receive(params); 1102 | } 1103 | }; 1104 | 1105 | window.RTCPeerConnection.prototype.setLocalDescription = 1106 | function(description) { 1107 | var self = this; 1108 | if (description.type === 'offer') { 1109 | if (!this._pendingOffer) { 1110 | } else { 1111 | this.transceivers = this._pendingOffer; 1112 | delete this._pendingOffer; 1113 | } 1114 | } else if (description.type === 'answer') { 1115 | var sections = SDPUtils.splitSections(self.remoteDescription.sdp); 1116 | var sessionpart = sections.shift(); 1117 | sections.forEach(function(mediaSection, sdpMLineIndex) { 1118 | var transceiver = self.transceivers[sdpMLineIndex]; 1119 | var iceGatherer = transceiver.iceGatherer; 1120 | var iceTransport = transceiver.iceTransport; 1121 | var dtlsTransport = transceiver.dtlsTransport; 1122 | var localCapabilities = transceiver.localCapabilities; 1123 | var remoteCapabilities = transceiver.remoteCapabilities; 1124 | var rejected = mediaSection.split('\n', 1)[0] 1125 | .split(' ', 2)[1] === '0'; 1126 | 1127 | if (!rejected) { 1128 | var remoteIceParameters = SDPUtils.getIceParameters(mediaSection, 1129 | sessionpart); 1130 | iceTransport.start(iceGatherer, remoteIceParameters, 'controlled'); 1131 | 1132 | var remoteDtlsParameters = SDPUtils.getDtlsParameters(mediaSection, 1133 | sessionpart); 1134 | dtlsTransport.start(remoteDtlsParameters); 1135 | 1136 | // Calculate intersection of capabilities. 1137 | var params = self._getCommonCapabilities(localCapabilities, 1138 | remoteCapabilities); 1139 | 1140 | // Start the RTCRtpSender. The RTCRtpReceiver for this transceiver 1141 | // has already been started in setRemoteDescription. 1142 | self._transceive(transceiver, 1143 | params.codecs.length > 0, 1144 | false); 1145 | } 1146 | }); 1147 | } 1148 | 1149 | this.localDescription = description; 1150 | switch (description.type) { 1151 | case 'offer': 1152 | this._updateSignalingState('have-local-offer'); 1153 | break; 1154 | case 'answer': 1155 | this._updateSignalingState('stable'); 1156 | break; 1157 | default: 1158 | throw new TypeError('unsupported type "' + description.type + '"'); 1159 | } 1160 | 1161 | // If a success callback was provided, emit ICE candidates after it has been 1162 | // executed. Otherwise, emit callback after the Promise is resolved. 1163 | var hasCallback = arguments.length > 1 && 1164 | typeof arguments[1] === 'function'; 1165 | if (hasCallback) { 1166 | var cb = arguments[1]; 1167 | window.setTimeout(function() { 1168 | cb(); 1169 | self._emitBufferedCandidates(); 1170 | }, 0); 1171 | } 1172 | var p = Promise.resolve(); 1173 | p.then(function() { 1174 | if (!hasCallback) { 1175 | window.setTimeout(self._emitBufferedCandidates.bind(self), 0); 1176 | } 1177 | }); 1178 | return p; 1179 | }; 1180 | 1181 | window.RTCPeerConnection.prototype.setRemoteDescription = 1182 | function(description) { 1183 | var self = this; 1184 | var stream = new MediaStream(); 1185 | var sections = SDPUtils.splitSections(description.sdp); 1186 | var sessionpart = sections.shift(); 1187 | sections.forEach(function(mediaSection, sdpMLineIndex) { 1188 | var lines = SDPUtils.splitLines(mediaSection); 1189 | var mline = lines[0].substr(2).split(' '); 1190 | var kind = mline[0]; 1191 | var rejected = mline[1] === '0'; 1192 | var direction = SDPUtils.getDirection(mediaSection, sessionpart); 1193 | 1194 | var transceiver; 1195 | var iceGatherer; 1196 | var iceTransport; 1197 | var dtlsTransport; 1198 | var rtpSender; 1199 | var rtpReceiver; 1200 | var sendSsrc; 1201 | var recvSsrc; 1202 | var localCapabilities; 1203 | 1204 | // FIXME: ensure the mediaSection has rtcp-mux set. 1205 | var remoteCapabilities = SDPUtils.parseRtpParameters(mediaSection); 1206 | var remoteIceParameters; 1207 | var remoteDtlsParameters; 1208 | if (!rejected) { 1209 | remoteIceParameters = SDPUtils.getIceParameters(mediaSection, 1210 | sessionpart); 1211 | remoteDtlsParameters = SDPUtils.getDtlsParameters(mediaSection, 1212 | sessionpart); 1213 | } 1214 | var mid = SDPUtils.matchPrefix(mediaSection, 'a=mid:')[0].substr(6); 1215 | 1216 | var cname; 1217 | // Gets the first SSRC. Note that with RTX there might be multiple SSRCs. 1218 | var remoteSsrc = SDPUtils.matchPrefix(mediaSection, 'a=ssrc:') 1219 | .map(function(line) { 1220 | return SDPUtils.parseSsrcMedia(line); 1221 | }) 1222 | .filter(function(obj) { 1223 | return obj.attribute === 'cname'; 1224 | })[0]; 1225 | if (remoteSsrc) { 1226 | recvSsrc = parseInt(remoteSsrc.ssrc, 10); 1227 | cname = remoteSsrc.value; 1228 | } 1229 | 1230 | if (description.type === 'offer') { 1231 | var transports = self._createIceAndDtlsTransports(mid, sdpMLineIndex); 1232 | 1233 | localCapabilities = RTCRtpReceiver.getCapabilities(kind); 1234 | sendSsrc = (2 * sdpMLineIndex + 2) * 1001; 1235 | 1236 | rtpReceiver = new RTCRtpReceiver(transports.dtlsTransport, kind); 1237 | 1238 | // FIXME: not correct when there are multiple streams but that is 1239 | // not currently supported in this shim. 1240 | stream.addTrack(rtpReceiver.track); 1241 | 1242 | // FIXME: look at direction. 1243 | if (self.localStreams.length > 0 && 1244 | self.localStreams[0].getTracks().length >= sdpMLineIndex) { 1245 | // FIXME: actually more complicated, needs to match types etc 1246 | var localtrack = self.localStreams[0].getTracks()[sdpMLineIndex]; 1247 | rtpSender = new RTCRtpSender(localtrack, transports.dtlsTransport); 1248 | } 1249 | 1250 | self.transceivers[sdpMLineIndex] = { 1251 | iceGatherer: transports.iceGatherer, 1252 | iceTransport: transports.iceTransport, 1253 | dtlsTransport: transports.dtlsTransport, 1254 | localCapabilities: localCapabilities, 1255 | remoteCapabilities: remoteCapabilities, 1256 | rtpSender: rtpSender, 1257 | rtpReceiver: rtpReceiver, 1258 | kind: kind, 1259 | mid: mid, 1260 | cname: cname, 1261 | sendSsrc: sendSsrc, 1262 | recvSsrc: recvSsrc 1263 | }; 1264 | // Start the RTCRtpReceiver now. The RTPSender is started in setLocalDescription. 1265 | self._transceive(self.transceivers[sdpMLineIndex], 1266 | false, 1267 | direction === 'sendrecv' || direction === 'sendonly'); 1268 | } else if (description.type === 'answer' && !rejected) { 1269 | transceiver = self.transceivers[sdpMLineIndex]; 1270 | iceGatherer = transceiver.iceGatherer; 1271 | iceTransport = transceiver.iceTransport; 1272 | dtlsTransport = transceiver.dtlsTransport; 1273 | rtpSender = transceiver.rtpSender; 1274 | rtpReceiver = transceiver.rtpReceiver; 1275 | sendSsrc = transceiver.sendSsrc; 1276 | //recvSsrc = transceiver.recvSsrc; 1277 | localCapabilities = transceiver.localCapabilities; 1278 | 1279 | self.transceivers[sdpMLineIndex].recvSsrc = recvSsrc; 1280 | self.transceivers[sdpMLineIndex].remoteCapabilities = 1281 | remoteCapabilities; 1282 | self.transceivers[sdpMLineIndex].cname = cname; 1283 | 1284 | iceTransport.start(iceGatherer, remoteIceParameters, 'controlling'); 1285 | dtlsTransport.start(remoteDtlsParameters); 1286 | 1287 | self._transceive(transceiver, 1288 | direction === 'sendrecv' || direction === 'recvonly', 1289 | direction === 'sendrecv' || direction === 'sendonly'); 1290 | 1291 | if (rtpReceiver && 1292 | (direction === 'sendrecv' || direction === 'sendonly')) { 1293 | stream.addTrack(rtpReceiver.track); 1294 | } else { 1295 | // FIXME: actually the receiver should be created later. 1296 | delete transceiver.rtpReceiver; 1297 | } 1298 | } 1299 | }); 1300 | 1301 | this.remoteDescription = description; 1302 | switch (description.type) { 1303 | case 'offer': 1304 | this._updateSignalingState('have-remote-offer'); 1305 | break; 1306 | case 'answer': 1307 | this._updateSignalingState('stable'); 1308 | break; 1309 | default: 1310 | throw new TypeError('unsupported type "' + description.type + '"'); 1311 | } 1312 | window.setTimeout(function() { 1313 | if (self.onaddstream !== null && stream.getTracks().length) { 1314 | self.remoteStreams.push(stream); 1315 | window.setTimeout(function() { 1316 | self.onaddstream({stream: stream}); 1317 | }, 0); 1318 | } 1319 | }, 0); 1320 | if (arguments.length > 1 && typeof arguments[1] === 'function') { 1321 | window.setTimeout(arguments[1], 0); 1322 | } 1323 | return Promise.resolve(); 1324 | }; 1325 | 1326 | window.RTCPeerConnection.prototype.close = function() { 1327 | this.transceivers.forEach(function(transceiver) { 1328 | /* not yet 1329 | if (transceiver.iceGatherer) { 1330 | transceiver.iceGatherer.close(); 1331 | } 1332 | */ 1333 | if (transceiver.iceTransport) { 1334 | transceiver.iceTransport.stop(); 1335 | } 1336 | if (transceiver.dtlsTransport) { 1337 | transceiver.dtlsTransport.stop(); 1338 | } 1339 | if (transceiver.rtpSender) { 1340 | transceiver.rtpSender.stop(); 1341 | } 1342 | if (transceiver.rtpReceiver) { 1343 | transceiver.rtpReceiver.stop(); 1344 | } 1345 | }); 1346 | // FIXME: clean up tracks, local streams, remote streams, etc 1347 | this._updateSignalingState('closed'); 1348 | }; 1349 | 1350 | // Update the signaling state. 1351 | window.RTCPeerConnection.prototype._updateSignalingState = 1352 | function(newState) { 1353 | this.signalingState = newState; 1354 | if (this.onsignalingstatechange !== null) { 1355 | this.onsignalingstatechange(); 1356 | } 1357 | }; 1358 | 1359 | // Determine whether to fire the negotiationneeded event. 1360 | window.RTCPeerConnection.prototype._maybeFireNegotiationNeeded = 1361 | function() { 1362 | // Fire away (for now). 1363 | if (this.onnegotiationneeded !== null) { 1364 | this.onnegotiationneeded(); 1365 | } 1366 | }; 1367 | 1368 | // Update the connection state. 1369 | window.RTCPeerConnection.prototype._updateConnectionState = 1370 | function() { 1371 | var self = this; 1372 | var newState; 1373 | var states = { 1374 | 'new': 0, 1375 | closed: 0, 1376 | connecting: 0, 1377 | checking: 0, 1378 | connected: 0, 1379 | completed: 0, 1380 | failed: 0 1381 | }; 1382 | this.transceivers.forEach(function(transceiver) { 1383 | states[transceiver.iceTransport.state]++; 1384 | states[transceiver.dtlsTransport.state]++; 1385 | }); 1386 | // ICETransport.completed and connected are the same for this purpose. 1387 | states['connected'] += states['completed']; 1388 | 1389 | newState = 'new'; 1390 | if (states['failed'] > 0) { 1391 | newState = 'failed'; 1392 | } else if (states['connecting'] > 0 || states['checking'] > 0) { 1393 | newState = 'connecting'; 1394 | } else if (states['disconnected'] > 0) { 1395 | newState = 'disconnected'; 1396 | } else if (states['new'] > 0) { 1397 | newState = 'new'; 1398 | } else if (states['connecting'] > 0 || states['completed'] > 0) { 1399 | newState = 'connected'; 1400 | } 1401 | 1402 | if (newState !== self.iceConnectionState) { 1403 | self.iceConnectionState = newState; 1404 | if (this.oniceconnectionstatechange !== null) { 1405 | this.oniceconnectionstatechange(); 1406 | } 1407 | } 1408 | }; 1409 | 1410 | window.RTCPeerConnection.prototype.createOffer = function() { 1411 | var self = this; 1412 | if (this._pendingOffer) { 1413 | throw new Error('createOffer called while there is a pending offer.'); 1414 | } 1415 | var offerOptions; 1416 | if (arguments.length === 1 && typeof arguments[0] !== 'function') { 1417 | offerOptions = arguments[0]; 1418 | } else if (arguments.length === 3) { 1419 | offerOptions = arguments[2]; 1420 | } 1421 | 1422 | var tracks = []; 1423 | var numAudioTracks = 0; 1424 | var numVideoTracks = 0; 1425 | // Default to sendrecv. 1426 | if (this.localStreams.length) { 1427 | numAudioTracks = this.localStreams[0].getAudioTracks().length; 1428 | numVideoTracks = this.localStreams[0].getVideoTracks().length; 1429 | } 1430 | // Determine number of audio and video tracks we need to send/recv. 1431 | if (offerOptions) { 1432 | // Reject Chrome legacy constraints. 1433 | if (offerOptions.mandatory || offerOptions.optional) { 1434 | throw new TypeError( 1435 | 'Legacy mandatory/optional constraints not supported.'); 1436 | } 1437 | if (offerOptions.offerToReceiveAudio !== undefined) { 1438 | numAudioTracks = offerOptions.offerToReceiveAudio; 1439 | } 1440 | if (offerOptions.offerToReceiveVideo !== undefined) { 1441 | numVideoTracks = offerOptions.offerToReceiveVideo; 1442 | } 1443 | } 1444 | if (this.localStreams.length) { 1445 | // Push local streams. 1446 | this.localStreams[0].getTracks().forEach(function(track) { 1447 | tracks.push({ 1448 | kind: track.kind, 1449 | track: track, 1450 | wantReceive: track.kind === 'audio' ? 1451 | numAudioTracks > 0 : numVideoTracks > 0 1452 | }); 1453 | if (track.kind === 'audio') { 1454 | numAudioTracks--; 1455 | } else if (track.kind === 'video') { 1456 | numVideoTracks--; 1457 | } 1458 | }); 1459 | } 1460 | // Create M-lines for recvonly streams. 1461 | while (numAudioTracks > 0 || numVideoTracks > 0) { 1462 | if (numAudioTracks > 0) { 1463 | tracks.push({ 1464 | kind: 'audio', 1465 | wantReceive: true 1466 | }); 1467 | numAudioTracks--; 1468 | } 1469 | if (numVideoTracks > 0) { 1470 | tracks.push({ 1471 | kind: 'video', 1472 | wantReceive: true 1473 | }); 1474 | numVideoTracks--; 1475 | } 1476 | } 1477 | 1478 | var sdp = SDPUtils.writeSessionBoilerplate(); 1479 | var transceivers = []; 1480 | tracks.forEach(function(mline, sdpMLineIndex) { 1481 | // For each track, create an ice gatherer, ice transport, dtls transport, 1482 | // potentially rtpsender and rtpreceiver. 1483 | var track = mline.track; 1484 | var kind = mline.kind; 1485 | var mid = SDPUtils.generateIdentifier(); 1486 | 1487 | var transports = self._createIceAndDtlsTransports(mid, sdpMLineIndex); 1488 | 1489 | var localCapabilities = RTCRtpSender.getCapabilities(kind); 1490 | var rtpSender; 1491 | var rtpReceiver; 1492 | 1493 | // generate an ssrc now, to be used later in rtpSender.send 1494 | var sendSsrc = (2 * sdpMLineIndex + 1) * 1001; 1495 | if (track) { 1496 | rtpSender = new RTCRtpSender(track, transports.dtlsTransport); 1497 | } 1498 | 1499 | if (mline.wantReceive) { 1500 | rtpReceiver = new RTCRtpReceiver(transports.dtlsTransport, kind); 1501 | } 1502 | 1503 | transceivers[sdpMLineIndex] = { 1504 | iceGatherer: transports.iceGatherer, 1505 | iceTransport: transports.iceTransport, 1506 | dtlsTransport: transports.dtlsTransport, 1507 | localCapabilities: localCapabilities, 1508 | remoteCapabilities: null, 1509 | rtpSender: rtpSender, 1510 | rtpReceiver: rtpReceiver, 1511 | kind: kind, 1512 | mid: mid, 1513 | sendSsrc: sendSsrc, 1514 | recvSsrc: null 1515 | }; 1516 | var transceiver = transceivers[sdpMLineIndex]; 1517 | sdp += SDPUtils.writeMediaSection(transceiver, 1518 | transceiver.localCapabilities, 'offer', self.localStreams[0]); 1519 | }); 1520 | 1521 | this._pendingOffer = transceivers; 1522 | var desc = new RTCSessionDescription({ 1523 | type: 'offer', 1524 | sdp: sdp 1525 | }); 1526 | if (arguments.length && typeof arguments[0] === 'function') { 1527 | window.setTimeout(arguments[0], 0, desc); 1528 | } 1529 | return Promise.resolve(desc); 1530 | }; 1531 | 1532 | window.RTCPeerConnection.prototype.createAnswer = function() { 1533 | var self = this; 1534 | var answerOptions; 1535 | if (arguments.length === 1 && typeof arguments[0] !== 'function') { 1536 | answerOptions = arguments[0]; 1537 | } else if (arguments.length === 3) { 1538 | answerOptions = arguments[2]; 1539 | } 1540 | 1541 | var sdp = SDPUtils.writeSessionBoilerplate(); 1542 | this.transceivers.forEach(function(transceiver) { 1543 | // Calculate intersection of capabilities. 1544 | var commonCapabilities = self._getCommonCapabilities( 1545 | transceiver.localCapabilities, 1546 | transceiver.remoteCapabilities); 1547 | 1548 | sdp += SDPUtils.writeMediaSection(transceiver, commonCapabilities, 1549 | 'answer', self.localStreams[0]); 1550 | }); 1551 | 1552 | var desc = new RTCSessionDescription({ 1553 | type: 'answer', 1554 | sdp: sdp 1555 | }); 1556 | if (arguments.length && typeof arguments[0] === 'function') { 1557 | window.setTimeout(arguments[0], 0, desc); 1558 | } 1559 | return Promise.resolve(desc); 1560 | }; 1561 | 1562 | window.RTCPeerConnection.prototype.addIceCandidate = function(candidate) { 1563 | var mLineIndex = candidate.sdpMLineIndex; 1564 | if (candidate.sdpMid) { 1565 | for (var i = 0; i < this.transceivers.length; i++) { 1566 | if (this.transceivers[i].mid === candidate.sdpMid) { 1567 | mLineIndex = i; 1568 | break; 1569 | } 1570 | } 1571 | } 1572 | var transceiver = this.transceivers[mLineIndex]; 1573 | if (transceiver) { 1574 | var cand = Object.keys(candidate.candidate).length > 0 ? 1575 | SDPUtils.parseCandidate(candidate.candidate) : {}; 1576 | // Ignore Chrome's invalid candidates since Edge does not like them. 1577 | if (cand.protocol === 'tcp' && cand.port === 0) { 1578 | return; 1579 | } 1580 | // Ignore RTCP candidates, we assume RTCP-MUX. 1581 | if (cand.component !== '1') { 1582 | return; 1583 | } 1584 | // A dirty hack to make samples work. 1585 | if (cand.type === 'endOfCandidates') { 1586 | cand = {}; 1587 | } 1588 | transceiver.iceTransport.addRemoteCandidate(cand); 1589 | } 1590 | if (arguments.length > 1 && typeof arguments[1] === 'function') { 1591 | window.setTimeout(arguments[1], 0); 1592 | } 1593 | return Promise.resolve(); 1594 | }; 1595 | 1596 | window.RTCPeerConnection.prototype.getStats = function() { 1597 | var promises = []; 1598 | this.transceivers.forEach(function(transceiver) { 1599 | ['rtpSender', 'rtpReceiver', 'iceGatherer', 'iceTransport', 1600 | 'dtlsTransport'].forEach(function(method) { 1601 | if (transceiver[method]) { 1602 | promises.push(transceiver[method].getStats()); 1603 | } 1604 | }); 1605 | }); 1606 | var cb = arguments.length > 1 && typeof arguments[1] === 'function' && 1607 | arguments[1]; 1608 | return new Promise(function(resolve) { 1609 | var results = {}; 1610 | Promise.all(promises).then(function(res) { 1611 | res.forEach(function(result) { 1612 | Object.keys(result).forEach(function(id) { 1613 | results[id] = result[id]; 1614 | }); 1615 | }); 1616 | if (cb) { 1617 | window.setTimeout(cb, 0, results); 1618 | } 1619 | resolve(results); 1620 | }); 1621 | }); 1622 | }; 1623 | }, 1624 | 1625 | // Attach a media stream to an element. 1626 | attachMediaStream: function(element, stream) { 1627 | logging('DEPRECATED, attachMediaStream will soon be removed.'); 1628 | element.srcObject = stream; 1629 | }, 1630 | 1631 | reattachMediaStream: function(to, from) { 1632 | logging('DEPRECATED, reattachMediaStream will soon be removed.'); 1633 | to.srcObject = from.srcObject; 1634 | } 1635 | } 1636 | 1637 | // Expose public methods. 1638 | module.exports = { 1639 | shimPeerConnection: edgeShim.shimPeerConnection, 1640 | attachMediaStream: edgeShim.attachMediaStream, 1641 | reattachMediaStream: edgeShim.reattachMediaStream 1642 | } 1643 | 1644 | 1645 | },{"../utils":6,"./edge_sdp":3}],5:[function(require,module,exports){ 1646 | /* 1647 | * Copyright (c) 2016 The WebRTC project authors. All Rights Reserved. 1648 | * 1649 | * Use of this source code is governed by a BSD-style license 1650 | * that can be found in the LICENSE file in the root of the source 1651 | * tree. 1652 | */ 1653 | 'use strict'; 1654 | 1655 | var logging = require('../utils').log; 1656 | var browserDetails = require('../utils').browserDetails; 1657 | 1658 | var firefoxShim = { 1659 | shimOnTrack: function() { 1660 | if (typeof window === 'object' && window.RTCPeerConnection && !('ontrack' in 1661 | window.RTCPeerConnection.prototype)) { 1662 | Object.defineProperty(window.RTCPeerConnection.prototype, 'ontrack', { 1663 | get: function() { return this._ontrack; }, 1664 | set: function(f) { 1665 | var self = this; 1666 | if (this._ontrack) { 1667 | this.removeEventListener('track', this._ontrack); 1668 | this.removeEventListener('addstream', this._ontrackpoly); 1669 | } 1670 | this.addEventListener('track', this._ontrack = f); 1671 | this.addEventListener('addstream', this._ontrackpoly = function(e) { 1672 | e.stream.getTracks().forEach(function(track) { 1673 | var event = new Event('track'); 1674 | event.track = track; 1675 | event.receiver = {track: track}; 1676 | event.streams = [e.stream]; 1677 | this.dispatchEvent(event); 1678 | }.bind(this)); 1679 | }.bind(this)); 1680 | } 1681 | }); 1682 | } 1683 | }, 1684 | 1685 | shimSourceObject: function() { 1686 | // Firefox has supported mozSrcObject since FF22, unprefixed in 42. 1687 | if (typeof window === 'object') { 1688 | if (window.HTMLMediaElement && 1689 | !('srcObject' in window.HTMLMediaElement.prototype)) { 1690 | // Shim the srcObject property, once, when HTMLMediaElement is found. 1691 | Object.defineProperty(window.HTMLMediaElement.prototype, 'srcObject', { 1692 | get: function() { 1693 | return this.mozSrcObject; 1694 | }, 1695 | set: function(stream) { 1696 | this.mozSrcObject = stream; 1697 | } 1698 | }); 1699 | } 1700 | } 1701 | }, 1702 | 1703 | shimPeerConnection: function() { 1704 | // The RTCPeerConnection object. 1705 | if (!window.RTCPeerConnection) { 1706 | window.RTCPeerConnection = function(pcConfig, pcConstraints) { 1707 | if (browserDetails.version < 38) { 1708 | // .urls is not supported in FF < 38. 1709 | // create RTCIceServers with a single url. 1710 | if (pcConfig && pcConfig.iceServers) { 1711 | var newIceServers = []; 1712 | for (var i = 0; i < pcConfig.iceServers.length; i++) { 1713 | var server = pcConfig.iceServers[i]; 1714 | if (server.hasOwnProperty('urls')) { 1715 | for (var j = 0; j < server.urls.length; j++) { 1716 | var newServer = { 1717 | url: server.urls[j] 1718 | }; 1719 | if (server.urls[j].indexOf('turn') === 0) { 1720 | newServer.username = server.username; 1721 | newServer.credential = server.credential; 1722 | } 1723 | newIceServers.push(newServer); 1724 | } 1725 | } else { 1726 | newIceServers.push(pcConfig.iceServers[i]); 1727 | } 1728 | } 1729 | pcConfig.iceServers = newIceServers; 1730 | } 1731 | } 1732 | return new mozRTCPeerConnection(pcConfig, pcConstraints); // jscs:ignore requireCapitalizedConstructors 1733 | }; 1734 | window.RTCPeerConnection.prototype = mozRTCPeerConnection.prototype; 1735 | 1736 | // wrap static methods. Currently just generateCertificate. 1737 | if (mozRTCPeerConnection.generateCertificate) { 1738 | Object.defineProperty(window.RTCPeerConnection, 'generateCertificate', { 1739 | get: function() { 1740 | if (arguments.length) { 1741 | return mozRTCPeerConnection.generateCertificate.apply(null, 1742 | arguments); 1743 | } else { 1744 | return mozRTCPeerConnection.generateCertificate; 1745 | } 1746 | } 1747 | }); 1748 | } 1749 | 1750 | window.RTCSessionDescription = mozRTCSessionDescription; 1751 | window.RTCIceCandidate = mozRTCIceCandidate; 1752 | } 1753 | }, 1754 | 1755 | shimGetUserMedia: function() { 1756 | // getUserMedia constraints shim. 1757 | var getUserMedia_ = function(constraints, onSuccess, onError) { 1758 | var constraintsToFF37_ = function(c) { 1759 | if (typeof c !== 'object' || c.require) { 1760 | return c; 1761 | } 1762 | var require = []; 1763 | Object.keys(c).forEach(function(key) { 1764 | if (key === 'require' || key === 'advanced' || key === 'mediaSource') { 1765 | return; 1766 | } 1767 | var r = c[key] = (typeof c[key] === 'object') ? 1768 | c[key] : {ideal: c[key]}; 1769 | if (r.min !== undefined || 1770 | r.max !== undefined || r.exact !== undefined) { 1771 | require.push(key); 1772 | } 1773 | if (r.exact !== undefined) { 1774 | if (typeof r.exact === 'number') { 1775 | r. min = r.max = r.exact; 1776 | } else { 1777 | c[key] = r.exact; 1778 | } 1779 | delete r.exact; 1780 | } 1781 | if (r.ideal !== undefined) { 1782 | c.advanced = c.advanced || []; 1783 | var oc = {}; 1784 | if (typeof r.ideal === 'number') { 1785 | oc[key] = {min: r.ideal, max: r.ideal}; 1786 | } else { 1787 | oc[key] = r.ideal; 1788 | } 1789 | c.advanced.push(oc); 1790 | delete r.ideal; 1791 | if (!Object.keys(r).length) { 1792 | delete c[key]; 1793 | } 1794 | } 1795 | }); 1796 | if (require.length) { 1797 | c.require = require; 1798 | } 1799 | return c; 1800 | }; 1801 | if (browserDetails.version < 38) { 1802 | logging('spec: ' + JSON.stringify(constraints)); 1803 | if (constraints.audio) { 1804 | constraints.audio = constraintsToFF37_(constraints.audio); 1805 | } 1806 | if (constraints.video) { 1807 | constraints.video = constraintsToFF37_(constraints.video); 1808 | } 1809 | logging('ff37: ' + JSON.stringify(constraints)); 1810 | } 1811 | return navigator.mozGetUserMedia(constraints, onSuccess, onError); 1812 | }; 1813 | 1814 | navigator.getUserMedia = getUserMedia_; 1815 | 1816 | // Returns the result of getUserMedia as a Promise. 1817 | var getUserMediaPromise_ = function(constraints) { 1818 | return new Promise(function(resolve, reject) { 1819 | navigator.getUserMedia(constraints, resolve, reject); 1820 | }); 1821 | } 1822 | 1823 | // Shim for mediaDevices on older versions. 1824 | if (!navigator.mediaDevices) { 1825 | navigator.mediaDevices = {getUserMedia: getUserMediaPromise_, 1826 | addEventListener: function() { }, 1827 | removeEventListener: function() { } 1828 | }; 1829 | } 1830 | navigator.mediaDevices.enumerateDevices = 1831 | navigator.mediaDevices.enumerateDevices || function() { 1832 | return new Promise(function(resolve) { 1833 | var infos = [ 1834 | {kind: 'audioinput', deviceId: 'default', label: '', groupId: ''}, 1835 | {kind: 'videoinput', deviceId: 'default', label: '', groupId: ''} 1836 | ]; 1837 | resolve(infos); 1838 | }); 1839 | }; 1840 | 1841 | if (browserDetails.version < 41) { 1842 | // Work around http://bugzil.la/1169665 1843 | var orgEnumerateDevices = 1844 | navigator.mediaDevices.enumerateDevices.bind(navigator.mediaDevices); 1845 | navigator.mediaDevices.enumerateDevices = function() { 1846 | return orgEnumerateDevices().then(undefined, function(e) { 1847 | if (e.name === 'NotFoundError') { 1848 | return []; 1849 | } 1850 | throw e; 1851 | }); 1852 | }; 1853 | } 1854 | }, 1855 | 1856 | // Attach a media stream to an element. 1857 | attachMediaStream: function(element, stream) { 1858 | logging('DEPRECATED, attachMediaStream will soon be removed.'); 1859 | element.srcObject = stream; 1860 | }, 1861 | 1862 | reattachMediaStream: function(to, from) { 1863 | logging('DEPRECATED, reattachMediaStream will soon be removed.'); 1864 | to.srcObject = from.srcObject; 1865 | } 1866 | } 1867 | 1868 | // Expose public methods. 1869 | module.exports = { 1870 | shimOnTrack: firefoxShim.shimOnTrack, 1871 | shimSourceObject: firefoxShim.shimSourceObject, 1872 | shimPeerConnection: firefoxShim.shimPeerConnection, 1873 | shimGetUserMedia: firefoxShim.shimGetUserMedia, 1874 | attachMediaStream: firefoxShim.attachMediaStream, 1875 | reattachMediaStream: firefoxShim.reattachMediaStream 1876 | } 1877 | 1878 | },{"../utils":6}],6:[function(require,module,exports){ 1879 | /* 1880 | * Copyright (c) 2016 The WebRTC project authors. All Rights Reserved. 1881 | * 1882 | * Use of this source code is governed by a BSD-style license 1883 | * that can be found in the LICENSE file in the root of the source 1884 | * tree. 1885 | */ 1886 | 'use strict'; 1887 | 1888 | var logDisabled_ = false; 1889 | 1890 | // Utility methods. 1891 | var utils = { 1892 | disableLog: function(bool) { 1893 | if (typeof bool !== 'boolean') { 1894 | return new Error('Argument type: ' + typeof bool + 1895 | '. Please use a boolean.'); 1896 | } 1897 | logDisabled_ = bool; 1898 | return (bool) ? 'adapter.js logging disabled' : 1899 | 'adapter.js logging enabled'; 1900 | }, 1901 | 1902 | log: function() { 1903 | if (typeof window === 'object') { 1904 | if (logDisabled_) { 1905 | return; 1906 | } 1907 | console.log.apply(console, arguments); 1908 | } 1909 | }, 1910 | 1911 | /** 1912 | * Extract browser version out of the provided user agent string. 1913 | * @param {!string} uastring userAgent string. 1914 | * @param {!string} expr Regular expression used as match criteria. 1915 | * @param {!number} pos position in the version string to be returned. 1916 | * @return {!number} browser version. 1917 | */ 1918 | extractVersion: function(uastring, expr, pos) { 1919 | var match = uastring.match(expr); 1920 | return match && match.length >= pos && parseInt(match[pos], 10); 1921 | }, 1922 | 1923 | /** 1924 | * Browser detector. 1925 | * @return {object} result containing browser, version and minVersion 1926 | * properties. 1927 | */ 1928 | detectBrowser: function() { 1929 | // Returned result object. 1930 | var result = {}; 1931 | result.browser = null; 1932 | result.version = null; 1933 | result.minVersion = null; 1934 | 1935 | // Non supported browser. 1936 | if (typeof window === 'undefined' || !window.navigator) { 1937 | result.browser = 'Not a supported browser.'; 1938 | return result; 1939 | } 1940 | 1941 | // Firefox. 1942 | if (navigator.mozGetUserMedia) { 1943 | result.browser = 'firefox'; 1944 | result.version = this.extractVersion(navigator.userAgent, 1945 | /Firefox\/([0-9]+)\./, 1); 1946 | result.minVersion = 31; 1947 | return result; 1948 | } 1949 | 1950 | // Chrome/Chromium/Webview. 1951 | if (navigator.webkitGetUserMedia && window.webkitRTCPeerConnection) { 1952 | result.browser = 'chrome'; 1953 | result.version = this.extractVersion(navigator.userAgent, 1954 | /Chrom(e|ium)\/([0-9]+)\./, 2); 1955 | result.minVersion = 38; 1956 | return result; 1957 | } 1958 | 1959 | // Edge. 1960 | if (navigator.mediaDevices && 1961 | navigator.userAgent.match(/Edge\/(\d+).(\d+)$/)) { 1962 | result.browser = 'edge'; 1963 | result.version = this.extractVersion(navigator.userAgent, 1964 | /Edge\/(\d+).(\d+)$/, 2); 1965 | result.minVersion = 10547; 1966 | return result; 1967 | } 1968 | 1969 | // Non supported browser default. 1970 | result.browser = 'Not a supported browser.'; 1971 | return result; 1972 | } 1973 | }; 1974 | 1975 | // Export. 1976 | module.exports = { 1977 | log: utils.log, 1978 | disableLog: utils.disableLog, 1979 | browserDetails: utils.detectBrowser(), 1980 | extractVersion: utils.extractVersion 1981 | }; 1982 | 1983 | },{}]},{},[1])(1) 1984 | }); -------------------------------------------------------------------------------- /static/js/adapter.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2014 The WebRTC project authors. All Rights Reserved. 3 | * 4 | * Use of this source code is governed by a BSD-style license 5 | * that can be found in the LICENSE file in the root of the source 6 | * tree. 7 | */ 8 | 9 | /* More information about these options at jshint.com/docs/options */ 10 | /* jshint browser: true, camelcase: true, curly: true, devel: true, 11 | eqeqeq: true, forin: false, globalstrict: true, node: true, 12 | quotmark: single, undef: true, unused: strict */ 13 | /* global mozRTCIceCandidate, mozRTCPeerConnection, Promise, 14 | mozRTCSessionDescription, webkitRTCPeerConnection, MediaStreamTrack */ 15 | /* exported trace,requestUserMedia */ 16 | 17 | 'use strict'; 18 | 19 | var getUserMedia = null; 20 | var attachMediaStream = null; 21 | var reattachMediaStream = null; 22 | var webrtcDetectedBrowser = null; 23 | var webrtcDetectedVersion = null; 24 | var webrtcMinimumVersion = null; 25 | var webrtcUtils = { 26 | log: function() { 27 | // suppress console.log output when being included as a module. 28 | if (typeof module !== 'undefined' || 29 | typeof require === 'function' && typeof define === 'function') { 30 | return; 31 | } 32 | console.log.apply(console, arguments); 33 | }, 34 | extractVersion: function(uastring, expr, pos) { 35 | var match = uastring.match(expr); 36 | return match && match.length >= pos && parseInt(match[pos]); 37 | } 38 | }; 39 | 40 | function trace(text) { 41 | // This function is used for logging. 42 | if (text[text.length - 1] === '\n') { 43 | text = text.substring(0, text.length - 1); 44 | } 45 | if (window.performance) { 46 | var now = (window.performance.now() / 1000).toFixed(3); 47 | webrtcUtils.log(now + ': ' + text); 48 | } else { 49 | webrtcUtils.log(text); 50 | } 51 | } 52 | 53 | if (typeof window === 'object') { 54 | if (window.HTMLMediaElement && 55 | !('srcObject' in window.HTMLMediaElement.prototype)) { 56 | // Shim the srcObject property, once, when HTMLMediaElement is found. 57 | Object.defineProperty(window.HTMLMediaElement.prototype, 'srcObject', { 58 | get: function() { 59 | // If prefixed srcObject property exists, return it. 60 | // Otherwise use the shimmed property, _srcObject 61 | return 'mozSrcObject' in this ? this.mozSrcObject : this._srcObject; 62 | }, 63 | set: function(stream) { 64 | if ('mozSrcObject' in this) { 65 | this.mozSrcObject = stream; 66 | } else { 67 | // Use _srcObject as a private property for this shim 68 | this._srcObject = stream; 69 | // TODO: revokeObjectUrl(this.src) when !stream to release resources? 70 | this.src = URL.createObjectURL(stream); 71 | } 72 | } 73 | }); 74 | } 75 | // Proxy existing globals 76 | getUserMedia = window.navigator && window.navigator.getUserMedia; 77 | } 78 | 79 | // Attach a media stream to an element. 80 | attachMediaStream = function(element, stream) { 81 | element.srcObject = stream; 82 | }; 83 | 84 | reattachMediaStream = function(to, from) { 85 | to.srcObject = from.srcObject; 86 | }; 87 | 88 | if (typeof window === 'undefined' || !window.navigator) { 89 | webrtcUtils.log('This does not appear to be a browser'); 90 | webrtcDetectedBrowser = 'not a browser'; 91 | } else if (navigator.mozGetUserMedia && window.mozRTCPeerConnection) { 92 | webrtcUtils.log('This appears to be Firefox'); 93 | 94 | webrtcDetectedBrowser = 'firefox'; 95 | 96 | // the detected firefox version. 97 | webrtcDetectedVersion = webrtcUtils.extractVersion(navigator.userAgent, 98 | /Firefox\/([0-9]+)\./, 1); 99 | 100 | // the minimum firefox version still supported by adapter. 101 | webrtcMinimumVersion = 31; 102 | 103 | // The RTCPeerConnection object. 104 | window.RTCPeerConnection = function(pcConfig, pcConstraints) { 105 | if (webrtcDetectedVersion < 38) { 106 | // .urls is not supported in FF < 38. 107 | // create RTCIceServers with a single url. 108 | if (pcConfig && pcConfig.iceServers) { 109 | var newIceServers = []; 110 | for (var i = 0; i < pcConfig.iceServers.length; i++) { 111 | var server = pcConfig.iceServers[i]; 112 | if (server.hasOwnProperty('urls')) { 113 | for (var j = 0; j < server.urls.length; j++) { 114 | var newServer = { 115 | url: server.urls[j] 116 | }; 117 | if (server.urls[j].indexOf('turn') === 0) { 118 | newServer.username = server.username; 119 | newServer.credential = server.credential; 120 | } 121 | newIceServers.push(newServer); 122 | } 123 | } else { 124 | newIceServers.push(pcConfig.iceServers[i]); 125 | } 126 | } 127 | pcConfig.iceServers = newIceServers; 128 | } 129 | } 130 | return new mozRTCPeerConnection(pcConfig, pcConstraints); // jscs:ignore requireCapitalizedConstructors 131 | }; 132 | 133 | // The RTCSessionDescription object. 134 | if (!window.RTCSessionDescription) { 135 | window.RTCSessionDescription = mozRTCSessionDescription; 136 | } 137 | 138 | // The RTCIceCandidate object. 139 | if (!window.RTCIceCandidate) { 140 | window.RTCIceCandidate = mozRTCIceCandidate; 141 | } 142 | 143 | // getUserMedia constraints shim. 144 | getUserMedia = function(constraints, onSuccess, onError) { 145 | var constraintsToFF37 = function(c) { 146 | if (typeof c !== 'object' || c.require) { 147 | return c; 148 | } 149 | var require = []; 150 | Object.keys(c).forEach(function(key) { 151 | if (key === 'require' || key === 'advanced' || key === 'mediaSource') { 152 | return; 153 | } 154 | var r = c[key] = (typeof c[key] === 'object') ? 155 | c[key] : {ideal: c[key]}; 156 | if (r.min !== undefined || 157 | r.max !== undefined || r.exact !== undefined) { 158 | require.push(key); 159 | } 160 | if (r.exact !== undefined) { 161 | if (typeof r.exact === 'number') { 162 | r.min = r.max = r.exact; 163 | } else { 164 | c[key] = r.exact; 165 | } 166 | delete r.exact; 167 | } 168 | if (r.ideal !== undefined) { 169 | c.advanced = c.advanced || []; 170 | var oc = {}; 171 | if (typeof r.ideal === 'number') { 172 | oc[key] = {min: r.ideal, max: r.ideal}; 173 | } else { 174 | oc[key] = r.ideal; 175 | } 176 | c.advanced.push(oc); 177 | delete r.ideal; 178 | if (!Object.keys(r).length) { 179 | delete c[key]; 180 | } 181 | } 182 | }); 183 | if (require.length) { 184 | c.require = require; 185 | } 186 | return c; 187 | }; 188 | if (webrtcDetectedVersion < 38) { 189 | webrtcUtils.log('spec: ' + JSON.stringify(constraints)); 190 | if (constraints.audio) { 191 | constraints.audio = constraintsToFF37(constraints.audio); 192 | } 193 | if (constraints.video) { 194 | constraints.video = constraintsToFF37(constraints.video); 195 | } 196 | webrtcUtils.log('ff37: ' + JSON.stringify(constraints)); 197 | } 198 | return navigator.mozGetUserMedia(constraints, onSuccess, onError); 199 | }; 200 | 201 | navigator.getUserMedia = getUserMedia; 202 | 203 | // Shim for mediaDevices on older versions. 204 | if (!navigator.mediaDevices) { 205 | navigator.mediaDevices = {getUserMedia: requestUserMedia, 206 | addEventListener: function() { }, 207 | removeEventListener: function() { } 208 | }; 209 | } 210 | navigator.mediaDevices.enumerateDevices = 211 | navigator.mediaDevices.enumerateDevices || function() { 212 | return new Promise(function(resolve) { 213 | var infos = [ 214 | {kind: 'audioinput', deviceId: 'default', label: '', groupId: ''}, 215 | {kind: 'videoinput', deviceId: 'default', label: '', groupId: ''} 216 | ]; 217 | resolve(infos); 218 | }); 219 | }; 220 | 221 | if (webrtcDetectedVersion < 41) { 222 | // Work around http://bugzil.la/1169665 223 | var orgEnumerateDevices = 224 | navigator.mediaDevices.enumerateDevices.bind(navigator.mediaDevices); 225 | navigator.mediaDevices.enumerateDevices = function() { 226 | return orgEnumerateDevices().then(undefined, function(e) { 227 | if (e.name === 'NotFoundError') { 228 | return []; 229 | } 230 | throw e; 231 | }); 232 | }; 233 | } 234 | } else if (navigator.webkitGetUserMedia && window.webkitRTCPeerConnection) { 235 | webrtcUtils.log('This appears to be Chrome'); 236 | 237 | webrtcDetectedBrowser = 'chrome'; 238 | 239 | // the detected chrome version. 240 | webrtcDetectedVersion = webrtcUtils.extractVersion(navigator.userAgent, 241 | /Chrom(e|ium)\/([0-9]+)\./, 2); 242 | 243 | // the minimum chrome version still supported by adapter. 244 | webrtcMinimumVersion = 38; 245 | 246 | // The RTCPeerConnection object. 247 | window.RTCPeerConnection = function(pcConfig, pcConstraints) { 248 | // Translate iceTransportPolicy to iceTransports, 249 | // see https://code.google.com/p/webrtc/issues/detail?id=4869 250 | if (pcConfig && pcConfig.iceTransportPolicy) { 251 | pcConfig.iceTransports = pcConfig.iceTransportPolicy; 252 | } 253 | 254 | var pc = new webkitRTCPeerConnection(pcConfig, pcConstraints); // jscs:ignore requireCapitalizedConstructors 255 | var origGetStats = pc.getStats.bind(pc); 256 | pc.getStats = function(selector, successCallback, errorCallback) { // jshint ignore: line 257 | var self = this; 258 | var args = arguments; 259 | 260 | // If selector is a function then we are in the old style stats so just 261 | // pass back the original getStats format to avoid breaking old users. 262 | if (arguments.length > 0 && typeof selector === 'function') { 263 | return origGetStats(selector, successCallback); 264 | } 265 | 266 | var fixChromeStats = function(response) { 267 | var standardReport = {}; 268 | var reports = response.result(); 269 | reports.forEach(function(report) { 270 | var standardStats = { 271 | id: report.id, 272 | timestamp: report.timestamp, 273 | type: report.type 274 | }; 275 | report.names().forEach(function(name) { 276 | standardStats[name] = report.stat(name); 277 | }); 278 | standardReport[standardStats.id] = standardStats; 279 | }); 280 | 281 | return standardReport; 282 | }; 283 | 284 | if (arguments.length >= 2) { 285 | var successCallbackWrapper = function(response) { 286 | args[1](fixChromeStats(response)); 287 | }; 288 | 289 | return origGetStats.apply(this, [successCallbackWrapper, arguments[0]]); 290 | } 291 | 292 | // promise-support 293 | return new Promise(function(resolve, reject) { 294 | if (args.length === 1 && selector === null) { 295 | origGetStats.apply(self, [ 296 | function(response) { 297 | resolve.apply(null, [fixChromeStats(response)]); 298 | }, reject]); 299 | } else { 300 | origGetStats.apply(self, [resolve, reject]); 301 | } 302 | }); 303 | }; 304 | 305 | return pc; 306 | }; 307 | 308 | // add promise support 309 | ['createOffer', 'createAnswer'].forEach(function(method) { 310 | var nativeMethod = webkitRTCPeerConnection.prototype[method]; 311 | webkitRTCPeerConnection.prototype[method] = function() { 312 | var self = this; 313 | if (arguments.length < 1 || (arguments.length === 1 && 314 | typeof(arguments[0]) === 'object')) { 315 | var opts = arguments.length === 1 ? arguments[0] : undefined; 316 | return new Promise(function(resolve, reject) { 317 | nativeMethod.apply(self, [resolve, reject, opts]); 318 | }); 319 | } else { 320 | return nativeMethod.apply(this, arguments); 321 | } 322 | }; 323 | }); 324 | 325 | ['setLocalDescription', 'setRemoteDescription', 326 | 'addIceCandidate'].forEach(function(method) { 327 | var nativeMethod = webkitRTCPeerConnection.prototype[method]; 328 | webkitRTCPeerConnection.prototype[method] = function() { 329 | var args = arguments; 330 | var self = this; 331 | return new Promise(function(resolve, reject) { 332 | nativeMethod.apply(self, [args[0], 333 | function() { 334 | resolve(); 335 | if (args.length >= 2) { 336 | args[1].apply(null, []); 337 | } 338 | }, 339 | function(err) { 340 | reject(err); 341 | if (args.length >= 3) { 342 | args[2].apply(null, [err]); 343 | } 344 | }] 345 | ); 346 | }); 347 | }; 348 | }); 349 | 350 | // getUserMedia constraints shim. 351 | var constraintsToChrome = function(c) { 352 | if (typeof c !== 'object' || c.mandatory || c.optional) { 353 | return c; 354 | } 355 | var cc = {}; 356 | Object.keys(c).forEach(function(key) { 357 | if (key === 'require' || key === 'advanced' || key === 'mediaSource') { 358 | return; 359 | } 360 | var r = (typeof c[key] === 'object') ? c[key] : {ideal: c[key]}; 361 | if (r.exact !== undefined && typeof r.exact === 'number') { 362 | r.min = r.max = r.exact; 363 | } 364 | var oldname = function(prefix, name) { 365 | if (prefix) { 366 | return prefix + name.charAt(0).toUpperCase() + name.slice(1); 367 | } 368 | return (name === 'deviceId') ? 'sourceId' : name; 369 | }; 370 | if (r.ideal !== undefined) { 371 | cc.optional = cc.optional || []; 372 | var oc = {}; 373 | if (typeof r.ideal === 'number') { 374 | oc[oldname('min', key)] = r.ideal; 375 | cc.optional.push(oc); 376 | oc = {}; 377 | oc[oldname('max', key)] = r.ideal; 378 | cc.optional.push(oc); 379 | } else { 380 | oc[oldname('', key)] = r.ideal; 381 | cc.optional.push(oc); 382 | } 383 | } 384 | if (r.exact !== undefined && typeof r.exact !== 'number') { 385 | cc.mandatory = cc.mandatory || {}; 386 | cc.mandatory[oldname('', key)] = r.exact; 387 | } else { 388 | ['min', 'max'].forEach(function(mix) { 389 | if (r[mix] !== undefined) { 390 | cc.mandatory = cc.mandatory || {}; 391 | cc.mandatory[oldname(mix, key)] = r[mix]; 392 | } 393 | }); 394 | } 395 | }); 396 | if (c.advanced) { 397 | cc.optional = (cc.optional || []).concat(c.advanced); 398 | } 399 | return cc; 400 | }; 401 | 402 | getUserMedia = function(constraints, onSuccess, onError) { 403 | if (constraints.audio) { 404 | constraints.audio = constraintsToChrome(constraints.audio); 405 | } 406 | if (constraints.video) { 407 | constraints.video = constraintsToChrome(constraints.video); 408 | } 409 | webrtcUtils.log('chrome: ' + JSON.stringify(constraints)); 410 | return navigator.webkitGetUserMedia(constraints, onSuccess, onError); 411 | }; 412 | navigator.getUserMedia = getUserMedia; 413 | 414 | if (!navigator.mediaDevices) { 415 | navigator.mediaDevices = {getUserMedia: requestUserMedia, 416 | enumerateDevices: function() { 417 | return new Promise(function(resolve) { 418 | var kinds = {audio: 'audioinput', video: 'videoinput'}; 419 | return MediaStreamTrack.getSources(function(devices) { 420 | resolve(devices.map(function(device) { 421 | return {label: device.label, 422 | kind: kinds[device.kind], 423 | deviceId: device.id, 424 | groupId: ''}; 425 | })); 426 | }); 427 | }); 428 | }}; 429 | } 430 | 431 | // A shim for getUserMedia method on the mediaDevices object. 432 | // TODO(KaptenJansson) remove once implemented in Chrome stable. 433 | if (!navigator.mediaDevices.getUserMedia) { 434 | navigator.mediaDevices.getUserMedia = function(constraints) { 435 | return requestUserMedia(constraints); 436 | }; 437 | } else { 438 | // Even though Chrome 45 has navigator.mediaDevices and a getUserMedia 439 | // function which returns a Promise, it does not accept spec-style 440 | // constraints. 441 | var origGetUserMedia = navigator.mediaDevices.getUserMedia. 442 | bind(navigator.mediaDevices); 443 | navigator.mediaDevices.getUserMedia = function(c) { 444 | webrtcUtils.log('spec: ' + JSON.stringify(c)); // whitespace for alignment 445 | c.audio = constraintsToChrome(c.audio); 446 | c.video = constraintsToChrome(c.video); 447 | webrtcUtils.log('chrome: ' + JSON.stringify(c)); 448 | return origGetUserMedia(c); 449 | }; 450 | } 451 | 452 | // Dummy devicechange event methods. 453 | // TODO(KaptenJansson) remove once implemented in Chrome stable. 454 | if (typeof navigator.mediaDevices.addEventListener === 'undefined') { 455 | navigator.mediaDevices.addEventListener = function() { 456 | webrtcUtils.log('Dummy mediaDevices.addEventListener called.'); 457 | }; 458 | } 459 | if (typeof navigator.mediaDevices.removeEventListener === 'undefined') { 460 | navigator.mediaDevices.removeEventListener = function() { 461 | webrtcUtils.log('Dummy mediaDevices.removeEventListener called.'); 462 | }; 463 | } 464 | 465 | // Attach a media stream to an element. 466 | attachMediaStream = function(element, stream) { 467 | if (webrtcDetectedVersion >= 43) { 468 | element.srcObject = stream; 469 | } else if (typeof element.src !== 'undefined') { 470 | element.src = URL.createObjectURL(stream); 471 | } else { 472 | webrtcUtils.log('Error attaching stream to element.'); 473 | } 474 | }; 475 | reattachMediaStream = function(to, from) { 476 | if (webrtcDetectedVersion >= 43) { 477 | to.srcObject = from.srcObject; 478 | } else { 479 | to.src = from.src; 480 | } 481 | }; 482 | 483 | } else if (navigator.mediaDevices && navigator.userAgent.match( 484 | /Edge\/(\d+).(\d+)$/)) { 485 | webrtcUtils.log('This appears to be Edge'); 486 | webrtcDetectedBrowser = 'edge'; 487 | 488 | webrtcDetectedVersion = webrtcUtils.extractVersion(navigator.userAgent, 489 | /Edge\/(\d+).(\d+)$/, 2); 490 | 491 | // the minimum version still supported by adapter. 492 | webrtcMinimumVersion = 12; 493 | } else { 494 | webrtcUtils.log('Browser does not appear to be WebRTC-capable'); 495 | } 496 | 497 | // Returns the result of getUserMedia as a Promise. 498 | function requestUserMedia(constraints) { 499 | return new Promise(function(resolve, reject) { 500 | getUserMedia(constraints, resolve, reject); 501 | }); 502 | } 503 | 504 | var webrtcTesting = {}; 505 | try { 506 | Object.defineProperty(webrtcTesting, 'version', { 507 | set: function(version) { 508 | webrtcDetectedVersion = version; 509 | } 510 | }); 511 | } catch (e) {} 512 | 513 | if (typeof module !== 'undefined') { 514 | var RTCPeerConnection; 515 | if (typeof window !== 'undefined') { 516 | RTCPeerConnection = window.RTCPeerConnection; 517 | } 518 | module.exports = { 519 | RTCPeerConnection: RTCPeerConnection, 520 | getUserMedia: getUserMedia, 521 | attachMediaStream: attachMediaStream, 522 | reattachMediaStream: reattachMediaStream, 523 | webrtcDetectedBrowser: webrtcDetectedBrowser, 524 | webrtcDetectedVersion: webrtcDetectedVersion, 525 | webrtcMinimumVersion: webrtcMinimumVersion, 526 | webrtcTesting: webrtcTesting, 527 | webrtcUtils: webrtcUtils 528 | //requestUserMedia: not exposed on purpose. 529 | //trace: not exposed on purpose. 530 | }; 531 | } else if ((typeof require === 'function') && (typeof define === 'function')) { 532 | // Expose objects and functions when RequireJS is doing the loading. 533 | define([], function() { 534 | return { 535 | RTCPeerConnection: window.RTCPeerConnection, 536 | getUserMedia: getUserMedia, 537 | attachMediaStream: attachMediaStream, 538 | reattachMediaStream: reattachMediaStream, 539 | webrtcDetectedBrowser: webrtcDetectedBrowser, 540 | webrtcDetectedVersion: webrtcDetectedVersion, 541 | webrtcMinimumVersion: webrtcMinimumVersion, 542 | webrtcTesting: webrtcTesting, 543 | webrtcUtils: webrtcUtils 544 | //requestUserMedia: not exposed on purpose. 545 | //trace: not exposed on purpose. 546 | }; 547 | }); 548 | } 549 | -------------------------------------------------------------------------------- /static/js/index.js: -------------------------------------------------------------------------------- 1 | var ws = new WebSocket('ws://' + location.host + '/ws'); 2 | var videoInput; 3 | var videoOutput; 4 | var roomNameInput; 5 | var webRtcPeer; 6 | var host; 7 | var state = null; 8 | 9 | const I_CAN_START = 0; 10 | const I_CAN_STOP = 1; 11 | const I_AM_STARTING = 2; 12 | 13 | window.onload = function() { 14 | // console = new Console(); 15 | console.log('Page loaded ...'); 16 | videoInput = document.getElementById('videoInput'); 17 | videoOutput = document.getElementById('videoOutput'); 18 | 19 | roomNameInput = $("input[name='roomName']"); 20 | 21 | // $('#startAudio').attr('onclick', 'startAudio()'); 22 | // $('#getStats').attr('onclick', 'getStats()'); 23 | // $('#startSendWebRTC').attr('onclick', 'startSendWebRTC()'); 24 | // $('#stopSendWebRTC').attr('onclick', 'stopSendWebRTC()'); 25 | host = location.protocol; 26 | setState(I_CAN_START); 27 | } 28 | 29 | window.onbeforeunload = function() { 30 | ws.close(); 31 | } 32 | 33 | ws.onmessage = function(message) { 34 | var parsedMessage = JSON.parse(message.data); 35 | console.info('Received message: ' + message.data); 36 | 37 | switch (parsedMessage.id) { 38 | case 'startResponse': 39 | startResponse(parsedMessage); 40 | break; 41 | case 'startAudioResponse': 42 | startAudioResponse(parsedMessage); 43 | break; 44 | case 'startSendWebrtcResponse': 45 | startSendWebrtcResponse(parsedMessage); 46 | break; 47 | case 'rtpAnswer': 48 | console.log(parsedMessage); 49 | break; 50 | case 'error': 51 | if (state == I_AM_STARTING) { 52 | setState(I_CAN_START); 53 | } 54 | onError('Error message from server: ' + parsedMessage.message); 55 | break; 56 | case 'iceCandidate': 57 | console.log('Remote Ice'); 58 | webRtcPeer.addIceCandidate(parsedMessage.candidate) 59 | break; 60 | case 'iceCandidateAudio': 61 | console.log('Remote Ice'); 62 | webRtcPeerSend.addIceCandidate(parsedMessage.candidate) 63 | break; 64 | case 'iceCandidateSendWebRtc': 65 | console.log('iceCandidateSendWebRtc Remote Ice'); 66 | webRtcPeerSendWebRtc.addIceCandidate(parsedMessage.candidate) 67 | break; 68 | case 'getStats': 69 | console.log('getStats message received'); 70 | console.log(parsedMessage); 71 | break; 72 | case 'sendStatus': 73 | console.log(parsedMessage); 74 | if (parsedMessage.sending) { 75 | $('#sendStatus').removeClass("label-warning"); 76 | $('#sendStatus').addClass("label-success"); 77 | $('#sendStatus').text('Sending'); 78 | } else { 79 | $('#sendStatus').removeClass("label-success"); 80 | $('#sendStatus').addClass("label-warning"); 81 | $('#sendStatus').text('Not Sending'); 82 | } 83 | break; 84 | default: 85 | if (state == I_AM_STARTING) { 86 | setState(I_CAN_START); 87 | } 88 | onError('Unrecognized message', parsedMessage); 89 | } 90 | } 91 | 92 | function setState(nextState) { 93 | switch (nextState) { 94 | case I_CAN_START: 95 | $('#start').attr('disabled', false); 96 | $('#start').attr('onclick', 'startWebRtc()'); 97 | $('#stop').attr('disabled', true); 98 | $('#stop').removeAttr('onclick'); 99 | break; 100 | 101 | case I_CAN_STOP: 102 | $('#start').attr('disabled', true); 103 | $('#stop').attr('disabled', false); 104 | $('#stop').attr('onclick', 'stop()'); 105 | break; 106 | 107 | case I_AM_STARTING: 108 | $('#start').attr('disabled', true); 109 | $('#start').removeAttr('onclick'); 110 | $('#stop').attr('disabled', true); 111 | $('#stop').removeAttr('onclick'); 112 | break; 113 | 114 | default: 115 | onError('Unknown state ' + nextState); 116 | return; 117 | } 118 | state = nextState; 119 | } 120 | 121 | function startWebRtc() { 122 | console.log('Starting WebRtc ...'); 123 | 124 | setState(I_AM_STARTING); 125 | showSpinner(videoOutput); 126 | 127 | var options = { 128 | localVideo: videoInput, 129 | remoteVideo: videoOutput, 130 | onicecandidate : onIceCandidate 131 | } 132 | 133 | webRtcPeer = kurentoUtils.WebRtcPeer.WebRtcPeerSendrecv(options, function(error) { 134 | if(error) return onError(error); 135 | this.generateOffer(onWebRtcOffer); 136 | }); 137 | } 138 | 139 | function showSpinner() { 140 | for (var i = 0; i < arguments.length; i++) { 141 | arguments[i].poster = './img/transparent-1px.png'; 142 | arguments[i].style.background = 'center transparent url("./img/spinner.gif") no-repeat'; 143 | } 144 | } 145 | 146 | function hideSpinner() { 147 | for (var i = 0; i < arguments.length; i++) { 148 | arguments[i].src = ''; 149 | arguments[i].poster = './img/webrtc.png'; 150 | arguments[i].style.background = ''; 151 | } 152 | } 153 | 154 | function sendMessage(message) { 155 | var jsonMessage = JSON.stringify(message); 156 | console.log('Senging message: ' + jsonMessage); 157 | ws.send(jsonMessage); 158 | } 159 | 160 | function onIceCandidate(candidate) { 161 | console.log('Local candidate' + JSON.stringify(candidate)); 162 | 163 | var message = { 164 | cmd: 'onIceCandidate', 165 | roomName: getRoomname(), 166 | candidate: candidate 167 | }; 168 | sendMessage(message); 169 | } 170 | 171 | function onWebRtcOffer(error, offerSdp) { 172 | if(error) return onError(error); 173 | 174 | console.info('onWebRtcOffer Invoking SDP offer callback function ' + location.host); 175 | console.log(roomNameInput, roomNameInput.val()); 176 | var message = { 177 | cmd : 'startWebRtc', 178 | roomName: getRoomname(), 179 | sdpOffer : offerSdp 180 | } 181 | sendMessage(message); 182 | } 183 | 184 | function startResponse(message) { 185 | setState(I_CAN_STOP); 186 | console.log('SDP answer received from server. Processing ...'); 187 | webRtcPeer.processAnswer(message.sdpAnswer); 188 | } 189 | 190 | function stop() { 191 | console.log('Stopping video call ...'); 192 | setState(I_CAN_START); 193 | if (webRtcPeer) { 194 | webRtcPeer.dispose(); 195 | webRtcPeer = null; 196 | 197 | var message = { 198 | cmd : 'stop', 199 | roomName: getRoomname() 200 | } 201 | sendMessage(message); 202 | } 203 | hideSpinner(videoInput, videoOutput); 204 | } 205 | 206 | function getRoomname() { 207 | var roomName = roomNameInput.val(); 208 | return roomName; 209 | } 210 | 211 | function onError(error) { 212 | console.error(error); 213 | } 214 | --------------------------------------------------------------------------------