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