"Fossies" - the Fresh Open Source Software Archive

Member "jitsi-meet-6444/resources/prosody-plugins/mod_speakerstats_component.lua" (8 Aug 2022, 13463 Bytes) of package /linux/misc/jitsi-meet-6444.tar.gz:


As a special service "Fossies" has tried to format the requested source page into HTML format using (guessed) Lua source code syntax highlighting (style: standard) with prefixed line numbers and code folding option. Alternatively you can here view or download the uninterpreted source code file.

    1 local get_room_from_jid = module:require "util".get_room_from_jid;
    2 local room_jid_match_rewrite = module:require "util".room_jid_match_rewrite;
    3 local is_healthcheck_room = module:require "util".is_healthcheck_room;
    4 local jid_resource = require "util.jid".resource;
    5 local ext_events = module:require "ext_events"
    6 local st = require "util.stanza";
    7 local socket = require "socket";
    8 local json = require "util.json";
    9 local um_is_admin = require "core.usermanager".is_admin;
   10 local jid_split = require 'util.jid'.split;
   11 
   12 -- we use async to detect Prosody 0.10 and earlier
   13 local have_async = pcall(require, "util.async");
   14 if not have_async then
   15     module:log("warn", "speaker stats will not work with Prosody version 0.10 or less.");
   16     return;
   17 end
   18 
   19 local muc_component_host = module:get_option_string("muc_component");
   20 local muc_domain_base = module:get_option_string("muc_mapper_domain_base");
   21 
   22 if muc_component_host == nil or muc_domain_base == nil then
   23     log("error", "No muc_component specified. No muc to operate on!");
   24     return;
   25 end
   26 local breakout_room_component_host = "breakout." .. muc_domain_base;
   27 
   28 log("info", "Starting speakerstats for %s", muc_component_host);
   29 
   30 local main_muc_service;
   31 
   32 local function is_admin(jid)
   33     return um_is_admin(jid, module.host);
   34 end
   35 
   36 -- Searches all rooms in the main muc component that holds a breakout room
   37 -- caches it if found so we don't search it again
   38 local function get_main_room(breakout_room)
   39     if breakout_room._data and breakout_room._data.main_room then
   40         return breakout_room._data.main_room;
   41     end
   42 
   43     -- let's search all rooms to find the main room
   44     for room in main_muc_service.each_room() do
   45         if room._data and room._data.breakout_rooms_active and room._data.breakout_rooms[breakout_room.jid] then
   46             breakout_room._data.main_room = room;
   47             return room;
   48         end
   49     end
   50 end
   51 
   52 -- receives messages from client currently connected to the room
   53 -- clients indicates their own dominant speaker events
   54 function on_message(event)
   55     -- Check the type of the incoming stanza to avoid loops:
   56     if event.stanza.attr.type == "error" then
   57         return; -- We do not want to reply to these, so leave.
   58     end
   59 
   60     local speakerStats
   61         = event.stanza:get_child('speakerstats', 'http://jitsi.org/jitmeet');
   62     if speakerStats then
   63         local roomAddress = speakerStats.attr.room;
   64         local room = get_room_from_jid(room_jid_match_rewrite(roomAddress));
   65 
   66         if not room then
   67             log("warn", "No room found %s", roomAddress);
   68             return false;
   69         end
   70 
   71         if not room.speakerStats then
   72             log("warn", "No speakerStats found for %s", roomAddress);
   73             return false;
   74         end
   75 
   76         local roomSpeakerStats = room.speakerStats;
   77         local from = event.stanza.attr.from;
   78 
   79         local occupant = room:get_occupant_by_real_jid(from);
   80         if not occupant then
   81             log("warn", "No occupant %s found for %s", from, roomAddress);
   82             return false;
   83         end
   84 
   85         local newDominantSpeaker = roomSpeakerStats[occupant.jid];
   86         local oldDominantSpeakerId = roomSpeakerStats['dominantSpeakerId'];
   87 
   88         if oldDominantSpeakerId then
   89             local oldDominantSpeaker = roomSpeakerStats[oldDominantSpeakerId];
   90             if oldDominantSpeaker then
   91                 oldDominantSpeaker:setDominantSpeaker(false);
   92             end
   93         end
   94 
   95         if newDominantSpeaker then
   96             newDominantSpeaker:setDominantSpeaker(true);
   97         end
   98 
   99         room.speakerStats['dominantSpeakerId'] = occupant.jid;
  100     end
  101 
  102     local faceExpression = event.stanza:get_child('faceExpression', 'http://jitsi.org/jitmeet');
  103 
  104     if faceExpression then
  105         local roomAddress = faceExpression.attr.room;
  106         local room = get_room_from_jid(room_jid_match_rewrite(roomAddress));
  107 
  108         if not room then
  109             log("warn", "No room found %s", roomAddress);
  110             return false;
  111         end
  112          if not room.speakerStats then
  113             log("warn", "No speakerStats found for %s", roomAddress);
  114             return false;
  115         end
  116         local from = event.stanza.attr.from;
  117 
  118         local occupant = room:get_occupant_by_real_jid(from);
  119         if not occupant then
  120             log("warn", "No occupant %s found for %s", from, roomAddress);
  121             return false;
  122         end
  123         local faceExpressions = room.speakerStats[occupant.jid].faceExpressions;
  124         faceExpressions[faceExpression.attr.expression] =
  125             faceExpressions[faceExpression.attr.expression] + tonumber(faceExpression.attr.duration);
  126     end
  127 
  128     return true
  129 end
  130 
  131 --- Start SpeakerStats implementation
  132 local SpeakerStats = {};
  133 SpeakerStats.__index = SpeakerStats;
  134 
  135 function new_SpeakerStats(nick, context_user)
  136     return setmetatable({
  137         totalDominantSpeakerTime = 0;
  138         _dominantSpeakerStart = 0;
  139         nick = nick;
  140         context_user = context_user;
  141         displayName = nil;
  142         faceExpressions = {
  143             happy = 0,
  144             neutral = 0,
  145             surprised = 0,
  146             angry = 0,
  147             fearful = 0,
  148             disgusted = 0,
  149             sad = 0
  150         };
  151     }, SpeakerStats);
  152 end
  153 
  154 -- Changes the dominantSpeaker data for current occupant
  155 -- saves start time if it is new dominat speaker
  156 -- or calculates and accumulates time of speaking
  157 function SpeakerStats:setDominantSpeaker(isNowDominantSpeaker)
  158     -- log("debug", "set isDominant %s for %s", tostring(isNowDominantSpeaker), self.nick);
  159 
  160     if not self:isDominantSpeaker() and isNowDominantSpeaker then
  161         self._dominantSpeakerStart = socket.gettime()*1000;
  162     elseif self:isDominantSpeaker() and not isNowDominantSpeaker then
  163         local now = socket.gettime()*1000;
  164         local timeElapsed = math.floor(now - self._dominantSpeakerStart);
  165 
  166         self.totalDominantSpeakerTime
  167             = self.totalDominantSpeakerTime + timeElapsed;
  168         self._dominantSpeakerStart = 0;
  169     end
  170 end
  171 
  172 -- Returns true if the tracked user is currently a dominant speaker.
  173 function SpeakerStats:isDominantSpeaker()
  174     return self._dominantSpeakerStart > 0;
  175 end
  176 --- End SpeakerStats
  177 
  178 -- create speakerStats for the room
  179 function room_created(event)
  180     local room = event.room;
  181 
  182     if is_healthcheck_room(room.jid) then
  183         return ;
  184     end
  185     room.speakerStats = {};
  186     room.speakerStats.sessionId = room._data.meetingId;
  187 end
  188 
  189 -- create speakerStats for the breakout
  190 function breakout_room_created(event)
  191     local room = event.room;
  192     if is_healthcheck_room(room.jid) then
  193         return ;
  194     end
  195     local main_room = get_main_room(room);
  196     room.speakerStats = {};
  197     room.speakerStats.isBreakout = true
  198     room.speakerStats.breakoutRoomId = jid_split(room.jid)
  199     room.speakerStats.sessionId = main_room._data.meetingId;
  200 end
  201 
  202 -- Create SpeakerStats object for the joined user
  203 function occupant_joined(event)
  204     local occupant, room = event.occupant, event.room;
  205 
  206     if is_healthcheck_room(room.jid) or is_admin(occupant.bare_jid) then
  207         return;
  208     end
  209 
  210     local occupant = event.occupant;
  211 
  212     local nick = jid_resource(occupant.nick);
  213 
  214     if room.speakerStats then
  215         -- lets send the current speaker stats to that user, so he can update
  216         -- its local stats
  217         if next(room.speakerStats) ~= nil then
  218             local users_json = {};
  219             for jid, values in pairs(room.speakerStats) do
  220                 -- skip reporting those without a nick('dominantSpeakerId')
  221                 -- and skip focus if sneaked into the table
  222                 if values and type(values) == 'table' and values.nick ~= nil and values.nick ~= 'focus' then
  223                     local totalDominantSpeakerTime = values.totalDominantSpeakerTime;
  224                     local faceExpressions = values.faceExpressions;
  225                     if totalDominantSpeakerTime > 0 or room:get_occupant_jid(jid) == nil or values:isDominantSpeaker()
  226                         or get_participant_expressions_count(faceExpressions) > 0 then
  227                         -- before sending we need to calculate current dominant speaker state
  228                         if values:isDominantSpeaker() then
  229                             local timeElapsed = math.floor(socket.gettime()*1000 - values._dominantSpeakerStart);
  230                             totalDominantSpeakerTime = totalDominantSpeakerTime + timeElapsed;
  231                         end
  232 
  233                         users_json[values.nick] =  {
  234                             displayName = values.displayName,
  235                             totalDominantSpeakerTime = totalDominantSpeakerTime,
  236                             faceExpressions = faceExpressions
  237                         };
  238                     end
  239                 end
  240             end
  241 
  242             if next(users_json) ~= nil then
  243                 local body_json = {};
  244                 body_json.type = 'speakerstats';
  245                 body_json.users = users_json;
  246 
  247                 local stanza = st.message({
  248                     from = module.host;
  249                     to = occupant.jid; })
  250                 :tag("json-message", {xmlns='http://jitsi.org/jitmeet'})
  251                 :text(json.encode(body_json)):up();
  252 
  253                 room:route_stanza(stanza);
  254             end
  255         end
  256 
  257         local context_user = event.origin and event.origin.jitsi_meet_context_user or nil;
  258         room.speakerStats[occupant.jid] = new_SpeakerStats(nick, context_user);
  259     end
  260 end
  261 
  262 -- Occupant left set its dominant speaker to false and update the store the
  263 -- display name
  264 function occupant_leaving(event)
  265     local room = event.room;
  266 
  267     if is_healthcheck_room(room.jid) then
  268         return;
  269     end
  270 
  271     if not room.speakerStats then
  272         return;
  273     end
  274 
  275     local occupant = event.occupant;
  276 
  277     local speakerStatsForOccupant = room.speakerStats[occupant.jid];
  278     if speakerStatsForOccupant then
  279         speakerStatsForOccupant:setDominantSpeaker(false);
  280 
  281         -- set display name
  282         local displayName = occupant:get_presence():get_child_text(
  283             'nick', 'http://jabber.org/protocol/nick');
  284         speakerStatsForOccupant.displayName = displayName;
  285     end
  286 end
  287 
  288 -- Conference ended, send speaker stats
  289 function room_destroyed(event)
  290     local room = event.room;
  291 
  292     if is_healthcheck_room(room.jid) then
  293         return;
  294     end
  295 
  296     ext_events.speaker_stats(room, room.speakerStats);
  297 end
  298 
  299 module:hook("message/host", on_message);
  300 
  301 function process_main_muc_loaded(main_muc, host_module)
  302     -- the conference muc component
  303     module:log("info", "Hook to muc events on %s", host_module);
  304     main_muc_service = main_muc;
  305     module:log("info", "Main muc service %s", main_muc_service)
  306     host_module:hook("muc-room-created", room_created, -1);
  307     host_module:hook("muc-occupant-joined", occupant_joined, -1);
  308     host_module:hook("muc-occupant-pre-leave", occupant_leaving, -1);
  309     host_module:hook("muc-room-destroyed", room_destroyed, -1);
  310 end
  311 
  312 function process_breakout_muc_loaded(breakout_muc, host_module)
  313     -- the Breakout muc component
  314     module:log("info", "Hook to muc events on %s", host_module);
  315     host_module:hook("muc-room-created", breakout_room_created, -1);
  316     host_module:hook("muc-occupant-joined", occupant_joined, -1);
  317     host_module:hook("muc-occupant-pre-leave", occupant_leaving, -1);
  318     host_module:hook("muc-room-destroyed", room_destroyed, -1);
  319 end
  320 
  321 -- process a host module directly if loaded or hooks to wait for its load
  322 function process_host_module(name, callback)
  323     local function process_host(host)
  324         if host == name then
  325             callback(module:context(host), host);
  326         end
  327     end
  328 
  329     if prosody.hosts[name] == nil then
  330         module:log('debug', 'No host/component found, will wait for it: %s', name)
  331 
  332         -- when a host or component is added
  333         prosody.events.add_handler('host-activated', process_host);
  334     else
  335         process_host(name);
  336     end
  337 end
  338 
  339 -- process or waits to process the conference muc component
  340 process_host_module(muc_component_host, function(host_module, host)
  341     module:log('info', 'Conference component loaded %s', host);
  342 
  343     local muc_module = prosody.hosts[host].modules.muc;
  344     if muc_module then
  345         process_main_muc_loaded(muc_module, host_module);
  346     else
  347         module:log('debug', 'Will wait for muc to be available');
  348         prosody.hosts[host].events.add_handler('module-loaded', function(event)
  349             if (event.module == 'muc') then
  350                 process_main_muc_loaded(prosody.hosts[host].modules.muc, host_module);
  351             end
  352         end);
  353     end
  354 end);
  355 
  356 -- process or waits to process the breakout rooms muc component
  357 process_host_module(breakout_room_component_host, function(host_module, host)
  358     module:log('info', 'Breakout component loaded %s', host);
  359 
  360     local muc_module = prosody.hosts[host].modules.muc;
  361     if muc_module then
  362         process_breakout_muc_loaded(muc_module, host_module);
  363     else
  364         module:log('debug', 'Will wait for muc to be available');
  365         prosody.hosts[host].events.add_handler('module-loaded', function(event)
  366             if (event.module == 'muc') then
  367                 process_breakout_muc_loaded(prosody.hosts[host].modules.muc, host_module);
  368             end
  369         end);
  370     end
  371 end);
  372 
  373 function get_participant_expressions_count(faceExpressions)
  374     local count = 0;
  375     for _, value in pairs(faceExpressions) do
  376         count = count + value;
  377     end
  378 
  379     return count;
  380 end