├── .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 | 
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 |
--------------------------------------------------------------------------------