Browse Source

added package.json to plugins

Dele Olajide 4 năm trước cách đây
mục cha
commit
3199755c1c

+ 17 - 0
packages/browsertab/package.json

@@ -0,0 +1,17 @@
+{
+  "name": "@converse-plugins/browsertab",
+  "author": "Dele Olajide",
+  "license": "Apache 2.0",
+  "version": "0.0.2",
+  "keywords": [
+    "converse.js",
+    "plugin"
+  ],
+  "publishConfig": {
+    "access": "public"
+  },
+  "repository": {
+    "type": "git",
+    "url": "https://github.com/conversejs/community-plugins.git"
+  }
+}

+ 17 - 0
packages/diagrams/package.json

@@ -0,0 +1,17 @@
+{
+  "name": "@converse-plugins/diagrams",
+  "author": "Dele Olajide",
+  "license": "Apache 2.0",
+  "version": "0.0.2",
+  "keywords": [
+    "converse.js",
+    "plugin"
+  ],
+  "publishConfig": {
+    "access": "public"
+  },
+  "repository": {
+    "type": "git",
+    "url": "https://github.com/conversejs/community-plugins.git"
+  }
+}

+ 17 - 0
packages/directory/package.json

@@ -0,0 +1,17 @@
+{
+  "name": "@converse-plugins/directory",
+  "author": "Dele Olajide",
+  "license": "Apache 2.0",
+  "version": "0.0.2",
+  "keywords": [
+    "converse.js",
+    "plugin"
+  ],
+  "publishConfig": {
+    "access": "public"
+  },
+  "repository": {
+    "type": "git",
+    "url": "https://github.com/conversejs/community-plugins.git"
+  }
+}

+ 17 - 0
packages/jitsimeet/package.json

@@ -0,0 +1,17 @@
+{
+  "name": "@converse-plugins/jitsimeet",
+  "author": "Dele Olajide",
+  "license": "Apache 2.0",
+  "version": "0.0.2",
+  "keywords": [
+    "converse.js",
+    "plugin"
+  ],
+  "publishConfig": {
+    "access": "public"
+  },
+  "repository": {
+    "type": "git",
+    "url": "https://github.com/conversejs/community-plugins.git"
+  }
+}

+ 17 - 0
packages/location/package.json

@@ -0,0 +1,17 @@
+{
+  "name": "@converse-plugins/location",
+  "author": "Dele Olajide",
+  "license": "Apache 2.0",
+  "version": "0.0.2",
+  "keywords": [
+    "converse.js",
+    "plugin"
+  ],
+  "publishConfig": {
+    "access": "public"
+  },
+  "repository": {
+    "type": "git",
+    "url": "https://github.com/conversejs/community-plugins.git"
+  }
+}

+ 17 - 0
packages/muc-directory/package.json

@@ -0,0 +1,17 @@
+{
+  "name": "@converse-plugins/muc-directory",
+  "author": "Dele Olajide",
+  "license": "Apache 2.0",
+  "version": "0.0.2",
+  "keywords": [
+    "converse.js",
+    "plugin"
+  ],
+  "publishConfig": {
+    "access": "public"
+  },
+  "repository": {
+    "type": "git",
+    "url": "https://github.com/conversejs/community-plugins.git"
+  }
+}

+ 17 - 0
packages/screencast/package.json

@@ -0,0 +1,17 @@
+{
+  "name": "@converse-plugins/screencast",
+  "author": "Dele Olajide",
+  "license": "Apache 2.0",
+  "version": "0.0.2",
+  "keywords": [
+    "converse.js",
+    "plugin"
+  ],
+  "publishConfig": {
+    "access": "public"
+  },
+  "repository": {
+    "type": "git",
+    "url": "https://github.com/conversejs/community-plugins.git"
+  }
+}

+ 17 - 0
packages/search/package.json

@@ -0,0 +1,17 @@
+{
+  "name": "@converse-plugins/search",
+  "author": "Dele Olajide",
+  "license": "Apache 2.0",
+  "version": "0.0.2",
+  "keywords": [
+    "converse.js",
+    "plugin"
+  ],
+  "publishConfig": {
+    "access": "public"
+  },
+  "repository": {
+    "type": "git",
+    "url": "https://github.com/conversejs/community-plugins.git"
+  }
+}

