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 14KB

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