From 3b4037553a941f9502075b4cb474fd706c144da5 Mon Sep 17 00:00:00 2001 From: damencho Date: Mon, 1 Oct 2018 14:02:51 -0500 Subject: [PATCH] Adds server-side speaker stats handling. Adds the component which receives the messages from client and a module which enabled on a virtual host will start advertising the component. When clients discover the component they will send message to the component with the name of the room where the dominant speaker event happen. --- doc/speakerstats-prosody.md | 22 +++ resources/prosody-plugins/ext_events.lib.lua | 19 ++- .../prosody-plugins/mod_speakerstats.lua | 7 + .../mod_speakerstats_component.lua | 144 ++++++++++++++++++ 4 files changed, 191 insertions(+), 1 deletion(-) create mode 100644 doc/speakerstats-prosody.md create mode 100644 resources/prosody-plugins/mod_speakerstats.lua create mode 100644 resources/prosody-plugins/mod_speakerstats_component.lua diff --git a/doc/speakerstats-prosody.md b/doc/speakerstats-prosody.md new file mode 100644 index 0000000000..52cc44d145 --- /dev/null +++ b/doc/speakerstats-prosody.md @@ -0,0 +1,22 @@ +# Enabling speakerstats prosody module + +To enable the speaker stats we need to enable speakerstats module under the main +virtual host, this is to enable the advertising the speaker stats component, +which address needs to be specified in `speakerstats_component` option. + +We need to also enable the component with the address specified in `speakerstats_component`. +The component needs also to have the option with the muc component address in +`muc_component` option. + +```lua +VirtualHost "jitsi.example.com" + speakerstats_component = "speakerstats.jitsi.example.com" + modules_enabled = { + "speakerstats"; + } + +Component "speakerstats.jitsi.example.com" "speakerstats_component" + muc_component = "conference.jitsi.example.com" + +Component "conference.jitsi.example.com" "muc" +``` diff --git a/resources/prosody-plugins/ext_events.lib.lua b/resources/prosody-plugins/ext_events.lib.lua index 064beb7574..edc0943bc0 100644 --- a/resources/prosody-plugins/ext_events.lib.lua +++ b/resources/prosody-plugins/ext_events.lib.lua @@ -31,11 +31,28 @@ local function missed(stanza, call_id) module:log("warn", "Implement this lib to trigger external events.") end +-- Event that speaker stats for a conference are available +-- this is a table where key is the jid and the value is a table: +--{ +-- totalDominantSpeakerTime +-- nick +-- displayName +--} +-- This trigger is left unimplemented. The implementation is expected +-- to be specific to the deployment. +local function speaker_stats(room, speakerStats) + module:log( + "warn", + "A module has been configured that triggers external events." + ) + module:log("warn", "Implement this lib to trigger external events.") +end local ext_events = { missed = missed, invite = invite, - cancel = cancel + cancel = cancel, + speaker_stats = speaker_stats } return ext_events diff --git a/resources/prosody-plugins/mod_speakerstats.lua b/resources/prosody-plugins/mod_speakerstats.lua new file mode 100644 index 0000000000..9651c4f952 --- /dev/null +++ b/resources/prosody-plugins/mod_speakerstats.lua @@ -0,0 +1,7 @@ +local speakerstats_component + = module:get_option_string( + "speakerstats_component", "speakerstats"..module.host); + +-- Advertise speaker stats so client can pick up the address and start sending +-- dominant speaker events +module:add_identity("component", "speakerstats", speakerstats_component); diff --git a/resources/prosody-plugins/mod_speakerstats_component.lua b/resources/prosody-plugins/mod_speakerstats_component.lua new file mode 100644 index 0000000000..ecb5ce0550 --- /dev/null +++ b/resources/prosody-plugins/mod_speakerstats_component.lua @@ -0,0 +1,144 @@ +local get_room_from_jid = module:require "util".get_room_from_jid; +local jid_resource = require "util.jid".resource; +local ext_events = module:require "ext_events" + +local muc_component_host = module:get_option_string("muc_component"); +if muc_component_host == nil then + log("error", "No muc_component specified. No muc to operate on!"); + return; +end +local muc_module = module:context("conference."..muc_component_host); +if muc_module == nil then + log("error", "No such muc found, check muc_component config."); + return; +end + +log("debug", "Starting speakerstats for %s", muc_component_host); + +-- receives messages from client currently connected to the room +-- clients indicates their own dominant speaker events +function on_message(event) + -- Check the type of the incoming stanza to avoid loops: + if event.stanza.attr.type == "error" then + return; -- We do not want to reply to these, so leave. + end + + local speakerStats + = event.stanza:get_child('speakerstats', 'http://jitsi.org/jitmeet'); + if speakerStats then + local roomAddress = speakerStats.attr.room; + local room = get_room_from_jid(roomAddress); + + if not room then + log("warn", "No room found %s", roomAddress); + return false; + end + + local roomSpeakerStats = room.speakerStats; + local from = event.stanza.attr.from; + + local occupant = room:get_occupant_by_real_jid(from); + if not occupant then + log("warn", "No occupant %s found for %s", from, roomAddress); + return false; + end + + local newDominantSpeaker = roomSpeakerStats[occupant.jid]; + local oldDominantSpeakerId = roomSpeakerStats['dominantSpeakerId']; + + if oldDominantSpeakerId then + roomSpeakerStats[oldDominantSpeakerId]:setIsDominantSpeaker(false); + end + + if newDominantSpeaker then + newDominantSpeaker:setIsDominantSpeaker(true); + end + + room.speakerStats['dominantSpeakerId'] = occupant.jid; + end + + return true +end + +--- Start SpeakerStats implementation +local SpeakerStats = {}; +SpeakerStats.__index = SpeakerStats; + +function new_SpeakerStats(nick) + return setmetatable({ + totalDominantSpeakerTime = 0; + _dominantSpeakerStart = nil; + _isDominantSpeaker = false; + nick = nick; + displayName = nil; + }, SpeakerStats); +end + +-- Changes the dominantSpeaker data for current occupant +-- saves start time if it is new dominat speaker +-- or calculates and accumulates time of speaking +function SpeakerStats:setIsDominantSpeaker(isNowDominantSpeaker) + log("debug", + "set isDominant %s for %s", tostring(isNowDominantSpeaker), self.nick); + + if not self._isDominantSpeaker and isNowDominantSpeaker then + self._dominantSpeakerStart = os.time(); + elseif self._isDominantSpeaker and not isNowDominantSpeaker then + local now = os.time(); + local timeElapsed = now - (self._dominantSpeakerStart or 0); + + self.totalDominantSpeakerTime + = self.totalDominantSpeakerTime + timeElapsed; + self._dominantSpeakerStart = nil; + end + + self._isDominantSpeaker = isNowDominantSpeaker; +end +--- End SpeakerStats + +-- create speakerStats for the room +function room_created(event) + local room = event.room; + room.speakerStats = {}; +end + +-- Create SpeakerStats object for the joined user +function occupant_joined(event) + local room = event.room; + local occupant = event.occupant; + local nick = jid_resource(occupant.nick); + + if room.speakerStats then + room.speakerStats[occupant.jid] = new_SpeakerStats(nick); + end +end + +-- Occupant left set its dominant speaker to false and update the store the +-- display name +function occupant_leaving(event) + local room = event.room; + local occupant = event.occupant; + + local speakerStatsForOccupant = room.speakerStats[occupant.jid]; + if speakerStatsForOccupant then + speakerStatsForOccupant:setIsDominantSpeaker(false); + + -- set display name + local displayName = occupant:get_presence():get_child_text( + 'nick', 'http://jabber.org/protocol/nick'); + speakerStatsForOccupant.displayName = displayName; + end +end + +-- Conference ended, send speaker stats +function room_destroyed(event) + local room = event.room; + + ext_events.speaker_stats(room, room.speakerStats); +end + +module:hook("message/host", on_message); +muc_module:hook("muc-room-created", room_created, -1); +muc_module:hook("muc-occupant-joined", occupant_joined, -1); +muc_module:hook("muc-occupant-pre-leave", occupant_leaving, -1); +muc_module:hook("muc-room-destroyed", room_destroyed, -1);