+ 17 - 0
packages/stickers/package.json

@@ -0,0 +1,17 @@
+{
+  "name": "@converse-plugins/stickers",
+  "author": "Dele Olajide",
+  "license": "Apache 2.0",
+  "version": "0.0.2",
+  "keywords": [
+    "converse.js",
+    "plugin"
+  ],
+  "publishConfig": {
+    "access": "public"
+  },
+  "repository": {
+    "type": "git",
+    "url": "https://github.com/conversejs/community-plugins.git"
+  }
+}

+ 17 - 0
packages/toolbar-utilities/package.json

@@ -0,0 +1,17 @@
+{
+  "name": "@converse-plugins/toolbar-utilities",
+  "author": "Dele Olajide",
+  "license": "Apache 2.0",
+  "version": "0.0.2",
+  "keywords": [
+    "converse.js",
+    "plugin"
+  ],
+  "publishConfig": {
+    "access": "public"
+  },
+  "repository": {
+    "type": "git",
+    "url": "https://github.com/conversejs/community-plugins.git"
+  }
+}

+ 17 - 0
packages/vmsg/package.json

@@ -0,0 +1,17 @@
+{
+  "name": "@converse-plugins/vmsg",
+  "author": "Dele Olajide",
+  "license": "Apache 2.0",
+  "version": "0.0.2",
+  "keywords": [
+    "converse.js",
+    "plugin"
+  ],
+  "publishConfig": {
+    "access": "public"
+  },
+  "repository": {
+    "type": "git",
+    "url": "https://github.com/conversejs/community-plugins.git"
+  }
+}

+ 13 - 0
packages/voice-chat/readme.md

