diff --git a/OpenSim/Addons/os-webrtc-janus/Janus/JanusMessages.cs b/OpenSim/Addons/os-webrtc-janus/Janus/JanusMessages.cs index c6c9ed10a4..c45d6d06b0 100644 --- a/OpenSim/Addons/os-webrtc-janus/Janus/JanusMessages.cs +++ b/OpenSim/Addons/os-webrtc-janus/Janus/JanusMessages.cs @@ -141,17 +141,17 @@ namespace osWebRtcVoice } public string sender { - get { return m_message.TryGetString("sender", out string sender) ? sender : string.Empty; } + get { return m_message is not null && m_message.TryGetString("sender", out string sender) ? sender : string.Empty; } } public virtual string ToJson() { - return m_message.ToString(); + return m_message is null ? "'null'": m_message.ToString(); } public override string ToString() { - return m_message.ToString(); + return m_message is null ? "'null'": m_message.ToString(); } } @@ -438,7 +438,7 @@ namespace osWebRtcVoice { public OSDMap m_pluginData; public OSDMap m_data; - public PluginMsgResp(JanusMessageResp pResp) : base(pResp.RawBody) + public PluginMsgResp(JanusMessageResp pResp) : base(pResp?.RawBody) { if (m_message is not null && m_message.TryGetOSDMap("plugindata", out m_pluginData)) { diff --git a/OpenSim/Addons/os-webrtc-janus/Janus/WebRtcJanusService.cs b/OpenSim/Addons/os-webrtc-janus/Janus/WebRtcJanusService.cs index cf6cf7ab93..0dac600dbc 100644 --- a/OpenSim/Addons/os-webrtc-janus/Janus/WebRtcJanusService.cs +++ b/OpenSim/Addons/os-webrtc-janus/Janus/WebRtcJanusService.cs @@ -446,18 +446,19 @@ namespace osWebRtcVoice } ]; resp = await viewerSession.Session.TrickleCandidates(viewerSession, candidatesArray); - _log.DebugFormat("{0} VoiceSignalingRequest: single candidate", LogHeader); + _log.Debug($"{LogHeader} VoiceSignalingRequest: single candidate"); } } else if (pRequest.TryGetOSDArray("candidates", out OSDArray candidates)) { OSDArray candidatesArray = []; - int sourceCount = candidates.Count; - int candidateLimit = _MaxSignalingCandidatesPerRequest; + //int sourceCount = candidates.Count; + //int candidateLimit = _MaxSignalingCandidatesPerRequest; foreach (OSDMap cand in candidates) { - if (candidateLimit > 0 && candidatesArray.Count >= candidateLimit) - break; + // TODO: can not limit candidates blindly +// if (candidateLimit > 0 && candidatesArray.Count >= candidateLimit) +// break; candidatesArray.Add(new OSDMap() { { "candidate", cand["candidate"].AsString() }, @@ -466,15 +467,15 @@ namespace osWebRtcVoice }); } resp = await viewerSession.Session.TrickleCandidates(viewerSession, candidatesArray).ConfigureAwait(false); - if (candidateLimit > 0 && sourceCount > candidatesArray.Count) - { - _log.WarnFormat("{0} VoiceSignalingRequest: capped candidates {1}/{2} (MaxSignalingCandidatesPerRequest={3})", - LogHeader, candidatesArray.Count, sourceCount, candidateLimit); - } - else - { +// if (candidateLimit > 0 && sourceCount > candidatesArray.Count) +// { +// _log.WarnFormat("{0} VoiceSignalingRequest: capped candidates {1}/{2} (MaxSignalingCandidatesPerRequest={3})", +// LogHeader, candidatesArray.Count, sourceCount, candidateLimit); +// } +// else +// { _log.DebugFormat("{0} VoiceSignalingRequest: {1} candidates", LogHeader, candidatesArray.Count); - } +// } } else { diff --git a/OpenSim/Addons/os-webrtc-janus/WebRtcVoice/VoiceViewerSession.cs b/OpenSim/Addons/os-webrtc-janus/WebRtcVoice/VoiceViewerSession.cs index 1e4240e8a1..ededa44a49 100644 --- a/OpenSim/Addons/os-webrtc-janus/WebRtcVoice/VoiceViewerSession.cs +++ b/OpenSim/Addons/os-webrtc-janus/WebRtcVoice/VoiceViewerSession.cs @@ -48,11 +48,7 @@ namespace osWebRtcVoice } public string ViewerSessionID { get; set; } public IWebRtcVoiceService VoiceService { get; set; } - public string VoiceServiceSessionId - { - get => throw new System.NotImplementedException(); - set => throw new System.NotImplementedException(); - } + public string VoiceServiceSessionId { get; set; } public UUID RegionId { get; set; } public UUID AgentId { get; set; } @@ -60,6 +56,7 @@ namespace osWebRtcVoice // ViewerSessions hold the connection information for the client connection through to the voice service. // This collection is static and is simulator wide so there will be sessions for all regions and all clients. public static Dictionary ViewerSessions = new Dictionary(); + // Get a viewer session by the viewer session ID public static bool TryGetViewerSession(string pViewerSessionId, out IVoiceViewerSession pViewerSession) { @@ -68,6 +65,7 @@ namespace osWebRtcVoice return ViewerSessions.TryGetValue(pViewerSessionId, out pViewerSession); } } + // public static bool TryGetViewerSessionByAgentId(UUID pAgentId, out IVoiceViewerSession pViewerSession) public static bool TryGetViewerSessionByAgentId(UUID pAgentId, out IEnumerable> pViewerSessions) { @@ -77,6 +75,7 @@ namespace osWebRtcVoice return pViewerSessions.Count() > 0; } } + // Get a viewer session by the VoiceService session ID public static bool TryGetViewerSessionByVSSessionId(string pVSSessionId, out IVoiceViewerSession pViewerSession) { @@ -92,6 +91,22 @@ namespace osWebRtcVoice return false; } } + + public static bool TryGetViewerSessionByAgentAndRegion(UUID pAgentId, UUID pRegionId, out IVoiceViewerSession pViewerSession) + { + lock (ViewerSessions) + { + IVoiceViewerSession session = ViewerSessions.Values.FirstOrDefault(v => v.AgentId == pAgentId && v.RegionId == pRegionId); + if (session is not null) + { + pViewerSession = session; + return true; + } + pViewerSession = null; + return false; + } + } + public static void AddViewerSession(IVoiceViewerSession pSession) { lock (ViewerSessions) @@ -125,8 +140,11 @@ namespace osWebRtcVoice public Task Shutdown() { - throw new System.NotImplementedException(); - } + if (!string.IsNullOrEmpty(ViewerSessionID)) + { + RemoveViewerSession(ViewerSessionID); + } + return Task.CompletedTask; } } } diff --git a/OpenSim/Addons/os-webrtc-janus/WebRtcVoice/WebRtcVoiceServerConnector.cs b/OpenSim/Addons/os-webrtc-janus/WebRtcVoice/WebRtcVoiceServerConnector.cs index 301c5b256e..f33b11af1e 100644 --- a/OpenSim/Addons/os-webrtc-janus/WebRtcVoice/WebRtcVoiceServerConnector.cs +++ b/OpenSim/Addons/os-webrtc-janus/WebRtcVoice/WebRtcVoiceServerConnector.cs @@ -58,6 +58,8 @@ namespace osWebRtcVoice public WebRtcVoiceServerConnector(IConfigSource pConfig, IHttpServer pServer, string pConfigName) { +// WebRtcDebugControl.ApplyFromConfig(pConfig); + IConfig moduleConfig = pConfig.Configs["WebRtcVoice"]; if (moduleConfig is not null) @@ -72,15 +74,13 @@ namespace osWebRtcVoice // The local service provides the IWebRtcVoiceService interface and directs the requests // to the WebRTC service. string localServiceModule = moduleConfig.GetString("LocalServiceModule", "WebRtcVoiceServiceModule.dll:WebRtcVoiceServiceModule"); - m_log.DebugFormat("{0} loading {1}", LogHeader, localServiceModule); - object[] args = new object[0]; - m_WebRtcVoiceService = ServerUtils.LoadPlugin(localServiceModule, args); + m_log.Debug($"{LogHeader} loading {localServiceModule}"); + m_WebRtcVoiceService = ServerUtils.LoadPlugin(localServiceModule, []); // The WebRtcVoiceServiceModule is both an IWebRtcVoiceService and a ISharedRegionModule // so we can initialize it as if it was the region module. - ISharedRegionModule sharedModule = m_WebRtcVoiceService as ISharedRegionModule; - if (sharedModule is null) + if (m_WebRtcVoiceService is not ISharedRegionModule sharedModule) { m_log.ErrorFormat("{0} local service module does not implement ISharedRegionModule", LogHeader); m_Enabled = false; @@ -98,7 +98,7 @@ namespace osWebRtcVoice private bool Handle_ProvisionVoiceAccountRequest(OSDMap pJson, ref JsonRpcResponse pResponse) { bool ret = false; - m_log.DebugFormat("{0} Handle_ProvisionVoiceAccountRequest", LogHeader); + m_log.Debug($"{LogHeader} Handle_ProvisionVoiceAccountRequest"); if (m_MessageDetails) m_log.DebugFormat("{0} PVAR: req={1}", LogHeader, pJson.ToString()); if (pJson.ContainsKey("params") && pJson["params"] is OSDMap paramsMap) diff --git a/OpenSim/Addons/os-webrtc-janus/WebRtcVoice/WebRtcVoiceServiceConnector.cs b/OpenSim/Addons/os-webrtc-janus/WebRtcVoice/WebRtcVoiceServiceConnector.cs index 69188b12e2..89726de0a9 100644 --- a/OpenSim/Addons/os-webrtc-janus/WebRtcVoice/WebRtcVoiceServiceConnector.cs +++ b/OpenSim/Addons/os-webrtc-janus/WebRtcVoice/WebRtcVoiceServiceConnector.cs @@ -44,14 +44,16 @@ namespace osWebRtcVoice { private static readonly ILog m_log = LogManager.GetLogger(MethodBase.GetCurrentMethod().DeclaringType); private static readonly string LogHeader = "[WEBRTC VOICE SERVICE CONNECTOR]"; - private bool m_Enabled = false; - private bool m_MessageDetails = false; + private readonly bool m_Enabled = false; + private readonly bool m_MessageDetails = false; private IConfigSource m_Config; string m_serverURI = "http://localhost:8080"; public WebRtcVoiceServiceConnector(IConfigSource pConfig) { +// WebRtcDebugControl.ApplyFromConfig(pConfig); + m_Config = pConfig; IConfig moduleConfig = m_Config.Configs["WebRtcVoice"]; @@ -65,13 +67,12 @@ namespace osWebRtcVoice { m_log.Error($"{LogHeader} WebRtcVoiceServiceConnector enabled but no WebRtcVoiceServerURI specified"); m_Enabled = false; - } - else - { - m_log.Info($"{LogHeader} WebRtcVoiceServiceConnector enabled"); + return; } m_MessageDetails = moduleConfig.GetBoolean("MessageDetails", false); + + m_log.Info($"{LogHeader} WebRtcVoiceServiceConnector enabled"); } } } @@ -82,7 +83,7 @@ namespace osWebRtcVoice public IVoiceViewerSession CreateViewerSession(OSDMap pRequest, UUID pUserID, UUID pSceneID) { m_log.Debug($"{LogHeader} CreateViewerSession"); - return new VoiceViewerSession(this, pUserID, pSceneID); + return new VoiceViewerSession(this, pSceneID, pUserID); } public OSDMap ProvisionVoiceAccountRequest(OSDMap pRequest, UUID pUserID, UUID pSceneID) @@ -122,7 +123,7 @@ namespace osWebRtcVoice public OSDMap VoiceSignalingRequest(IVoiceViewerSession pVSession, OSDMap pRequest, UUID pUserID, UUID pSceneID) { - m_log.DebugFormat("{0} VoiceSignalingRequest. uID={1}, sID={2}", LogHeader, pUserID, pSceneID); + m_log.Debug($"{LogHeader} VoiceSignalingRequest. uID={pUserID}, sID={pSceneID}"); OSDMap req = new() { { "request", pRequest }, @@ -139,66 +140,74 @@ namespace osWebRtcVoice if(string.IsNullOrWhiteSpace(uri)) return null; - OSDMap request = new() + OSDMap request = new() + { + { "jsonrpc", OSD.FromString("2.0") }, + { "id", OSD.FromString(jsonId) }, + { "method", OSD.FromString(method) }, + { "params", pParams } + }; + + OSDMap outerResponse = null; + try + { + if (m_MessageDetails) m_log.Debug($"{LogHeader}: request: {request}"); + + outerResponse = WebUtil.PostToService(uri, request, 10000, true); + + if (m_MessageDetails) m_log.Debug($"{LogHeader}: response: {outerResponse}"); + } + catch (Exception e) + { + m_log.Error($"{LogHeader}: JsonRpc request '{method}' to {uri} failed: {e.Message}"); + m_log.Debug($"{LogHeader}: request: {request}"); + return new OSDMap() { - { "jsonrpc", OSD.FromString("2.0") }, - { "id", OSD.FromString(jsonId) }, - { "method", OSD.FromString(method) }, - { "params", pParams } + { "error", OSD.FromString(e.Message) } }; + } - OSDMap outerResponse = null; - try + if (outerResponse is null || outerResponse.Count == 0) + { + string errm = $"JsonRpc request '{method}' to {uri} returned an empty response"; + m_log.Error(errm); + return new OSDMap() { - if (m_MessageDetails) m_log.Debug($"{LogHeader}: request: {request}"); + { "error", errm } + }; + } - outerResponse = WebUtil.PostToService(uri, request, 10000, true); - - if (m_MessageDetails) m_log.Debug($"{LogHeader}: response: {outerResponse}"); - } - catch (Exception e) + if (!outerResponse.TryGetOSDMap("_Result", out OSDMap response)) + { + string errm = $"JsonRpc request '{method}' to {uri} returned an invalid response: {OSDParser.SerializeJsonString(outerResponse)}"; + m_log.Error(errm); + return new OSDMap() { - m_log.Error($"{LogHeader}: JsonRpc request '{method}' to {uri} failed: {e.Message}"); - m_log.Debug($"{LogHeader}: request: {request}"); - return new OSDMap() - { - { "error", OSD.FromString(e.Message) } - }; - } + { "error", errm } + }; + } - if (!outerResponse.TryGetOSDMap("_Result", out OSDMap response)) + if (response.TryGetValue("error", out OSD osdtmp)) + { + string errm = $"JsonRpc request '{method}' to {uri} returned an error: {OSDParser.SerializeJsonString(osdtmp)}"; + m_log.Error(errm); + return new OSDMap() { - string errm = $"JsonRpc request '{method}' to {1} returned an invalid response: {OSDParser.SerializeJsonString(outerResponse)}"; - m_log.Error(errm); - return new OSDMap() - { - { "error", errm } - }; - } + { "error", errm } + }; + } - OSD osdtmp; - if (response.TryGetValue("error", out osdtmp)) + if (!response.TryGetOSDMap("result", out OSDMap resultmap )) + { + string errm = $"JsonRpc request '{method}' to {uri} returned result as non-OSDMap: {OSDParser.SerializeJsonString(outerResponse)}"; + m_log.Error(errm); + return new OSDMap() { - string errm = $"JsonRpc request '{method}' to {uri} returned an error: {OSDParser.SerializeJsonString(osdtmp)}"; - m_log.Error(errm); - return new OSDMap() - { - { "error", errm } - }; - } - - if (!response.TryGetOSDMap("result", out OSDMap resultmap )) - { - string errm = $"JsonRpc request '{method}' to {uri} returned result as non-OSDMap: {OSDParser.SerializeJsonString(outerResponse)}"; - m_log.Error(errm); - return new OSDMap() - { - { "error", errm } - }; - } + { "error", errm } + }; + } return resultmap; } - } } \ No newline at end of file diff --git a/OpenSim/Addons/os-webrtc-janus/WebRtcVoiceRegionModule/WebRtcVoiceRegionModule.cs b/OpenSim/Addons/os-webrtc-janus/WebRtcVoiceRegionModule/WebRtcVoiceRegionModule.cs index 71062c11d0..8ea6d620b4 100644 --- a/OpenSim/Addons/os-webrtc-janus/WebRtcVoiceRegionModule/WebRtcVoiceRegionModule.cs +++ b/OpenSim/Addons/os-webrtc-janus/WebRtcVoiceRegionModule/WebRtcVoiceRegionModule.cs @@ -68,7 +68,7 @@ namespace osWebRtcVoice private static readonly ILog m_log = LogManager.GetLogger(MethodBase.GetCurrentMethod().DeclaringType); private static readonly string logHeader = "[REGION WEBRTC VOICE]"; - private static byte[] llsdUndefAnswerBytes = Util.UTF8.GetBytes(""); + private static byte[] llsdUndefAnswerBytes = Util.UTF8.GetBytes(""); private bool _MessageDetails = false; // Control info @@ -79,6 +79,8 @@ namespace osWebRtcVoice // ISharedRegionModule.Initialize public void Initialise(IConfigSource config) { +// WebRtcDebugControl.ApplyFromConfig(config); + m_Config = config.Configs["WebRtcVoice"]; if (m_Config is not null) { @@ -100,7 +102,7 @@ namespace osWebRtcVoice // ISharedRegionModule.AddRegion public void AddRegion(Scene scene) { - // todo register module to get parcels changes etc + // TODO: register module to get parcels changes etc } // ISharedRegionModule.RemoveRegion @@ -161,7 +163,7 @@ namespace osWebRtcVoice public void OnRegisterCaps(Scene scene, UUID agentID, Caps caps) { m_log.Debug( - $"{logHeader}: OnRegisterCaps called with agentID {agentID} caps {caps} in scene {scene.Name}"); + $"{logHeader}: OnRegisterCaps called with agentID {agentID} in scene {scene.Name}"); caps.RegisterSimpleHandler("ProvisionVoiceAccountRequest", new SimpleStreamHandler("/" + UUID.Random(), (IOSHttpRequest httpRequest, IOSHttpResponse httpResponse) => @@ -224,7 +226,10 @@ namespace osWebRtcVoice { if (vstosd is OSDString vst && !((string)vst).Equals("webrtc", StringComparison.OrdinalIgnoreCase)) { - m_log.Warn($"{logHeader}[ProvisionVoice]: voice_server_type is not 'webrtc'. Request: {map}"); + m_log.Warn($"{logHeader}[ProvisionVoice]: voice_server_type is not 'webrtc'"); + if (m_log.IsDebugEnabled) + m_log.Warn($"{logHeader}[ProvisionVoice]: Request detail: {map}"); + response.RawBuffer = llsdUndefAnswerBytes; response.StatusCode = (int)HttpStatusCode.OK; return; @@ -289,6 +294,9 @@ namespace osWebRtcVoice if ((land.Flags & (uint)ParcelFlags.UseEstateVoiceChan) != 0) { + // By removing the parcel_local_id, the voice service will treat this as an estate channel + // request and return the appropriate voice credentials for the estate channel + // instead of a parcel channel map.Remove("parcel_local_id"); // estate channel } else if(parcel.IsRestrictedFromLand(agentID) || parcel.IsBannedFromLand(agentID)) @@ -330,7 +338,7 @@ namespace osWebRtcVoice IWebRtcVoiceService voiceService = scene.RequestModuleInterface(); if (voiceService is null) { - m_log.Error($"{logHeader}[VoiceSignalingRequest]: avatar \"{agentID}\": no voice service"); + m_log.Error($"{logHeader}[VoiceSignalingRequest]: No voice service, Agent={agentID}"); response.StatusCode = (int)HttpStatusCode.NotFound; return; } @@ -366,8 +374,8 @@ namespace osWebRtcVoice if (_MessageDetails) m_log.Debug($"{logHeader}[VoiceSignalingRequest]: Response: {resp}"); - // TODO: check for errors and package the response - + // TODO: check for errors + // viewers ignore response response.RawBuffer = llsdUndefAnswerBytes; response.StatusCode = (int)HttpStatusCode.OK; return; @@ -480,9 +488,9 @@ namespace osWebRtcVoice { try { - using Stream inputStream = request.InputStream; - if (inputStream.Length > 0) - { + if (request.InputStream.Length > 0) + { + using Stream inputStream = request.InputStream; OSD tmp = OSDParser.DeserializeLLSDXml(inputStream); if (_MessageDetails) m_log.Debug($"{pCaller} BodyToMap: Request: {tmp}"); diff --git a/OpenSim/Addons/os-webrtc-janus/WebRtcVoiceServiceModule/WebRtcVoiceServiceModule.cs b/OpenSim/Addons/os-webrtc-janus/WebRtcVoiceServiceModule/WebRtcVoiceServiceModule.cs index 806879702a..a8a24ffbd1 100644 --- a/OpenSim/Addons/os-webrtc-janus/WebRtcVoiceServiceModule/WebRtcVoiceServiceModule.cs +++ b/OpenSim/Addons/os-webrtc-janus/WebRtcVoiceServiceModule/WebRtcVoiceServiceModule.cs @@ -74,6 +74,8 @@ namespace osWebRtcVoice // Get configuration and load the modules that will handle spatial and non-spatial voice. public void Initialise(IConfigSource pConfig) { +// WebRtcDebugControl.ApplyFromConfig(pConfig); + m_Config = pConfig; IConfig moduleConfig = m_Config.Configs["WebRtcVoice"]; @@ -83,6 +85,11 @@ namespace osWebRtcVoice if (m_Enabled) { // Get the DLLs for the two voice services + // TODO: spacial/nonspacial names are wrong + // spacial here means service for region parcels, that can be spacial or not + // non spacial means for other uses like IMs, that just happen to be non spacial + // in fact this needs more consideration than just this 2 options + string spatialDllName = moduleConfig.GetString("SpatialVoiceService", string.Empty); string nonSpatialDllName = moduleConfig.GetString("NonSpatialVoiceService", string.Empty); if (string.IsNullOrEmpty(spatialDllName) && string.IsNullOrEmpty(nonSpatialDllName)) @@ -205,49 +212,206 @@ namespace osWebRtcVoice } } } + + private static bool TryGetViewerSessionByAgentAndScene(UUID pAgentID, UUID pSceneID, out IVoiceViewerSession pViewerSession) + { + if (VoiceViewerSession.TryGetViewerSessionByAgentId(pAgentID, out IEnumerable> vSessions)) + { + foreach (KeyValuePair v in vSessions) + { + if (v.Value.RegionId == pSceneID) + { + pViewerSession = v.Value; + return true; + } + } + } + pViewerSession = null; + return false; + } + + private static List> GetViewerSessionsByAgentAndScene(UUID pAgentID, UUID pSceneID) + { + List> matches = []; + if (VoiceViewerSession.TryGetViewerSessionByAgentId(pAgentID, out IEnumerable> vSessions)) + { + foreach (KeyValuePair v in vSessions) + { + if (v.Value.RegionId == pSceneID) + { + matches.Add(v); + } + } + } + return matches; + } + + private static object TryGetPropertyValue(object pSource, string pPropertyName) + { + if (pSource is null || string.IsNullOrEmpty(pPropertyName)) + return null; + + PropertyInfo propertyInfo = pSource.GetType().GetProperty(pPropertyName); + if (propertyInfo is null) + return null; + + return propertyInfo.GetValue(pSource); + } + + private static bool IsViewerSessionReusable(IVoiceViewerSession pViewerSession) + { + if (pViewerSession is null) + return false; + + if (string.IsNullOrEmpty(pViewerSession.ViewerSessionID) || string.IsNullOrEmpty(pViewerSession.VoiceServiceSessionId)) + return false; + + object disconnectReason = TryGetPropertyValue(pViewerSession, "DisconnectReason"); + if (disconnectReason is string reason && !string.IsNullOrEmpty(reason)) + return false; + + object sessionObj = TryGetPropertyValue(pViewerSession, "Session"); + if (sessionObj is not null) + { + object isConnectedObj = TryGetPropertyValue(sessionObj, "IsConnected"); + if (isConnectedObj is bool isConnected && !isConnected) + return false; + } + + return true; + } + + private void CleanupDuplicateSessions(UUID pAgentID, UUID pSceneID, string pKeepViewerSessionId) + { + List> candidates = GetViewerSessionsByAgentAndScene(pAgentID, pSceneID); + foreach (KeyValuePair candidate in candidates) + { + if (!string.IsNullOrEmpty(pKeepViewerSessionId) && candidate.Key == pKeepViewerSessionId) + continue; + + m_log.Warn( + $"{LogHeader} CleanupDuplicateSessions: removing stale viewer_session {candidate.Key} for agent {pAgentID}, scene {pSceneID}"); + + VoiceViewerSession.RemoveViewerSession(candidate.Key); + _ = Task.Run(async () => + { + try + { + await candidate.Value.Shutdown(); + } + catch (Exception ex) + { + m_log.Debug( + $"{LogHeader} CleanupDuplicateSessions: shutdown failed for viewer_session {candidate.Key}: {ex.Message}"); + } + }); + } + } + private bool TryGetReusableViewerSession(UUID pAgentID, UUID pSceneID, out IVoiceViewerSession pViewerSession) + { + List> sessions = GetViewerSessionsByAgentAndScene(pAgentID, pSceneID); + foreach (KeyValuePair candidate in sessions) + { + if (IsViewerSessionReusable(candidate.Value)) + { + pViewerSession = candidate.Value; + CleanupDuplicateSessions(pAgentID, pSceneID, candidate.Key); + return true; + } + } + + if (sessions.Count > 0) + { + // No reusable session found: remove all stale sessions to force a clean create path. + CleanupDuplicateSessions(pAgentID, pSceneID, null); + } + + pViewerSession = null; + return false; + } + // ===================================================================== // IWebRtcVoiceService // IWebRtcVoiceService.ProvisionVoiceAccountRequest public OSDMap ProvisionVoiceAccountRequest(OSDMap pRequest, UUID pUserID, UUID pSceneID) { - OSDMap response = null; IVoiceViewerSession vSession = null; if (pRequest.TryGetString("viewer_session", out string viewerSessionId)) { // request has a viewer session. Use that to find the voice service - if (!VoiceViewerSession.TryGetViewerSession(viewerSessionId, out vSession)) + if (VoiceViewerSession.TryGetViewerSession(viewerSessionId, out vSession)) { - m_log.Error($"{0} ProvisionVoiceAccountRequest: viewer session {viewerSessionId} not found"); + CleanupDuplicateSessions(pUserID, pSceneID, viewerSessionId); } - } + else + { + if (TryGetReusableViewerSession(pUserID, pSceneID, out vSession)) + m_log.Info( + $"{LogHeader} ProvisionVoiceAccountRequest: viewer session {viewerSessionId} not found, reconnect fallback reused {vSession.ViewerSessionID}"); + else + m_log.Error($"{LogHeader} ProvisionVoiceAccountRequest: viewer session {viewerSessionId} not found"); + } + } else { // the request does not have a viewer session. See if it's an initial request if (pRequest.TryGetString("channel_type", out string channelType)) { - if (channelType == "local") + if (TryGetReusableViewerSession(pUserID, pSceneID, out vSession)) { - // TODO: check if this userId is making a new session (case that user is reconnecting) - vSession = m_spatialVoiceService.CreateViewerSession(pRequest, pUserID, pSceneID); - VoiceViewerSession.AddViewerSession(vSession); + m_log.Info( + $"{LogHeader} ProvisionVoiceAccountRequest: reconnect reuse for agent {pUserID}, scene {pSceneID}, viewer_session {vSession.ViewerSessionID}"); } else { - // TODO: check if this userId is making a new session (case that user is reconnecting) - vSession = m_nonSpatialVoiceService.CreateViewerSession(pRequest, pUserID, pSceneID); - VoiceViewerSession.AddViewerSession(vSession); + // Ensure stale sessions are cleared before creating a new one. + CleanupDuplicateSessions(pUserID, pSceneID, null); + if (channelType == "local") + { + // TODO: check if this userId is making a new session (case that user is reconnecting) + vSession = m_spatialVoiceService.CreateViewerSession(pRequest, pUserID, pSceneID); + VoiceViewerSession.AddViewerSession(vSession); + } + else + { + // TODO: check if this userId is making a new session (case that user is reconnecting) + vSession = m_nonSpatialVoiceService.CreateViewerSession(pRequest, pUserID, pSceneID); + VoiceViewerSession.AddViewerSession(vSession); + } } } else { - m_log.Error($"{LogHeader} ProvisionVoiceAccountRequest: no channel_type in request"); + if (TryGetReusableViewerSession(pUserID, pSceneID, out vSession)) + { + m_log.Info( + $"{LogHeader} ProvisionVoiceAccountRequest: missing channel_type, reused existing session for agent {pUserID}, scene {pSceneID}, viewer_session {vSession.ViewerSessionID}"); + } + else + { + m_log.Error( + $"{LogHeader} ProvisionVoiceAccountRequest: no channel_type in request and no existing session for agent {pUserID}, scene {pSceneID}"); + } } } + + OSDMap response = null; + if (vSession is not null) { response = vSession.VoiceService.ProvisionVoiceAccountRequest(vSession, pRequest, pUserID, pSceneID); } + + if (response is null) + { + return new OSDMap + { + { "response", "error" }, + { "message", "Unable to provision voice session (missing viewer_session/channel_type or session not found)" } + }; + } + return response; } @@ -265,12 +429,12 @@ namespace osWebRtcVoice } else { - m_log.ErrorFormat("{0} VoiceSignalingRequest: viewer session {1} not found", LogHeader, viewerSessionId); + m_log.Error($"{LogHeader} VoiceSignalingRequest: viewer session {viewerSessionId} not found"); } - } + } else { - m_log.ErrorFormat("{0} VoiceSignalingRequest: no viewer_session in request", LogHeader); + m_log.Error($"{LogHeader} VoiceSignalingRequest: no viewer_session in request"); } return response; }