From e661b5d6b5d234889d573970a870a092cd5a1a75 Mon Sep 17 00:00:00 2001 From: UbitUmarov Date: Sat, 14 Mar 2026 20:55:05 +0000 Subject: [PATCH] some changes adapted from Manni patch in mantis 9235 note participantID still type int --- .../os-webrtc-janus/Janus/JanusMessages.cs | 29 ++-- .../Addons/os-webrtc-janus/Janus/JanusRoom.cs | 160 ++++++++++++++++-- 2 files changed, 164 insertions(+), 25 deletions(-) diff --git a/OpenSim/Addons/os-webrtc-janus/Janus/JanusMessages.cs b/OpenSim/Addons/os-webrtc-janus/Janus/JanusMessages.cs index c959be231c..af3e4b3515 100644 --- a/OpenSim/Addons/os-webrtc-janus/Janus/JanusMessages.cs +++ b/OpenSim/Addons/os-webrtc-janus/Janus/JanusMessages.cs @@ -72,13 +72,13 @@ namespace osWebRtcVoice public string TransactionId { - get { return m_message.TryGetString("transaction", out string tid) ? tid : null; } + get { return m_message.TryGetString("transaction", out string tid) ? tid : null; } set { m_message["transaction"] = value; } } public string Sender { - get { return m_message.TryGetString("sender", out string tid) ? tid : null; } + get { return m_message.TryGetString("sender", out string tid) ? tid : null; } set { m_message["sender"] = value; } } @@ -199,7 +199,7 @@ namespace osWebRtcVoice } // Return the "data" portion of the response as an OSDMap or null if there is none - public OSDMap dataSection { get { return m_message.TryGetOSDMap("data", out OSDMap osdm) ? osdm : null; } } + public OSDMap dataSection { get { return m_message.TryGetOSDMap("data", out OSDMap data) ? data : null; } } // Check if a successful response code is in the response public virtual bool isSuccess { get { return CheckReturnCode("success"); } } @@ -216,8 +216,8 @@ namespace osWebRtcVoice { get { - return m_message is not null && m_message.ContainsKey("janus") ? - m_message["janus"].AsString() : string.Empty; + return m_message is not null && m_message.TryGetString("janus", out string sjanus) ? + sjanus : string.Empty; } } } @@ -464,6 +464,14 @@ namespace osWebRtcVoice return m_data.TryGetValue(pKey, out OSD okey) ? (int)okey.AsLong(): 0; } + // Get an long value for a key in the response data or zero if not there + public long PluginRespDataLong(string pKey) + { + if (m_data is null) + return 0L; + return m_data.TryGetValue(pKey, out OSD okey) ? okey.AsLong(): 0L; + } + // Get a string value for a key in the response data or empty string if not there public string PluginRespDataString(string pKey) { @@ -519,7 +527,7 @@ namespace osWebRtcVoice { "spatial_audio", pSpatial }, { "denoise", false }, { "record", false } - }) + }) { if (!String.IsNullOrEmpty(pDesc)) AddStringToBody("description", pDesc); @@ -533,7 +541,7 @@ namespace osWebRtcVoice { "request", "destroy" }, { "room", pRoomId }, { "permanent", true } - }) + }) { } } @@ -558,6 +566,7 @@ namespace osWebRtcVoice } public int ParticipantId { get { return PluginRespDataInt("id"); } } + //public long ParticipantId { get { return PluginRespDataLong("id"); } } } // ============================================================== @@ -565,7 +574,7 @@ namespace osWebRtcVoice { // TODO: public AudioBridgeConfigRoomReq(int pRoomId, string pSdp) : base(new OSDMap() { - { "request", "configure" }, + { "request", "configure" } }) { } @@ -586,7 +595,7 @@ namespace osWebRtcVoice { "request", "leave" }, { "room", pRoomId }, { "id", pAttendeeId } - }) + }) { } } @@ -607,7 +616,7 @@ namespace osWebRtcVoice public AudioBridgeListParticipantsReq(int pRoom) : base(new OSDMap() { { "request", "listparticipants" }, { "room", pRoom } - }) + }) { } } diff --git a/OpenSim/Addons/os-webrtc-janus/Janus/JanusRoom.cs b/OpenSim/Addons/os-webrtc-janus/Janus/JanusRoom.cs index 7a76f11b19..dd824199c5 100644 --- a/OpenSim/Addons/os-webrtc-janus/Janus/JanusRoom.cs +++ b/OpenSim/Addons/os-webrtc-janus/Janus/JanusRoom.cs @@ -28,18 +28,11 @@ using System; using System.Reflection; -using OpenSim.Framework; -using OpenSim.Services.Interfaces; -using OpenSim.Services.Base; - using OpenMetaverse.StructuredData; -using OpenMetaverse; using Nini.Config; using log4net; using System.Threading.Tasks; -using System.Text.RegularExpressions; -using System.Collections.Generic; namespace osWebRtcVoice { @@ -76,30 +69,136 @@ namespace osWebRtcVoice // and, if removed, the viewer complains that the "m=" sections are // out of order. Not "cleaning" (removing the data section) seems to work. // string cleanSdp = CleanupSdp(pSdp); - var joinReq = new AudioBridgeJoinRoomReq(RoomId, pVSession.AgentId.ToString()); + AudioBridgeJoinRoomReq joinReq = new(RoomId, pVSession.AgentId.ToString()); // joinReq.SetJsep("offer", cleanSdp); joinReq.SetJsep("offer", pVSession.Offer); JanusMessageResp resp = await _AudioBridge.SendPluginMsg(joinReq); - AudioBridgeJoinRoomResp joinResp = new AudioBridgeJoinRoomResp(resp); + AudioBridgeJoinRoomResp joinResp = new(resp); + // if (joinResp is not null && joinResp.AudioBridgeReturnCode == "joined" && joinResp.ParticipantId > 0) if (joinResp is not null && joinResp.AudioBridgeReturnCode == "joined") { pVSession.ParticipantId = joinResp.ParticipantId; pVSession.Answer = joinResp.Jsep; ret = true; - m_log.DebugFormat("{0} JoinRoom. Joined room {1}. Participant={2}", LogHeader, RoomId, pVSession.ParticipantId); + m_log.Debug($"{LogHeader} JoinRoom. Joined room {RoomId}. Participant={pVSession.ParticipantId}"); } + else if (joinResp is not null && joinResp.AudioBridgeErrorCode == 491) + { + m_log.Warn($"{LogHeader} JoinRoom. Already in a room for agent {pVSession.AgentId}. Attempting recovery."); + + bool recovered = await RecoverAlreadyInRoomAndLeave(pVSession.AgentId.ToString()); + if (recovered) + { + AudioBridgeJoinRoomReq retryJoinReq = new(RoomId, pVSession.AgentId.ToString()); + retryJoinReq.SetJsep("offer", pVSession.Offer); + JanusMessageResp retryResp = await _AudioBridge.SendPluginMsg(retryJoinReq); + AudioBridgeJoinRoomResp retryJoinResp = new(retryResp); + +// if (retryJoinResp is not null && retryJoinResp.AudioBridgeReturnCode == "joined" && retryJoinResp.ParticipantId > 0) + if (retryJoinResp is not null && retryJoinResp.AudioBridgeReturnCode == "joined") + { + pVSession.ParticipantId = retryJoinResp.ParticipantId; + pVSession.Answer = retryJoinResp.Jsep; + ret = true; + m_log.Info($"{LogHeader} JoinRoom. Recovery succeeded for room {RoomId}. Participant={pVSession.ParticipantId}"); + } + else + { + m_log.Error($"{LogHeader} JoinRoom. Recovery retry failed for room {RoomId}. Resp={retryJoinResp?.ToString() ?? "null"}"); + } + } + else + { + m_log.Error($"{LogHeader} JoinRoom. Recovery failed: could not clear previous room membership. Resp={joinResp}"); + } + } +/* else { - m_log.ErrorFormat("{0} JoinRoom. Failed to join room {1}. Resp={2}", LogHeader, RoomId, joinResp.ToString()); + if (joinResp is not null && joinResp.AudioBridgeReturnCode == "joined" && joinResp.ParticipantId <= 0) + { + m_log.Error($"{LogHeader} JoinRoom. Joined response contains invalid participant id {joinResp.ParticipantId} for room {RoomId}. Resp={joinResp}"); + } + m_log.Error($"{LogHeader} JoinRoom. Failed to join room {RoomId}. Resp={joinResp}"); + } +*/ + } + catch (Exception e) + { + m_log.Error($"{LogHeader} JoinRoom. Exception ", e); + } + return ret; + } + + private async Task RecoverAlreadyInRoomAndLeave(string pDisplay) + { + try + { + JanusMessageResp listRoomsRespRaw = await _AudioBridge.SendPluginMsg(new AudioBridgeListRoomsReq()); + AudioBridgeResp listRoomsResp = new AudioBridgeResp(listRoomsRespRaw); + if (listRoomsResp?.PluginRespData is null || + !listRoomsResp.PluginRespData.TryGetValue("list", out OSD roomListNode) || + roomListNode is not OSDArray roomList) + { + return false; + } + + foreach (OSD roomNode in roomList) + { + if (roomNode is not OSDMap roomMap || + !roomMap.TryGetValue("room", out OSD roomIdNode)) + continue; + + int roomId = roomIdNode.AsInteger(); +// if (roomId <= 0) +// continue; + + JanusMessageResp listParticipantsRespRaw = await _AudioBridge.SendPluginMsg(new AudioBridgeListParticipantsReq(roomId)); + AudioBridgeResp listParticipantsResp = new AudioBridgeResp(listParticipantsRespRaw); + if (listParticipantsResp?.PluginRespData is null || + !listParticipantsResp.PluginRespData.TryGetValue("participants", out OSD participantsNode) || + participantsNode is not OSDArray participants) + continue; + + foreach (OSD participantNode in participants) + { + if (participantNode is not OSDMap participant) + continue; + + string display = participant.TryGetValue("display", out OSD displayNode) ? displayNode.AsString() : string.Empty; + if (!string.Equals(display, pDisplay, StringComparison.Ordinal)) + continue; + + int participantId = participant.TryGetValue("id", out OSD idNode) ? (int)idNode.AsLong() : 0; + if (participantId <= 0) + continue; + + JanusMessageResp leaveRespRaw = await _AudioBridge.SendPluginMsg(new AudioBridgeLeaveRoomReq(roomId, participantId)); + AudioBridgeResp leaveResp = new(leaveRespRaw); + + if (leaveResp is not null) + { + int errorCode = leaveResp.AudioBridgeErrorCode; + string abCode = leaveResp.AudioBridgeReturnCode; + string janusCode = leaveRespRaw.ReturnCode; + + if (errorCode == 0 || abCode == "left" || abCode == "event" || janusCode == "ack") + { + m_log.Info($"{LogHeader} RecoverAlreadyInRoomAndLeave. Cleared stale participant {participantId} from room {roomId}"); + return true; + } + } + } } } catch (Exception e) { - m_log.ErrorFormat("{0} JoinRoom. Exception {1}", LogHeader, e); + m_log.Error($"{LogHeader} RecoverAlreadyInRoomAndLeave. Exception ", e); } - return ret; + + return false; } // TODO: this doesn't work. @@ -126,13 +225,44 @@ namespace osWebRtcVoice { JanusMessageResp resp = await _AudioBridge.SendPluginMsg( new AudioBridgeLeaveRoomReq(RoomId, pAttendeeSession.ParticipantId)); + + if (resp is null) + { + m_log.Error($"{LogHeader} LeaveRoom. Null response for room {RoomId}, participant={pAttendeeSession.ParticipantId}"); + return false; + } + + AudioBridgeResp abResp = new(resp); + string returnCode = abResp.AudioBridgeReturnCode; + string janusReturnCode = resp.ReturnCode; + int errorCode = abResp.AudioBridgeErrorCode; + bool isBenignAlreadyLeft = errorCode == 487 && + (returnCode == "event" || janusReturnCode == "event" || janusReturnCode == "ack"); + + if (errorCode == 0 && + (abResp.isSuccess || returnCode == "left" || returnCode == "event" || returnCode == "success" || janusReturnCode == "ack")) + { + ret = true; + if (janusReturnCode == "ack" && string.IsNullOrEmpty(returnCode)) + { + m_log.Debug($"{LogHeader} LeaveRoom. Ack accepted for room {RoomId}, participant={pAttendeeSession.ParticipantId}"); + } + } + else if (isBenignAlreadyLeft) + { + ret = true; + m_log.Info($"{LogHeader} LeaveRoom. Participant already left room {RoomId}, participant={pAttendeeSession.ParticipantId} (errorCode=487)"); + } + else + { + m_log.Error($"{LogHeader} LeaveRoom. Failed room {RoomId}, participant={pAttendeeSession.ParticipantId}, janus={janusReturnCode}, audiobridge={returnCode}, errorCode={errorCode}"); + } } catch (Exception e) { - m_log.ErrorFormat("{0} LeaveRoom. Exception {1}", LogHeader, e); + m_log.Error($"{LogHeader} LeaveRoom. Exception ", e); } return ret; } - } }