@@ -0,0 +1,13 @@
+# Voice chat plugin for converse.js
+
+<img src="https://github.com/conversejs/community-plugins/blob/master/packages/voice-chat/voice-chat.png" />
+
+## Overview
+This plugin adds an audio conferencing voice chat to Converse. It requires an XMPP wrapper on the [Kraken webRTC SFU](https://github.com/MixinNetwork/kraken) like the [Ohun plugin for Openfire](https://github.com/igniterealtime/openfire-ohun-plugin)
+
+
+## Install
+See https://m.conversejs.org/docs/html/plugin_development.html on how to install this plugin
+
+## How to use
+Click on the telephone icon on the conversation toolbar to start talking. See https://discourse.igniterealtime.org/t/ohun-a-new-openfire-plugin-for-voice-chat/88794

+ 838 - 0
packages/voice-chat/voice-chat.js

@@ -0,0 +1,838 @@
+(function (root, factory) {
+    if (typeof define === 'function' && define.amd) {
+        define(["converse"], factory);
+    } else {
+        factory(converse);
+    }
+}(this, function (converse) {
+    let _converse, Strophe, $iq, $msg, $pres, $build, b64_sha1, _ , dayjs, html;
+    let ohun = {}, ohunRoom, configuration;
+
+    window.addEventListener("unload", function()
+    {
+        const peers = Object.getOwnPropertyNames(ohun)
+
+        for (let i=0; i<peers.length; i++)
+        {
+           if (ohun[peers[i]].peer)         ohun[peers[i]].peer.close();
+           if (ohun[peers[i]].localStream)  ohun[peers[i]].localStream.getTracks().forEach(track => track.stop());
+        }
+    });
+
+    converse.plugins.add("voice-chat", {
+        dependencies: [],
+
+        initialize: function () {
+            _converse = this._converse;
+            Strophe = converse.env.Strophe;
+            $iq = converse.env.$iq;
+            $msg = converse.env.$msg;
+            $pres = converse.env.$pres;
+            $build = converse.env.$build;
+            b64_sha1 = converse.env.b64_sha1;
+            _ = converse.env._;
+            dayjs = converse.env.dayjs;
+
+            _converse.api.settings.set("visible_toolbar_buttons", {call: true});
+
+            _converse.api.listen.on('connected', function()
+            {
+                listenForOhunEvents();
+            });
+
+            _converse.api.listen.on('getToolbarButtons', function(toolbar_el, buttons)
+            {
+                const jid = toolbar_el.model.get("jid");
+
+                if (jid.endsWith(_converse.connection.domain))
+                {
+                    setTimeout(function()
+                    {
+                        const icon = toolbar_el.querySelector("button.toggle-call converse-icon.fa.fa-phone");
+                        ohun[jid] = {icon: icon};
+                        console.debug("getToolbarButtons", ohun);
+
+                    }, 3000);
+                }
+
+                return buttons;
+            });
+
+            _converse.api.listen.on('callButtonClicked', function(data)
+            {
+                const jid = data.model.get("jid");
+                const view = _converse.chatboxviews.get(jid);
+
+                console.debug("callButtonClicked", jid, view);
+
+                if (jid.endsWith(_converse.connection.domain))
+                {
+                    if (configuration && view)
+                    {
+                        ohun[jid].view = view;
+                        const icon = view.el.querySelector("button.toggle-call converse-icon.fa.fa-phone svg");
+                        icon.setAttribute("data-status", "off");
+                        startVoiceChat(view.model, icon);
+                    }
+                }
+            });
+
+            _converse.api.listen.on('chatBoxClosed', function (view)
+            {
+                const jid = view.model.get("jid");
+                console.debug("chatBoxClosed", jid);
+                disconnectKraken(view.model, true);
+            });
+
+            _converse.api.listen.on('parseMessage', parseVoiceChatMessage);
+            _converse.api.listen.on('parseMUCMessage', parseVoiceChatMessage);
+
+            console.debug("voice-chat plugin is ready");
+        },
+
+        overrides: {
+            MessageView: {
+
+                renderChatMessage: async function renderChatMessage()
+                {
+                    if (this.model.get("json_type")) {
+                        const json = this.model.get("json_payload");
+                        console.debug('ohun - renderChatMessage', this.model.get("json_jid"), json);
+
+                        if (json.method == "publish" || json.method == "end")
+                        {
+                            this.model.set('message', '/me ' + (json.method == 'publish' ? 'starts' : 'stops') + ' ohun');
+                            await this.__super__.renderChatMessage.apply(this, arguments);
+                        }
+                    } else {
+                        await this.__super__.renderChatMessage.apply(this, arguments);
+                    }
+                }
+            },
+
+            ChatBoxView: {
+
+                parseMessageForCommands: function(text)
+                {
+                    console.debug('ohun - parseMessageForCommands', text);
+
+                    const match = text.replace(/^\s*/, "").match(/^\/(.*?)(?: (.*))?$/) || [false, '', ''];
+                    const command = match[1].toLowerCase();
+                    const view = this;
+
+                    let nick = match[2];
+
+                    if (_converse.connection.pass && _converse.connection.pass != "" && (command === "vchat"))
+                    {
+                        let jid = view.model.get("jid");
+
+                        if (ohun[jid].localStream)
+                        {
+                            if (this.model.get("type") == "chatroom")
+                            {
+                                if (!nick)
+                                {
+                                    this.showHelpMessages(["Nickname required"]);
+                                    return true;
+                                }
+
+                                nick = nick.trim();
+                                if (nick.startsWith("@")) nick = nick.substring(1);
+                            }
+
+                            const occupant = this.model.occupants.findWhere({'nick': nick});
+
+                            if (occupant) {
+                                handleUserClick(jid, occupant.get('jid'));
+                            } else {
+                                this.showHelpMessages(["Nickname not found"]);
+                            }
+                        } else {
+                            this.showHelpMessages(["Ohun not yet started"]);
+                        }
+                        return true;
+                    }
+
+                    return false;
+                }
+            }
+        },
+
+        closeKraken: function(chat)
+        {
+            console.debug("closeKraken", chat);
+            disconnectKraken(chat, true);
+        }
+    });
+
+    function parseVoiceChatMessage(stanza, attrs)
+    {
+        const json = stanza.querySelector('json');
+
+        if (json)
+        {
+            console.debug("parseVoiceChatMessage", json);
+
+            attrs['json_jid'] = json.getAttribute('jid');
+            attrs['json_type'] = json.getAttribute('type');
+            attrs['json_payload'] = json.innerHTML;
+        }
+
+        return attrs
+    }
+
+    function listenForOhunEvents()
+    {
+        console.debug("listenForOhunEvents");
+
+        getStunTurn();
+        addXmppEventHandler();
+    }
+
+
+    function addXmppEventHandler()
+    {
+        _converse.connection.addHandler(function (message)
+        {
+            const to = Strophe.getBareJidFromJid(message.getAttribute("to"));
+            const from = Strophe.getBareJidFromJid(message.getAttribute("from"));
+            const json_ele = message.querySelector("json");
+            const json = JSON.parse(json_ele.innerHTML);
+
+            ohunRoom = decodeURIComponent(json.id);
+
+            const room = ohunRoom;
+            const json_jid = Strophe.getBareJidFromJid(json_ele.getAttribute("jid"));
+            const json_type = json_ele.getAttribute("type");
+
+            async function handlePeerEnd(json)
+            {
+                console.debug("handlePeerEnd", from, json);
+                const chat = _converse.chatboxes.get(room);
+                disconnectKraken(chat, false);
+            }
+
+            async function handlePeerTrickle(json)
+            {
+                console.debug("handlePeerTrickle", from, json);
+
+                const data = JSON.parse(json.params[3]);
+                const candidate = new RTCIceCandidate(data);
+                await ohun[room].peer.addIceCandidate(candidate);
+            }
+
+            async function handlePeerAnswer(json)
+            {
+                console.debug("handlePeerAnswer", from, json);
+
+                ohun[room].ucid = json.id;
+                const data = JSON.parse(json.params[2]);
+                await ohun[room].peer.setRemoteDescription(new RTCSessionDescription(data));
+                sendCandidates();
+            }
+
+            async function handlePeerPublish(json)
+            {
+                console.debug("handlePublish", from, json);
+
+                var prompt = new Notification(from, {
+                    'body': "Voice Chat?",
+                    'lang': _converse.locale,
+                    'icon': _converse.notification_icon,
+                    'requireInteraction': true
+                });
+
+                prompt.onclick = function(event)
+                {
+                    //event.preventDefault();
+
+                    _converse.api.chats.open(from, {'bring_to_foreground': true}, true).then(chat =>
+                    {
+                        const room = chat.get("jid");
+                        const view = _converse.chatboxviews.views[chat.id];
+                        const icon = view.el.querySelector(".fa-volume-up");
+
+                        ohun[room].p2p = json;
+                        ohun[room].sfu = false;
+                        ohun[room].view = view;
+                        connectKraken(chat, icon);
+                    });
+                }
+
+                // hack - emove me
+                prompt.onclick();
+            }
+
+            async function handleAnswer(json)
+            {
+                console.debug("handleAnswer", from, json);
+                ohun[room].ucid = json.data.track;
+                await ohun[room].peer.setRemoteDescription(json.data.sdp);
+                sendCandidates();
+            }
+
+            function sendCandidates()
+            {
+                console.debug("sendCandidates", room);
+
+                if (ohun[room].candidates)
+                {
+                    for (let i=0; i<ohun[room].candidates.length; i++)
+                    {
+                        console.debug("handleAnswer - candidate", ohun[room].candidates[i]);
+                        sendMessage('trickle', room, ohun[room].candidates[i]);
+                    }
+                }
+            }
+
+            function subscribe()
+            {
+                console.debug("listenForOhunEvents - subscribe", room);
+                sendMessage('subscribe', room);
+            }
+
+            async function handleOffer(json)
+            {
+                console.debug("handleOffer", from, json);
+                await ohun[room].peer.setRemoteDescription(json.data);
+                var sdp = await ohun[room].peer.createAnswer();
+                await ohun[room].peer.setLocalDescription(sdp);
+                sendMessage('answer', room, sdp);
+            }
+
+            if (json_type == "response")
+            {
+                console.debug("Ohun Response", from, json_jid, json.id, room, json);
+
+                if (json.data && json.data.sdp)
+                {
+                    if (json.data.sdp.type === 'answer')
+                    {
+                        if (_converse.bare_jid == json_jid) handleAnswer(json);
+                        if (ohun[room].peer) setTimeout(subscribe, 1000);
+                    }
+                    else
+
+                    if (json.data.type === 'offer' && _converse.bare_jid == json_jid)
+                    {
+                        handleOffer(json);
+                    }
+                }
+            }
+            else
+
+            if (json_type == "peer" && _converse.bare_jid == to)
+            {
+                console.debug("Ohun Request", to, from, json_jid, json.id, room, json);
+
+                if (json.method === 'publish')  handlePeerPublish(json);
+                if (json.method === 'answer')   handlePeerAnswer(json);
+                if (json.method === 'trickle')  handlePeerTrickle(json);
+                if (json.method === 'end')      handlePeerEnd(json);
+            }
+
+            return true;
+
+        }, "urn:xmpp:json:0", 'message');
+    }
+
+    function getStunTurn()
+    {
+        configuration = {iceServers: [], bundlePolicy: 'max-bundle',  rtcpMuxPolicy: 'require', sdpSemantics: 'unified-plan'};
+
+        _converse.connection.sendIQ($iq({type: 'get', to: _converse.connection.domain}).c('services', {xmlns: 'urn:xmpp:extdisco:1'}).c('service', {host: 'turn.' + _converse.connection.domain}), function (res)
+        {
+            console.debug('ohun - getStunAndTurnCredentials', res);
+
+            res.querySelectorAll('service').forEach(function (el)
+            {
+                console.debug('getStunTurn - getStunAndTurnCredentials - item', el);
+                var dict = {};
+
+                switch (el.getAttribute('type'))
+                {
+                case 'stun':
+                    dict.url = 'stun:' + el.getAttribute('host');
+                    if (el.getAttribute('port')) {
+                        dict.url += ':' + el.getAttribute('port');
+                    }
+                    configuration.iceServers.push(dict);
+                    break;
+                case 'turn':
+                    dict.url = 'turn:';
+
+                    if (el.getAttribute('username')) {
+                        dict.username = el.getAttribute('username');
+                    }
+                    dict.url += el.getAttribute('host');
+
+                    if (el.getAttribute('port')) {
+                        dict.url += ':' + el.getAttribute('port');
+                    }
+                    if (el.getAttribute('transport')) {
+                        dict.url += '?transport=' + el.getAttribute('transport');
+                    }
+                    if (el.getAttribute('password')) {
+                        dict.credential = el.getAttribute('password');
+                    }
+                    configuration.iceServers.push(dict);
+                    break;
+                }
+            });
+
+            if (configuration.iceServers.length > 0)
+            {
+                configuration.iceTransportPolicy = 'relay';
+                console.debug('getStunTurn - getStunAndTurnCredentials - config', configuration);
+            }
+
+        }, function (err) {
+            console.warn('getting turn credentials failed', err);
+        });
+    }
+
+    function newElement(el, id, html, className)
+    {
+        var ele = document.createElement(el);
+        if (id) ele.id = id;
+        if (html) ele.innerHTML = html;
+        if (className) ele.classList.add(className);
+        document.body.appendChild(ele);
+        return ele;
+    }
+
+    function attachBadge(room, jid, flag, stream)
+    {
+        const chatbox = _converse.chatboxes.get(room);
+
+        if (chatbox)
+        {
+            const occupant = chatbox.occupants.findWhere({'jid': jid});
+            const id = occupant.get('id');
+            const nick = occupant.get('nick');
+            const element = document.getElementById(id);
+
+            console.debug("ohun attachBadge", jid, nick, flag, room, element, stream);
+
+            if (element)
+            {
+                const badges = element.querySelector(".occupant-badges");
+                const html = '<a data-status="on" data-room="' + room + '" id="' + id + '" data-jid="' + jid + '" class="fas fa-volume-up" title="Voice chat" style="font-size: 20px;"></a>';
+                let ohunEle = element.querySelector(".occupants-ohun");
+
+                if (ohunEle)
+                {
+                    ohunEle.innerHTML = html;
+                    ohunEle.style.display = "";
+                }
+                else {
+                    ohunEle = newElement('span', null, html, 'occupants-ohun');
+                    badges.appendChild(ohunEle);
+
+                    ohunEle.addEventListener('click', function(evt)
+                    {
+                        evt.stopPropagation();
+                        handleUserClick(evt.target.getAttribute("data-room"), evt.target.getAttribute("data-jid"));
+
+                    }, false);
+
+                }
+
+                const harker = hark(stream, {interval: 100, history: 4 });
+
+                harker.on('speaking', function()
+                {
+                    if (stream.getTracks()[0].enabled)
+                    {
+                        const icon = ohunEle.querySelector("a");
+                        //console.debug("ohun speaking", jid, ohunEle, icon);
+                        changeIcon(icon, "fa-volume-up", "green", "on");
+                        showStatusMessage(ohun[room].view, nick + ' is speaking');
+                    }
+                });
+
+                harker.on('stopped_speaking', function()
+                {
+                    if (stream.getTracks()[0].enabled)
+                    {
+                        const icon = ohunEle.querySelector("a");
+                        //console.debug("ohun quiet", jid, ohunEle, icon);
+                        changeIcon(icon, "fa-volume-up", "#aaa", "off");
+                        showStatusMessage(ohun[room].view, nick + ' stopped speaking');
+                    }
+                });
+
+                ohun[room].icons[jid] = {icon: ohunEle, stream: stream, harker: harker, nick: nick};
+
+            }
+        }
+    }
+
+    function showStatusMessage(view, msg)
+    {
+        if (view && msg)
+        {
+            const div = view.el.querySelector(".message.chat-info");
+            //console.debug("showStatusMessage", div, msg);
+
+            if (div) {
+                div.innerHTML = msg
+            } else {
+                view.showHelpMessages([msg]);
+            }
+        }
+    }
+
+    function uuidv4() {
+        return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
+          var r = Math.random() * 16 | 0, v = c == 'x' ? r : (r & 0x3 | 0x8);
+          return v.toString(16);
+        });
+    }
+
+    function handleUserClick(room, jid)
+    {
+        const ohunEle = ohun[room].icons[jid].icon;
+        const stream = ohun[room].icons[jid].stream;
+        const nick = ohun[room].icons[jid].nick;
+        const track = stream.getTracks()[0];
+
+        if (ohunEle && stream)
+        {
+            const icon = ohunEle.querySelector("a");
+            console.debug("ohun handleUserClick", jid, nick, ohunEle, icon);
+
+            if (!track.enabled) {
+                track.enabled = true;
+                changeIcon(icon, "fa-volume-up", "#aaa", "on");
+                showStatusMessage(ohun[room].view, nick + ' is unmuted');
+            } else {
+                track.enabled = false;
+                changeIcon(icon, "fa-volume-up", "red", "off");
+                showStatusMessage(ohun[room].view, nick + ' is muted');
+            }
+        }
+        else log.warn("No stream or icon found");
+    }
+
+    function startVoiceChat(chat, icon)
+    {
+        console.debug("startVoiceChat", chat, icon);
+
+        if (icon.getAttribute("data-status") == "off") {
+
+            if (confirm("Ohun with " + chat.get("jid"))) connectKraken(chat, icon);
+
+        } else {
+            disconnectKraken(chat, true);
+        }
+    }
+
+    function changeIcon(icon, newClass, color, status)
+    {
+        icon.classList.remove("fa-volume-off");
+        icon.classList.remove("fa-volume-up");
+        icon.classList.remove("fa-volume-down");
+        icon.classList.add(newClass);
+        icon.style.color = color;
+        icon.style.fill = color;
+        icon.setAttribute("data-status", status);
+
+console.log("RRRRRRRRRRRRRRRRRRRRRRRRRRRRR", icon);
+    }
+
+    function connectKraken(chat, icon)
+    {
+        const room = chat.get("jid");
+        const sfu = chat.get("type") == "chatroom";
+        console.debug("ohun connect kraken", room, sfu);
+
+        setupKraken(room, sfu, icon);
+        //setupSpeechRecognition(room);
+    }
+
+    function disconnectKraken(chat, send)
+    {
+        if (chat)
+        {
+            const room = chat.get("jid");
+            console.debug("ohun disconnect kraken", room, ohun[room]);
+
+            if (send) sendMessage('end', room);
+
+            if (ohun[room])
+            {
+                if (ohun[room].icons && ohun[room].localStream && ohun[room].peer)
+                {
+                    ohun[room].localStream.getTracks().forEach((track) => { track.stop() });
+                    ohun[room].peer.close();
+
+                    const icons = Object.getOwnPropertyNames(ohun[room].icons)
+
+                    for (let i=0; i<icons.length; i++)
+                    {
+                       ohun[room].icons[icons[i]].icon.style.display = "none";
+                    }
+
+                    updateOhunIcon(room, 'voice chat stopped', "#aaa", "off");
+                }
+
+                if (ohun[room].recognitionActive && ohun[room].recognition)
+                {
+                    ohun[room].recognition.stop();
+                    ohun[room].recognitionActive = false;
+                }
+
+                ohun[room] = {};
+            }
+        }
+    }
+
+    function updateOhunIcon(room, msg, color, status)
+    {
+        console.debug("updateOhunIcon", room, msg, color, status, ohun);
+
+        showStatusMessage(ohun[room].view, msg);
+        changeIcon(ohun[room].icon, "fa-volume-up", color, status);
+
+        const id = ohun[room].view.model.get('box_id');
+        const item = document.getElementById('pade-active-conv-ohun-' + id);
+
+        if (item)
+        {
+            item.style.color = color;
+            item.style.visibility = (status == "off") ? "hidden" : "visible";
+        }
+    }
+
+    function getRoom()
+    {
+        let room = ohunRoom;
+        if (!room) room = getSelectedChatBox().model.get("jid");
+        return room;
+    }
+
+    function getSelectedChatBox()
+    {
+        var views = _converse.chatboxviews.model.models;
+        var view = null;
+
+        console.debug("getSelectedChatBox", views);
+
+        for (var i=0; i<views.length; i++)
+        {
+            if ((views[i].get('type') === "chatroom" || views[i].get('type') === "chatbox") && !views[i].get('hidden'))
+            {
+                view = _converse.chatboxviews.views[views[i].id];
+                break;
+            }
+        }
+        return view;
+    }
+
+    async function setupKraken(room, sfu, icon)
+    {
+        console.debug("ohun setup kraken", room, sfu);
+
+        ohun[room].candidates = [];
+        ohun[room].icons= {};
+        if (icon) ohun[room].icon = icon;
+        ohun[room].sfu = sfu;
+
+        ohun[room].peer = new RTCPeerConnection(configuration);
+        ohun[room].peer.createDataChannel('useless');
+
+        ohun[room].peer.onicecandidate = ({candidate}) =>
+        {
+            if (candidate)
+            {
+                const chat = getRoom();
+                console.debug("candidate", ohun[room].ucid, candidate, chat);
+
+                if (ohun[chat].ucid)
+                {
+                    sendMessage('trickle', chat, candidate);
+                }
+                else {
+                    ohun[chat].candidates.push(candidate);
+                }
+            }
+        };
+
+        ohun[room].peer.ontrack = function(event)
+        {
+            const stream = event.streams[0];
+            let chat = getRoom();
+
+            function createAudioElement()
+            {
+                event.track.onmute = function(event)
+                {
+                  console.debug("ohun onmute", event.target.id, event);
+                }
+
+                const aid = 'peer-audio-' + stream.id;
+                let el = document.getElementById(aid);
+
+                if (el) {
+                    el.srcObject = stream;
+                } else {
+                    el = document.createElement(event.track.kind)
+                    el.id = aid;
+                    el.srcObject = stream;
+                    el.autoplay = true;
+                    el.controls = false;
+                    document.body.appendChild(el);
+                }
+            }
+
+            if (!ohun[chat].sfu || ohun[chat].p2p)
+            {
+                console.debug('ohun track p2p data', stream);
+
+                createAudioElement();
+                updateOhunIcon(chat, 'voice chat started', "red", "on");
+
+            } else {
+                const uname = JSON.parse(atob(decodeURIComponent(stream.id)));
+                const json_jid = uname.jid;
+
+                chat = uname.room;
+                console.debug('ohun track sfu data', chat, json_jid, stream);
+
+                if (json_jid == _converse.bare_jid)
+                {
+                    attachBadge(chat, json_jid, true, ohun[chat].localStream);
+                    updateOhunIcon(chat, 'voice chat started', "red", "on");
+                }
+                else {
+                    createAudioElement();
+                    attachBadge(chat, json_jid, true, stream);
+                }
+            }
+        }
+
+        try {
+            const constraints = {audio: true, video: false };
+            ohun[room].localStream = await navigator.mediaDevices.getUserMedia(constraints);
+            console.debug("ohun - local stream", ohun[room].localStream);
+
+            ohun[room].localStream.getTracks().forEach((track) => {
+                ohun[room].peer.addTrack(track, ohun[room].localStream);
+            });
+
+            if (ohun[room].p2p) {
+                answerStream(room);
+            } else {
+                publishStream(room);
+            }
+        } catch (err) {
+            console.error(err);
+        }
+    }
+
+    async function answerStream(room)
+    {
+        ohun[room].rnameRPC = encodeURIComponent(Strophe.getNodeFromJid(room));
+        ohun[room].unameRPC = encodeURIComponent(btoa(JSON.stringify({room: room, jid: _converse.bare_jid, nick: _converse.nickname})));
+
+        console.debug("answerStream", room, ohun[room].p2p);
+
+        const data = JSON.parse(ohun[room].p2p.params[2]);
+
+        await ohun[room].peer.setRemoteDescription(new RTCSessionDescription(data));
+        var sdp = await ohun[room].peer.createAnswer();
+        await ohun[room].peer.setLocalDescription(sdp);
+        sendMessage('answer', room, sdp);
+    }
+
+    async function publishStream(room)
+    {
+        ohun[room].rnameRPC = encodeURIComponent(Strophe.getNodeFromJid(room));
+        ohun[room].unameRPC = encodeURIComponent(btoa(JSON.stringify({room: room, jid: _converse.bare_jid, nick: _converse.nickname})));
+
+        await ohun[room].peer.setLocalDescription(await ohun[room].peer.createOffer());
+
+        console.debug("ohun - publish", room, ohun[room].rnameRPC, ohun[room].unameRPC, ohun[room].peer.localDescription);
+        sendMessage('publish', room, ohun[room].peer.localDescription);
+    }
+
+    function sendMessage(method, room, payload)
+    {
+        console.debug("sendMessage", method, room, payload);
+
+        if (ohun[room])
+        {
+            const target = ohun[room].sfu ? room : _converse.bare_jid;
+            const json_type = ohun[room].sfu ? 'request' : 'peer';
+            const type = ohun[room].sfu ? 'groupchat' : 'chat';
+            const params = [ohun[room].rnameRPC, ohun[room].unameRPC];
+
+            if (ohun[room].ucid)    params.push(ohun[room].ucid);
+            if (payload)            params.push(JSON.stringify(payload));
+
+            const body = JSON.stringify({id: target, method: method, params: params});
+            _converse.connection.send($msg({type: type, to: room}).c("json",{xmlns: "urn:xmpp:json:0", type: json_type}).t(body));
+        }
+        else console.warn("sendMessage - voice chat not ready");
+    }
+
+    function uuidv4()
+    {
+        return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
+          var r = Math.random() * 16 | 0, v = c == 'x' ? r : (r & 0x3 | 0x8);
+          return v.toString(16);
+        });
+    }
+
+    function setupSpeechRecognition(room)
+    {
+        console.debug("setupSpeechRecognition", room);
+
+        ohun[room].recognition = new webkitSpeechRecognition();
+        ohun[room].recognition.lang = _converse.locale;
+        ohun[room].recognition.continuous = true;
+        ohun[room].recognition.interimResults = false;
+
+        ohun[room].recognition.onresult = function(event)
+        {
+            console.debug("Speech recog event", event)
+
+            if(event.results[event.resultIndex].isFinal==true)
+            {
+                const transcript = event.results[event.resultIndex][0].transcript;
+                console.debug("Speech recog transcript", transcript);
+                ohun[room].view.model.sendMessage(transcript);
+            }
+        }
+
+        ohun[room].recognition.onspeechend  = function(event)
+        {
+            console.debug("Speech recog onspeechend", event);
+        }
+
+        ohun[room].recognition.onstart = function(event)
+        {
+            console.debug("Speech to text started", event);
+            ohun[room].recognitionActive = true;
+        }
+
+        ohun[room].recognition.onend = function(event)
+        {
+            console.debug("Speech to text ended", event);
+
+            if (ohun[room].recognitionActive)
+            {
+                console.debug("Speech to text restarted");
+                setTimeout(function() {ohun[room].recognition.start()}, 1000);
+            }
+        }
+
+        ohun[room].recognition.onerror = function(event)
+        {
+            console.debug("Speech to text error", event);
+        }
+
+        ohun[room].recognition.start();
+    }
+}));

BIN
packages/voice-chat/voice-chat.png