diff --git a/kurento-one2one-composite-record/package.json b/kurento-one2one-composite-record/package.json new file mode 100644 index 00000000..89e47e4b --- /dev/null +++ b/kurento-one2one-composite-record/package.json @@ -0,0 +1,17 @@ +{ + "name": "kurento-one2one-composite-record", + "version": "6.1.0", + "private": true, + "scripts": { + "postinstall": "cd static && bower install" + }, + "dependencies": { + "express": "~4.12.4", + "minimist": "^1.1.1", + "ws": "~0.7.2", + "kurento-client": "6.1.0" + }, + "devDependencies": { + "bower": "^1.4.1" + } +} diff --git a/kurento-one2one-composite-record/server.js b/kurento-one2one-composite-record/server.js new file mode 100644 index 00000000..9dfa9809 --- /dev/null +++ b/kurento-one2one-composite-record/server.js @@ -0,0 +1,677 @@ +/* + * (C) Copyright 2014 Kurento (http://kurento.org/) + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the GNU Lesser General Public License + * (LGPL) version 2.1 which accompanies this distribution, and is available at + * http://www.gnu.org/licenses/lgpl-2.1.html + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + */ + +var path = require('path'); +var express = require('express'); +var ws = require('ws'); +var minimist = require('minimist'); +var url = require('url'); +var kurento = require('kurento-client'); + +var argv = minimist(process.argv.slice(2), { + default: { + as_uri: "http://localhost:8080/", + ws_uri: "ws://localhost:8888/kurento" + } +}); + +var app = express(); + +/* + * Definition of global variables. + */ + +var kurentoClient = null; +var userRegistry = new UserRegistry(); +var pipelines = {}; +var candidatesQueue = {}; +var idCounter = 0; +var candidates_ready = {}; +var recordsCounter = 0; + +function nextUniqueId() { + idCounter++; + return idCounter.toString(); +} + +/* + * Definition of helper classes + */ + +// Represents caller and callee sessions +function UserSession(id, name, ws) { + this.id = id; + this.name = name; + this.ws = ws; + this.peer = null; + this.sdpOffer = null; +} + +UserSession.prototype.sendMessage = function(message) { + this.ws.send(JSON.stringify(message)); +} + +// Represents registrar of users +function UserRegistry() { + this.usersById = {}; + this.usersByName = {}; +} + +UserRegistry.prototype.register = function(user) { + this.usersById[user.id] = user; + this.usersByName[user.name] = user; +} + +UserRegistry.prototype.unregister = function(id) { + var user = this.getById(id); + if (user) delete this.usersById[id] + if (user && this.getByName(user.name)) delete this.usersByName[user.name]; +} + +UserRegistry.prototype.getById = function(id) { + return this.usersById[id]; +} + +UserRegistry.prototype.getByName = function(name) { + return this.usersByName[name]; +} + +UserRegistry.prototype.removeById = function(id) { + var userSession = this.usersById[id]; + if (!userSession) return; + delete this.usersById[id]; + delete this.usersByName[userSession.name]; +} + +// Represents a B2B active call +function CallMediaPipeline() { + this.pipeline = null; + this.webRtcEndpoint = {}; + this.recorderEndpoint = null; +} + +CallMediaPipeline.prototype.createPipeline = function(callerId, calleeId, ws, callback) { + var self = this; + getKurentoClient(function(error, kurentoClient) { + if (error) { + return callback(error); + } + + kurentoClient.create('MediaPipeline', function(error, pipeline) { + if (error) { + return callback(error); + } + + pipeline.create('WebRtcEndpoint', function(error, callerWebRtcEndpoint) { + if (error) { + pipeline.release(); + return callback(error); + } + + if (candidatesQueue[callerId]) { + while(candidatesQueue[callerId].length) { + var candidate = candidatesQueue[callerId].shift(); + callerWebRtcEndpoint.addIceCandidate(candidate); + } + } + + callerWebRtcEndpoint.on('OnIceCandidate', function(event) { + var candidate = kurento.register.complexTypes.IceCandidate(event.candidate); + userRegistry.getById(callerId).ws.send(JSON.stringify({ + id : 'iceCandidate', + candidate : candidate + })); + }); + + pipeline.create('WebRtcEndpoint', function(error, calleeWebRtcEndpoint) { + if (error) { + pipeline.release(); + return callback(error); + } + + if (candidatesQueue[calleeId]) { + while(candidatesQueue[calleeId].length) { + var candidate = candidatesQueue[calleeId].shift(); + calleeWebRtcEndpoint.addIceCandidate(candidate); + } + } + + calleeWebRtcEndpoint.on('OnIceCandidate', function(event) { + var candidate = kurento.register.complexTypes.IceCandidate(event.candidate); + userRegistry.getById(calleeId).ws.send(JSON.stringify({ + id : 'iceCandidate', + candidate : candidate + })); + }); + + pipeline.create('Composite', function(error, _composite) { + if (error) { + pipeline.release(); + return callback(error); + } + + _composite.createHubPort(function(error, _callerHubport) { + if (error) { + pipeline.release(); + return callback(error); + } + + _composite.createHubPort(function(error, _calleeHubport) { + if (error) { + pipeline.release(); + return callback(error); + } + + _composite.createHubPort(function(error, _recorderHubport) { + if (error) { + pipeline.release(); + return callback(error); + } + + recordsCounter++; + + var recorderParams = { + mediaProfile: 'MP4', + uri : "file:///tmp/kurento-one2one-composite-" + recordsCounter + ".mp4" + }; + + pipeline.create('RecorderEndpoint', recorderParams, function(error, recorderEndpoint) { + if (error) { + pipeline.release(); + return callback(error); + } + + self.recorderEndpoint = recorderEndpoint; + + _recorderHubport.connect(recorderEndpoint, function(error) { + if (error) { + pipeline.release(); + return callback(error); + } + + callerWebRtcEndpoint.connect(_callerHubport, function(error) { + if (error) { + pipeline.release(); + return callback(error); + } + calleeWebRtcEndpoint.connect(_calleeHubport, function(error) { + if (error) { + pipeline.release(); + return callback(error); + } + + console.log('Hubports are created'); + + _callerHubport.connect(callerWebRtcEndpoint, function(error) { + if (error) { + pipeline.release(); + return callback(error); + } + _calleeHubport.connect(calleeWebRtcEndpoint, function(error) { + if (error) { + pipeline.release(); + return callback(error); + } + + callerWebRtcEndpoint.on('OnIceGatheringDone', function(error) { + candidates_ready[callerId] = true; + if (candidates_ready[calleeId]) { + recorderEndpoint.record(); + } + }); + calleeWebRtcEndpoint.on('OnIceGatheringDone', function(error) { + candidates_ready[calleeId] = true; + if (candidates_ready[callerId]) { + recorderEndpoint.record(); + } + }); + + self.pipeline = pipeline; + self.webRtcEndpoint[callerId] = callerWebRtcEndpoint; + self.webRtcEndpoint[calleeId] = calleeWebRtcEndpoint; + callback(null); + }); + }); + }); + }); + }); + }); + }); + }); + }); + }); + }); + }); + }); + }); +}; + +CallMediaPipeline.prototype.generateSdpAnswer = function(id, sdpOffer, callback) { + this.webRtcEndpoint[id].processOffer(sdpOffer, callback); + this.webRtcEndpoint[id].gatherCandidates(function(error) { + if (error) { + return callback(error); + } + }); +}; + +CallMediaPipeline.prototype.release = function() { + if (this.pipeline) this.pipeline.release(); + this.pipeline = null; +}; + + +// Represents player pipeline +function PlayMediaPipeline() { + this.pipeline = null; + this.webRtcEndpoint = {}; + this.playerEndpoint = null; +} + +PlayMediaPipeline.prototype.createPipeline = function(callerId, ws, callback) { + var self = this; + getKurentoClient(function(error, kurentoClient) { + if (error) { + return callback(error); + } + + kurentoClient.create('MediaPipeline', function(error, pipeline) { + if (error) { + return callback(error); + } + + pipeline.create('WebRtcEndpoint', function(error, callerWebRtcEndpoint) { + if (error) { + pipeline.release(); + return callback(error); + } + + if (candidatesQueue[callerId]) { + while(candidatesQueue[callerId].length) { + var candidate = candidatesQueue[callerId].shift(); + callerWebRtcEndpoint.addIceCandidate(candidate); + } + } + + callerWebRtcEndpoint.on('OnIceCandidate', function(event) { + var candidate = kurento.register.complexTypes.IceCandidate(event.candidate); + userRegistry.getById(callerId).ws.send(JSON.stringify({ + id : 'iceCandidate', + candidate : candidate + })); + }); + + var options = { + uri : "file:///tmp/kurento-one2one-composite-" + recordsCounter + ".mp4" + }; + + pipeline.create("PlayerEndpoint", options, function(error, player) { + if (error) return onError(error); + + callerWebRtcEndpoint.on('OnIceGatheringDone', function(error) { + player.play(); + }); + + player.on('EndOfStream', function(event){ + userRegistry.getById(callerId).sendMessage({id : 'stopPlay'}); + stop(callerId); + }); + + player.connect(callerWebRtcEndpoint, function(error) { + if (error) return onError(error); + + self.playerEndpoint = player; + self.pipeline = pipeline; + self.webRtcEndpoint[callerId] = callerWebRtcEndpoint; + callback(null); + }); + }); + }); + }); + }); +}; + +PlayMediaPipeline.prototype.generateSdpAnswer = function(id, sdpOffer, callback) { + this.webRtcEndpoint[id].processOffer(sdpOffer, callback); + this.webRtcEndpoint[id].gatherCandidates(function(error) { + if (error) { + return callback(error); + } + }); +}; + +PlayMediaPipeline.prototype.release = function() { + if (this.pipeline) this.pipeline.release(); + this.pipeline = null; +}; + + + +/* + * Server startup + */ + +var asUrl = url.parse(argv.as_uri); +var port = asUrl.port; +var server = app.listen(port, function() { + console.log('Kurento Tutorial started'); + console.log('Open ' + url.format(asUrl) + ' with a WebRTC capable browser'); +}); + +var wss = new ws.Server({ + server : server, + path : '/one2onecomposrec' +}); + +wss.on('connection', function(ws) { + var sessionId = nextUniqueId(); + console.log('Connection received with sessionId ' + sessionId); + + ws.on('error', function(error) { + console.log('Connection ' + sessionId + ' error'); + stop(sessionId); + }); + + ws.on('close', function() { + console.log('Connection ' + sessionId + ' closed'); + stop(sessionId); + userRegistry.unregister(sessionId); + }); + + ws.on('message', function(_message) { + var message = JSON.parse(_message); + if (message.id !== 'onIceCandidate') + console.log('Connection ' + sessionId + ' received message ', message); + + switch (message.id) { + case 'register': + register(sessionId, message.name, ws); + break; + + case 'call': + call(sessionId, message.to, message.from, message.sdpOffer); + break; + + case 'incomingCallResponse': + incomingCallResponse(sessionId, message.from, message.callResponse, message.sdpOffer, ws); + break; + + case 'play': + play(sessionId, message.sdpOffer); + break; + + case 'stop': + stop(sessionId); + break; + + case 'onIceCandidate': + onIceCandidate(sessionId, message.candidate); + break; + + default: + ws.send(JSON.stringify({ + id : 'error', + message : 'Invalid message ' + message + })); + break; + } + + }); +}); + +// Recover kurentoClient for the first time. +function getKurentoClient(callback) { + if (kurentoClient !== null) { + return callback(null, kurentoClient); + } + + kurento(argv.ws_uri, function(error, _kurentoClient) { + if (error) { + var message = 'Coult not find media server at address ' + argv.ws_uri; + return callback(message + ". Exiting with error " + error); + } + + kurentoClient = _kurentoClient; + callback(null, kurentoClient); + }); +} + +function stop(sessionId) { + if (!pipelines[sessionId]) { + return; + } + + var pipeline = pipelines[sessionId]; + delete pipelines[sessionId]; + + if (pipeline.recorderEndpoint) + pipeline.recorderEndpoint.stop(); + + if (pipeline.playerEndpoint) + pipeline.playerEndpoint.stop(); + + pipeline.release(); + var stopperUser = userRegistry.getById(sessionId); + var stoppedUser = userRegistry.getByName(stopperUser.peer); + stopperUser.peer = null; + + if (stoppedUser) { + stoppedUser.peer = null; + delete pipelines[stoppedUser.id]; + var message = { + id: 'stopCommunication', + message: 'remote user hanged out' + } + stoppedUser.sendMessage(message) + } + + clearCandidatesQueue(sessionId); +} + +function incomingCallResponse(calleeId, from, callResponse, calleeSdp, ws) { + + clearCandidatesQueue(calleeId); + + function onError(callerReason, calleeReason) { + if (pipeline) pipeline.release(); + if (caller) { + var callerMessage = { + id: 'callResponse', + response: 'rejected' + } + if (callerReason) callerMessage.message = callerReason; + caller.sendMessage(callerMessage); + } + + var calleeMessage = { + id: 'stopCommunication' + }; + if (calleeReason) calleeMessage.message = calleeReason; + callee.sendMessage(calleeMessage); + } + + var callee = userRegistry.getById(calleeId); + if (!from || !userRegistry.getByName(from)) { + return onError(null, 'unknown from = ' + from); + } + var caller = userRegistry.getByName(from); + + if (callResponse === 'accept') { + var pipeline = new CallMediaPipeline(); + pipelines[caller.id] = pipeline; + pipelines[callee.id] = pipeline; + + pipeline.createPipeline(caller.id, callee.id, ws, function(error) { + if (error) { + return onError(error, error); + } + + pipeline.generateSdpAnswer(caller.id, caller.sdpOffer, function(error, callerSdpAnswer) { + if (error) { + return onError(error, error); + } + + pipeline.generateSdpAnswer(callee.id, calleeSdp, function(error, calleeSdpAnswer) { + if (error) { + return onError(error, error); + } + + var message = { + id: 'startCommunication', + sdpAnswer: calleeSdpAnswer + }; + callee.sendMessage(message); + + message = { + id: 'callResponse', + response : 'accepted', + sdpAnswer: callerSdpAnswer + }; + caller.sendMessage(message); + }); + }); + }); + } else { + var decline = { + id: 'callResponse', + response: 'rejected', + message: 'user declined' + }; + caller.sendMessage(decline); + } +} + +function call(callerId, to, from, sdpOffer) { + clearCandidatesQueue(callerId); + + var caller = userRegistry.getById(callerId); + var rejectCause = 'User ' + to + ' is not registered'; + if (userRegistry.getByName(to)) { + var callee = userRegistry.getByName(to); + caller.sdpOffer = sdpOffer + callee.peer = from; + caller.peer = to; + var message = { + id: 'incomingCall', + from: from + }; + try{ + return callee.sendMessage(message); + } catch(exception) { + rejectCause = "Error " + exception; + } + } + var message = { + id: 'callResponse', + response: 'rejected: ', + message: rejectCause + }; + caller.sendMessage(message); +} + +function play(callerId, sdpOffer) { + clearCandidatesQueue(callerId); + + var caller = userRegistry.getById(callerId); + + caller.sdpOffer = sdpOffer; + + clearCandidatesQueue(callerId); + + function onError(callerReason) { + if (caller) { + var callerMessage = { + id: 'playResponse', + response: 'rejected' + }; + if (callerReason) callerMessage.message = callerReason; + caller.sendMessage(callerMessage); + } + } + + if (!recordsCounter) { + return onError('There are no record.'); + } + + var pipeline = new PlayMediaPipeline(); + + pipelines[caller.id] = pipeline; + + pipeline.createPipeline(caller.id, ws, function(error) { + if (error) { + return onError(error); + } + console.log('Pipeline is created.'); + + pipeline.generateSdpAnswer(caller.id, sdpOffer, function(error, callerSdpAnswer) { + if (error) { + return onError(error); + } + + var message = { + id: 'playResponse', + response : 'accepted', + sdpAnswer: callerSdpAnswer + }; + caller.sendMessage(message); + }); + }); +} + + +function register(id, name, ws, callback) { + function onError(error) { + ws.send(JSON.stringify({id:'registerResponse', response : 'rejected ', message: error})); + } + + if (!name) { + return onError("empty user name"); + } + + if (userRegistry.getByName(name)) { + return onError("User " + name + " is already registered"); + } + + userRegistry.register(new UserSession(id, name, ws)); + try { + ws.send(JSON.stringify({id: 'registerResponse', response: 'accepted'})); + } catch(exception) { + onError(exception); + } +} + +function clearCandidatesQueue(sessionId) { + if (candidatesQueue[sessionId]) { + delete candidatesQueue[sessionId]; + } +} + +function onIceCandidate(sessionId, _candidate) { + var candidate = kurento.register.complexTypes.IceCandidate(_candidate); + var user = userRegistry.getById(sessionId); + + if (pipelines[user.id] && pipelines[user.id].webRtcEndpoint && pipelines[user.id].webRtcEndpoint[user.id]) { + var webRtcEndpoint = pipelines[user.id].webRtcEndpoint[user.id]; + webRtcEndpoint.addIceCandidate(candidate); + } + else { + if (!candidatesQueue[user.id]) { + candidatesQueue[user.id] = []; + } + candidatesQueue[sessionId].push(candidate); + } +} + +app.use(express.static(path.join(__dirname, 'static'))); diff --git a/kurento-one2one-composite-record/static/bower.json b/kurento-one2one-composite-record/static/bower.json new file mode 100644 index 00000000..d35e5c21 --- /dev/null +++ b/kurento-one2one-composite-record/static/bower.json @@ -0,0 +1,29 @@ +{ + "name": "kurento-one2one-call", + "description": "Kurento Browser JavaScript Tutorial", + "authors": [ + "Kurento " + ], + "main": "index.html", + "moduleType": [ + "globals" + ], + "license": "LGPL", + "homepage": "http://www.kurento.org/", + "private": true, + "ignore": [ + "**/.*", + "node_modules", + "bower_components", + "test", + "tests" + ], + "dependencies": { + "adapter.js": "*", + "bootstrap": "~3.3.0", + "draggabilly": "~1.2.4", + "demo-console": "1.5.1", + "ekko-lightbox": "~3.3.0", + "kurento-utils": "6.1.0" + } +} diff --git a/kurento-one2one-composite-record/static/css/kurento.css b/kurento-one2one-composite-record/static/css/kurento.css new file mode 100644 index 00000000..1aaf3422 --- /dev/null +++ b/kurento-one2one-composite-record/static/css/kurento.css @@ -0,0 +1,77 @@ +/* + * (C) Copyright 2014-2015 Kurento (http://kurento.org/) + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the GNU Lesser General Public License + * (LGPL) version 2.1 which accompanies this distribution, and is available at + * http://www.gnu.org/licenses/lgpl-2.1.html + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + */ +@CHARSET "UTF-8"; + +html { + position: relative; + min-height: 100%; +} + +body { + padding-top: 40px; + body +} + +video,#console { + display: block; + font-size: 14px; + line-height: 1.42857143; + color: #555; + background-color: #fff; + background-image: none; + border: 1px solid #ccc; + border-radius: 4px; + -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, .075); + box-shadow: inset 0 1px 1px rgba(0, 0, 0, .075); + -webkit-transition: border-color ease-in-out .15s, box-shadow + ease-in-out .15s; + transition: border-color ease-in-out .15s, box-shadow ease-in-out .15s; +} + +#console { + min-height: 120px; + max-height: 360px; +} + +#videoContainer { + position: absolute; + float: left; +} + +#videoBig { + width: 640px; + height: 480px; + top: 0; + left: 0; + z-index: 1; +} + +div#videoSmall { + width: 240px; + height: 180px; + padding: 0px; + position: absolute; + top: 15px; + left: 400px; + cursor: pointer; + z-index: 10; + padding: 0px; +} + +div.dragged { + cursor: all-scroll !important; + border-color: blue !important; + z-index: 10 !important; +} diff --git a/kurento-one2one-composite-record/static/img/kurento.png b/kurento-one2one-composite-record/static/img/kurento.png new file mode 100644 index 00000000..6f1a4ad3 Binary files /dev/null and b/kurento-one2one-composite-record/static/img/kurento.png differ diff --git a/kurento-one2one-composite-record/static/img/naevatec.png b/kurento-one2one-composite-record/static/img/naevatec.png new file mode 100644 index 00000000..05ee7041 Binary files /dev/null and b/kurento-one2one-composite-record/static/img/naevatec.png differ diff --git a/kurento-one2one-composite-record/static/img/pipeline.png b/kurento-one2one-composite-record/static/img/pipeline.png new file mode 100644 index 00000000..25e02ccc Binary files /dev/null and b/kurento-one2one-composite-record/static/img/pipeline.png differ diff --git a/kurento-one2one-composite-record/static/img/spinner.gif b/kurento-one2one-composite-record/static/img/spinner.gif new file mode 100644 index 00000000..8be8ba33 Binary files /dev/null and b/kurento-one2one-composite-record/static/img/spinner.gif differ diff --git a/kurento-one2one-composite-record/static/img/transparent-1px.png b/kurento-one2one-composite-record/static/img/transparent-1px.png new file mode 100644 index 00000000..9da19eac Binary files /dev/null and b/kurento-one2one-composite-record/static/img/transparent-1px.png differ diff --git a/kurento-one2one-composite-record/static/img/urjc.gif b/kurento-one2one-composite-record/static/img/urjc.gif new file mode 100644 index 00000000..cd8a7703 Binary files /dev/null and b/kurento-one2one-composite-record/static/img/urjc.gif differ diff --git a/kurento-one2one-composite-record/static/img/webrtc.png b/kurento-one2one-composite-record/static/img/webrtc.png new file mode 100644 index 00000000..d47e2e4c Binary files /dev/null and b/kurento-one2one-composite-record/static/img/webrtc.png differ diff --git a/kurento-one2one-composite-record/static/index.html b/kurento-one2one-composite-record/static/index.html new file mode 100644 index 00000000..01503b34 --- /dev/null +++ b/kurento-one2one-composite-record/static/index.html @@ -0,0 +1,146 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + Kurento Tutorial 4: Video Call 1 to 1 with WebRTC + + + +
+ +
+ +
+ +
+
+ +
+
+ +
+ +
+
+
+ +
+
+ +
+ +
+
+

