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);