diff --git a/Janus/JanusMessages.cs b/Janus/JanusMessages.cs index 92493a1..feec4c1 100644 --- a/Janus/JanusMessages.cs +++ b/Janus/JanusMessages.cs @@ -179,9 +179,9 @@ namespace WebRtcVoice }} } // ============================================================== - public class SessionDestroyReq : JanusMessageReq + public class DestroySessionReq : JanusMessageReq { - public SessionDestroyReq() : base("destroy") + public DestroySessionReq() : base("destroy") { // Doesn't include the session ID because it is the URI } @@ -238,10 +238,18 @@ namespace WebRtcVoice { public DetachPluginReq() : base("detach") { - // Doesn't include the plugin ID because it is the URI + // Doesn't include the session ID or plugin ID because it is the URI } } // ============================================================== + public class HangupReq : JanusMessageReq + { + public HangupReq() : base("hangup") + { + // Doesn't include the session ID or plugin ID because it is the URI + } + } + // ============================================================== // Plugin messages are defined here as wrappers around OSDMap. // The ToJson() method is overridden to put the OSDMap into the // message body. @@ -426,6 +434,15 @@ namespace WebRtcVoice { } - public string sender { get { return m_message.ContainsKey("sender") ? m_message["sender"] : String.Empty; }} + public EventResp(JanusMessageResp pResp) : base(pResp.RawBody) + { + } + + public string sessionId { + get { return m_message.ContainsKey("session_id") ? m_message["session_id"].AsLong().ToString() : String.Empty; } + } + public string sender { + get { return m_message.ContainsKey("sender") ? m_message["sender"] : String.Empty; } + } } } diff --git a/Janus/JanusPlugin.cs b/Janus/JanusPlugin.cs index caec9b0..148208c 100644 --- a/Janus/JanusPlugin.cs +++ b/Janus/JanusPlugin.cs @@ -99,5 +99,30 @@ namespace WebRtcVoice return ret; } + + public virtual async Task Detach() + { + bool ret = false; + try + { + // We send the 'detach' message to the plugin URI + var resp = await _JanusSession.SendToJanus(new DetachPluginReq(), PluginUri); + if (resp is not null && resp.isSuccess) + { + m_log.DebugFormat("{0} Detach. Detached", LogHeader); + ret = true; + } + else + { + m_log.ErrorFormat("{0} Detach: failed", LogHeader); + } + } + catch (Exception e) + { + m_log.ErrorFormat("{0} Detach: exception {1}", LogHeader, e); + } + + return ret; + } } } \ No newline at end of file diff --git a/Janus/JanusRoom.cs b/Janus/JanusRoom.cs index 009ee09..150911d 100644 --- a/Janus/JanusRoom.cs +++ b/Janus/JanusRoom.cs @@ -61,7 +61,7 @@ namespace WebRtcVoice // 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); + var joinReq = new AudioBridgeJoinRoomReq(RoomId, pVSession.AgentId.ToString()); // m_log.DebugFormat("{0} JoinRoom. New joinReq for room {1}", LogHeader, RoomId); // joinReq.SetJsep("offer", cleanSdp); joinReq.SetJsep("offer", pVSession.Offer); @@ -72,7 +72,7 @@ namespace WebRtcVoice if (joinResp is not null && joinResp.AudioBridgeReturnCode == "joined") { m_log.DebugFormat("{0} JoinRoom. Joined room {1}", LogHeader, RoomId); - pVSession.JanusAttendeeId = joinResp.ParticipantId; + pVSession.ParticipantId = joinResp.ParticipantId; pVSession.Answer = joinResp.Jsep; ret = true; } @@ -88,24 +88,22 @@ namespace WebRtcVoice return ret; } - // The SDP coming from the client has some things that Janus doesn't like. - // In particular, the data section must be removed so the audio bridge - // gets an audio only offer. - private string CleanupSdp(string pSdp) + // TODO: this doesn't work. + // Not sure if it is needed. Janus generates Hangup events when the viewer leaves. + /* + public async Task Hangup(JanusViewerSession pAttendeeSession) { - string ret = pSdp; + bool ret = false; try { - string dataSection = "m=application [^\\r\\n]* webrtc-datachannel.*"; - ret = Regex.Replace(pSdp, dataSection, "", RegexOptions.Singleline); - m_log.DebugFormat("{0} CleanupSdp. cleaned={1}", LogHeader, ret); } catch (Exception e) { - m_log.ErrorFormat("{0} CleanupSdp. Exception {1}", LogHeader, e); + m_log.ErrorFormat("{0} LeaveRoom. Exception {1}", LogHeader, e); } return ret; } + */ public async Task LeaveRoom(JanusViewerSession pAttendeeSession) { @@ -113,7 +111,7 @@ namespace WebRtcVoice try { JanusMessageResp resp = await _AudioBridge.SendPluginMsg( - new AudioBridgeLeaveRoomReq(RoomId, pAttendeeSession.JanusAttendeeId)); + new AudioBridgeLeaveRoomReq(RoomId, pAttendeeSession.ParticipantId)); } catch (Exception e) { diff --git a/Janus/JanusSession.cs b/Janus/JanusSession.cs index 0b59a1e..b846f99 100644 --- a/Janus/JanusSession.cs +++ b/Janus/JanusSession.cs @@ -17,14 +17,8 @@ using System.Net.Mime; using System.Reflection; using System.Threading.Tasks; -using OpenSim.Framework; -using OpenSim.Services.Interfaces; -using OpenSim.Services.Base; - using OpenMetaverse.StructuredData; -using OpenMetaverse; -using Nini.Config; using log4net; namespace WebRtcVoice @@ -62,16 +56,16 @@ namespace WebRtcVoice public void Dispose() { + ClearEventSubscriptions(); + if (IsConnected) + { + _ = DestroySession(); + } if (_HttpClient is not null) { _HttpClient.Dispose(); _HttpClient = null; } - if (IsConnected) - { - // Close the session - - } } /// @@ -107,6 +101,32 @@ namespace WebRtcVoice return ret; } + public async Task DestroySession() + { + bool ret = false; + try + { + var resp = await SendToSession(new DestroySessionReq()); + if (resp is not null && resp.isSuccess) + { + // Note that setting SessionID to null will cause the long poll to exit + SessionId = String.Empty; + SessionUri = String.Empty; + m_log.DebugFormat("{0} DestroySession. Destroyed", LogHeader); + } + else + { + m_log.ErrorFormat("{0} DestroySession: failed", LogHeader); + } + } + catch (Exception e) + { + m_log.ErrorFormat("{0} DestroySession: exception {1}", LogHeader, e); + } + + return ret; + } + // ==================================================================== public async Task TrickleCandidates(JanusViewerSession pVSession, OSDArray pCandidates) { @@ -212,9 +232,12 @@ namespace WebRtcVoice { // Some messages are asynchronous and completed with an event m_log.DebugFormat("{0} SendToJanus: ack response {1}", LogHeader, respStr); - ret = await _OutstandingRequests[pReq.TransactionId].TaskCompletionSource.Task; - _OutstandingRequests.Remove(pReq.TransactionId); - + if (_OutstandingRequests.TryGetValue(pReq.TransactionId, out OutstandingRequest outstandingRequest)) + { + ret = await outstandingRequest.TaskCompletionSource.Task; + _OutstandingRequests.Remove(pReq.TransactionId); + } + // If there is no OutstandingRequest, the request was not waiting for an event or already processed } else { @@ -326,10 +349,10 @@ namespace WebRtcVoice } // ==================================================================== - public delegate void JanusEventHandler(JanusMessageResp pResp); + public delegate void JanusEventHandler(EventResp pResp); // Not all the events are used. CS0067 is to suppress the warning that the event is not used. - #pragma warning disable CS0067 + #pragma warning disable CS0067,CS0414 public event JanusEventHandler OnKeepAlive; public event JanusEventHandler OnServerInfo; public event JanusEventHandler OnTrickle; @@ -339,7 +362,19 @@ namespace WebRtcVoice public event JanusEventHandler OnEvent; public event JanusEventHandler OnJoined; public event JanusEventHandler OnLeaving; - #pragma warning restore CS0067 + #pragma warning restore CS0067,CS0414 + public void ClearEventSubscriptions() + { + OnKeepAlive = null; + OnServerInfo = null; + OnTrickle = null; + OnHangup = null; + OnDetached = null; + OnError = null; + OnEvent = null; + OnJoined = null; + OnLeaving = null; + } // ==================================================================== /// /// In the REST API, events are returned by a long poll. This @@ -359,11 +394,12 @@ namespace WebRtcVoice { _ = Task.Run(() => { + EventResp eventResp = new EventResp(resp); switch (resp.ReturnCode) { case "keepalive": // These should happen every 30 seconds - m_log.DebugFormat("{0} EventLongPoll: keepalive {1}", LogHeader, resp.ToString()); + // m_log.DebugFormat("{0} EventLongPoll: keepalive {1}", LogHeader, resp.ToString()); break; case "server_info": // Just info on the Janus instance @@ -381,7 +417,7 @@ namespace WebRtcVoice // got a trickle ICE candidate from Janus // this is for reverse communication from Janus to the client and we don't do that m_log.DebugFormat("{0} EventLongPoll: trickle {1}", LogHeader, resp.ToString()); - OnTrickle?.Invoke(resp); + OnTrickle?.Invoke(eventResp); break; case "webrtcup": // ICE and DTLS succeeded, and so Janus correctly established a PeerConnection with the user/application; @@ -392,12 +428,12 @@ namespace WebRtcVoice case "hangup": // The PeerConnection was closed, either by the user/application or by Janus itself; m_log.DebugFormat("{0} EventLongPoll: hangup {1}", LogHeader, resp.ToString()); - OnHangup?.Invoke(resp); + OnHangup?.Invoke(eventResp); break; case "detached": // a plugin asked the core to detach one of our handles m_log.DebugFormat("{0} EventLongPoll: event {1}", LogHeader, resp.ToString()); - OnDetached?.Invoke(resp); + OnDetached?.Invoke(eventResp); break; case "media": // Janus is receiving (receiving: true/false) audio/video (type: "audio/video") on this PeerConnection; @@ -419,7 +455,7 @@ namespace WebRtcVoice } else { - OnError?.Invoke(resp); + OnError?.Invoke(eventResp); m_log.ErrorFormat("{0} EventLongPoll: error with no transaction. {1}", LogHeader, resp.ToString()); } break; @@ -441,12 +477,12 @@ namespace WebRtcVoice break; case "joined": // Events for the audio bridge - OnJoined?.Invoke(resp); + OnJoined?.Invoke(eventResp); m_log.DebugFormat("{0} EventLongPoll: joined {1}", LogHeader, resp.ToString()); break; case "leaving": // Events for the audio bridge - OnLeaving?.Invoke(resp); + OnLeaving?.Invoke(eventResp); m_log.DebugFormat("{0} EventLongPoll: leaving {1}", LogHeader, resp.ToString()); break; default: diff --git a/Janus/JanusViewerSession.cs b/Janus/JanusViewerSession.cs index 1a61b38..2100445 100644 --- a/Janus/JanusViewerSession.cs +++ b/Janus/JanusViewerSession.cs @@ -10,39 +10,76 @@ // See the License for the specific language governing permissions and // limitations under the License. +using System.Threading.Tasks; + using OMV = OpenMetaverse; using OpenMetaverse.StructuredData; +using OpenMetaverse; namespace WebRtcVoice { public class JanusViewerSession : IVoiceViewerSession { // 'viewer_session' that is passed to and from the viewer - public IWebRtcVoiceService VoiceService { get; set; } + // IVoiceViewerSession.ViewerSessionID public string ViewerSessionID { get; set; } + // IVoiceViewerSession.VoiceService + public IWebRtcVoiceService VoiceService { get; set; } + // The Janus server keeps track of the user by this ID + // IVoiceViewerSession.VoiceServiceSessionId + public string VoiceServiceSessionId { get; set; } + // IVoiceViewerSession.RegionId + public UUID RegionId { get; set; } + // IVoiceViewerSession.AgentId + public UUID AgentId { get; set; } + + // Janus keeps track of the user by this ID + public int ParticipantId { get; set; } + + // Connections to the Janus server public JanusSession Session { get; set; } public JanusAudioBridge AudioBridge { get; set; } public JanusRoom Room { get; set; } + + // This keeps copies of the offer/answer incase we need to resend public string OfferOrig { get; set; } public string Offer { get; set; } // Contains "type" and "sdp" fields public OSDMap Answer { get; set; } - // The simulator has a GUID to identify the user - public string AgentId { get; set; } - - // The Janus server keeps track of the user by this ID - public int JanusAttendeeId; - public JanusViewerSession(IWebRtcVoiceService pVoiceService) { - VoiceService = pVoiceService; ViewerSessionID = OMV.UUID.Random().ToString(); - } - public JanusViewerSession(string pSessionID, IWebRtcVoiceService pVoiceService) - { VoiceService = pVoiceService; - ViewerSessionID = pSessionID; + } + public JanusViewerSession(string pViewerSessionID, IWebRtcVoiceService pVoiceService) + { + ViewerSessionID = pViewerSessionID; + VoiceService = pVoiceService; + } + + // Send the messages to the voice service to try and get rid of the session + // IVoiceViewerSession.Shutdown + public async Task Shutdown() + { + if (Room is not null) + { + var rm = Room; + Room = null; + await rm.LeaveRoom(this); + } + if (AudioBridge is not null) + { + var ab = AudioBridge; + AudioBridge = null; + await ab.Detach(); + } + if (Session is not null) + { + var s = Session; + Session = null; + await s.DestroySession(); + } } } } diff --git a/Janus/WebRtcJanusService.cs b/Janus/WebRtcJanusService.cs index ae0ebd8..3df3e0b 100644 --- a/Janus/WebRtcJanusService.cs +++ b/Janus/WebRtcJanusService.cs @@ -70,6 +70,7 @@ namespace WebRtcVoice { _log.DebugFormat("{0} Enabled", LogHeader); StartConnectionToJanus(); + RegisterConsoleCommands(); } } else @@ -110,9 +111,12 @@ namespace WebRtcVoice JanusAudioBridge audioBridge = new JanusAudioBridge(janusSession); janusSession.AddPlugin(audioBridge); + pViewerSession.VoiceServiceSessionId = janusSession.SessionId; pViewerSession.Session = janusSession; pViewerSession.AudioBridge = audioBridge; + janusSession.OnHangup += Handle_Hangup; + if (await audioBridge.Activate(_Config)) { _log.DebugFormat("{0} AudioBridgePluginHandle created", LogHeader); @@ -129,6 +133,29 @@ namespace WebRtcVoice } } + private void Handle_Hangup(EventResp pResp) + { + if (pResp is not null) + { + var sessionId = pResp.sessionId; + _log.DebugFormat("{0} Handle_Hangup: {1}, sessionId={2}", LogHeader, pResp.RawBody.ToString(), sessionId); + if (VoiceViewerSession.TryGetViewerSessionByVSSessionId(sessionId, out IVoiceViewerSession viewerSession)) + { + // There is a viewer session associated with this session + Task.Run(() => + { + VoiceViewerSession.RemoveViewerSession(viewerSession.ViewerSessionID); + // No need to wait for the session to be shutdown + _ = viewerSession.Shutdown(); + }); + } + else + { + _log.DebugFormat("{0} Handle_Hangup: no session found", LogHeader); + } + } + } + // The pRequest parameter is a straight conversion of the JSON request from the client. // This is the logic that takes the client's request and converts it into // operations on rooms in the audio bridge. @@ -181,7 +208,7 @@ namespace WebRtcVoice if (jsepType == "offer") { // The client is sending an offer. Find the right room and join it. - _log.DebugFormat("{0} ProvisionVoiceAccountRequest: jsep type={1} sdp={2}", LogHeader, jsepType, jsepSdp); + // _log.DebugFormat("{0} ProvisionVoiceAccountRequest: jsep type={1} sdp={2}", LogHeader, jsepType, jsepSdp); viewerSession.Room = await viewerSession.AudioBridge.SelectRoom(pScene.RegionInfo.RegionID.ToString(), channel_type, isSpacial, parcel_local_id, channel_id); if (viewerSession.Room is null) @@ -192,7 +219,7 @@ namespace WebRtcVoice else { viewerSession.Offer = jsepSdp; viewerSession.OfferOrig = jsepSdp; - viewerSession.AgentId = pUserID.ToString(); + viewerSession.AgentId = pUserID; if (await viewerSession.Room.JoinRoom(viewerSession)) { ret = new OSDMap @@ -294,20 +321,53 @@ namespace WebRtcVoice return ret; } + // This module should not be invoked with this signature + // IWebRtcVoiceService.ProvisionVoiceAccountRequest public Task ProvisionVoiceAccountRequest(OSDMap pRequest, UUID pUserID, IScene pScene) { throw new NotImplementedException(); } + // This module should not be invoked with this signature + // IWebRtcVoiceService.VoiceSignalingRequest public Task VoiceSignalingRequest(OSDMap pRequest, UUID pUserID, IScene pScene) { throw new NotImplementedException(); } // The viewer session object holds all the connection information to Janus. + // IWebRtcVoiceService.CreateViewerSession public IVoiceViewerSession CreateViewerSession(OSDMap pRequest, UUID pUserID, IScene pScene) { - return new JanusViewerSession(this); + return new JanusViewerSession(this) + { + AgentId = pUserID, + RegionId = pScene.RegionInfo.RegionID + }; } + + // ====================================================================================================== + private void RegisterConsoleCommands() + { + MainConsole.Instance.Commands.AddCommand("Webrtc", false, "janus info", + "janus info", + "Show Janus server information", + HandleJanusInfo); + // List rooms + // List participants in a room + } + + private void HandleJanusInfo(string module, string[] cmdparms) + { + WriteOut("{0} Janus server: {1}", LogHeader, _JanusServerURI); + } + + private void WriteOut(string msg, params object[] args) + { + // m_log.InfoFormat(msg, args); + MainConsole.Instance.Output(msg, args); + } + + } } diff --git a/WebRtcVoice/IVoiceViewerSession.cs b/WebRtcVoice/IVoiceViewerSession.cs index 2550d00..294a64a 100644 --- a/WebRtcVoice/IVoiceViewerSession.cs +++ b/WebRtcVoice/IVoiceViewerSession.cs @@ -10,26 +10,25 @@ // See the License for the specific language governing permissions and // limitations under the License. -using System; -using System.Reflection; - -using OpenSim.Server.Base; -using OpenSim.Services.Base; -using OpenSim.Services.Interfaces; - -using OpenMetaverse.StructuredData; -using OpenMetaverse; -using OpenSim.Framework; - -using Nini.Config; -using log4net; using System.Threading.Tasks; +using OpenMetaverse; namespace WebRtcVoice { public interface IVoiceViewerSession { + // This ID is passed to and from the viewer to identify the session public string ViewerSessionID { get; set; } public IWebRtcVoiceService VoiceService { get; set; } + // THis ID is passed between us and the voice service to idetify the session + public string VoiceServiceSessionId { get; set; } + // The UUID of the region that is being connected to + public UUID RegionId { get; set; } + + // The simulator has a GUID to identify the user + public UUID AgentId { get; set; } + + // Disconnect the connection to the voice service for this session + public Task Shutdown(); } } diff --git a/WebRtcVoice/VoiceViewerSession.cs b/WebRtcVoice/VoiceViewerSession.cs new file mode 100644 index 0000000..5233fdc --- /dev/null +++ b/WebRtcVoice/VoiceViewerSession.cs @@ -0,0 +1,75 @@ +// Copyright 2024 Robert Adams (misterblue@misterblue.com) +// +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +using System.Linq; +using System.Collections.Generic; + +using OpenMetaverse; + +namespace WebRtcVoice +{ + public static class VoiceViewerSession + { + // ===================================================================== + // 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) + { + lock (ViewerSessions) + { + return ViewerSessions.TryGetValue(pViewerSessionId, out pViewerSession); + } + } + // public static bool TryGetViewerSessionByAgentId(UUID pAgentId, out IVoiceViewerSession pViewerSession) + public static bool TryGetViewerSessionByAgentId(UUID pAgentId, out IEnumerable> pViewerSessions) + { + lock (ViewerSessions) + { + pViewerSessions = ViewerSessions.Where(v => v.Value.AgentId == pAgentId); + return pViewerSessions.Count() > 0; + } + } + // Get a viewer session by the VoiceService session ID + public static bool TryGetViewerSessionByVSSessionId(string pVSSessionId, out IVoiceViewerSession pViewerSession) + { + lock (ViewerSessions) + { + var sessions = ViewerSessions.Where(v => v.Value.VoiceServiceSessionId == pVSSessionId); + if (sessions.Count() > 0) + { + pViewerSession = sessions.First().Value; + return true; + } + pViewerSession = null; + return false; + } + } + public static void AddViewerSession(IVoiceViewerSession pSession) + { + lock (ViewerSessions) + { + ViewerSessions[pSession.ViewerSessionID] = pSession; + } + } + public static void RemoveViewerSession(string pSessionId) + { + lock (ViewerSessions) + { + ViewerSessions.Remove(pSessionId); + } + } + + } +} + diff --git a/WebRtcVoice/WebRtcVoice.csproj b/WebRtcVoice/WebRtcVoice.csproj index 84b585f..f7be691 100644 --- a/WebRtcVoice/WebRtcVoice.csproj +++ b/WebRtcVoice/WebRtcVoice.csproj @@ -114,5 +114,8 @@ Code + + Code + diff --git a/WebRtcVoiceRegionModule/WebRtcVoiceRegionModule.cs b/WebRtcVoiceRegionModule/WebRtcVoiceRegionModule.cs index 87d45a5..503d097 100644 --- a/WebRtcVoiceRegionModule/WebRtcVoiceRegionModule.cs +++ b/WebRtcVoiceRegionModule/WebRtcVoiceRegionModule.cs @@ -99,6 +99,7 @@ namespace WebRtcVoice { OnRegisterCaps(scene, agentID, caps); }; + } } @@ -212,7 +213,7 @@ namespace WebRtcVoice m_log.DebugFormat("{0}[ProvisionVoice]: Request for {1}", logHeader, agentID.ToString()); - // Deserialize the request + // Deserialize the request. Convert the LLSDXml to OSD for our use OSDMap map = null; using (Stream inputStream = request.InputStream) { @@ -234,6 +235,7 @@ namespace WebRtcVoice return; } + // Get the voice service. If it doesn't exist, return an error. IWebRtcVoiceService voiceService = scene.RequestModuleInterface(); if (voiceService is null) { @@ -253,11 +255,14 @@ namespace WebRtcVoice } } + // The checks passed. Send the request to the voice service. OSDMap resp = voiceService.ProvisionVoiceAccountRequest(map, agentID, scene).Result; m_log.DebugFormat("{0}[ProvisionVoice]: response: {1}", logHeader, resp.ToString()); // TODO: check for erros and package the response + + // Convert the OSD to LLSDXml for the response string xmlResp = OSDParser.SerializeLLSDXmlString(resp); response.StatusCode = (int)HttpStatusCode.OK; @@ -276,7 +281,7 @@ namespace WebRtcVoice m_log.DebugFormat("{0}[VoiceSignaling]: Request for {1}", logHeader, agentID.ToString()); - // Deserialize the request + // Deserialize the request. Convert the LLSDXml to OSD for our use OSDMap map = null; using (Stream inputStream = request.InputStream) { @@ -316,17 +321,16 @@ namespace WebRtcVoice return; } - m_log.DebugFormat("{0}[VoiceSignalingRequest]: message: {1}", logHeader, map.ToString()); OSDMap resp = voiceService.VoiceSignalingRequest(map, agentID, scene).Result; // TODO: check for erros and package the response - m_log.DebugFormat("{0}[VoiceSignalingRequest]: message: {1}", logHeader, map.ToString()); response.StatusCode = (int)HttpStatusCode.OK; response.RawBuffer = Util.UTF8.GetBytes(""); return; } + // NOTE NOTE!! This is code from the FreeSwitch module. It is not clear if this is correct for WebRtc. /// /// Callback for a client request for ParcelVoiceInfo /// @@ -425,6 +429,7 @@ namespace WebRtcVoice } } + // NOTE NOTE!! This is code from the FreeSwitch module. It is not clear if this is correct for WebRtc. // Not sure what this Uri is for. Is this FreeSwitch specific? // TODO: is this useful for WebRtc? private string ChannelUri(Scene scene, LandData land) diff --git a/WebRtcVoiceServiceModule/WebRtcVoiceServiceModule.cs b/WebRtcVoiceServiceModule/WebRtcVoiceServiceModule.cs index 7dcd94b..d1f6ac9 100644 --- a/WebRtcVoiceServiceModule/WebRtcVoiceServiceModule.cs +++ b/WebRtcVoiceServiceModule/WebRtcVoiceServiceModule.cs @@ -11,17 +11,18 @@ // limitations under the License. using System; +using System.Linq; using System.Collections.Generic; using System.Reflection; using System.Threading.Tasks; -using OpenSim.Server.Base; +using OpenSim.Framework; using OpenSim.Region.Framework.Scenes; using OpenSim.Region.Framework.Interfaces; +using OpenSim.Server.Base; -using OpenMetaverse.StructuredData; using OpenMetaverse; -using OpenSim.Framework; +using OpenMetaverse.StructuredData; using Mono.Addins; @@ -54,20 +55,6 @@ namespace WebRtcVoice private IWebRtcVoiceService m_spacialVoiceService; private IWebRtcVoiceService m_nonSpacialVoiceService; - // ===================================================================== - public static Dictionary ViewerSessions = new Dictionary(); - public static bool TryGetViewerSession(string pSessionId, out IVoiceViewerSession pViewerSession) - { - return ViewerSessions.TryGetValue(pSessionId, out pViewerSession); - } - public static void AddViewerSession(IVoiceViewerSession pSession) - { - ViewerSessions[pSession.ViewerSessionID] = pSession; - } - public static void RemoveViewerSession(string pSessionId) - { - ViewerSessions.Remove(pSessionId); - } // ===================================================================== // ISharedRegionModule.Initialize @@ -153,6 +140,19 @@ namespace WebRtcVoice { m_log.DebugFormat("{0} Adding WebRtcVoiceService to region {1}", LogHeader, scene.Name); scene.RegisterModuleInterface(this); + + // TODO: figure out what events we care about + // When new client (child or root) is added to scene, before OnClientLogin + // scene.EventManager.OnNewClient += Event_OnNewClient; + // When client is added on login. + // scene.EventManager.OnClientLogin += Event_OnClientLogin; + // New presence is added to scene. Child, root, and NPC. See Scene.AddNewAgent() + // scene.EventManager.OnNewPresence += Event_OnNewPresence; + // scene.EventManager.OnRemovePresence += Event_OnRemovePresence; + // update to client position (either this or 'significant') + // scene.EventManager.OnClientMovement += Event_OnClientMovement; + // "significant" update to client position + // scene.EventManager.OnSignificantClientMovement += Event_OnSignificantClientMovement; } } @@ -171,6 +171,22 @@ namespace WebRtcVoice { } + // ===================================================================== + // Thought about doing this but currently relying on the voice service + // event ("hangup") to remove the viewer session. + private void Event_OnRemovePresence(UUID pAgentID) + { + // When a presence is removed, remove the viewer sessions for that agent + IEnumerable> vSessions; + if (VoiceViewerSession.TryGetViewerSessionByAgentId(pAgentID, out vSessions)) + { + foreach(KeyValuePair v in vSessions) + { + VoiceViewerSession.RemoveViewerSession(v.Key); + v.Value.Shutdown(); + } + } + } // ===================================================================== // IWebRtcVoiceService @@ -183,11 +199,7 @@ namespace WebRtcVoice { // request has a viewer session. Use that to find the voice service string viewerSessionId = pRequest["viewer_session"].AsString(); - if (TryGetViewerSession(viewerSessionId, out vSession)) - { - response = await vSession.VoiceService.ProvisionVoiceAccountRequest(vSession, pRequest, pUserID, pScene); - } - else + if (!VoiceViewerSession.TryGetViewerSession(viewerSessionId, out vSession)) { m_log.ErrorFormat("{0} ProvisionVoiceAccountRequest: viewer session {1} not found", LogHeader, viewerSessionId); } @@ -200,15 +212,15 @@ namespace WebRtcVoice string channelType = pRequest["channel_type"].AsString(); if (channelType == "local") { + // TODO: check if this userId is making a new session (case that user is reconnecting) vSession = m_spacialVoiceService.CreateViewerSession(pRequest, pUserID, pScene); - AddViewerSession(vSession); - response = await m_spacialVoiceService.ProvisionVoiceAccountRequest(vSession, pRequest, pUserID, pScene); + VoiceViewerSession.AddViewerSession(vSession); } else { + // TODO: check if this userId is making a new session (case that user is reconnecting) vSession = m_nonSpacialVoiceService.CreateViewerSession(pRequest, pUserID, pScene); - AddViewerSession(vSession); - response = await m_nonSpacialVoiceService.ProvisionVoiceAccountRequest(vSession, pRequest, pUserID, pScene); + VoiceViewerSession.AddViewerSession(vSession); } } else @@ -216,6 +228,10 @@ namespace WebRtcVoice m_log.ErrorFormat("{0} ProvisionVoiceAccountRequest: no channel_type in request", LogHeader); } } + if (vSession is not null) + { + response = await vSession.VoiceService.ProvisionVoiceAccountRequest(vSession, pRequest, pUserID, pScene); + } return response; } @@ -228,7 +244,7 @@ namespace WebRtcVoice { // request has a viewer session. Use that to find the voice service string viewerSessionId = pRequest["viewer_session"].AsString(); - if (TryGetViewerSession(viewerSessionId, out vSession)) + if (VoiceViewerSession.TryGetViewerSession(viewerSessionId, out vSession)) { response = await vSession.VoiceService.VoiceSignalingRequest(vSession, pRequest, pUserID, pScene); }