+
+
    +
    +
    +
    +
    + +
    +
    + +
    +
    + + Play +
    +
    +
    + + + + + diff --git a/kurento-one2one-composite-record/static/js/index.js b/kurento-one2one-composite-record/static/js/index.js new file mode 100644 index 00000000..a70d6999 --- /dev/null +++ b/kurento-one2one-composite-record/static/js/index.js @@ -0,0 +1,411 @@ +/* + * (C) Copyright 2014-2015 Kurento (http://kurento.org/) + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the GNU Lesser General Public License + * (LGPL) version 2.1 which accompanies this distribution, and is available at + * http://www.gnu.org/licenses/lgpl-2.1.html + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + */ + +var ws = new WebSocket('ws://' + location.host + '/one2onecomposrec'); +var videoInput; +var videoOutput; +var webRtcPeer; + +var registerName = null; +const NOT_REGISTERED = 0; +const REGISTERING = 1; +const REGISTERED = 2; +var registerState = null; + +function setRegisterState(nextState) { + switch (nextState) { + case NOT_REGISTERED: + $('#register').attr('disabled', false); + $('#call').attr('disabled', true); + $('#terminate').attr('disabled', true); + $('#play').attr('disabled', true); + break; + + case REGISTERING: + $('#register').attr('disabled', true); + $('#play').attr('disabled', false); + break; + + case REGISTERED: + $('#play').attr('disabled', false); + $('#register').attr('disabled', true); + setCallState(NO_CALL); + break; + + default: + return; + } + registerState = nextState; +} + +const NO_CALL = 0; +const PROCESSING_CALL = 1; +const IN_CALL = 2; +const PLAYING = 4; +var callState = null + +function setCallState(nextState) { + switch (nextState) { + case NO_CALL: + $('#call').attr('disabled', false); + $('#terminate').attr('disabled', true); + $('#play').attr('disabled', false); + $('#videoInput').show(); + break; + + case PROCESSING_CALL: + $('#call').attr('disabled', true); + $('#terminate').attr('disabled', true); + $('#play').attr('disabled', true); + $('#videoInput').show(); + break; + + case IN_CALL: + $('#call').attr('disabled', true); + $('#terminate').attr('disabled', false); + $('#play').attr('disabled', true); + $('#videoInput').show(); + break; + + case PLAYING: + $('#call').attr('disabled', true); + $('#terminate').attr('disabled', false); + $('#play').attr('disabled', true); + $('#play').attr('disabled', true); + $('#videoInput').hide(); + + break; + default: + return; + } + callState = nextState; +} + +window.onload = function() { + console = new Console(); + setRegisterState(NOT_REGISTERED); + var drag = new Draggabilly(document.getElementById('videoSmall')); + videoInput = document.getElementById('videoInput'); + videoOutput = document.getElementById('videoOutput'); + document.getElementById('name').focus(); + + document.getElementById('register').addEventListener('click', function() { + register(); + }); + document.getElementById('call').addEventListener('click', function() { + call(); + }); + document.getElementById('terminate').addEventListener('click', function() { + stop(); + }); + document.getElementById('play').addEventListener('click', function() { + play(); + }); +} + +window.onbeforeunload = function() { + ws.close(); +} + +ws.onmessage = function(message) { + var parsedMessage = JSON.parse(message.data); + console.info('Received message: ' + message.data); + + switch (parsedMessage.id) { + case 'registerResponse': + resgisterResponse(parsedMessage); + break; + case 'callResponse': + callResponse(parsedMessage); + break; + case 'incomingCall': + incomingCall(parsedMessage); + break; + case 'playResponse': + playResponse(parsedMessage); + break; + case 'startPlay': + startPlay(parsedMessage); + break; + case 'stopPlay': + stop(true); + break; + case 'startCommunication': + startCommunication(parsedMessage); + break; + case 'stopCommunication': + console.info("Communication ended by remote peer"); + stop(true); + break; + case 'iceCandidate': + webRtcPeer.addIceCandidate(parsedMessage.candidate) + break; + default: + console.error('Unrecognized message', parsedMessage); + } +} + +function resgisterResponse(message) { + if (message.response == 'accepted') { + setRegisterState(REGISTERED); + } else { + setRegisterState(NOT_REGISTERED); + var errorMessage = message.message ? message.message + : 'Unknown reason for register rejection.'; + console.log(errorMessage); + alert('Error registering user. See console for further information.'); + } +} + +function callResponse(message) { + if (message.response != 'accepted') { + console.info('Call not accepted by peer. Closing call'); + var errorMessage = message.message ? message.message + : 'Unknown reason for call rejection.'; + console.log(errorMessage); + stop(true); + } else { + setCallState(IN_CALL); + webRtcPeer.processAnswer(message.sdpAnswer); + } +} + +function playResponse(message) { + if (message.response != 'accepted') { + console.info('Playing not accepted by peer. Closing player'); + var errorMessage = message.message ? message.message + : 'Unknown reason for play rejection.'; + console.log(errorMessage); + stop(true); + } else { + setCallState(PLAYING); + webRtcPeer.processAnswer(message.sdpAnswer); + } +} + +function startCommunication(message) { + setCallState(IN_CALL); + webRtcPeer.processAnswer(message.sdpAnswer); +} + +function startPlay(message) { + setCallState(IN_CALL); + webRtcPeer.processAnswer(message.sdpAnswer); +} + +function incomingCall(message) { + // If bussy just reject without disturbing user + if (callState != NO_CALL) { + var response = { + id : 'incomingCallResponse', + from : message.from, + callResponse : 'reject', + message : 'bussy' + + }; + return sendMessage(response); + } + + setCallState(PROCESSING_CALL); + if (confirm('User ' + message.from + + ' is calling you. Do you accept the call?')) { + showSpinner(videoInput, videoOutput); + + var options = { + localVideo : videoInput, + remoteVideo : videoOutput, + onicecandidate : onIceCandidate + } + + webRtcPeer = kurentoUtils.WebRtcPeer.WebRtcPeerSendrecv(options, + function(error) { + if (error) { + console.error(error); + setCallState(NO_CALL); + } + + this.generateOffer(function(error, offerSdp) { + if (error) { + console.error(error); + setCallState(NO_CALL); + } + var response = { + id : 'incomingCallResponse', + from : message.from, + callResponse : 'accept', + sdpOffer : offerSdp + }; + sendMessage(response); + }); + }); + + } else { + var response = { + id : 'incomingCallResponse', + from : message.from, + callResponse : 'reject', + message : 'user declined' + }; + sendMessage(response); + stop(true); + } +} + +function register() { + var name = document.getElementById('name').value; + if (name == '') { + window.alert("You must insert your user name"); + return; + } + + setRegisterState(REGISTERING); + + var message = { + id : 'register', + name : name + }; + sendMessage(message); + document.getElementById('peer').focus(); +} + +function call() { + if (document.getElementById('peer').value == '') { + window.alert("You must specify the peer name"); + return; + } + + setCallState(PROCESSING_CALL); + + showSpinner(videoInput, videoOutput); + + var options = { + localVideo : videoInput, + remoteVideo : videoOutput, + onicecandidate : onIceCandidate + } + + webRtcPeer = kurentoUtils.WebRtcPeer.WebRtcPeerSendrecv(options, function( + error) { + if (error) { + console.error(error); + setCallState(NO_CALL); + } + + this.generateOffer(function(error, offerSdp) { + if (error) { + console.error(error); + setCallState(NO_CALL); + } + var message = { + id : 'call', + from : document.getElementById('name').value, + to : document.getElementById('peer').value, + sdpOffer : offerSdp + }; + sendMessage(message); + }); + }); + +} + +function play() { + + setCallState(PLAYING); + + showSpinner(videoInput, videoOutput); + + var options = { + localVideo : videoInput, + remoteVideo : videoOutput, + onicecandidate : onIceCandidate + } + + webRtcPeer = kurentoUtils.WebRtcPeer.WebRtcPeerRecvonly(options, function( + error) { + if (error) { + console.error(error); + setCallState(NO_CALL); + } + + this.generateOffer(function(error, offerSdp) { + if (error) { + console.error(error); + setCallState(NO_CALL); + } + var message = { + id : 'play', + sdpOffer : offerSdp + }; + sendMessage(message); + }); + }); + +} + + +function stop(message) { + setCallState(NO_CALL); + if (webRtcPeer) { + webRtcPeer.dispose(); + webRtcPeer = null; + + if (!message) { + var message = { + id : 'stop' + } + sendMessage(message); + } + } + hideSpinner(videoInput, videoOutput); +} + +function sendMessage(message) { + var jsonMessage = JSON.stringify(message); + console.log('Senging message: ' + jsonMessage); + ws.send(jsonMessage); +} + +function onIceCandidate(candidate) { + console.log('Local candidate' + JSON.stringify(candidate)); + + var message = { + id : 'onIceCandidate', + candidate : candidate + } + sendMessage(message); +} + +function showSpinner() { + for (var i = 0; i < arguments.length; i++) { + arguments[i].poster = './img/transparent-1px.png'; + arguments[i].style.background = 'center transparent url("./img/spinner.gif") no-repeat'; + } +} + +function hideSpinner() { + for (var i = 0; i < arguments.length; i++) { + arguments[i].src = ''; + arguments[i].poster = './img/webrtc.png'; + arguments[i].style.background = ''; + } +} + +/** + * Lightbox utility (to display media pipeline image in a modal dialog) + */ +$(document).delegate('*[data-toggle="lightbox"]', 'click', function(event) { + event.preventDefault(); + $(this).ekkoLightbox(); +}); diff --git a/kurento-one2one-composite/package.json b/kurento-one2one-composite/package.json new file mode 100644 index 00000000..4a8f7b4b --- /dev/null +++ b/kurento-one2one-composite/package.json @@ -0,0 +1,17 @@ +{ + "name": "kurento-one2one-composite", + "version": "6.1.0", + "private": true, + "scripts": { + "postinstall": "cd static && bower install" + }, + "dependencies": { + "express": "~4.12.4", + "minimist": "^1.1.1", + "ws": "~0.7.2", + "kurento-client": "6.1.0" + }, + "devDependencies": { + "bower": "^1.4.1" + } +} diff --git a/kurento-one2one-composite/server.js b/kurento-one2one-composite/server.js new file mode 100644 index 00000000..9a0737b0 --- /dev/null +++ b/kurento-one2one-composite/server.js @@ -0,0 +1,482 @@ +/* + * (C) Copyright 2014 Kurento (http://kurento.org/) + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the GNU Lesser General Public License + * (LGPL) version 2.1 which accompanies this distribution, and is available at + * http://www.gnu.org/licenses/lgpl-2.1.html + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + */ + +var path = require('path'); +var express = require('express'); +var ws = require('ws'); +var minimist = require('minimist'); +var url = require('url'); +var kurento = require('kurento-client'); + +var argv = minimist(process.argv.slice(2), { + default: { + as_uri: "http://localhost:8080/", + ws_uri: "ws://localhost:8888/kurento" + } +}); + +var app = express(); + +/* + * Definition of global variables. + */ + +var kurentoClient = null; +var userRegistry = new UserRegistry(); +var pipelines = {}; +var candidatesQueue = {}; +var idCounter = 0; + +function nextUniqueId() { + idCounter++; + return idCounter.toString(); +} + +/* + * Definition of helper classes + */ + +// Represents caller and callee sessions +function UserSession(id, name, ws) { + this.id = id; + this.name = name; + this.ws = ws; + this.peer = null; + this.sdpOffer = null; +} + +UserSession.prototype.sendMessage = function(message) { + this.ws.send(JSON.stringify(message)); +} + +// Represents registrar of users +function UserRegistry() { + this.usersById = {}; + this.usersByName = {}; +} + +UserRegistry.prototype.register = function(user) { + this.usersById[user.id] = user; + this.usersByName[user.name] = user; +} + +UserRegistry.prototype.unregister = function(id) { + var user = this.getById(id); + if (user) delete this.usersById[id] + if (user && this.getByName(user.name)) delete this.usersByName[user.name]; +} + +UserRegistry.prototype.getById = function(id) { + return this.usersById[id]; +} + +UserRegistry.prototype.getByName = function(name) { + return this.usersByName[name]; +} + +UserRegistry.prototype.removeById = function(id) { + var userSession = this.usersById[id]; + if (!userSession) return; + delete this.usersById[id]; + delete this.usersByName[userSession.name]; +} + +// Represents a B2B active call +function CallMediaPipeline() { + this.pipeline = null; + this.webRtcEndpoint = {}; +} + +CallMediaPipeline.prototype.createPipeline = function(callerId, calleeId, ws, callback) { + var self = this; + getKurentoClient(function(error, kurentoClient) { + if (error) { + return callback(error); + } + + kurentoClient.create('MediaPipeline', function(error, pipeline) { + if (error) { + return callback(error); + } + + pipeline.create('WebRtcEndpoint', function(error, callerWebRtcEndpoint) { + if (error) { + pipeline.release(); + return callback(error); + } + + if (candidatesQueue[callerId]) { + while(candidatesQueue[callerId].length) { + var candidate = candidatesQueue[callerId].shift(); + callerWebRtcEndpoint.addIceCandidate(candidate); + } + } + + callerWebRtcEndpoint.on('OnIceCandidate', function(event) { + var candidate = kurento.register.complexTypes.IceCandidate(event.candidate); + userRegistry.getById(callerId).ws.send(JSON.stringify({ + id : 'iceCandidate', + candidate : candidate + })); + }); + + pipeline.create('WebRtcEndpoint', function(error, calleeWebRtcEndpoint) { + if (error) { + pipeline.release(); + return callback(error); + } + + if (candidatesQueue[calleeId]) { + while(candidatesQueue[calleeId].length) { + var candidate = candidatesQueue[calleeId].shift(); + calleeWebRtcEndpoint.addIceCandidate(candidate); + } + } + + calleeWebRtcEndpoint.on('OnIceCandidate', function(event) { + var candidate = kurento.register.complexTypes.IceCandidate(event.candidate); + userRegistry.getById(calleeId).ws.send(JSON.stringify({ + id : 'iceCandidate', + candidate : candidate + })); + }); + + pipeline.create('Composite', function(error, _composite) { + if (error) { + pipeline.release(); + return callback(error); + } + + _composite.createHubPort(function(error, _callerHubport) { + if (error) { + pipeline.release(); + return callback(error); + } + + _composite.createHubPort(function(error, _calleeHubport) { + if (error) { + pipeline.release(); + return callback(error); + } + + callerWebRtcEndpoint.connect(_callerHubport, function(error) { + if (error) { + pipeline.release(); + return callback(error); + } + calleeWebRtcEndpoint.connect(_calleeHubport, function(error) { + if (error) { + pipeline.release(); + return callback(error); + } + + console.log('Hubports are created'); + + _callerHubport.connect(callerWebRtcEndpoint, function(error) { + if (error) { + pipeline.release(); + return callback(error); + } + _calleeHubport.connect(calleeWebRtcEndpoint, function(error) { + if (error) { + pipeline.release(); + return callback(error); + } + self.pipeline = pipeline; + self.webRtcEndpoint[callerId] = callerWebRtcEndpoint; + self.webRtcEndpoint[calleeId] = calleeWebRtcEndpoint; + callback(null); + }); + }); + }); + }); + }); + }); + }); + }); + }); + }); + }); +}; + +CallMediaPipeline.prototype.generateSdpAnswer = function(id, sdpOffer, callback) { + this.webRtcEndpoint[id].processOffer(sdpOffer, callback); + this.webRtcEndpoint[id].gatherCandidates(function(error) { + if (error) { + return callback(error); + } + }); +} + +CallMediaPipeline.prototype.release = function() { + if (this.pipeline) this.pipeline.release(); + this.pipeline = null; +} + +/* + * Server startup + */ + +var asUrl = url.parse(argv.as_uri); +var port = asUrl.port; +var server = app.listen(port, function() { + console.log('Kurento Tutorial started'); + console.log('Open ' + url.format(asUrl) + ' with a WebRTC capable browser'); +}); + +var wss = new ws.Server({ + server : server, + path : '/one2onecomposite' +}); + +wss.on('connection', function(ws) { + var sessionId = nextUniqueId(); + console.log('Connection received with sessionId ' + sessionId); + + ws.on('error', function(error) { + console.log('Connection ' + sessionId + ' error'); + stop(sessionId); + }); + + ws.on('close', function() { + console.log('Connection ' + sessionId + ' closed'); + stop(sessionId); + userRegistry.unregister(sessionId); + }); + + ws.on('message', function(_message) { + var message = JSON.parse(_message); + if (message.id !== 'onIceCandidate') + console.log('Connection ' + sessionId + ' received message ', message); + + switch (message.id) { + case 'register': + register(sessionId, message.name, ws); + break; + + case 'call': + call(sessionId, message.to, message.from, message.sdpOffer); + break; + + case 'incomingCallResponse': + incomingCallResponse(sessionId, message.from, message.callResponse, message.sdpOffer, ws); + break; + + case 'stop': + stop(sessionId); + break; + + case 'onIceCandidate': + onIceCandidate(sessionId, message.candidate); + break; + + default: + ws.send(JSON.stringify({ + id : 'error', + message : 'Invalid message ' + message + })); + break; + } + + }); +}); + +// Recover kurentoClient for the first time. +function getKurentoClient(callback) { + if (kurentoClient !== null) { + return callback(null, kurentoClient); + } + + kurento(argv.ws_uri, function(error, _kurentoClient) { + if (error) { + var message = 'Coult not find media server at address ' + argv.ws_uri; + return callback(message + ". Exiting with error " + error); + } + + kurentoClient = _kurentoClient; + callback(null, kurentoClient); + }); +} + +function stop(sessionId) { + if (!pipelines[sessionId]) { + return; + } + + var pipeline = pipelines[sessionId]; + delete pipelines[sessionId]; + pipeline.release(); + var stopperUser = userRegistry.getById(sessionId); + var stoppedUser = userRegistry.getByName(stopperUser.peer); + stopperUser.peer = null; + + if (stoppedUser) { + stoppedUser.peer = null; + delete pipelines[stoppedUser.id]; + var message = { + id: 'stopCommunication', + message: 'remote user hanged out' + } + stoppedUser.sendMessage(message) + } + + clearCandidatesQueue(sessionId); +} + +function incomingCallResponse(calleeId, from, callResponse, calleeSdp, ws) { + + clearCandidatesQueue(calleeId); + + function onError(callerReason, calleeReason) { + if (pipeline) pipeline.release(); + if (caller) { + var callerMessage = { + id: 'callResponse', + response: 'rejected' + } + if (callerReason) callerMessage.message = callerReason; + caller.sendMessage(callerMessage); + } + + var calleeMessage = { + id: 'stopCommunication' + }; + if (calleeReason) calleeMessage.message = calleeReason; + callee.sendMessage(calleeMessage); + } + + var callee = userRegistry.getById(calleeId); + if (!from || !userRegistry.getByName(from)) { + return onError(null, 'unknown from = ' + from); + } + var caller = userRegistry.getByName(from); + + if (callResponse === 'accept') { + var pipeline = new CallMediaPipeline(); + pipelines[caller.id] = pipeline; + pipelines[callee.id] = pipeline; + + pipeline.createPipeline(caller.id, callee.id, ws, function(error) { + if (error) { + return onError(error, error); + } + + pipeline.generateSdpAnswer(caller.id, caller.sdpOffer, function(error, callerSdpAnswer) { + if (error) { + return onError(error, error); + } + + pipeline.generateSdpAnswer(callee.id, calleeSdp, function(error, calleeSdpAnswer) { + if (error) { + return onError(error, error); + } + + var message = { + id: 'startCommunication', + sdpAnswer: calleeSdpAnswer + }; + callee.sendMessage(message); + + message = { + id: 'callResponse', + response : 'accepted', + sdpAnswer: callerSdpAnswer + }; + caller.sendMessage(message); + }); + }); + }); + } else { + var decline = { + id: 'callResponse', + response: 'rejected', + message: 'user declined' + }; + caller.sendMessage(decline); + } +} + +function call(callerId, to, from, sdpOffer) { + clearCandidatesQueue(callerId); + + var caller = userRegistry.getById(callerId); + var rejectCause = 'User ' + to + ' is not registered'; + if (userRegistry.getByName(to)) { + var callee = userRegistry.getByName(to); + caller.sdpOffer = sdpOffer + callee.peer = from; + caller.peer = to; + var message = { + id: 'incomingCall', + from: from + }; + try{ + return callee.sendMessage(message); + } catch(exception) { + rejectCause = "Error " + exception; + } + } + var message = { + id: 'callResponse', + response: 'rejected: ', + message: rejectCause + }; + caller.sendMessage(message); +} + +function register(id, name, ws, callback) { + function onError(error) { + ws.send(JSON.stringify({id:'registerResponse', response : 'rejected ', message: error})); + } + + if (!name) { + return onError("empty user name"); + } + + if (userRegistry.getByName(name)) { + return onError("User " + name + " is already registered"); + } + + userRegistry.register(new UserSession(id, name, ws)); + try { + ws.send(JSON.stringify({id: 'registerResponse', response: 'accepted'})); + } catch(exception) { + onError(exception); + } +} + +function clearCandidatesQueue(sessionId) { + if (candidatesQueue[sessionId]) { + delete candidatesQueue[sessionId]; + } +} + +function onIceCandidate(sessionId, _candidate) { + var candidate = kurento.register.complexTypes.IceCandidate(_candidate); + var user = userRegistry.getById(sessionId); + + if (pipelines[user.id] && pipelines[user.id].webRtcEndpoint && pipelines[user.id].webRtcEndpoint[user.id]) { + var webRtcEndpoint = pipelines[user.id].webRtcEndpoint[user.id]; + webRtcEndpoint.addIceCandidate(candidate); + } + else { + if (!candidatesQueue[user.id]) { + candidatesQueue[user.id] = []; + } + candidatesQueue[sessionId].push(candidate); + } +} + +app.use(express.static(path.join(__dirname, 'static'))); diff --git a/kurento-one2one-composite/static/bower.json b/kurento-one2one-composite/static/bower.json new file mode 100644 index 00000000..d35e5c21 --- /dev/null +++ b/kurento-one2one-composite/static/bower.json @@ -0,0 +1,29 @@ +{ + "name": "kurento-one2one-call", + "description": "Kurento Browser JavaScript Tutorial", + "authors": [ + "Kurento " + ], + "main": "index.html", + "moduleType": [ + "globals" + ], + "license": "LGPL", + "homepage": "http://www.kurento.org/", + "private": true, + "ignore": [ + "**/.*", + "node_modules", + "bower_components", + "test", + "tests" + ], + "dependencies": { + "adapter.js": "*", + "bootstrap": "~3.3.0", + "draggabilly": "~1.2.4", + "demo-console": "1.5.1", + "ekko-lightbox": "~3.3.0", + "kurento-utils": "6.1.0" + } +} diff --git a/kurento-one2one-composite/static/css/kurento.css b/kurento-one2one-composite/static/css/kurento.css new file mode 100644 index 00000000..1aaf3422 --- /dev/null +++ b/kurento-one2one-composite/static/css/kurento.css @@ -0,0 +1,77 @@ +/* + * (C) Copyright 2014-2015 Kurento (http://kurento.org/) + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the GNU Lesser General Public License + * (LGPL) version 2.1 which accompanies this distribution, and is available at + * http://www.gnu.org/licenses/lgpl-2.1.html + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + */ +@CHARSET "UTF-8"; + +html { + position: relative; + min-height: 100%; +} + +body { + padding-top: 40px; + body +} + +video,#console { + display: block; + font-size: 14px; + line-height: 1.42857143; + color: #555; + background-color: #fff; + background-image: none; + border: 1px solid #ccc; + border-radius: 4px; + -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, .075); + box-shadow: inset 0 1px 1px rgba(0, 0, 0, .075); + -webkit-transition: border-color ease-in-out .15s, box-shadow + ease-in-out .15s; + transition: border-color ease-in-out .15s, box-shadow ease-in-out .15s; +} + +#console { + min-height: 120px; + max-height: 360px; +} + +#videoContainer { + position: absolute; + float: left; +} + +#videoBig { + width: 640px; + height: 480px; + top: 0; + left: 0; + z-index: 1; +} + +div#videoSmall { + width: 240px; + height: 180px; + padding: 0px; + position: absolute; + top: 15px; + left: 400px; + cursor: pointer; + z-index: 10; + padding: 0px; +} + +div.dragged { + cursor: all-scroll !important; + border-color: blue !important; + z-index: 10 !important; +} diff --git a/kurento-one2one-composite/static/img/kurento.png b/kurento-one2one-composite/static/img/kurento.png new file mode 100644 index 00000000..6f1a4ad3 Binary files /dev/null and b/kurento-one2one-composite/static/img/kurento.png differ diff --git a/kurento-one2one-composite/static/img/naevatec.png b/kurento-one2one-composite/static/img/naevatec.png new file mode 100644 index 00000000..05ee7041 Binary files /dev/null and b/kurento-one2one-composite/static/img/naevatec.png differ diff --git a/kurento-one2one-composite/static/img/pipeline.png b/kurento-one2one-composite/static/img/pipeline.png new file mode 100644 index 00000000..25e02ccc Binary files /dev/null and b/kurento-one2one-composite/static/img/pipeline.png differ diff --git a/kurento-one2one-composite/static/img/spinner.gif b/kurento-one2one-composite/static/img/spinner.gif new file mode 100644 index 00000000..8be8ba33 Binary files /dev/null and b/kurento-one2one-composite/static/img/spinner.gif differ diff --git a/kurento-one2one-composite/static/img/transparent-1px.png b/kurento-one2one-composite/static/img/transparent-1px.png new file mode 100644 index 00000000..9da19eac Binary files /dev/null and b/kurento-one2one-composite/static/img/transparent-1px.png differ diff --git a/kurento-one2one-composite/static/img/urjc.gif b/kurento-one2one-composite/static/img/urjc.gif new file mode 100644 index 00000000..cd8a7703 Binary files /dev/null and b/kurento-one2one-composite/static/img/urjc.gif differ diff --git a/kurento-one2one-composite/static/img/webrtc.png b/kurento-one2one-composite/static/img/webrtc.png new file mode 100644 index 00000000..d47e2e4c Binary files /dev/null and b/kurento-one2one-composite/static/img/webrtc.png differ diff --git a/kurento-one2one-composite/static/index.html b/kurento-one2one-composite/static/index.html new file mode 100644 index 00000000..6552f2e8 --- /dev/null +++ b/kurento-one2one-composite/static/index.html @@ -0,0 +1,143 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + Kurento Tutorial 4: Video Call 1 to 1 with WebRTC and composing of media streams + + + +
    + +
    + +
    + +
    +
    + +
    +
    + +
    + +
    +
    +
    + +
    +
    + +
    + +
    +
    +

    +
    +
      +
      +
      +
      +
      + +
      +
      + +
      +
      +
      +
      + + + + + diff --git a/kurento-one2one-composite/static/js/index.js b/kurento-one2one-composite/static/js/index.js new file mode 100644 index 00000000..124fddbf --- /dev/null +++ b/kurento-one2one-composite/static/js/index.js @@ -0,0 +1,326 @@ +/* + * (C) Copyright 2014-2015 Kurento (http://kurento.org/) + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the GNU Lesser General Public License + * (LGPL) version 2.1 which accompanies this distribution, and is available at + * http://www.gnu.org/licenses/lgpl-2.1.html + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + */ + +var ws = new WebSocket('ws://' + location.host + '/one2onecomposite'); +var videoInput; +var videoOutput; +var webRtcPeer; + +var registerName = null; +const NOT_REGISTERED = 0; +const REGISTERING = 1; +const REGISTERED = 2; +var registerState = null + +function setRegisterState(nextState) { + switch (nextState) { + case NOT_REGISTERED: + $('#register').attr('disabled', false); + $('#call').attr('disabled', true); + $('#terminate').attr('disabled', true); + break; + + case REGISTERING: + $('#register').attr('disabled', true); + break; + + case REGISTERED: + $('#register').attr('disabled', true); + setCallState(NO_CALL); + break; + + default: + return; + } + registerState = nextState; +} + +const NO_CALL = 0; +const PROCESSING_CALL = 1; +const IN_CALL = 2; +var callState = null + +function setCallState(nextState) { + switch (nextState) { + case NO_CALL: + $('#call').attr('disabled', false); + $('#terminate').attr('disabled', true); + break; + + case PROCESSING_CALL: + $('#call').attr('disabled', true); + $('#terminate').attr('disabled', true); + break; + case IN_CALL: + $('#call').attr('disabled', true); + $('#terminate').attr('disabled', false); + break; + default: + return; + } + callState = nextState; +} + +window.onload = function() { + console = new Console(); + setRegisterState(NOT_REGISTERED); + var drag = new Draggabilly(document.getElementById('videoSmall')); + videoInput = document.getElementById('videoInput'); + videoOutput = document.getElementById('videoOutput'); + document.getElementById('name').focus(); + + document.getElementById('register').addEventListener('click', function() { + register(); + }); + document.getElementById('call').addEventListener('click', function() { + call(); + }); + document.getElementById('terminate').addEventListener('click', function() { + stop(); + }); +} + +window.onbeforeunload = function() { + ws.close(); +} + +ws.onmessage = function(message) { + var parsedMessage = JSON.parse(message.data); + console.info('Received message: ' + message.data); + + switch (parsedMessage.id) { + case 'registerResponse': + resgisterResponse(parsedMessage); + break; + case 'callResponse': + callResponse(parsedMessage); + break; + case 'incomingCall': + incomingCall(parsedMessage); + break; + case 'startCommunication': + startCommunication(parsedMessage); + break; + case 'stopCommunication': + console.info("Communication ended by remote peer"); + stop(true); + break; + case 'iceCandidate': + webRtcPeer.addIceCandidate(parsedMessage.candidate) + break; + default: + console.error('Unrecognized message', parsedMessage); + } +} + +function resgisterResponse(message) { + if (message.response == 'accepted') { + setRegisterState(REGISTERED); + } else { + setRegisterState(NOT_REGISTERED); + var errorMessage = message.message ? message.message + : 'Unknown reason for register rejection.'; + console.log(errorMessage); + alert('Error registering user. See console for further information.'); + } +} + +function callResponse(message) { + if (message.response != 'accepted') { + console.info('Call not accepted by peer. Closing call'); + var errorMessage = message.message ? message.message + : 'Unknown reason for call rejection.'; + console.log(errorMessage); + stop(true); + } else { + setCallState(IN_CALL); + webRtcPeer.processAnswer(message.sdpAnswer); + } +} + +function startCommunication(message) { + setCallState(IN_CALL); + webRtcPeer.processAnswer(message.sdpAnswer); +} + +function incomingCall(message) { + // If bussy just reject without disturbing user + if (callState != NO_CALL) { + var response = { + id : 'incomingCallResponse', + from : message.from, + callResponse : 'reject', + message : 'bussy' + + }; + return sendMessage(response); + } + + setCallState(PROCESSING_CALL); + if (confirm('User ' + message.from + + ' is calling you. Do you accept the call?')) { + showSpinner(videoInput, videoOutput); + + var options = { + localVideo : videoInput, + remoteVideo : videoOutput, + onicecandidate : onIceCandidate + } + + webRtcPeer = kurentoUtils.WebRtcPeer.WebRtcPeerSendrecv(options, + function(error) { + if (error) { + console.error(error); + setCallState(NO_CALL); + } + + this.generateOffer(function(error, offerSdp) { + if (error) { + console.error(error); + setCallState(NO_CALL); + } + var response = { + id : 'incomingCallResponse', + from : message.from, + callResponse : 'accept', + sdpOffer : offerSdp + }; + sendMessage(response); + }); + }); + + } else { + var response = { + id : 'incomingCallResponse', + from : message.from, + callResponse : 'reject', + message : 'user declined' + }; + sendMessage(response); + stop(true); + } +} + +function register() { + var name = document.getElementById('name').value; + if (name == '') { + window.alert("You must insert your user name"); + return; + } + + setRegisterState(REGISTERING); + + var message = { + id : 'register', + name : name + }; + sendMessage(message); + document.getElementById('peer').focus(); +} + +function call() { + if (document.getElementById('peer').value == '') { + window.alert("You must specify the peer name"); + return; + } + + setCallState(PROCESSING_CALL); + + showSpinner(videoInput, videoOutput); + + var options = { + localVideo : videoInput, + remoteVideo : videoOutput, + onicecandidate : onIceCandidate + } + + webRtcPeer = kurentoUtils.WebRtcPeer.WebRtcPeerSendrecv(options, function( + error) { + if (error) { + console.error(error); + setCallState(NO_CALL); + } + + this.generateOffer(function(error, offerSdp) { + if (error) { + console.error(error); + setCallState(NO_CALL); + } + var message = { + id : 'call', + from : document.getElementById('name').value, + to : document.getElementById('peer').value, + sdpOffer : offerSdp + }; + sendMessage(message); + }); + }); + +} + +function stop(message) { + setCallState(NO_CALL); + if (webRtcPeer) { + webRtcPeer.dispose(); + webRtcPeer = null; + + if (!message) { + var message = { + id : 'stop' + } + sendMessage(message); + } + } + hideSpinner(videoInput, videoOutput); +} + +function sendMessage(message) { + var jsonMessage = JSON.stringify(message); + console.log('Senging message: ' + jsonMessage); + ws.send(jsonMessage); +} + +function onIceCandidate(candidate) { + console.log('Local candidate' + JSON.stringify(candidate)); + + var message = { + id : 'onIceCandidate', + candidate : candidate + } + sendMessage(message); +} + +function showSpinner() { + for (var i = 0; i < arguments.length; i++) { + arguments[i].poster = './img/transparent-1px.png'; + arguments[i].style.background = 'center transparent url("./img/spinner.gif") no-repeat'; + } +} + +function hideSpinner() { + for (var i = 0; i < arguments.length; i++) { + arguments[i].src = ''; + arguments[i].poster = './img/webrtc.png'; + arguments[i].style.background = ''; + } +} + +/** + * Lightbox utility (to display media pipeline image in a modal dialog) + */ +$(document).delegate('*[data-toggle="lightbox"]', 'click', function(event) { + event.preventDefault(); + $(this).ekkoLightbox(); +});