repo: janusweb
action: commit
revision: 
path_from: 
revision_from: c3c71b875f9b8dae2932cfa6454e0bc9344bb4c3:
path_to: 
revision_to: 
git.thebackupbox.net
janusweb
git clone git://git.thebackupbox.net/janusweb
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
index 178185c8c0d89656326b282dec22b39399325f79..
index ..7d78dc9df958c162bc977967ebfe3dbb8e49968d 100644
--- 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
index 4261fe711ee0af0cdf3ad060696855aba83e9397..
index ..61f8f325fe30fbbb33424a46ea5eb41e327098ce 100644
--- 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
index de2ae860b5a83d3cc207e656e73864fc8139c477..
index ..b9e38770ff13dfa2b651d6e9fc38cf51e5ce5ff9 100644
--- 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
index f72c8e9822c3b3ef5b0209e021f94ee9976f486a..
index ..6b963b790a9c18275d1a1116aa42c38cea65ce29 100644
--- 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
index 0622f587e50a4969c3af1f79bdfdcc1f3559c957..
index ..241318af9874d1702131db7c0b704431d62ef30e 100644
--- 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
index 3598ef80316e6e011fa3f6faf0546e8063e177b3..
index ..073e23f12c2169e4fab2cbe046c2e4f54236802e 100644
--- 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
index 8f2ba182ef895291c46e205f61763ba0f9ab553e..
index ..eb2a4721127295749a5385ba11f4143b2f612d86 100644
--- 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
index bd13bd7e27b71743abc67b3c339b8cb7ebb1ce03..
index ..2dacf4bea8b2d3d5c43c525b59f889a8c74a1cd7 100644
--- 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-----