"Fossies" - the Fresh Open Source Software Archive

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