You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

mod_speakerstats_component.lua 13KB

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