"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