mirror of
https://github.com/opensim/opensim.git
synced 2026-05-13 01:46:07 +08:00
make sure we close parcel voice channels when user leaves region
This commit is contained in:
@@ -53,6 +53,8 @@ namespace osWebRtcVoice
|
||||
// IVoiceViewerSession.AgentId
|
||||
public OMV.UUID AgentId { get; set; }
|
||||
|
||||
public IVoiceViewerSession.VFlags Flags { get; set; }
|
||||
|
||||
// Janus keeps track of the user by this ID
|
||||
public long ParticipantId { get; set; }
|
||||
|
||||
|
||||
@@ -227,24 +227,6 @@ namespace osWebRtcVoice
|
||||
return $"flow={pFlowId}, viewer_session={pViewerSession?.ViewerSessionID ?? "<none>"}";
|
||||
}
|
||||
|
||||
private async Task EnforceRejoinCooldown(UUID pAgentId, JanusViewerSession pViewerSession, long pFlowId)
|
||||
{
|
||||
if (_RejoinCooldownMs <= 0)
|
||||
return;
|
||||
|
||||
if (_LastDisconnectByAgent.TryGetValue(pAgentId, out DateTime lastDisconnectUtc))
|
||||
{
|
||||
int elapsedMs = (int)(DateTime.UtcNow - lastDisconnectUtc).TotalMilliseconds;
|
||||
int waitMs = _RejoinCooldownMs - elapsedMs;
|
||||
if (waitMs > 0)
|
||||
{
|
||||
_log.DebugFormat("{0} ProvisionVoiceAccountRequest: applying rejoin cooldown {1}ms ({2})",
|
||||
LogHeader, waitMs, FlowTag(pFlowId, pViewerSession));
|
||||
await Task.Delay(waitMs);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Disconnect the viewer session. This is called when the viewer logs out or hangs up.
|
||||
|
||||
private void DisconnectViewerSession(JanusViewerSession pViewerSession, string pReason)
|
||||
|
||||
@@ -25,6 +25,7 @@
|
||||
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
*/
|
||||
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
using OMV = OpenMetaverse;
|
||||
@@ -37,6 +38,16 @@ namespace osWebRtcVoice
|
||||
/// </summary>
|
||||
public interface IVoiceViewerSession
|
||||
{
|
||||
[Flags]
|
||||
public enum VFlags: uint
|
||||
{
|
||||
None = 0,
|
||||
IsParcel = 1,
|
||||
IsEstate = IsParcel | 2,
|
||||
IsAdmin = 4,
|
||||
IsChildAgent = 8,
|
||||
}
|
||||
|
||||
// This ID is passed to and from the viewer to identify the session
|
||||
public string ViewerSessionID { get; set; }
|
||||
public IWebRtcVoiceService VoiceService { get; set; }
|
||||
@@ -47,6 +58,7 @@ namespace osWebRtcVoice
|
||||
|
||||
// The simulator has a GUID to identify the user
|
||||
public OMV.UUID AgentId { get; set; }
|
||||
public VFlags Flags { get; set; }
|
||||
|
||||
// Disconnect the connection to the voice service for this session
|
||||
public Task Shutdown();
|
||||
|
||||
@@ -49,6 +49,7 @@ namespace osWebRtcVoice
|
||||
public string VoiceServiceSessionId { get; set; }
|
||||
public UUID RegionId { get; set; }
|
||||
public UUID AgentId { get; set; }
|
||||
public IVoiceViewerSession.VFlags Flags { get; set; }
|
||||
|
||||
// =====================================================================
|
||||
// ViewerSessions hold the connection information for the client connection through to the voice service.
|
||||
|
||||
@@ -81,7 +81,7 @@ namespace osWebRtcVoice
|
||||
public IVoiceViewerSession CreateViewerSession(OSDMap pRequest, UUID pUserID, UUID pSceneID)
|
||||
{
|
||||
m_log.Debug($"{LogHeader} CreateViewerSession");
|
||||
return new VoiceViewerSession(this, pSceneID, pUserID);
|
||||
return new VoiceViewerSession(this, pSceneID, pUserID);
|
||||
}
|
||||
|
||||
public OSDMap ProvisionVoiceAccountRequest(OSDMap pRequest, UUID pUserID, UUID pSceneID)
|
||||
|
||||
@@ -166,6 +166,11 @@ namespace osWebRtcVoice
|
||||
OnRegisterCaps(scene, agentID, caps);
|
||||
};
|
||||
|
||||
scene.EventManager.OnRemovePresence += delegate (UUID agentID)
|
||||
{
|
||||
OnRemovePresence(scene, agentID);
|
||||
};
|
||||
|
||||
ISimulatorFeaturesModule simFeatures = scene.RequestModuleInterface<ISimulatorFeaturesModule>();
|
||||
simFeatures?.AddFeature("VoiceServerType", OSD.FromString("webrtc"));
|
||||
}
|
||||
@@ -188,42 +193,52 @@ namespace osWebRtcVoice
|
||||
get { return null; }
|
||||
}
|
||||
|
||||
// =====================================================================
|
||||
// Thought about doing this but currently relying on the voice service
|
||||
// event ("hangup") to remove the viewer session.
|
||||
/*
|
||||
private void Event_OnRemovePresence(UUID pAgentID)
|
||||
private static void OnRemovePresence(Scene pScene, UUID pAgentID)
|
||||
{
|
||||
// When a presence is removed, remove the viewer sessions for that agent
|
||||
IEnumerable<KeyValuePair<string, IVoiceViewerSession>> vSessions;
|
||||
if (VoiceViewerSession.TryGetViewerSessionByAgentId(pAgentID, out vSessions))
|
||||
// When a presence is removed, remove the parcel viewer sessions for that agent
|
||||
List<IVoiceViewerSession> toremove = [];
|
||||
if (VoiceViewerSession.TryGetViewerSessionsByAgentAndRegion(pAgentID, pScene.RegionInfo.RegionID, out IEnumerable<KeyValuePair<string, IVoiceViewerSession>> vSessions))
|
||||
{
|
||||
foreach(KeyValuePair<string, IVoiceViewerSession> v in vSessions)
|
||||
{
|
||||
m_log.DebugFormat("{0} Event_OnRemovePresence: removing viewer session {1}", LogHeader, v.Key);
|
||||
VoiceViewerSession.RemoveViewerSession(v.Key);
|
||||
v.Value.Shutdown();
|
||||
if((v.Value.Flags & IVoiceViewerSession.VFlags.IsParcel) != 0)
|
||||
toremove.Add(v.Value);
|
||||
}
|
||||
|
||||
if(toremove.Count > 0)
|
||||
{
|
||||
foreach(IVoiceViewerSession v in toremove)
|
||||
VoiceViewerSession.RemoveViewerSession(v.ViewerSessionID);
|
||||
|
||||
Util.FireAndForget( x =>
|
||||
{
|
||||
List<IVoiceViewerSession> toremoveas = toremove;
|
||||
foreach(IVoiceViewerSession v in toremoveas)
|
||||
try
|
||||
{
|
||||
OSDMap vreq = new()
|
||||
{
|
||||
{ "logout" , true},
|
||||
{ "viewer_session" , v.ViewerSessionID}
|
||||
};
|
||||
v.VoiceService.ProvisionVoiceAccountRequest(v, vreq , v.AgentId, v.RegionId);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
m_log.Debug(
|
||||
$"{LogHeader} OnRemovePresence: shutdown failed for viewer_session {v.ViewerSessionID}: {ex.Message}");
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
*/
|
||||
|
||||
private static List<KeyValuePair<string, IVoiceViewerSession>> GetViewerSessionsByAgentAndScene(UUID pAgentID, UUID pSceneID)
|
||||
{
|
||||
List<KeyValuePair<string, IVoiceViewerSession>> matches = [];
|
||||
if (VoiceViewerSession.TryGetViewerSessionsByAgentId(pAgentID, out IEnumerable<KeyValuePair<string, IVoiceViewerSession>> vSessions))
|
||||
{
|
||||
foreach (KeyValuePair<string, IVoiceViewerSession> v in vSessions)
|
||||
matches.Add(v);
|
||||
}
|
||||
return matches;
|
||||
}
|
||||
|
||||
|
||||
private static void CleanupDuplicateSessions(UUID pAgentID, UUID pSceneID, string pKeepViewerSessionId)
|
||||
{
|
||||
if(VoiceViewerSession.TryGetViewerSessionsByAgentAndRegion(pAgentID, pSceneID, out IEnumerable<KeyValuePair<string, IVoiceViewerSession>> candidates))
|
||||
{
|
||||
bool noskip = string.IsNullOrEmpty(pKeepViewerSessionId);
|
||||
List<IVoiceViewerSession> toremove = [];
|
||||
foreach (KeyValuePair<string, IVoiceViewerSession> candidate in candidates)
|
||||
{
|
||||
if (noskip && candidate.Key == pKeepViewerSessionId)
|
||||
@@ -231,25 +246,37 @@ namespace osWebRtcVoice
|
||||
|
||||
m_log.Warn(
|
||||
$"{LogHeader} CleanupDuplicateSessions: removing stale viewer_session {candidate.Key} for agent {pAgentID}, scene {pSceneID}");
|
||||
toremove.Add(candidate.Value);
|
||||
}
|
||||
foreach(IVoiceViewerSession v in toremove)
|
||||
VoiceViewerSession.RemoveViewerSession(v.ViewerSessionID);
|
||||
|
||||
VoiceViewerSession.RemoveViewerSession(candidate.Key);
|
||||
_ = Task.Run(async () =>
|
||||
if(toremove.Count > 0)
|
||||
{
|
||||
Util.FireAndForget( x =>
|
||||
{
|
||||
try
|
||||
foreach(IVoiceViewerSession v in toremove)
|
||||
{
|
||||
await candidate.Value.Shutdown();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
m_log.Debug(
|
||||
$"{LogHeader} CleanupDuplicateSessions: shutdown failed for viewer_session {candidate.Key}: {ex.Message}");
|
||||
try
|
||||
{
|
||||
OSDMap vreq = new()
|
||||
{
|
||||
{ "logout" , true},
|
||||
{ "viewer_session" , v.ViewerSessionID}
|
||||
};
|
||||
v.VoiceService.ProvisionVoiceAccountRequest(v, vreq , v.AgentId, v.RegionId);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
m_log.Debug(
|
||||
$"{LogHeader} CleanupDuplicateSessions: shutdown failed for viewer_session {v.ViewerSessionID}: {ex.Message}");
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// <summary>
|
||||
// OnRegisterCaps is invoked via the scene.EventManager
|
||||
// everytime OpenSim hands out capabilities to a client
|
||||
@@ -357,7 +384,8 @@ namespace osWebRtcVoice
|
||||
IVoiceViewerSession v = kvp.Value;
|
||||
if(v is null)
|
||||
continue;
|
||||
vreq["viewer_session"] = v.VoiceServiceSessionId;
|
||||
vreq["viewer_session"] = v.ViewerSessionID;
|
||||
VoiceViewerSession.RemoveViewerSession(v.ViewerSessionID);
|
||||
v.VoiceService.ProvisionVoiceAccountRequest(v, vreq , agentID, scene.RegionInfo.RegionID);
|
||||
}
|
||||
}
|
||||
@@ -367,18 +395,19 @@ namespace osWebRtcVoice
|
||||
return ;
|
||||
}
|
||||
|
||||
OSDMap logoutresp = null;
|
||||
if (VoiceViewerSession.TryGetViewerSession(viewerSessionId, out vSession))
|
||||
{
|
||||
VoiceViewerSession.RemoveViewerSession(viewerSessionId);
|
||||
OSDMap logoutresp = vSession.VoiceService.ProvisionVoiceAccountRequest(vSession, map, agentID, scene.RegionInfo.RegionID);
|
||||
logoutresp ??= new OSDMap() {
|
||||
{ "response", "error" },
|
||||
{ "message", "Logout session not found" } };
|
||||
|
||||
response.RawBuffer = OSDParser.SerializeLLSDXmlBytes(logoutresp);
|
||||
response.StatusCode = (int)HttpStatusCode.OK;
|
||||
return ;
|
||||
logoutresp = vSession.VoiceService.ProvisionVoiceAccountRequest(vSession, map, agentID, scene.RegionInfo.RegionID);
|
||||
}
|
||||
logoutresp ??= new OSDMap() {
|
||||
{ "response", "error" },
|
||||
{ "message", "Logout session not found" } };
|
||||
|
||||
response.RawBuffer = OSDParser.SerializeLLSDXmlBytes(logoutresp);
|
||||
response.StatusCode = (int)HttpStatusCode.OK;
|
||||
return ;
|
||||
}
|
||||
|
||||
// request has a viewer session. Use that to find the voice service
|
||||
@@ -387,7 +416,6 @@ namespace osWebRtcVoice
|
||||
CleanupDuplicateSessions(agentID, scene.RegionInfo.RegionID, viewerSessionId);
|
||||
}
|
||||
}
|
||||
|
||||
else
|
||||
{
|
||||
//no session id.. new channel?
|
||||
@@ -395,6 +423,16 @@ namespace osWebRtcVoice
|
||||
{
|
||||
CleanupDuplicateSessions(agentID, scene.RegionInfo.RegionID, null);
|
||||
|
||||
if(!scene.TryGetScenePresence(agentID, out ScenePresence sp))
|
||||
{
|
||||
m_log.Debug($"{LogHeader}[ProvisionVoice]:avatar not found");
|
||||
response.RawBuffer = llsdUndefAnswerBytes;
|
||||
response.StatusCode = (int)HttpStatusCode.NotFound;
|
||||
return;
|
||||
}
|
||||
|
||||
IVoiceViewerSession.VFlags flags = IVoiceViewerSession.VFlags.None;
|
||||
|
||||
//do fully not trust viewers voice parcel requests
|
||||
if (channelType == "local")
|
||||
{
|
||||
@@ -413,14 +451,6 @@ namespace osWebRtcVoice
|
||||
return;
|
||||
}
|
||||
|
||||
if(!scene.TryGetScenePresence(agentID, out ScenePresence sp))
|
||||
{
|
||||
m_log.Debug($"{LogHeader}[ProvisionVoice]:avatar not found");
|
||||
response.RawBuffer = llsdUndefAnswerBytes;
|
||||
response.StatusCode = (int)HttpStatusCode.NotFound;
|
||||
return;
|
||||
}
|
||||
|
||||
if(map.TryGetInt("parcel_local_id", out int parcelID))
|
||||
{
|
||||
ILandObject parcel = scene.LandChannel.GetLandObject(parcelID);
|
||||
@@ -430,7 +460,7 @@ namespace osWebRtcVoice
|
||||
response.StatusCode = (int)HttpStatusCode.NotFound;
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
LandData land = parcel.LandData;
|
||||
if (land == null)
|
||||
{
|
||||
@@ -453,25 +483,57 @@ namespace osWebRtcVoice
|
||||
// request and return the appropriate voice credentials for the estate channel
|
||||
// instead of a parcel channel
|
||||
map.Remove("parcel_local_id"); // estate channel
|
||||
flags = IVoiceViewerSession.VFlags.IsEstate;
|
||||
}
|
||||
else if(parcel.IsRestrictedFromLand(agentID) || parcel.IsBannedFromLand(agentID))
|
||||
else
|
||||
{
|
||||
// check Z distance?
|
||||
m_log.Debug($"{LogHeader}[ProvisionVoice]:agent not allowed on parcel");
|
||||
response.RawBuffer = llsdUndefAnswerBytes;
|
||||
response.StatusCode = (int)HttpStatusCode.Forbidden;
|
||||
return;
|
||||
if(parcel.IsRestrictedFromLand(agentID) || parcel.IsBannedFromLand(agentID))
|
||||
{
|
||||
// check Z distance?
|
||||
m_log.Debug($"{LogHeader}[ProvisionVoice]:agent not allowed on parcel");
|
||||
response.RawBuffer = llsdUndefAnswerBytes;
|
||||
response.StatusCode = (int)HttpStatusCode.Forbidden;
|
||||
return;
|
||||
}
|
||||
flags = parcel.OwnerID.Equals(agentID) ?
|
||||
IVoiceViewerSession.VFlags.IsAdmin | IVoiceViewerSession.VFlags.IsParcel :
|
||||
IVoiceViewerSession.VFlags.IsParcel;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
flags = IVoiceViewerSession.VFlags.IsEstate;
|
||||
}
|
||||
|
||||
// TODO: check if this userId is making a new session (case that user is reconnecting)
|
||||
vSession = m_spatialVoiceService.CreateViewerSession(map, agentID, scene.RegionInfo.RegionID);
|
||||
if(vSession != null) VoiceViewerSession.AddViewerSession(vSession);
|
||||
if(vSession != null)
|
||||
{
|
||||
if(sp.IsChildAgent)
|
||||
flags |= IVoiceViewerSession.VFlags.IsChildAgent;
|
||||
else if(scene.Permissions.IsEstateManager(agentID))
|
||||
flags |= IVoiceViewerSession.VFlags.IsAdmin;
|
||||
vSession.Flags = flags;
|
||||
VoiceViewerSession.AddViewerSession(vSession);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// TODO: check if this userId is making a new session (case that user is reconnecting)
|
||||
if(sp.IsChildAgent)
|
||||
{
|
||||
// check Z distance?
|
||||
m_log.Debug($"{LogHeader}[ProvisionVoice]:child agent request non local voice");
|
||||
response.RawBuffer = llsdUndefAnswerBytes;
|
||||
response.StatusCode = (int)HttpStatusCode.Forbidden;
|
||||
return;
|
||||
}
|
||||
|
||||
vSession = m_nonSpatialVoiceService.CreateViewerSession(map, agentID, scene.RegionInfo.RegionID);
|
||||
if(vSession != null) VoiceViewerSession.AddViewerSession(vSession);
|
||||
if(vSession != null)
|
||||
{
|
||||
vSession.Flags = IVoiceViewerSession.VFlags.IsAdmin;
|
||||
VoiceViewerSession.AddViewerSession(vSession);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -496,7 +558,6 @@ namespace osWebRtcVoice
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
public void VoiceSignalingRequest(IOSHttpRequest request, IOSHttpResponse response, UUID agentID, Scene scene)
|
||||
{
|
||||
if(request.HttpMethod != "POST")
|
||||
@@ -602,6 +663,23 @@ namespace osWebRtcVoice
|
||||
return;
|
||||
}
|
||||
|
||||
string servertype = null;
|
||||
if(reqmap.TryGetOSDMap("alt_params", out OSDMap altparams))
|
||||
{
|
||||
if(!altparams.TryGetString("voice_server_type", out servertype))
|
||||
_ = altparams.TryGetString("preferred_voice_server_type", out servertype);
|
||||
}
|
||||
|
||||
if(!string.IsNullOrEmpty(servertype))
|
||||
{
|
||||
if(!servertype.Equals("webrtc", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
response.RawBuffer = llsdUndefAnswerBytes;
|
||||
response.StatusCode = (int)HttpStatusCode.OK;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
switch (method.ToLower())
|
||||
{
|
||||
// Several different method requests that we don't know how to handle.
|
||||
@@ -643,6 +721,10 @@ namespace osWebRtcVoice
|
||||
response.StatusCode = (int)HttpStatusCode.OK;
|
||||
}
|
||||
break;
|
||||
case "call":
|
||||
m_log.Debug($"{LogHeader}: ChatSessionRequest call: {reqmap}");
|
||||
response.StatusCode = (int)HttpStatusCode.BadRequest;
|
||||
break;
|
||||
default:
|
||||
response.StatusCode = (int)HttpStatusCode.BadRequest;
|
||||
break;
|
||||
|
||||
@@ -133,30 +133,7 @@ namespace osWebRtcVoice
|
||||
}
|
||||
|
||||
|
||||
private static List<KeyValuePair<string, IVoiceViewerSession>> GetViewerSessionsByAgentAndScene(UUID pAgentID, UUID pSceneID)
|
||||
{
|
||||
List<KeyValuePair<string, IVoiceViewerSession>> matches = [];
|
||||
if (VoiceViewerSession.TryGetViewerSessionsByAgentId(pAgentID, out IEnumerable<KeyValuePair<string, IVoiceViewerSession>> vSessions))
|
||||
{
|
||||
foreach (KeyValuePair<string, IVoiceViewerSession> v in vSessions)
|
||||
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 void CleanupDuplicateSessions(UUID pAgentID, UUID pSceneID, string pKeepViewerSessionId)
|
||||
private static void CleanupDuplicateSessions(UUID pAgentID, UUID pSceneID, string pKeepViewerSessionId)
|
||||
{
|
||||
if(VoiceViewerSession.TryGetViewerSessionsByAgentAndRegion(pAgentID, pSceneID, out IEnumerable<KeyValuePair<string, IVoiceViewerSession>> candidates))
|
||||
{
|
||||
@@ -211,32 +188,25 @@ namespace osWebRtcVoice
|
||||
IVoiceViewerSession v = kvp.Value;
|
||||
if(v is null)
|
||||
continue;
|
||||
vreq["viewer_session"] = v.VoiceServiceSessionId;
|
||||
vreq["viewer_session"] = v.ViewerSessionID;
|
||||
|
||||
VoiceViewerSession.RemoveViewerSession(v.ViewerSessionID);
|
||||
v.VoiceService.ProvisionVoiceAccountRequest(v, vreq , pUserID, pSceneID);
|
||||
}
|
||||
//return new OSDMap {{ "response", "closed" }};
|
||||
}
|
||||
/*
|
||||
else
|
||||
{
|
||||
return new OSDMap
|
||||
{
|
||||
{ "response", "error" },
|
||||
{ "message", "Unable to provision voice session not found)" }
|
||||
};
|
||||
}
|
||||
*/
|
||||
|
||||
return new OSDMap {{ "response", "closed" }};
|
||||
}
|
||||
|
||||
OSDMap resp = null;
|
||||
if (VoiceViewerSession.TryGetViewerSession(viewerSessionId, out vSession))
|
||||
{
|
||||
VoiceViewerSession.RemoveViewerSession(viewerSessionId);
|
||||
OSDMap resp = vSession.VoiceService.ProvisionVoiceAccountRequest(vSession, pRequest, pUserID, pSceneID);
|
||||
return resp ?? new OSDMap() {
|
||||
{ "response", "error" },
|
||||
{ "message", "Logout session not found" } };
|
||||
resp = vSession.VoiceService.ProvisionVoiceAccountRequest(vSession, pRequest, pUserID, pSceneID);
|
||||
}
|
||||
return resp ?? new OSDMap() {
|
||||
{ "response", "error" },
|
||||
{ "message", "Logout session not found" } };
|
||||
}
|
||||
|
||||
// request has a viewer session. Use that to find the voice service
|
||||
@@ -256,7 +226,14 @@ namespace osWebRtcVoice
|
||||
{
|
||||
// TODO: check if this userId is making a new session (case that user is reconnecting)
|
||||
vSession = m_spatialVoiceService.CreateViewerSession(pRequest, pUserID, pSceneID);
|
||||
if(vSession != null) VoiceViewerSession.AddViewerSession(vSession);
|
||||
if(vSession != null)
|
||||
{
|
||||
if(pRequest.TryGetInt("parcel_local_id", out int parcelID))
|
||||
vSession.Flags = IVoiceViewerSession.VFlags.IsParcel;
|
||||
else
|
||||
vSession.Flags = IVoiceViewerSession.VFlags.IsEstate;
|
||||
VoiceViewerSession.AddViewerSession(vSession);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user