You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@openmeetings.apache.org by so...@apache.org on 2020/09/06 05:08:37 UTC

[openmeetings] branch master updated: [OPENMEETINGS-2424] audio/video stream is better cleaned

This is an automated email from the ASF dual-hosted git repository.

solomax pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/openmeetings.git


The following commit(s) were added to refs/heads/master by this push:
     new f48bff6  [OPENMEETINGS-2424] audio/video stream is better cleaned
f48bff6 is described below

commit f48bff6dca8ad62934462bde08975642373fc569
Author: Maxim Solodovnik <so...@gmail.com>
AuthorDate: Sun Sep 6 12:08:18 2020 +0700

    [OPENMEETINGS-2424] audio/video stream is better cleaned
---
 .../apache/openmeetings/core/remote/KStream.java   |   3 +
 openmeetings-web/pom.xml                           |   1 +
 .../web/room/activities/ActivitiesPanel.java       |   8 -
 .../activities.js => raw-activities.js}            |   0
 .../org/apache/openmeetings/web/room/raw-room.js   |   6 +
 .../apache/openmeetings/web/room/raw-settings.js   |  14 +-
 .../openmeetings/web/room/raw-video-manager.js     |  16 +-
 .../apache/openmeetings/web/room/raw-video-util.js |  50 +++--
 .../org/apache/openmeetings/web/room/raw-video.js  | 213 +++++++++++++--------
 .../openmeetings/webservice/NetTestWebService.java |  15 +-
 10 files changed, 198 insertions(+), 128 deletions(-)

