/**
* Function that sends the stats to the API server.
* @method _postStatsToServer
* @private
* @for Skylink
* @since 0.6.31
*/
Skylink.prototype._postStats = function (endpoint, params) {
var self = this;
var requestBody = {};
if(self._initOptions.enableStatsGathering){
if(Array.isArray(params)){
requestBody.data = params;
}
else{
requestBody = params;
}
requestBody.client_id = ((self._user && self._user.uid) || 'dummy') + '_' + self._statIdRandom;
requestBody.app_key = self._initOptions.appKey;
requestBody.timestamp = (new Date()).toISOString();
// Simply post the data directly to the API server without caring if it is successful or not.
try {
var xhr = new XMLHttpRequest();
xhr.onerror = function () { };
xhr.open('POST', self._initOptions.statsServer + endpoint, true);
xhr.setRequestHeader('Content-Type', 'application/json;charset=UTF-8');
xhr.send(JSON.stringify(requestBody));
} catch (error) {
log.error([null, 'XMLHttpRequest', "POST", 'Error in posting stats data ->'], error);
}
}
};
/**
* Function that handles the posting of client information.
* @method _handleClientStats
* @private
* @for Skylink
* @since 0.6.31
*/
Skylink.prototype._handleClientStats = function() {
var self = this;
var statsObject = {
username: (self._user && self._user.uid) || null,
sdk_name: 'web',
sdk_version: self.VERSION,
agent_name: AdapterJS.webrtcDetectedBrowser,
agent_version: AdapterJS.webrtcDetectedVersion,
agent_platform: navigator.platform,
agent_plugin_version: (AdapterJS.WebRTCPlugin.plugin && AdapterJS.WebRTCPlugin.plugin.VERSION) || null
};
self._postStats('/rest/stats/client', statsObject);
};
/**
* Function that handles the posting of session states.
* @method _handleSessionStats
* @private
* @for Skylink
* @since 0.6.31
*/
Skylink.prototype._handleSessionStats = function(message) {
var self = this;
var statsObject = {
room_id: self._room && self._room.id,
user_id: (self._user && self._user.sid) || null,
state: message.type,
contents: message
};
self._postStats('/rest/stats/session', statsObject);
};
/**
* Function that handles the posting of app key authentication states.
* @method _handleAuthStats
* @private
* @for Skylink
* @since 0.6.31
*/
Skylink.prototype._handleAuthStats = function(state, result, status, error) {
var self = this;
var statsObject = {
room_id: (result && result.room_key) || null,
state: state,
http_status: status,
http_error: (typeof error === 'string' ? error : (error && error.message)) || null,
api_url: self._path,
api_result: result
};
self._postStats('/rest/stats/auth', statsObject);
};
/**
* Function that handles the posting of socket connection states.
* @method _handleSignalingStats
* @private
* @for Skylink
* @since 0.6.31
*/
Skylink.prototype._handleSignalingStats = function(state, retries, error) {
var self = this;
var socketSession = clone(self._socketSession);
var statsObject = {
room_id: self._room && self._room.id,
user_id: (self._user && self._user.sid) || null,
state: state,
signaling_url: socketSession.socketServer,
signaling_transport: socketSession.transportType.toLowerCase(),
// Use the retries from the function itself to prevent non-sequential event calls issues.
attempts: retries,
error: (typeof error === 'string' ? error : (error && error.message)) || null
};
self._postStats('/rest/stats/client/signaling', statsObject);
};
/**
* Function that handles the posting of peer ICE connection states.
* @method _handleIceConnectionStats
* @private
* @for Skylink
* @since 0.6.31
*/
Skylink.prototype._handleIceConnectionStats = function(state, peerId) {
var self = this;
var statsObject = {
room_id: self._room && self._room.id,
user_id: self._user && self._user.sid,
peer_id: peerId,
state: state,
local_candidate: {},
remote_candidate: {}
};
// Set a timeout to pause process to ensure the stats retrieval does not run at the same time
// when the state is triggered, so that the selected ICE candidate pair information can be returned.
self._retrieveStats(peerId, function (error, stats) {
if (stats) {
// Parse the selected ICE candidate pair for both local and remote candidate.
['local', 'remote'].forEach(function (dirType) {
var candidate = stats.selectedCandidate[dirType];
if (candidate) {
statsObject[dirType + '_candidate'].ip_address = candidate.ipAddress || null;
statsObject[dirType + '_candidate'].port_number = candidate.portNumber || null;
statsObject[dirType + '_candidate'].candidate_type = candidate.candidateType || null;
statsObject[dirType + '_candidate'].protocol = candidate.transport || null;
statsObject[dirType + '_candidate'].priority = candidate.priority || null;
// This is only available for the local ICE candidate.
if (dirType === 'local') {
statsObject.local_candidate.network_type = candidate.networkType || null;
}
}
});
}
self._postStats('/rest/stats/client/iceconnection', statsObject);
}, true);
};
/**
* Function that handles the posting of peer local/remote ICE candidate processing states.
* @method _handleIceCandidateStats
* @private
* @for Skylink
* @since 0.6.31
*/
Skylink.prototype._handleIceCandidateStats = function(state, peerId, candidateId, candidate, error) {
var self = this;
var statsObject = {
room_id: self._room && self._room.id,
user_id: self._user && self._user.sid,
peer_id: peerId,
state: state,
is_remote: !!candidateId,
candidate_id: candidateId || null,
candidate_sdp_mid: candidate.sdpMid,
candidate_sdp_mindex: candidate.sdpMLineIndex,
candidate_candidate: candidate.candidate,
error: (typeof error === 'string' ? error : (error && error.message)) || null,
};
self._manageStatsBuffer('iceCandidate', statsObject, '/rest/stats/client/icecandidate');
};
/**
* Function that handles the posting of peer local/remote ICE gathering states.
* @method _handleIceGatheringStats
* @private
* @for Skylink
* @since 0.6.31
*/
Skylink.prototype._handleIceGatheringStats = function(state, peerId, isRemote) {
var self = this;
var statsObject = {
room_id: self._room && self._room.id,
user_id: self._user && self._user.sid,
peer_id: peerId,
state: state,
is_remote: isRemote
};
self._manageStatsBuffer('iceGathering', statsObject, '/rest/stats/client/icegathering');
};
/**
* Function that handles the posting of peer connection negotiation states.
* @method _handleNegotiationStats
* @private
* @for Skylink
* @since 0.6.31
*/
Skylink.prototype._handleNegotiationStats = function(state, peerId, sdpOrMessage, isRemote, error) {
var self = this;
var statsObject = {
room_id: self._room && self._room.id,
user_id: self._user && self._user.sid,
peer_id: peerId,
state: state,
is_remote: isRemote,
// Currently sharing a parameter "sdpOrMessage" that indicates a "welcome" message
// or session description to save parameters length.
weight: sdpOrMessage.weight,
sdp_type: null,
sdp_sdp: null,
error: (typeof error === 'string' ? error : (error && error.message)) || null,
};
// Retrieve the weight for states where the "weight" field is not available.
if (['enter', 'welcome', 'restart'].indexOf(state) === -1) {
// Retrieve the peer's weight if it from remote end.
statsObject.weight = self.getPeerInfo(isRemote ? peerId : undefined).config.priorityWeight;
statsObject.sdp_type = (sdpOrMessage && sdpOrMessage.type) || null;
statsObject.sdp_sdp = (sdpOrMessage && sdpOrMessage.sdp) || null;
}
self._manageStatsBuffer('negotiation', statsObject, '/rest/stats/client/negotiation');
};
/**
* Function that handles the posting of peer connection bandwidth information.
* @method _handleBandwidthStats
* @private
* @for Skylink
* @since 0.6.31
*/
Skylink.prototype._handleBandwidthStats = function (peerId) {
var self = this;
var statsObject = {
room_id: self._room && self._room.id,
user_id: self._user && self._user.sid,
peer_id: peerId,
audio_send: { tracks: [] },
audio_recv: {},
video_send: { tracks: [] },
video_recv: {}
};
var useStream = self._streams.screenshare || self._streams.userMedia || null;
var mutedStatus = self.getPeerInfo().mediaStatus;
// When stream is available, format the stream tracks information.
// The SDK currently only allows sending of 1 stream at a time that has only 1 audio and video track each.
if (useStream) {
// Parse the audio track if it exists only.
if (useStream.tracks.audio) {
statsObject.audio_send.tracks = [{
stream_id: useStream.id,
id: useStream.tracks.audio.id,
label: useStream.tracks.audio.label,
muted: mutedStatus.audioMuted
}];
}
// Parse the video track if it exists only.
if (useStream.tracks.video) {
statsObject.video_send.tracks = [{
stream_id: useStream.id,
id: useStream.tracks.video.id,
label: useStream.tracks.video.label,
height: useStream.tracks.video.height,
width: useStream.tracks.video.width,
muted: mutedStatus.videoMuted
}];
}
}
self._retrieveStats(peerId, function (error, stats) {
if (error) {
statsObject.error = error && error.message;
stats = {
audio: { sending: {}, receiving: {} },
video: { sending: {}, receiving: {} }
};
}
// Common function to parse and handle any `null`/`undefined` values.
var formatValue = function (mediaType, directionType, itemKey) {
var value = stats[mediaType][directionType === 'send' ? 'sending' : 'receiving'][itemKey];
if (['number', 'string', 'boolean'].indexOf(typeof value) > -1) {
return value;
}
return null;
};
// Parse bandwidth information for sending audio packets.
statsObject.audio_send.bytes = formatValue('audio', 'send', 'bytes');
statsObject.audio_send.packets = formatValue('audio', 'send', 'packets');
statsObject.audio_send.round_trip_time = formatValue('audio', 'send', 'rtt');
statsObject.audio_send.nack_count = formatValue('audio', 'send', 'nacks');
statsObject.audio_send.echo_return_loss = formatValue('audio', 'send', 'echoReturnLoss');
statsObject.audio_send.echo_return_loss_enhancement = formatValue('audio', 'send', 'echoReturnLossEnhancement');
// Parse bandwidth information for receiving audio packets.
statsObject.audio_recv.bytes = formatValue('audio', 'recv', 'bytes');
statsObject.audio_recv.packets = formatValue('audio', 'recv', 'packets');
statsObject.audio_recv.packets_lost = formatValue('audio', 'recv', 'packetsLost');
statsObject.video_recv.packets_discarded = formatValue('audio', 'recv', 'packetsDiscarded');
statsObject.audio_recv.jitter = formatValue('audio', 'recv', 'jitter');
statsObject.audio_recv.nack_count = formatValue('audio', 'recv', 'nacks');
// Parse bandwidth information for sending video packets.
statsObject.video_send.bytes = formatValue('video', 'send', 'bytes');
statsObject.video_send.packets = formatValue('video', 'send', 'packets');
statsObject.video_send.round_trip_time = formatValue('video', 'send', 'rtt');
statsObject.video_send.nack_count = formatValue('video', 'send', 'nacks');
statsObject.video_send.firs_count = formatValue('video', 'send', 'firs');
statsObject.video_send.plis_count = formatValue('video', 'send', 'plis');
statsObject.video_send.frames = formatValue('video', 'send', 'frames');
statsObject.video_send.frames_encoded = formatValue('video', 'send', 'framesEncoded');
statsObject.video_send.frames_dropped = formatValue('video', 'send', 'framesDropped');
statsObject.video_send.frame_width = formatValue('video', 'send', 'frameWidth');
statsObject.video_send.frame_height = formatValue('video', 'send', 'frameHeight');
statsObject.video_send.framerate = formatValue('video', 'send', 'frameRate');
statsObject.video_send.framerate_input = formatValue('video', 'send', 'frameRateInput');
statsObject.video_send.framerate_encoded = formatValue('video', 'send', 'frameRateEncoded');
statsObject.video_send.framerate_mean = formatValue('video', 'send', 'frameRateMean');
statsObject.video_send.framerate_std_dev = formatValue('video', 'send', 'frameRateStdDev');
statsObject.video_send.cpu_limited_resolution = formatValue('video', 'send', 'cpuLimitedResolution');
statsObject.video_send.bandwidth_limited_resolution = formatValue('video', 'send', 'bandwidthLimitedResolution');
// Parse bandwidth information for receiving video packets.
statsObject.video_recv.bytes = formatValue('video', 'recv', 'bytes');
statsObject.video_recv.packets = formatValue('video', 'recv', 'packets');
statsObject.video_recv.packets_lost = formatValue('video', 'recv', 'packetsLost');
statsObject.video_recv.packets_discarded = formatValue('video', 'recv', 'packetsDiscarded');
statsObject.video_recv.jitter = formatValue('video', 'recv', 'jitter');
statsObject.video_recv.nack_count = formatValue('video', 'recv', 'nacks');
statsObject.video_recv.firs_count = formatValue('video', 'recv', 'firs');
statsObject.video_recv.plis_count = formatValue('video', 'recv', 'plis');
statsObject.video_recv.frames = formatValue('video', 'recv', 'frames');
statsObject.video_recv.frames_decoded = formatValue('video', 'recv', 'framesDecoded');
statsObject.video_recv.frame_width = formatValue('video', 'recv', 'frameWidth');
statsObject.video_recv.frame_height = formatValue('video', 'recv', 'frameHeight');
statsObject.video_recv.framerate = formatValue('video', 'recv', 'frameRate');
statsObject.video_recv.framerate_output = formatValue('video', 'recv', 'frameRateOutput');
statsObject.video_recv.framerate_decoded = formatValue('video', 'recv', 'frameRateDecoded');
statsObject.video_recv.framerate_mean = formatValue('video', 'recv', 'frameRateMean');
statsObject.video_recv.framerate_std_dev = formatValue('video', 'recv', 'frameRateStdDev');
statsObject.video_recv.qp_sum = formatValue('video', 'recv', 'qpSum');
self._postStats('/rest/stats/client/bandwidth', statsObject);
}, true);
};
/**
* Function that handles the posting of recording states.
* @method _handleRecordingStats
* @private
* @for Skylink
* @since 0.6.31
*/
Skylink.prototype._handleRecordingStats = function(state, recordingId, recordings, error) {
var self = this;
var statsObject = {
room_id: self._room && self._room.id,
user_id: self._user && self._user.sid,
state: state,
recording_id: recordingId || null,
recordings: recordings,
error: (typeof error === 'string' ? error : (error && error.message)) || null
};
self._postStats('/rest/stats/client/recording', statsObject);
};
/**
* Function that handles the posting of datachannel states.
* @method _handleDatachannelStats
* @private
* @for Skylink
* @since 0.6.31
*/
Skylink.prototype._handleDatachannelStats = function(state, peerId, channel, channelProp, error) {
var self = this;
var statsObject = {
room_id: self._room && self._room.id,
user_id: self._user && self._user.sid,
peer_id: peerId,
state: state,
channel_id: channel && channel.id,
channel_label: channel && channel.label,
channel_type: channelProp === 'main' ? 'persistent' : 'temporal',
channel_binary_type: channel && channel.binaryType,
error: (typeof error === 'string' ? error : (error && error.message)) || null
};
if (channel && AdapterJS.webrtcDetectedType === 'plugin') {
statsObject.channel_binary_type = 'int8Array';
// For IE 10 and below browsers, binary support is not available.
if (AdapterJS.webrtcDetectedBrowser === 'IE' && AdapterJS.webrtcDetectedVersion < 11) {
statsObject.channel_binary_type = 'none';
}
}
self._postStats('/rest/stats/client/datachannel', statsObject);
};
Skylink.prototype._stats_buffer = {};
/**
* Function that handles buffer of stats data
* @method _handleDatachannelStats
* @private
* @for Skylink
* @since 0.6.35
*/
Skylink.prototype._manageStatsBuffer = function(operation, data, url){
var self = this;
if(self._stats_buffer[operation] === undefined){
self._stats_buffer[operation] = {};
self._stats_buffer[operation].url = url;
self._stats_buffer[operation].data = [];
}
self._stats_buffer[operation].data.push(data);
setInterval(function () {
for (var key in self._stats_buffer) {
if (self._stats_buffer[key]["data"].length > 0) {
self._postStats(self._stats_buffer[key]["url"], self._stats_buffer[key]["data"]);
self._stats_buffer[key]["data"] = [];
}
}
}, 5000);
};