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

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248
  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. -- we use async to detect Prosody 0.10 and earlier
  11. local have_async = pcall(require, "util.async");
  12. if not have_async then
  13. module:log("warn", "speaker stats will not work with Prosody version 0.10 or less.");
  14. return;
  15. end
  16. local muc_component_host = module:get_option_string("muc_component");
  17. if muc_component_host == nil then
  18. log("error", "No muc_component specified. No muc to operate on!");
  19. return;
  20. end
  21. log("info", "Starting speakerstats for %s", muc_component_host);
  22. local function is_admin(jid)
  23. return um_is_admin(jid, module.host);
  24. end
  25. -- receives messages from client currently connected to the room
  26. -- clients indicates their own dominant speaker events
  27. function on_message(event)
  28. -- Check the type of the incoming stanza to avoid loops:
  29. if event.stanza.attr.type == "error" then
  30. return; -- We do not want to reply to these, so leave.
  31. end
  32. local speakerStats
  33. = event.stanza:get_child('speakerstats', 'http://jitsi.org/jitmeet');
  34. if speakerStats then
  35. local roomAddress = speakerStats.attr.room;
  36. local room = get_room_from_jid(room_jid_match_rewrite(roomAddress));
  37. if not room then
  38. log("warn", "No room found %s", roomAddress);
  39. return false;
  40. end
  41. if not room.speakerStats then
  42. log("warn", "No speakerStats found for %s", roomAddress);
  43. return false;
  44. end
  45. local roomSpeakerStats = room.speakerStats;
  46. local from = event.stanza.attr.from;
  47. local occupant = room:get_occupant_by_real_jid(from);
  48. if not occupant then
  49. log("warn", "No occupant %s found for %s", from, roomAddress);
  50. return false;
  51. end
  52. local newDominantSpeaker = roomSpeakerStats[occupant.jid];
  53. local oldDominantSpeakerId = roomSpeakerStats['dominantSpeakerId'];
  54. if oldDominantSpeakerId then
  55. local oldDominantSpeaker = roomSpeakerStats[oldDominantSpeakerId];
  56. if oldDominantSpeaker then
  57. oldDominantSpeaker:setDominantSpeaker(false);
  58. end
  59. end
  60. if newDominantSpeaker then
  61. newDominantSpeaker:setDominantSpeaker(true);
  62. end
  63. room.speakerStats['dominantSpeakerId'] = occupant.jid;
  64. end
  65. return true
  66. end
  67. --- Start SpeakerStats implementation
  68. local SpeakerStats = {};
  69. SpeakerStats.__index = SpeakerStats;
  70. function new_SpeakerStats(nick, context_user)
  71. return setmetatable({
  72. totalDominantSpeakerTime = 0;
  73. _dominantSpeakerStart = 0;
  74. nick = nick;
  75. context_user = context_user;
  76. displayName = nil;
  77. }, SpeakerStats);
  78. end
  79. -- Changes the dominantSpeaker data for current occupant
  80. -- saves start time if it is new dominat speaker
  81. -- or calculates and accumulates time of speaking
  82. function SpeakerStats:setDominantSpeaker(isNowDominantSpeaker)
  83. -- log("debug", "set isDominant %s for %s", tostring(isNowDominantSpeaker), self.nick);
  84. if not self:isDominantSpeaker() and isNowDominantSpeaker then
  85. self._dominantSpeakerStart = socket.gettime()*1000;
  86. elseif self:isDominantSpeaker() and not isNowDominantSpeaker then
  87. local now = socket.gettime()*1000;
  88. local timeElapsed = math.floor(now - self._dominantSpeakerStart);
  89. self.totalDominantSpeakerTime
  90. = self.totalDominantSpeakerTime + timeElapsed;
  91. self._dominantSpeakerStart = 0;
  92. end
  93. end
  94. -- Returns true if the tracked user is currently a dominant speaker.
  95. function SpeakerStats:isDominantSpeaker()
  96. return self._dominantSpeakerStart > 0;
  97. end
  98. --- End SpeakerStats
  99. -- create speakerStats for the room
  100. function room_created(event)
  101. local room = event.room;
  102. if is_healthcheck_room(room.jid) then
  103. return;
  104. end
  105. room.speakerStats = {};
  106. end
  107. -- Create SpeakerStats object for the joined user
  108. function occupant_joined(event)
  109. local occupant, room = event.occupant, event.room;
  110. if is_healthcheck_room(room.jid) or is_admin(occupant.bare_jid) then
  111. return;
  112. end
  113. local occupant = event.occupant;
  114. local nick = jid_resource(occupant.nick);
  115. if room.speakerStats then
  116. -- lets send the current speaker stats to that user, so he can update
  117. -- its local stats
  118. if next(room.speakerStats) ~= nil then
  119. local users_json = {};
  120. for jid, values in pairs(room.speakerStats) do
  121. -- skip reporting those without a nick('dominantSpeakerId')
  122. -- and skip focus if sneaked into the table
  123. if values.nick ~= nil and values.nick ~= 'focus' then
  124. local totalDominantSpeakerTime = values.totalDominantSpeakerTime;
  125. if totalDominantSpeakerTime > 0 or room:get_occupant_jid(jid) == nil then
  126. -- before sending we need to calculate current dominant speaker state
  127. if values:isDominantSpeaker() then
  128. local timeElapsed = math.floor(socket.gettime()*1000 - values._dominantSpeakerStart);
  129. totalDominantSpeakerTime = totalDominantSpeakerTime + timeElapsed;
  130. end
  131. users_json[values.nick] = {
  132. displayName = values.displayName,
  133. totalDominantSpeakerTime = totalDominantSpeakerTime
  134. };
  135. end
  136. end
  137. end
  138. if next(users_json) ~= nil then
  139. local body_json = {};
  140. body_json.type = 'speakerstats';
  141. body_json.users = users_json;
  142. local stanza = st.message({
  143. from = module.host;
  144. to = occupant.jid; })
  145. :tag("json-message", {xmlns='http://jitsi.org/jitmeet'})
  146. :text(json.encode(body_json)):up();
  147. room:route_stanza(stanza);
  148. end
  149. end
  150. local context_user = event.origin and event.origin.jitsi_meet_context_user or nil;
  151. room.speakerStats[occupant.jid] = new_SpeakerStats(nick, context_user);
  152. end
  153. end
  154. -- Occupant left set its dominant speaker to false and update the store the
  155. -- display name
  156. function occupant_leaving(event)
  157. local room = event.room;
  158. if is_healthcheck_room(room.jid) then
  159. return;
  160. end
  161. if not room.speakerStats then
  162. return;
  163. end
  164. local occupant = event.occupant;
  165. local speakerStatsForOccupant = room.speakerStats[occupant.jid];
  166. if speakerStatsForOccupant then
  167. speakerStatsForOccupant:setDominantSpeaker(false);
  168. -- set display name
  169. local displayName = occupant:get_presence():get_child_text(
  170. 'nick', 'http://jabber.org/protocol/nick');
  171. speakerStatsForOccupant.displayName = displayName;
  172. end
  173. end
  174. -- Conference ended, send speaker stats
  175. function room_destroyed(event)
  176. local room = event.room;
  177. if is_healthcheck_room(room.jid) then
  178. return;
  179. end
  180. ext_events.speaker_stats(room, room.speakerStats);
  181. end
  182. module:hook("message/host", on_message);
  183. -- executed on every host added internally in prosody, including components
  184. function process_host(host)
  185. if host == muc_component_host then -- the conference muc component
  186. module:log("info","Hook to muc events on %s", host);
  187. local muc_module = module:context(host);
  188. muc_module:hook("muc-room-created", room_created, -1);
  189. muc_module:hook("muc-occupant-joined", occupant_joined, -1);
  190. muc_module:hook("muc-occupant-pre-leave", occupant_leaving, -1);
  191. muc_module:hook("muc-room-destroyed", room_destroyed, -1);
  192. end
  193. end
  194. if prosody.hosts[muc_component_host] == nil then
  195. module:log("info","No muc component found, will listen for it: %s", muc_component_host)
  196. -- when a host or component is added
  197. prosody.events.add_handler("host-activated", process_host);
  198. else
  199. process_host(muc_component_host);
  200. end