403Webshell
Server IP : 80.87.202.40  /  Your IP : 216.73.216.169
Web Server : Apache
System : Linux rospirotorg.ru 5.14.0-539.el9.x86_64 #1 SMP PREEMPT_DYNAMIC Thu Dec 5 22:26:13 UTC 2024 x86_64
User : bitrix ( 600)
PHP Version : 8.2.27
Disable Function : NONE
MySQL : OFF |  cURL : ON |  WGET : ON |  Perl : ON |  Python : OFF |  Sudo : ON |  Pkexec : ON
Directory :  /home/bitrix/ext_www/rospirotorg.ru/bitrix/js/im/call/

Upload File :
current_dir [ Writeable] document_root [ Writeable]

 

Command :


[ Back ]     

Current File : /home/bitrix/ext_www/rospirotorg.ru/bitrix/js/im/call/voximplant_call.js
;(function()
{
	/**
	 * Implements Call interface
	 * Public methods:
	 * - inviteUsers
	 * - cancel
	 * - answer
	 * - decline
	 * - hangup
	 *
	 * Events:
	 * - onCallStateChanged //not sure about this.
	 * - onUserStateChanged
	 * - onStreamReceived
	 * - onStreamRemoved
	 * - onDestroy
	 */

	BX.namespace('BX.Call');

	var ajaxActions = {
		invite: 'im.call.invite',
		cancel: 'im.call.cancel',
		answer: 'im.call.answer',
		decline: 'im.call.decline',
		hangup: 'im.call.hangup',
		ping: 'im.call.ping'
	};

	var pullEvents = {
		ping: 'Call::ping',
		answer: 'Call::answer',
		hangup: 'Call::hangup',
		userInviteTimeout: 'Call::userInviteTimeout',
		repeatAnswer: 'Call::repeatAnswer',
	};

	var clientEvents = {
		voiceStarted: 'Call::voiceStarted',
		voiceStopped: 'Call::voiceStopped',
		microphoneState: 'Call::microphoneState',
		cameraState: 'Call::cameraState',
		videoPaused: 'Call::videoPaused',
		screenState: 'Call::screenState',
		recordState: 'Call::recordState',
		floorRequest: 'Call::floorRequest',
		emotion: 'Call::emotion',
		customMessage: 'Call::customMessage',
		showUsers: 'Call::showUsers',
		showAll: 'Call::showAll',
		hideAll: 'Call::hideAll',

		joinRoom: 'Call::joinRoom',
		leaveRoom: 'Call::leaveRoom',
		listRooms: 'Call::listRooms',
		requestRoomSpeaker: 'Call::requestRoomSpeaker',
	};

	var scenarioEvents = {
		viewerJoined: 'Call::viewerJoined',
		viewerLeft: 'Call::viewerLeft',

		joinRoomOffer: 'Call::joinRoomOffer',
		transferRoomHost: 'Call::transferRoomHost',
		listRoomsResponse: 'Call::listRoomsResponse',
		roomUpdated: 'Call::roomUpdated',
	};

	var VoximplantCallEvent = {
		onCallConference: 'VoximplantCall::onCallConference'
	};

	var pingPeriod = 5000;
	var backendPingPeriod = 25000;

	var reinvitePeriod = 5500;

	var connectionRestoreTime = 15000;

	var MAX_USERS_WITHOUT_SIMULCAST = 6;

	// screensharing workaround
	if(window["BXDesktopSystem"])
	{
		navigator['getDisplayMedia'] = function()
		{
			var mediaParams = {
				audio: false,
				video: {
					mandatory: {
						chromeMediaSource: 'desktop',
						maxWidth: screen.width > 1920 ? screen.width : 1920,
						maxHeight: screen.height > 1080 ? screen.height : 1080,
					},
					optional: [{googTemporalLayeredScreencast: true}],
				},
			};
			return navigator.mediaDevices.getUserMedia(mediaParams);
		};
	}

	BX.Call.VoximplantCall = function(config)
	{
		BX.Call.VoximplantCall.superclass.constructor.apply(this, arguments);

		this.videoQuality = BX.Call.Quality.VeryHigh; // initial video quality. will drop on new peers connecting

		this.voximplantCall = null;

		this.signaling = new BX.Call.VoximplantCall.Signaling({
			call: this
		});

		this.peers = {};
		this.joinedElsewhere = false;
		this.joinedAsViewer = false;
		this.localVideoShown = false;
		this._localUserState = BX.Call.UserState.Idle;
		this.clientEventsBound = false;
		this._screenShared = false;
		this.videoAllowedFrom = BX.Call.UserMnemonic.all;
		this.direction = BX.Call.EndpointDirection.SendRecv;

		this.localVAD = null;
		this.microphoneLevelInterval = null;

		this.rooms = {};

		Object.defineProperty(this, "screenShared", {
			get: function() {
				return this._screenShared;
			},
			set: function(screenShared) {
				if (screenShared != this._screenShared)
				{
					this._screenShared = screenShared;
					this.signaling.sendScreenState(this._screenShared);
				}
			}
		});

		Object.defineProperty(this, "localUserState", {
			get: function()
			{
				return this._localUserState
			},
			set: function (state)
			{
				if (state == this._localUserState)
				{
					return;
				}
				this.runCallback(BX.Call.Event.onUserStateChanged, {
					userId: this.userId,
					state: state,
					previousState: this._localUserState,
					direction: this.direction,
				});
				this._localUserState = state;
			}
		});

		this.deviceList = [];

		// event handlers
		this.__onLocalDevicesUpdatedHandler = this.__onLocalDevicesUpdated.bind(this);
		this.__onLocalMediaRendererAddedHandler = this.__onLocalMediaRendererAdded.bind(this);
		this.__onBeforeLocalMediaRendererRemovedHandler = this.__onBeforeLocalMediaRendererRemoved.bind(this);
		this.__onMicAccessResultHandler = this.__onMicAccessResult.bind(this);
		this.__onClientReconnectingHandler = this.__onClientReconnecting.bind(this);
		this.__onClientReconnectedHandler = this.__onClientReconnected.bind(this);

		this.__onCallDisconnectedHandler = this.__onCallDisconnected.bind(this);
		this.__onCallMessageReceivedHandler = this.__onCallMessageReceived.bind(this);
		this.__onCallStatsReceivedHandler = this.__onCallStatsReceived.bind(this);
		this.__onCallEndpointAddedHandler = this.__onCallEndpointAdded.bind(this);
		this.__onCallReconnectingHandler = this.__onCallReconnecting.bind(this);
		this.__onCallReconnectedHandler = this.__onCallReconnected.bind(this);

		this.__onWindowUnloadHandler = this.__onWindowUnload.bind(this);
		window.addEventListener("unload", this.__onWindowUnloadHandler);

		this.initPeers();

		this.pingUsersInterval = setInterval(this.pingUsers.bind(this), pingPeriod);
		this.pingBackendInterval = setInterval(this.pingBackend.bind(this), backendPingPeriod);

		this.lastPingReceivedTimeout = null;
		this.lastSelfPingReceivedTimeout = null;

		this.reinviteTimeout = null;

		// There are two kinds of reconnection events: from call (for media connection) and from client (for signaling).
		// So we have to use counter to convert these two events to one
		this._reconnectionEventCount = 0;
		Object.defineProperty(this, 'reconnectionEventCount', {
			get: function()
			{
				return this._reconnectionEventCount;
			},
			set: function(newValue)
			{
				if (this._reconnectionEventCount === 0 && newValue > 0)
				{
					this.runCallback(BX.Call.Event.onReconnecting);
				}
				if (newValue === 0)
				{
					this.runCallback(BX.Call.Event.onReconnected);
				}
				this._reconnectionEventCount = newValue;
			}
		})
	};

	BX.extend(BX.Call.VoximplantCall, BX.Call.AbstractCall);

	BX.Call.VoximplantCall.prototype.initPeers = function ()
	{
		this.users.forEach(function(userId)
		{
			userId = Number(userId);
			this.peers[userId] = this.createPeer(userId);
		}, this);
	};

	BX.Call.VoximplantCall.prototype.reinitPeers = function ()
	{
		for (var userId in this.peers)
		{
			if(this.peers.hasOwnProperty(userId) && this.peers[userId])
			{
				this.peers[userId].destroy();
				this.peers[userId] = null;
			}
		}

		this.initPeers();
	};

	BX.Call.VoximplantCall.prototype.pingUsers = function()
	{
		if (this.ready)
		{
			var users = this.users.concat(this.userId);
			this.signaling.sendPingToUsers({userId: users}, true);
		}
	};

	BX.Call.VoximplantCall.prototype.pingBackend = function()
	{
		if (this.ready)
		{
			this.signaling.sendPingToBackend();
		}
	};

	BX.Call.VoximplantCall.prototype.createPeer = function (userId)
	{
		var incomingVideoAllowed;
		if (this.videoAllowedFrom === BX.Call.UserMnemonic.all)
		{
			incomingVideoAllowed = true;
		}
		else if (this.videoAllowedFrom === BX.Call.UserMnemonic.none)
		{
			incomingVideoAllowed = false;
		}
		else if (BX.type.isArray(this.videoAllowedFrom))
		{
			incomingVideoAllowed = this.videoAllowedFrom.some(function(allowedUserId) {
				return allowedUserId == userId;
			});
		}
		else
		{
			incomingVideoAllowed = true;
		}

		return new BX.Call.VoximplantCall.Peer({
			call: this,
			userId: userId,
			ready: userId == this.initiatorId,
			isIncomingVideoAllowed: incomingVideoAllowed,

			onMediaReceived: function(e)
			{
				console.log("onMediaReceived: ", e);
				this.runCallback(BX.Call.Event.onRemoteMediaReceived, e);
				if (e.kind === 'video')
				{
					this.runCallback(BX.Call.Event.onUserVideoPaused, {
						userId: userId,
						videoPaused: false
					});
				}
			}.bind(this),
			onMediaRemoved: function(e)
			{
				console.log("onMediaRemoved: ", e);
				this.runCallback(BX.Call.Event.onRemoteMediaStopped, e);
			}.bind(this),
			onVoiceStarted: function(e)
			{
				// todo: uncomment to switch to SDK VAD events
				/*this.runCallback(BX.Call.Event.onUserVoiceStarted, {
					userId: userId
				});*/
			}.bind(this),
			onVoiceEnded: function(e)
			{
				// todo: uncomment to switch to SDK VAD events
				/*this.runCallback(BX.Call.Event.onUserVoiceStopped, {
					userId: userId
				});*/
			}.bind(this),
			onStateChanged: this.__onPeerStateChanged.bind(this),
			onInviteTimeout: this.__onPeerInviteTimeout.bind(this),

		})
	};

	BX.Call.VoximplantCall.prototype.getUsers = function ()
	{
		var result = {};
		for (var userId in this.peers)
		{
			result[userId] = this.peers[userId].calculatedState;
		}
		return result;
	};

	BX.Call.VoximplantCall.prototype.getUserCount = function ()
	{
		return Object.keys(this.peers).length;
	};

	BX.Call.VoximplantCall.prototype.getClient = function()
	{
		return new Promise(function(resolve, reject)
		{
			BX.Voximplant.getClient({restClient: BX.CallEngine.getRestClient()}).then(function(client)
			{
				client.enableSilentLogging();
				client.setLoggerCallback(function(e)
				{
					this.log(e.label + ": " + e.message);
				}.bind(this));
				this.log("User agent: " + navigator.userAgent);
				this.log("Voximplant SDK version: " + VoxImplant.version);

				this.bindClientEvents();

				resolve(client);
			}.bind(this)).catch(reject);
		}.bind(this));
	};

	BX.Call.VoximplantCall.prototype.bindClientEvents = function()
	{
		var streamManager = VoxImplant.Hardware.StreamManager.get();

		if(!this.clientEventsBound)
		{
			VoxImplant.getInstance().on(VoxImplant.Events.MicAccessResult, this.__onMicAccessResultHandler);
			if (VoxImplant.Events.Reconnecting)
			{
				VoxImplant.getInstance().on(VoxImplant.Events.Reconnecting, this.__onClientReconnectingHandler);
				VoxImplant.getInstance().on(VoxImplant.Events.Reconnected, this.__onClientReconnectedHandler);
			}

			streamManager.on(VoxImplant.Hardware.HardwareEvents.DevicesUpdated, this.__onLocalDevicesUpdatedHandler);
			streamManager.on(VoxImplant.Hardware.HardwareEvents.MediaRendererAdded, this.__onLocalMediaRendererAddedHandler);
			streamManager.on(VoxImplant.Hardware.HardwareEvents.MediaRendererUpdated, this.__onLocalMediaRendererAddedHandler);
			streamManager.on(VoxImplant.Hardware.HardwareEvents.BeforeMediaRendererRemoved, this.__onBeforeLocalMediaRendererRemovedHandler);
			this.clientEventsBound = true;
		}
	};

	BX.Call.VoximplantCall.prototype.removeClientEvents = function()
	{
		if (!('VoxImplant' in window))
		{
			return;
		}

		VoxImplant.getInstance().off(VoxImplant.Events.MicAccessResult, this.__onMicAccessResultHandler);
		if (VoxImplant.Events.Reconnecting)
		{
			VoxImplant.getInstance().off(VoxImplant.Events.Reconnecting, this.__onClientReconnectingHandler);
			VoxImplant.getInstance().off(VoxImplant.Events.Reconnected, this.__onClientReconnectedHandler);
		}

		var streamManager = VoxImplant.Hardware.StreamManager.get();
		streamManager.off(VoxImplant.Hardware.HardwareEvents.DevicesUpdated, this.__onLocalDevicesUpdatedHandler);
		streamManager.off(VoxImplant.Hardware.HardwareEvents.MediaRendererAdded, this.__onLocalMediaRendererAddedHandler);
		streamManager.off(VoxImplant.Hardware.HardwareEvents.BeforeMediaRendererRemoved, this.__onBeforeLocalMediaRendererRemovedHandler);
		this.clientEventsBound = false;
	};

	BX.Call.VoximplantCall.prototype.setMuted = function(muted)
	{
		if(this.muted == muted)
		{
			return;
		}

		this.muted = muted;

		if(this.voximplantCall)
		{
			if(this.muted)
			{
				this.voximplantCall.muteMicrophone();
			}
			else
			{
				this.voximplantCall.unmuteMicrophone();
			}
			this.signaling.sendMicrophoneState(!this.muted);
		}
	};

	BX.Call.VoximplantCall.prototype.isMuted = function()
	{
		return this.muted;
	}

	BX.Call.VoximplantCall.prototype.setVideoEnabled = function(videoEnabled)
	{
		videoEnabled = (videoEnabled === true);
		if(this.videoEnabled == videoEnabled)
		{
			return;
		}

		this.videoEnabled = videoEnabled;
		if(this.voximplantCall)
		{
			if(videoEnabled)
			{
				this._showLocalVideo();
			}
			else
			{
				if(this.localVideoShown)
				{
					VoxImplant.Hardware.StreamManager.get().hideLocalVideo().then(function()
					{
						this.localVideoShown = false;
						this.runCallback(BX.Call.Event.onLocalMediaReceived, {
							tag: "main",
							stream: new MediaStream(),
						});
					}.bind(this));
				}
			}

			this.voximplantCall.sendVideo(this.videoEnabled);
			this.signaling.sendCameraState(this.videoEnabled);
		}
	};

	BX.Call.VoximplantCall.prototype.setCameraId = function(cameraId)
	{
		if(this.cameraId == cameraId)
		{
			return;
		}
		this.cameraId = cameraId;

		if(this.voximplantCall)
		{
			VoxImplant.Hardware.CameraManager.get().getInputDevices().then(function()
			{
				VoxImplant.Hardware.CameraManager.get().setCallVideoSettings(this.voximplantCall, this.constructCameraParams());
			}.bind(this));
		}
	};

	BX.Call.VoximplantCall.prototype.setMicrophoneId = function(microphoneId)
	{
		if(this.microphoneId == microphoneId)
		{
			return;
		}

		this.microphoneId = microphoneId;
		if(this.voximplantCall)
		{
			VoxImplant.Hardware.AudioDeviceManager.get().getInputDevices().then(function(){
				VoxImplant.Hardware.AudioDeviceManager.get().setCallAudioSettings(this.voximplantCall, {
					inputId: this.microphoneId
				});
			}.bind(this));
		}
	};

	BX.Call.VoximplantCall.prototype.getCurrentMicrophoneId = function()
	{
		if (this.voximplantCall.peerConnection.impl.getTransceivers)
		{
			var transceivers = this.voximplantCall.peerConnection.impl.getTransceivers();
			if(transceivers.length > 0)
			{
				var audioTrack = transceivers[0].sender.track;
				var audioTrackSettings = audioTrack.getSettings();
				return audioTrackSettings.deviceId;
			}
		}
		return this.microphoneId;
	};

	BX.Call.VoximplantCall.prototype.constructCameraParams = function()
	{
		var result = {};

		if(this.cameraId)
		{
			result.cameraId = this.cameraId;
		}

		result.videoQuality = this.videoHd ? VoxImplant.Hardware.VideoQuality.VIDEO_SIZE_HD : VoxImplant.Hardware.VideoQuality.VIDEO_SIZE_nHD;
		result.facingMode = true;
		return result;
	};

	BX.Call.VoximplantCall.prototype.useHdVideo = function(flag)
	{
		this.videoHd = (flag === true);
	};

	BX.Call.VoximplantCall.prototype.requestFloor = function(requestActive)
	{
		this.signaling.sendFloorRequest(requestActive);
	};

	BX.Call.VoximplantCall.prototype.sendRecordState = function(recordState)
	{
		this.signaling.sendRecordState(recordState);
	};

	BX.Call.VoximplantCall.prototype.sendEmotion = function(toUserId, emotion)
	{
		this.signaling.sendEmotion(toUserId, emotion);
	};

	BX.Call.VoximplantCall.prototype.sendCustomMessage = function(message, repeatOnConnect)
	{
		this.signaling.sendCustomMessage(message, repeatOnConnect);
	};

	/**
	 * Updates list of users,
	 * @param {BX.Call.UserMnemonic | int[]} userList
	 */
	BX.Call.VoximplantCall.prototype.allowVideoFrom = function(userList)
	{
		if (this.videoAllowedFrom == userList)
		{
			return;
		}
		this.videoAllowedFrom = userList;

		if (userList === BX.Call.UserMnemonic.all)
		{
			this.signaling.sendShowAll();
			userList = Object.keys(this.peers);
		}
		else if (userList === BX.Call.UserMnemonic.none)
		{
			this.signaling.sendHideAll();
			userList = [];
		}
		else if (BX.type.isArray(userList))
		{
			this.signaling.sendShowUsers(userList)
		}
		else
		{
			throw new Error("userList is in wrong format");
		}

		var users = {};
		userList.forEach(function(userId)
		{
			users[userId] = true;
		});

		for (var userId in this.peers)
		{
			if(!this.peers.hasOwnProperty(userId))
			{
				continue;
			}
			if(users[userId])
			{
				this.peers[userId].allowIncomingVideo(true);
			}
			else
			{
				this.peers[userId].allowIncomingVideo(false);
			}
		}
	};

	/**
	 * Sets bitrate cap for outgoing video
	 * @param bitrate
	 */
	BX.Call.VoximplantCall.prototype._setMaxBitrate = function(bitrate)
	{
		if(this.voximplantCall)
		{
			var transceivers = this.voximplantCall.peerConnection.getTransceivers();
			if(!transceivers)
			{
				return;
			}
			transceivers.forEach(function (tr)
			{
				if(tr.sender && tr.sender.track && tr.sender.track.kind === 'video' && !tr.stoped && tr.currentDirection.indexOf('send') !== -1)
				{
					var sender = tr.sender;
					var parameters = sender.getParameters();
					if (!parameters.encodings)
					{
						parameters.encodings = [{}];
					}
					if(bitrate === 0)
					{
						delete parameters.encodings[0].maxBitrate;
					}
					else
					{
						parameters.encodings[0].maxBitrate = bitrate * 1000;
					}
					sender.setParameters(parameters);
				}
			}, this);
		}
	};

	BX.Call.VoximplantCall.prototype._showLocalVideo = function()
	{
		return new Promise(function(resolve, reject)
		{
			VoxImplant.Hardware.StreamManager.get().showLocalVideo(false).then(
				function()
				{
					this.localVideoShown = true;
					resolve();
				}.bind(this),
				function()
				{
					this.localVideoShown = true;
					resolve();
				}.bind(this)
			)
		}.bind(this))
	};

	BX.Call.VoximplantCall.prototype._hideLocalVideo = function()
	{
		return new Promise(function(resolve, reject)
		{
			if (!('VoxImplant' in window))
			{
				resolve();
				return;
			}

			VoxImplant.Hardware.StreamManager.get().hideLocalVideo().then(
				function()
				{
					this.localVideoShown = false;
					resolve();
				}.bind(this),
				function()
				{
					this.localVideoShown = false;
					resolve();
				}.bind(this)
			);
		})
	};

	BX.Call.VoximplantCall.prototype.startScreenSharing = function()
	{
		if(!this.voximplantCall)
		{
			return;
		}

		var showLocalView = !this.videoEnabled;
		var replaceTrack = this.videoEnabled || this.screenShared;

		this.voximplantCall.shareScreen(showLocalView, replaceTrack).then(function()
		{
			this.log("Screen shared");
			this.screenShared = true;
		}.bind(this)).catch(function(error)
		{
			console.error(error);
			this.log("Screen sharing error:", error)
		}.bind(this));
	};

	BX.Call.VoximplantCall.prototype.stopScreenSharing = function()
	{
		if(!this.voximplantCall)
		{
			return;
		}

		this.voximplantCall.stopSharingScreen().then(function()
		{
			this.log("Screen is no longer shared");
			this.screenShared = false;
		}.bind(this));
	};

	BX.Call.VoximplantCall.prototype.isScreenSharingStarted = function()
	{
		return this.screenShared;
	};

	/**
	 * Invites users to participate in the call.
	 *
	 * @param {Object} config
	 * @param {int[]} [config.users] Array of ids of the users to be invited.
	 */
	BX.Call.VoximplantCall.prototype.inviteUsers = function(config)
	{
		var self = this;
		this.ready = true;
		if(!BX.type.isPlainObject(config))
		{
			config = {};
		}
		var users = BX.type.isArray(config.users) ? config.users : this.users;

		this.attachToConference().then(function()
		{
			self.signaling.sendPingToUsers({userId: users});

			if(users.length > 0)
			{
				return self.signaling.inviteUsers({
					userIds: users,
					video: self.videoEnabled ? 'Y' : 'N'
				})
			}
		}).then(function(response)
		{
			self.state = BX.Call.State.Connected;
			self.runCallback(BX.Call.Event.onJoin, {
				local: true
			});
			for (var i = 0; i < users.length; i++)
			{
				var userId = parseInt(users[i], 10);
				if(!self.users.includes(userId))
				{
					self.users.push(userId);
				}
				if(!self.peers[userId])
				{
					self.peers[userId] = self.createPeer(userId);

					if (self.type === BX.Call.Type.Instant)
					{
						self.runCallback(BX.Call.Event.onUserInvited, {
							userId: userId
						});
					}
				}
				if (self.type === BX.Call.Type.Instant)
				{
					self.peers[userId].onInvited();
					self.scheduleRepeatInvite();
				}
			}
		}).catch(self.onFatalError.bind(self));
	};

	BX.Call.VoximplantCall.prototype.scheduleRepeatInvite = function()
	{
		clearTimeout(this.reinviteTimeout);
		this.reinviteTimeout = setTimeout(this.repeatInviteUsers.bind(this), reinvitePeriod)
	};

	BX.Call.VoximplantCall.prototype.repeatInviteUsers = function()
	{
		clearTimeout(this.reinviteTimeout);
		if(!this.ready)
		{
			return;
		}
		var usersToRepeatInvite = [];

		for (var userId in this.peers)
		{
			if(this.peers.hasOwnProperty(userId) && this.peers[userId].calculatedState === BX.Call.UserState.Calling)
			{
				usersToRepeatInvite.push(userId);
			}
		}

		if(usersToRepeatInvite.length === 0)
		{
			return;
		}
		this.signaling.inviteUsers({
			userIds: usersToRepeatInvite,
			video: this.videoEnabled ? 'Y' : 'N',
			isRepeated: 'Y',
		}).then(function()
		{
			this.scheduleRepeatInvite();
		}.bind(this));
	};

	/**
	 * @param {Object} config
	 * @param {bool} [config.useVideo]
	 */
	BX.Call.VoximplantCall.prototype.answer = function(config)
	{
		this.ready = true;
		var joinAsViewer = BX.prop.getBoolean(config, "joinAsViewer", false);
		if(!BX.type.isPlainObject(config))
		{
			config = {};
		}
		this.videoEnabled = (config.useVideo == true);

		if (!joinAsViewer)
		{
			this.signaling.sendAnswer();
		}
		this.attachToConference({joinAsViewer: joinAsViewer}).then(() =>
		{
			this.log("Attached to conference");
			this.state = BX.Call.State.Connected;
			this.runCallback(BX.Call.Event.onJoin, {
				local: true
			});
		}).catch((err) => {
			this.onFatalError(err);
		});
	};

	BX.Call.VoximplantCall.prototype.decline = function(code)
	{
		this.ready = false;
		var data = {
			callId: this.id,
			callInstanceId: this.instanceId,
		};
		if(code)
		{
			data.code = code
		}

		BX.CallEngine.getRestClient().callMethod(ajaxActions.decline, data);
	};

	BX.Call.VoximplantCall.prototype.hangup = function(code, reason)
	{
		if(!this.ready)
		{
			var error = new Error("Hangup in wrong state");
			this.log(error);
			return;
		}

		var tempError = new Error();
		tempError.name = "Call stack:";
		this.log("Hangup received \n" + tempError.stack);

		if (this.localVAD)
		{
			this.localVAD.destroy();
			this.localVAD = null;
		}
		clearInterval(this.microphoneLevelInterval);

		var data = {};
		this.ready = false;
		if(typeof(code) != 'undefined')
		{
			data.code = code;
		}
		if(typeof(reason) != 'undefined')
		{
			data.reason = reason;
		}
		this.state = BX.Call.State.Proceeding;
		this.runCallback(BX.Call.Event.onLeave, {local: true});

		//clone users and append current user id to send event to all participants of the call
		data.userId = this.users.slice(0).concat(this.userId);
		this.signaling.sendHangup(data);
		this.muted = false;

		// for future reconnections
		this.reinitPeers();

		if(this.voximplantCall)
		{
			this.voximplantCall._replaceVideoSharing = false;
			try
			{
				this.voximplantCall.hangup();
			}
			catch (e)
			{
				this.log("Voximplant hangup error: ", e);
				console.error("Voximplant hangup error: ", e);
			}
		}
		else
		{
			this.log("Tried to hangup, but this.voximplantCall points nowhere");
			console.error("Tried to hangup, but this.voximplantCall points nowhere");
		}

		this.screenShared = false;
		this._hideLocalVideo();
	};

	BX.Call.VoximplantCall.prototype.attachToConference = function(options)
	{
		var self = this;

		var joinAsViewer = BX.prop.getBoolean(options, "joinAsViewer", false);

		return new Promise(function(resolve, reject)
		{
			if(self.voximplantCall && self.voximplantCall.state() === "CONNECTED")
			{
				if (self.joinedAsViewer === joinAsViewer)
				{
					return resolve();
				}
				else
				{
					return reject("Already joined call in another mode");
				}
			}

			self.direction = joinAsViewer ? BX.Call.EndpointDirection.RecvOnly : BX.Call.EndpointDirection.SendRecv;
			self.sendTelemetryEvent("call");

			self.getClient().then(function(voximplantClient)
			{
				self.localUserState = BX.Call.UserState.Connecting;

				// workaround to set default video settings before starting call. ugly, but I do not see another way
				VoxImplant.Hardware.CameraManager.get().setDefaultVideoSettings(self.constructCameraParams());
				if (self.microphoneId)
				{
					VoxImplant.Hardware.AudioDeviceManager.get().setDefaultAudioSettings({
						inputId: self.microphoneId
					});
				}

				if(self.videoEnabled)
				{
					self._showLocalVideo();
				}

				try
				{
					if (joinAsViewer)
					{
						self.voximplantCall = voximplantClient.joinAsViewer("bx_conf_" + self.id, {
							'X-Direction': BX.Call.EndpointDirection.RecvOnly
						});
					}
					else
					{
						self.voximplantCall = voximplantClient.callConference({
							number: "bx_conf_" + self.id,
							video: {sendVideo: self.videoEnabled, receiveVideo: true},
							// simulcast: (self.getUserCount() > MAX_USERS_WITHOUT_SIMULCAST),
							// simulcastProfileName: 'b24',
							customData: JSON.stringify({
								cameraState: self.videoEnabled,
							})
						});
					}
				}
				catch (e)
				{
					console.error(e);
					return reject(e);
				}
				self.joinedAsViewer = joinAsViewer;

				if(!self.voximplantCall)
				{
					self.log("Error: could not create voximplant call");
					return reject({code: "VOX_NO_CALL"});
				}

				self.runCallback(BX.Call.VoximplantCall.Event.onCallConference, {
					call: self
				});

				self.bindCallEvents();

				var onCallConnected = function()
				{
					self.log("Call connected");
					self.sendTelemetryEvent("connect");
					self.localUserState = BX.Call.UserState.Connected;

					self.voximplantCall.removeEventListener(VoxImplant.CallEvents.Connected, onCallConnected);
					self.voximplantCall.removeEventListener(VoxImplant.CallEvents.Failed, onCallFailed);

					self.voximplantCall.addEventListener(VoxImplant.CallEvents.Failed, self.__onCallDisconnectedHandler);

					if(self.deviceList.length === 0)
					{
						navigator.mediaDevices.enumerateDevices().then(function(deviceList)
						{
							self.deviceList = deviceList;
							self.runCallback(BX.Call.Event.onDeviceListUpdated, {
								deviceList: self.deviceList
							})
						});
					}
					else
					{
						self.runCallback(BX.Call.Event.onDeviceListUpdated, {
							deviceList: self.deviceList
						})
					}
					if(self.muted)
					{
						self.voximplantCall.muteMicrophone();
					}
					self.signaling.sendMicrophoneState(!self.muted);
					self.signaling.sendCameraState(self.videoEnabled);

					if (self.videoAllowedFrom == BX.Call.UserMnemonic.none)
					{
						self.signaling.sendHideAll();
					}
					else if (BX.type.isArray(self.videoAllowedFrom))
					{
						self.signaling.sendShowUsers(self.videoAllowedFrom);
					}

					resolve();
				};

				var onCallFailed = function(e)
				{
					self.log("Could not attach to conference", e);
					self.sendTelemetryEvent("connect_failure");
					self.localUserState = BX.Call.UserState.Failed;

					self.voximplantCall.removeEventListener(VoxImplant.CallEvents.Connected, onCallConnected);
					self.voximplantCall.removeEventListener(VoxImplant.CallEvents.Failed, onCallFailed);

					var client = VoxImplant.getInstance();
					client.enableSilentLogging(false);
					client.setLoggerCallback(null);

					reject(e);
				};

				self.voximplantCall.addEventListener(VoxImplant.CallEvents.Connected, onCallConnected);
				self.voximplantCall.addEventListener(VoxImplant.CallEvents.Failed, onCallFailed);
			}).catch(self.onFatalError.bind(self));
		});
	};

	BX.Call.VoximplantCall.prototype.bindCallEvents = function()
	{
		this.voximplantCall.addEventListener(VoxImplant.CallEvents.Disconnected, this.__onCallDisconnectedHandler);
		this.voximplantCall.addEventListener(VoxImplant.CallEvents.MessageReceived, this.__onCallMessageReceivedHandler);
		if (BX.Call.Util.shouldCollectStats())
		{
			this.voximplantCall.addEventListener(VoxImplant.CallEvents.CallStatsReceived, this.__onCallStatsReceivedHandler);
		}

		this.voximplantCall.addEventListener(VoxImplant.CallEvents.EndpointAdded, this.__onCallEndpointAddedHandler);
		if (VoxImplant.CallEvents.Reconnecting)
		{
			this.voximplantCall.addEventListener(VoxImplant.CallEvents.Reconnecting, this.__onCallReconnectingHandler);
			this.voximplantCall.addEventListener(VoxImplant.CallEvents.Reconnected, this.__onCallReconnectedHandler);
		}
	};

	BX.Call.VoximplantCall.prototype.removeCallEvents = function()
	{
		if(this.voximplantCall)
		{
			this.voximplantCall.removeEventListener(VoxImplant.CallEvents.Disconnected, this.__onCallDisconnectedHandler);
			this.voximplantCall.removeEventListener(VoxImplant.CallEvents.MessageReceived, this.__onCallMessageReceivedHandler);
			if (BX.Call.Util.shouldCollectStats())
			{
				this.voximplantCall.removeEventListener(VoxImplant.CallEvents.CallStatsReceived, this.__onCallStatsReceivedHandler);
			}
			this.voximplantCall.removeEventListener(VoxImplant.CallEvents.EndpointAdded, this.__onCallEndpointAddedHandler);
			if (VoxImplant.CallEvents.Reconnecting)
			{
				this.voximplantCall.removeEventListener(VoxImplant.CallEvents.Reconnecting, this.__onCallReconnectingHandler);
				this.voximplantCall.removeEventListener(VoxImplant.CallEvents.Reconnected, this.__onCallReconnectedHandler);
			}
		}
	};

	/**
	 * Adds new users to call
	 * @param {Number[]} users
	 */
	BX.Call.VoximplantCall.prototype.addJoinedUsers = function(users)
	{
		for(var i = 0; i < users.length; i++)
		{
			var userId = Number(users[i]);
			if(userId == this.userId || this.peers[userId])
			{
				continue;
			}
			this.peers[userId] = this.createPeer(userId);
			if(!this.users.includes(userId))
			{
				this.users.push(userId);
			}
			this.runCallback(BX.Call.Event.onUserInvited, {
				userId: userId
			});
		}
	};

	/**
	 * Adds users, invited by you or someone else
	 * @param {Number[]} users
	 */
	BX.Call.VoximplantCall.prototype.addInvitedUsers = function(users)
	{
		for(var i = 0; i < users.length; i++)
		{
			var userId = Number(users[i]);
			if(userId == this.userId)
			{
				continue;
			}

			if(this.peers[userId])
			{
				if(this.peers[userId].calculatedState === BX.Call.UserState.Failed || this.peers[userId].calculatedState === BX.Call.UserState.Idle)
				{
					if (this.type === BX.Call.Type.Instant)
					{
						this.peers[userId].onInvited();
					}
				}
			}
			else
			{
				this.peers[userId] = this.createPeer(userId);
				if (this.type === BX.Call.Type.Instant)
				{
					this.peers[userId].onInvited();
				}
			}
			if(!this.users.includes(userId))
			{
				this.users.push(userId);
			}
			this.runCallback(BX.Call.Event.onUserInvited, {
				userId: userId
			});
		}
	};

	BX.Call.VoximplantCall.prototype.isAnyoneParticipating = function()
	{
		for (var userId in this.peers)
		{
			if(this.peers[userId].isParticipating())
			{
				return true;
			}
		}

		return false;
	};

	BX.Call.VoximplantCall.prototype.getParticipatingUsers = function()
	{
		var result = [];
		for (var userId in this.peers)
		{
			if(this.peers[userId].isParticipating())
			{
				result.push(userId);
			}
		}
		return result;
	};

	BX.Call.VoximplantCall.prototype.updateRoom = function(roomData)
	{
		if (!this.rooms[roomData.id])
		{
			this.rooms[roomData.id] = {
				id: roomData.id,
				users: roomData.users,
				speaker: roomData.speaker
			}
		}
		else
		{
			this.rooms[roomData.id].users = roomData.users;
			this.rooms[roomData.id].speaker = roomData.speaker;
		}
	}

	BX.Call.VoximplantCall.prototype.currentRoom = function()
	{
		return this._currentRoomId ? this.rooms[this._currentRoomId] : null;
	}

	BX.Call.VoximplantCall.prototype.isRoomSpeaker = function()
	{
		return this.currentRoom() ? this.currentRoom().speaker == this.userId : false;
	}

	BX.Call.VoximplantCall.prototype.joinRoom = function(roomId)
	{
		this.signaling.sendJoinRoom(roomId);
	}

	BX.Call.VoximplantCall.prototype.requestRoomSpeaker = function()
	{
		this.signaling.sendRequestRoomSpeaker(this._currentRoomId);
	}

	BX.Call.VoximplantCall.prototype.leaveCurrentRoom = function()
	{
		this.signaling.sendLeaveRoom(this._currentRoomId);
	}

	BX.Call.VoximplantCall.prototype.listRooms = function()
	{
		return new Promise((resolve, reject) =>
		{
			this.signaling.sendListRooms();
			this.__resolveListRooms = resolve;
		});
	}

	BX.Call.VoximplantCall.prototype.__onPeerStateChanged = function(e)
	{
		this.runCallback(BX.Call.Event.onUserStateChanged, e);

		if(!this.ready)
		{
			return;
		}
		if(e.state == BX.Call.UserState.Failed || e.state == BX.Call.UserState.Unavailable || e.state == BX.Call.UserState.Declined || e.state == BX.Call.UserState.Idle)
		{
			if(this.type == BX.Call.Type.Instant && !this.isAnyoneParticipating())
			{
				this.hangup();
			}
		}
	};

	BX.Call.VoximplantCall.prototype.__onPeerInviteTimeout = function(e)
	{
		if(!this.ready)
		{
			return;
		}
		this.signaling.sendUserInviteTimeout({
			userId: this.users,
			failedUserId: e.userId
		})
	};

	BX.Call.VoximplantCall.prototype.__onPullEvent = function(command, params, extra)
	{
		var handlers = {
			'Call::answer': this.__onPullEventAnswer.bind(this),
			'Call::hangup': this.__onPullEventHangup.bind(this),
			'Call::usersJoined': this.__onPullEventUsersJoined.bind(this),
			'Call::usersInvited': this.__onPullEventUsersInvited.bind(this),
			'Call::userInviteTimeout': this.__onPullEventUserInviteTimeout.bind(this),
			'Call::ping': this.__onPullEventPing.bind(this),
			'Call::finish': this.__onPullEventFinish.bind(this),
			'Call::repeatAnswer': this.__onPullEventRepeatAnswer.bind(this),
		};

		if(handlers[command])
		{
			if (command != 'Call::ping')
			{
				this.log("Signaling: " + command + "; Parameters: " + JSON.stringify(params));
			}
			handlers[command].call(this, params);
		}
	};

	BX.Call.VoximplantCall.prototype.__onPullEventAnswer = function(params)
	{
		var senderId = Number(params.senderId);

		if(senderId == this.userId)
		{
			return this.__onPullEventAnswerSelf(params);
		}

		if(!this.peers[senderId])
		{
			this.peers[senderId] = this.createPeer(senderId);
			this.runCallback(BX.Call.Event.onUserInvited, {
				userId: senderId
			});
		}

		if(!this.users.includes(senderId))
		{
			this.users.push(senderId);
		}

		this.peers[senderId].setReady(true);
	};

	BX.Call.VoximplantCall.prototype.__onPullEventAnswerSelf = function(params)
	{
		if(params.callInstanceId === this.instanceId)
		{
			return;
		}

		// call was answered elsewhere
		this.joinedElsewhere = true;
		this.runCallback(BX.Call.Event.onJoin, {
			local: false
		});
	};


	BX.Call.VoximplantCall.prototype.__onPullEventHangup = function(params)
	{
		var senderId = params.senderId;

		if(this.userId == senderId && this.instanceId != params.callInstanceId)
		{
			// Call declined by the same user elsewhere
			this.runCallback(BX.Call.Event.onLeave, {local: false});
			return;
		}

		if(!this.peers[senderId])
			return;

		this.peers[senderId].setReady(false);

		if(params.code == 603)
		{
			this.peers[senderId].setDeclined(true);
		}
		else if (params.code == 486)
		{
			this.peers[senderId].setBusy(true);
			console.error("user " + senderId + " is busy");
		}

		if(this.ready && this.type == BX.Call.Type.Instant && !this.isAnyoneParticipating())
		{
			this.hangup();
		}
	};

	BX.Call.VoximplantCall.prototype.__onPullEventUsersJoined = function(params)
	{
		this.log('__onPullEventUsersJoined', params);
		var users = params.users;

		this.addJoinedUsers(users);
	};

	BX.Call.VoximplantCall.prototype.__onPullEventUsersInvited = function(params)
	{
		this.log('__onPullEventUsersInvited', params);
		var users = params.users;

		if (this.type === BX.Call.Type.Instant)
		{
			this.addInvitedUsers(users);
		}
	};

	BX.Call.VoximplantCall.prototype.__onPullEventUserInviteTimeout = function(params)
	{
		this.log('__onPullEventUserInviteTimeout', params);
		var failedUserId = params.failedUserId;

		if(this.peers[failedUserId])
		{
			this.peers[failedUserId].onInviteTimeout(false);
		}
	};

	BX.Call.VoximplantCall.prototype.__onPullEventPing = function(params)
	{
		if(params.callInstanceId == this.instanceId)
		{
			// ignore self ping
			return;
		}

		var senderId = Number(params.senderId);

		if (senderId == this.userId)
		{
			if (!this.joinedElsewhere)
			{
				this.runCallback(BX.Call.Event.onJoin, {
					local: false
				});
				this.joinedElsewhere = true;
			}
			clearTimeout(this.lastSelfPingReceivedTimeout);
			this.lastSelfPingReceivedTimeout = setTimeout(this.__onNoSelfPingsReceived.bind(this), pingPeriod * 2.1);
		}
		clearTimeout(this.lastPingReceivedTimeout);
		this.lastPingReceivedTimeout = setTimeout(this.__onNoPingsReceived.bind(this), pingPeriod * 2.1);
		if(this.peers[senderId])
		{
			this.peers[senderId].setReady(true);
		}
	};

	BX.Call.VoximplantCall.prototype.__onNoPingsReceived = function()
	{
		if(!this.ready)
		{
			this.destroy();
		}
	};

	BX.Call.VoximplantCall.prototype.__onNoSelfPingsReceived = function()
	{
		this.runCallback(BX.Call.Event.onLeave, {
			local: false
		});
		this.joinedElsewhere = false;
	};

	BX.Call.VoximplantCall.prototype.__onPullEventFinish = function(params)
	{
		this.destroy();
	};

	BX.Call.VoximplantCall.prototype.__onPullEventRepeatAnswer = function()
	{
		if (this.ready)
		{
			this.signaling.sendAnswer({userId: this.userId}, true);
		}
	}

	BX.Call.VoximplantCall.prototype.__onLocalDevicesUpdated = function(e)
	{
		this.log("__onLocalDevicesUpdated", e);
	};

	BX.Call.VoximplantCall.prototype.__onLocalMediaRendererAdded = function(e)
	{
		var renderer = e.renderer;
		var trackLabel = renderer.stream.getVideoTracks().length > 0 ? renderer.stream.getVideoTracks()[0].label : "";
		this.log("__onLocalMediaRendererAdded", renderer.kind, trackLabel);

		if(renderer.kind === "video")
		{
			if (trackLabel.match(/^screen|window|tab|web-contents-media-stream/i))
			{
				var tag = "screen";
			}
			else
			{
				tag = "main";
			}

			this.screenShared = tag === "screen";

			this.runCallback(BX.Call.Event.onLocalMediaReceived, {
				tag: tag,
				stream: renderer.stream,
			});
		}
		else if (renderer.kind === "sharing")
		{
			this.runCallback(BX.Call.Event.onLocalMediaReceived, {
				tag: "screen",
				stream: renderer.stream,
			});
			this.screenShared = true;
		}
	};

	BX.Call.VoximplantCall.prototype.__onBeforeLocalMediaRendererRemoved = function(e)
	{
		var renderer = e.renderer;
		this.log("__onBeforeLocalMediaRendererRemoved", renderer.kind);

		if(renderer.kind === "sharing" && !this.videoEnabled)
		{
			this.runCallback(BX.Call.Event.onLocalMediaReceived, {
				tag: "main",
				stream: new MediaStream(),
			});
			this.screenShared = false;
		}
	};

	BX.Call.VoximplantCall.prototype.__onMicAccessResult = function(e)
	{
		if (e.result)
		{
			if (e.stream.getAudioTracks().length > 0)
			{
				if (this.localVAD)
				{
					this.localVAD.destroy();
				}
				this.localVAD = new BX.SimpleVAD({
					mediaStream: e.stream,
					onVoiceStarted: function()
					{
						this.runCallback(BX.Call.Event.onUserVoiceStarted, {
							userId: this.userId,
							local: true
						});
					}.bind(this),
					onVoiceStopped: function()
					{
						this.runCallback(BX.Call.Event.onUserVoiceStopped, {
							userId: this.userId,
							local: true
						});
					}.bind(this),
				});

				clearInterval(this.microphoneLevelInterval);
				this.microphoneLevelInterval = setInterval(function()
				{
					this.microphoneLevel = this.localVAD.currentVolume;
				}.bind(this), 200)
			}
		}
	};

	BX.Call.VoximplantCall.prototype.__onCallReconnecting = function(e)
	{
		this.reconnectionEventCount++;
	};

	BX.Call.VoximplantCall.prototype.__onCallReconnected = function(e)
	{
		this.reconnectionEventCount--;
	};

	BX.Call.VoximplantCall.prototype.__onClientReconnecting = function(e)
	{
		this.reconnectionEventCount++;
	};

	BX.Call.VoximplantCall.prototype.__onClientReconnected = function(e)
	{
		this.reconnectionEventCount--;
	};

	BX.Call.VoximplantCall.prototype.__onCallDisconnected = function(e)
	{
		this.log("__onCallDisconnected", (e && e.headers ? {headers: e.headers} : null));
		this.sendTelemetryEvent("disconnect");
		this.localUserState = BX.Call.UserState.Idle;

		this.ready = false;
		this.muted = false;
		this.joinedAsViewer = false;
		this.reinitPeers();

		this._hideLocalVideo();
		this.removeCallEvents();
		this.voximplantCall = null;

		var client = VoxImplant.getInstance();
		client.enableSilentLogging(false);
		client.setLoggerCallback(null);

		this.state = BX.Call.State.Proceeding;
		this.runCallback(BX.Call.Event.onLeave, {
			local: true
		});
	};

	BX.Call.VoximplantCall.prototype.__onWindowUnload = function()
	{
		if(this.ready && this.voximplantCall)
		{
			this.signaling.sendHangup({
				userId: this.users
			});
		}
	};

	BX.Call.VoximplantCall.prototype.onFatalError = function(error)
	{
		if(error && error.call)
		{
			delete error.call;
		}
		this.log("onFatalError", error);

		this.ready = false;
		this.muted = false;
		this.localUserState = BX.Call.UserState.Failed;
		this.reinitPeers();

		this._hideLocalVideo().then(function()
		{
			if(this.voximplantCall)
			{
				this.removeCallEvents();
				try
				{
					this.voximplantCall.hangup({
						'X-Reason': 'Fatal error',
						'X-Error': typeof(error) === 'string' ? error : error.code || error.name
					})
				}
				catch (e)
				{
					this.log("Voximplant hangup error: ", e);
					console.error("Voximplant hangup error: ", e);
				}
				this.voximplantCall = null;
			}

			if (typeof(VoxImplant) !== 'undefined')
			{
				var client = VoxImplant.getInstance();

				client.enableSilentLogging(false);
				client.setLoggerCallback(null);
			}

			if(typeof(error) === "string")
			{
				this.runCallback(BX.Call.Event.onCallFailure, {
					name: error
				});
			}
			else if(error.name)
			{
				this.runCallback(BX.Call.Event.onCallFailure, error);
			}
		}.bind(this))
	};

	BX.Call.VoximplantCall.prototype.__onCallEndpointAdded = function(e)
	{
		var endpoint = e.endpoint;
		var userName = endpoint.userName;
		this.log("__onCallEndpointAdded (" + userName + ")", e.endpoint);
		console.log("__onCallEndpointAdded (" + userName + ")", e.endpoint);

		if(BX.type.isNotEmptyString(userName) && userName.substr(0, 4) == 'user')
		{
			// user connected to conference
			var userId = parseInt(userName.substr(4));
			if(this.peers[userId])
			{
				this.peers[userId].setEndpoint(endpoint);
			}
			this.wasConnected = true;
		}
		else
		{
			endpoint.addEventListener(VoxImplant.EndpointEvents.InfoUpdated, function(e)
			{
				var endpoint = e.endpoint;
				var userName = endpoint.userName;
				this.log("VoxImplant.EndpointEvents.InfoUpdated (" + userName + ")", e.endpoint);

				if(BX.type.isNotEmptyString(userName) && userName.substr(0, 4) == 'user')
				{
					// user connected to conference
					var userId = parseInt(userName.substr(4));
					if(this.peers[userId])
					{
						this.peers[userId].setEndpoint(endpoint);
					}
				}
			}.bind(this));

			this.log('Unknown endpoint ' + userName);
			console.warn('Unknown endpoint ' + userName);
		}
	};

	BX.Call.VoximplantCall.prototype.__onCallStatsReceived = function(e)
	{
		if(this.logger)
		{
			this.logger.sendStat(transformVoxStats(e.stats, this.voximplantCall));
		}
	}

	BX.Call.VoximplantCall.prototype.__onJoinRoomOffer = function(e)
	{
		console.warn("__onJoinRoomOffer", e)
		this.updateRoom({
			id: e.roomId,
			users: e.users,
			speaker: e.speaker,
		});
		this.runCallback(BX.Call.Event.onJoinRoomOffer, {
			roomId: e.roomId,
			users: e.users,
			initiator: e.initiator,
			speaker: e.speaker,
		})
	}

	BX.Call.VoximplantCall.prototype.__onRoomUpdated = function(e)
	{
		const speakerChanged = (
			e.roomId === this._currentRoomId
			&& this.rooms[e.roomId]
			&& this.rooms[e.roomId].speaker != e.speaker
		);
		const previousSpeaker = speakerChanged && this.rooms[e.roomId].speaker;

		console.log("__onRoomUpdated; speakerChanged: ", speakerChanged)
		this.updateRoom({
			id: e.roomId,
			users: e.users,
			speaker: e.speaker,
		});


		if (e.roomId === this._currentRoomId && e.users.indexOf(this.userId) === -1)
		{
			this._currentRoomId = null;
			this.runCallback(BX.Call.Event.onLeaveRoom, {
				roomId: e.roomId
			})
		}
		else if (e.roomId !== this._currentRoomId && e.users.indexOf(this.userId) !== -1)
		{
			this._currentRoomId = e.roomId;
			this.runCallback(BX.Call.Event.onJoinRoom, {
				roomId: e.roomId,
				speaker: this.currentRoom().speaker,
				users: this.currentRoom().users,
			})
		}
		else if (speakerChanged)
		{
			this.runCallback(BX.Call.Event.onTransferRoomSpeaker, {
				roomId: e.roomId,
				speaker: e.speaker,
				previousSpeaker: previousSpeaker,
				initiator: e.initiator,
			})
		}
	};

	BX.Call.VoximplantCall.prototype.__onCallMessageReceived = function(e)
	{
		var message;
		var peer;

		try
		{
			message = JSON.parse(e.text);
		}
		catch(err)
		{
			this.log("Could not parse scenario message.", err);
			return;
		}

		var eventName = message.eventName;
		if(eventName === clientEvents.voiceStarted)
		{
			// todo: remove after switching to SDK VAD events
			this.runCallback(BX.Call.Event.onUserVoiceStarted, {
				userId: message.senderId
			});
		}
		else if(eventName === clientEvents.voiceStopped)
		{
			// todo: remove after switching to SDK VAD events
			this.runCallback(BX.Call.Event.onUserVoiceStopped, {
				userId: message.senderId
			});
		}
		else if (eventName === clientEvents.microphoneState)
		{
			this.runCallback(BX.Call.Event.onUserMicrophoneState, {
				userId: message.senderId,
				microphoneState: message.microphoneState === "Y"
			});
		}
		else if (eventName === clientEvents.cameraState)
		{
			this.runCallback(BX.Call.Event.onUserCameraState, {
				userId: message.senderId,
				cameraState: message.cameraState === "Y"
			});
		}
		else if (eventName === clientEvents.videoPaused)
		{
			this.runCallback(BX.Call.Event.onUserVideoPaused, {
				userId: message.senderId,
				videoPaused: message.videoPaused === "Y"
			});
		}
		else if (eventName === clientEvents.screenState)
		{
			this.runCallback(BX.Call.Event.onUserScreenState, {
				userId: message.senderId,
				screenState: message.screenState === "Y"
			});
		}
		else if (eventName === clientEvents.recordState)
		{
			this.runCallback(BX.Call.Event.onUserRecordState, {
				userId: message.senderId,
				recordState: message.recordState
			});
		}
		else if (eventName === clientEvents.floorRequest)
		{
			this.runCallback(BX.Call.Event.onUserFloorRequest, {
				userId: message.senderId,
				requestActive: message.requestActive === "Y"
			})
		}
		else if (eventName === clientEvents.emotion)
		{
			this.runCallback(BX.Call.Event.onUserEmotion, {
				userId: message.senderId,
				toUserId: message.toUserId,
				emotion: message.emotion
			})
		}
		else if (eventName === clientEvents.customMessage)
		{
			this.runCallback(BX.Call.Event.onCustomMessage, {
				message: message.message
			})
		}
		else if (eventName === "scenarioLogUrl")
		{
			console.warn("scenario log url: " + message.logUrl)
		}
		else if (eventName === scenarioEvents.joinRoomOffer)
		{
			console.log(message)
			this.__onJoinRoomOffer(message);
		}
		else if (eventName === scenarioEvents.roomUpdated)
		{
			// console.log(message)
			this.__onRoomUpdated(message);
		}
		else if (eventName === scenarioEvents.listRoomsResponse)
		{
			if (this.__resolveListRooms)
			{
				this.__resolveListRooms(message.rooms)
				delete this.__resolveListRooms;
			}
		}
		else if (eventName === scenarioEvents.viewerJoined)
		{
			console.log("viewer " + message.senderId + " joined");
			peer = this.peers[message.senderId];
			if (peer)
			{
				peer.setDirection(BX.Call.EndpointDirection.RecvOnly);
				peer.setReady(true);
			}
		}
		else if (eventName === scenarioEvents.viewerLeft)
		{
			console.log("viewer " + message.senderId + " left");
			peer = this.peers[message.senderId];
			if (peer)
			{
				peer.setReady(false);
			}
		}
		else
		{
			this.log("Unknown scenario event " + eventName);
		}
	};

	BX.Call.VoximplantCall.prototype.sendTelemetryEvent = function(eventName)
	{
		BX.Call.Util.sendTelemetryEvent({
			call_id: this.id,
			user_id: this.userId,
			kind: "voximplant",
			event: eventName,
		})
	};

	BX.Call.VoximplantCall.prototype.destroy = function()
	{
		this.ready = false;
		this.joinedAsViewer = false;
		this._hideLocalVideo();
		if (this.localVAD)
		{
			this.localVAD.destroy();
			this.localVAD = null;
		}
		clearInterval(this.microphoneLevelInterval);
		if(this.voximplantCall)
		{
			this.removeCallEvents();
			if(this.voximplantCall.state() != "ENDED")
			{
				this.voximplantCall.hangup();
			}
			this.voximplantCall = null;
		}

		for(var userId in this.peers)
		{
			if(this.peers.hasOwnProperty(userId) && this.peers[userId])
			{
				this.peers[userId].destroy();
			}
		}

		this.removeClientEvents();

		clearTimeout(this.lastPingReceivedTimeout);
		clearTimeout(this.lastSelfPingReceivedTimeout);
		clearInterval(this.pingUsersInterval);
		clearInterval(this.pingBackendInterval);

		window.removeEventListener("unload", this.__onWindowUnloadHandler);
		this.superclass.destroy.apply(this, arguments);
	};

	BX.Call.VoximplantCall.Signaling = function(params)
	{
		this.call = params.call;
	};

	BX.Call.VoximplantCall.Signaling.prototype.inviteUsers = function(data)
	{
		return this.__runRestAction(ajaxActions.invite, data);
	};

	BX.Call.VoximplantCall.Signaling.prototype.sendAnswer = function(data, repeated)
	{
		if (repeated && BX.CallEngine.getPullClient().isPublishingEnabled())
		{
			this.__sendPullEvent(pullEvents.answer, data);
		}
		else
		{
			return this.__runRestAction(ajaxActions.answer, data);
		}
	};

	BX.Call.VoximplantCall.Signaling.prototype.sendCancel = function(data)
	{
		return this.__runRestAction(ajaxActions.cancel, data);
	};

	BX.Call.VoximplantCall.Signaling.prototype.sendHangup = function(data)
	{
		if(BX.CallEngine.getPullClient().isPublishingEnabled())
		{
			this.__sendPullEvent(pullEvents.hangup, data);
			data.retransmit = false;
			this.__runRestAction(ajaxActions.hangup, data);
		}
		else
		{
			data.retransmit = true;
			this.__runRestAction(ajaxActions.hangup, data);
		}
	};

	BX.Call.VoximplantCall.Signaling.prototype.sendVoiceStarted = function(data)
	{
		return this.__sendMessage(clientEvents.voiceStarted, data);
	};

	BX.Call.VoximplantCall.Signaling.prototype.sendVoiceStopped = function(data)
	{
		return this.__sendMessage(clientEvents.voiceStopped, data);
	};

	BX.Call.VoximplantCall.Signaling.prototype.sendMicrophoneState = function(microphoneState)
	{
		return this.__sendMessage(clientEvents.microphoneState, {
			microphoneState: microphoneState ? "Y" : "N"
		});
	};

	BX.Call.VoximplantCall.Signaling.prototype.sendCameraState = function(cameraState)
	{
		return this.__sendMessage(clientEvents.cameraState, {
			cameraState: cameraState ? "Y" : "N"
		});
	};

	BX.Call.VoximplantCall.Signaling.prototype.sendScreenState = function(screenState)
	{
		return this.__sendMessage(clientEvents.screenState, {
			screenState: screenState ? "Y" : "N"
		});
	};

	BX.Call.VoximplantCall.Signaling.prototype.sendRecordState = function(recordState)
	{
		return this.__sendMessage(clientEvents.recordState, recordState);
	};

	BX.Call.VoximplantCall.Signaling.prototype.sendFloorRequest = function(requestActive)
	{
		return this.__sendMessage(clientEvents.floorRequest, {
			requestActive: requestActive ? "Y" : "N"
		});
	};

	BX.Call.VoximplantCall.Signaling.prototype.sendEmotion = function(toUserId, emotion)
	{
		return this.__sendMessage(clientEvents.emotion, {
			toUserId: toUserId,
			emotion: emotion
		});
	};

	BX.Call.VoximplantCall.Signaling.prototype.sendCustomMessage = function(message, repeatOnConnect)
	{
		return this.__sendMessage(clientEvents.customMessage, {
			message: message,
			repeatOnConnect: !!repeatOnConnect
		});
	};

	BX.Call.VoximplantCall.Signaling.prototype.sendShowUsers = function(users)
	{
		return this.__sendMessage(clientEvents.showUsers, {
			users: users
		});
	};

	BX.Call.VoximplantCall.Signaling.prototype.sendShowAll = function()
	{
		return this.__sendMessage(clientEvents.showAll, {});
	};

	BX.Call.VoximplantCall.Signaling.prototype.sendHideAll = function()
	{
		return this.__sendMessage(clientEvents.hideAll, {});
	};

	BX.Call.VoximplantCall.Signaling.prototype.sendPingToUsers = function(data)
	{
		if (BX.CallEngine.getPullClient().isPublishingEnabled())
		{
			this.__sendPullEvent(pullEvents.ping, data, 0);
		}
	};

	BX.Call.VoximplantCall.Signaling.prototype.sendPingToBackend = function()
	{
		this.__runRestAction(ajaxActions.ping, {retransmit: false});
	};

	BX.Call.VoximplantCall.Signaling.prototype.sendUserInviteTimeout = function(data)
	{
		if (BX.CallEngine.getPullClient().isPublishingEnabled())
		{
			this.__sendPullEvent(pullEvents.userInviteTimeout, data, 0);
		}
	};

	BX.Call.VoximplantCall.Signaling.prototype.sendJoinRoom = function(roomId)
	{
		return this.__sendMessage(clientEvents.joinRoom, {
			roomId: roomId
		});
	};

	BX.Call.VoximplantCall.Signaling.prototype.sendLeaveRoom = function(roomId)
	{
		return this.__sendMessage(clientEvents.leaveRoom, {
			roomId: roomId
		});
	};

	BX.Call.VoximplantCall.Signaling.prototype.sendListRooms = function()
	{
		return this.__sendMessage(clientEvents.listRooms);
	};

	BX.Call.VoximplantCall.Signaling.prototype.sendRequestRoomSpeaker = function(roomId)
	{
		return this.__sendMessage(clientEvents.requestRoomSpeaker, {
			roomId: roomId
		});
	};

	BX.Call.VoximplantCall.Signaling.prototype.__sendPullEvent = function(eventName, data, expiry)
	{
		expiry = expiry || 5;
		if(!data.userId)
		{
			throw new Error('userId is not found in data');
		}

		if(!BX.type.isArray(data.userId))
		{
			data.userId = [data.userId];
		}
		if(data.userId.length === 0)
		{
			// nobody to send, exit
			return;
		}

		data.callInstanceId = this.call.instanceId;
		data.senderId = this.call.userId;
		data.callId = this.call.id;
		data.requestId = BX.Call.Engine.getInstance().getUuidv4();

		this.call.log('Sending p2p signaling event ' + eventName + '; ' + JSON.stringify(data));
		BX.CallEngine.getPullClient().sendMessage(data.userId, 'im', eventName, data, expiry);
	};

	BX.Call.VoximplantCall.Signaling.prototype.__sendMessage = function(eventName, data)
	{
		if(!this.call.voximplantCall)
		{
			return;
		}

		if(!BX.type.isPlainObject(data))
		{
			data = {};
		}
		data.eventName = eventName;
		data.requestId = BX.Call.Engine.getInstance().getUuidv4();

		this.call.voximplantCall.sendMessage(JSON.stringify(data));
	};

	BX.Call.VoximplantCall.Signaling.prototype.__runRestAction = function(signalName, data)
	{
		if(!BX.type.isPlainObject(data))
		{
			data = {};
		}

		data.callId = this.call.id;
		data.callInstanceId = this.call.instanceId;
		data.requestId = BX.Call.Engine.getInstance().getUuidv4();
		return BX.CallEngine.getRestClient().callMethod(signalName, data);
	};

	BX.Call.VoximplantCall.Peer = function(params)
	{
		this.userId = params.userId;
		this.call = params.call;

		this.ready = !!params.ready;
		this.calling = false;
		this.declined = false;
		this.busy = false;
		this.inviteTimeout = false;
		this.endpoint = null;
		this.direction = params.direction || BX.Call.EndpointDirection.SendRecv;

		this.stream = null;
		this.mediaRenderers = [];

		this.isIncomingVideoAllowed = params.isIncomingVideoAllowed !== false;

		this.callingTimeout = 0;
		this.connectionRestoreTimeout = 0;

		this.callbacks = {
			onStateChanged: BX.type.isFunction(params.onStateChanged) ? params.onStateChanged : BX.DoNothing,
			onInviteTimeout: BX.type.isFunction(params.onInviteTimeout) ? params.onInviteTimeout : BX.DoNothing,
			onMediaReceived: BX.type.isFunction(params.onMediaReceived) ? params.onMediaReceived : BX.DoNothing,
			onMediaRemoved: BX.type.isFunction(params.onMediaRemoved) ? params.onMediaRemoved : BX.DoNothing,
			onVoiceStarted: BX.type.isFunction(params.onVoiceStarted) ? params.onVoiceStarted : BX.DoNothing,
			onVoiceEnded: BX.type.isFunction(params.onVoiceEnded) ? params.onVoiceEnded : BX.DoNothing,
		};

		// event handlers
		this.__onEndpointRemoteMediaAddedHandler = this.__onEndpointRemoteMediaAdded.bind(this);
		this.__onEndpointRemoteMediaRemovedHandler = this.__onEndpointRemoteMediaRemoved.bind(this);
		this.__onEndpointVoiceStartHandler = this.__onEndpointVoiceStart.bind(this);
		this.__onEndpointVoiceEndHandler = this.__onEndpointVoiceEnd.bind(this);
		this.__onEndpointRemovedHandler = this.__onEndpointRemoved.bind(this);

		this.calculatedState = this.calculateState();
	};

	BX.Call.VoximplantCall.Peer.prototype = {

		setReady: function(ready)
		{
			ready = !!ready;
			if (this.ready == ready)
			{
				return;
			}
			this.ready = ready;
			this.readyStack = (new Error()).stack;
			if(this.calling)
			{
				clearTimeout(this.callingTimeout);
				this.calling = false;
				this.inviteTimeout = false;
			}
			if(this.ready)
			{
				this.declined = false;
				this.busy = false;
			}
			else
			{
				clearTimeout(this.connectionRestoreTimeout);
			}

			this.updateCalculatedState();
		},

		setDirection: function(direction)
		{
			if (this.direction == direction)
			{
				return;
			}
			this.direction = direction;
		},

		setDeclined: function(declined)
		{
			this.declined = declined;
			if(this.calling)
			{
				clearTimeout(this.callingTimeout);
				this.calling = false;
			}
			if(this.declined)
			{
				this.ready = false;
				this.busy = false;
			}
			clearTimeout(this.connectionRestoreTimeout);
			this.updateCalculatedState();
		},

		setBusy: function(busy)
		{
			this.busy = busy;
			if(this.calling)
			{
				clearTimeout(this.callingTimeout);
				this.calling = false;
			}
			if(this.busy)
			{
				this.ready = false;
				this.declined = false;
			}
			clearTimeout(this.connectionRestoreTimeout);
			this.updateCalculatedState();
		},

		setEndpoint: function(endpoint)
		{
			this.log("Adding endpoint with " + endpoint.mediaRenderers.length + " media renderers");

			this.setReady(true);
			this.inviteTimeout = false;
			this.declined = false;
			clearTimeout(this.connectionRestoreTimeout);

			if(this.endpoint)
			{
				this.removeEndpointEventHandlers();
				this.endpoint = null;
			}

			this.endpoint = endpoint;

			for(var i = 0; i < this.endpoint.mediaRenderers.length; i++)
			{
				this.addMediaRenderer(this.endpoint.mediaRenderers[i]);
				if(this.endpoint.mediaRenderers[i].element)
				{
					//BX.remove(this.endpoint.mediaRenderers[i].element);
				}
			}

			this.bindEndpointEventHandlers();
		},

		allowIncomingVideo: function(isIncomingVideoAllowed)
		{
			if(this.isIncomingVideoAllowed == isIncomingVideoAllowed)
			{
				return;
			}

			this.isIncomingVideoAllowed = !!isIncomingVideoAllowed;
		},

		addMediaRenderer: function(mediaRenderer)
		{
			this.log('Adding media renderer for user' + this.userId, mediaRenderer);

			this.mediaRenderers.push(mediaRenderer);
			this.callbacks.onMediaReceived({
				userId: this.userId,
				kind: mediaRenderer.kind,
				mediaRenderer: mediaRenderer
			});
			this.updateCalculatedState();
		},

		removeMediaRenderer: function(mediaRenderer)
		{
			console.log('Removing media renderer for user' + this.userId, mediaRenderer);
			this.log('Removing media renderer for user' + this.userId, mediaRenderer);

			var i = this.mediaRenderers.indexOf(mediaRenderer);
			if (i >= 0)
			{
				this.mediaRenderers.splice(i, 1);
			}
			this.callbacks.onMediaRemoved({
				userId: this.userId,
				kind: mediaRenderer.kind,
				mediaRenderer: mediaRenderer
			});
			this.updateCalculatedState();
		},

		bindEndpointEventHandlers: function()
		{
			this.endpoint.addEventListener(VoxImplant.EndpointEvents.RemoteMediaAdded, this.__onEndpointRemoteMediaAddedHandler);
			this.endpoint.addEventListener(VoxImplant.EndpointEvents.RemoteMediaRemoved, this.__onEndpointRemoteMediaRemovedHandler);
			this.endpoint.addEventListener(VoxImplant.EndpointEvents.VoiceStart, this.__onEndpointVoiceStartHandler);
			this.endpoint.addEventListener(VoxImplant.EndpointEvents.VoiceEnd, this.__onEndpointVoiceEndHandler);
			this.endpoint.addEventListener(VoxImplant.EndpointEvents.Removed, this.__onEndpointRemovedHandler);
		},

		removeEndpointEventHandlers: function()
		{
			this.endpoint.removeEventListener(VoxImplant.EndpointEvents.RemoteMediaAdded, this.__onEndpointRemoteMediaAddedHandler);
			this.endpoint.removeEventListener(VoxImplant.EndpointEvents.RemoteMediaRemoved, this.__onEndpointRemoteMediaRemovedHandler);
			this.endpoint.removeEventListener(VoxImplant.EndpointEvents.VoiceStart, this.__onEndpointVoiceStartHandler);
			this.endpoint.removeEventListener(VoxImplant.EndpointEvents.VoiceEnd, this.__onEndpointVoiceEndHandler);
			this.endpoint.removeEventListener(VoxImplant.EndpointEvents.Removed, this.__onEndpointRemovedHandler);
		},

		calculateState: function()
		{
			if(this.endpoint)
				return BX.Call.UserState.Connected;

			if(this.calling)
				return BX.Call.UserState.Calling;

			if(this.inviteTimeout)
				return BX.Call.UserState.Unavailable;

			if(this.declined)
				return BX.Call.UserState.Declined;

			if(this.busy)
				return BX.Call.UserState.Busy;

			if(this.ready)
				return BX.Call.UserState.Ready;

			return BX.Call.UserState.Idle;
		},

		updateCalculatedState: function()
		{
			var calculatedState = this.calculateState();

			if(this.calculatedState != calculatedState)
			{
				this.callbacks.onStateChanged({
					userId: this.userId,
					state: calculatedState,
					previousState: this.calculatedState,
					direction: this.direction,
				});
				this.calculatedState = calculatedState;
			}
		},

		isParticipating: function()
		{
			return ((this.calling || this.ready || this.endpoint) && !this.declined);
		},

		waitForConnectionRestore: function()
		{
			clearTimeout(this.connectionRestoreTimeout);
			this.connectionRestoreTimeout = setTimeout(
				this.onConnectionRestoreTimeout.bind(this),
				connectionRestoreTime
			);
		},

		onInvited: function()
		{
			this.ready = false;
			this.inviteTimeout = false;
			this.declined = false;
			this.calling = true;

			clearTimeout(this.connectionRestoreTimeout);
			if(this.callingTimeout)
			{
				clearTimeout(this.callingTimeout);
			}
			this.callingTimeout = setTimeout(function()
			{
				this.onInviteTimeout(true);
			}.bind(this), 30000);
			this.updateCalculatedState();
		},

		onInviteTimeout: function(internal)
		{
			clearTimeout(this.callingTimeout);
			if(!this.calling)
			{
				return;
			}
			this.calling = false;
			this.inviteTimeout = true;
			if(internal)
			{
				this.callbacks.onInviteTimeout({
					userId: this.userId
				});
			}
			this.updateCalculatedState();
		},

		onConnectionRestoreTimeout: function()
		{
			if(this.endpoint || !this.ready)
			{
				return;
			}

			this.log("Done waiting for connection restoration");
			this.setReady(false);
		},

		__onEndpointRemoteMediaAdded: function(e)
		{
			this.log("VoxImplant.EndpointEvents.RemoteMediaAdded", e.mediaRenderer);

			// voximplant audio auto-play bug workaround:
			if(e.mediaRenderer.element)
			{
				e.mediaRenderer.element.volume = 0;
				e.mediaRenderer.element.srcObject = null;
			}
			this.addMediaRenderer(e.mediaRenderer);
		},

		__onEndpointRemoteMediaRemoved: function(e)
		{
			console.log("VoxImplant.EndpointEvents.RemoteMediaRemoved, ", e.mediaRenderer)
			//this.log("VoxImplant.EndpointEvents.RemoteMediaRemoved, ", e);
			this.removeMediaRenderer(e.mediaRenderer);
		},

		__onEndpointVoiceStart: function(e)
		{
			this.callbacks.onVoiceStarted();
		},

		__onEndpointVoiceEnd: function(e)
		{
			this.callbacks.onVoiceEnded();
		},

		__onEndpointRemoved: function(e)
		{
			this.log("VoxImplant.EndpointEvents.Removed", e);

			if(this.endpoint)
			{
				this.removeEndpointEventHandlers();
				this.endpoint = null;
			}
			if(this.stream)
			{
				this.stream = null;
			}

			if(this.ready)
			{
				this.waitForConnectionRestore();
			}

			this.updateCalculatedState();
		},

		log: function()
		{
			this.call.log.apply(this.call, arguments);
		},

		destroy: function()
		{
			if(this.stream)
			{
				this.stream.getTracks().forEach(function(track)
				{
					track.stop();
				});
				this.stream = null;
			}
			if(this.endpoint)
			{
				this.removeEndpointEventHandlers();
				this.endpoint = null;
			}

			this.callbacks.onStateChanged = BX.DoNothing;
			this.callbacks.onInviteTimeout = BX.DoNothing;
			this.callbacks.onMediaReceived = BX.DoNothing;
			this.callbacks.onMediaRemoved = BX.DoNothing;

			clearTimeout(this.callingTimeout);
			clearTimeout(this.connectionRestoreTimeout);
			this.callingTimeout = null;
			this.connectionRestoreTimeout = null;
		}
	};

	var transformVoxStats = function(s, voximplantCall)
	{
		let result = {
			connection: s.connection,
			outboundAudio: [],
			outboundVideo: [],
			inboundAudio: [],
			inboundVideo: [],
		}

		let endpoints = {};
		if (voximplantCall.getEndpoints)
		{
			voximplantCall.getEndpoints().forEach(endpoint => endpoints[endpoint.id] = endpoint)
		}

		if (!result.connection.timestamp)
		{
			result.connection.timestamp = Date.now();
		}
		for (let trackId in s.outbound)
		{
			if (!s.outbound.hasOwnProperty(trackId))
			{
				continue;
			}
			var statGroup = s.outbound[trackId];
			for (var i = 0; i < statGroup.length; i ++)
			{
				let stat = statGroup[i];
				stat.trackId = trackId;
				if ('audioLevel' in stat)
				{
					result.outboundAudio.push(stat)
				}
				else
				{
					result.outboundVideo.push(stat)
				}
			}
		}
		for (let trackId in s.inbound)
		{
			if (!s.inbound.hasOwnProperty(trackId))
			{
				continue;
			}
			let stat = s.inbound[trackId];
			if (!('endpoint' in stat))
			{
				continue;
			}
			stat.trackId = trackId;
			if ('audioLevel' in stat)
			{
				result.inboundAudio.push(stat)
			}
			else
			{
				if (endpoints[stat.endpoint])
				{
					let videoRenderer = endpoints[stat.endpoint].mediaRenderers.find(r => r.id == stat.trackId)
					if (videoRenderer && videoRenderer.element)
					{
						stat.actualHeight = videoRenderer.element.videoHeight;
						stat.actualWidth = videoRenderer.element.videoWidth;
					}
				}

				result.inboundVideo.push(stat)
			}
		}
		return result;
	}

	BX.Call.VoximplantCall.Event = VoximplantCallEvent;
})();

Youez - 2016 - github.com/yon3zu
LinuXploit