diff --git a/openmeetings-core/src/main/java/org/apache/openmeetings/core/remote/KStream.java b/openmeetings-core/src/main/java/org/apache/openmeetings/core/remote/KStream.java
index 17fd834..77a5e2d 100644
--- a/openmeetings-core/src/main/java/org/apache/openmeetings/core/remote/KStream.java
+++ b/openmeetings-core/src/main/java/org/apache/openmeetings/core/remote/KStream.java
@@ -390,6 +390,9 @@ public class KStream extends AbstractStream {
 
 	public void addCandidate(IceCandidate candidate, String uid) {
 		if (this.uid.equals(uid)) {
+			if (outgoingMedia == null) {
+				return;
+			}
 			outgoingMedia.addIceCandidate(candidate);
 		} else {
 			WebRtcEndpoint endpoint = listeners.get(uid);
diff --git a/openmeetings-web/pom.xml b/openmeetings-web/pom.xml
index d72d8cf..e192aa6 100644
--- a/openmeetings-web/pom.xml
+++ b/openmeetings-web/pom.xml
@@ -202,6 +202,7 @@
 							<jsSourceDir>../java/org/apache/openmeetings/web/room</jsSourceDir>
 							<jsSourceFiles>
 								<jsSourceFile>NoSleep.js</jsSourceFile>
+								<jsSourceFile>raw-activities.js</jsSourceFile>
 								<jsSourceFile>raw-video.js</jsSourceFile>
 								<jsSourceFile>raw-video-manager.js</jsSourceFile>
 								<jsSourceFile>raw-sharer.js</jsSourceFile>
diff --git a/openmeetings-web/src/main/java/org/apache/openmeetings/web/room/activities/ActivitiesPanel.java b/openmeetings-web/src/main/java/org/apache/openmeetings/web/room/activities/ActivitiesPanel.java
index 41c9b41..bdc567f 100644
--- a/openmeetings-web/src/main/java/org/apache/openmeetings/web/room/activities/ActivitiesPanel.java
+++ b/openmeetings-web/src/main/java/org/apache/openmeetings/web/room/activities/ActivitiesPanel.java
@@ -40,10 +40,8 @@ import org.apache.wicket.ajax.AbstractDefaultAjaxBehavior;
 import org.apache.wicket.ajax.AjaxRequestTarget;
 import org.apache.wicket.core.request.handler.IPartialPageRequestHandler;
 import org.apache.wicket.markup.head.IHeaderResponse;
-import org.apache.wicket.markup.head.JavaScriptHeaderItem;
 import org.apache.wicket.markup.head.PriorityHeaderItem;
 import org.apache.wicket.markup.html.panel.Panel;
-import org.apache.wicket.request.resource.JavaScriptResourceReference;
 import org.apache.wicket.spring.injection.annot.SpringBean;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
@@ -260,12 +258,6 @@ public class ActivitiesPanel extends Panel {
 		handler.appendJavaScript(String.format("Activities.remove(%s);", arr));
 	}
 
-	@Override
-	public void renderHead(IHeaderResponse response) {
-		super.renderHead(response);
-		response.render(new PriorityHeaderItem(JavaScriptHeaderItem.forReference(new JavaScriptResourceReference(ActivitiesPanel.class, "activities.js"))));
-	}
-
 	private static CharSequence getClass(Activity a) {
 		StringBuilder cls = new StringBuilder();
 		switch (a.getType()) {
diff --git a/openmeetings-web/src/main/java/org/apache/openmeetings/web/room/activities/activities.js b/openmeetings-web/src/main/java/org/apache/openmeetings/web/room/raw-activities.js
similarity index 100%
rename from openmeetings-web/src/main/java/org/apache/openmeetings/web/room/activities/activities.js
rename to openmeetings-web/src/main/java/org/apache/openmeetings/web/room/raw-activities.js
diff --git a/openmeetings-web/src/main/java/org/apache/openmeetings/web/room/raw-room.js b/openmeetings-web/src/main/java/org/apache/openmeetings/web/room/raw-room.js
index 6e5297f..4d8a7c4 100644
--- a/openmeetings-web/src/main/java/org/apache/openmeetings/web/room/raw-room.js
+++ b/openmeetings-web/src/main/java/org/apache/openmeetings/web/room/raw-room.js
@@ -372,6 +372,9 @@ var Room = (function() {
 		}).appendTo(container);
 	}
 	function _addClient(_clients) {
+		if (!options) {
+			return; //too early
+		}
 		const clients = Array.isArray(_clients) ? _clients : [_clients];
 		clients.forEach(c => {
 			const self = c.uid === options.uid;
@@ -392,6 +395,9 @@ var Room = (function() {
 		__sortUserList();
 	}
 	function _updateClient(c) {
+		if (!options) {
+			return; //too early
+		}
 		const self = c.uid === options.uid
 			, le = Room.getClient(c.uid)
 			, hasAudio = VideoUtil.hasMic(c)
diff --git a/openmeetings-web/src/main/java/org/apache/openmeetings/web/room/raw-settings.js b/openmeetings-web/src/main/java/org/apache/openmeetings/web/room/raw-settings.js
index 4ad895d..72fb92c 100644
--- a/openmeetings-web/src/main/java/org/apache/openmeetings/web/room/raw-settings.js
+++ b/openmeetings-web/src/main/java/org/apache/openmeetings/web/room/raw-settings.js
@@ -307,7 +307,7 @@ var VideoSettings = (function() {
 					options
 					, function(error) {
 						if (error) {
-							if (true === this.cleaned) {
+							if (true === rtcPeer.cleaned) {
 								return;
 							}
 							return OmUtil.error(error);
@@ -321,7 +321,7 @@ var VideoSettings = (function() {
 						}
 						rtcPeer.generateOffer(function(error, _offerSdp) {
 							if (error) {
-								if (true === this.cleaned) {
+								if (true === rtcPeer.cleaned) {
 									return;
 								}
 								return OmUtil.error('Error generating the offer');
@@ -496,14 +496,14 @@ var VideoSettings = (function() {
 						options
 						, function(error) {
 							if (error) {
-								if (true === this.cleaned) {
+								if (true === rtcPeer.cleaned) {
 									return;
 								}
 								return OmUtil.error(error);
 							}
 							rtcPeer.generateOffer(function(error, offerSdp) {
 								if (error) {
-									if (true === this.cleaned) {
+									if (true === rtcPeer.cleaned) {
 										return;
 									}
 									return OmUtil.error('Error generating the offer');
@@ -520,7 +520,7 @@ var VideoSettings = (function() {
 				OmUtil.log('Play SDP answer received from server. Processing ...');
 				rtcPeer.processAnswer(m.sdpAnswer, function(error) {
 					if (error) {
-						if (true === this.cleaned) {
+						if (true === rtcPeer.cleaned) {
 							return;
 						}
 						return OmUtil.error(error);
@@ -534,7 +534,7 @@ var VideoSettings = (function() {
 				OmUtil.log('SDP answer received from server. Processing ...');
 				rtcPeer.processAnswer(m.sdpAnswer, function(error) {
 					if (error) {
-						if (true === this.cleaned) {
+						if (true === rtcPeer.cleaned) {
 							return;
 						}
 						return OmUtil.error(error);
@@ -544,7 +544,7 @@ var VideoSettings = (function() {
 			case 'iceCandidate':
 				rtcPeer.addIceCandidate(m.candidate, function(error) {
 					if (error) {
-						if (true === this.cleaned) {
+						if (true === rtcPeer.cleaned) {
 							return;
 						}
 						return OmUtil.error('Error adding candidate: ' + error);
diff --git a/openmeetings-web/src/main/java/org/apache/openmeetings/web/room/raw-video-manager.js b/openmeetings-web/src/main/java/org/apache/openmeetings/web/room/raw-video-manager.js
index 2e49eed..b456652 100644
--- a/openmeetings-web/src/main/java/org/apache/openmeetings/web/room/raw-video-manager.js
+++ b/openmeetings-web/src/main/java/org/apache/openmeetings/web/room/raw-video-manager.js
@@ -8,12 +8,12 @@ var VideoManager = (function() {
 			, v = w.data()
 			, peer = v && v.getPeer();
 
-		if (peer) {
+		if (peer && false === peer.cleaned) {
 			peer.processAnswer(m.sdpAnswer, function (error) {
+				if (true === peer.cleaned) {
+					return;
+				}
 				if (error) {
-					if (true === this.cleaned) {
-						return;
-					}
 					return OmUtil.error(error);
 				}
 				const vidEls = w.find('audio, video')
@@ -85,12 +85,12 @@ var VideoManager = (function() {
 						, v = w.data()
 						, peer = v && v.getPeer();
 
-					if (peer) {
+					if (peer && false === peer.cleaned) {
 						peer.addIceCandidate(m.candidate, function (error) {
+							if (true === this.cleaned) {
+								return;
+							}
 							if (error) {
-								if (true === this.cleaned) {
-									return;
-								}
 								OmUtil.error('Error adding candidate: ' + error);
 								return;
 							}
diff --git a/openmeetings-web/src/main/java/org/apache/openmeetings/web/room/raw-video-util.js b/openmeetings-web/src/main/java/org/apache/openmeetings/web/room/raw-video-util.js
index 91a7549..92ff8f9 100644
--- a/openmeetings-web/src/main/java/org/apache/openmeetings/web/room/raw-video-util.js
+++ b/openmeetings-web/src/main/java/org/apache/openmeetings/web/room/raw-video-util.js
@@ -175,34 +175,52 @@ var VideoUtil = (function() {
 	}
 	function _cleanStream(stream) {
 		if (!!stream) {
-			stream.getTracks().forEach(function(track) {
-				try {
-					track.stop();
-				} catch(e) {
-					//no-op
-				}
-			});
+			stream.getTracks().forEach(track => track.stop());
 		}
 	}
 	function _cleanPeer(peer) {
 		if (!!peer) {
 			peer.cleaned = true;
-			const pc = peer.peerConnection;
 			try {
-				if (!!pc && !!pc.getLocalStreams()) {
-					pc.getLocalStreams().forEach(function(stream) {
-						_cleanStream(stream);
+				const pc = peer.peerConnection;
+				if (!!pc) {
+					pc.getSenders().forEach(sender => {
+						try {
+							if (sender.track) {
+								sender.track.stop();
+							}
+						} catch(e) {
+							OmUtil.log('Failed to clean sender' + e);
+						}
+					});
+					pc.getReceivers().forEach(receiver => {
+						try {
+							if (receiver.track) {
+								receiver.track.stop();
+							}
+						} catch(e) {
+							OmUtil.log('Failed to clean receiver' + e);
+						}
 					});
+					pc.onconnectionstatechange = null;
+					pc.ontrack = null;
+					pc.onremovetrack = null;
+					pc.onremovestream = null;
+					pc.onicecandidate = null;
+					pc.oniceconnectionstatechange = null;
+					pc.onsignalingstatechange = null;
+					pc.onicegatheringstatechange = null;
+					pc.onnegotiationneeded = null;
 				}
-			} catch(e) {
-				OmUtil.log('Failed to clean peer' + e);
-			}
-			try {
 				peer.dispose();
+				peer.removeAllListeners('icecandidate');
+				delete peer.generateOffer;
+				delete peer.processAnswer;
+				delete peer.processOffer;
+				delete peer.addIceCandidate;
 			} catch(e) {
 				//no-op
 			}
-			peer = null;
 		}
 	}
 	function _isChrome(_b) {
diff --git a/openmeetings-web/src/main/java/org/apache/openmeetings/web/room/raw-video.js b/openmeetings-web/src/main/java/org/apache/openmeetings/web/room/raw-video.js
index f350d5a..5cf51f3 100644
--- a/openmeetings-web/src/main/java/org/apache/openmeetings/web/room/raw-video.js
+++ b/openmeetings-web/src/main/java/org/apache/openmeetings/web/room/raw-video.js
@@ -1,8 +1,8 @@
 /* Licensed under the Apache License, Version 2.0 (the "License") http://www.apache.org/licenses/LICENSE-2.0 */
 var Video = (function() {
-	const self = {}
+	const self = {}, states = []
 		, AudioCtx = window.AudioContext || window.webkitAudioContext;
-	let sd, v, vc, t, footer, size, vol, video, iceServers
+	let sd, v, vc, t, footer, size, vol, iceServers
 		, lm, level, userSpeaks = false, muteOthers
 		, hasVideo, isSharing, isRecording;
 
@@ -19,7 +19,7 @@ var Video = (function() {
 			OmUtil.sendMessage({type: 'mic', id: 'activity', active: speaks});
 		}
 	}
-	function _getScreenStream(msg, callback) {
+	function _getScreenStream(msg, state, callback) {
 		function __handleScreenError(err) {
 			VideoManager.sendMessage({id: 'errorSharing'});
 			Sharer.setShareState(SHARE_STOPPED);
@@ -50,11 +50,14 @@ var Video = (function() {
 			});
 		}
 		promise.then(function(stream) {
-			__createVideo();
-			callback(msg, cnts, stream);
+			if (!state.disposed) {
+				__createVideo(state);
+				state.stream = stream;
+				callback(msg, state, cnts);
+			}
 		}).catch(__handleScreenError);
 	}
-	function _getVideoStream(msg, callback) {
+	function _getVideoStream(msg, state, callback) {
 		VideoSettings.constraints(sd, function(cnts) {
 			if ((VideoUtil.hasCam(sd) && !cnts.video) || (VideoUtil.hasMic(sd) && !cnts.audio)) {
 				VideoManager.sendMessage({
@@ -71,7 +74,7 @@ var Video = (function() {
 			}
 			navigator.mediaDevices.getUserMedia(cnts)
 				.then(function(stream) {
-					if (msg.instanceUid !== v.data('instance-uid')) {
+					if (state.disposed || msg.instanceUid !== v.data('instance-uid')) {
 						return;
 					}
 					let _stream = stream;
@@ -97,8 +100,10 @@ var Video = (function() {
 							});
 						}
 					}
-					__createVideo(data);
-					callback(msg, cnts, _stream);
+					state.data = data;
+					__createVideo(state);
+					state.stream = _stream;
+					callback(msg, state, cnts);
 				})
 				.catch(function(err) {
 					VideoManager.sendMessage({
@@ -116,9 +121,9 @@ var Video = (function() {
 				});
 		});
 	}
-	function __attachListener(rtcPeer) {
-		if (rtcPeer) {
-			const pc = rtcPeer.peerConnection;
+	function __attachListener(state) {
+		if (!state.disposed && state.data.rtcPeer) {
+			const pc = state.data.rtcPeer.peerConnection;
 			pc.onconnectionstatechange = function(event) {
 				console.warn(`!!RTCPeerConnection state changed: ${pc.connectionState}, user: ${sd.user.displayName}, uid: ${sd.uid}`);
 				switch(pc.connectionState) {
@@ -141,20 +146,20 @@ var Video = (function() {
 			}
 		}
 	}
-	function __createSendPeer(msg, cnts, stream) {
-		const options = {
-			videoStream: stream
+	function __createSendPeer(msg, state, cnts) {
+		state.options = {
+			videoStream: state.stream
 			, mediaConstraints: cnts
 			, onicecandidate: self.onIceCandidate
 		};
 		if (!isSharing) {
-			options.localVideo = video[0];
+			state.options.localVideo = state.video[0];
 		}
-		const data = video.data();
+		const data = state.data;
 		data.rtcPeer = new kurentoUtils.WebRtcPeer.WebRtcPeerSendonly(
-			VideoUtil.addIceServers(options, msg)
+			VideoUtil.addIceServers(state.options, msg)
 			, function (error) {
-				if (true === this.cleaned) {
+				if (state.disposed || true === data.rtcPeer.cleaned) {
 					return;
 				}
 				if (error) {
@@ -164,8 +169,8 @@ var Video = (function() {
 					level = MicLevel();
 					level.meter(data.analyser, lm, _micActivity, OmUtil.error);
 				}
-				this.generateOffer(function(error, offerSdp) {
-					if (true === this.cleaned) {
+				data.rtcPeer.generateOffer(function(error, offerSdp) {
+					if (state.disposed || true === data.rtcPeer.cleaned) {
 						return;
 					}
 					if (error) {
@@ -185,33 +190,34 @@ var Video = (function() {
 					}
 				});
 			});
-		__attachListener(data.rtcPeer);
+		data.rtcPeer.cleaned = false;
+		__attachListener(state);
 	}
-	function _createSendPeer(msg) {
+	function _createSendPeer(msg, state) {
 		if (isSharing || isRecording) {
-			_getScreenStream(msg, __createSendPeer);
+			_getScreenStream(msg, state, __createSendPeer);
 		} else {
-			_getVideoStream(msg, __createSendPeer);
+			_getVideoStream(msg, state, __createSendPeer);
 		}
 	}
-	function _createResvPeer(msg) {
-		__createVideo();
+	function _createResvPeer(msg, state) {
+		__createVideo(state);
 		const options = VideoUtil.addIceServers({
-			remoteVideo : video[0]
+			remoteVideo : state.video[0]
 			, onicecandidate : self.onIceCandidate
 		}, msg);
-		const data = video.data();
+		const data = state.data;
 		data.rtcPeer = new kurentoUtils.WebRtcPeer.WebRtcPeerRecvonly(
 			options
 			, function(error) {
-				if (true === this.cleaned) {
+				if (state.disposed || true === data.rtcPeer.cleaned) {
 					return;
 				}
 				if (error) {
 					return OmUtil.error(error);
 				}
-				this.generateOffer(function(error, offerSdp) {
-					if (true === this.cleaned) {
+				data.rtcPeer.generateOffer(function(error, offerSdp) {
+					if (state.disposed || true === data.rtcPeer.cleaned) {
 						return;
 					}
 					if (error) {
@@ -225,7 +231,8 @@ var Video = (function() {
 					});
 				});
 			});
-		__attachListener(data.rtcPeer);
+		data.rtcPeer.cleaned = false;
+		__attachListener(state);
 	}
 	function _handleMicStatus(state) {
 		if (!footer || !footer.is(':visible')) {
@@ -403,27 +410,27 @@ var Video = (function() {
 			_init({stream: sd, iceServers: iceServers});
 		}
 	}
-	function __createVideo(data) {
+	function __createVideo(state) {
 		const _id = VideoUtil.getVid(sd.uid);
 		_resizeDlgArea(size.width, size.height);
 		if (hasVideo && !isSharing && !isRecording) {
 			VideoUtil.setPos(v, VideoUtil.getPos(VideoUtil.getRects(VIDWIN_SEL), sd.width, sd.height + 25));
 		}
-		video = $(hasVideo ? '<video>' : '<audio>').attr('id', 'vid' + _id)
+		state.video = $(hasVideo ? '<video>' : '<audio>').attr('id', 'vid' + _id)
 			.attr('playsinline', 'playsinline')
 			.width(vc.width()).height(vc.height())
 			.prop('autoplay', true).prop('controls', false);
-		if (data) {
-			video.data(data);
+		if (state.data) {
+			state.video.data(state.data);
 		}
 		if (hasVideo) {
 			vc.removeClass('audio-only').css('background-image', '');;
 			vc.parents('.ui-dialog').removeClass('audio-only');
-			video.attr('poster', sd.user.pictureUri);
+			state.video.attr('poster', sd.user.pictureUri);
 		} else {
 			vc.addClass('audio-only');
 		}
-		vc.append(video);
+		vc.append(state.video);
 		if (VideoUtil.hasMic(sd)) {
 			const volIco = vol.create(self)
 			if (hasVideo) {
@@ -439,12 +446,17 @@ var Video = (function() {
 	function _refresh(_msg) {
 		const msg = _msg || {iceServers: iceServers};
 		_cleanup();
-		const hasAudio = VideoUtil.hasMic(sd);
+		const hasAudio = VideoUtil.hasMic(sd)
+			, state = {
+				disposed: false
+				, data: {}
+			};
+		states.push(state);
 		if (sd.self) {
-			_createSendPeer(msg);
+			_createSendPeer(msg, state);
 			_handleMicStatus(hasAudio);
 		} else {
-			_createResvPeer(msg);
+			_createResvPeer(msg, state);
 		}
 	}
 	function _setRights() {
@@ -456,43 +468,73 @@ var Video = (function() {
 			muteOthers.removeClass('enabled').off();
 		}
 	}
-	function _cleanup() {
-		OmUtil.log('Disposing participant ' + sd.uid);
-		if (video && video.length > 0) {
-			const data = video.data();
-			if (data.analyser) {
-				VideoUtil.disconnect(data.analyser);
-				data.analyser = null;
-			}
-			if (data.gainNode) {
-				VideoUtil.disconnect(data.gainNode);
-				data.gainNode = null;
+	function _cleanData(data) {
+		if (!data) {
+			return;
+		}
+		if (data.analyser) {
+			VideoUtil.disconnect(data.analyser);
+			data.analyser = null;
+		}
+		if (data.gainNode) {
+			VideoUtil.disconnect(data.gainNode);
+			data.gainNode = null;
+		}
+		if (data.aSrc) {
+			VideoUtil.cleanStream(data.aSrc.mediaStream);
+			VideoUtil.cleanStream(data.aSrc.origStream);
+			VideoUtil.disconnect(data.aSrc);
+			data.aSrc = null;
+		}
+		if (data.aDest) {
+			VideoUtil.disconnect(data.aDest);
+			data.aDest = null;
+		}
+		if (data.aCtx) {
+			if (data.aCtx.destination) {
+				VideoUtil.disconnect(data.aCtx.destination);
 			}
-			if (data.aSrc) {
-				VideoUtil.cleanStream(data.aSrc.mediaStream);
-				VideoUtil.cleanStream(data.aSrc.origStream);
-				VideoUtil.disconnect(data.aSrc);
-				data.aSrc = null;
+			if ('closed' !== data.aCtx.state) {
+				try {
+					data.aCtx.close();
+				} catch(e) {
+					console.error(e);
+				}
 			}
-			if (data.aDest) {
-				VideoUtil.disconnect(data.aDest);
-				data.aDest = null;
+			data.aCtx = null;
+		}
+		VideoUtil.cleanPeer(data.rtcPeer);
+		data.rtcPeer = null;
+	}
+	function _cleanup(evt) {
+		OmUtil.log('!!Disposing participant ' + sd.uid);
+		let state;
+		while(state = states.pop()) {
+			state.disposed = true;
+			if (state.options) {
+				delete state.options.videoStream;
+				delete state.options.mediaConstraints;
+				delete state.options.onicecandidate;
+				delete state.options.localVideo;
+				state.options = null;
 			}
-			if (data.aCtx) {
-				if (data.aCtx.destination) {
-					VideoUtil.disconnect(data.aCtx.destination);
-				}
-				data.aCtx.close();
-				data.aCtx = null;
+			_cleanData(state.data);
+			VideoUtil.cleanStream(state.stream);
+			state.data = null;
+			state.stream = null;
+			const video = state.video;
+			if (video && video.length > 0) {
+				video.attr('id', 'dummy');
+				const vidNode = video[0];
+				VideoUtil.cleanStream(vidNode.srcObject);
+				vidNode.srcObject = null;
+				vidNode.load();
+				vidNode.removeAttribute("src");
+				vidNode.removeAttribute("srcObject");
+				vidNode.parentNode.removeChild(vidNode);
+				state.video.data({});
+				state.video = null;
 			}
-			video.attr('id', 'dummy');
-			const vidNode = video[0];
-			VideoUtil.cleanStream(vidNode.srcObject);
-			vidNode.srcObject = null;
-			vidNode.parentNode.removeChild(vidNode);
-
-			VideoUtil.cleanPeer(data.rtcPeer);
-			video = null;
 		}
 		if (lm && lm.length > 0) {
 			_micActivity(false);
@@ -505,14 +547,19 @@ var Video = (function() {
 		}
 		vc.find('audio,video').remove();
 		vol.destroy();
+		if (evt && evt.target) {
+			$(evt).off();
+		}
 	}
 	function _reattachStream() {
-		if (video && video.length > 0) {
-			const data = video.data();
-			if (data.rtcPeer) {
-				video[0].srcObject = sd.self ? data.rtcPeer.getLocalStream() : data.rtcPeer.getRemoteStream();
+		states.forEach(state => {
+			if (state.video && state.video.length > 0) {
+				const data = state.data;
+				if (data.rtcPeer) {
+					state.video[0].srcObject = sd.self ? data.rtcPeer.getLocalStream() : data.rtcPeer.getRemoteStream();
+				}
 			}
-		}
+		});
 	}
 
 	self.update = _update;
@@ -526,7 +573,9 @@ var Video = (function() {
 	self.init = _init;
 	self.stream = function() { return sd; };
 	self.setRights = _setRights;
-	self.getPeer = function() { return video ? video.data().rtcPeer : null; };
+	self.getPeer = function() {
+		return states.length > 0 ? states[0].data.rtcPeer : null;
+	};
 	self.onIceCandidate = function(candidate) {
 		const opts = Room.getOptions();
 		OmUtil.log('Local candidate ' + JSON.stringify(candidate));
@@ -539,7 +588,7 @@ var Video = (function() {
 	};
 	self.reattachStream = _reattachStream;
 	self.video = function() {
-		return video;
+		return states.length > 0 ? states[0].video : null;
 	};
 	self.handleMicStatus = _handleMicStatus;
 	return self;
diff --git a/openmeetings-webservice/src/main/java/org/apache/openmeetings/webservice/NetTestWebService.java b/openmeetings-webservice/src/main/java/org/apache/openmeetings/webservice/NetTestWebService.java
index a2de47c..67da389 100644
--- a/openmeetings-webservice/src/main/java/org/apache/openmeetings/webservice/NetTestWebService.java
+++ b/openmeetings-webservice/src/main/java/org/apache/openmeetings/webservice/NetTestWebService.java
@@ -19,9 +19,9 @@
 package org.apache.openmeetings.webservice;
 
 
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-import org.springframework.stereotype.Service;
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.concurrent.ThreadLocalRandom;
 
 import javax.ws.rs.Consumes;
 import javax.ws.rs.GET;
@@ -32,9 +32,10 @@ import javax.ws.rs.QueryParam;
 import javax.ws.rs.core.MediaType;
 import javax.ws.rs.core.Response;
 import javax.ws.rs.core.Response.ResponseBuilder;
-import java.io.IOException;
-import java.io.InputStream;
-import java.util.concurrent.ThreadLocalRandom;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.stereotype.Service;
 
 @Service("netTestWebService")
 @Path("/networktest")
@@ -58,7 +59,7 @@ public class NetTestWebService {
 	public Response get(@QueryParam("type") String type, @QueryParam("size") int _size) {
 		final int size;
 		TestType testType = getTypeByString(type);
-		log.debug("Network test:: get");
+		log.debug("Network test:: get, {}, {}", testType, _size);
 
 		// choose data to send
 		switch (testType) {