1
0
mirror of https://github.com/lynckia/licode.git synced 2025-05-15 17:38:34 +08:00
Carlos Arriaga Prieto da4c767948
Handler API ()
2021-05-25 10:59:08 +02:00

594 lines
22 KiB
JavaScript

/* global require, exports */
const events = require('events');
// eslint-disable-next-line import/no-unresolved
const addon = require('./../../../erizoAPI/build/Release/addon');
const logger = require('./../../common/logger').logger;
const SessionDescription = require('./SessionDescription');
const SemanticSdp = require('./../../common/semanticSdp/SemanticSdp');
const PerformanceStats = require('./../../common/PerformanceStats');
const sdpTransform = require('sdp-transform');
const Helpers = require('./Helpers');
const log = logger.getLogger('Connection');
const CONN_INITIAL = 101;
// CONN_STARTED = 102,
const CONN_GATHERED = 103;
const CONN_READY = 104;
const CONN_FINISHED = 105;
const CONN_QUALITY_LEVEL = 150;
const CONN_CANDIDATE = 201;
const CONN_SDP = 202;
const CONN_SDP_PROCESSED = 203;
const CONN_FAILED = 500;
const WARN_BAD_CONNECTION = 502;
const CONNECTION_QUALITY_LEVEL_UPDATE_INTERVAL = 5000; // ms
const CONNECTION_QUALITY_LEVEL_INCREASE_UPDATE_INTERVAL = 30000; // ms
class Connection extends events.EventEmitter {
constructor(erizoControllerId, id, threadPool, ioThreadPool, clientId,
streamPriorityStrategy = false, options = {}) {
super();
log.info(`message: constructor, id: ${id},`, logger.objectToLog(options), logger.objectToLog(options.metadata));
this.id = id;
this.erizoControllerId = erizoControllerId;
this.clientId = clientId;
this.threadPool = threadPool;
this.ioThreadPool = ioThreadPool;
this.mediaConfiguration = 'default';
// {id: stream}
this.mediaStreams = new Map();
this.options = options;
this.streamPriorityStrategy = streamPriorityStrategy;
this.wrtc = this._createWrtc();
this.initialized = false;
this.qualityLevel = -1;
this.trickleIce = options.trickleIce || false;
this.onGathered = new Promise((resolve, reject) => {
this._gatheredResolveFunction = resolve;
this._gatheredRejectFunction = reject;
});
this.onInitialized = new Promise((resolve, reject) => {
this._initializeResolveFunction = resolve;
this._initializeRejectFunction = reject;
});
this.onStarted = new Promise((resolve, reject) => {
this._startResolveFunction = resolve;
this._startRejectFunction = reject;
});
this.onReady = new Promise((resolve, reject) => {
this._readyResolveFunction = resolve;
this._readyRejectFunction = reject;
});
this.isNegotiationLocked = false;
this.queue = [];
this.lastQualityLevelChanged = new Date() - CONNECTION_QUALITY_LEVEL_INCREASE_UPDATE_INTERVAL;
}
_logSdp(...message) {
log.debug('negotiation:', ...message, ', id:', this.id, ', lockReason: ', this.lockReason, ',',
logger.objectToLog(this.options), logger.objectToLog(this.options.metadata));
}
static _getMediaConfiguration(mediaConfiguration = 'default') {
if (global.mediaConfig && global.mediaConfig.codecConfigurations) {
if (global.mediaConfig.codecConfigurations[mediaConfiguration]) {
return JSON.stringify(global.mediaConfig.codecConfigurations[mediaConfiguration]);
} else if (global.mediaConfig.codecConfigurations.default) {
return JSON.stringify(global.mediaConfig.codecConfigurations.default);
}
log.warn(
'message: Bad media config file. You need to specify a default codecConfiguration,',
logger.objectToLog(this.options));
return JSON.stringify({});
}
log.warn(
'message: Bad media config file. You need to specify a default codecConfiguration,',
logger.objectToLog(this.options));
return JSON.stringify({});
}
static _getBwDistributionConfig(strategyId) {
if (strategyId &&
global.bwDistributorConfig.strategyDefinitions &&
global.bwDistributorConfig.strategyDefinitions[strategyId]) {
const requestedStrategyDefinition =
global.bwDistributorConfig.strategyDefinitions[strategyId];
if (requestedStrategyDefinition.priorities) {
const serialized = Helpers.serializeStreamPriorityStrategy(requestedStrategyDefinition);
if (serialized) {
const result = {
type: 'StreamPriority',
strategyId,
strategy: serialized,
};
return JSON.stringify(result);
}
}
log.warn(`message: Bad strategy definition. Using default distributor Config ${global.bwDistributorConfig.defaultType}`);
return JSON.stringify({ type: global.bwDistributorConfig.defaultType });
}
log.info(`message: No strategy definiton. Using default distributor Config ${global.bwDistributorConfig.defaultType}`);
return JSON.stringify({ type: global.bwDistributorConfig.defaultType });
}
_createWrtc() {
const wrtc = new addon.WebRtcConnection(this.threadPool, this.ioThreadPool, this.id,
global.config.erizo.stunserver,
global.config.erizo.stunport,
global.config.erizo.minport,
global.config.erizo.maxport,
this.trickleIce,
Connection._getMediaConfiguration(this.mediaConfiguration),
Connection._getBwDistributionConfig(this.streamPriorityStrategy),
global.config.erizo.useConnectionQualityCheck,
global.config.erizo.turnserver,
global.config.erizo.turnport,
global.config.erizo.turnusername,
global.config.erizo.turnpass,
global.config.erizo.networkinterface);
if (this.options) {
const metadata = this.options.metadata || {};
wrtc.setMetadata(JSON.stringify(metadata));
}
return wrtc;
}
_createMediaStream(id, options = {}, isPublisher = true, offerFromErizo = false) {
log.debug(`message: _createMediaStream, connectionId: ${this.id}, ` +
`mediaStreamId: ${id}, isPublisher: ${isPublisher},`,
logger.objectToLog(this.options), logger.objectToLog(this.options.metadata),
logger.objectToLog(options), logger.objectToLog(options.metadata));
const sessionVersion = offerFromErizo ? this.sessionVersion : -1;
const handlerProfile = options.handlerProfile ? options.handlerProfile : 0;
const handlers = global.config.erizo.handlerProfiles
? global.config.erizo.handlerProfiles[handlerProfile].slice() : [];
for (let i = 0; i < handlers.length; i += 1) {
handlers[i] = JSON.stringify(handlers[i]);
}
const mediaStream = new addon.MediaStream(this.threadPool,
this.wrtc, id,
options.label,
Connection._getMediaConfiguration(this.mediaConfiguration),
isPublisher,
sessionVersion,
handlers);
mediaStream.id = id;
mediaStream.label = options.label;
if (options.metadata) {
mediaStream.metadata = options.metadata;
mediaStream.setMetadata(JSON.stringify(options.metadata));
}
mediaStream.onMediaStreamEvent((type, message) => {
this._onMediaStreamEvent(type, message, mediaStream.id);
});
return mediaStream;
}
_onMediaStreamEvent(type, message, mediaStreamId) {
const streamEvent = {
type,
mediaStreamId,
message,
};
this.emit('media_stream_event', streamEvent);
}
_onStatusEvent(info, evt) {
this.emit('status_event', this.erizoControllerId, this.clientId, this.id, info, evt);
}
createAnswer() {
return this.getLocalSdp().then((info) => {
log.debug('getting local sdp for answer', info, ',',
logger.objectToLog(this.options), logger.objectToLog(this.options.metadata));
return { type: 'answer', sdp: info };
});
}
createOffer(requestId = undefined) {
return this.getLocalSdp().then((info) => {
PerformanceStats.mark(requestId, PerformanceStats.Marks.CONNECTION_OFFER_CREATED);
log.debug('getting local sdp for offer', info, ',',
logger.objectToLog(this.options), logger.objectToLog(this.options.metadata));
return { type: 'offer', sdp: info };
});
}
getLocalSdp() {
if (!this.wrtc) {
return Promise.resolve();
}
return this.wrtc.getLocalDescription().then((desc) => {
if (!this.wrtc || !desc) {
log.error('Cannot get local description,',
logger.objectToLog(this.options), logger.objectToLog(this.options.metadata));
return '';
}
this.wrtc.localDescription = new SessionDescription(desc);
const sdp = this.wrtc.localDescription.getSdp(this.sessionVersion);
this.sessionVersion += 1;
let message = sdp.toString();
message = message.replace(this.options.privateRegexp, this.options.publicIP);
return message;
});
}
updateConnectionQualityLevel() {
if (this.wrtc) {
const newQualityLevel = this.wrtc.getConnectionQualityLevel();
const timeSinceLastQualityLevel = new Date() - this.lastQualityLevelChanged;
const canIncreaseQualityLevel = newQualityLevel > this.qualityLevel &&
timeSinceLastQualityLevel > CONNECTION_QUALITY_LEVEL_INCREASE_UPDATE_INTERVAL;
const canDecreaseQualityLevel = newQualityLevel < this.qualityLevel;
if (canIncreaseQualityLevel || canDecreaseQualityLevel) {
this.qualityLevel = newQualityLevel;
this.lastQualityLevelChanged = new Date();
this._onStatusEvent({ type: 'quality_level', level: this.qualityLevel }, CONN_QUALITY_LEVEL);
}
}
}
sendOffer(requestId = undefined) {
PerformanceStats.mark(requestId, PerformanceStats.Marks.CONNECTION_OFFER_ENQUEUED);
return this._enqueueOrSendOffer(requestId);
}
_enqueueOrSendOffer(requestId = undefined) {
if (this.isNegotiationLocked) {
this._logSdp('Enqueueing sendOffer, id:', this.id);
return this._enqueueNegotiation(this._enqueueOrSendOffer.bind(this, requestId));
}
this._logSdp('SendOffer');
this._lockNegotiation('sendOffer');
PerformanceStats.mark(requestId, PerformanceStats.Marks.CONNECTION_OFFER_DEQUEUED);
return this._sendOffer(requestId);
}
_sendOffer(requestId = undefined) {
if (!this.alreadyGathered && !this.trickleIce) {
return Promise.resolve();
}
this._logSdp('_sendOffer');
return this.createOffer(requestId).then((info) => {
log.debug(`message: sendOffer sending event, type: ${info.type}, sessionVersion: ${this.sessionVersion},`,
logger.objectToLog(this.options), logger.objectToLog(this.options.metadata));
this._onStatusEvent(info, CONN_SDP);
PerformanceStats.mark(requestId, PerformanceStats.Marks.CONNECTION_OFFER_SENT);
});
}
sendAnswer() {
if (!this.alreadyGathered && !this.trickleIce) {
return Promise.resolve();
}
this._logSdp('sendAnswer');
return this.createAnswer().then((info) => {
log.debug(`message: sendAnswer sending event, type: ${info.type}, sessionVersion: ${this.sessionVersion},`,
logger.objectToLog(this.options), logger.objectToLog(this.options.metadata));
this._onStatusEvent(info, CONN_SDP);
});
}
init(createOffer = this.options.createOffer) {
if (this.initialized) {
return false;
}
this.initialized = true;
this.qualityLevelInterval = setInterval(this.updateConnectionQualityLevel.bind(this),
CONNECTION_QUALITY_LEVEL_UPDATE_INTERVAL);
log.debug(`message: Init Connection, connectionId: ${this.id},`,
logger.objectToLog(this.options), logger.objectToLog(this.options.metadata));
this.sessionVersion = 0;
this.wrtc.init((newStatus, mess) => {
log.info('message: WebRtcConnection status update, ',
`id: ${this.id}, status: ${newStatus},`,
logger.objectToLog(this.options), logger.objectToLog(this.options.metadata));
switch (newStatus) {
case CONN_INITIAL:
this._startResolveFunction();
break;
case CONN_SDP_PROCESSED:
case CONN_SDP:
break;
case CONN_GATHERED:
this.alreadyGathered = true;
this._gatheredResolveFunction();
break;
case CONN_CANDIDATE:
// eslint-disable-next-line no-param-reassign
mess = mess.replace(this.options.privateRegexp, this.options.publicIP);
this._onStatusEvent({ type: 'candidate', candidate: mess }, newStatus);
break;
case CONN_FAILED:
log.warn(`message: failed the ICE process, code: ${WARN_BAD_CONNECTION},` +
`id: ${this.id},`,
logger.objectToLog(this.options), logger.objectToLog(this.options.metadata));
this._onStatusEvent({ type: 'failed', sdp: mess }, newStatus);
break;
case CONN_READY:
log.debug(`message: connection ready, id: ${this.id} status: ${newStatus},`,
logger.objectToLog(this.options), logger.objectToLog(this.options.metadata));
this._readyResolveFunction();
this._onStatusEvent({ type: 'ready' }, newStatus);
break;
default:
log.error(`message: unknown webrtc status ${newStatus},`,
logger.objectToLog(this.options), logger.objectToLog(this.options.metadata));
}
});
if (createOffer) {
log.debug('message: create offer requested, id:', this.id, ',',
logger.objectToLog(this.options), logger.objectToLog(this.options.metadata));
const audioEnabled = createOffer.audio;
const videoEnabled = createOffer.video;
const bundle = createOffer.bundle;
this.createOfferPromise = this.wrtc.createOffer(videoEnabled, audioEnabled, bundle);
}
this._initializeResolveFunction();
return true;
}
addMediaStream(id, options, isPublisher, offerFromErizo) {
let promise = Promise.resolve();
log.info(`message: addMediaStream, connectionId: ${this.id}, mediaStreamId: ${id},`,
logger.objectToLog(this.options), logger.objectToLog(this.options.metadata),
logger.objectToLog(options), logger.objectToLog(options.metadata));
if (this.mediaStreams.get(id) === undefined) {
const mediaStream = this._createMediaStream(id, options, isPublisher, offerFromErizo);
promise = this.wrtc.addMediaStream(mediaStream);
this.mediaStreams.set(id, mediaStream);
}
return promise;
}
removeMediaStream(id, sendOffer = true, requestId = undefined) {
const promise = Promise.resolve();
if (this.mediaStreams.get(id) !== undefined) {
const removePromise = this.wrtc.removeMediaStream(id);
const closePromise = this.mediaStreams.get(id).close();
removePromise.then(() => PerformanceStats.mark(requestId,
PerformanceStats.Marks.CONNECTION_STREAM_REMOVED));
closePromise.then(() => PerformanceStats.mark(requestId,
PerformanceStats.Marks.CONNECTION_STREAM_CLOSED));
this.mediaStreams.delete(id);
return Promise.all([removePromise, closePromise]).then(() => {
if (sendOffer) {
return this.sendOffer(requestId);
}
return Promise.resolve();
});
}
log.error(`message: Trying to remove mediaStream not found, clientId: ${this.clientId}, streamId: ${id}`,
logger.objectToLog(this.options), logger.objectToLog(this.options.metadata));
return promise;
}
setRemoteDescription(sdp, receivedSessionVersion = -1) {
const sdpInfo = SemanticSdp.SDPInfo.processString(sdp);
let oldIceCredentials = ['', ''];
if (this.remoteDescription) {
oldIceCredentials = this.remoteDescription.getICECredentials();
}
this.remoteDescription = new SessionDescription(sdpInfo, this.mediaConfiguration);
this._logSdp('setRemoteDescription');
const iceCredentials = this.remoteDescription.getICECredentials();
if (oldIceCredentials[0] !== '' && oldIceCredentials[0] !== iceCredentials[0]) {
this.alreadyGathered = false;
this.onGathered = new Promise((resolve, reject) => {
this._gatheredResolveFunction = resolve;
this._gatheredRejectFunction = reject;
});
log.info(`message: ICE restart detected, clientId: ${this.clientId}`,
logger.objectToLog(this.options), logger.objectToLog(this.options.metadata));
this._logSdp('restartIce');
this.wrtc.maybeRestartIce(iceCredentials[0], iceCredentials[1]);
}
return this.wrtc.setRemoteDescription(this.remoteDescription.connectionDescription,
receivedSessionVersion);
}
addRemoteCandidate(sdpCandidate) {
const candidatesInfo = sdpTransform.parse(sdpCandidate.candidate);
if (candidatesInfo.sdpMid === 'end' || candidatesInfo.candidates === undefined) {
return;
}
candidatesInfo.candidates.forEach((candidate) => {
if (candidate.transport.toLowerCase() !== 'udp') {
return;
}
this.wrtc.addRemoteCandidate(sdpCandidate.sdpMid, sdpCandidate.sdpMLineIndex,
candidate.foundation, candidate.component, candidate.priority, candidate.transport,
candidate.ip, candidate.port, candidate.type, candidate.raddr, candidate.rport,
sdpCandidate.candidate);
});
}
onSignalingMessage(msg) {
this._logSdp('onSignalingMessage, type:', msg.type);
if (msg.type === 'offer') {
this._lockNegotiation('processOffer');
return this._onSignalingMessage(msg).then(() => {
this._unlockNegotiation();
this._dequeueSignalingMessage();
});
}
if (msg.type === 'answer') {
if (!this.isNegotiationLocked) {
log.warn('message: Received answer and negotiation was not locked, connectionId: ', this.id, ',',
logger.objectToLog(this.options), logger.objectToLog(this.options.metadata));
}
const promise = this._onSignalingMessage(msg);
this._unlockNegotiation();
this._dequeueSignalingMessage();
return promise;
}
if (this.isNegotiationLocked) {
return this._enqueueNegotiation(this.onSignalingMessage.bind(this, msg));
}
return this._onSignalingMessage(msg);
}
_lockNegotiation(reason) {
this.lockReason = reason;
this._logSdp('_lockNegotiation');
this.isNegotiationLocked = true;
}
_unlockNegotiation() {
this.isNegotiationLocked = false;
this._logSdp('_unlockNegotiation');
}
_enqueueNegotiation(negotiationCall) {
this._logSdp('_enqueueNegotiation');
return new Promise((success) => {
this.queue.push(() => {
negotiationCall().then(() => {
success();
}).catch(() => {
success();
});
this._dequeueSignalingMessage();
});
});
}
_dequeueSignalingMessage() {
if (this.isNegotiationLocked) {
return;
}
this._logSdp('_dequeueNegotiation');
if (this.queue.length > 0) {
const func = this.queue.shift();
func();
}
}
_onSignalingMessage(msg) {
this._logSdp('_onSignalingMessage, type:', msg.type);
if (msg.type === 'offer') {
return this.setRemoteDescription(msg.sdp, msg.receivedSessionVersion)
.then(() => {
const onEvent = this.trickleIce ? this.onInitialized : this.onGathered;
return onEvent;
})
.then(() => this.sendAnswer())
.catch(() => {
log.error('message: Error processing offer/answer in connection, connectionId:', this.id, ',',
logger.objectToLog(this.options), logger.objectToLog(this.options.metadata));
});
} else if (msg.type === 'offer-noanswer') {
return this.setRemoteDescription(msg.sdp, msg.receivedSessionVersion).catch(() => {
log.error('message: Error processing offer/noanswer in connection, connectionId:', this.id, ',',
logger.objectToLog(this.options), logger.objectToLog(this.options.metadata));
});
} else if (msg.type === 'answer') {
return this.setRemoteDescription(msg.sdp, msg.receivedSessionVersion).catch(() => {
log.error('message: Error processing answer in connection, connectionId:', this.id, ',',
logger.objectToLog(this.options), logger.objectToLog(this.options.metadata));
});
} else if (msg.type === 'candidate') {
this.addRemoteCandidate(msg.candidate);
return Promise.resolve();
} else if (msg.type === 'updatestream') {
if (msg.sdp) {
return this.setRemoteDescription(msg.sdp, msg.receivedSessionVersion).catch(() => {
log.error('message: Error processing updatestream in connection, connectionId:', this.id, ',',
logger.objectToLog(this.options), logger.objectToLog(this.options.metadata));
});
}
} else if (msg.type === 'offer-dropped') {
log.debug('message: Offer dropped, sending again', this.id, ',',
logger.objectToLog(this.options), logger.objectToLog(this.options.metadata));
return this.sendOffer();
} else if (msg.type === 'answer-dropped') {
log.error('message: Answer dropped, sending again', this.id, ',',
logger.objectToLog(this.options), logger.objectToLog(this.options.metadata));
}
return Promise.resolve();
}
setStreamPriorityStrategy(strategyId) {
this.streamPriorityStrategy = strategyId;
this.wrtc.setBwDistributionConfig(
Connection._getBwDistributionConfig(this.streamPriorityStrategy));
}
getMediaStream(id) {
return this.mediaStreams.get(id);
}
getNumMediaStreams() {
return this.mediaStreams.size;
}
getStats(callback) {
if (!this.wrtc) {
callback('{}');
return true;
}
return this.wrtc.getStats(callback);
}
getDurationDistribution() {
if (!this.wrtc) {
return [];
}
return this.wrtc.getDurationDistribution();
}
getDelayDistribution() {
if (!this.wrtc) {
return [];
}
return this.wrtc.getDelayDistribution();
}
resetStats() {
if (!this.wrtc) {
return;
}
this.wrtc.resetStats();
}
close() {
log.info(`message: Closing connection, id: ${this.id},`,
logger.objectToLog(this.options), logger.objectToLog(this.options.metadata));
log.info(`message: WebRtcConnection status update, id: ${this.id}, status: ${CONN_FINISHED},`,
logger.objectToLog(this.options), logger.objectToLog(this.options.metadata));
clearInterval(this.qualityLevelInterval);
const promises = [];
this.mediaStreams.forEach((mediaStream, id) => {
log.debug(`message: Closing mediaStream, connectionId : ${this.id}, ` +
`mediaStreamId: ${id},`,
logger.objectToLog(this.options), logger.objectToLog(this.options.metadata));
promises.push(mediaStream.close());
});
Promise.all(promises).then(() => {
log.debug(`message: Closing WRTC, id: ${this.id},`,
logger.objectToLog(this.options), logger.objectToLog(this.options.metadata));
this.wrtc.close().then(() => {
log.debug(`message: WRTC closed, id: ${this.id},`,
logger.objectToLog(this.options), logger.objectToLog(this.options.metadata));
delete this.wrtc;
});
this.mediaStreams.clear();
});
}
}
exports.Connection = Connection;