2018-10-01 19:02:51 +00:00
|
|
|
local get_room_from_jid = module:require "util".get_room_from_jid;
|
2019-11-13 15:37:09 +00:00
|
|
|
local room_jid_match_rewrite = module:require "util".room_jid_match_rewrite;
|
2020-04-30 21:26:58 +00:00
|
|
|
local is_healthcheck_room = module:require "util".is_healthcheck_room;
|
2018-10-01 19:02:51 +00:00
|
|
|
local jid_resource = require "util.jid".resource;
|
|
|
|
local ext_events = module:require "ext_events"
|
2018-10-23 14:44:58 +00:00
|
|
|
local st = require "util.stanza";
|
|
|
|
local socket = require "socket";
|
|
|
|
local json = require "util.json";
|
2021-06-21 09:23:17 +00:00
|
|
|
local um_is_admin = require "core.usermanager".is_admin;
|
2022-06-09 18:15:04 +00:00
|
|
|
local jid_split = require 'util.jid'.split;
|
2018-10-01 19:02:51 +00:00
|
|
|
|
2019-12-05 10:09:58 +00:00
|
|
|
-- we use async to detect Prosody 0.10 and earlier
|
|
|
|
local have_async = pcall(require, "util.async");
|
|
|
|
if not have_async then
|
|
|
|
module:log("warn", "speaker stats will not work with Prosody version 0.10 or less.");
|
|
|
|
return;
|
|
|
|
end
|
|
|
|
|
2018-10-01 19:02:51 +00:00
|
|
|
local muc_component_host = module:get_option_string("muc_component");
|
2022-06-09 18:15:04 +00:00
|
|
|
local muc_domain_base = module:get_option_string("muc_mapper_domain_base");
|
|
|
|
|
|
|
|
if muc_component_host == nil or muc_domain_base == nil then
|
2022-08-30 15:32:36 +00:00
|
|
|
module:log("error", "No muc_component specified. No muc to operate on!");
|
2018-10-01 19:02:51 +00:00
|
|
|
return;
|
|
|
|
end
|
2022-06-09 18:15:04 +00:00
|
|
|
local breakout_room_component_host = "breakout." .. muc_domain_base;
|
2018-10-01 19:02:51 +00:00
|
|
|
|
2022-08-30 15:32:36 +00:00
|
|
|
module:log("info", "Starting speakerstats for %s", muc_component_host);
|
2018-10-01 19:02:51 +00:00
|
|
|
|
2022-06-09 18:15:04 +00:00
|
|
|
local main_muc_service;
|
|
|
|
|
2021-06-21 09:23:17 +00:00
|
|
|
local function is_admin(jid)
|
|
|
|
return um_is_admin(jid, module.host);
|
|
|
|
end
|
|
|
|
|
2022-06-09 18:15:04 +00:00
|
|
|
-- Searches all rooms in the main muc component that holds a breakout room
|
|
|
|
-- caches it if found so we don't search it again
|
|
|
|
local function get_main_room(breakout_room)
|
|
|
|
if breakout_room._data and breakout_room._data.main_room then
|
|
|
|
return breakout_room._data.main_room;
|
|
|
|
end
|
|
|
|
|
|
|
|
-- let's search all rooms to find the main room
|
|
|
|
for room in main_muc_service.each_room() do
|
|
|
|
if room._data and room._data.breakout_rooms_active and room._data.breakout_rooms[breakout_room.jid] then
|
|
|
|
breakout_room._data.main_room = room;
|
|
|
|
return room;
|
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2018-10-01 19:02:51 +00:00
|
|
|
-- receives messages from client currently connected to the room
|
|
|
|
-- clients indicates their own dominant speaker events
|
|
|
|
function on_message(event)
|
|
|
|
-- Check the type of the incoming stanza to avoid loops:
|
|
|
|
if event.stanza.attr.type == "error" then
|
|
|
|
return; -- We do not want to reply to these, so leave.
|
|
|
|
end
|
|
|
|
|
|
|
|
local speakerStats
|
|
|
|
= event.stanza:get_child('speakerstats', 'http://jitsi.org/jitmeet');
|
|
|
|
if speakerStats then
|
|
|
|
local roomAddress = speakerStats.attr.room;
|
2022-09-08 21:14:00 +00:00
|
|
|
local silence = speakerStats.attr.silence == 'true';
|
2019-11-13 15:37:09 +00:00
|
|
|
local room = get_room_from_jid(room_jid_match_rewrite(roomAddress));
|
2018-10-01 19:02:51 +00:00
|
|
|
|
|
|
|
if not room then
|
|
|
|
log("warn", "No room found %s", roomAddress);
|
|
|
|
return false;
|
|
|
|
end
|
2022-06-09 18:15:04 +00:00
|
|
|
|
2020-10-05 14:32:48 +00:00
|
|
|
if not room.speakerStats then
|
|
|
|
log("warn", "No speakerStats found for %s", roomAddress);
|
|
|
|
return false;
|
|
|
|
end
|
2018-10-01 19:02:51 +00:00
|
|
|
|
|
|
|
local roomSpeakerStats = room.speakerStats;
|
|
|
|
local from = event.stanza.attr.from;
|
|
|
|
|
|
|
|
local occupant = room:get_occupant_by_real_jid(from);
|
|
|
|
if not occupant then
|
|
|
|
log("warn", "No occupant %s found for %s", from, roomAddress);
|
|
|
|
return false;
|
|
|
|
end
|
|
|
|
|
|
|
|
local newDominantSpeaker = roomSpeakerStats[occupant.jid];
|
|
|
|
local oldDominantSpeakerId = roomSpeakerStats['dominantSpeakerId'];
|
|
|
|
|
2022-09-08 21:14:00 +00:00
|
|
|
if oldDominantSpeakerId and occupant.jid ~= oldDominantSpeakerId then
|
2020-03-06 11:33:41 +00:00
|
|
|
local oldDominantSpeaker = roomSpeakerStats[oldDominantSpeakerId];
|
|
|
|
if oldDominantSpeaker then
|
2022-09-08 21:14:00 +00:00
|
|
|
oldDominantSpeaker:setDominantSpeaker(false, false);
|
2020-03-06 11:33:41 +00:00
|
|
|
end
|
2018-10-01 19:02:51 +00:00
|
|
|
end
|
|
|
|
|
|
|
|
if newDominantSpeaker then
|
2022-09-08 21:14:00 +00:00
|
|
|
newDominantSpeaker:setDominantSpeaker(true, silence);
|
2018-10-01 19:02:51 +00:00
|
|
|
end
|
|
|
|
|
|
|
|
room.speakerStats['dominantSpeakerId'] = occupant.jid;
|
|
|
|
end
|
|
|
|
|
2022-11-22 13:56:37 +00:00
|
|
|
local newFaceLandmarks = event.stanza:get_child('faceLandmarks', 'http://jitsi.org/jitmeet');
|
2021-11-17 14:33:03 +00:00
|
|
|
|
2022-11-22 13:56:37 +00:00
|
|
|
if newFaceLandmarks then
|
|
|
|
local roomAddress = newFaceLandmarks.attr.room;
|
2021-11-17 14:33:03 +00:00
|
|
|
local room = get_room_from_jid(room_jid_match_rewrite(roomAddress));
|
|
|
|
|
|
|
|
if not room then
|
|
|
|
log("warn", "No room found %s", roomAddress);
|
|
|
|
return false;
|
|
|
|
end
|
|
|
|
if not room.speakerStats then
|
|
|
|
log("warn", "No speakerStats found for %s", roomAddress);
|
|
|
|
return false;
|
|
|
|
end
|
|
|
|
local from = event.stanza.attr.from;
|
|
|
|
|
|
|
|
local occupant = room:get_occupant_by_real_jid(from);
|
|
|
|
if not occupant then
|
|
|
|
log("warn", "No occupant %s found for %s", from, roomAddress);
|
|
|
|
return false;
|
|
|
|
end
|
2022-11-22 13:56:37 +00:00
|
|
|
local faceLandmarks = room.speakerStats[occupant.jid].faceLandmarks;
|
|
|
|
table.insert(faceLandmarks,
|
|
|
|
{
|
|
|
|
faceExpression = newFaceLandmarks.attr.faceExpression,
|
|
|
|
timestamp = tonumber(newFaceLandmarks.attr.timestamp),
|
|
|
|
duration = tonumber(newFaceLandmarks.attr.duration),
|
|
|
|
})
|
2021-11-17 14:33:03 +00:00
|
|
|
end
|
|
|
|
|
2018-10-01 19:02:51 +00:00
|
|
|
return true
|
|
|
|
end
|
|
|
|
|
|
|
|
--- Start SpeakerStats implementation
|
|
|
|
local SpeakerStats = {};
|
|
|
|
SpeakerStats.__index = SpeakerStats;
|
|
|
|
|
2019-10-23 07:58:14 +00:00
|
|
|
function new_SpeakerStats(nick, context_user)
|
2018-10-01 19:02:51 +00:00
|
|
|
return setmetatable({
|
|
|
|
totalDominantSpeakerTime = 0;
|
2018-12-28 09:53:22 +00:00
|
|
|
_dominantSpeakerStart = 0;
|
2022-09-08 21:14:00 +00:00
|
|
|
_isSilent = false;
|
|
|
|
_isDominantSpeaker = false;
|
2018-10-01 19:02:51 +00:00
|
|
|
nick = nick;
|
2019-10-23 07:58:14 +00:00
|
|
|
context_user = context_user;
|
2018-10-01 19:02:51 +00:00
|
|
|
displayName = nil;
|
2022-11-22 13:56:37 +00:00
|
|
|
faceLandmarks = {};
|
2018-10-01 19:02:51 +00:00
|
|
|
}, SpeakerStats);
|
|
|
|
end
|
|
|
|
|
|
|
|
-- Changes the dominantSpeaker data for current occupant
|
|
|
|
-- saves start time if it is new dominat speaker
|
|
|
|
-- or calculates and accumulates time of speaking
|
2022-09-08 21:14:00 +00:00
|
|
|
function SpeakerStats:setDominantSpeaker(isNowDominantSpeaker, silence)
|
2020-11-04 14:25:03 +00:00
|
|
|
-- log("debug", "set isDominant %s for %s", tostring(isNowDominantSpeaker), self.nick);
|
2018-10-01 19:02:51 +00:00
|
|
|
|
2022-09-08 21:14:00 +00:00
|
|
|
local now = socket.gettime()*1000;
|
2018-10-01 19:02:51 +00:00
|
|
|
|
2022-09-08 21:14:00 +00:00
|
|
|
if not self:isDominantSpeaker() and isNowDominantSpeaker and not silence then
|
|
|
|
self._dominantSpeakerStart = now;
|
|
|
|
elseif self:isDominantSpeaker() then
|
|
|
|
if not isNowDominantSpeaker then
|
|
|
|
if not self._isSilent then
|
|
|
|
local timeElapsed = math.floor(now - self._dominantSpeakerStart);
|
|
|
|
|
|
|
|
self.totalDominantSpeakerTime = self.totalDominantSpeakerTime + timeElapsed;
|
|
|
|
self._dominantSpeakerStart = 0;
|
|
|
|
end
|
|
|
|
elseif self._isSilent and not silence then
|
|
|
|
self._dominantSpeakerStart = now;
|
|
|
|
elseif not self._isSilent and silence then
|
|
|
|
local timeElapsed = math.floor(now - self._dominantSpeakerStart);
|
|
|
|
|
|
|
|
self.totalDominantSpeakerTime = self.totalDominantSpeakerTime + timeElapsed;
|
|
|
|
self._dominantSpeakerStart = 0;
|
|
|
|
end
|
2018-10-01 19:02:51 +00:00
|
|
|
end
|
2022-09-08 21:14:00 +00:00
|
|
|
|
|
|
|
self._isDominantSpeaker = isNowDominantSpeaker;
|
|
|
|
self._isSilent = silence;
|
2018-12-28 09:53:22 +00:00
|
|
|
end
|
2018-10-01 19:02:51 +00:00
|
|
|
|
2018-12-28 09:53:22 +00:00
|
|
|
-- Returns true if the tracked user is currently a dominant speaker.
|
|
|
|
function SpeakerStats:isDominantSpeaker()
|
2022-09-08 21:14:00 +00:00
|
|
|
return self._isDominantSpeaker;
|
|
|
|
end
|
|
|
|
|
|
|
|
-- Returns true if the tracked user is currently silent.
|
|
|
|
function SpeakerStats:isSilent()
|
|
|
|
return self._isSilent;
|
2018-10-01 19:02:51 +00:00
|
|
|
end
|
|
|
|
--- End SpeakerStats
|
|
|
|
|
|
|
|
-- create speakerStats for the room
|
|
|
|
function room_created(event)
|
|
|
|
local room = event.room;
|
2020-04-30 21:26:58 +00:00
|
|
|
|
2020-05-12 21:40:39 +00:00
|
|
|
if is_healthcheck_room(room.jid) then
|
2022-06-09 18:15:04 +00:00
|
|
|
return ;
|
2020-04-30 21:26:58 +00:00
|
|
|
end
|
2022-06-09 18:15:04 +00:00
|
|
|
room.speakerStats = {};
|
|
|
|
room.speakerStats.sessionId = room._data.meetingId;
|
|
|
|
end
|
2020-04-30 21:26:58 +00:00
|
|
|
|
2022-06-09 18:15:04 +00:00
|
|
|
-- create speakerStats for the breakout
|
|
|
|
function breakout_room_created(event)
|
|
|
|
local room = event.room;
|
|
|
|
if is_healthcheck_room(room.jid) then
|
|
|
|
return ;
|
|
|
|
end
|
|
|
|
local main_room = get_main_room(room);
|
2018-10-01 19:02:51 +00:00
|
|
|
room.speakerStats = {};
|
2022-06-09 18:15:04 +00:00
|
|
|
room.speakerStats.isBreakout = true
|
|
|
|
room.speakerStats.breakoutRoomId = jid_split(room.jid)
|
|
|
|
room.speakerStats.sessionId = main_room._data.meetingId;
|
2018-10-01 19:02:51 +00:00
|
|
|
end
|
|
|
|
|
|
|
|
-- Create SpeakerStats object for the joined user
|
|
|
|
function occupant_joined(event)
|
2021-06-21 09:23:17 +00:00
|
|
|
local occupant, room = event.occupant, event.room;
|
2020-04-30 21:26:58 +00:00
|
|
|
|
2021-06-21 09:23:17 +00:00
|
|
|
if is_healthcheck_room(room.jid) or is_admin(occupant.bare_jid) then
|
2020-04-30 21:26:58 +00:00
|
|
|
return;
|
|
|
|
end
|
|
|
|
|
2018-10-01 19:02:51 +00:00
|
|
|
local occupant = event.occupant;
|
2019-10-23 07:58:14 +00:00
|
|
|
|
2018-10-01 19:02:51 +00:00
|
|
|
local nick = jid_resource(occupant.nick);
|
|
|
|
|
|
|
|
if room.speakerStats then
|
2018-10-23 14:44:58 +00:00
|
|
|
-- lets send the current speaker stats to that user, so he can update
|
|
|
|
-- its local stats
|
|
|
|
if next(room.speakerStats) ~= nil then
|
|
|
|
local users_json = {};
|
|
|
|
for jid, values in pairs(room.speakerStats) do
|
|
|
|
-- skip reporting those without a nick('dominantSpeakerId')
|
|
|
|
-- and skip focus if sneaked into the table
|
2022-06-09 18:15:04 +00:00
|
|
|
if values and type(values) == 'table' and values.nick ~= nil and values.nick ~= 'focus' then
|
2021-06-21 09:23:17 +00:00
|
|
|
local totalDominantSpeakerTime = values.totalDominantSpeakerTime;
|
2022-11-22 13:56:37 +00:00
|
|
|
local faceLandmarks = values.faceLandmarks;
|
2021-11-17 14:33:03 +00:00
|
|
|
if totalDominantSpeakerTime > 0 or room:get_occupant_jid(jid) == nil or values:isDominantSpeaker()
|
2022-11-22 13:56:37 +00:00
|
|
|
or next(faceLandmarks) ~= nil then
|
2021-06-21 09:23:17 +00:00
|
|
|
-- before sending we need to calculate current dominant speaker state
|
2022-09-08 21:14:00 +00:00
|
|
|
if values:isDominantSpeaker() and not values:isSilent() then
|
2021-06-21 09:23:17 +00:00
|
|
|
local timeElapsed = math.floor(socket.gettime()*1000 - values._dominantSpeakerStart);
|
|
|
|
totalDominantSpeakerTime = totalDominantSpeakerTime + timeElapsed;
|
|
|
|
end
|
|
|
|
|
|
|
|
users_json[values.nick] = {
|
|
|
|
displayName = values.displayName,
|
2021-11-17 14:33:03 +00:00
|
|
|
totalDominantSpeakerTime = totalDominantSpeakerTime,
|
2022-11-22 13:56:37 +00:00
|
|
|
faceLandmarks = faceLandmarks
|
2021-06-21 09:23:17 +00:00
|
|
|
};
|
2018-10-23 14:44:58 +00:00
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2021-07-15 13:31:29 +00:00
|
|
|
if next(users_json) ~= nil then
|
|
|
|
local body_json = {};
|
|
|
|
body_json.type = 'speakerstats';
|
|
|
|
body_json.users = users_json;
|
|
|
|
|
|
|
|
local stanza = st.message({
|
|
|
|
from = module.host;
|
|
|
|
to = occupant.jid; })
|
|
|
|
:tag("json-message", {xmlns='http://jitsi.org/jitmeet'})
|
|
|
|
:text(json.encode(body_json)):up();
|
|
|
|
|
|
|
|
room:route_stanza(stanza);
|
2021-06-21 09:23:17 +00:00
|
|
|
end
|
2018-10-23 14:44:58 +00:00
|
|
|
end
|
|
|
|
|
2019-10-23 07:58:14 +00:00
|
|
|
local context_user = event.origin and event.origin.jitsi_meet_context_user or nil;
|
|
|
|
room.speakerStats[occupant.jid] = new_SpeakerStats(nick, context_user);
|
2018-10-01 19:02:51 +00:00
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
-- Occupant left set its dominant speaker to false and update the store the
|
|
|
|
-- display name
|
|
|
|
function occupant_leaving(event)
|
|
|
|
local room = event.room;
|
2020-04-30 21:26:58 +00:00
|
|
|
|
2020-05-12 21:40:39 +00:00
|
|
|
if is_healthcheck_room(room.jid) then
|
2020-04-30 21:26:58 +00:00
|
|
|
return;
|
|
|
|
end
|
2021-11-17 14:33:03 +00:00
|
|
|
|
2020-10-05 14:32:48 +00:00
|
|
|
if not room.speakerStats then
|
|
|
|
return;
|
|
|
|
end
|
2020-04-30 21:26:58 +00:00
|
|
|
|
2018-10-01 19:02:51 +00:00
|
|
|
local occupant = event.occupant;
|
|
|
|
|
|
|
|
local speakerStatsForOccupant = room.speakerStats[occupant.jid];
|
|
|
|
if speakerStatsForOccupant then
|
2022-09-08 21:14:00 +00:00
|
|
|
speakerStatsForOccupant:setDominantSpeaker(false, false);
|
2018-10-01 19:02:51 +00:00
|
|
|
|
|
|
|
-- set display name
|
|
|
|
local displayName = occupant:get_presence():get_child_text(
|
|
|
|
'nick', 'http://jabber.org/protocol/nick');
|
|
|
|
speakerStatsForOccupant.displayName = displayName;
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
-- Conference ended, send speaker stats
|
|
|
|
function room_destroyed(event)
|
|
|
|
local room = event.room;
|
|
|
|
|
2020-05-12 21:40:39 +00:00
|
|
|
if is_healthcheck_room(room.jid) then
|
2020-04-30 21:26:58 +00:00
|
|
|
return;
|
|
|
|
end
|
|
|
|
|
2018-10-01 19:02:51 +00:00
|
|
|
ext_events.speaker_stats(room, room.speakerStats);
|
|
|
|
end
|
|
|
|
|
|
|
|
module:hook("message/host", on_message);
|
2019-07-26 09:21:09 +00:00
|
|
|
|
2022-06-09 18:15:04 +00:00
|
|
|
function process_main_muc_loaded(main_muc, host_module)
|
|
|
|
-- the conference muc component
|
|
|
|
module:log("info", "Hook to muc events on %s", host_module);
|
|
|
|
main_muc_service = main_muc;
|
|
|
|
module:log("info", "Main muc service %s", main_muc_service)
|
|
|
|
host_module:hook("muc-room-created", room_created, -1);
|
|
|
|
host_module:hook("muc-occupant-joined", occupant_joined, -1);
|
|
|
|
host_module:hook("muc-occupant-pre-leave", occupant_leaving, -1);
|
|
|
|
host_module:hook("muc-room-destroyed", room_destroyed, -1);
|
|
|
|
end
|
2019-07-26 09:21:09 +00:00
|
|
|
|
2022-06-09 18:15:04 +00:00
|
|
|
function process_breakout_muc_loaded(breakout_muc, host_module)
|
|
|
|
-- the Breakout muc component
|
|
|
|
module:log("info", "Hook to muc events on %s", host_module);
|
|
|
|
host_module:hook("muc-room-created", breakout_room_created, -1);
|
|
|
|
host_module:hook("muc-occupant-joined", occupant_joined, -1);
|
|
|
|
host_module:hook("muc-occupant-pre-leave", occupant_leaving, -1);
|
|
|
|
host_module:hook("muc-room-destroyed", room_destroyed, -1);
|
2019-07-26 09:21:09 +00:00
|
|
|
end
|
|
|
|
|
2022-06-09 18:15:04 +00:00
|
|
|
-- process a host module directly if loaded or hooks to wait for its load
|
|
|
|
function process_host_module(name, callback)
|
|
|
|
local function process_host(host)
|
|
|
|
if host == name then
|
|
|
|
callback(module:context(host), host);
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
if prosody.hosts[name] == nil then
|
|
|
|
module:log('debug', 'No host/component found, will wait for it: %s', name)
|
2019-07-26 09:21:09 +00:00
|
|
|
|
2022-06-09 18:15:04 +00:00
|
|
|
-- when a host or component is added
|
|
|
|
prosody.events.add_handler('host-activated', process_host);
|
|
|
|
else
|
|
|
|
process_host(name);
|
|
|
|
end
|
2019-12-05 10:09:58 +00:00
|
|
|
end
|
2021-11-17 14:33:03 +00:00
|
|
|
|
2022-06-09 18:15:04 +00:00
|
|
|
-- process or waits to process the conference muc component
|
|
|
|
process_host_module(muc_component_host, function(host_module, host)
|
|
|
|
module:log('info', 'Conference component loaded %s', host);
|
|
|
|
|
|
|
|
local muc_module = prosody.hosts[host].modules.muc;
|
|
|
|
if muc_module then
|
|
|
|
process_main_muc_loaded(muc_module, host_module);
|
|
|
|
else
|
|
|
|
module:log('debug', 'Will wait for muc to be available');
|
|
|
|
prosody.hosts[host].events.add_handler('module-loaded', function(event)
|
|
|
|
if (event.module == 'muc') then
|
|
|
|
process_main_muc_loaded(prosody.hosts[host].modules.muc, host_module);
|
|
|
|
end
|
|
|
|
end);
|
|
|
|
end
|
|
|
|
end);
|
|
|
|
|
|
|
|
-- process or waits to process the breakout rooms muc component
|
|
|
|
process_host_module(breakout_room_component_host, function(host_module, host)
|
|
|
|
module:log('info', 'Breakout component loaded %s', host);
|
|
|
|
|
|
|
|
local muc_module = prosody.hosts[host].modules.muc;
|
|
|
|
if muc_module then
|
|
|
|
process_breakout_muc_loaded(muc_module, host_module);
|
|
|
|
else
|
|
|
|
module:log('debug', 'Will wait for muc to be available');
|
|
|
|
prosody.hosts[host].events.add_handler('module-loaded', function(event)
|
|
|
|
if (event.module == 'muc') then
|
|
|
|
process_breakout_muc_loaded(prosody.hosts[host].modules.muc, host_module);
|
|
|
|
end
|
|
|
|
end);
|
|
|
|
end
|
|
|
|
end);
|