mirror of https://github.com/OpenVidu/openvidu.git
openvidu-browser refactoring: kurento deps integrated
parent
8d0aefb959
commit
3f25170615
|
@ -15,13 +15,18 @@
|
|||
"author": "",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"kurento-jsonrpc": "5.1.3",
|
||||
"wolfy87-eventemitter": "4.2.9",
|
||||
"@types/wolfy87-eventemitter": "4.2.31",
|
||||
"webrtc-adapter": "3.3.2",
|
||||
"kurento-utils": "6.6.2",
|
||||
"webrtc-adapter": "5.0.4",
|
||||
"uuid": "~2.0.1",
|
||||
"sdp-translator": "^0.1.15"
|
||||
"freeice": "2.2.0",
|
||||
"inherits": "^2.0.3",
|
||||
"merge": "^1.2.0",
|
||||
"ua-parser-js": "^0.7.7",
|
||||
"hark": "1.1.3",
|
||||
"bufferutil": "3.0.2",
|
||||
"utf-8-validate": "3.0.3",
|
||||
"sdp-translator": "0.1.24"
|
||||
},
|
||||
"devDependencies": {
|
||||
"typescript": "2.5.2",
|
||||
|
|
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
1
openvidu-browser/src/main/resources/static/js/openvidu-browser-1.0.5-beta.3.min.js
vendored
Normal file
1
openvidu-browser/src/main/resources/static/js/openvidu-browser-1.0.5-beta.3.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
@ -0,0 +1,66 @@
|
|||
function Mapper()
|
||||
{
|
||||
var sources = {};
|
||||
|
||||
|
||||
this.forEach = function(callback)
|
||||
{
|
||||
for(var key in sources)
|
||||
{
|
||||
var source = sources[key];
|
||||
|
||||
for(var key2 in source)
|
||||
callback(source[key2]);
|
||||
};
|
||||
};
|
||||
|
||||
this.get = function(id, source)
|
||||
{
|
||||
var ids = sources[source];
|
||||
if(ids == undefined)
|
||||
return undefined;
|
||||
|
||||
return ids[id];
|
||||
};
|
||||
|
||||
this.remove = function(id, source)
|
||||
{
|
||||
var ids = sources[source];
|
||||
if(ids == undefined)
|
||||
return;
|
||||
|
||||
delete ids[id];
|
||||
|
||||
// Check it's empty
|
||||
for(var i in ids){return false}
|
||||
|
||||
delete sources[source];
|
||||
};
|
||||
|
||||
this.set = function(value, id, source)
|
||||
{
|
||||
if(value == undefined)
|
||||
return this.remove(id, source);
|
||||
|
||||
var ids = sources[source];
|
||||
if(ids == undefined)
|
||||
sources[source] = ids = {};
|
||||
|
||||
ids[id] = value;
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
Mapper.prototype.pop = function(id, source)
|
||||
{
|
||||
var value = this.get(id, source);
|
||||
if(value == undefined)
|
||||
return undefined;
|
||||
|
||||
this.remove(id, source);
|
||||
|
||||
return value;
|
||||
};
|
||||
|
||||
|
||||
module.exports = Mapper;
|
|
@ -0,0 +1,21 @@
|
|||
/*
|
||||
* (C) Copyright 2014 Kurento (http://kurento.org/)
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*
|
||||
*/
|
||||
|
||||
var JsonRpcClient = require('./jsonrpcclient');
|
||||
|
||||
|
||||
exports.JsonRpcClient = JsonRpcClient;
|
|
@ -0,0 +1,276 @@
|
|||
/*
|
||||
* (C) Copyright 2014 Kurento (http://kurento.org/)
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*
|
||||
*/
|
||||
|
||||
var RpcBuilder = require('../');
|
||||
var WebSocketWithReconnection = require('./transports/webSocketWithReconnection');
|
||||
|
||||
Date.now = Date.now || function() {
|
||||
return +new Date;
|
||||
};
|
||||
|
||||
var PING_INTERVAL = 5000;
|
||||
|
||||
var RECONNECTING = 'RECONNECTING';
|
||||
var CONNECTED = 'CONNECTED';
|
||||
var DISCONNECTED = 'DISCONNECTED';
|
||||
|
||||
var Logger = console;
|
||||
|
||||
/**
|
||||
*
|
||||
* heartbeat: interval in ms for each heartbeat message,
|
||||
* sendCloseMessage : true / false, before closing the connection, it sends a closeSession message
|
||||
* <pre>
|
||||
* ws : {
|
||||
* uri : URI to conntect to,
|
||||
* useSockJS : true (use SockJS) / false (use WebSocket) by default,
|
||||
* onconnected : callback method to invoke when connection is successful,
|
||||
* ondisconnect : callback method to invoke when the connection is lost,
|
||||
* onreconnecting : callback method to invoke when the client is reconnecting,
|
||||
* onreconnected : callback method to invoke when the client succesfully reconnects,
|
||||
* onerror : callback method to invoke when there is an error
|
||||
* },
|
||||
* rpc : {
|
||||
* requestTimeout : timeout for a request,
|
||||
* sessionStatusChanged: callback method for changes in session status,
|
||||
* mediaRenegotiation: mediaRenegotiation
|
||||
* }
|
||||
* </pre>
|
||||
*/
|
||||
function JsonRpcClient(configuration) {
|
||||
|
||||
var self = this;
|
||||
|
||||
var wsConfig = configuration.ws;
|
||||
|
||||
var notReconnectIfNumLessThan = -1;
|
||||
|
||||
var pingNextNum = 0;
|
||||
var enabledPings = true;
|
||||
var pingPongStarted = false;
|
||||
var pingInterval;
|
||||
|
||||
var status = DISCONNECTED;
|
||||
|
||||
var onreconnecting = wsConfig.onreconnecting;
|
||||
var onreconnected = wsConfig.onreconnected;
|
||||
var onconnected = wsConfig.onconnected;
|
||||
var onerror = wsConfig.onerror;
|
||||
|
||||
configuration.rpc.pull = function(params, request) {
|
||||
request.reply(null, "push");
|
||||
}
|
||||
|
||||
wsConfig.onreconnecting = function() {
|
||||
Logger.debug("--------- ONRECONNECTING -----------");
|
||||
if (status === RECONNECTING) {
|
||||
Logger.error("Websocket already in RECONNECTING state when receiving a new ONRECONNECTING message. Ignoring it");
|
||||
return;
|
||||
}
|
||||
|
||||
status = RECONNECTING;
|
||||
if (onreconnecting) {
|
||||
onreconnecting();
|
||||
}
|
||||
}
|
||||
|
||||
wsConfig.onreconnected = function() {
|
||||
Logger.debug("--------- ONRECONNECTED -----------");
|
||||
if (status === CONNECTED) {
|
||||
Logger.error("Websocket already in CONNECTED state when receiving a new ONRECONNECTED message. Ignoring it");
|
||||
return;
|
||||
}
|
||||
status = CONNECTED;
|
||||
|
||||
enabledPings = true;
|
||||
updateNotReconnectIfLessThan();
|
||||
usePing();
|
||||
|
||||
if (onreconnected) {
|
||||
onreconnected();
|
||||
}
|
||||
}
|
||||
|
||||
wsConfig.onconnected = function() {
|
||||
Logger.debug("--------- ONCONNECTED -----------");
|
||||
if (status === CONNECTED) {
|
||||
Logger.error("Websocket already in CONNECTED state when receiving a new ONCONNECTED message. Ignoring it");
|
||||
return;
|
||||
}
|
||||
status = CONNECTED;
|
||||
|
||||
enabledPings = true;
|
||||
usePing();
|
||||
|
||||
if (onconnected) {
|
||||
onconnected();
|
||||
}
|
||||
}
|
||||
|
||||
wsConfig.onerror = function(error) {
|
||||
Logger.debug("--------- ONERROR -----------");
|
||||
|
||||
status = DISCONNECTED;
|
||||
|
||||
if (onerror) {
|
||||
onerror(error);
|
||||
}
|
||||
}
|
||||
|
||||
var ws = new WebSocketWithReconnection(wsConfig);
|
||||
|
||||
Logger.debug('Connecting websocket to URI: ' + wsConfig.uri);
|
||||
|
||||
var rpcBuilderOptions = {
|
||||
request_timeout: configuration.rpc.requestTimeout,
|
||||
ping_request_timeout: configuration.rpc.heartbeatRequestTimeout
|
||||
};
|
||||
|
||||
var rpc = new RpcBuilder(RpcBuilder.packers.JsonRPC, rpcBuilderOptions, ws,
|
||||
function(request) {
|
||||
|
||||
Logger.debug('Received request: ' + JSON.stringify(request));
|
||||
|
||||
try {
|
||||
var func = configuration.rpc[request.method];
|
||||
|
||||
if (func === undefined) {
|
||||
Logger.error("Method " + request.method + " not registered in client");
|
||||
} else {
|
||||
func(request.params, request);
|
||||
}
|
||||
} catch (err) {
|
||||
Logger.error('Exception processing request: ' + JSON.stringify(request));
|
||||
Logger.error(err);
|
||||
}
|
||||
});
|
||||
|
||||
this.send = function(method, params, callback) {
|
||||
if (method !== 'ping') {
|
||||
Logger.debug('Request: method:' + method + " params:" + JSON.stringify(params));
|
||||
}
|
||||
|
||||
var requestTime = Date.now();
|
||||
|
||||
rpc.encode(method, params, function(error, result) {
|
||||
if (error) {
|
||||
try {
|
||||
Logger.error("ERROR:" + error.message + " in Request: method:" +
|
||||
method + " params:" + JSON.stringify(params) + " request:" +
|
||||
error.request);
|
||||
if (error.data) {
|
||||
Logger.error("ERROR DATA:" + JSON.stringify(error.data));
|
||||
}
|
||||
} catch (e) {}
|
||||
error.requestTime = requestTime;
|
||||
}
|
||||
if (callback) {
|
||||
if (result != undefined && result.value !== 'pong') {
|
||||
Logger.debug('Response: ' + JSON.stringify(result));
|
||||
}
|
||||
callback(error, result);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function updateNotReconnectIfLessThan() {
|
||||
Logger.debug("notReconnectIfNumLessThan = " + pingNextNum + ' (old=' +
|
||||
notReconnectIfNumLessThan + ')');
|
||||
notReconnectIfNumLessThan = pingNextNum;
|
||||
}
|
||||
|
||||
function sendPing() {
|
||||
if (enabledPings) {
|
||||
var params = null;
|
||||
if (pingNextNum == 0 || pingNextNum == notReconnectIfNumLessThan) {
|
||||
params = {
|
||||
interval: configuration.heartbeat || PING_INTERVAL
|
||||
};
|
||||
}
|
||||
pingNextNum++;
|
||||
|
||||
self.send('ping', params, (function(pingNum) {
|
||||
return function(error, result) {
|
||||
if (error) {
|
||||
Logger.debug("Error in ping request #" + pingNum + " (" +
|
||||
error.message + ")");
|
||||
if (pingNum > notReconnectIfNumLessThan) {
|
||||
enabledPings = false;
|
||||
updateNotReconnectIfLessThan();
|
||||
Logger.debug("Server did not respond to ping message #" +
|
||||
pingNum + ". Reconnecting... ");
|
||||
ws.reconnectWs();
|
||||
}
|
||||
}
|
||||
}
|
||||
})(pingNextNum));
|
||||
} else {
|
||||
Logger.debug("Trying to send ping, but ping is not enabled");
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* If configuration.hearbeat has any value, the ping-pong will work with the interval
|
||||
* of configuration.hearbeat
|
||||
*/
|
||||
function usePing() {
|
||||
if (!pingPongStarted) {
|
||||
Logger.debug("Starting ping (if configured)")
|
||||
pingPongStarted = true;
|
||||
|
||||
if (configuration.heartbeat != undefined) {
|
||||
pingInterval = setInterval(sendPing, configuration.heartbeat);
|
||||
sendPing();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this.close = function() {
|
||||
Logger.debug("Closing jsonRpcClient explicitly by client");
|
||||
|
||||
if (pingInterval != undefined) {
|
||||
Logger.debug("Clearing ping interval");
|
||||
clearInterval(pingInterval);
|
||||
}
|
||||
pingPongStarted = false;
|
||||
enabledPings = false;
|
||||
|
||||
if (configuration.sendCloseMessage) {
|
||||
Logger.debug("Sending close message")
|
||||
this.send('closeSession', null, function(error, result) {
|
||||
if (error) {
|
||||
Logger.error("Error sending close message: " + JSON.stringify(error));
|
||||
}
|
||||
ws.close();
|
||||
});
|
||||
} else {
|
||||
ws.close();
|
||||
}
|
||||
}
|
||||
|
||||
// This method is only for testing
|
||||
this.forceClose = function(millis) {
|
||||
ws.forceClose(millis);
|
||||
}
|
||||
|
||||
this.reconnect = function() {
|
||||
ws.reconnectWs();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
module.exports = JsonRpcClient;
|
|
@ -0,0 +1,21 @@
|
|||
/*
|
||||
* (C) Copyright 2014 Kurento (http://kurento.org/)
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*
|
||||
*/
|
||||
|
||||
var WebSocketWithReconnection = require('./webSocketWithReconnection');
|
||||
|
||||
|
||||
exports.WebSocketWithReconnection = WebSocketWithReconnection;
|
|
@ -0,0 +1,242 @@
|
|||
/*
|
||||
* (C) Copyright 2013-2015 Kurento (http://kurento.org/)
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
"use strict";
|
||||
|
||||
var BrowserWebSocket = global.WebSocket || global.MozWebSocket;
|
||||
|
||||
var Logger = console;
|
||||
|
||||
/**
|
||||
* Get either the `WebSocket` or `MozWebSocket` globals
|
||||
* in the browser or try to resolve WebSocket-compatible
|
||||
* interface exposed by `ws` for Node-like environment.
|
||||
*/
|
||||
|
||||
/*var WebSocket = BrowserWebSocket;
|
||||
if (!WebSocket && typeof window === 'undefined') {
|
||||
try {
|
||||
WebSocket = require('ws');
|
||||
} catch (e) { }
|
||||
}*/
|
||||
|
||||
//var SockJS = require('sockjs-client');
|
||||
|
||||
var MAX_RETRIES = 2000; // Forever...
|
||||
var RETRY_TIME_MS = 3000; // FIXME: Implement exponential wait times...
|
||||
|
||||
var CONNECTING = 0;
|
||||
var OPEN = 1;
|
||||
var CLOSING = 2;
|
||||
var CLOSED = 3;
|
||||
|
||||
/*
|
||||
config = {
|
||||
uri : wsUri,
|
||||
useSockJS : true (use SockJS) / false (use WebSocket) by default,
|
||||
onconnected : callback method to invoke when connection is successful,
|
||||
ondisconnect : callback method to invoke when the connection is lost,
|
||||
onreconnecting : callback method to invoke when the client is reconnecting,
|
||||
onreconnected : callback method to invoke when the client succesfully reconnects,
|
||||
};
|
||||
*/
|
||||
function WebSocketWithReconnection(config) {
|
||||
|
||||
var closing = false;
|
||||
var registerMessageHandler;
|
||||
var wsUri = config.uri;
|
||||
var useSockJS = config.useSockJS;
|
||||
var reconnecting = false;
|
||||
|
||||
var forcingDisconnection = false;
|
||||
|
||||
var ws;
|
||||
|
||||
if (useSockJS) {
|
||||
ws = new SockJS(wsUri);
|
||||
} else {
|
||||
ws = new WebSocket(wsUri);
|
||||
}
|
||||
|
||||
ws.onopen = function() {
|
||||
logConnected(ws, wsUri);
|
||||
if (config.onconnected) {
|
||||
config.onconnected();
|
||||
}
|
||||
};
|
||||
|
||||
ws.onerror = function(error) {
|
||||
Logger.error("Could not connect to " + wsUri + " (invoking onerror if defined)", error);
|
||||
if (config.onerror) {
|
||||
config.onerror(error);
|
||||
}
|
||||
};
|
||||
|
||||
function logConnected(ws, wsUri) {
|
||||
try {
|
||||
Logger.debug("WebSocket connected to " + wsUri);
|
||||
} catch (e) {
|
||||
Logger.error(e);
|
||||
}
|
||||
}
|
||||
|
||||
var reconnectionOnClose = function() {
|
||||
if (ws.readyState === CLOSED) {
|
||||
if (closing) {
|
||||
Logger.debug("Connection closed by user");
|
||||
} else {
|
||||
Logger.debug("Connection closed unexpectecly. Reconnecting...");
|
||||
reconnectToSameUri(MAX_RETRIES, 1);
|
||||
}
|
||||
} else {
|
||||
Logger.debug("Close callback from previous websocket. Ignoring it");
|
||||
}
|
||||
};
|
||||
|
||||
ws.onclose = reconnectionOnClose;
|
||||
|
||||
function reconnectToSameUri(maxRetries, numRetries) {
|
||||
Logger.debug("reconnectToSameUri (attempt #" + numRetries + ", max=" + maxRetries + ")");
|
||||
|
||||
if (numRetries === 1) {
|
||||
if (reconnecting) {
|
||||
Logger.warn("Trying to reconnectToNewUri when reconnecting... Ignoring this reconnection.")
|
||||
return;
|
||||
} else {
|
||||
reconnecting = true;
|
||||
}
|
||||
|
||||
if (config.onreconnecting) {
|
||||
config.onreconnecting();
|
||||
}
|
||||
}
|
||||
|
||||
if (forcingDisconnection) {
|
||||
reconnectToNewUri(maxRetries, numRetries, wsUri);
|
||||
|
||||
} else {
|
||||
if (config.newWsUriOnReconnection) {
|
||||
config.newWsUriOnReconnection(function(error, newWsUri) {
|
||||
|
||||
if (error) {
|
||||
Logger.debug(error);
|
||||
setTimeout(function() {
|
||||
reconnectToSameUri(maxRetries, numRetries + 1);
|
||||
}, RETRY_TIME_MS);
|
||||
} else {
|
||||
reconnectToNewUri(maxRetries, numRetries, newWsUri);
|
||||
}
|
||||
})
|
||||
} else {
|
||||
reconnectToNewUri(maxRetries, numRetries, wsUri);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TODO Test retries. How to force not connection?
|
||||
function reconnectToNewUri(maxRetries, numRetries, reconnectWsUri) {
|
||||
Logger.debug("Reconnection attempt #" + numRetries);
|
||||
|
||||
ws.close();
|
||||
|
||||
wsUri = reconnectWsUri || wsUri;
|
||||
|
||||
var newWs;
|
||||
if (useSockJS) {
|
||||
newWs = new SockJS(wsUri);
|
||||
} else {
|
||||
newWs = new WebSocket(wsUri);
|
||||
}
|
||||
|
||||
newWs.onopen = function() {
|
||||
Logger.debug("Reconnected after " + numRetries + " attempts...");
|
||||
logConnected(newWs, wsUri);
|
||||
reconnecting = false;
|
||||
registerMessageHandler();
|
||||
if (config.onreconnected()) {
|
||||
config.onreconnected();
|
||||
}
|
||||
|
||||
newWs.onclose = reconnectionOnClose;
|
||||
};
|
||||
|
||||
var onErrorOrClose = function(error) {
|
||||
Logger.warn("Reconnection error: ", error);
|
||||
|
||||
if (numRetries === maxRetries) {
|
||||
if (config.ondisconnect) {
|
||||
config.ondisconnect();
|
||||
}
|
||||
} else {
|
||||
setTimeout(function() {
|
||||
reconnectToSameUri(maxRetries, numRetries + 1);
|
||||
}, RETRY_TIME_MS);
|
||||
}
|
||||
};
|
||||
|
||||
newWs.onerror = onErrorOrClose;
|
||||
|
||||
ws = newWs;
|
||||
}
|
||||
|
||||
this.close = function() {
|
||||
closing = true;
|
||||
ws.close();
|
||||
};
|
||||
|
||||
|
||||
// This method is only for testing
|
||||
this.forceClose = function(millis) {
|
||||
Logger.debug("Testing: Force WebSocket close");
|
||||
|
||||
if (millis) {
|
||||
Logger.debug("Testing: Change wsUri for " + millis + " millis to simulate net failure");
|
||||
var goodWsUri = wsUri;
|
||||
wsUri = "wss://21.234.12.34.4:443/";
|
||||
|
||||
forcingDisconnection = true;
|
||||
|
||||
setTimeout(function() {
|
||||
Logger.debug("Testing: Recover good wsUri " + goodWsUri);
|
||||
wsUri = goodWsUri;
|
||||
|
||||
forcingDisconnection = false;
|
||||
|
||||
}, millis);
|
||||
}
|
||||
|
||||
ws.close();
|
||||
};
|
||||
|
||||
this.reconnectWs = function() {
|
||||
Logger.debug("reconnectWs");
|
||||
reconnectToSameUri(MAX_RETRIES, 1, wsUri);
|
||||
};
|
||||
|
||||
this.send = function(message) {
|
||||
ws.send(message);
|
||||
};
|
||||
|
||||
this.addEventListener = function(type, callback) {
|
||||
registerMessageHandler = function() {
|
||||
ws.addEventListener(type, callback);
|
||||
};
|
||||
|
||||
registerMessageHandler();
|
||||
};
|
||||
}
|
||||
|
||||
module.exports = WebSocketWithReconnection;
|
|
@ -0,0 +1,822 @@
|
|||
/*
|
||||
* (C) Copyright 2014 Kurento (http://kurento.org/)
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*
|
||||
*/
|
||||
|
||||
|
||||
var defineProperty_IE8 = false
|
||||
if(Object.defineProperty)
|
||||
{
|
||||
try
|
||||
{
|
||||
Object.defineProperty({}, "x", {});
|
||||
}
|
||||
catch(e)
|
||||
{
|
||||
defineProperty_IE8 = true
|
||||
}
|
||||
}
|
||||
|
||||
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Function/bind
|
||||
if (!Function.prototype.bind) {
|
||||
Function.prototype.bind = function(oThis) {
|
||||
if (typeof this !== 'function') {
|
||||
// closest thing possible to the ECMAScript 5
|
||||
// internal IsCallable function
|
||||
throw new TypeError('Function.prototype.bind - what is trying to be bound is not callable');
|
||||
}
|
||||
|
||||
var aArgs = Array.prototype.slice.call(arguments, 1),
|
||||
fToBind = this,
|
||||
fNOP = function() {},
|
||||
fBound = function() {
|
||||
return fToBind.apply(this instanceof fNOP && oThis
|
||||
? this
|
||||
: oThis,
|
||||
aArgs.concat(Array.prototype.slice.call(arguments)));
|
||||
};
|
||||
|
||||
fNOP.prototype = this.prototype;
|
||||
fBound.prototype = new fNOP();
|
||||
|
||||
return fBound;
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
var EventEmitter = require('events').EventEmitter;
|
||||
|
||||
var inherits = require('inherits');
|
||||
|
||||
var packers = require('./packers');
|
||||
var Mapper = require('./Mapper');
|
||||
|
||||
|
||||
var BASE_TIMEOUT = 5000;
|
||||
|
||||
|
||||
function unifyResponseMethods(responseMethods)
|
||||
{
|
||||
if(!responseMethods) return {};
|
||||
|
||||
for(var key in responseMethods)
|
||||
{
|
||||
var value = responseMethods[key];
|
||||
|
||||
if(typeof value == 'string')
|
||||
responseMethods[key] =
|
||||
{
|
||||
response: value
|
||||
}
|
||||
};
|
||||
|
||||
return responseMethods;
|
||||
};
|
||||
|
||||
function unifyTransport(transport)
|
||||
{
|
||||
if(!transport) return;
|
||||
|
||||
// Transport as a function
|
||||
if(transport instanceof Function)
|
||||
return {send: transport};
|
||||
|
||||
// WebSocket & DataChannel
|
||||
if(transport.send instanceof Function)
|
||||
return transport;
|
||||
|
||||
// Message API (Inter-window & WebWorker)
|
||||
if(transport.postMessage instanceof Function)
|
||||
{
|
||||
transport.send = transport.postMessage;
|
||||
return transport;
|
||||
}
|
||||
|
||||
// Stream API
|
||||
if(transport.write instanceof Function)
|
||||
{
|
||||
transport.send = transport.write;
|
||||
return transport;
|
||||
}
|
||||
|
||||
// Transports that only can receive messages, but not send
|
||||
if(transport.onmessage !== undefined) return;
|
||||
if(transport.pause instanceof Function) return;
|
||||
|
||||
throw new SyntaxError("Transport is not a function nor a valid object");
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Representation of a RPC notification
|
||||
*
|
||||
* @class
|
||||
*
|
||||
* @constructor
|
||||
*
|
||||
* @param {String} method -method of the notification
|
||||
* @param params - parameters of the notification
|
||||
*/
|
||||
function RpcNotification(method, params)
|
||||
{
|
||||
if(defineProperty_IE8)
|
||||
{
|
||||
this.method = method
|
||||
this.params = params
|
||||
}
|
||||
else
|
||||
{
|
||||
Object.defineProperty(this, 'method', {value: method, enumerable: true});
|
||||
Object.defineProperty(this, 'params', {value: params, enumerable: true});
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* @class
|
||||
*
|
||||
* @constructor
|
||||
*
|
||||
* @param {object} packer
|
||||
*
|
||||
* @param {object} [options]
|
||||
*
|
||||
* @param {object} [transport]
|
||||
*
|
||||
* @param {Function} [onRequest]
|
||||
*/
|
||||
function RpcBuilder(packer, options, transport, onRequest)
|
||||
{
|
||||
var self = this;
|
||||
|
||||
if(!packer)
|
||||
throw new SyntaxError('Packer is not defined');
|
||||
|
||||
if(!packer.pack || !packer.unpack)
|
||||
throw new SyntaxError('Packer is invalid');
|
||||
|
||||
var responseMethods = unifyResponseMethods(packer.responseMethods);
|
||||
|
||||
|
||||
if(options instanceof Function)
|
||||
{
|
||||
if(transport != undefined)
|
||||
throw new SyntaxError("There can't be parameters after onRequest");
|
||||
|
||||
onRequest = options;
|
||||
transport = undefined;
|
||||
options = undefined;
|
||||
};
|
||||
|
||||
if(options && options.send instanceof Function)
|
||||
{
|
||||
if(transport && !(transport instanceof Function))
|
||||
throw new SyntaxError("Only a function can be after transport");
|
||||
|
||||
onRequest = transport;
|
||||
transport = options;
|
||||
options = undefined;
|
||||
};
|
||||
|
||||
if(transport instanceof Function)
|
||||
{
|
||||
if(onRequest != undefined)
|
||||
throw new SyntaxError("There can't be parameters after onRequest");
|
||||
|
||||
onRequest = transport;
|
||||
transport = undefined;
|
||||
};
|
||||
|
||||
if(transport && transport.send instanceof Function)
|
||||
if(onRequest && !(onRequest instanceof Function))
|
||||
throw new SyntaxError("Only a function can be after transport");
|
||||
|
||||
options = options || {};
|
||||
|
||||
|
||||
EventEmitter.call(this);
|
||||
|
||||
if(onRequest)
|
||||
this.on('request', onRequest);
|
||||
|
||||
|
||||
if(defineProperty_IE8)
|
||||
this.peerID = options.peerID
|
||||
else
|
||||
Object.defineProperty(this, 'peerID', {value: options.peerID});
|
||||
|
||||
var max_retries = options.max_retries || 0;
|
||||
|
||||
|
||||
function transportMessage(event)
|
||||
{
|
||||
self.decode(event.data || event);
|
||||
};
|
||||
|
||||
this.getTransport = function()
|
||||
{
|
||||
return transport;
|
||||
}
|
||||
this.setTransport = function(value)
|
||||
{
|
||||
// Remove listener from old transport
|
||||
if(transport)
|
||||
{
|
||||
// W3C transports
|
||||
if(transport.removeEventListener)
|
||||
transport.removeEventListener('message', transportMessage);
|
||||
|
||||
// Node.js Streams API
|
||||
else if(transport.removeListener)
|
||||
transport.removeListener('data', transportMessage);
|
||||
};
|
||||
|
||||
// Set listener on new transport
|
||||
if(value)
|
||||
{
|
||||
// W3C transports
|
||||
if(value.addEventListener)
|
||||
value.addEventListener('message', transportMessage);
|
||||
|
||||
// Node.js Streams API
|
||||
else if(value.addListener)
|
||||
value.addListener('data', transportMessage);
|
||||
};
|
||||
|
||||
transport = unifyTransport(value);
|
||||
}
|
||||
|
||||
if(!defineProperty_IE8)
|
||||
Object.defineProperty(this, 'transport',
|
||||
{
|
||||
get: this.getTransport.bind(this),
|
||||
set: this.setTransport.bind(this)
|
||||
})
|
||||
|
||||
this.setTransport(transport);
|
||||
|
||||
|
||||
var request_timeout = options.request_timeout || BASE_TIMEOUT;
|
||||
var ping_request_timeout = options.ping_request_timeout || request_timeout;
|
||||
var response_timeout = options.response_timeout || BASE_TIMEOUT;
|
||||
var duplicates_timeout = options.duplicates_timeout || BASE_TIMEOUT;
|
||||
|
||||
|
||||
var requestID = 0;
|
||||
|
||||
var requests = new Mapper();
|
||||
var responses = new Mapper();
|
||||
var processedResponses = new Mapper();
|
||||
|
||||
var message2Key = {};
|
||||
|
||||
|
||||
/**
|
||||
* Store the response to prevent to process duplicate request later
|
||||
*/
|
||||
function storeResponse(message, id, dest)
|
||||
{
|
||||
var response =
|
||||
{
|
||||
message: message,
|
||||
/** Timeout to auto-clean old responses */
|
||||
timeout: setTimeout(function()
|
||||
{
|
||||
responses.remove(id, dest);
|
||||
},
|
||||
response_timeout)
|
||||
};
|
||||
|
||||
responses.set(response, id, dest);
|
||||
};
|
||||
|
||||
/**
|
||||
* Store the response to ignore duplicated messages later
|
||||
*/
|
||||
function storeProcessedResponse(ack, from)
|
||||
{
|
||||
var timeout = setTimeout(function()
|
||||
{
|
||||
processedResponses.remove(ack, from);
|
||||
},
|
||||
duplicates_timeout);
|
||||
|
||||
processedResponses.set(timeout, ack, from);
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Representation of a RPC request
|
||||
*
|
||||
* @class
|
||||
* @extends RpcNotification
|
||||
*
|
||||
* @constructor
|
||||
*
|
||||
* @param {String} method -method of the notification
|
||||
* @param params - parameters of the notification
|
||||
* @param {Integer} id - identifier of the request
|
||||
* @param [from] - source of the notification
|
||||
*/
|
||||
function RpcRequest(method, params, id, from, transport)
|
||||
{
|
||||
RpcNotification.call(this, method, params);
|
||||
|
||||
this.getTransport = function()
|
||||
{
|
||||
return transport;
|
||||
}
|
||||
this.setTransport = function(value)
|
||||
{
|
||||
transport = unifyTransport(value);
|
||||
}
|
||||
|
||||
if(!defineProperty_IE8)
|
||||
Object.defineProperty(this, 'transport',
|
||||
{
|
||||
get: this.getTransport.bind(this),
|
||||
set: this.setTransport.bind(this)
|
||||
})
|
||||
|
||||
var response = responses.get(id, from);
|
||||
|
||||
/**
|
||||
* @constant {Boolean} duplicated
|
||||
*/
|
||||
if(!(transport || self.getTransport()))
|
||||
{
|
||||
if(defineProperty_IE8)
|
||||
this.duplicated = Boolean(response)
|
||||
else
|
||||
Object.defineProperty(this, 'duplicated',
|
||||
{
|
||||
value: Boolean(response)
|
||||
});
|
||||
}
|
||||
|
||||
var responseMethod = responseMethods[method];
|
||||
|
||||
this.pack = packer.pack.bind(packer, this, id)
|
||||
|
||||
/**
|
||||
* Generate a response to this request
|
||||
*
|
||||
* @param {Error} [error]
|
||||
* @param {*} [result]
|
||||
*
|
||||
* @returns {string}
|
||||
*/
|
||||
this.reply = function(error, result, transport)
|
||||
{
|
||||
// Fix optional parameters
|
||||
if(error instanceof Function || error && error.send instanceof Function)
|
||||
{
|
||||
if(result != undefined)
|
||||
throw new SyntaxError("There can't be parameters after callback");
|
||||
|
||||
transport = error;
|
||||
result = null;
|
||||
error = undefined;
|
||||
}
|
||||
|
||||
else if(result instanceof Function
|
||||
|| result && result.send instanceof Function)
|
||||
{
|
||||
if(transport != undefined)
|
||||
throw new SyntaxError("There can't be parameters after callback");
|
||||
|
||||
transport = result;
|
||||
result = null;
|
||||
};
|
||||
|
||||
transport = unifyTransport(transport);
|
||||
|
||||
// Duplicated request, remove old response timeout
|
||||
if(response)
|
||||
clearTimeout(response.timeout);
|
||||
|
||||
if(from != undefined)
|
||||
{
|
||||
if(error)
|
||||
error.dest = from;
|
||||
|
||||
if(result)
|
||||
result.dest = from;
|
||||
};
|
||||
|
||||
var message;
|
||||
|
||||
// New request or overriden one, create new response with provided data
|
||||
if(error || result != undefined)
|
||||
{
|
||||
if(self.peerID != undefined)
|
||||
{
|
||||
if(error)
|
||||
error.from = self.peerID;
|
||||
else
|
||||
result.from = self.peerID;
|
||||
}
|
||||
|
||||
// Protocol indicates that responses has own request methods
|
||||
if(responseMethod)
|
||||
{
|
||||
if(responseMethod.error == undefined && error)
|
||||
message =
|
||||
{
|
||||
error: error
|
||||
};
|
||||
|
||||
else
|
||||
{
|
||||
var method = error
|
||||
? responseMethod.error
|
||||
: responseMethod.response;
|
||||
|
||||
message =
|
||||
{
|
||||
method: method,
|
||||
params: error || result
|
||||
};
|
||||
}
|
||||
}
|
||||
else
|
||||
message =
|
||||
{
|
||||
error: error,
|
||||
result: result
|
||||
};
|
||||
|
||||
message = packer.pack(message, id);
|
||||
}
|
||||
|
||||
// Duplicate & not-overriden request, re-send old response
|
||||
else if(response)
|
||||
message = response.message;
|
||||
|
||||
// New empty reply, response null value
|
||||
else
|
||||
message = packer.pack({result: null}, id);
|
||||
|
||||
// Store the response to prevent to process a duplicated request later
|
||||
storeResponse(message, id, from);
|
||||
|
||||
// Return the stored response so it can be directly send back
|
||||
transport = transport || this.getTransport() || self.getTransport();
|
||||
|
||||
if(transport)
|
||||
return transport.send(message);
|
||||
|
||||
return message;
|
||||
}
|
||||
};
|
||||
inherits(RpcRequest, RpcNotification);
|
||||
|
||||
|
||||
function cancel(message)
|
||||
{
|
||||
var key = message2Key[message];
|
||||
if(!key) return;
|
||||
|
||||
delete message2Key[message];
|
||||
|
||||
var request = requests.pop(key.id, key.dest);
|
||||
if(!request) return;
|
||||
|
||||
clearTimeout(request.timeout);
|
||||
|
||||
// Start duplicated responses timeout
|
||||
storeProcessedResponse(key.id, key.dest);
|
||||
};
|
||||
|
||||
/**
|
||||
* Allow to cancel a request and don't wait for a response
|
||||
*
|
||||
* If `message` is not given, cancel all the request
|
||||
*/
|
||||
this.cancel = function(message)
|
||||
{
|
||||
if(message) return cancel(message);
|
||||
|
||||
for(var message in message2Key)
|
||||
cancel(message);
|
||||
};
|
||||
|
||||
|
||||
this.close = function()
|
||||
{
|
||||
// Prevent to receive new messages
|
||||
var transport = this.getTransport();
|
||||
if(transport && transport.close)
|
||||
transport.close();
|
||||
|
||||
// Request & processed responses
|
||||
this.cancel();
|
||||
|
||||
processedResponses.forEach(clearTimeout);
|
||||
|
||||
// Responses
|
||||
responses.forEach(function(response)
|
||||
{
|
||||
clearTimeout(response.timeout);
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Generates and encode a JsonRPC 2.0 message
|
||||
*
|
||||
* @param {String} method -method of the notification
|
||||
* @param params - parameters of the notification
|
||||
* @param [dest] - destination of the notification
|
||||
* @param {object} [transport] - transport where to send the message
|
||||
* @param [callback] - function called when a response to this request is
|
||||
* received. If not defined, a notification will be send instead
|
||||
*
|
||||
* @returns {string} A raw JsonRPC 2.0 request or notification string
|
||||
*/
|
||||
this.encode = function(method, params, dest, transport, callback)
|
||||
{
|
||||
// Fix optional parameters
|
||||
if(params instanceof Function)
|
||||
{
|
||||
if(dest != undefined)
|
||||
throw new SyntaxError("There can't be parameters after callback");
|
||||
|
||||
callback = params;
|
||||
transport = undefined;
|
||||
dest = undefined;
|
||||
params = undefined;
|
||||
}
|
||||
|
||||
else if(dest instanceof Function)
|
||||
{
|
||||
if(transport != undefined)
|
||||
throw new SyntaxError("There can't be parameters after callback");
|
||||
|
||||
callback = dest;
|
||||
transport = undefined;
|
||||
dest = undefined;
|
||||
}
|
||||
|
||||
else if(transport instanceof Function)
|
||||
{
|
||||
if(callback != undefined)
|
||||
throw new SyntaxError("There can't be parameters after callback");
|
||||
|
||||
callback = transport;
|
||||
transport = undefined;
|
||||
};
|
||||
|
||||
if(self.peerID != undefined)
|
||||
{
|
||||
params = params || {};
|
||||
|
||||
params.from = self.peerID;
|
||||
};
|
||||
|
||||
if(dest != undefined)
|
||||
{
|
||||
params = params || {};
|
||||
|
||||
params.dest = dest;
|
||||
};
|
||||
|
||||
// Encode message
|
||||
var message =
|
||||
{
|
||||
method: method,
|
||||
params: params
|
||||
};
|
||||
|
||||
if(callback)
|
||||
{
|
||||
var id = requestID++;
|
||||
var retried = 0;
|
||||
|
||||
message = packer.pack(message, id);
|
||||
|
||||
function dispatchCallback(error, result)
|
||||
{
|
||||
self.cancel(message);
|
||||
|
||||
callback(error, result);
|
||||
};
|
||||
|
||||
var request =
|
||||
{
|
||||
message: message,
|
||||
callback: dispatchCallback,
|
||||
responseMethods: responseMethods[method] || {}
|
||||
};
|
||||
|
||||
var encode_transport = unifyTransport(transport);
|
||||
|
||||
function sendRequest(transport)
|
||||
{
|
||||
var rt = (method === 'ping' ? ping_request_timeout : request_timeout);
|
||||
request.timeout = setTimeout(timeout, rt*Math.pow(2, retried++));
|
||||
message2Key[message] = {id: id, dest: dest};
|
||||
requests.set(request, id, dest);
|
||||
|
||||
transport = transport || encode_transport || self.getTransport();
|
||||
if(transport)
|
||||
return transport.send(message);
|
||||
|
||||
return message;
|
||||
};
|
||||
|
||||
function retry(transport)
|
||||
{
|
||||
transport = unifyTransport(transport);
|
||||
|
||||
console.warn(retried+' retry for request message:',message);
|
||||
|
||||
var timeout = processedResponses.pop(id, dest);
|
||||
clearTimeout(timeout);
|
||||
|
||||
return sendRequest(transport);
|
||||
};
|
||||
|
||||
function timeout()
|
||||
{
|
||||
if(retried < max_retries)
|
||||
return retry(transport);
|
||||
|
||||
var error = new Error('Request has timed out');
|
||||
error.request = message;
|
||||
|
||||
error.retry = retry;
|
||||
|
||||
dispatchCallback(error)
|
||||
};
|
||||
|
||||
return sendRequest(transport);
|
||||
};
|
||||
|
||||
// Return the packed message
|
||||
message = packer.pack(message);
|
||||
|
||||
transport = transport || this.getTransport();
|
||||
if(transport)
|
||||
return transport.send(message);
|
||||
|
||||
return message;
|
||||
};
|
||||
|
||||
/**
|
||||
* Decode and process a JsonRPC 2.0 message
|
||||
*
|
||||
* @param {string} message - string with the content of the message
|
||||
*
|
||||
* @returns {RpcNotification|RpcRequest|undefined} - the representation of the
|
||||
* notification or the request. If a response was processed, it will return
|
||||
* `undefined` to notify that it was processed
|
||||
*
|
||||
* @throws {TypeError} - Message is not defined
|
||||
*/
|
||||
this.decode = function(message, transport)
|
||||
{
|
||||
if(!message)
|
||||
throw new TypeError("Message is not defined");
|
||||
|
||||
try
|
||||
{
|
||||
message = packer.unpack(message);
|
||||
}
|
||||
catch(e)
|
||||
{
|
||||
// Ignore invalid messages
|
||||
return console.debug(e, message);
|
||||
};
|
||||
|
||||
var id = message.id;
|
||||
var ack = message.ack;
|
||||
var method = message.method;
|
||||
var params = message.params || {};
|
||||
|
||||
var from = params.from;
|
||||
var dest = params.dest;
|
||||
|
||||
// Ignore messages send by us
|
||||
if(self.peerID != undefined && from == self.peerID) return;
|
||||
|
||||
// Notification
|
||||
if(id == undefined && ack == undefined)
|
||||
{
|
||||
var notification = new RpcNotification(method, params);
|
||||
|
||||
if(self.emit('request', notification)) return;
|
||||
return notification;
|
||||
};
|
||||
|
||||
|
||||
function processRequest()
|
||||
{
|
||||
// If we have a transport and it's a duplicated request, reply inmediatly
|
||||
transport = unifyTransport(transport) || self.getTransport();
|
||||
if(transport)
|
||||
{
|
||||
var response = responses.get(id, from);
|
||||
if(response)
|
||||
return transport.send(response.message);
|
||||
};
|
||||
|
||||
var idAck = (id != undefined) ? id : ack;
|
||||
var request = new RpcRequest(method, params, idAck, from, transport);
|
||||
|
||||
if(self.emit('request', request)) return;
|
||||
return request;
|
||||
};
|
||||
|
||||
function processResponse(request, error, result)
|
||||
{
|
||||
request.callback(error, result);
|
||||
};
|
||||
|
||||
function duplicatedResponse(timeout)
|
||||
{
|
||||
console.warn("Response already processed", message);
|
||||
|
||||
// Update duplicated responses timeout
|
||||
clearTimeout(timeout);
|
||||
storeProcessedResponse(ack, from);
|
||||
};
|
||||
|
||||
|
||||
// Request, or response with own method
|
||||
if(method)
|
||||
{
|
||||
// Check if it's a response with own method
|
||||
if(dest == undefined || dest == self.peerID)
|
||||
{
|
||||
var request = requests.get(ack, from);
|
||||
if(request)
|
||||
{
|
||||
var responseMethods = request.responseMethods;
|
||||
|
||||
if(method == responseMethods.error)
|
||||
return processResponse(request, params);
|
||||
|
||||
if(method == responseMethods.response)
|
||||
return processResponse(request, null, params);
|
||||
|
||||
return processRequest();
|
||||
}
|
||||
|
||||
var processed = processedResponses.get(ack, from);
|
||||
if(processed)
|
||||
return duplicatedResponse(processed);
|
||||
}
|
||||
|
||||
// Request
|
||||
return processRequest();
|
||||
};
|
||||
|
||||
var error = message.error;
|
||||
var result = message.result;
|
||||
|
||||
// Ignore responses not send to us
|
||||
if(error && error.dest && error.dest != self.peerID) return;
|
||||
if(result && result.dest && result.dest != self.peerID) return;
|
||||
|
||||
// Response
|
||||
var request = requests.get(ack, from);
|
||||
if(!request)
|
||||
{
|
||||
var processed = processedResponses.get(ack, from);
|
||||
if(processed)
|
||||
return duplicatedResponse(processed);
|
||||
|
||||
return console.warn("No callback was defined for this message", message);
|
||||
};
|
||||
|
||||
// Process response
|
||||
processResponse(request, error, result);
|
||||
};
|
||||
};
|
||||
inherits(RpcBuilder, EventEmitter);
|
||||
|
||||
|
||||
RpcBuilder.RpcNotification = RpcNotification;
|
||||
|
||||
|
||||
module.exports = RpcBuilder;
|
||||
|
||||
var clients = require('./clients');
|
||||
var transports = require('./clients/transports');
|
||||
|
||||
RpcBuilder.clients = clients;
|
||||
RpcBuilder.clients.transports = transports;
|
||||
RpcBuilder.packers = packers;
|
|
@ -0,0 +1,103 @@
|
|||
/**
|
||||
* JsonRPC 2.0 packer
|
||||
*/
|
||||
|
||||
/**
|
||||
* Pack a JsonRPC 2.0 message
|
||||
*
|
||||
* @param {Object} message - object to be packaged. It requires to have all the
|
||||
* fields needed by the JsonRPC 2.0 message that it's going to be generated
|
||||
*
|
||||
* @return {String} - the stringified JsonRPC 2.0 message
|
||||
*/
|
||||
function pack(message, id)
|
||||
{
|
||||
var result =
|
||||
{
|
||||
jsonrpc: "2.0"
|
||||
};
|
||||
|
||||
// Request
|
||||
if(message.method)
|
||||
{
|
||||
result.method = message.method;
|
||||
|
||||
if(message.params)
|
||||
result.params = message.params;
|
||||
|
||||
// Request is a notification
|
||||
if(id != undefined)
|
||||
result.id = id;
|
||||
}
|
||||
|
||||
// Response
|
||||
else if(id != undefined)
|
||||
{
|
||||
if(message.error)
|
||||
{
|
||||
if(message.result !== undefined)
|
||||
throw new TypeError("Both result and error are defined");
|
||||
|
||||
result.error = message.error;
|
||||
}
|
||||
else if(message.result !== undefined)
|
||||
result.result = message.result;
|
||||
else
|
||||
throw new TypeError("No result or error is defined");
|
||||
|
||||
result.id = id;
|
||||
};
|
||||
|
||||
return JSON.stringify(result);
|
||||
};
|
||||
|
||||
/**
|
||||
* Unpack a JsonRPC 2.0 message
|
||||
*
|
||||
* @param {String} message - string with the content of the JsonRPC 2.0 message
|
||||
*
|
||||
* @throws {TypeError} - Invalid JsonRPC version
|
||||
*
|
||||
* @return {Object} - object filled with the JsonRPC 2.0 message content
|
||||
*/
|
||||
function unpack(message)
|
||||
{
|
||||
var result = message;
|
||||
|
||||
if(typeof message === 'string' || message instanceof String) {
|
||||
result = JSON.parse(message);
|
||||
}
|
||||
|
||||
// Check if it's a valid message
|
||||
|
||||
var version = result.jsonrpc;
|
||||
if(version !== '2.0')
|
||||
throw new TypeError("Invalid JsonRPC version '" + version + "': " + message);
|
||||
|
||||
// Response
|
||||
if(result.method == undefined)
|
||||
{
|
||||
if(result.id == undefined)
|
||||
throw new TypeError("Invalid message: "+message);
|
||||
|
||||
var result_defined = result.result !== undefined;
|
||||
var error_defined = result.error !== undefined;
|
||||
|
||||
// Check only result or error is defined, not both or none
|
||||
if(result_defined && error_defined)
|
||||
throw new TypeError("Both result and error are defined: "+message);
|
||||
|
||||
if(!result_defined && !error_defined)
|
||||
throw new TypeError("No result or error is defined: "+message);
|
||||
|
||||
result.ack = result.id;
|
||||
delete result.id;
|
||||
}
|
||||
|
||||
// Return unpacked message
|
||||
return result;
|
||||
};
|
||||
|
||||
|
||||
exports.pack = pack;
|
||||
exports.unpack = unpack;
|
|
@ -0,0 +1,13 @@
|
|||
function pack(message)
|
||||
{
|
||||
throw new TypeError("Not yet implemented");
|
||||
};
|
||||
|
||||
function unpack(message)
|
||||
{
|
||||
throw new TypeError("Not yet implemented");
|
||||
};
|
||||
|
||||
|
||||
exports.pack = pack;
|
||||
exports.unpack = unpack;
|
|
@ -0,0 +1,6 @@
|
|||
var JsonRPC = require('./JsonRPC');
|
||||
var XmlRPC = require('./XmlRPC');
|
||||
|
||||
|
||||
exports.JsonRPC = JsonRPC;
|
||||
exports.XmlRPC = XmlRPC;
|
|
@ -0,0 +1,777 @@
|
|||
/*
|
||||
* (C) Copyright 2014-2015 Kurento (http://kurento.org/)
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
var freeice = require('freeice')
|
||||
var inherits = require('inherits')
|
||||
var UAParser = require('ua-parser-js')
|
||||
var uuid = require('uuid')
|
||||
var hark = require('hark')
|
||||
|
||||
var EventEmitter = require('events').EventEmitter
|
||||
var recursive = require('merge').recursive.bind(undefined, true)
|
||||
var sdpTranslator = require('sdp-translator')
|
||||
var logger = window.Logger || console
|
||||
|
||||
// var gUM = navigator.mediaDevices.getUserMedia || function (constraints) {
|
||||
// return new Promise(navigator.getUserMedia(constraints, function (stream) {
|
||||
// videoStream = stream
|
||||
// start()
|
||||
// }).eror(callback));
|
||||
// }
|
||||
|
||||
try {
|
||||
require('kurento-browser-extensions')
|
||||
} catch (error) {
|
||||
if (typeof getScreenConstraints === 'undefined') {
|
||||
logger.warn('screen sharing is not available')
|
||||
|
||||
getScreenConstraints = function getScreenConstraints(sendSource, callback) {
|
||||
callback(new Error("This library is not enabled for screen sharing"))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var MEDIA_CONSTRAINTS = {
|
||||
audio: true,
|
||||
video: {
|
||||
width: 640,
|
||||
framerate: 15
|
||||
}
|
||||
}
|
||||
|
||||
// Somehow, the UAParser constructor gets an empty window object.
|
||||
// We need to pass the user agent string in order to get information
|
||||
var ua = (window && window.navigator) ? window.navigator.userAgent : ''
|
||||
var parser = new UAParser(ua)
|
||||
var browser = parser.getBrowser()
|
||||
|
||||
var usePlanB = false
|
||||
if (browser.name === 'Chrome' || browser.name === 'Chromium') {
|
||||
logger.debug(browser.name + ": using SDP PlanB")
|
||||
usePlanB = true
|
||||
}
|
||||
|
||||
function noop(error) {
|
||||
if (error) logger.error(error)
|
||||
}
|
||||
|
||||
function trackStop(track) {
|
||||
track.stop && track.stop()
|
||||
}
|
||||
|
||||
function streamStop(stream) {
|
||||
stream.getTracks().forEach(trackStop)
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a string representation of a SessionDescription object.
|
||||
*/
|
||||
var dumpSDP = function (description) {
|
||||
if (typeof description === 'undefined' || description === null) {
|
||||
return ''
|
||||
}
|
||||
|
||||
return 'type: ' + description.type + '\r\n' + description.sdp
|
||||
}
|
||||
|
||||
function bufferizeCandidates(pc, onerror) {
|
||||
var candidatesQueue = []
|
||||
|
||||
pc.addEventListener('signalingstatechange', function () {
|
||||
if (this.signalingState === 'stable') {
|
||||
while (candidatesQueue.length) {
|
||||
var entry = candidatesQueue.shift()
|
||||
|
||||
this.addIceCandidate(entry.candidate, entry.callback, entry.callback)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
return function (candidate, callback) {
|
||||
callback = callback || onerror
|
||||
|
||||
switch (pc.signalingState) {
|
||||
case 'closed':
|
||||
callback(new Error('PeerConnection object is closed'))
|
||||
break
|
||||
case 'stable':
|
||||
if (pc.remoteDescription) {
|
||||
pc.addIceCandidate(candidate, callback, callback)
|
||||
break
|
||||
}
|
||||
default:
|
||||
candidatesQueue.push({
|
||||
candidate: candidate,
|
||||
callback: callback
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* Simulcast utilities */
|
||||
|
||||
function removeFIDFromOffer(sdp) {
|
||||
var n = sdp.indexOf("a=ssrc-group:FID");
|
||||
|
||||
if (n > 0) {
|
||||
return sdp.slice(0, n);
|
||||
} else {
|
||||
return sdp;
|
||||
}
|
||||
}
|
||||
|
||||
function getSimulcastInfo(videoStream) {
|
||||
var videoTracks = videoStream.getVideoTracks();
|
||||
if (!videoTracks.length) {
|
||||
logger.warn('No video tracks available in the video stream')
|
||||
return ''
|
||||
}
|
||||
var lines = [
|
||||
'a=x-google-flag:conference',
|
||||
'a=ssrc-group:SIM 1 2 3',
|
||||
'a=ssrc:1 cname:localVideo',
|
||||
'a=ssrc:1 msid:' + videoStream.id + ' ' + videoTracks[0].id,
|
||||
'a=ssrc:1 mslabel:' + videoStream.id,
|
||||
'a=ssrc:1 label:' + videoTracks[0].id,
|
||||
'a=ssrc:2 cname:localVideo',
|
||||
'a=ssrc:2 msid:' + videoStream.id + ' ' + videoTracks[0].id,
|
||||
'a=ssrc:2 mslabel:' + videoStream.id,
|
||||
'a=ssrc:2 label:' + videoTracks[0].id,
|
||||
'a=ssrc:3 cname:localVideo',
|
||||
'a=ssrc:3 msid:' + videoStream.id + ' ' + videoTracks[0].id,
|
||||
'a=ssrc:3 mslabel:' + videoStream.id,
|
||||
'a=ssrc:3 label:' + videoTracks[0].id
|
||||
];
|
||||
|
||||
lines.push('');
|
||||
|
||||
return lines.join('\n');
|
||||
}
|
||||
|
||||
/**
|
||||
* Wrapper object of an RTCPeerConnection. This object is aimed to simplify the
|
||||
* development of WebRTC-based applications.
|
||||
*
|
||||
* @constructor module:kurentoUtils.WebRtcPeer
|
||||
*
|
||||
* @param {String} mode Mode in which the PeerConnection will be configured.
|
||||
* Valid values are: 'recv', 'send', and 'sendRecv'
|
||||
* @param localVideo Video tag for the local stream
|
||||
* @param remoteVideo Video tag for the remote stream
|
||||
* @param {MediaStream} videoStream Stream to be used as primary source
|
||||
* (typically video and audio, or only video if combined with audioStream) for
|
||||
* localVideo and to be added as stream to the RTCPeerConnection
|
||||
* @param {MediaStream} audioStream Stream to be used as second source
|
||||
* (typically for audio) for localVideo and to be added as stream to the
|
||||
* RTCPeerConnection
|
||||
*/
|
||||
function WebRtcPeer(mode, options, callback) {
|
||||
if (!(this instanceof WebRtcPeer)) {
|
||||
return new WebRtcPeer(mode, options, callback)
|
||||
}
|
||||
|
||||
WebRtcPeer.super_.call(this)
|
||||
|
||||
if (options instanceof Function) {
|
||||
callback = options
|
||||
options = undefined
|
||||
}
|
||||
|
||||
options = options || {}
|
||||
callback = (callback || noop).bind(this)
|
||||
|
||||
var self = this
|
||||
var localVideo = options.localVideo
|
||||
var remoteVideo = options.remoteVideo
|
||||
var videoStream = options.videoStream
|
||||
var audioStream = options.audioStream
|
||||
var mediaConstraints = options.mediaConstraints
|
||||
|
||||
var connectionConstraints = options.connectionConstraints
|
||||
var pc = options.peerConnection
|
||||
var sendSource = options.sendSource || 'webcam'
|
||||
|
||||
var dataChannelConfig = options.dataChannelConfig
|
||||
var useDataChannels = options.dataChannels || false
|
||||
var dataChannel
|
||||
|
||||
var guid = uuid.v4()
|
||||
var configuration = recursive({
|
||||
iceServers: freeice()
|
||||
},
|
||||
options.configuration)
|
||||
|
||||
var onicecandidate = options.onicecandidate
|
||||
if (onicecandidate) this.on('icecandidate', onicecandidate)
|
||||
|
||||
var oncandidategatheringdone = options.oncandidategatheringdone
|
||||
if (oncandidategatheringdone) {
|
||||
this.on('candidategatheringdone', oncandidategatheringdone)
|
||||
}
|
||||
|
||||
var simulcast = options.simulcast
|
||||
var multistream = options.multistream
|
||||
var interop = new sdpTranslator.Interop()
|
||||
var candidatesQueueOut = []
|
||||
var candidategatheringdone = false
|
||||
|
||||
Object.defineProperties(this, {
|
||||
'peerConnection': {
|
||||
get: function () {
|
||||
return pc
|
||||
}
|
||||
},
|
||||
|
||||
'id': {
|
||||
value: options.id || guid,
|
||||
writable: false
|
||||
},
|
||||
|
||||
'remoteVideo': {
|
||||
get: function () {
|
||||
return remoteVideo
|
||||
}
|
||||
},
|
||||
|
||||
'localVideo': {
|
||||
get: function () {
|
||||
return localVideo
|
||||
}
|
||||
},
|
||||
|
||||
'dataChannel': {
|
||||
get: function () {
|
||||
return dataChannel
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* @member {(external:ImageData|undefined)} currentFrame
|
||||
*/
|
||||
'currentFrame': {
|
||||
get: function () {
|
||||
// [ToDo] Find solution when we have a remote stream but we didn't set
|
||||
// a remoteVideo tag
|
||||
if (!remoteVideo) return;
|
||||
|
||||
if (remoteVideo.readyState < remoteVideo.HAVE_CURRENT_DATA)
|
||||
throw new Error('No video stream data available')
|
||||
|
||||
var canvas = document.createElement('canvas')
|
||||
canvas.width = remoteVideo.videoWidth
|
||||
canvas.height = remoteVideo.videoHeight
|
||||
|
||||
canvas.getContext('2d').drawImage(remoteVideo, 0, 0)
|
||||
|
||||
return canvas
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
// Init PeerConnection
|
||||
if (!pc) {
|
||||
pc = new RTCPeerConnection(configuration);
|
||||
if (useDataChannels && !dataChannel) {
|
||||
var dcId = 'WebRtcPeer-' + self.id
|
||||
var dcOptions = undefined
|
||||
if (dataChannelConfig) {
|
||||
dcId = dataChannelConfig.id || dcId
|
||||
dcOptions = dataChannelConfig.options
|
||||
}
|
||||
dataChannel = pc.createDataChannel(dcId, dcOptions);
|
||||
if (dataChannelConfig) {
|
||||
dataChannel.onopen = dataChannelConfig.onopen;
|
||||
dataChannel.onclose = dataChannelConfig.onclose;
|
||||
dataChannel.onmessage = dataChannelConfig.onmessage;
|
||||
dataChannel.onbufferedamountlow = dataChannelConfig.onbufferedamountlow;
|
||||
dataChannel.onerror = dataChannelConfig.onerror || noop;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pc.addEventListener('icecandidate', function (event) {
|
||||
var candidate = event.candidate
|
||||
|
||||
if (EventEmitter.listenerCount(self, 'icecandidate') ||
|
||||
EventEmitter.listenerCount(
|
||||
self, 'candidategatheringdone')) {
|
||||
if (candidate) {
|
||||
var cand
|
||||
|
||||
if (multistream && usePlanB) {
|
||||
cand = interop.candidateToUnifiedPlan(candidate)
|
||||
} else {
|
||||
cand = candidate
|
||||
}
|
||||
|
||||
self.emit('icecandidate', cand)
|
||||
candidategatheringdone = false
|
||||
} else if (!candidategatheringdone) {
|
||||
self.emit('candidategatheringdone')
|
||||
candidategatheringdone = true
|
||||
}
|
||||
} else if (!candidategatheringdone) {
|
||||
// Not listening to 'icecandidate' or 'candidategatheringdone' events, queue
|
||||
// the candidate until one of them is listened
|
||||
candidatesQueueOut.push(candidate)
|
||||
|
||||
if (!candidate) candidategatheringdone = true
|
||||
}
|
||||
})
|
||||
|
||||
pc.ontrack = options.onaddstream
|
||||
pc.onnegotiationneeded = options.onnegotiationneeded
|
||||
this.on('newListener', function (event, listener) {
|
||||
if (event === 'icecandidate' || event === 'candidategatheringdone') {
|
||||
while (candidatesQueueOut.length) {
|
||||
var candidate = candidatesQueueOut.shift()
|
||||
|
||||
if (!candidate === (event === 'candidategatheringdone')) {
|
||||
listener(candidate)
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
var addIceCandidate = bufferizeCandidates(pc)
|
||||
|
||||
/**
|
||||
* Callback function invoked when an ICE candidate is received. Developers are
|
||||
* expected to invoke this function in order to complete the SDP negotiation.
|
||||
*
|
||||
* @function module:kurentoUtils.WebRtcPeer.prototype.addIceCandidate
|
||||
*
|
||||
* @param iceCandidate - Literal object with the ICE candidate description
|
||||
* @param callback - Called when the ICE candidate has been added.
|
||||
*/
|
||||
this.addIceCandidate = function (iceCandidate, callback) {
|
||||
var candidate
|
||||
|
||||
if (multistream && usePlanB) {
|
||||
candidate = interop.candidateToPlanB(iceCandidate)
|
||||
} else {
|
||||
candidate = new RTCIceCandidate(iceCandidate)
|
||||
}
|
||||
|
||||
logger.debug('Remote ICE candidate received', iceCandidate)
|
||||
callback = (callback || noop).bind(this)
|
||||
addIceCandidate(candidate, callback)
|
||||
}
|
||||
|
||||
this.generateOffer = function (callback) {
|
||||
callback = callback.bind(this)
|
||||
|
||||
var offerAudio = true
|
||||
var offerVideo = true
|
||||
// Constraints must have both blocks
|
||||
if (mediaConstraints) {
|
||||
offerAudio = (typeof mediaConstraints.audio === 'boolean') ?
|
||||
mediaConstraints.audio : true
|
||||
offerVideo = (typeof mediaConstraints.video === 'boolean') ?
|
||||
mediaConstraints.video : true
|
||||
}
|
||||
|
||||
var browserDependantConstraints = {
|
||||
offerToReceiveAudio: (mode !== 'sendonly' && offerAudio),
|
||||
offerToReceiveVideo: (mode !== 'sendonly' && offerVideo)
|
||||
}
|
||||
|
||||
//FIXME: clarify possible constraints passed to createOffer()
|
||||
/*var constraints = recursive(browserDependantConstraints,
|
||||
connectionConstraints)*/
|
||||
|
||||
var constraints = browserDependantConstraints;
|
||||
|
||||
logger.debug('constraints: ' + JSON.stringify(constraints))
|
||||
|
||||
pc.createOffer(constraints).then(function (offer) {
|
||||
logger.debug('Created SDP offer')
|
||||
offer = mangleSdpToAddSimulcast(offer)
|
||||
return pc.setLocalDescription(offer)
|
||||
}).then(function () {
|
||||
var localDescription = pc.localDescription
|
||||
logger.debug('Local description set', localDescription.sdp)
|
||||
if (multistream && usePlanB) {
|
||||
localDescription = interop.toUnifiedPlan(localDescription)
|
||||
logger.debug('offer::origPlanB->UnifiedPlan', dumpSDP(
|
||||
localDescription))
|
||||
}
|
||||
callback(null, localDescription.sdp, self.processAnswer.bind(
|
||||
self))
|
||||
}).catch(callback)
|
||||
}
|
||||
|
||||
this.getLocalSessionDescriptor = function () {
|
||||
return pc.localDescription
|
||||
}
|
||||
|
||||
this.getRemoteSessionDescriptor = function () {
|
||||
return pc.remoteDescription
|
||||
}
|
||||
|
||||
function setRemoteVideo() {
|
||||
if (remoteVideo) {
|
||||
var stream = pc.getRemoteStreams()[0]
|
||||
var url = stream ? URL.createObjectURL(stream) : ''
|
||||
|
||||
remoteVideo.pause()
|
||||
remoteVideo.src = url
|
||||
remoteVideo.load()
|
||||
|
||||
logger.debug('Remote URL:', url)
|
||||
}
|
||||
}
|
||||
|
||||
this.showLocalVideo = function () {
|
||||
localVideo.src = URL.createObjectURL(videoStream)
|
||||
localVideo.muted = true
|
||||
}
|
||||
|
||||
this.send = function (data) {
|
||||
if (dataChannel && dataChannel.readyState === 'open') {
|
||||
dataChannel.send(data)
|
||||
} else {
|
||||
logger.warn(
|
||||
'Trying to send data over a non-existing or closed data channel')
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Callback function invoked when a SDP answer is received. Developers are
|
||||
* expected to invoke this function in order to complete the SDP negotiation.
|
||||
*
|
||||
* @function module:kurentoUtils.WebRtcPeer.prototype.processAnswer
|
||||
*
|
||||
* @param sdpAnswer - Description of sdpAnswer
|
||||
* @param callback -
|
||||
* Invoked after the SDP answer is processed, or there is an error.
|
||||
*/
|
||||
this.processAnswer = function (sdpAnswer, callback) {
|
||||
callback = (callback || noop).bind(this)
|
||||
|
||||
var answer = new RTCSessionDescription({
|
||||
type: 'answer',
|
||||
sdp: sdpAnswer
|
||||
})
|
||||
|
||||
if (multistream && usePlanB) {
|
||||
var planBAnswer = interop.toPlanB(answer)
|
||||
logger.debug('asnwer::planB', dumpSDP(planBAnswer))
|
||||
answer = planBAnswer
|
||||
}
|
||||
|
||||
logger.debug('SDP answer received, setting remote description')
|
||||
|
||||
if (pc.signalingState === 'closed') {
|
||||
return callback('PeerConnection is closed')
|
||||
}
|
||||
|
||||
pc.setRemoteDescription(answer, function () {
|
||||
setRemoteVideo()
|
||||
|
||||
callback()
|
||||
},
|
||||
callback)
|
||||
}
|
||||
|
||||
/**
|
||||
* Callback function invoked when a SDP offer is received. Developers are
|
||||
* expected to invoke this function in order to complete the SDP negotiation.
|
||||
*
|
||||
* @function module:kurentoUtils.WebRtcPeer.prototype.processOffer
|
||||
*
|
||||
* @param sdpOffer - Description of sdpOffer
|
||||
* @param callback - Called when the remote description has been set
|
||||
* successfully.
|
||||
*/
|
||||
this.processOffer = function (sdpOffer, callback) {
|
||||
callback = callback.bind(this)
|
||||
|
||||
var offer = new RTCSessionDescription({
|
||||
type: 'offer',
|
||||
sdp: sdpOffer
|
||||
})
|
||||
|
||||
if (multistream && usePlanB) {
|
||||
var planBOffer = interop.toPlanB(offer)
|
||||
logger.debug('offer::planB', dumpSDP(planBOffer))
|
||||
offer = planBOffer
|
||||
}
|
||||
|
||||
logger.debug('SDP offer received, setting remote description')
|
||||
|
||||
if (pc.signalingState === 'closed') {
|
||||
return callback('PeerConnection is closed')
|
||||
}
|
||||
|
||||
pc.setRemoteDescription(offer).then(function () {
|
||||
return setRemoteVideo()
|
||||
}).then(function () {
|
||||
return pc.createAnswer()
|
||||
}).then(function (answer) {
|
||||
answer = mangleSdpToAddSimulcast(answer)
|
||||
logger.debug('Created SDP answer')
|
||||
return pc.setLocalDescription(answer)
|
||||
}).then(function () {
|
||||
var localDescription = pc.localDescription
|
||||
if (multistream && usePlanB) {
|
||||
localDescription = interop.toUnifiedPlan(localDescription)
|
||||
logger.debug('answer::origPlanB->UnifiedPlan', dumpSDP(
|
||||
localDescription))
|
||||
}
|
||||
logger.debug('Local description set', localDescription.sdp)
|
||||
callback(null, localDescription.sdp)
|
||||
}).catch(callback)
|
||||
}
|
||||
|
||||
function mangleSdpToAddSimulcast(answer) {
|
||||
if (simulcast) {
|
||||
if (browser.name === 'Chrome' || browser.name === 'Chromium') {
|
||||
logger.debug('Adding multicast info')
|
||||
answer = new RTCSessionDescription({
|
||||
'type': answer.type,
|
||||
'sdp': removeFIDFromOffer(answer.sdp) + getSimulcastInfo(
|
||||
videoStream)
|
||||
})
|
||||
} else {
|
||||
logger.warn('Simulcast is only available in Chrome browser.')
|
||||
}
|
||||
}
|
||||
|
||||
return answer
|
||||
}
|
||||
|
||||
/**
|
||||
* This function creates the RTCPeerConnection object taking into account the
|
||||
* properties received in the constructor. It starts the SDP negotiation
|
||||
* process: generates the SDP offer and invokes the onsdpoffer callback. This
|
||||
* callback is expected to send the SDP offer, in order to obtain an SDP
|
||||
* answer from another peer.
|
||||
*/
|
||||
function start() {
|
||||
if (pc.signalingState === 'closed') {
|
||||
callback(
|
||||
'The peer connection object is in "closed" state. This is most likely due to an invocation of the dispose method before accepting in the dialogue'
|
||||
)
|
||||
}
|
||||
|
||||
if (videoStream && localVideo) {
|
||||
self.showLocalVideo()
|
||||
}
|
||||
|
||||
if (videoStream) {
|
||||
pc.addStream(videoStream)
|
||||
}
|
||||
|
||||
if (audioStream) {
|
||||
pc.addStream(audioStream)
|
||||
}
|
||||
|
||||
// [Hack] https://code.google.com/p/chromium/issues/detail?id=443558
|
||||
var browser = parser.getBrowser()
|
||||
if (mode === 'sendonly' &&
|
||||
(browser.name === 'Chrome' || browser.name === 'Chromium') &&
|
||||
browser.major === 39) {
|
||||
mode = 'sendrecv'
|
||||
}
|
||||
|
||||
callback()
|
||||
}
|
||||
|
||||
if (mode !== 'recvonly' && !videoStream && !audioStream) {
|
||||
function getMedia(constraints) {
|
||||
if (constraints === undefined) {
|
||||
constraints = MEDIA_CONSTRAINTS
|
||||
}
|
||||
|
||||
navigator.mediaDevices.getUserMedia(constraints).then(function (stream) {
|
||||
videoStream = stream
|
||||
start()
|
||||
}).catch(callback);
|
||||
}
|
||||
if (sendSource === 'webcam') {
|
||||
getMedia(mediaConstraints)
|
||||
} else {
|
||||
getScreenConstraints(sendSource, function (error, constraints_) {
|
||||
if (error)
|
||||
return callback(error)
|
||||
|
||||
constraints = [mediaConstraints]
|
||||
constraints.unshift(constraints_)
|
||||
getMedia(recursive.apply(undefined, constraints))
|
||||
}, guid)
|
||||
}
|
||||
} else {
|
||||
setTimeout(start, 0)
|
||||
}
|
||||
|
||||
this.on('_dispose', function () {
|
||||
if (localVideo) {
|
||||
localVideo.pause()
|
||||
localVideo.src = ''
|
||||
localVideo.load()
|
||||
//Unmute local video in case the video tag is later used for remote video
|
||||
localVideo.muted = false
|
||||
}
|
||||
if (remoteVideo) {
|
||||
remoteVideo.pause()
|
||||
remoteVideo.src = ''
|
||||
remoteVideo.load()
|
||||
}
|
||||
self.removeAllListeners()
|
||||
|
||||
if (window.cancelChooseDesktopMedia !== undefined) {
|
||||
window.cancelChooseDesktopMedia(guid)
|
||||
}
|
||||
})
|
||||
}
|
||||
inherits(WebRtcPeer, EventEmitter)
|
||||
|
||||
function createEnableDescriptor(type) {
|
||||
var method = 'get' + type + 'Tracks'
|
||||
|
||||
return {
|
||||
enumerable: true,
|
||||
get: function () {
|
||||
// [ToDo] Should return undefined if not all tracks have the same value?
|
||||
|
||||
if (!this.peerConnection) return
|
||||
|
||||
var streams = this.peerConnection.getLocalStreams()
|
||||
if (!streams.length) return
|
||||
|
||||
for (var i = 0, stream; stream = streams[i]; i++) {
|
||||
var tracks = stream[method]()
|
||||
for (var j = 0, track; track = tracks[j]; j++)
|
||||
if (!track.enabled) return false
|
||||
}
|
||||
|
||||
return true
|
||||
},
|
||||
set: function (value) {
|
||||
function trackSetEnable(track) {
|
||||
track.enabled = value
|
||||
}
|
||||
|
||||
this.peerConnection.getLocalStreams().forEach(function (stream) {
|
||||
stream[method]().forEach(trackSetEnable)
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Object.defineProperties(WebRtcPeer.prototype, {
|
||||
'enabled': {
|
||||
enumerable: true,
|
||||
get: function () {
|
||||
return this.audioEnabled && this.videoEnabled
|
||||
},
|
||||
set: function (value) {
|
||||
this.audioEnabled = this.videoEnabled = value
|
||||
}
|
||||
},
|
||||
'audioEnabled': createEnableDescriptor('Audio'),
|
||||
'videoEnabled': createEnableDescriptor('Video')
|
||||
})
|
||||
|
||||
WebRtcPeer.prototype.getLocalStream = function (index) {
|
||||
if (this.peerConnection) {
|
||||
return this.peerConnection.getLocalStreams()[index || 0]
|
||||
}
|
||||
}
|
||||
|
||||
WebRtcPeer.prototype.getRemoteStream = function (index) {
|
||||
if (this.peerConnection) {
|
||||
return this.peerConnection.getRemoteStreams()[index || 0]
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @description This method frees the resources used by WebRtcPeer.
|
||||
*
|
||||
* @function module:kurentoUtils.WebRtcPeer.prototype.dispose
|
||||
*/
|
||||
WebRtcPeer.prototype.dispose = function () {
|
||||
logger.debug('Disposing WebRtcPeer')
|
||||
|
||||
var pc = this.peerConnection
|
||||
var dc = this.dataChannel
|
||||
try {
|
||||
if (dc) {
|
||||
if (dc.signalingState === 'closed') return
|
||||
|
||||
dc.close()
|
||||
}
|
||||
|
||||
if (pc) {
|
||||
if (pc.signalingState === 'closed') return
|
||||
|
||||
pc.getLocalStreams().forEach(streamStop)
|
||||
|
||||
// FIXME This is not yet implemented in firefox
|
||||
// if(videoStream) pc.removeStream(videoStream);
|
||||
// if(audioStream) pc.removeStream(audioStream);
|
||||
|
||||
pc.close()
|
||||
}
|
||||
} catch (err) {
|
||||
logger.warn('Exception disposing webrtc peer ' + err)
|
||||
}
|
||||
|
||||
this.emit('_dispose')
|
||||
}
|
||||
|
||||
//
|
||||
// Specialized child classes
|
||||
//
|
||||
|
||||
function WebRtcPeerRecvonly(options, callback) {
|
||||
if (!(this instanceof WebRtcPeerRecvonly)) {
|
||||
return new WebRtcPeerRecvonly(options, callback)
|
||||
}
|
||||
|
||||
WebRtcPeerRecvonly.super_.call(this, 'recvonly', options, callback)
|
||||
}
|
||||
inherits(WebRtcPeerRecvonly, WebRtcPeer)
|
||||
|
||||
function WebRtcPeerSendonly(options, callback) {
|
||||
if (!(this instanceof WebRtcPeerSendonly)) {
|
||||
return new WebRtcPeerSendonly(options, callback)
|
||||
}
|
||||
|
||||
WebRtcPeerSendonly.super_.call(this, 'sendonly', options, callback)
|
||||
}
|
||||
inherits(WebRtcPeerSendonly, WebRtcPeer)
|
||||
|
||||
function WebRtcPeerSendrecv(options, callback) {
|
||||
if (!(this instanceof WebRtcPeerSendrecv)) {
|
||||
return new WebRtcPeerSendrecv(options, callback)
|
||||
}
|
||||
|
||||
WebRtcPeerSendrecv.super_.call(this, 'sendrecv', options, callback)
|
||||
}
|
||||
inherits(WebRtcPeerSendrecv, WebRtcPeer)
|
||||
|
||||
function harkUtils(stream, options) {
|
||||
return hark(stream, options);
|
||||
}
|
||||
|
||||
exports.bufferizeCandidates = bufferizeCandidates
|
||||
|
||||
exports.WebRtcPeerRecvonly = WebRtcPeerRecvonly
|
||||
exports.WebRtcPeerSendonly = WebRtcPeerSendonly
|
||||
exports.WebRtcPeerSendrecv = WebRtcPeerSendrecv
|
||||
exports.hark = harkUtils
|
|
@ -0,0 +1,18 @@
|
|||
/*
|
||||
* (C) Copyright 2015 Kurento (http://kurento.org/)
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
// Don't run on Internet Explorer 8, so exit inmediatly
|
||||
if (window.addEventListener) module.exports = require('./index');
|
|
@ -0,0 +1,30 @@
|
|||
/*
|
||||
* (C) Copyright 2014 Kurento (http://kurento.org/)
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*
|
||||
*/
|
||||
|
||||
/**
|
||||
* This module contains a set of reusable components that have been found useful
|
||||
* during the development of the WebRTC applications with Kurento.
|
||||
*
|
||||
* @module kurentoUtils
|
||||
*
|
||||
* @copyright 2014 Kurento (http://kurento.org/)
|
||||
* @license ALv2
|
||||
*/
|
||||
|
||||
var WebRtcPeer = require('./WebRtcPeer');
|
||||
|
||||
exports.WebRtcPeer = WebRtcPeer;
|
|
@ -31,6 +31,7 @@ export class OpenVidu {
|
|||
|
||||
constructor() {
|
||||
this.openVidu = new OpenViduInternal();
|
||||
console.info("'OpenVidu' initialized");
|
||||
};
|
||||
|
||||
initSession(apiKey: string, sessionId: string): Session;
|
||||
|
@ -73,8 +74,10 @@ export class OpenVidu {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
return new Publisher(this.openVidu.initPublisherTagged(parentId, cameraOptions, callback), parentId);
|
||||
var publisher = new Publisher(this.openVidu.initPublisherTagged(parentId, cameraOptions, callback), parentId);
|
||||
|
||||
console.info("'Publisher' initialized");
|
||||
return publisher;
|
||||
|
||||
} else {
|
||||
alert("Browser not supported");
|
||||
|
@ -100,9 +103,16 @@ export class OpenVidu {
|
|||
navigator.mediaDevices.enumerateDevices().then((deviceInfos) => {
|
||||
callback(null, deviceInfos);
|
||||
}).catch((error) => {
|
||||
console.log("Error getting devices: " + error);
|
||||
console.error("Error getting devices", error);
|
||||
callback(error, null);
|
||||
});
|
||||
}
|
||||
|
||||
enableProdMode() {
|
||||
console.log = function() {};
|
||||
console.debug = function() {};
|
||||
console.info = function() {};
|
||||
console.warn = function() {};
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -52,8 +52,17 @@ export class Publisher {
|
|||
return this;
|
||||
}
|
||||
|
||||
subscribeToRemote() {
|
||||
this.stream.subscribeToMyRemote();
|
||||
}
|
||||
|
||||
on(eventName: string, callback) {
|
||||
this.ee.addListener(eventName, event => {
|
||||
if (event) {
|
||||
console.info("Event '" + eventName + "' triggered by 'Publisher'", event);
|
||||
} else {
|
||||
console.info("Event '" + eventName + "' triggered by 'Publisher'");
|
||||
}
|
||||
callback(event);
|
||||
});
|
||||
if (eventName == 'videoElementCreated') {
|
||||
|
@ -62,8 +71,7 @@ export class Publisher {
|
|||
element: this.stream.getVideoElement()
|
||||
}]);
|
||||
} else {
|
||||
this.stream.addEventListener('video-element-created-by-stream', (element) => {
|
||||
console.warn('Publisher emitting videoElementCreated');
|
||||
this.stream.addOnceEventListener('video-element-created-by-stream', (element) => {
|
||||
this.id = element.id;
|
||||
this.ee.emitEvent('videoElementCreated', [{
|
||||
element: element.element
|
||||
|
@ -71,6 +79,42 @@ export class Publisher {
|
|||
});
|
||||
}
|
||||
}
|
||||
if (eventName == 'videoPlaying') {
|
||||
var video = this.stream.getVideoElement();
|
||||
if (!this.stream.displayMyRemote() && video &&
|
||||
video.currentTime > 0 &&
|
||||
video.paused == false &&
|
||||
video.ended == false &&
|
||||
video.readyState == 4) {
|
||||
this.ee.emitEvent('videoPlaying', [{
|
||||
element: this.stream.getVideoElement()
|
||||
}]);
|
||||
} else {
|
||||
this.stream.addOnceEventListener('video-is-playing', (element) => {
|
||||
this.ee.emitEvent('videoPlaying', [{
|
||||
element: element.element
|
||||
}]);
|
||||
});
|
||||
}
|
||||
}
|
||||
if (eventName == 'remoteVideoPlaying') {
|
||||
var video = this.stream.getVideoElement();
|
||||
if (this.stream.displayMyRemote() && video &&
|
||||
video.currentTime > 0 &&
|
||||
video.paused == false &&
|
||||
video.ended == false &&
|
||||
video.readyState == 4) {
|
||||
this.ee.emitEvent('remoteVideoPlaying', [{
|
||||
element: this.stream.getVideoElement()
|
||||
}]);
|
||||
} else {
|
||||
this.stream.addOnceEventListener('remote-video-is-playing', (element) => {
|
||||
this.ee.emitEvent('remoteVideoPlaying', [{
|
||||
element: element.element
|
||||
}]);
|
||||
});
|
||||
}
|
||||
}
|
||||
if (eventName == 'streamCreated') {
|
||||
if (this.stream.isReady) {
|
||||
this.ee.emitEvent('streamCreated', [{ stream: this.stream }]);
|
||||
|
|
|
@ -86,6 +86,11 @@ export class Session {
|
|||
|
||||
on(eventName: string, callback) {
|
||||
this.session.addEventListener(eventName, event => {
|
||||
if (event) {
|
||||
console.info("Event '" + eventName + "' triggered by 'Session'", event);
|
||||
} else {
|
||||
console.info("Event '" + eventName + "' triggered by 'Session'");
|
||||
}
|
||||
callback(event);
|
||||
});
|
||||
}
|
||||
|
|
|
@ -19,6 +19,11 @@ export class Subscriber {
|
|||
|
||||
on(eventName: string, callback) {
|
||||
this.ee.addListener(eventName, event => {
|
||||
if (event) {
|
||||
console.info("Event '" + eventName + "' triggered by 'Subscriber'", event);
|
||||
} else {
|
||||
console.info("Event '" + eventName + "' triggered by 'Subscriber'");
|
||||
}
|
||||
callback(event);
|
||||
});
|
||||
if (eventName == 'videoElementCreated') {
|
||||
|
@ -27,7 +32,7 @@ export class Subscriber {
|
|||
element: this.stream.getVideoElement()
|
||||
}]);
|
||||
} else {
|
||||
this.stream.addEventListener('video-element-created-by-stream', element => {
|
||||
this.stream.addOnceEventListener('video-element-created-by-stream', element => {
|
||||
console.warn("Subscriber emitting videoElementCreated");
|
||||
this.id = element.id;
|
||||
this.ee.emitEvent('videoElementCreated', [{
|
||||
|
@ -36,5 +41,23 @@ export class Subscriber {
|
|||
});
|
||||
}
|
||||
}
|
||||
if (eventName == 'videoPlaying') {
|
||||
var video = this.stream.getVideoElement();
|
||||
if (!this.stream.displayMyRemote() && video &&
|
||||
video.currentTime > 0 &&
|
||||
video.paused == false &&
|
||||
video.ended == false &&
|
||||
video.readyState == 4) {
|
||||
this.ee.emitEvent('videoPlaying', [{
|
||||
element: this.stream.getVideoElement()
|
||||
}]);
|
||||
} else {
|
||||
this.stream.addOnceEventListener('video-is-playing', (element) => {
|
||||
this.ee.emitEvent('videoPlaying', [{
|
||||
element: element.element
|
||||
}]);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"declaration": true,
|
||||
"allowJs": true,
|
||||
"target": "es5",
|
||||
"module": "commonjs",
|
||||
//"noImplicitAny": true,
|
||||
|
|
|
@ -21,36 +21,18 @@ export class Connection {
|
|||
|
||||
constructor( private openVidu: OpenViduInternal, private local: boolean, private room: SessionInternal, private options?: ConnectionOptions ) {
|
||||
|
||||
console.info( "'Connection' created (" + ( local ? "local" : "remote" ) + ")" + ( local ? "" : ", with 'connectionId' [" + (options ? options.id : '') + "] " ));
|
||||
|
||||
if ( options ) {
|
||||
|
||||
this.connectionId = options.id;
|
||||
this.data = options.metadata;
|
||||
|
||||
if ( options.streams ) {
|
||||
|
||||
for ( let streamOptions of options.streams ) {
|
||||
|
||||
let streamOpts = {
|
||||
id: streamOptions.id,
|
||||
connection: this,
|
||||
recvVideo: ( streamOptions.recvVideo == undefined ? true : streamOptions.recvVideo ),
|
||||
recvAudio: ( streamOptions.recvAudio == undefined ? true : streamOptions.recvAudio ),
|
||||
audio: streamOptions.audio,
|
||||
video: streamOptions.video,
|
||||
data: streamOptions.data,
|
||||
mediaConstraints: streamOptions.mediaConstraints,
|
||||
audioOnly: streamOptions.audioOnly
|
||||
}
|
||||
let stream = new Stream( openVidu, false, room, streamOpts );
|
||||
|
||||
this.addStream( stream );
|
||||
this.streamsOpts.push( streamOpts );
|
||||
}
|
||||
this.initStreams(options);
|
||||
}
|
||||
}
|
||||
|
||||
console.log( "New " + ( local ? "local " : "remote " ) + "participant " + this.connectionId
|
||||
+ ", streams opts: ", this.streamsOpts );
|
||||
}
|
||||
|
||||
addStream( stream: Stream ) {
|
||||
|
@ -85,4 +67,27 @@ export class Connection {
|
|||
}
|
||||
});
|
||||
}
|
||||
|
||||
initStreams(options) {
|
||||
for ( let streamOptions of options.streams ) {
|
||||
|
||||
let streamOpts = {
|
||||
id: streamOptions.id,
|
||||
connection: this,
|
||||
recvVideo: ( streamOptions.recvVideo == undefined ? true : streamOptions.recvVideo ),
|
||||
recvAudio: ( streamOptions.recvAudio == undefined ? true : streamOptions.recvAudio ),
|
||||
audio: streamOptions.audio,
|
||||
video: streamOptions.video,
|
||||
data: streamOptions.data,
|
||||
mediaConstraints: streamOptions.mediaConstraints,
|
||||
audioOnly: streamOptions.audioOnly,
|
||||
}
|
||||
let stream = new Stream(this.openVidu, false, this.room, streamOpts );
|
||||
|
||||
this.addStream( stream );
|
||||
this.streamsOpts.push( streamOpts );
|
||||
}
|
||||
|
||||
console.info("Remote 'Connection' with 'connectionId' [" + this.connectionId + "] is now configured for receiving Streams with options: ", this.streamsOpts );
|
||||
}
|
||||
}
|
|
@ -16,7 +16,7 @@
|
|||
*/
|
||||
import { SessionInternal, SessionOptions } from './SessionInternal';
|
||||
import { Stream } from './Stream';
|
||||
import * as RpcBuilder from 'kurento-jsonrpc';
|
||||
import * as RpcBuilder from '../KurentoUtils/kurento-jsonrpc';
|
||||
|
||||
export type Callback<T> = (error?: any, openVidu?: T) => void;
|
||||
|
||||
|
@ -37,20 +37,19 @@ export class OpenViduInternal {
|
|||
|
||||
/* NEW METHODS */
|
||||
initSession(sessionId) {
|
||||
console.log("Session initialized!");
|
||||
console.info("'Session' initialized with 'sessionId' [" + sessionId + "]");
|
||||
this.session = new SessionInternal(this, sessionId);
|
||||
return this.session;
|
||||
}
|
||||
|
||||
initPublisherTagged(parentId: string, cameraOptions: any, callback?) {
|
||||
console.log("Publisher tagged initialized!");
|
||||
|
||||
this.getCamera(cameraOptions);
|
||||
|
||||
if (callback == null) {
|
||||
this.camera.requestCameraAccess((error, camera) => {
|
||||
if (error) {
|
||||
console.log("Error accessing the camera");
|
||||
console.error("Error accessing the camera", error);
|
||||
}
|
||||
else {
|
||||
this.camera.setVideoElement(this.cameraReady(camera!, parentId));
|
||||
|
@ -79,8 +78,6 @@ export class OpenViduInternal {
|
|||
}
|
||||
|
||||
initPublisher(cameraOptions: any, callback) {
|
||||
console.log("Publisher initialized!");
|
||||
|
||||
this.getCamera(cameraOptions);
|
||||
this.camera.requestCameraAccess((error, camera) => {
|
||||
if (error) callback(error);
|
||||
|
@ -184,7 +181,7 @@ export class OpenViduInternal {
|
|||
}
|
||||
|
||||
private disconnectCallback() {
|
||||
console.log('Websocket connection lost');
|
||||
console.warn('Websocket connection lost');
|
||||
if (this.isRoomAvailable()) {
|
||||
this.session.onLostConnection();
|
||||
} else {
|
||||
|
@ -193,7 +190,7 @@ export class OpenViduInternal {
|
|||
}
|
||||
|
||||
private reconnectingCallback() {
|
||||
console.log('Websocket connection lost (reconnecting)');
|
||||
console.warn('Websocket connection lost (reconnecting)');
|
||||
if (this.isRoomAvailable()) {
|
||||
this.session.onLostConnection();
|
||||
} else {
|
||||
|
@ -202,7 +199,7 @@ export class OpenViduInternal {
|
|||
}
|
||||
|
||||
private reconnectedCallback() {
|
||||
console.log('Websocket reconnected');
|
||||
console.warn('Websocket reconnected');
|
||||
}
|
||||
|
||||
private onParticipantJoined(params) {
|
||||
|
@ -271,12 +268,12 @@ export class OpenViduInternal {
|
|||
for (let index in this.rpcParams) {
|
||||
if (this.rpcParams.hasOwnProperty(index)) {
|
||||
params[index] = this.rpcParams[index];
|
||||
console.log('RPC param added to request {' + index + ': ' + this.rpcParams[index] + '}');
|
||||
console.debug('RPC param added to request {' + index + ': ' + this.rpcParams[index] + '}');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
console.log('Sending request: { method:"' + method + '", params: ' + JSON.stringify(params) + ' }');
|
||||
console.debug('Sending request: {method:"' + method + '", params: ' + JSON.stringify(params) + '}');
|
||||
|
||||
this.jsonRpcClient.send(method, params, callback);
|
||||
}
|
||||
|
|
|
@ -237,13 +237,13 @@ export class SessionInternal {
|
|||
}
|
||||
|
||||
unsuscribe(stream) {
|
||||
console.log("Unsubscribing from " + stream.getId());
|
||||
console.info("Unsubscribing from " + stream.getId());
|
||||
this.openVidu.sendRequest('unsubscribeFromVideo', {
|
||||
sender: stream.getId()
|
||||
},
|
||||
function (error, response) {
|
||||
if (error) {
|
||||
console.error(error);
|
||||
console.error("Error unsubscribing from Subscriber", error);
|
||||
} else {
|
||||
console.info("Unsubscribed correctly from " + stream.getId());
|
||||
}
|
||||
|
@ -254,15 +254,25 @@ export class SessionInternal {
|
|||
|
||||
options.metadata = this.participants[options.id].data;
|
||||
|
||||
let connection = new Connection(this.openVidu, false, this, options);
|
||||
// Get the existing Connection created on 'onParticipantJoined' for
|
||||
// existing participants or create a new one for new participants
|
||||
let connection = this.participants[options.id];
|
||||
if (connection) {
|
||||
// Update existing Connection
|
||||
connection.options = options;
|
||||
connection.initStreams(options);
|
||||
} else {
|
||||
// Create new Connection
|
||||
connection = new Connection(this.openVidu, false, this, options);
|
||||
}
|
||||
|
||||
let pid = connection.connectionId;
|
||||
if (!(pid in this.participants)) {
|
||||
console.info("Publisher not found in participants list by its id", pid);
|
||||
console.debug("Remote Connection not found in connections list by its id [" + pid + "]");
|
||||
} else {
|
||||
console.log("Publisher found in participants list by its id", pid);
|
||||
console.debug("Remote Connection found in connections list by its id [" + pid + "]");
|
||||
}
|
||||
//replacing old connection (this one has streams)
|
||||
|
||||
connection.creationTime = this.participants[pid].creationTime;
|
||||
this.participants[pid] = connection;
|
||||
|
||||
|
@ -288,12 +298,11 @@ export class SessionInternal {
|
|||
|
||||
let pid = connection.connectionId;
|
||||
if (!(pid in this.participants)) {
|
||||
console.log("New participant to participants list with id", pid);
|
||||
this.participants[pid] = connection;
|
||||
} else {
|
||||
//use existing so that we don't lose streams info
|
||||
console.info("Participant already exists in participants list with " +
|
||||
"the same id, old:", this.participants[pid], ", joined now:", connection);
|
||||
console.warn("Connection already exists in connections list with " +
|
||||
"the same connectionId, old:", this.participants[pid], ", joined now:", connection);
|
||||
connection = this.participants[pid];
|
||||
}
|
||||
|
||||
|
@ -354,7 +363,7 @@ export class SessionInternal {
|
|||
|
||||
onNewMessage(msg) {
|
||||
|
||||
console.log("New message: " + JSON.stringify(msg));
|
||||
console.info("New message: " + JSON.stringify(msg));
|
||||
let room = msg.room;
|
||||
let user = msg.user;
|
||||
let message = msg.message;
|
||||
|
@ -401,7 +410,7 @@ export class SessionInternal {
|
|||
|
||||
onRoomClosed(msg) {
|
||||
|
||||
console.log("Room closed: " + JSON.stringify(msg));
|
||||
console.info("Room closed: " + JSON.stringify(msg));
|
||||
let room = msg.room;
|
||||
if (room !== undefined) {
|
||||
this.ee.emitEvent('room-closed', [{
|
||||
|
@ -422,7 +431,7 @@ export class SessionInternal {
|
|||
return;
|
||||
}
|
||||
|
||||
console.log('Lost connection in room ' + this.id);
|
||||
console.warn('Lost connection in Session ' + this.id);
|
||||
let room = this.id;
|
||||
if (room !== undefined) {
|
||||
this.ee.emitEvent('lost-connection', [{ room }]);
|
||||
|
@ -451,7 +460,7 @@ export class SessionInternal {
|
|||
|
||||
forced = !!forced;
|
||||
|
||||
console.log("Leaving room (forced=" + forced + ")");
|
||||
console.info("Leaving Session (forced=" + forced + ")");
|
||||
|
||||
if (this.connected && !forced) {
|
||||
this.openVidu.sendRequest('leaveRoom', function (error, response) {
|
||||
|
@ -485,7 +494,7 @@ export class SessionInternal {
|
|||
|
||||
if (connection === this.localParticipant) {
|
||||
|
||||
console.log("Unpublishing my media (I'm " + connection.connectionId + ")");
|
||||
console.info("Unpublishing my media (I'm " + connection.connectionId + ")");
|
||||
delete this.localParticipant;
|
||||
this.openVidu.sendRequest('unpublishVideo', function (error, response) {
|
||||
if (error) {
|
||||
|
@ -513,7 +522,7 @@ export class SessionInternal {
|
|||
delete this.participants[connection.connectionId];
|
||||
connection.dispose();
|
||||
|
||||
console.log("Unpublishing my media (I'm " + connection.connectionId + ")");
|
||||
console.info("Unpublishing my media (I'm " + connection.connectionId + ")");
|
||||
delete this.localParticipant;
|
||||
this.openVidu.sendRequest('unpublishVideo', function (error, response) {
|
||||
if (error) {
|
||||
|
|
|
@ -9,7 +9,7 @@ import { Connection } from './Connection';
|
|||
import { SessionInternal } from './SessionInternal';
|
||||
import { OpenViduInternal, Callback } from './OpenViduInternal';
|
||||
import EventEmitter = require('wolfy87-eventemitter');
|
||||
import * as kurentoUtils from 'kurento-utils';
|
||||
import * as kurentoUtils from '../KurentoUtils/kurento-utils-js';
|
||||
|
||||
import * as adapter from 'webrtc-adapter';
|
||||
declare var navigator: any;
|
||||
|
@ -73,7 +73,7 @@ export class Stream {
|
|||
|
||||
private audioOnly = false;
|
||||
|
||||
private videoSrc: string;
|
||||
private videoSrcObject: MediaStream | null;
|
||||
private parentId: string;
|
||||
public isReady: boolean = false;
|
||||
public isVideoELementCreated: boolean = false;
|
||||
|
@ -98,15 +98,15 @@ export class Stream {
|
|||
this.audioOnly = options.audioOnly || false;
|
||||
|
||||
this.addEventListener('src-added', (srcEvent) => {
|
||||
this.videoSrc = srcEvent.src;
|
||||
if (this.video) this.video.src = srcEvent.src;
|
||||
console.warn("Videosrc [" + srcEvent.src + "] added to stream [" + this.getId() + "]");
|
||||
this.videoSrcObject = srcEvent.srcObject;
|
||||
if (this.video) this.video.srcObject = srcEvent.srcObject;
|
||||
console.debug("Video srcObject [" + srcEvent.srcObject + "] added to stream [" + this.getId() + "]");
|
||||
});
|
||||
}
|
||||
|
||||
emitSrcEvent(wrstream) {
|
||||
this.ee.emitEvent('src-added', [{
|
||||
src: URL.createObjectURL(wrstream)
|
||||
srcObject: wrstream
|
||||
}]);
|
||||
}
|
||||
|
||||
|
@ -114,8 +114,8 @@ export class Stream {
|
|||
this.ee.emitEvent('stream-ready'), [{}];
|
||||
}
|
||||
|
||||
getVideoSrc() {
|
||||
return this.videoSrc;
|
||||
getVideoSrcObject() {
|
||||
return this.videoSrcObject;
|
||||
}
|
||||
|
||||
removeVideo(parentElement: string);
|
||||
|
@ -193,12 +193,12 @@ export class Stream {
|
|||
}
|
||||
|
||||
onDataChannelOpen(event) {
|
||||
console.log('Data channel is opened');
|
||||
console.debug('Data channel is opened');
|
||||
this.dataChannelOpened = true;
|
||||
}
|
||||
|
||||
onDataChannelClosed(event) {
|
||||
console.log('Data channel is closed');
|
||||
console.debug('Data channel is closed');
|
||||
this.dataChannelOpened = false;
|
||||
}
|
||||
|
||||
|
@ -209,7 +209,7 @@ export class Stream {
|
|||
if (!this.dataChannelOpened) {
|
||||
throw new Error('Data channel is not opened');
|
||||
}
|
||||
console.log("Sending through data channel: " + data);
|
||||
console.info("Sending through data channel: " + data);
|
||||
this.wp.send(data);
|
||||
}
|
||||
|
||||
|
@ -254,18 +254,24 @@ export class Stream {
|
|||
|
||||
this.video = document.createElement('video');
|
||||
|
||||
this.video.id = 'native-video-' + this.getId();
|
||||
this.video.id = (this.local ? 'local-' : 'remote-') + 'video-' + this.getId();
|
||||
this.video.autoplay = true;
|
||||
this.video.controls = false;
|
||||
this.video.src = this.videoSrc;
|
||||
this.video.srcObject = this.videoSrcObject;
|
||||
|
||||
this.videoElements.push({
|
||||
thumb: thumbnailId,
|
||||
video: this.video
|
||||
});
|
||||
|
||||
if (this.local) {
|
||||
if (this.local && !this.displayMyRemote()) {
|
||||
this.video.muted = true;
|
||||
this.video.onplay = () => {
|
||||
console.info("Local 'Stream' with id [" + this.getId() + "] video is now playing");
|
||||
this.ee.emitEvent('video-is-playing', [{
|
||||
element: this.video
|
||||
}]);
|
||||
};
|
||||
} else {
|
||||
this.video.title = this.getId();
|
||||
}
|
||||
|
@ -330,11 +336,7 @@ export class Stream {
|
|||
}
|
||||
|
||||
getId() {
|
||||
if (this.connection) {
|
||||
return this.connection.connectionId + "_" + this.id;
|
||||
} else {
|
||||
return this.id + "_webcam";
|
||||
}
|
||||
return this.connection.connectionId + "_" + this.id;
|
||||
}
|
||||
|
||||
getRTCPeerConnection() {
|
||||
|
@ -430,7 +432,7 @@ export class Stream {
|
|||
+ JSON.stringify(error));
|
||||
}
|
||||
|
||||
console.log("Sending SDP offer to publish as "
|
||||
console.debug("Sending SDP offer to publish as "
|
||||
+ this.getId(), sdpOfferParam);
|
||||
|
||||
this.openVidu.sendRequest("publishVideo", {
|
||||
|
@ -441,10 +443,8 @@ export class Stream {
|
|||
if (error) {
|
||||
console.error("Error on publishVideo: " + JSON.stringify(error));
|
||||
} else {
|
||||
this.room.emitEvent('stream-published', [{
|
||||
stream: this
|
||||
}]);
|
||||
this.processSdpAnswer(response.sdpAnswer);
|
||||
console.info("'Publisher' succesfully published to session");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
@ -454,7 +454,7 @@ export class Stream {
|
|||
return console.error("(subscribe) SDP offer error: "
|
||||
+ JSON.stringify(error));
|
||||
}
|
||||
console.log("Sending SDP offer to subscribe to "
|
||||
console.debug("Sending SDP offer to subscribe to "
|
||||
+ this.getId(), sdpOfferParam);
|
||||
this.openVidu.sendRequest("receiveVideoFrom", {
|
||||
sender: this.getId(),
|
||||
|
@ -492,14 +492,14 @@ export class Stream {
|
|||
}
|
||||
|
||||
if (this.displayMyRemote()) {
|
||||
this.wp = new kurentoUtils.WebRtcPeer.WebRtcPeerSendrecv(options, error => {
|
||||
this.wp = kurentoUtils.WebRtcPeer.WebRtcPeerSendrecv(options, error => {
|
||||
if (error) {
|
||||
return console.error(error);
|
||||
}
|
||||
this.wp.generateOffer(sdpOfferCallback.bind(this));
|
||||
});
|
||||
} else {
|
||||
this.wp = new kurentoUtils.WebRtcPeer.WebRtcPeerSendonly(options, error => {
|
||||
this.wp = kurentoUtils.WebRtcPeer.WebRtcPeerSendonly(options, error => {
|
||||
if (error) {
|
||||
return console.error(error);
|
||||
}
|
||||
|
@ -511,21 +511,21 @@ export class Stream {
|
|||
audio: this.recvAudio,
|
||||
video: !this.audioOnly
|
||||
};
|
||||
console.log("Constraints of generate SDP offer (subscribing)",
|
||||
console.debug("'Session.subscribe(Stream)' called. Constraints of generate SDP offer",
|
||||
offerConstraints);
|
||||
let options = {
|
||||
onicecandidate: this.connection.sendIceCandidate.bind(this.connection),
|
||||
mediaConstraints: offerConstraints
|
||||
}
|
||||
this.wp = new kurentoUtils.WebRtcPeer.WebRtcPeerRecvonly(options, error => {
|
||||
this.wp = kurentoUtils.WebRtcPeer.WebRtcPeerRecvonly(options, error => {
|
||||
if (error) {
|
||||
return console.error(error);
|
||||
}
|
||||
this.wp.generateOffer(sdpOfferCallback.bind(this));
|
||||
});
|
||||
}
|
||||
console.log("Waiting for SDP offer to be generated ("
|
||||
+ (this.local ? "local" : "remote") + " peer: " + this.getId() + ")");
|
||||
console.debug("Waiting for SDP offer to be generated ("
|
||||
+ (this.local ? "local" : "remote") + " 'Stream': " + this.getId() + ")");
|
||||
}
|
||||
|
||||
publish() {
|
||||
|
@ -560,7 +560,7 @@ export class Stream {
|
|||
type: 'answer',
|
||||
sdp: sdpAnswer,
|
||||
});
|
||||
console.log(this.getId() + ": set peer connection with recvd SDP answer",
|
||||
console.debug(this.getId() + ": set peer connection with recvd SDP answer",
|
||||
sdpAnswer);
|
||||
let participantId = this.getId();
|
||||
let pc = this.wp.peerConnection;
|
||||
|
@ -569,7 +569,7 @@ export class Stream {
|
|||
// except when showMyRemote is true
|
||||
if (!this.local || this.displayMyRemote()) {
|
||||
this.wrStream = pc.getRemoteStreams()[0];
|
||||
console.log("Peer remote stream", this.wrStream);
|
||||
console.debug("Peer remote stream", this.wrStream);
|
||||
|
||||
if (this.wrStream != undefined) {
|
||||
|
||||
|
@ -594,9 +594,19 @@ export class Stream {
|
|||
for (let videoElement of this.videoElements) {
|
||||
let thumbnailId = videoElement.thumb;
|
||||
let video = videoElement.video;
|
||||
video.src = URL.createObjectURL(this.wrStream);
|
||||
video.srcObject = this.wrStream;
|
||||
video.onplay = () => {
|
||||
console.log(this.getId() + ': ' + 'Video playing');
|
||||
if (this.local && this.displayMyRemote()) {
|
||||
console.info("Your own remote 'Stream' with id [" + this.getId() + "] video is now playing");
|
||||
this.ee.emitEvent('remote-video-is-playing', [{
|
||||
element: this.video
|
||||
}]);
|
||||
} else if (!this.local && !this.displayMyRemote()) {
|
||||
console.info("Remote 'Stream' with id [" + this.getId() + "] video is now playing");
|
||||
this.ee.emitEvent('video-is-playing', [{
|
||||
element: this.video
|
||||
}]);
|
||||
}
|
||||
//show(thumbnailId);
|
||||
//this.hideSpinner(this.getId());
|
||||
};
|
||||
|
@ -629,7 +639,7 @@ export class Stream {
|
|||
this.speechEvent.stop();
|
||||
}
|
||||
|
||||
console.log(this.getId() + ": Stream '" + this.id + "' unpublished");
|
||||
console.info(this.getId() + ": Stream '" + this.id + "' unpublished");
|
||||
}
|
||||
|
||||
dispose() {
|
||||
|
@ -663,6 +673,6 @@ export class Stream {
|
|||
this.speechEvent.stop();
|
||||
}
|
||||
|
||||
console.log(this.getId() + ": Stream '" + this.id + "' disposed");
|
||||
console.info((this.local ? "Local " : "Remote ") + "'Stream' with id [" + this.getId() + "]' has been succesfully disposed");
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"declaration": true,
|
||||
"allowJs": true,
|
||||
"target": "es5",
|
||||
"module": "commonjs",
|
||||
//"noImplicitAny": true,
|
||||
|
|
|
@ -1,5 +1,8 @@
|
|||
VERSION="$1"
|
||||
|
||||
# openvidu-hello-world
|
||||
cp openvidu-browser/src/main/resources/static/js/openvidu-browser-"$1".js ../openvidu-tutorials/openvidu-hello-world/web/openvidu-browser-"$1".js
|
||||
|
||||
# openvidu-insecure-js
|
||||
cp openvidu-browser/src/main/resources/static/js/openvidu-browser-"$1".js ../openvidu-tutorials/openvidu-insecure-js/web/openvidu-browser-"$1".js
|
||||
|
||||
|
|
Loading…
Reference in New Issue