mirror of
https://gitcode.com/GitHub_Trending/ji/jitsi-meet.git
synced 2025-12-30 03:12:29 +00:00
We were using prosody,util.json and cjson at the same time, but the latter is more performant. Adds some error handling which were missing with the prosody util json one.
380 lines
14 KiB
Lua
380 lines
14 KiB
Lua
local util = module:require "util";
|
|
local get_room_from_jid = util.get_room_from_jid;
|
|
local room_jid_match_rewrite = util.room_jid_match_rewrite;
|
|
local is_healthcheck_room = util.is_healthcheck_room;
|
|
local process_host_module = util.process_host_module;
|
|
local jid_resource = require "util.jid".resource;
|
|
local st = require "util.stanza";
|
|
local socket = require "socket";
|
|
local json = require 'cjson.safe';
|
|
local um_is_admin = require "core.usermanager".is_admin;
|
|
local jid_split = require 'util.jid'.split;
|
|
|
|
-- 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
|
|
|
|
local muc_component_host = module:get_option_string("muc_component");
|
|
local muc_domain_base = module:get_option_string("muc_mapper_domain_base");
|
|
|
|
if muc_component_host == nil or muc_domain_base == nil then
|
|
module:log("error", "No muc_component specified. No muc to operate on!");
|
|
return;
|
|
end
|
|
local breakout_room_component_host = "breakout." .. muc_domain_base;
|
|
|
|
module:log("info", "Starting speakerstats for %s", muc_component_host);
|
|
|
|
local main_muc_service;
|
|
|
|
local function is_admin(jid)
|
|
return um_is_admin(jid, module.host);
|
|
end
|
|
|
|
-- Searches all rooms in the main muc component that holds a breakout room
|
|
-- caches it if found so we don't search it again
|
|
-- we should not cache objects in _data as this is being serialized when calling room:save()
|
|
local function get_main_room(breakout_room)
|
|
if breakout_room.main_room then
|
|
return breakout_room.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.main_room = room;
|
|
return room;
|
|
end
|
|
end
|
|
end
|
|
|
|
-- 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;
|
|
local silence = speakerStats.attr.silence == 'true';
|
|
local room = get_room_from_jid(room_jid_match_rewrite(roomAddress));
|
|
|
|
if not room then
|
|
module:log("warn", "No room found %s", roomAddress);
|
|
return false;
|
|
end
|
|
|
|
if not room.speakerStats then
|
|
module:log("warn", "No speakerStats found for %s", roomAddress);
|
|
return false;
|
|
end
|
|
|
|
local roomSpeakerStats = room.speakerStats;
|
|
local from = event.stanza.attr.from;
|
|
|
|
local occupant = room:get_occupant_by_real_jid(from);
|
|
if not occupant then
|
|
module:log("warn", "No occupant %s found for %s", from, roomAddress);
|
|
return false;
|
|
end
|
|
|
|
local newDominantSpeaker = roomSpeakerStats[occupant.jid];
|
|
local oldDominantSpeakerId = roomSpeakerStats['dominantSpeakerId'];
|
|
|
|
if oldDominantSpeakerId and occupant.jid ~= oldDominantSpeakerId then
|
|
local oldDominantSpeaker = roomSpeakerStats[oldDominantSpeakerId];
|
|
if oldDominantSpeaker then
|
|
oldDominantSpeaker:setDominantSpeaker(false, false);
|
|
end
|
|
end
|
|
|
|
if newDominantSpeaker then
|
|
newDominantSpeaker:setDominantSpeaker(true, silence);
|
|
end
|
|
|
|
room.speakerStats['dominantSpeakerId'] = occupant.jid;
|
|
end
|
|
|
|
local newFaceLandmarks = event.stanza:get_child('faceLandmarks', 'http://jitsi.org/jitmeet');
|
|
|
|
if newFaceLandmarks then
|
|
local roomAddress = newFaceLandmarks.attr.room;
|
|
local room = get_room_from_jid(room_jid_match_rewrite(roomAddress));
|
|
|
|
if not room then
|
|
module:log("warn", "No room found %s", roomAddress);
|
|
return false;
|
|
end
|
|
if not room.speakerStats then
|
|
module: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 or not room.speakerStats[occupant.jid] then
|
|
module:log("warn", "No occupant %s found for %s", from, roomAddress);
|
|
return false;
|
|
end
|
|
local faceLandmarks = room.speakerStats[occupant.jid].faceLandmarks;
|
|
table.insert(faceLandmarks,
|
|
{
|
|
faceExpression = newFaceLandmarks.attr.faceExpression,
|
|
timestamp = tonumber(newFaceLandmarks.attr.timestamp),
|
|
duration = tonumber(newFaceLandmarks.attr.duration),
|
|
})
|
|
end
|
|
|
|
return true
|
|
end
|
|
|
|
--- Start SpeakerStats implementation
|
|
local SpeakerStats = {};
|
|
SpeakerStats.__index = SpeakerStats;
|
|
|
|
function new_SpeakerStats(nick, context_user)
|
|
return setmetatable({
|
|
totalDominantSpeakerTime = 0;
|
|
_dominantSpeakerStart = 0;
|
|
_isSilent = false;
|
|
_isDominantSpeaker = false;
|
|
nick = nick;
|
|
context_user = context_user;
|
|
displayName = nil;
|
|
faceLandmarks = {};
|
|
}, 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
|
|
function SpeakerStats:setDominantSpeaker(isNowDominantSpeaker, silence)
|
|
-- module:log("debug", "set isDominant %s for %s", tostring(isNowDominantSpeaker), self.nick);
|
|
|
|
local now = socket.gettime()*1000;
|
|
|
|
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
|
|
end
|
|
|
|
self._isDominantSpeaker = isNowDominantSpeaker;
|
|
self._isSilent = silence;
|
|
end
|
|
|
|
-- Returns true if the tracked user is currently a dominant speaker.
|
|
function SpeakerStats:isDominantSpeaker()
|
|
return self._isDominantSpeaker;
|
|
end
|
|
|
|
-- Returns true if the tracked user is currently silent.
|
|
function SpeakerStats:isSilent()
|
|
return self._isSilent;
|
|
end
|
|
--- End SpeakerStats
|
|
|
|
-- create speakerStats for the room
|
|
function room_created(event)
|
|
local room = event.room;
|
|
|
|
if is_healthcheck_room(room.jid) then
|
|
return ;
|
|
end
|
|
room.speakerStats = {};
|
|
room.speakerStats.sessionId = room._data.meetingId;
|
|
end
|
|
|
|
-- 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);
|
|
room.speakerStats = {};
|
|
room.speakerStats.isBreakout = true
|
|
room.speakerStats.breakoutRoomId = jid_split(room.jid)
|
|
room.speakerStats.sessionId = main_room._data.meetingId;
|
|
end
|
|
|
|
-- Create SpeakerStats object for the joined user
|
|
function occupant_joined(event)
|
|
local occupant, room = event.occupant, event.room;
|
|
|
|
if is_healthcheck_room(room.jid) or is_admin(occupant.bare_jid) then
|
|
return;
|
|
end
|
|
|
|
local occupant = event.occupant;
|
|
|
|
local nick = jid_resource(occupant.nick);
|
|
|
|
if room.speakerStats then
|
|
-- 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
|
|
if values and type(values) == 'table' and values.nick ~= nil and values.nick ~= 'focus' then
|
|
local totalDominantSpeakerTime = values.totalDominantSpeakerTime;
|
|
local faceLandmarks = values.faceLandmarks;
|
|
if totalDominantSpeakerTime > 0 or room:get_occupant_jid(jid) == nil or values:isDominantSpeaker()
|
|
or next(faceLandmarks) ~= nil then
|
|
-- before sending we need to calculate current dominant speaker state
|
|
if values:isDominantSpeaker() and not values:isSilent() then
|
|
local timeElapsed = math.floor(socket.gettime()*1000 - values._dominantSpeakerStart);
|
|
totalDominantSpeakerTime = totalDominantSpeakerTime + timeElapsed;
|
|
end
|
|
|
|
users_json[values.nick] = {
|
|
displayName = values.displayName,
|
|
totalDominantSpeakerTime = totalDominantSpeakerTime,
|
|
faceLandmarks = faceLandmarks
|
|
};
|
|
end
|
|
end
|
|
end
|
|
|
|
if next(users_json) ~= nil then
|
|
local body_json = {};
|
|
body_json.type = 'speakerstats';
|
|
body_json.users = users_json;
|
|
|
|
local json_msg_str, error = json.encode(body_json);
|
|
|
|
if json_msg_str then
|
|
local stanza = st.message({
|
|
from = module.host;
|
|
to = occupant.jid; })
|
|
:tag("json-message", {xmlns='http://jitsi.org/jitmeet'})
|
|
:text(json_msg_str):up();
|
|
|
|
room:route_stanza(stanza);
|
|
else
|
|
module:log('error', 'Error encoding room:%s error:%s', room.jid, error);
|
|
end
|
|
end
|
|
end
|
|
|
|
local context_user = event.origin and event.origin.jitsi_meet_context_user or nil;
|
|
room.speakerStats[occupant.jid] = new_SpeakerStats(nick, context_user);
|
|
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;
|
|
|
|
if is_healthcheck_room(room.jid) then
|
|
return;
|
|
end
|
|
|
|
if not room.speakerStats then
|
|
return;
|
|
end
|
|
|
|
local occupant = event.occupant;
|
|
|
|
local speakerStatsForOccupant = room.speakerStats[occupant.jid];
|
|
if speakerStatsForOccupant then
|
|
speakerStatsForOccupant:setDominantSpeaker(false, false);
|
|
|
|
-- 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;
|
|
|
|
if is_healthcheck_room(room.jid) then
|
|
return;
|
|
end
|
|
|
|
module:fire_event("send-speaker-stats", { room = room; roomSpeakerStats = room.speakerStats; });
|
|
end
|
|
|
|
module:hook("message/host", on_message);
|
|
|
|
function process_main_muc_loaded(main_muc, host_module)
|
|
-- the conference muc component
|
|
module:log("info", "Hook to muc events on %s", host_module.host);
|
|
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
|
|
|
|
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);
|
|
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);
|
|
end
|
|
|
|
-- 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);
|