repo: janusweb
action: commit
revision:
path_from:
revision_from: c3c71b875f9b8dae2932cfa6454e0bc9344bb4c3:
path_to:
revision_to:
commit c3c71b875f9b8dae2932cfa6454e0bc9344bb4c3
Author: James Baicoianu
Date: Fri Oct 23 15:36:13 2020 -0700
VOIP and chat improvements
diff --git a/media/assets/webui/apps/comms/comms.css b/media/assets/webui/apps/comms/comms.css
--- a/media/assets/webui/apps/comms/comms.css
+++ b/media/assets/webui/apps/comms/comms.css
@@ -7,6 +7,7 @@ janus-comms-panel {
z-index: 100;
padding-right: .5em;
width: 25vw;
+ min-width: 20em;
}
janus-comms-panel details>summary {
cursor: pointer;
@@ -78,14 +79,16 @@ janus-comms-chat .selfchat .userid {
color: #fff;
font-weight: bold;
}
+/*
janus-comms-chat .chat .userid::before,
janus-comms-chat .selfchat .userid::before {
content: '<';
color: #666;
}
+*/
janus-comms-chat .chat .userid::after,
janus-comms-chat .selfchat .userid::after {
- content: '>';
+ content: ':';
color: #666;
}
janus-comms-chat .print .userid {
@@ -116,3 +119,39 @@ ui-list[name="userlist_room"] {
grid-template-columns: repeat(auto-fill, minmax(10em, 1fr));
font-size: .75em;
}
+janus-voip-picker>ul li {
+ display: inline-block;
+ border: 1px solid black;
+ border-radius: 10px;
+ background: #666;
+ box-shadow: 0 0 5px black;
+}
+janus-voip-picker>ul li button,
+janus-voip-picker ui-button {
+ width: 300px;
+ xheight: 100px;
+ xbackground: #666;
+ border-radius: 10px;
+ cursor: pointer;
+ font-size: 1.5em;
+ xcolor: white;
+ xtext-shadow: 0 0 5px black;
+}
+janus-voip-picker ui-button {
+ margin-top: 1em;
+ border: 1px solid black;
+ box-shadow: 0 0 5px black;
+ xline-height: 50px;
+ xheight: 50px;
+}
+janus-voip-picker>ul li button:hover {
+ xbackground: #777;
+}
+janus-voip-picker>ul li button:focus {
+ background: #6a6;
+}
+janus-voip-picker>ul li button[disabled] {
+ color: #888;
+ text-shadow: 0 0 5px #444;
+ cursor: not-allowed;
+
diff --git a/media/assets/webui/apps/comms/comms.html b/media/assets/webui/apps/comms/comms.html
--- a/media/assets/webui/apps/comms/comms.html
+++ b/media/assets/webui/apps/comms/comms.html
@@ -1,5 +1,5 @@
-
+
diff --git a/media/assets/webui/apps/comms/comms.js b/media/assets/webui/apps/comms/comms.js
--- a/media/assets/webui/apps/comms/comms.js
+++ b/media/assets/webui/apps/comms/comms.js
@@ -1,5 +1,8 @@
elation.elements.define('janus-comms-panel', class extends elation.elements.base {
create() {
+ this.defineAttributes({
+ labelfont: { type: 'string', default: 'monospace' },
+ });
this.player = player;
this.client = this.getClient();
this.janusweb = this.client.janusweb;
@@ -44,6 +47,9 @@ elation.elements.define('janus-comms-status', class extends elation.elements.bas
});
elation.elements.define('janus-comms-userlist', class extends elation.elements.ui.list {
create() {
+ this.defineAttributes({
+ labelfont: { type: 'string', default: 'monospace' },
+ });
this.player = player;
if (typeof room != 'undefined') {
this.room = room;
@@ -92,27 +98,26 @@ elation.elements.define('janus-comms-userlist', class extends elation.elements.u
this.userlist_room.setItems(users);
for (let k in remoteplayers) {
- let p = remoteplayers[k].getProxyObject();
+ let p = remoteplayers[k];
+ if (this.usermarkers[k] && this.usermarkers[k].parent !== p) {
+ this.usermarkers[k].die();
+ delete this.usermarkers[k];
+ }
if (!this.usermarkers[k]) {
-// FIXME - timeout hacks probably aren't needed
-setTimeout(() => {
-// simple test of a 3d object controlled from the ui
this.usermarkers[k] = p.createObject('playerlabel', {
player_name: k,
pos: V(p.userid_pos),
+ font: this.labelfont,
});
this.usermarkers[k].start();
-}, 100);
- } else if (this.usermarkers[k].parent !== p) {
-if (this.usermarkers[k].parent) {
- this.usermarkers[k].parent.remove(this.usermarkers[k]);
-}
-setTimeout(() => {
- p.appendChild(this.usermarkers[k]);
- this.usermarkers[k].updateCanvas();
- this.usermarkers[k].start();
-}, 1000);
- }
+ }
+
+ }
+ }
+ setFont(fontname) {
+ this.labelfont = fontname;
+ for (let k in this.usermarkers) {
+ this.usermarkers[k].setFont(fontname);
}
}
});
@@ -131,8 +136,11 @@ elation.elements.define('janus-comms-chat', class extends elation.elements.base
//this.elements.chatinput.addEventListener('accept', (ev) => this.sendMessage(ev.value));
this.elements.chatinput.onaccept = (ev) => {
this.sendMessage(this.elements.chatinput.value);
- if (this.shouldreturnfocus) {
+ if (false && this.shouldreturnfocus) {
player.enable();
+ this.shouldreturnfocus = false;
+ } else {
+ this.elements.chatinput.focus();
}
}
this.elements.chatinput.onfocus = (ev) => {
@@ -245,6 +253,7 @@ elation.elements.define('janus-comms-voip', class extends elation.elements.base
janus.registerElement('playerlabel', {
player_name: '',
+ font: 'monospace',
create() {
this.currentcolor = V(255,255,255);
@@ -260,7 +269,7 @@ janus.registerElement('playerlabel', {
});
this.label = this.createObject('object', {
id: 'plane',
- collision_id: 'cube',
+ //collision_id: 'cube',
collision_scale: V(.85,7,.1),
collision_pos: V(0,-3,0),
//collidable: true,
@@ -272,50 +281,62 @@ janus.registerElement('playerlabel', {
opacity: .9,
transparent: true,
renderorder: 10,
+ shadow_cast: false,
});
this.updateCanvas();
this.label.addEventListener('mouseover', ev => this.handleMouseOver(ev));
this.label.addEventListener('mouseout', ev => this.handleMouseOut(ev));
this.label.addEventListener('click', ev => this.handleClick(ev));
+
+ console.log('create a player label', this.player_name, this);
},
updateCanvas() {
let ctx = this.canvas.getContext('2d');
- let font = 'bold 60px monospace';
- ctx.font = font;
- let measure = ctx.measureText(this.player_name);
+ let font = 'bold 60px "' + this.font + '"';
+ document.fonts.load(font).then(fonts => {
+ ctx.font = font;
+ let measure = ctx.measureText(this.player_name);
- let width = Math.pow(2, Math.ceil(Math.log(measure.width) / Math.log(2)));
- this.canvas.width = width;
- if (this.label) {
- let oldscale = this.label.scale.x;
- this.label.scale.x = width / this.canvas.height * this.label.scale.y;
- this.label.collision_scale = V(.85 / this.label.scale.x, 7, .1); // FIXME - shouldn't be hardcoded
- }
+ let width = Math.pow(2, Math.ceil(Math.log(measure.width) / Math.log(2)));
+ this.canvas.width = width;
+ if (this.label) {
+ let oldscale = this.label.scale.x;
+ this.label.scale.x = width / this.canvas.height * this.label.scale.y;
+ this.label.collision_scale = V(.85 / this.label.scale.x, 7, .1); // FIXME - shouldn't be hardcoded
+ }
- ctx.font = font;
- ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
+ ctx.font = font;
+ ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
- let c = this.currentcolor;
- // background color
-/*
- ctx.fillStyle = 'rgba(' + c.x + ', ' + c.y + ', ' + c.z + ', 0.1)';
- ctx.fillRect(0, 0, this.canvas.width, this.canvas.height);
-*/
+ let c = this.currentcolor;
+ // background color
+ /*
+ ctx.fillStyle = 'rgba(' + c.x + ', ' + c.y + ', ' + c.z + ', 0.1)';
+ ctx.fillRect(0, 0, this.canvas.width, this.canvas.height);
+ */
- // text with shadow
- ctx.fillStyle = 'rgba(' + c.x + ', ' + c.y + ', ' + c.z + ', 1)';
- ctx.shadowBlur = 2;
- ctx.shadowColor = 'rgba(0,0,0,1)';
- ctx.fillText(this.player_name, (this.canvas.width - measure.width) / 2, this.canvas.height - 10);
+ // text with shadow
+ ctx.fillStyle = 'rgba(' + c.x + ', ' + c.y + ', ' + c.z + ', 1)';
+ ctx.shadowBlur = 2;
+ ctx.shadowColor = 'rgba(0,0,0,1)';
+ ctx.fillText(this.player_name, (this.canvas.width - measure.width) / 2, this.canvas.height - 10);
-/*
- // outer border
- ctx.strokeStyle = 'rgba(' + c.x + ', ' + c.y + ', ' + c.z + ', 1)';
- ctx.shadowColor = 'rgba(' + c.x + ', ' + c.y + ', ' + c.z + ', 1)';
- ctx.strokeRect(0, 0, this.canvas.width, this.canvas.height);
-*/
- elation.events.fire({element: this.canvas, type: 'update'});
+ /*
+ // outer border
+ ctx.strokeStyle = 'rgba(' + c.x + ', ' + c.y + ', ' + c.z + ', 1)';
+ ctx.shadowColor = 'rgba(' + c.x + ', ' + c.y + ', ' + c.z + ', 1)';
+ ctx.strokeRect(0, 0, this.canvas.width, this.canvas.height);
+ */
+ elation.events.fire({element: this.canvas, type: 'update'});
+ });
+ },
+ setFont(fontname) {
+ this.font = fontname;
+ this.updateCanvas();
+ },
+ setAudioSource(source) {
+ console.log('got an audio source', source);
},
handleMouseOver(ev) {
this.currentcolor.set(0,255,0);
diff --git a/media/assets/webui/apps/comms/external/naf-janus-adapter.js b/media/assets/webui/apps/comms/external/naf-janus-adapter.js
--- a/media/assets/webui/apps/comms/external/naf-janus-adapter.js
+++ b/media/assets/webui/apps/comms/external/naf-janus-adapter.js
@@ -2235,7 +2235,6 @@ class JanusAdapter {
_this2.pendingOccupants.delete(occupantId);
_this2.occupantIds.push(occupantId);
_this2.occupants[occupantId] = subscriber;
-console.log('ADD THE OCCUPANT', _this2.occupants);
_this2.setMediaStream(occupantId, subscriber.mediaStream);
@@ -2561,6 +2560,16 @@ console.log('ADD THE OCCUPANT', _this2.occupants);
});
}
+ sendLeave(handle, subscribe) {
+ return handle.sendMessage({
+ kind: "leave",
+ room_id: this.room,
+ user_id: this.clientId,
+ subscribe,
+ token: this.joinToken
+ });
+ }
+
toggleFreeze() {
if (this.frozen) {
this.unfreeze();
diff --git a/media/assets/webui/apps/comms/external/naf-shim.js b/media/assets/webui/apps/comms/external/naf-shim.js
--- a/media/assets/webui/apps/comms/external/naf-shim.js
+++ b/media/assets/webui/apps/comms/external/naf-shim.js
@@ -6,7 +6,10 @@ window.NAF = {
NAF.adapters.adapters[name] = instance;
},
make: function(name) {
- return new NAF.adapters.adapters[name];
+ if (typeof NAF != 'undefined' && name in NAF.adapters.adapters) {
+ return new NAF.adapters.adapters[name];
+ }
+ return null;
}
}
};
@@ -19,12 +22,16 @@ class JanusNAF extends EventTarget {
}
connect(serverURL, appName, roomName) {
let adapter = NAF.adapters.make('janus');
+ if (!adapter) {
+ console.error('Janus NAF adapter not found');
+ return;
+ }
adapter.setServerUrl(serverURL);
adapter.setApp(appName);
adapter.setRoom(roomName);
adapter.setClientId(this.clientId);
adapter.setPeerConnectionConfig(this.getPeerConnectionConfig());
- console.log('I have an adapter', adapter, this.clientId);
+ //console.log('I have an adapter', adapter, this.clientId);
var webrtcOptions = this.getMediaConstraints();
adapter.setWebRtcOptions(webrtcOptions);
@@ -96,7 +103,7 @@ console.log('datachannel closed', id);
console.log('received data', id);
}
occupantsReceived(occupantList) {
-console.log('occupants received', occupantList);
+console.log('occupants received', Object.keys(occupantList), Object.keys(this.connectedClients));
var prevConnectedClients = Object.assign({}, this.connectedClients);
this.connectedClients = occupantList;
this.checkForDisconnectingClients(prevConnectedClients, occupantList);
@@ -158,4 +165,7 @@ console.log('occupants received', occupantList);
}
}
}
+ setRoom(roomId) {
+ this.adapter.setRoom(roomId);
+ }
}
diff --git a/media/assets/webui/apps/comms/userlist.html b/media/assets/webui/apps/comms/userlist.html
--- a/media/assets/webui/apps/comms/userlist.html
+++ b/media/assets/webui/apps/comms/userlist.html
@@ -16,5 +16,4 @@
-
diff --git a/media/assets/webui/apps/comms/voip.css b/media/assets/webui/apps/comms/voip.css
--- a/media/assets/webui/apps/comms/voip.css
+++ b/media/assets/webui/apps/comms/voip.css
@@ -1,22 +1,21 @@
janus-voip-client {
- display: block;
+ display: flex;
border: 1px solid black;
position: relative;
+ max-width: 75vh;
+ flex-wrap: wrap;
}
janus-voip-localuser {
display: block;
- position: absolute;
bottom: 0px;
- right: -72px;
border: 1px solid black;
padding: 0px;
- background: white;
box-shadow: 0 0 5px black;
z-index: 10;
}
janus-voip-localuser video {
- max-width: 64px;
- height: 64px;
+ max-width: 128px;
+ height: 128px;
display: block;
margin: 1px 0 0 1px;
}
@@ -40,12 +39,15 @@ janus-voip-remoteuser video.active {
janus-voip-remoteuser[active] {
transform: scale(1, 1);
}
+janus-voip-localuser h2,
janus-voip-remoteuser h2 {
+/*
position: absolute;
bottom: 0;
left: 0;
right: 0;
margin: 0;
+*/
background: rgba(0,0,0,.2);
text-shadow: 0 0 5px black;
color: white;
@@ -54,10 +56,22 @@ janus-voip-remoteuser h2 {
janus-voip-remoteuser {
}
janus-voip-localuser ui-button {
+ position: absolute;
+ bottom: 1em;
+ right: .2em;
+ font-size: .5em;
+ xdisplay: none;
+ opacity: 0;
+ transition: opacity 200ms linear;
}
-janus-voip-localuser ui-button.muted {
+janus-voip-localuser ui-button.muted,
+janus-voip-remoteuser ui-button.muted {
background: red;
}
+janus-voip-localuser:hover ui-button {
+ display: block;
+ opacity: 1;
+}
janus-voip-picker-audio {
display: block;
text-align: center;
@@ -75,3 +89,28 @@ janus-voip-picker-audio ui-label {
janus-voip-picker-audio ui-select>select {
width: 100%;
}
+janus-voip-remoteuser ui-panel[bottom] {
+ display: flex;
+ flex-direction: row;
+ width: 100%;
+ opacity: 0;
+ transition: opacity 200ms linear;
+}
+janus-voip-remoteuser:hover ui-panel[bottom] {
+ opacity: 1;
+}
+janus-voip-remoteuser ui-slider {
+ font-size: .5em;
+ flex: 1;
+}
+janus-voip-remoteuser ui-button {
+ font-size: .5em;
+ flex: 0;
+ padding: .2em;
+}
+janus-voip-remoteuser[hasvideo] {
+ order: 1;
+}
+janus-voip-remoteuser:not([hasvideo]) {
+ order: 2;
+}
diff --git a/media/assets/webui/apps/comms/voip.js b/media/assets/webui/apps/comms/voip.js
--- a/media/assets/webui/apps/comms/voip.js
+++ b/media/assets/webui/apps/comms/voip.js
@@ -6,8 +6,7 @@ elation.elements.define('janus-voip-client', class extends elation.elements.base
let sfu = new JanusNAF(player.getNetworkUsername()); //'testclient-' + Math.floor(Math.random() * 1e6));
this.sfu = sfu;
-console.log('new thing', room.id);
- sfu.connect('wss://voip.janusxr.org/', 'default', room.id, true);
+ sfu.connect('wss://voip.janusxr.org/', 'default', room.url, true);
this.localuser = document.createElement('janus-voip-localuser');
this.appendChild(this.localuser);
@@ -37,6 +36,33 @@ setTimeout(() => this.removeChild(user), 250);
sfu.adapter.setLocalMediaStream(localStream);
sfu.dispatchEvent(new CustomEvent('voip-media-change', {detail: { stream: localStream }}));
});
+ elation.events.add(janus._target, 'room_change', (ev) => {
+ if (!sfu || !sfu.adapter) return;
+ console.log('change SFU room', room.url, sfu.adapter.publisher, remoteusers, sfu.adapter.room);
+ if (sfu.adapter.publisher) {
+ let handle = sfu.adapter.publisher.handle;
+ sfu.adapter.sendLeave(handle, {
+ notifications: true,
+ data: true
+ });
+ for (let k in remoteusers) {
+ sfu.adapter.sendLeave(handle, {
+ media: k
+ });
+ }
+ }
+ for (let k in remoteusers) {
+ remoteusers[k].destroy();
+ delete remoteusers[k];
+ }
+ sfu.adapter.removeAllOccupants()
+ sfu.adapter.requestedOccupants = null;
+ sfu.adapter.availableOccupants = [];
+ sfu.connectedClients = {};
+ sfu.adapter.setRoom(room.url)
+console.log('room is now', sfu.adapter.room);
+ sfu.adapter.reconnect();
+ });
// FIXME - player proxy should expose .addEventListener()
elation.events.add(player._target, 'username_change', (ev) => this.handleUsernameChange(ev));
}
@@ -46,24 +72,43 @@ setTimeout(() => this.removeChild(user), 250);
});
elation.elements.define('janus-voip-localuser', class extends elation.elements.base {
+ create() {
+ window.addEventListener('keypress', (ev) => {
+ if (ev.key == 'm') {
+ if (player.enabled) {
+ this.toggleMute();
+ }
+ }
+ });
+ }
setData(data) {
-console.log('set my local data', data);
this.muted = false;
if (this.video) {
this.removeChild(this.video);
}
+ if (!this.label) {
+ // label
+ let label = document.createElement('h2');
+ label.innerText = player.getNetworkUsername();
+ this.appendChild(label);
+ this.label = label;
+ }
+
+
// video
- let video = document.createElement('video');
- video.srcObject = data;
- this.appendChild(video);
- video.muted = true;
- video.play();
- this.video = video;
+ if (data.getVideoTracks().length > 0) {
+ let video = document.createElement('video');
+ video.srcObject = data;
+ this.appendChild(video);
+ video.muted = true;
+ video.play();
+ this.video = video;
+ }
if (!this.mutebutton) {
- let mute = elation.elements.create('ui-button', {label: 'Mute'});
+ let mute = elation.elements.create('ui-button', {label: 'Mute', name: 'mutebutton'});
this.appendChild(mute);
mute.addEventListener('click', (ev) => this.toggleMute());
this.mutebutton = mute;
@@ -101,15 +146,21 @@ console.log('set my local data', data);
});
elation.elements.define('janus-voip-remoteuser', class extends elation.elements.base {
create() {
+ this.defineAttributes({
+ 'muted': { type: 'boolean', default: false },
+ 'hasvideo': { type: 'boolean', default: false },
+ });
setTimeout(() => this.setAttribute('active', true), 100);
}
setUserData(data) {
this.id = data.id;
- let label = document.createElement('h2');
- label.innerText = this.id;
- this.appendChild(label);
- this.label = label;
+ if (!this.label) {
+ let label = document.createElement('h2');
+ label.innerText = this.id;
+ this.appendChild(label);
+ this.label = label;
+ }
if (data.media.video) {
let track = data.media.video.getVideoTracks()[0];
@@ -123,18 +174,52 @@ elation.elements.define('janus-voip-remoteuser', class extends elation.elements.
this.video = video;
video.addEventListener('resize', (ev) => { console.log('video resized', video); this.updateVideo(); });
this.updateVideo();
+ this.hasvideo = true;
+ } else {
+ this.hasvideo = false;
}
// audio
let audio = document.createElement('audio');
audio.srcObject = data.media.audio;
this.audio = audio;
+ this.appendChild(audio);
let remoteuser = janus.network.remoteplayers[this.id];
+
+ if (!this.controlpanel) {
+ this.controlpanel = elation.elements.create('ui-panel', { bottom: 1, append: this });
+ this.mutebutton = elation.elements.create('ui-button', { label: 'Mute', name: "mutebutton", append: this.controlpanel });
+ this.volume = elation.elements.create('ui-slider', { name: "volume", min: 0, max: 2, value: this.video.volume, append: this.controlpanel });
+
+ elation.events.add(this.volume, 'change', (ev) => {
+ remoteuser.setVolume(ev.data);
+ this.muted = (ev.data == 0);
+ if (this.muted && !this.mutebutton.hasclass('muted')) {
+ this.mutebutton.addclass('muted');
+ } else if (!this.muted && this.mutebutton.hasclass('muted')) {
+ this.mutebutton.removeclass('muted');
+ }
+ });
+ elation.events.add(this.mutebutton, 'click', (ev) => {
+/*
+ remoteuser.setVolume(0);
+ this.volume.handles[0].value = 0;
+ this.volume.handles[0].render();
+*/
+ this.toggleMute();
+ });
+ }
+
if (remoteuser) {
remoteuser.addVoice(data.media.audio);
this.audio.muted = true;
+ let label = remoteuser.getElementsByTagName('playerlabel')[0];
+ if (label) {
+ label.setAudioSource(audio);
+ }
+
// FIXME - this is pretty unreliable right now, probably some race conditions about loading the avatar vs when we get the video stream
// it also breaks if the avatar changes their avatar after initializing their webcam
/*
@@ -173,24 +258,47 @@ elation.elements.define('janus-voip-remoteuser', class extends elation.elements.
destroy() {
console.log('stop media', this.id);
if (this.video) {
- //this.video.stop();
+ //this.video.pause();
}
if (this.audio) {
- //this.audio.stop();
+ //this.audio.pause();
+ let tracks = this.audio.srcObject.getTracks();
+ tracks.forEach(track => { console.log('stop track', track); track.stop(); });
}
this.removeAttribute('active');
+ if (this.parentNode) {
+ this.parentNode.removeChild(this);
+ }
}
updateVideo() {
let video = this.video;
if (video) {
let hasclass = elation.html.hasclass(video, 'active');
if (video.videoWidth > 0 && video.videoHeight > 0 && !hasclass) {
+ this.hasvideo = true;
elation.html.addclass(video, 'active');
} else if (hasclass && (video.videoWidth == 0 || video.videoHeight == 0)) {
+ this.hasvideo = false;
elation.html.removeclass(video, 'active');
}
}
}
+ toggleMute() {
+ this.muted = !this.muted;
+ let remoteuser = janus.network.remoteplayers[this.id];
+ if (this.muted) {
+ this.mutebutton.addclass('muted');
+ this.lastvolume = this.volume.handles[0].value;
+ remoteuser.setVolume(0);
+ this.volume.handles[0].value = 0;
+ this.volume.handles[0].render();
+ } else {
+ this.mutebutton.removeclass('muted');
+ remoteuser.setVolume(this.lastvolume);
+ this.volume.handles[0].value = this.lastvolume;
+ this.volume.handles[0].render();
+ }
+ }
});
elation.elements.define('janus-voip-picker', class extends elation.elements.base {
create() {
@@ -231,7 +339,7 @@ elation.elements.define('janus-voip-picker', class extends elation.elements.base
elation.elements.define('janus-voip-picker-audio', class extends elation.elements.base {
create() {
this.elements = elation.elements.fromString(`
-
+
Waiting for permission to be granted
@@ -275,6 +383,7 @@ elation.elements.define('janus-voip-picker-audio', class extends elation.element
}
updateDevices() {
this.elements.submit.disabled = true;
+ this.elements.permissions.style.display = 'block';
navigator.mediaDevices.enumerateDevices()
.then(devices => {
this.elements.submit.disabled = false;
@@ -316,6 +425,7 @@ console.log(this.elements.videoDevice.select.childNodes);
this.elements.webcamEnabled.show();
this.elements.videoDevice.hide();
}
+ this.elements.permissions.style.display = 'none';
});
}
-----END OF PAGE-----