├── .gitignore ├── .jsbeautifyrc ├── Gruntfile.js ├── LICENSE ├── README.md ├── docs ├── overview_messages.graphml └── overview_messages.png ├── example ├── package.json ├── server.js └── static │ ├── bower.json │ ├── index.html │ ├── js │ ├── conferenceroom.js │ └── participant.js │ └── style.css ├── lib ├── backend │ ├── call.js │ ├── call_manager.js │ ├── logger.js │ ├── test │ │ ├── index.html │ │ ├── mocha.opts │ │ └── test.user.registry.js │ ├── user_registry.js │ └── user_session.js └── server.js └── package.json /.gitignore: -------------------------------------------------------------------------------- 1 | npm-debug.log 2 | node_modules/ 3 | example/static/bower_components/ 4 | -------------------------------------------------------------------------------- /.jsbeautifyrc: -------------------------------------------------------------------------------- 1 | { 2 | "brace_style": "collapse", 3 | "break_chained_methods": false, 4 | "e4x": false, 5 | "eval_code": false, 6 | "indent_char": " ", 7 | "indent_level": 0, 8 | "indent_size": 2, 9 | "indent_with_tabs": false, 10 | "jslint_happy": true, 11 | "keep_array_indentation": false, 12 | "keep_function_indentation": false, 13 | "max_preserve_newlines": 2, 14 | "preserve_newlines": true, 15 | "space_before_conditional": true, 16 | "space_in_paren": false, 17 | "unescape_strings": false, 18 | "wrap_line_length": 80 19 | } 20 | -------------------------------------------------------------------------------- /Gruntfile.js: -------------------------------------------------------------------------------- 1 | module.exports = function (grunt) { 2 | 3 | var pkg = grunt.file.readJSON('package.json'); 4 | 5 | // Project configuration. 6 | grunt.initConfig({ 7 | pkg: pkg, 8 | 9 | githooks: { 10 | all: { 11 | 'pre-commit': 'jsbeautifier:git-pre-commit' 12 | } 13 | }, 14 | 15 | jsbeautifier: { 16 | options: { 17 | config: '.jsbeautifyrc' 18 | }, 19 | "default": { 20 | src: ["lib/**/*.js", "*.js", "test/*.js", "backend/*.js"] 21 | }, 22 | "git-pre-commit": { 23 | src: ["lib/**/*.js", "*.js", "test/*.js", "backend/*.js"], 24 | options: { 25 | config: '.jsbeautifyrc', 26 | mode: 'VERIFY_ONLY' 27 | //mode: 'VERIFY_AND_WRITE' 28 | } 29 | } 30 | }, 31 | 32 | jshint: { 33 | all: ['backend/**/*.js', "server.js"], 34 | options: { 35 | jshintrc: true 36 | }, 37 | } 38 | 39 | }); 40 | 41 | // Load plugins 42 | grunt.loadNpmTasks('grunt-jsbeautifier'); 43 | grunt.loadNpmTasks('grunt-contrib-jshint'); 44 | 45 | // Alias tasks 46 | grunt.registerTask('default', [ 47 | 'jsbeautifier:git-pre-commit' 48 | ]); 49 | }; 50 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 dragosch 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Kurento Group Call 2 | === 3 | 4 | Simple javascript library build on Node.js used to initiate a group call using Kurento Media Server. 5 | This library uses WebRTC for establishing a many to many video and audio call. 6 | 7 | This library has used this java implementatioan as a blueprint (https://github.com/Kurento/kurento-tutorial-java/tree/release-5.1/kurento-group-call). 8 | 9 | For more in deep information please have a look at this site describing the 10 | concepts behind 11 | (http://builds.kurento.org/dev/latest/docs/tutorials/java/tutorial-6-group.html !! not available any more). 12 | 13 | 14 | # Internals 15 | 16 | ## Communication / Activity Diagram 17 | ![Overview Messages](/docs/overview_messages.png?raw=true "Activity Diagram") 18 | 19 | Installation 20 | === 21 | 22 | You can install this nodejs library using npm (https://www.npmjs.com/package/kurento-group-call). 23 | 24 | For starting the example: 25 | ``` 26 | cd example 27 | npm install 28 | node server.js 29 | ``` 30 | This example is currently only running with Chrome or Firefox. 31 | 32 | Usage 33 | === 34 | 35 | 36 | 37 | 38 | 39 | -------------------------------------------------------------------------------- /docs/overview_messages.graphml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | <<webbrowser>> 26 | User 1 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | <<webbrowser>> 49 | User 2 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | NodeJs Server 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | Actor 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | Invite Users 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | 174 | Folder 3 175 | 176 | 177 | 178 | 179 | 180 | 181 | 182 | 183 | 184 | 185 | 186 | 187 | 188 | 189 | 190 | 191 | 192 | 193 | [x] User 3 194 | [x] User 3 195 | 196 | 197 | 198 | 199 | 200 | 201 | 202 | 203 | 204 | 205 | 206 | 207 | 208 | 209 | 210 | 211 | 212 | 213 | 214 | 215 | 216 | OK 217 | 218 | 219 | 220 | 221 | 222 | 223 | 224 | 225 | 226 | 227 | 228 | 229 | 230 | 231 | 232 | 233 | 234 | 235 | 236 | 237 | 238 | 239 | 240 | 241 | 242 | 243 | 244 | 245 | 246 | 247 | 248 | 249 | 250 | 251 | 252 | 253 | 254 | 255 | 256 | 257 | 258 | 259 | 260 | 261 | 262 | 263 | 264 | 265 | Accept Call ? 266 | 267 | 268 | 269 | 270 | 271 | 272 | 273 | 274 | 275 | 276 | Folder 3 277 | 278 | 279 | 280 | 281 | 282 | 283 | 284 | 285 | 286 | 287 | 288 | 289 | 290 | 291 | 292 | 293 | 294 | 295 | Accept 296 | 297 | 298 | 299 | 300 | 301 | 302 | 303 | 304 | 305 | 306 | 307 | 308 | 309 | 310 | 311 | 312 | 313 | 314 | 315 | 316 | 317 | Reject 318 | 319 | 320 | 321 | 322 | 323 | 324 | 325 | 326 | 327 | 328 | 329 | 330 | 331 | 332 | 333 | 334 | 335 | 336 | 337 | 338 | 339 | 340 | 341 | <<webbrowser>> 342 | User 3 343 | 344 | 345 | 346 | 347 | 348 | 349 | 350 | 351 | 352 | 353 | 354 | 355 | 356 | 357 | 358 | 359 | 360 | 361 | 362 | 363 | 364 | 365 | 366 | 367 | 368 | 369 | 370 | 371 | 372 | 373 | 374 | 375 | 376 | 377 | 378 | 379 | 380 | 381 | 382 | 383 | 384 | 385 | 386 | 387 | 388 | 389 | 390 | 391 | 392 | 393 | 394 | 395 | 396 | 397 | 398 | 399 | 400 | 401 | 402 | 403 | 404 | 405 | 406 | 407 | 408 | 409 | 410 | 411 | 412 | 413 | 414 | 415 | 416 | 417 | 418 | 419 | 420 | 421 | 422 | 423 | 424 | 425 | 426 | 427 | 428 | 429 | 430 | 431 | 432 | 433 | 434 | 435 | 436 | 437 | 438 | 439 | 440 | 441 | 442 | 443 | 444 | 445 | id: startNewCall 446 | userId: u1 447 | 448 | 449 | 450 | 451 | 452 | 453 | 454 | 455 | 456 | 457 | 458 | 459 | 460 | id: existingParticipants 461 | data: [u1] 462 | callId: c1 463 | callerId: u1 464 | 465 | 466 | 467 | 468 | 469 | 470 | 471 | 472 | 473 | 474 | 475 | 476 | 477 | id: receiveVideoFrom 478 | sender: u1 479 | sdpOffer: ... 480 | 481 | 482 | 483 | 484 | 485 | 486 | 487 | 488 | 489 | 490 | 491 | 492 | 493 | id: receiveVideoAnswer 494 | name: u1 495 | sdpAnswer: ... 496 | callerid: u1 497 | 498 | 499 | 500 | 501 | 502 | 503 | 504 | 505 | 506 | 507 | 508 | 509 | 510 | click "Start New Call" 511 | 512 | 513 | 514 | 515 | 516 | 517 | 518 | 519 | 520 | 521 | 522 | 523 | 524 | id: inviteUsers 525 | userIds: [u2,u3] 526 | 527 | 528 | 529 | 530 | 531 | 532 | 533 | 534 | 535 | 536 | 537 | 538 | 539 | 540 | 541 | 542 | 543 | 544 | 545 | 546 | 547 | 548 | 549 | 550 | id: incomingCall 551 | from: u1 552 | callId: c1 553 | 554 | 555 | 556 | 557 | 558 | 559 | 560 | 561 | 562 | 563 | 564 | 565 | 566 | id: incomingCallAnswer 567 | from: u2 568 | answer: accepted | rejected 569 | callId: c1 570 | 571 | 572 | 573 | 574 | 575 | 576 | 577 | 578 | 579 | 580 | 581 | 582 | 583 | 584 | 585 | 586 | 587 | 588 | 589 | 590 | 591 | 592 | 593 | 594 | 595 | 596 | 597 | 598 | 599 | 600 | 601 | 602 | 603 | 604 | 605 | 606 | websocket connect 607 | 608 | 609 | 610 | 611 | 612 | 613 | 614 | 615 | 616 | 617 | 618 | 619 | 620 | websocket connect 621 | 622 | 623 | 624 | 625 | 626 | 627 | 628 | 629 | 630 | 631 | 632 | 633 | 634 | websocket connect 635 | 636 | 637 | 638 | 639 | 640 | 641 | 642 | 643 | <?xml version="1.0" encoding="UTF-8" standalone="no"?> 644 | <!-- Created with Inkscape (http://www.inkscape.org/) --> 645 | <svg 646 | xmlns:dc="http://purl.org/dc/elements/1.1/" 647 | xmlns:cc="http://web.resource.org/cc/" 648 | xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" 649 | xmlns:svg="http://www.w3.org/2000/svg" 650 | xmlns="http://www.w3.org/2000/svg" 651 | xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" 652 | xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" 653 | width="41" 654 | height="68.997391" 655 | id="svg2" 656 | sodipodi:version="0.32" 657 | inkscape:version="0.45.1" 658 | sodipodi:docbase="C:\Daten\alberts\projects\yfx" 659 | sodipodi:docname="uml_actor.svg" 660 | inkscape:output_extension="org.inkscape.output.svg.inkscape" 661 | version="1.0"> 662 | <defs 663 | id="defs4" /> 664 | <sodipodi:namedview 665 | id="base" 666 | pagecolor="#ffffff" 667 | bordercolor="#666666" 668 | borderopacity="1.0" 669 | inkscape:pageopacity="0.0" 670 | inkscape:pageshadow="2" 671 | inkscape:zoom="2.934351" 672 | inkscape:cx="144.21983" 673 | inkscape:cy="28.533711" 674 | inkscape:document-units="px" 675 | inkscape:current-layer="layer1" 676 | showgrid="true" 677 | inkscape:window-width="1280" 678 | inkscape:window-height="968" 679 | inkscape:window-x="-4" 680 | inkscape:window-y="-4" 681 | width="48px" 682 | height="48px" 683 | showborder="false" 684 | inkscape:showpageshadow="false" /> 685 | <metadata 686 | id="metadata7"> 687 | <rdf:RDF> 688 | <cc:Work 689 | rdf:about=""> 690 | <dc:format>image/svg+xml</dc:format> 691 | <dc:type 692 | rdf:resource="http://purl.org/dc/dcmitype/StillImage" /> 693 | </cc:Work> 694 | </rdf:RDF> 695 | </metadata> 696 | <g 697 | inkscape:label="Ebene 1" 698 | inkscape:groupmode="layer" 699 | id="layer1" 700 | transform="translate(-29.5,-42.959476)"> 701 | <a 702 | id="a3142" 703 | transform="matrix(1.0873906,0,0,1,-4.4741999,0)"> 704 | <path 705 | transform="translate(11.586889,5.2908993)" 706 | d="M 47.02914 47.36993 A 8.5197716 9.2013531 0 1 1 29.989597,47.36993 A 8.5197716 9.2013531 0 1 1 47.02914 47.36993 z" 707 | sodipodi:ry="9.2013531" 708 | sodipodi:rx="8.5197716" 709 | sodipodi:cy="47.36993" 710 | sodipodi:cx="38.509369" 711 | id="path2160" 712 | style="fill:none;fill-rule:evenodd;stroke:#000000;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" 713 | sodipodi:type="arc" /> 714 | </a> 715 | <path 716 | sodipodi:type="arc" 717 | style="fill:none" 718 | id="path3134" 719 | sodipodi:cx="43.962021" 720 | sodipodi:cy="48.392303" 721 | sodipodi:rx="3.7486994" 722 | sodipodi:ry="0" 723 | d="M 47.71072 48.392303 A 3.7486994 0 0 1 1 40.213321,48.392303 A 3.7486994 0 0 1 1 47.71072 48.392303 z" /> 724 | <path 725 | style="fill:none;fill-rule:evenodd;stroke:#000000;stroke-width:1.24319649px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" 726 | d="M 50,61.33709 C 50,91.363211 50,92.247838 50,92.247838" 727 | id="path3136" /> 728 | <path 729 | style="fill:none;fill-rule:evenodd;stroke:#000000;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" 730 | d="M 69.760668,72.362183 C 69.760668,72.362183 69.760668,72.362183 50.239332,72.362183 C 30.239332,72.362183 30.239332,72.362183 30.239332,72.362183 L 30.239332,72.362183" 731 | id="path3138" /> 732 | <path 733 | style="fill:none;fill-rule:evenodd;stroke:#000000;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" 734 | d="M 30,111.45687 C 30,111.45687 30,111.45687 50,92.013532 C 70,111.45687 70,111.45687 70,111.45687" 735 | id="path3140" /> 736 | </g> 737 | </svg> 738 | 739 | 740 | 741 | 742 | -------------------------------------------------------------------------------- /docs/overview_messages.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dragosch/kurento-group-call/b9e4120829964de93c31ceffe2958de76cf2dd03/docs/overview_messages.png -------------------------------------------------------------------------------- /example/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "kurento-group-call-example", 3 | "version": "0.0.1", 4 | "description": "Simple javascript library used to initiate a group call (many to many video and audio call) using Kurento Media Server", 5 | "main": "server.js", 6 | "private": false, 7 | "scripts": { 8 | "postinstall": "cd static && bower install", 9 | "start": "supervisor app", 10 | "test": "mocha -w backend/test" 11 | }, 12 | "repository": { 13 | "type": "git", 14 | "url": "https://github.com/dragosch/kurento-group-call.git" 15 | }, 16 | "licenses": [ 17 | { 18 | "type": "MIT", 19 | "url": "https://github.com/dragosch/kurento-group-call/blob/master/LICENSE" 20 | } 21 | ], 22 | "dependencies": { 23 | "async": "~0.9.0", 24 | "cookie-parser": "^1.3.5", 25 | "express": "~4.12.3", 26 | "express-session": "^1.11.3", 27 | "kurento-client": "5.1.0", 28 | "kurento-group-call": "0.0.1", 29 | "waitfor": "^0.1.3" 30 | }, 31 | "devDependencies": { 32 | "bower": "^1.3.12", 33 | "grunt": "^0.4.5", 34 | "grunt-browserify": "~3.7.0", 35 | "grunt-cli": "~0.1.13", 36 | "grunt-contrib-clean": "~0.6.0", 37 | "grunt-contrib-jshint": "^0.11.2", 38 | "grunt-githooks": "^0.3.1", 39 | "grunt-jsbeautifier": "^0.2.10", 40 | "grunt-jscoverage": "^0.1.3", 41 | "grunt-jsdoc": "^0.6.3", 42 | "grunt-npm2bower-sync": "^0.8.1", 43 | "grunt-shell": "^1.1.2", 44 | "qunitjs": "^1.18.0", 45 | "minimist": "^1.1.0", 46 | "mocha": "^2.2.5", 47 | "should": "^6.0.3", 48 | "socket.io": "^1.3.5" 49 | }, 50 | "bugs": { 51 | "url": "https://github.com/dragosch/kurento-group-call/issues" 52 | }, 53 | "homepage": "https://github.com/dragosch/kurento-group-call", 54 | "directories": { 55 | "example": "example" 56 | }, 57 | "keywords": [ 58 | "Kurento", 59 | "WebRTC", 60 | "video", 61 | "audio" 62 | ], 63 | "author": "Alexander Dragosch ", 64 | "license": "MIT" 65 | } 66 | -------------------------------------------------------------------------------- /example/server.js: -------------------------------------------------------------------------------- 1 | /* 2 | * The MIT License (MIT) 3 | * 4 | * Copyright (c) 2015 MAPT 5 | * 6 | * Permission is hereby granted, free of charge, to any person obtaining a copy 7 | * of this software and associated documentation files (the "Software"), to deal 8 | * in the Software without restriction, including without limitation the rights 9 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | * copies of the Software, and to permit persons to whom the Software is 11 | * furnished to do so, subject to the following conditions: 12 | * 13 | * The above copyright notice and this permission notice shall be included in 14 | * all copies or substantial portions of the Software. 15 | * 16 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | * THE SOFTWARE. 23 | * 24 | */ 25 | 26 | 'use strict'; 27 | 28 | //mkdir node_modules/kurento-group-call && cp * node_modules/kurento-group-call && cp -r lib/ node_modules/kurento-group-call 29 | var kurentoGroupCall = require('kurento-group-call'); 30 | 31 | var path = require('path'); 32 | var express = require('express'); 33 | var session = require('express-session'); 34 | var cookieParser = require('cookie-parser'); 35 | var MemoryStore = session.MemoryStore; 36 | var minimist = require('minimist'); 37 | var url = require('url'); 38 | var authenticatedSockets = {}; 39 | 40 | 41 | var argv = minimist(process.argv.slice(2), { 42 | default: { 43 | as_uri: 'http://localhost:8080/', 44 | ws_uri: 'ws://localhost:8888/kurento' 45 | } 46 | }); 47 | 48 | 49 | var app = express(); 50 | 51 | var parseCookie = cookieParser(); 52 | var store = new MemoryStore(); 53 | 54 | app.use(parseCookie); 55 | app.use(session({ 56 | store: store, 57 | secret: '123456', 58 | key: 'sid' 59 | }) 60 | ); 61 | 62 | app.get('/users', function (req, res) { 63 | var userIds = []; 64 | for (var key in authenticatedSockets) { 65 | if (authenticatedSockets.hasOwnProperty(key)) { 66 | userIds.push(key); 67 | } 68 | } 69 | res.send(userIds); 70 | }); 71 | 72 | function initWebRtc(socket){ 73 | var sessionId = socket.id; 74 | console.log('initWebRtc: sessionId=', sessionId ); 75 | var sendMessage = function( messageName, data ) { 76 | socket.emit(messageName, data); 77 | }; 78 | kurentoGroupCall.start('ws://52.17.163.149:8888/kurento', sessionId, 79 | sendMessage); 80 | socket.on('error', function(error){ 81 | kurentoGroupCall.onError(error, sessionId); 82 | }); 83 | socket.on('close', function(){ 84 | kurentoGroupCall.onClose(sessionId); 85 | }); 86 | socket.on('startNewCall', function(data){ 87 | kurentoGroupCall.onStartNewCall(data, sessionId, sendMessage); 88 | }); 89 | socket.on('incomingCallAnswer', function(data){ 90 | kurentoGroupCall.onIncomingCallAnswer(data, sessionId, sendMessage); 91 | }); 92 | socket.on('receiveVideoFrom', function(data){ 93 | kurentoGroupCall.onReceiveVideoFrom(data, sessionId); 94 | }); 95 | socket.on('message', function(data){ 96 | kurentoGroupCall.onMessage(data, sessionId, sendMessage); 97 | }); 98 | socket.on('inviteUsers', function(data){ 99 | console.log('inviteUsers: ', data.userIds); 100 | 101 | var currentUserId = socket.id; 102 | console.log('inviteUsers: currentUserId=', currentUserId, ' socketId=', socket.id); 103 | var length = data.userIds.length; 104 | for (var i = 0; i < length; i++) { 105 | var userId = data.userIds[i]; 106 | var socks = authenticatedSockets[userId]; 107 | if (socks) { 108 | console.log('inviteUsers: send incomingCall message', socks.id); 109 | socks.emit('incomingCall', { from: currentUserId, callId: data.callId } ); 110 | } else { 111 | console.log('ERROR user not connected: userId=', userId); 112 | } 113 | } 114 | }); 115 | }; 116 | 117 | 118 | var initWebSockets = function(server) { 119 | console.log('initWebSockets:'); 120 | }; 121 | 122 | /* 123 | * Server startup 124 | */ 125 | var asUrl = url.parse(argv.as_uri); 126 | var port = asUrl.port; 127 | var server = app.listen(port, function () { 128 | console.log('Kurento Tutorial started'); 129 | console.log('Open ' + url.format(asUrl) + 130 | ' with a WebRTC capable browser'); 131 | }); 132 | var io = require('socket.io').listen(server); 133 | 134 | 135 | io.on('connection', function (socket) { 136 | console.log('CONNECT'); 137 | authenticatedSockets[socket.id] = socket; 138 | initWebRtc(socket); 139 | 140 | socket.on('disconnect', function () { 141 | console.log('CLOSE'); 142 | delete authenticatedSockets[socket.id]; 143 | }); 144 | }); 145 | 146 | 147 | 148 | 149 | app.use(express.static(path.join(__dirname, 'static'))); 150 | -------------------------------------------------------------------------------- /example/static/bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "kurento-groupcall", 3 | "version": "5.1.1-dev", 4 | "description": "Kurento Browser JavaScript Tutorial ", 5 | "authors": [ 6 | "Kurento " 7 | ], 8 | "main": "index.html", 9 | "moduleType": [ 10 | "globals" 11 | ], 12 | "license": "LGPL", 13 | "homepage": "http://www.kurento.org/", 14 | "private": true, 15 | "ignore": [ 16 | "**/.*", 17 | "node_modules", 18 | "bower_components", 19 | "test", 20 | "tests" 21 | ], 22 | "dependencies": { 23 | "bootstrap": "~3.3.0", 24 | "ekko-lightbox": "~3.1.4", 25 | "draggabilly": "~1.1.1", 26 | "adapter.js": "*", 27 | "kurento-utils": "5.1.0" 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /example/static/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 |
16 |
17 |
18 |

Join a Room

19 |

20 | 21 |

22 |
23 | 29 |
30 |
31 | 32 | 45 | 46 | 61 | 62 | 63 | 64 | -------------------------------------------------------------------------------- /example/static/js/conferenceroom.js: -------------------------------------------------------------------------------- 1 | /* 2 | * (C) Copyright 2014 Kurento (http://kurento.org/) 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 | */ 15 | 16 | var socket = io.connect('http://' + location.host + '/'); 17 | var currentUserId = null; 18 | var showInviteDialog = true; 19 | var participants = {}; 20 | var name; 21 | var callId = null; 22 | var users = []; 23 | 24 | socket.on('existingParticipants', onExistingParticipants); 25 | socket.on('incomingCall', onIncomingCall); 26 | socket.on('connect', function(){ 27 | currentUserId = socket.id; 28 | console.log('connect: socket.id=', currentUserId); 29 | }); 30 | 31 | socket.on('message', function (message) { 32 | var parsedMessage = message; 33 | console.info('Received message: ', message); 34 | 35 | switch (parsedMessage.id) { 36 | case 'newParticipantArrived': 37 | onNewParticipant(parsedMessage); 38 | break; 39 | case 'participantLeft': 40 | onParticipantLeft(parsedMessage); 41 | break; 42 | case 'receiveVideoAnswer': 43 | receiveVideoAnswer(parsedMessage); 44 | break; 45 | case 'allRooms': 46 | receiveAllRooms(parsedMessage); 47 | break; 48 | default: 49 | console.error('Unrecognized message', parsedMessage); 50 | } 51 | }); 52 | 53 | 54 | window.onbeforeunload = function() { 55 | socket.close(); 56 | }; 57 | 58 | function sendMessage() { 59 | var messageName = 'message'; 60 | var message; 61 | if (arguments.length === 1) { 62 | message = arguments[0]; 63 | } 64 | else 65 | if (arguments.length === 2) { 66 | messageName = arguments[0]; 67 | message = arguments[1]; 68 | } 69 | else { 70 | console.log('ERROR: invalid number of arguments.'); 71 | return; 72 | } 73 | console.log('Sending message:', messageName, message); 74 | socket.emit(messageName, message); 75 | } 76 | 77 | startNewCall = function () { 78 | document.getElementById('join').style.display = 'none'; 79 | document.getElementById('room').style.display = 'block'; 80 | 81 | if (currentUserId === null) { 82 | currentUserId = socket.id; 83 | } 84 | 85 | var message = { 86 | id: 'startNewCall', 87 | userId: currentUserId 88 | }; 89 | sendMessage('startNewCall', message); 90 | }; 91 | 92 | showAcceptCallDialog = function() { 93 | document.getElementById('acceptcall').className = 'modal show'; 94 | }; 95 | hideAcceptCallDialog = function() { 96 | document.getElementById('acceptcall').className = 'modal hide'; 97 | }; 98 | 99 | acceptCall = function() { 100 | console.info('accept call ', callId); 101 | document.getElementById('join').style.display = 'none'; 102 | document.getElementById('room').style.display = 'block'; 103 | sendIncomingCallAnswer('accepted', callId); 104 | hideAcceptCallDialog(); 105 | }; 106 | rejectCall = function() { 107 | console.info('reject call'); 108 | sendIncomingCallAnswer('rejected', callId); 109 | hideAcceptCallDialog(); 110 | }; 111 | hideInviteUsersDialog = function() { 112 | console.info('hideInviteUsersDialog'); 113 | document.getElementById('inviteusers').className = 'modal hide'; 114 | }; 115 | showInviteUsersDialog = function() { 116 | var element = document.getElementById('userlist'); 117 | while(element.firstChild) { 118 | element.removeChild(element.firstChild); 119 | } 120 | console.log( location.host ); 121 | $.ajax({ 122 | url: 'http://' + location.host + '/users' 123 | }).then(function(data) { 124 | console.log(data); 125 | users = data 126 | createCheckBoxes(element, users); 127 | document.getElementById('inviteusers').className = 'modal show'; 128 | }); 129 | }; 130 | 131 | 132 | inviteUsers = function() { 133 | console.info('inviteUsers'); 134 | var userIds = []; 135 | for (var i = 0; i < users.length; i++) { 136 | var chkbox = document.getElementById('chk_'+users[i]); 137 | console.info('inviteUsers ', chkbox); 138 | if (chkbox){ 139 | console.info('inviteUsers 1'); 140 | if (chkbox.checked) { 141 | console.info('inviteUsers 2'); 142 | userIds.push( users[i] ); 143 | } 144 | } 145 | } 146 | console.log( userIds ); 147 | var msg = { 148 | id: 'inviteUsers', 149 | userIds: userIds, 150 | callId: callId 151 | }; 152 | sendMessage('inviteUsers', msg); 153 | hideInviteUsersDialog(); 154 | }; 155 | 156 | 157 | function receiveVideoAnswer(result) { 158 | participants[result.name].rtcPeer.processSdpAnswer(result.sdpAnswer); 159 | if (currentUserId === result.callerId && showInviteDialog === true) { 160 | showInviteDialog = false; 161 | showInviteUsersDialog(); 162 | } 163 | } 164 | function sendIncomingCallAnswer(answer, pCallId) { 165 | var responseMsg = { 166 | id : 'incomingCallAnswer', 167 | from : currentUserId, 168 | answer: answer, //accepted | rejected 169 | callId: pCallId 170 | }; 171 | sendMessage('incomingCallAnswer', responseMsg); 172 | } 173 | function onIncomingCall(msg) { 174 | console.log('onIncomingCall:', msg); 175 | callId = msg.callId; 176 | showAcceptCallDialog(); 177 | } 178 | 179 | 180 | 181 | socket.on('message', function(message) { 182 | var parsedMessage = JSON.parse(message.data); 183 | console.info('Received message: ' + message.data); 184 | 185 | switch (parsedMessage.id) { 186 | case 'existingParticipants': 187 | onExistingParticipants(parsedMessage); 188 | break; 189 | case 'newParticipantArrived': 190 | onNewParticipant(parsedMessage); 191 | break; 192 | case 'participantLeft': 193 | onParticipantLeft(parsedMessage); 194 | break; 195 | case 'receiveVideoAnswer': 196 | receiveVideoResponse(parsedMessage); 197 | break; 198 | default: 199 | console.error('Unrecognized message', parsedMessage); 200 | } 201 | }); 202 | 203 | function register() { 204 | name = document.getElementById('name').value; 205 | var room = document.getElementById('roomName').value; 206 | 207 | document.getElementById('room-header').innerText = 'ROOM ' + room; 208 | document.getElementById('join').style.display = 'none'; 209 | document.getElementById('room').style.display = 'block'; 210 | 211 | var message = { 212 | id : 'joinRoom', 213 | name : name, 214 | room : room, 215 | } 216 | sendMessage(message); 217 | } 218 | 219 | function onNewParticipant(request) { 220 | receiveVideo(request.name); 221 | } 222 | 223 | function receiveVideoResponse(result) { 224 | participants[result.name].rtcPeer.processSdpAnswer(result.sdpAnswer); 225 | } 226 | 227 | function callResponse(message) { 228 | if (message.response != 'accepted') { 229 | console.info('Call not accepted by peer. Closing call'); 230 | stop(); 231 | } else { 232 | webRtcPeer.processSdpAnswer(message.sdpAnswer); 233 | } 234 | } 235 | 236 | function onExistingParticipants(msg) { 237 | console.log('onExistingParticipants:', msg); 238 | var constraints = { 239 | audio : true, 240 | video : { 241 | mandatory : { 242 | maxWidth : 320, 243 | maxFrameRate : 15, 244 | minFrameRate : 15 245 | } 246 | } 247 | }; 248 | console.log(currentUserId + ' registered'); 249 | 250 | if (msg.data.length === 0) { 251 | callId = msg.callId; 252 | } 253 | 254 | var participant = new Participant(currentUserId); 255 | participants[currentUserId] = participant; 256 | var video = participant.getVideoElement(); 257 | participant.rtcPeer = kurentoUtils.WebRtcPeer.startSendOnly(video, 258 | participant.offerToReceiveVideo.bind(participant), null, 259 | constraints); 260 | msg.data.forEach(receiveVideo); 261 | } 262 | 263 | 264 | 265 | function leaveRoom() { 266 | sendMessage({ 267 | id : 'leaveRoom' 268 | }); 269 | 270 | for ( var key in participants) { 271 | participants[key].dispose(); 272 | } 273 | 274 | document.getElementById('join').style.display = 'block'; 275 | document.getElementById('room').style.display = 'none'; 276 | } 277 | 278 | function receiveVideo(sender) { 279 | var participant = new Participant(sender); 280 | participants[sender] = participant; 281 | var video = participant.getVideoElement(); 282 | participant.rtcPeer = kurentoUtils.WebRtcPeer.startRecvOnly(video, 283 | participant.offerToReceiveVideo.bind(participant)); 284 | } 285 | 286 | function onParticipantLeft(request) { 287 | console.log('Participant ' + request.name + ' left'); 288 | var participant = participants[request.name]; 289 | participant.dispose(); 290 | delete participants[request.name]; 291 | } 292 | 293 | 294 | function createCheckBoxes( parentElement, userIds ){ 295 | console.log('createCheckBoxes: currentUserId=', currentUserId) 296 | var ul = document.createElement('ul'); 297 | parentElement.appendChild(ul); 298 | for (var i=0; i' 25 | * @return 26 | */ 27 | function Participant(name) { 28 | this.name = name; 29 | var container = document.createElement('div'); 30 | container.className = isPresentMainParticipant() ? PARTICIPANT_CLASS : PARTICIPANT_MAIN_CLASS; 31 | container.id = name; 32 | var span = document.createElement('span'); 33 | var video = document.createElement('video'); 34 | var rtcPeer; 35 | 36 | container.appendChild(video); 37 | container.appendChild(span); 38 | container.onclick = switchContainerClass; 39 | document.getElementById('participants').appendChild(container); 40 | 41 | span.appendChild(document.createTextNode(name)); 42 | 43 | video.id = 'video-' + name; 44 | video.autoplay = true; 45 | video.controls = false; 46 | 47 | 48 | this.getElement = function() { 49 | return container; 50 | } 51 | 52 | this.getVideoElement = function() { 53 | return video; 54 | } 55 | 56 | function switchContainerClass() { 57 | if (container.className === PARTICIPANT_CLASS) { 58 | var elements = Array.prototype.slice.call(document.getElementsByClassName(PARTICIPANT_MAIN_CLASS)); 59 | elements.forEach(function(item) { 60 | item.className = PARTICIPANT_CLASS; 61 | }); 62 | 63 | container.className = PARTICIPANT_MAIN_CLASS; 64 | } else { 65 | container.className = PARTICIPANT_CLASS; 66 | } 67 | } 68 | 69 | function isPresentMainParticipant() { 70 | return ((document.getElementsByClassName(PARTICIPANT_MAIN_CLASS)).length != 0); 71 | } 72 | 73 | this.offerToReceiveVideo = function(offerSdp, wp){ 74 | console.log('Invoking SDP offer callback function'); 75 | var msg = { id : "receiveVideoFrom", 76 | sender : name, 77 | sdpOffer : offerSdp 78 | }; 79 | sendMessage('receiveVideoFrom', msg); 80 | } 81 | 82 | Object.defineProperty(this, 'rtcPeer', { writable: true}); 83 | 84 | this.dispose = function() { 85 | console.log('Disposing participant ' + this.name); 86 | this.rtcPeer.dispose(); 87 | container.parentNode.removeChild(container); 88 | }; 89 | } 90 | -------------------------------------------------------------------------------- /example/static/style.css: -------------------------------------------------------------------------------- 1 | @CHARSET "UTF-8"; 2 | 3 | body { 4 | font: 13px/20px "Lucida Grande", Tahoma, Verdana, sans-serif; 5 | color: #404040; 6 | background: #0ca3d2; 7 | } 8 | 9 | input[type=checkbox], input[type=radio] { 10 | border: 1px solid #c0c0c0; 11 | margin: 0 0.1em 0 0; 12 | padding: 0; 13 | font-size: 16px; 14 | line-height: 1em; 15 | width: 1.25em; 16 | height: 1.25em; 17 | background: #fff; 18 | background: -webkit-gradient(linear, 0% 0%, 0% 100%, from(#ededed), 19 | to(#fbfbfb)); 20 | -webkit-appearance: none; 21 | -webkit-box-shadow: 1px 1px 1px #fff; 22 | -webkit-border-radius: 0.25em; 23 | vertical-align: text-top; 24 | display: inline-block; 25 | } 26 | 27 | input[type=radio] { 28 | -webkit-border-radius: 2em; /* Make radios round */ 29 | } 30 | 31 | input[type=checkbox]:checked::after { 32 | content: "✔"; 33 | display: block; 34 | text-align: center; 35 | font-size: 16px; 36 | height: 16px; 37 | line-height: 18px; 38 | } 39 | 40 | input[type=radio]:checked::after { 41 | content: "●"; 42 | display: block; 43 | height: 16px; 44 | line-height: 15px; 45 | font-size: 20px; 46 | text-align: center; 47 | } 48 | 49 | select { 50 | border: 1px solid #D0D0D0; 51 | background: url(http://www.quilor.com/i/select.png) no-repeat right 52 | center, -webkit-gradient(linear, 0% 0%, 0% 100%, from(#fbfbfb), 53 | to(#ededed)); 54 | background: -moz-linear-gradient(19% 75% 90deg, #ededed, #fbfbfb); 55 | -webkit-box-shadow: 0 1px 2px rgba(0, 0, 0, .05); 56 | -moz-box-shadow: 0 1px 2px rgba(0, 0, 0, .05); 57 | -webkit-box-shadow: 0 1px 2px rgba(0, 0, 0, .05); 58 | color: #444; 59 | } 60 | 61 | .container { 62 | margin: 50px auto; 63 | width: 640px; 64 | } 65 | 66 | .join { 67 | position: relative; 68 | margin: 0 auto; 69 | padding: 20px 20px 20px; 70 | width: 310px; 71 | background: white; 72 | border-radius: 3px; 73 | -webkit-box-shadow: 0 0 200px rgba(255, 255, 255, 0.5), 0 1px 2px 74 | rgba(0, 0, 0, 0.3); 75 | box-shadow: 0 0 200px rgba(255, 255, 255, 0.5), 0 1px 2px 76 | rgba(0, 0, 0, 0.3); 77 | /*Transition*/ 78 | -webkit-transition: all 0.3s linear; 79 | -moz-transition: all 0.3s linear; 80 | -o-transition: all 0.3s linear; 81 | transition: all 0.3s linear; 82 | } 83 | 84 | .join:before { 85 | content: ''; 86 | position: absolute; 87 | top: -8px; 88 | right: -8px; 89 | bottom: -8px; 90 | left: -8px; 91 | z-index: -1; 92 | background: rgba(0, 0, 0, 0.08); 93 | border-radius: 4px; 94 | } 95 | 96 | .join h1 { 97 | margin: -20px -20px 21px; 98 | line-height: 40px; 99 | font-size: 15px; 100 | font-weight: bold; 101 | color: #555; 102 | text-align: center; 103 | text-shadow: 0 1px white; 104 | background: #f3f3f3; 105 | border-bottom: 1px solid #cfcfcf; 106 | border-radius: 3px 3px 0 0; 107 | background-image: -webkit-linear-gradient(top, whiteffd, #eef2f5); 108 | background-image: -moz-linear-gradient(top, whiteffd, #eef2f5); 109 | background-image: -o-linear-gradient(top, whiteffd, #eef2f5); 110 | background-image: linear-gradient(to bottom, whiteffd, #eef2f5); 111 | -webkit-box-shadow: 0 1px whitesmoke; 112 | box-shadow: 0 1px whitesmoke; 113 | } 114 | 115 | .join p { 116 | margin: 20px 0 0; 117 | } 118 | 119 | .join p:first-child { 120 | margin-top: 0; 121 | } 122 | 123 | .join input[type=text], .join input[type=password] { 124 | width: 278px; 125 | } 126 | 127 | .join p.submit { 128 | text-align: center; 129 | } 130 | 131 | :-moz-placeholder { 132 | color: #c9c9c9 !important; 133 | font-size: 13px; 134 | } 135 | 136 | ::-webkit-input-placeholder { 137 | color: #ccc; 138 | font-size: 13px; 139 | } 140 | 141 | input { 142 | font-family: 'Lucida Grande', Tahoma, Verdana, sans-serif; 143 | font-size: 14px; 144 | } 145 | 146 | input[type=text], input[type=password] { 147 | margin: 5px; 148 | padding: 0 10px; 149 | width: 200px; 150 | height: 34px; 151 | color: #404040; 152 | background: white; 153 | border: 1px solid; 154 | border-color: #c4c4c4 #d1d1d1 #d4d4d4; 155 | border-radius: 2px; 156 | outline: 5px solid #eff4f7; 157 | -moz-outline-radius: 3px; 158 | -webkit-box-shadow: inset 0 1px 3px rgba(0, 0, 0, 0.12); 159 | box-shadow: inset 0 1px 3px rgba(0, 0, 0, 0.12); 160 | } 161 | 162 | input[type=text]:focus, input[type=password]:focus { 163 | border-color: #7dc9e2; 164 | outline-color: #dceefc; 165 | outline-offset: 0; 166 | } 167 | 168 | input[type=button], input[type=submit] { 169 | padding: 0 18px; 170 | height: 29px; 171 | font-size: 12px; 172 | font-weight: bold; 173 | color: #527881; 174 | text-shadow: 0 1px #e3f1f1; 175 | background: #cde5ef; 176 | border: 1px solid; 177 | border-color: #b4ccce #b3c0c8 #9eb9c2; 178 | border-radius: 16px; 179 | outline: 0; 180 | -webkit-box-sizing: content-box; 181 | -moz-box-sizing: content-box; 182 | box-sizing: content-box; 183 | background-image: -webkit-linear-gradient(top, #edf5f8, #cde5ef); 184 | background-image: -moz-linear-gradient(top, #edf5f8, #cde5ef); 185 | background-image: -o-linear-gradient(top, #edf5f8, #cde5ef); 186 | background-image: linear-gradient(to bottom, #edf5f8, #cde5ef); 187 | -webkit-box-shadow: inset 0 1px white, 0 1px 2px rgba(0, 0, 0, 0.15); 188 | box-shadow: inset 0 1px white, 0 1px 2px rgba(0, 0, 0, 0.15); 189 | } 190 | 191 | input[type=button]:active, input[type=submit]:active { 192 | background: #cde5ef; 193 | border-color: #9eb9c2 #b3c0c8 #b4ccce; 194 | -webkit-box-shadow: inset 0 0 3px rgba(0, 0, 0, 0.2); 195 | box-shadow: inset 0 0 3px rgba(0, 0, 0, 0.2); 196 | } 197 | 198 | .lt-ie9 input[type=text], .lt-ie9 input[type=password] { 199 | line-height: 34px; 200 | } 201 | 202 | #room { 203 | width: 100%; 204 | text-align: center; 205 | } 206 | 207 | #button-leave { 208 | text-align: center; 209 | position: absolute; 210 | bottom: 10px; 211 | } 212 | 213 | .participant { 214 | border-radius: 4px; 215 | /* border: 2px groove; */ 216 | margin-left: 5; 217 | margin-right: 5; 218 | width: 150; 219 | text-align: center; 220 | overflow: hide; 221 | float: left; 222 | padding: 5px; 223 | border-radius: 10px; 224 | -webkit-box-shadow: 0 0 200px rgba(255, 255, 255, 0.5), 0 1px 2px 225 | rgba(0, 0, 0, 0.3); 226 | box-shadow: 0 0 200px rgba(255, 255, 255, 0.5), 0 1px 2px 227 | rgba(0, 0, 0, 0.3); 228 | /*Transition*/ 229 | -webkit-transition: all 0.3s linear; 230 | -moz-transition: all 0.3s linear; 231 | -o-transition: all 0.3s linear; 232 | transition: all 0.3s linear; 233 | } 234 | 235 | .participant:before { 236 | content: ''; 237 | position: absolute; 238 | top: -8px; 239 | right: -8px; 240 | bottom: -8px; 241 | left: -8px; 242 | z-index: -1; 243 | background: rgba(0, 0, 0, 0.08); 244 | border-radius: 4px; 245 | } 246 | 247 | .participant:hover { 248 | opacity: 1; 249 | background-color: 0A33B6; 250 | -webkit-transition: all 0.5s linear; 251 | transition: all 0.5s linear; 252 | } 253 | 254 | .participant video, .participant.main video { 255 | width: 100%; ! important; 256 | height: auto; 257 | ! 258 | important; 259 | } 260 | 261 | .participant span { 262 | color: PapayaWhip; 263 | } 264 | 265 | .participant.main { 266 | width: 20%; 267 | margin: 0 auto; 268 | } 269 | 270 | .participant.main video { 271 | height: auto; 272 | } 273 | 274 | .animate { 275 | -webkit-animation-duration: 0.5s; 276 | -webkit-animation-fill-mode: both; 277 | -moz-animation-duration: 0.5s; 278 | -moz-animation-fill-mode: both; 279 | -o-animation-duration: 0.5s; 280 | -o-animation-fill-mode: both; 281 | -ms-animation-duration: 0.5s; 282 | -ms-animation-fill-mode: both; 283 | animation-duration: 0.5s; 284 | animation-fill-mode: both; 285 | } 286 | 287 | .removed { 288 | -webkit-animation: disapear 1s; 289 | -webkit-animation-fill-mode: forwards; 290 | animation: disapear 1s; 291 | animation-fill-mode: forwards; 292 | } 293 | 294 | @ 295 | -webkit-keyframes disapear { 50% { 296 | -webkit-transform: translateX(-5%); 297 | transform: translateX(-5%); 298 | } 299 | 300 | 100% 301 | { 302 | -webkit-transform 303 | 304 | 305 | : 306 | 307 | 308 | 309 | translateX 310 | 311 | 312 | (200%); 313 | transform 314 | 315 | 316 | : 317 | 318 | 319 | 320 | translateX 321 | 322 | 323 | (200%); 324 | } 325 | } 326 | @ 327 | keyframes disapear { 50% { 328 | -webkit-transform: translateX(-5%); 329 | transform: translateX(-5%); 330 | } 331 | 332 | 100% 333 | { 334 | -webkit-transform 335 | 336 | 337 | : 338 | 339 | 340 | 341 | translateX 342 | 343 | 344 | (200%); 345 | transform 346 | 347 | 348 | : 349 | 350 | 351 | 352 | translateX 353 | 354 | 355 | (200%); 356 | } 357 | } 358 | a.hovertext { 359 | position: relative; 360 | width: 500px; 361 | text-decoration: none !important; 362 | text-align: center; 363 | } 364 | 365 | a.hovertext:after { 366 | content: attr(title); 367 | position: absolute; 368 | left: 0; 369 | bottom: 0; 370 | padding: 0.5em 20px; 371 | width: 460px; 372 | background: rgba(0, 0, 0, 0.8); 373 | text-decoration: none !important; 374 | color: #fff; 375 | opacity: 0; 376 | -webkit-transition: 0.5s; 377 | -moz-transition: 0.5s; 378 | -o-transition: 0.5s; 379 | -ms-transition: 0.5s; 380 | } 381 | 382 | a.hovertext:hover:after, a.hovertext:focus:after { 383 | opacity: 1.0; 384 | } -------------------------------------------------------------------------------- /lib/backend/call.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var logger = require('./logger'); 4 | var UserSession = require('./user_session'); 5 | var shortid = require('shortid'); 6 | 7 | // Constructor 8 | function Call(mediaPipeline) { 9 | // always initialize all instance properties 10 | this.mediaPipeline = mediaPipeline; 11 | this.participants = {}; 12 | this.callId = shortid.generate(); 13 | this.callerId = null; // the first caller who initiate this call session 14 | 15 | logger.info('CALL ' + this.callId + ' has been created.'); 16 | } 17 | 18 | Call.prototype.getId = function () { 19 | return this.callId; 20 | }; 21 | 22 | Call.prototype.isEmpty = function () { 23 | if (Object.keys(this.participants).length === 0) { 24 | return false; 25 | } 26 | return true; 27 | }; 28 | 29 | Call.prototype.leave = function (userSession) { 30 | logger.info('PARTICIPANT ' + userSession.getUserName() + ' leaving call ' + 31 | this.callId); 32 | this.removeParticipant(userSession.getUserName(), function (user) { 33 | user.close(); 34 | }); 35 | }; 36 | 37 | Call.prototype.join = function (userId, sendMessageCallback, sessionId, userRegistry) { 38 | logger.info('CALL ' + this.callId + ': adding participant ' + userId); 39 | var participant = new UserSession(userId, this, sendMessageCallback, 40 | sessionId, this.mediaPipeline); 41 | 42 | if (this.callerId === null) { 43 | this.callerId = userId; 44 | } 45 | 46 | var self = this; 47 | participant.createWebRtcEndpoint(function () { 48 | userRegistry.register(participant); 49 | 50 | self.notifyOtherParticipants(participant); 51 | self.sendParticipantNames(participant); 52 | self.participants[userId] = participant; 53 | }); 54 | }; 55 | 56 | Call.prototype.sendParticipantNames = function (userSession) { 57 | var otherParticipants = []; 58 | var userName = userSession.getUserName(); 59 | var name; 60 | for (name in this.participants) { 61 | if (name !== userName) { 62 | otherParticipants.push(name); 63 | } 64 | } 65 | 66 | var participantsMessage = { 67 | id : 'existingParticipants', 68 | data : otherParticipants, 69 | callId : this.callId, 70 | callerId : this.callerId 71 | }; 72 | 73 | logger.info('PARTICIPANT ' + userName + ': sending a list of ' + 74 | otherParticipants.length + ' participants'); 75 | userSession.sendMessage('existingParticipants', participantsMessage); 76 | }; 77 | 78 | Call.prototype.notifyOtherParticipants = function (newParticipant) { 79 | 80 | var newParticipantMsg = { 81 | id : 'newParticipantArrived', 82 | name : newParticipant.getUserName() 83 | }; 84 | 85 | logger.info('CALL ' + this.callId + 86 | ': notifying other participants of new participant ' + newParticipant.getUserName() 87 | ); 88 | for (var name in this.participants) { 89 | this.participants[name].sendMessage(newParticipantMsg); 90 | } 91 | }; 92 | 93 | Call.prototype.removeParticipant = function (userName, callback) { 94 | logger.info('CALL ' + this.callId + ': notifying all users that ' + 95 | userName + ' is leaving the call'); 96 | 97 | var participant = this.participants[userName]; 98 | 99 | if (participant) { 100 | logger.info('Remove from participants list.'); 101 | delete this.participants[userName]; 102 | } else { 103 | logger.info('Participant not found in this call.'); 104 | return; 105 | } 106 | 107 | var participantLeftMsg = {}; 108 | participantLeftMsg.id = 'participantLeft'; 109 | participantLeftMsg.name = userName; 110 | 111 | for (var name in this.participants) { 112 | if (name !== userName) { 113 | logger.info('Notifying user ' + name); 114 | this.participants[name].cancelVideoFrom(userName); 115 | this.participants[name].sendMessage(participantLeftMsg); 116 | } 117 | } 118 | 119 | callback(participant); 120 | }; 121 | 122 | Call.prototype.close = function () { 123 | logger.info('close call ' + this.callId); 124 | for (var name in this.participants) { 125 | this.participants[name].close(); 126 | delete this.participants[name]; 127 | } 128 | 129 | this.mediaPipeline.release(function (error) { 130 | logger.info('release mediapipeline...'); 131 | if (error) { 132 | logger.error('failed to release mediapipeline: ' + error); 133 | } 134 | }); 135 | }; 136 | 137 | // export the class 138 | module.exports = Call; 139 | -------------------------------------------------------------------------------- /lib/backend/call_manager.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var async = require('async'); 4 | var logger = require('./logger'); 5 | var Call = require('./call'); 6 | var kurento = require('kurento-client'); 7 | 8 | var kurentoClient = null; 9 | 10 | function getKurentoClient(ws_uri, callback) { 11 | logger.info('getKurentoClient: ' + ws_uri); 12 | 13 | if (kurentoClient !== null) { 14 | return callback(null, kurentoClient); 15 | } 16 | 17 | async.waterfall([ 18 | async.apply(kurento, ws_uri) 19 | ], 20 | function (error, _kurentoClient) { 21 | if (error) { 22 | var message = 'Could not find media server at address ' + ws_uri; 23 | logger.info(message); 24 | return null; 25 | } 26 | logger.info( 27 | 'getKurentoClient: global variable kurentoClient initialized'); 28 | kurentoClient = _kurentoClient; 29 | //return _kurentoClient; 30 | callback(null, kurentoClient); 31 | }); 32 | // return kurentoClient; 33 | 34 | /* 35 | kurento(ws_uri, function (error, _kurentoClient) { 36 | if (error) { 37 | var message = 'Could not find media server at address ' + ws_uri; 38 | logger.info(message); 39 | return callback(message + ". Exiting with error " + error); 40 | } 41 | 42 | logger.info( 43 | 'getKurentoClient: global variable kurentoClient initialized'); 44 | kurentoClient = _kurentoClient; 45 | callback(null, kurentoClient); 46 | });*/ 47 | } 48 | 49 | function CallManager(ws_uri) { 50 | this.ws_uri = ws_uri; 51 | this.callsById = {}; 52 | } 53 | 54 | CallManager.prototype.getCall = function (callId, callback) { 55 | logger.info('searching for call ' + callId); 56 | 57 | var call = this.callsById[callId]; 58 | if (call) { 59 | logger.info('Call ' + callId + ' found.'); 60 | 61 | if (callback) { 62 | callback(null, call); 63 | } 64 | } else { 65 | 66 | logger.info('Call ' + callId + ' not existent. Create now !'); 67 | 68 | var self = this; 69 | async.waterfall([ 70 | //async.apply( getKurentoClient, this.ws_uri ) 71 | function (callback)  { 72 | getKurentoClient(self.ws_uri, callback); 73 | //callback( null, kurentoClient ); 74 | } 75 | 76 | ], function end(error, k) { 77 | console.log(k); 78 | if (error) { 79 | logger.error('createPipeline: error=' + error); 80 | return; 81 | } 82 | kurentoClient.create('MediaPipeline', function (error, pipeline) { 83 | if (error) { 84 | logger.error('createPipeline: MediaPipeline: error=' + 85 | error); 86 | callback(error); 87 | return; 88 | } 89 | var r = new Call(pipeline); 90 | logger.info('MediaPipeline for call ' + r.callId + 91 | ' created: ' + pipeline); 92 | self.callsById[r.callId] = r; 93 | callback(null, r); 94 | }); 95 | }); 96 | //var k = getKurentoClient(this.ws_uri); 97 | // console.log('---');console.log(k); 98 | /* 99 | var self = this; 100 | getKurentoClient(this.ws_uri, function (error, kurentoClient) { 101 | if (error) { 102 | logger.error('createPipeline: error=' + error); 103 | return; 104 | } 105 | 106 | kurentoClient.create('MediaPipeline', function (error, pipeline) { 107 | if (error) { 108 | logger.error('createPipeline: MediaPipeline: error=' + 109 | error); 110 | return; 111 | } 112 | logger.info('MediaPipeline for room ' + roomName + 113 | ' created: ' + pipeline); 114 | var r = new Room(roomName, pipeline); 115 | self.roomsByName[roomName] = r; 116 | callback(r); 117 | }); 118 | });*/ 119 | } 120 | }; 121 | 122 | CallManager.prototype.removeCall = function (call) { 123 | var callId = call.getId(); 124 | var r = this.callsById[callId]; 125 | if (r) { 126 | delete this.callsById[callId]; 127 | } 128 | call.close(); 129 | logger.info('Call ' + callId + ' removed and closed.'); 130 | }; 131 | 132 | CallManager.prototype.getCallIds = function () { 133 | var calls = []; 134 | for (var id in this.callsById) { 135 | calls.push(id); 136 | } 137 | return calls; 138 | }; 139 | 140 | module.exports = CallManager; 141 | -------------------------------------------------------------------------------- /lib/backend/logger.js: -------------------------------------------------------------------------------- 1 | /* 2 | var winston = require('winston'); 3 | 4 | var logger = new (winston.Logger)({ 5 | transports: [ 6 | new (winston.transports.Console)({ json: false, timestamp: true }), 7 | new winston.transports.File({ filename: __dirname + '/debug.log', json: false }) 8 | ], 9 | exceptionHandlers: [ 10 | new (winston.transports.Console)({ json: false, timestamp: true }), 11 | new winston.transports.File({ filename: __dirname + '/exceptions.log', json: false }) 12 | ], 13 | exitOnError: false 14 | }); 15 | */ 16 | 17 | 'use strict'; 18 | 19 | function Logger() {} 20 | 21 | Logger.prototype.info = function (s) { 22 | console.log(s); 23 | }; 24 | Logger.prototype.error = function (s) { 25 | console.log(s); 26 | }; 27 | 28 | var logger = new Logger(); 29 | 30 | module.exports = logger; 31 | -------------------------------------------------------------------------------- /lib/backend/test/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | JavaScript Kurento Utils 6 | 7 | 10 | 11 | 16 | 17 | 18 |
19 |
20 | 21 | 22 |
23 | 24 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /lib/backend/test/mocha.opts: -------------------------------------------------------------------------------- 1 | --require should 2 | --reporter spec 3 | --ui bdd 4 | --recursive 5 | -------------------------------------------------------------------------------- /lib/backend/test/test.user.registry.js: -------------------------------------------------------------------------------- 1 | 2 | 'use strict'; 3 | 4 | var should = require('should'); 5 | var UserRegistry = require('../user_registry.js'); 6 | var UserSession = require('../user_session.js'); 7 | 8 | 9 | 10 | describe('test user registry:', function () { 11 | it('getByName should return null if user does not exist', function (done) { 12 | var userRegistry = new UserRegistry(); 13 | var user = userRegistry.getByName('huhu'); 14 | should.equal(user, null); 15 | //should(user).not.be.ok; 16 | // must call done() so that mocha know that we are... done. 17 | // Useful for async tests. 18 | done(); 19 | }); 20 | 21 | it('getByName should return a user if user exists', function (done) { 22 | var userRegistry = new UserRegistry(); 23 | var userSession = new UserSession( 'userOne', 'room', null, 'sessionOne' ); 24 | 25 | userRegistry.register(userSession); 26 | var user = userRegistry.getByName('userOne'); 27 | should.exist(user); 28 | done(); 29 | }); 30 | 31 | it('getBySessionId should return null if user does not exist', function (done) { 32 | var userRegistry = new UserRegistry(); 33 | var user = userRegistry.getBySessionId('huhu'); 34 | should.equal(user, null); 35 | done(); 36 | }); 37 | 38 | it('getBySessionId should return a user if user exists', function (done) { 39 | var userRegistry = new UserRegistry(); 40 | var userSession = new UserSession( 'userOne', 'room', null, 'sessionOne' ); 41 | 42 | userRegistry.register(userSession); 43 | var user = userRegistry.getBySessionId('sessionOne'); 44 | should.exist(user); 45 | done(); 46 | }); 47 | 48 | it('removeBySession should return null if user does not exist', function (done) { 49 | var userRegistry = new UserRegistry(); 50 | var user = userRegistry.removeBySession('huhu'); 51 | should.equal(user, null); 52 | done(); 53 | }); 54 | 55 | it('removeBySession should return the removed user if user exists', function (done) { 56 | var userRegistry = new UserRegistry(); 57 | var userSession = new UserSession( 'userOne', 'room', null, 'sessionOne' ); 58 | 59 | userRegistry.register(userSession); 60 | var user = userRegistry.removeBySession('sessionOne'); 61 | should.exist(user); 62 | done(); 63 | }); 64 | 65 | it('after call of removeBySession the user does not exist any more', function (done) { 66 | var userRegistry = new UserRegistry(); 67 | var userSession = new UserSession( 'userOne', 'room', null, 'sessionOne' ); 68 | 69 | userRegistry.register(userSession); 70 | var user = userRegistry.removeBySession('sessionOne'); 71 | 72 | user = userRegistry.getByName('userOne'); 73 | should.equal(user, null); 74 | 75 | user = userRegistry.getBySessionId('sessionOne'); 76 | should.equal(user, null); 77 | 78 | user = userRegistry.removeBySession('userOne'); 79 | should.equal(user, null); 80 | 81 | done(); 82 | }); 83 | 84 | }); -------------------------------------------------------------------------------- /lib/backend/user_registry.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var logger = require('./logger'); 4 | 5 | /* 6 | * Map of users registered in the system. 7 | */ 8 | function UserRegistry() { 9 | this.usersByName = {}; 10 | this.usersBySessionId = {}; 11 | } 12 | 13 | UserRegistry.prototype.getUserIds = function () { 14 | var ids = []; 15 | for (var id in this.usersByName) { 16 | ids.push( id ); 17 | } 18 | return ids; 19 | }; 20 | 21 | UserRegistry.prototype.register = function (userSession) { 22 | var userName = userSession.getUserName(); 23 | logger.info('register user ' + userName); 24 | 25 | this.usersByName[userName] = userSession; 26 | this.usersBySessionId[userSession.getSessionId()] = userSession; 27 | }; 28 | 29 | UserRegistry.prototype.getByName = function (userName) { 30 | return this.usersByName[userName]; 31 | }; 32 | 33 | UserRegistry.prototype.getBySessionId = function (sessionId) { 34 | return this.usersBySessionId[sessionId]; 35 | }; 36 | 37 | UserRegistry.prototype.removeBySession = function (sessionId) { 38 | var user = this.getBySessionId(sessionId); 39 | if (user) { 40 | delete this.usersByName[user.getUserName()]; 41 | delete this.usersBySessionId[sessionId]; 42 | return user; 43 | } 44 | }; 45 | 46 | module.exports = UserRegistry; 47 | -------------------------------------------------------------------------------- /lib/backend/user_session.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var logger = require('./logger'); 4 | 5 | function UserSession(userName, call, sendMessageCallback, sessionId, mediaPipeline) { 6 | this.userName = userName; 7 | this.call = call; 8 | this.sendMessageCallback = sendMessageCallback; 9 | this.sessionId = sessionId; 10 | this.mediaPipeline = mediaPipeline; 11 | this.outgoingMedia = null; 12 | this.incomingMedia = {}; 13 | } 14 | 15 | UserSession.prototype.createWebRtcEndpoint = function (callback) { 16 | var self = this; 17 | if (this.mediaPipeline) { 18 | this.mediaPipeline.create('WebRtcEndpoint', function (error, webRtc) { 19 | 20 | logger.info('create WebRtcEndpoint for ' + self.userName); 21 | if (error) { 22 | logger.error('failed to create WebRtcEndpoint for user ' + self.userName + 23 | ', call ' + self.call.callId); 24 | return; 25 | } 26 | 27 | self.outgoingMedia = webRtc; 28 | logger.info('WebRtcEndpoint for user ' + self.userName + ', call ' + 29 | self.call.callId + ' created.'); 30 | callback(); 31 | }); 32 | } 33 | }; 34 | 35 | UserSession.prototype.getOutgoingWebRtcPeer = function () { 36 | return this.outgoingMedia; 37 | }; 38 | 39 | UserSession.prototype.getUserName = function () { 40 | return this.userName; 41 | }; 42 | 43 | UserSession.prototype.getCallId = function () { 44 | return this.call.callId; 45 | }; 46 | 47 | UserSession.prototype.getSessionId = function () { 48 | return this.sessionId; 49 | }; 50 | 51 | function addMidsForFirefox(sdpOffer, sdpAnswer) { 52 | var sdpOfferLines = sdpOffer.split('\r\n'); 53 | 54 | var bundleLine = ''; 55 | var audioMid = ''; 56 | var videoMid = ''; 57 | var nextMid = ''; 58 | 59 | for (var i = 0; i < sdpOfferLines.length; ++i) { 60 | if (sdpOfferLines[i].indexOf('a=group:BUNDLE') === 0) { 61 | bundleLine = sdpOfferLines[i]; 62 | } else if (sdpOfferLines[i].indexOf('m=') === 0) { 63 | nextMid = sdpOfferLines[i].split(' ')[0]; 64 | } else if (sdpOfferLines[i].indexOf('a=mid') === 0) { 65 | if (nextMid === 'm=audio') { 66 | audioMid = sdpOfferLines[i]; 67 | } else if (nextMid === 'm=video') { 68 | videoMid = sdpOfferLines[i]; 69 | } 70 | } 71 | } 72 | 73 | return sdpAnswer.replace(/a=group:BUNDLE.*/, bundleLine) 74 | .replace(/a=mid:audio/, audioMid) 75 | .replace(/a=mid:video/, videoMid); 76 | } 77 | 78 | UserSession.prototype.receiveVideoFrom = function (sender, sdpOffer) { 79 | var senderName = sender.getUserName(); 80 | logger.info('USER ' + this.userName + ': connecting with ' + senderName + 81 | ' in call ' + this.call.callId); 82 | logger.info('USER ' + this.userName + ': SdpOffer for ' + senderName + 83 | ' is ..'); // + sdpOffer ); 84 | 85 | var self = this; 86 | this.getEndpointForUser(sender, function (webRtc, error) { 87 | if (error) { 88 | logger.error('ERROR: ' + error); 89 | return; 90 | } 91 | //console.log(webRtc); 92 | webRtc.processOffer(sdpOffer, function (error, sdpAnswer) { 93 | logger.info('processOffer'); 94 | if (error) { 95 | logger.error('ERROR: ' + error); 96 | return; 97 | } 98 | 99 | //console.log(sdpAnswer) 100 | var params = { 101 | id: 'receiveVideoAnswer', 102 | name: sender.getUserName(), 103 | sdpAnswer: addMidsForFirefox(sdpOffer, sdpAnswer), 104 | callerId: self.call.callerId 105 | }; 106 | 107 | //logger.info('USER ' + self.userName + ': SdpAnswer for ' + senderName + ' is ' + sdpAnswer ); 108 | logger.info('USER ' + self.userName + ': SdpAnswer for ' + 109 | senderName + '...'); 110 | self.sendMessage(params); 111 | }); 112 | }); 113 | }; 114 | 115 | UserSession.prototype.getEndpointForUser = function (senderSession, callback) { 116 | 117 | var senderName = senderSession.getUserName(); 118 | 119 | if (senderName === this.userName) { 120 | logger.info('PARTICIPANT ' + this.userName + ': configuring loopback'); 121 | callback(this.outgoingMedia); 122 | return; 123 | } 124 | 125 | logger.info('PARTICIPANT ' + this.userName + ': receiving video from ' + 126 | senderName); 127 | 128 | var incoming = this.incomingMedia[senderName]; 129 | if (incoming === undefined) { 130 | this.createNewEndpointForUser(senderSession, callback); 131 | } else { 132 | logger.info('PARTICIPANT ' + this.userName + 133 | ': using existing endpoint for ' + senderName); 134 | //console.log(incoming); 135 | callback(incoming); 136 | } 137 | }; 138 | 139 | UserSession.prototype.createNewEndpointForUser = function (senderSession, 140 | callback) { 141 | var senderName = senderSession.getUserName(); 142 | logger.info('PARTICIPANT ' + this.userName + 143 | ': creating new endpoint for ' + senderName); 144 | var self = this; 145 | this.mediaPipeline.create('WebRtcEndpoint', function (error, webRtc) { 146 | logger.info('create WebRtcEndpoint'); 147 | if (error) { 148 | logger.error('failed to create WebRtcEndpoint for user ' + self.userName + 149 | ', call ' + self.call.callId); 150 | return; 151 | } 152 | 153 | self.incomingMedia[senderName] = webRtc; 154 | logger.info('PARTICIPANT ' + self.userName + 155 | ': obtained endpoint for ' + senderName); 156 | senderSession.getOutgoingWebRtcPeer().connect(webRtc, function ( 157 | error) { 158 | if (error) { 159 | //TODO pipeline.release(); 160 | logger.info( 161 | 'createPipeline: MediaPipeline: WebRtcEndpoint: WebRtcEndpoint: connect: error=' + 162 | error); 163 | //return callback(error); 164 | callback(null, error); 165 | return; 166 | } 167 | callback(webRtc); 168 | return; 169 | }); 170 | }); 171 | }; 172 | 173 | UserSession.prototype.cancelVideoFrom = function (senderName) { 174 | logger.info('PARTICIPANT ' + this.userName + 175 | ': canceling video reception from ' + senderName); 176 | 177 | var incoming = this.incomingMedia[senderName]; 178 | if (incoming) { 179 | delete this.incomingMedia[senderName]; 180 | } 181 | 182 | logger.info('PARTICIPANT ' + this.userName + ': removing endpoint for ' + 183 | senderName); 184 | // console.log(incoming); 185 | 186 | var self = this; 187 | incoming.release(function (error) { 188 | self.logEndpointRelease(error, senderName); 189 | }); 190 | }; 191 | 192 | /** 193 | * Function does logging if release of one endpoint is successfull or not. 194 | */ 195 | UserSession.prototype.logEndpointRelease = function (error, participantName) { 196 | 197 | if (error !== null) { 198 | if (participantName) { 199 | logger.error('PARTICIPANT ' + this.userName + 200 | ': Could not release incoming EndPoint for ' + participantName + 201 | ':' + error); 202 | } else { 203 | logger.error('PARTICIPANT ' + this.userName + 204 | ': Could not release outgoing EndPoint: ' + error); 205 | } 206 | } else { 207 | if (participantName) { 208 | logger.info('PARTICIPANT ' + this.userName + 209 | ': Released successfully incoming endpoint for ' + participantName); 210 | } else { 211 | logger.error('PARTICIPANT ' + this.userName + 212 | ': Released successfully outgoing EndPoint.'); 213 | } 214 | } 215 | }; 216 | 217 | UserSession.prototype.close = function () { 218 | logger.info('PARTICIPANT ' + this.userName + ': Releasing resources'); 219 | 220 | var self = this; 221 | for (var remoteParticipantName in this.incomingMedia) { 222 | logger.info('PARTICIPANT ' + remoteParticipantName + 223 | ': Released incoming EP for ' + this.userName); 224 | 225 | var ep = this.incomingMedia[remoteParticipantName]; 226 | 227 | ep.release(function (error) { 228 | self.logEndpointRelease(error, remoteParticipantName); 229 | }); 230 | } 231 | 232 | if (this.outgoingMedia !== null) { 233 | this.outgoingMedia.release(function (error) { 234 | self.logEndpointRelease(error); 235 | self.outgoingMedia = null; 236 | }); 237 | } 238 | }; 239 | 240 | UserSession.prototype.sendMessage = function () { 241 | var messageName = 'message'; 242 | var message; 243 | if (arguments.length == 1) { 244 | message = arguments[0]; 245 | } 246 | else 247 | if (arguments.length == 2) { 248 | messageName = arguments[0]; 249 | message = arguments[1]; 250 | } 251 | else { 252 | console.log('ERROR: invalid number of arguments.'); 253 | return; 254 | } 255 | 256 | logger.info('USER ' + this.userName + ': Sending message ' + messageName); 257 | this.sendMessageCallback(messageName, message); 258 | 259 | // TODO errorhandling send failed 260 | // logger.info( 261 | // 'TODO -----------------ROOM {}: The users {} could not be notified that {} left the room' 262 | // ); 263 | 264 | }; 265 | 266 | module.exports = UserSession; 267 | -------------------------------------------------------------------------------- /lib/server.js: -------------------------------------------------------------------------------- 1 | /* 2 | * The MIT License (MIT) 3 | * 4 | * Copyright (c) 2015 MAPT 5 | * 6 | * Permission is hereby granted, free of charge, to any person obtaining a copy 7 | * of this software and associated documentation files (the "Software"), to deal 8 | * in the Software without restriction, including without limitation the rights 9 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | * copies of the Software, and to permit persons to whom the Software is 11 | * furnished to do so, subject to the following conditions: 12 | * 13 | * The above copyright notice and this permission notice shall be included in 14 | * all copies or substantial portions of the Software. 15 | * 16 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | * THE SOFTWARE. 23 | * 24 | */ 25 | 26 | 'use strict'; 27 | 28 | var CallManager = require('./backend/call_manager'); 29 | var UserRegistry = require('./backend/user_registry'); 30 | 31 | var userRegistry = new UserRegistry(); 32 | var callManager = null; 33 | 34 | function leaveRoom(userSession) { 35 | if (userSession === null || userSession === undefined) { 36 | return; 37 | } 38 | console.log('PARTICIPANT ' + userSession.getUserName() + ' leaves call ' + 39 | userSession.getCallId()); 40 | callManager.getCall(userSession.getCallId(), function (error, call) { 41 | if (error) { 42 | console.log('ERROR: ' + error); 43 | return; 44 | } 45 | if (call) { 46 | call.leave(userSession); 47 | if (call.isEmpty()) { 48 | console.log('Call ' + call.getId() + ' is empty.'); 49 | callManager.removeCall(call); 50 | } 51 | } 52 | }); 53 | } 54 | 55 | function onStartNewCall(message, sessionId, sendMessageCallback) { 56 | 57 | console.log(message) 58 | var userName = message.userId; 59 | console.log('PARTICIPANT ', userName, ': trying to join new call, sessionId=', sessionId); 60 | 61 | callManager.getCall('', function (error, call) { 62 | console.log('getCall:', call); 63 | if (call) { 64 | call.join(userName, sendMessageCallback, sessionId, userRegistry); 65 | } 66 | }); 67 | } 68 | 69 | 70 | module.exports = { 71 | 72 | start: function(ws_uri, sessionId, sendMessageCb) { 73 | if (callManager === null) { 74 | callManager = new CallManager(ws_uri); 75 | } 76 | }, 77 | 78 | getCallIds: function() { 79 | return callManager.getCallIds(); 80 | }, 81 | 82 | getUserIds: function() { 83 | return userRegistry.getUserIds(); 84 | }, 85 | 86 | onError: function (error, sessionId) { 87 | console.log('Connection ' + sessionId + ' error:' + error); 88 | var user = userRegistry.getBySessionId(sessionId); 89 | leaveRoom(user); 90 | userRegistry.removeBySession(sessionId); 91 | }, 92 | 93 | onClose: function (sessionId) { 94 | console.log('Connection ' + sessionId + ' closed'); 95 | var user = userRegistry.getBySessionId(sessionId); 96 | leaveRoom(user); 97 | userRegistry.removeBySession(sessionId); 98 | }, 99 | 100 | /* 101 | onInviteUsers: function(data, currentUserId, invitees){ 102 | console.log('inviteUsers: ', data.userIds); 103 | 104 | var currentUserId = socket.client.user._id; 105 | console.log('inviteUsers: currentUserId=', currentUserId, ' socketId=', socket.id); 106 | var length = data.userIds.length; 107 | for (var i = 0; i < length; i++) { 108 | var userId = data.userIds[i]; 109 | var socks = authenticatedSockets[userId]; 110 | if (socks) { 111 | console.log('inviteUsers: send incomingCall message', socks[0].id); 112 | socks[0].emit('incomingCall', { from: currentUserId, callId: data.callId } ); 113 | } else { 114 | console.log('ERROR user not connected: userId=', userId); 115 | } 116 | } 117 | }); 118 | */ 119 | 120 | onIncomingCallAnswer: function(data, sessionId, sendMessageCallback){ 121 | console.log('incomingCallAnswer: ', data.from, data.answer); 122 | if (data.answer === 'accepted') { 123 | var userId = data.from; 124 | var callId = data.callId; 125 | console.log('PARTICIPANT ', userId, ': trying to join call ', callId); 126 | 127 | callManager.getCall(callId, function (error, call) { 128 | console.log('getCall:', call); 129 | if (call) { 130 | call.join(userId, sendMessageCallback, sessionId, userRegistry); 131 | } 132 | }); 133 | } 134 | }, 135 | 136 | onGetCalls: function(sendMesage){ 137 | sendMessage({ 138 | id: 'allCalls', 139 | rooms: callManager.getCallIds() 140 | }); 141 | }, 142 | 143 | onReceiveVideoFrom: function(message, sessionId){ 144 | var user = userRegistry.getBySessionId(sessionId); 145 | 146 | if (user) { 147 | console.log('Incoming message from user ', user.getUserName(), 148 | ': id=', message.id); 149 | } else { 150 | console.log('Incoming message from new user : id=', message.id); 151 | } 152 | 153 | var sender = userRegistry.getByName(message.sender); 154 | user.receiveVideoFrom(sender, message.sdpOffer); 155 | }, 156 | 157 | onMessage: function(message, sessionId, sendMessageCallback) { 158 | console.log('Connection ' + sessionId + ' received message ', message); 159 | 160 | var user = userRegistry.getBySessionId(sessionId); 161 | 162 | if (user) { 163 | console.log('Incoming message from user ', user.getUserName(), 164 | ': id=', message.id); 165 | } else { 166 | console.log('Incoming message from new user : id=', message.id); 167 | } 168 | 169 | switch (message.id) { 170 | 171 | case 'leaveRoom': 172 | leaveRoom(user); 173 | break; 174 | case 'closeRoom': 175 | //TODO only admins should have the right of closing rooms 176 | callManager.removeCall(message.roomId); 177 | break; 178 | 179 | default: 180 | console.log('Invalid message'); 181 | socket.emit({ 182 | id: 'error', 183 | message: 'Invalid message ' + message 184 | }); 185 | break; 186 | } 187 | } 188 | 189 | }; 190 | 191 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "kurento-group-call", 3 | "version": "0.0.3", 4 | "description": "Simple javascript library used to initiate a group call (many to many video and audio call) using Kurento Media Server", 5 | "main": "lib/server.js", 6 | "private": false, 7 | "scripts": { 8 | "test": "mocha -w lib/backend/test" 9 | }, 10 | "repository": { 11 | "type": "git", 12 | "url": "https://github.com/dragosch/kurento-group-call.git" 13 | }, 14 | "licenses": [ 15 | { 16 | "type": "MIT", 17 | "url": "https://github.com/dragosch/kurento-group-call/blob/master/LICENSE" 18 | } 19 | ], 20 | "dependencies": { 21 | "async": "~0.9.0", 22 | "kurento-client": "5.1.0", 23 | "shortid": "^2.2.2" 24 | }, 25 | "devDependencies": { 26 | "bower": "^1.3.12", 27 | "grunt": "^0.4.5", 28 | "grunt-browserify": "~3.7.0", 29 | "grunt-cli": "~0.1.13", 30 | "grunt-contrib-clean": "~0.6.0", 31 | "grunt-contrib-jshint": "^0.11.2", 32 | "grunt-githooks": "^0.3.1", 33 | "grunt-jsbeautifier": "^0.2.10", 34 | "grunt-jscoverage": "^0.1.3", 35 | "grunt-jsdoc": "^0.6.3", 36 | "grunt-npm2bower-sync": "^0.8.1", 37 | "grunt-shell": "^1.1.2", 38 | "qunitjs": "^1.18.0", 39 | "mocha": "^2.2.5", 40 | "should": "^6.0.3" 41 | }, 42 | "bugs": { 43 | "url": "https://github.com/dragosch/kurento-group-call/issues" 44 | }, 45 | "homepage": "https://github.com/dragosch/kurento-group-call", 46 | "directories": { 47 | "example": "example" 48 | }, 49 | "keywords": [ 50 | "Kurento", "WebRTC", "video", "audio" 51 | ], 52 | "author": "Alexander Dragosch ", 53 | "license": "MIT" 54 | } 55 | --------------------------------------------------------------------------------