mirror of
https://github.com/opensim/opensim.git
synced 2026-05-13 01:46:07 +08:00
782 lines
39 KiB
C#
782 lines
39 KiB
C#
/*
|
|
* Copyright (c) Contributors, http://opensimulator.org/
|
|
* See CONTRIBUTORS.TXT for a full list of copyright holders.
|
|
*
|
|
* Redistribution and use in source and binary forms, with or without
|
|
* modification, are permitted provided that the following conditions are met:
|
|
* * Redistributions of source code must retain the above copyright
|
|
* notice, this list of conditions and the following disclaimer.
|
|
* * Redistributions in binary form must reproduce the above copyright
|
|
* notice, this list of conditions and the following disclaimer in the
|
|
* documentation and/or other materials provided with the distribution.
|
|
* * Neither the name of the OpenSimulator Project nor the
|
|
* names of its contributors may be used to endorse or promote products
|
|
* derived from this software without specific prior written permission.
|
|
*
|
|
* THIS SOFTWARE IS PROVIDED BY THE DEVELOPERS ``AS IS'' AND ANY
|
|
* EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
|
* WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
|
* DISCLAIMED. IN NO EVENT SHALL THE CONTRIBUTORS BE LIABLE FOR ANY
|
|
* DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
|
|
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
|
|
* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
|
|
* ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
|
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
|
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|
*/
|
|
|
|
using System;
|
|
using System.Collections.Concurrent;
|
|
using System.Collections.Generic;
|
|
using System.Net.Http;
|
|
using System.Net.Mime;
|
|
using System.Reflection;
|
|
using System.Threading.Tasks;
|
|
|
|
using OpenMetaverse;
|
|
using OpenMetaverse.StructuredData;
|
|
|
|
using log4net;
|
|
using System.Threading;
|
|
using OpenSim.Framework;
|
|
|
|
namespace osWebRtcVoice
|
|
{
|
|
// Encapsulization of a Session to the Janus server
|
|
public class JanusSession : IDisposable
|
|
{
|
|
private static readonly ILog m_log = LogManager.GetLogger(MethodBase.GetCurrentMethod().DeclaringType);
|
|
private static readonly string LogHeader = "[JANUS SESSION]";
|
|
|
|
// Set to true to enable Janus session debug logging.
|
|
private bool _DebugEnabled = false;
|
|
// Set to 'true' to get the messages send and received from Janus
|
|
private bool _MessageDetails = false;
|
|
|
|
private string _JanusServerURI = string.Empty;
|
|
private string _JanusAPIToken = string.Empty;
|
|
private string _JanusAdminURI = string.Empty;
|
|
private string _JanusAdminToken = string.Empty;
|
|
|
|
public string JanusServerURI => _JanusServerURI;
|
|
public string JanusAdminURI => _JanusAdminURI;
|
|
|
|
public string SessionId { get; private set; }
|
|
public string SessionUri { get ; private set ; }
|
|
|
|
public string PluginId { get; set; }
|
|
|
|
// private CancellationTokenSource _CancelTokenSource = new();
|
|
|
|
public bool IsConnected { get; set; }
|
|
|
|
// Wrapper around the session connection to Janus-gateway
|
|
public JanusSession(string pServerURI, string pAPIToken, string pAdminURI, string pAdminToken, bool pDebugEnabled = false, bool pDebugMessages = false)
|
|
{
|
|
// m_log.DebugFormat("{0} JanusSession constructor", LogHeader);
|
|
_DebugEnabled = pDebugEnabled;
|
|
DebugLog("{0} JanusSession constructor", LogHeader);
|
|
_JanusServerURI = pServerURI;
|
|
_JanusAPIToken = pAPIToken;
|
|
_JanusAdminURI = pAdminURI;
|
|
_JanusAdminToken = pAdminToken;
|
|
_MessageDetails = pDebugMessages;
|
|
}
|
|
|
|
public void Dispose()
|
|
{
|
|
ClearEventSubscriptions();
|
|
if (IsConnected)
|
|
{
|
|
_ = DestroySession();
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Make the create session request to the Janus server, get the
|
|
/// sessionID and return TRUE if successful.
|
|
/// </summary>
|
|
/// <returns>TRUE if session was created successfully</returns>
|
|
public async Task<bool> CreateSession()
|
|
{
|
|
bool ret = false;
|
|
try
|
|
{
|
|
JanusMessageResp resp = await SendToJanus(new CreateSessionReq());
|
|
if (resp is not null && resp.isSuccess)
|
|
{
|
|
CreateSessionResp sessionResp = new(resp);
|
|
SessionId = sessionResp.returnedId;
|
|
IsConnected = true;
|
|
SessionUri = _JanusServerURI + "/" + SessionId;
|
|
m_log.DebugFormat("{0} CreateSession. Created. ID={1}, URL={2}", LogHeader, SessionId, SessionUri);
|
|
// DebugLog("{0} CreateSession. Created. ID={1}, URL={2}", LogHeader, SessionId, SessionUri);
|
|
ret = true;
|
|
StartLongPoll();
|
|
}
|
|
else
|
|
{
|
|
m_log.ErrorFormat("{0} CreateSession: failed", LogHeader);
|
|
}
|
|
}
|
|
catch (Exception e)
|
|
{
|
|
m_log.ErrorFormat("{0} CreateSession: exception {1}", LogHeader, e);
|
|
}
|
|
|
|
return ret;
|
|
}
|
|
|
|
public async Task<bool> DestroySession()
|
|
{
|
|
bool ret = false;
|
|
try
|
|
{
|
|
JanusMessageResp resp = await SendToSession(new DestroySessionReq());
|
|
if (resp is not null && resp.isSuccess)
|
|
{
|
|
// Note that setting IsConnected to false will cause the long poll to exit
|
|
m_log.Debug($"{LogHeader} DestroySession. Destroyed");
|
|
// Debug("{0} DestroySession. Destroyed", LogHeader);
|
|
}
|
|
else
|
|
{
|
|
if (resp.isError)
|
|
{
|
|
ErrorResp eResp = new(resp);
|
|
switch (eResp.errorCode)
|
|
{
|
|
case 458:
|
|
// This is the error code for a session that is already destroyed
|
|
m_log.Debug($"{LogHeader} DestroySession: session already destroyed");
|
|
// DebugLog("{0} DestroySession: session already destroyed", LogHeader);
|
|
break;
|
|
case 459:
|
|
// This is the error code for handle already destroyed
|
|
if (_MessageDetails) m_log.Debug($"{LogHeader} DestroySession: Handle not found");
|
|
// if (_MessageDetails) DebugLog("{0} DestroySession: Handle not found", LogHeader);
|
|
break;
|
|
default:
|
|
m_log.Error($"{LogHeader} DestroySession: failed {eResp.errorReason}");
|
|
break;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
m_log.Error($"{LogHeader} DestroySession: failed response");
|
|
if (m_log.IsDebugEnabled)
|
|
m_log.DebugFormat("{0} DestroySession: response detail {1}", LogHeader, resp.ToString()); }
|
|
}
|
|
}
|
|
catch (Exception e)
|
|
{
|
|
m_log.Error($"{LogHeader} DestroySession: exception ", e);
|
|
}
|
|
IsConnected = false;
|
|
// _CancelTokenSource.Cancel();
|
|
|
|
return ret;
|
|
}
|
|
|
|
// ====================================================================
|
|
public async Task<JanusMessageResp> TrickleCandidates(JanusViewerSession pVSession, OSDArray pCandidates)
|
|
{
|
|
JanusMessageResp ret = null;
|
|
// if the audiobridge is active, the trickle message is sent to it
|
|
if (pVSession.AudioBridge is null)
|
|
{
|
|
ret = await SendToJanusNoWait(new TrickleReq(pVSession, pCandidates));
|
|
}
|
|
else
|
|
{
|
|
ret = await SendToJanusNoWait(new TrickleReq(pVSession, pCandidates), pVSession.AudioBridge.PluginUri);
|
|
}
|
|
return ret;
|
|
}
|
|
// ====================================================================
|
|
public async Task<JanusMessageResp> TrickleCompleted(JanusViewerSession pVSession)
|
|
{
|
|
JanusMessageResp ret = null;
|
|
// if the audiobridge is active, the trickle message is sent to it
|
|
if (pVSession.AudioBridge is null)
|
|
{
|
|
ret = await SendToJanusNoWait(new TrickleReq(pVSession));
|
|
}
|
|
else
|
|
{
|
|
ret = await SendToJanusNoWait(new TrickleReq(pVSession), pVSession.AudioBridge.PluginUri);
|
|
}
|
|
return ret;
|
|
}
|
|
// ====================================================================
|
|
public Dictionary<string, JanusPlugin> _Plugins = new Dictionary<string, JanusPlugin>();
|
|
public void AddPlugin(JanusPlugin pPlugin)
|
|
{
|
|
_Plugins.Add(pPlugin.PluginName, pPlugin);
|
|
}
|
|
|
|
private void DebugLog(string pFormat, params object[] pArgs)
|
|
{
|
|
if (_DebugEnabled)
|
|
{
|
|
m_log.DebugFormat(pFormat, pArgs);
|
|
}
|
|
}
|
|
|
|
private void DebugLog(string message)
|
|
{
|
|
if (_DebugEnabled)
|
|
{
|
|
m_log.Debug(message);
|
|
}
|
|
}
|
|
|
|
// ====================================================================
|
|
// Post to the session
|
|
public async Task<JanusMessageResp> SendToSession(JanusMessageReq pReq)
|
|
{
|
|
return await SendToJanus(pReq, SessionUri);
|
|
}
|
|
|
|
private class OutstandingRequest
|
|
{
|
|
public string TransactionId;
|
|
public DateTime RequestTime;
|
|
public TaskCompletionSource<JanusMessageResp> TaskCompletionSource;
|
|
}
|
|
private readonly ConcurrentDictionary<string, OutstandingRequest> _OutstandingRequests = new();
|
|
|
|
// Send a request directly to the Janus server.
|
|
// NOTE: this is probably NOT what you want to do. This is a direct call that is outside the session.
|
|
private async Task<JanusMessageResp> SendToJanus(JanusMessageReq pReq)
|
|
{
|
|
return await SendToJanus(pReq, _JanusServerURI);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Send a request to the Janus server. This is the basic call that sends a request to the server.
|
|
/// The transaction ID is used to match the response to the request.
|
|
/// If the request returns an 'ack' response, the code waits for the matching event
|
|
/// before returning the response.
|
|
/// </summary>
|
|
/// <param name="pReq"></param>
|
|
/// <param name="pURI"></param>
|
|
/// <returns></returns>
|
|
public async Task<JanusMessageResp> SendToJanus(JanusMessageReq pReq, string pURI, bool admin = false)
|
|
{
|
|
AddJanusHeaders(pReq, admin);
|
|
if (_MessageDetails) m_log.DebugFormat($"{LogHeader} SendToJanus. URI={pURI}, req={pReq.ToJson}");
|
|
// if (_MessageDetails) DebugLog("{0} SendToJanus. URI={1}, req={2}", LogHeader, pURI, pReq.ToJson());
|
|
|
|
JanusMessageResp ret = null;
|
|
try
|
|
{
|
|
OutstandingRequest outReq = new OutstandingRequest
|
|
{
|
|
TransactionId = pReq.TransactionId,
|
|
RequestTime = DateTime.Now,
|
|
TaskCompletionSource = new TaskCompletionSource<JanusMessageResp>()
|
|
};
|
|
_OutstandingRequests[pReq.TransactionId] = outReq;
|
|
|
|
string reqStr = pReq.ToJson();
|
|
|
|
HttpClient httpClient = WebUtil.GetNewGlobalHttpClient(30000);
|
|
HttpRequestMessage reqMsg = new(HttpMethod.Post, pURI);
|
|
reqMsg.Content = new StringContent(reqStr, System.Text.Encoding.UTF8, MediaTypeNames.Application.Json);
|
|
reqMsg.Headers.TryAddWithoutValidation("Accept", "application/json");
|
|
// HttpResponseMessage response = await httpClient.SendAsync(reqMsg, _CancelTokenSource.Token);
|
|
HttpResponseMessage response = await httpClient.SendAsync(reqMsg);
|
|
|
|
if (response.IsSuccessStatusCode)
|
|
{
|
|
string respStr = await response.Content.ReadAsStringAsync();
|
|
ret = JanusMessageResp.FromJson(respStr);
|
|
if (ret.CheckReturnCode("ack"))
|
|
{
|
|
// Some messages are asynchronous and completed with an event
|
|
if (_MessageDetails) m_log.Debug($"{LogHeader} SendToJanus: ack response {respStr}");
|
|
// if (_MessageDetails) DebugLog("{0} SendToJanus: ack response {1}", LogHeader, respStr);
|
|
/*
|
|
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
|
|
*/
|
|
|
|
// Wait on the local TaskCompletionSource instead of re-reading the dictionary.
|
|
// This avoids a race where the long-poll thread already removed the request.
|
|
ret = await outReq.TaskCompletionSource.Task;
|
|
}
|
|
else
|
|
{
|
|
// If the response is not an ack, that means a synchronous request/response so return the response
|
|
_= _OutstandingRequests.TryRemove(pReq.TransactionId, out _);
|
|
if (_MessageDetails) m_log.Debug($"{LogHeader} SendToJanus: response {respStr}");
|
|
// if (_MessageDetails) DebugLog("{0} SendToJanus: response {1}", LogHeader, respStr);
|
|
|
|
}
|
|
}
|
|
else
|
|
{
|
|
m_log.Error("{LogHeader} SendToJanus: response not successful {response}");
|
|
_= _OutstandingRequests.TryRemove(pReq.TransactionId, out _);
|
|
}
|
|
}
|
|
catch (Exception e)
|
|
{
|
|
m_log.ErrorFormat("{0} SendToJanus: exception {1}", LogHeader, e.Message);
|
|
_= _OutstandingRequests.TryRemove(pReq.TransactionId, out _);
|
|
}
|
|
finally
|
|
{
|
|
_= _OutstandingRequests.TryRemove(pReq.TransactionId, out _);
|
|
}
|
|
return ret;
|
|
}
|
|
/// <summary>
|
|
/// Send a request to the Janus server but we just return the response and don't wait for any
|
|
/// event or anything.
|
|
/// There are some requests that are just fire-and-forget.
|
|
/// </summary>
|
|
/// <param name="pReq"></param>
|
|
/// <returns></returns>
|
|
private async Task<JanusMessageResp> SendToJanusNoWait(JanusMessageReq pReq, string pURI)
|
|
{
|
|
JanusMessageResp ret = new();
|
|
|
|
AddJanusHeaders(pReq);
|
|
|
|
try
|
|
{
|
|
HttpClient httpClient = WebUtil.GetNewGlobalHttpClient(30000);
|
|
HttpRequestMessage reqMsg = new(HttpMethod.Post, pURI);
|
|
string reqStr = pReq.ToJson();
|
|
reqMsg.Content = new StringContent(reqStr, System.Text.Encoding.UTF8, MediaTypeNames.Application.Json);
|
|
reqMsg.Headers.TryAddWithoutValidation("Accept", "application/json");
|
|
HttpResponseMessage response = await httpClient.SendAsync(reqMsg);
|
|
string respStr = await response.Content.ReadAsStringAsync();
|
|
ret = JanusMessageResp.FromJson(respStr);
|
|
}
|
|
catch (Exception e)
|
|
{
|
|
m_log.Error($"{LogHeader} SendToJanusNoWait: exception {e.Message}");
|
|
}
|
|
return ret;
|
|
|
|
}
|
|
private async Task<JanusMessageResp> SendToJanusNoWait(JanusMessageReq pReq)
|
|
{
|
|
return await SendToJanusNoWait(pReq, SessionUri);
|
|
}
|
|
|
|
// There are various headers that are in most Janus requests. Add them here.
|
|
private void AddJanusHeaders(JanusMessageReq pReq, bool admin = false)
|
|
{
|
|
// Authentication token
|
|
if(admin)
|
|
{
|
|
if (!string.IsNullOrEmpty(_JanusAdminToken))
|
|
{
|
|
pReq.AddAdminToken(_JanusAdminToken);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
if (!string.IsNullOrEmpty(_JanusAPIToken))
|
|
{
|
|
pReq.AddAPIToken(_JanusAPIToken);
|
|
}
|
|
}
|
|
|
|
// Transaction ID that matches responses to requests
|
|
if (string.IsNullOrEmpty(pReq.TransactionId))
|
|
{
|
|
pReq.TransactionId = UUID.Random().ToString();
|
|
}
|
|
// The following two are required for the WebSocket interface. They are optional for the
|
|
// HTTP interface since the session and plugin handle are in the URL.
|
|
// SessionId is added to the message if not already there
|
|
if (!pReq.hasSessionId && !string.IsNullOrEmpty(SessionId))
|
|
{
|
|
pReq.AddSessionId(SessionId);
|
|
}
|
|
// HandleId connects to the plugin
|
|
if (!pReq.hasHandleId && !string.IsNullOrEmpty(PluginId))
|
|
{
|
|
pReq.AddHandleId(PluginId);
|
|
}
|
|
}
|
|
|
|
bool TryGetOutstandingRequest(string pTransactionId, out OutstandingRequest pOutstandingRequest)
|
|
{
|
|
if (string.IsNullOrEmpty(pTransactionId))
|
|
{
|
|
pOutstandingRequest = null;
|
|
return false;
|
|
}
|
|
|
|
if (_OutstandingRequests.TryGetValue(pTransactionId, out pOutstandingRequest))
|
|
return true;
|
|
|
|
pOutstandingRequest = null;
|
|
return false;
|
|
}
|
|
|
|
public Task<JanusMessageResp> SendToJanusAdmin(JanusMessageReq pReq)
|
|
{
|
|
return SendToJanus(pReq, _JanusAdminURI, true);
|
|
}
|
|
|
|
public Task<JanusMessageResp> GetFromJanus()
|
|
{
|
|
return GetFromJanus(_JanusServerURI);
|
|
}
|
|
/// <summary>
|
|
/// Do a GET to the Janus server and return the response.
|
|
/// If the response is an HTTP error, we return fake JanusMessageResp with the error.
|
|
/// </summary>
|
|
/// <param name="pURI"></param>
|
|
/// <returns></returns>
|
|
public async Task<JanusMessageResp> GetFromJanus(string pURI, int timeout = 30000)
|
|
{
|
|
if (!string.IsNullOrEmpty(_JanusAPIToken))
|
|
{
|
|
pURI += "?apisecret=" + _JanusAPIToken;
|
|
}
|
|
|
|
JanusMessageResp ret = null;
|
|
try
|
|
{
|
|
// m_log.DebugFormat("{0} GetFromJanus: URI = \"{1}\"", LogHeader, pURI);
|
|
//HttpClient httpClient = WebUtil.GetNewGlobalHttpClient(timeout);
|
|
HttpClient httpClient = WebUtil.GetGlobalNoRedirHttpClient(timeout);
|
|
HttpRequestMessage reqMsg = new HttpRequestMessage(HttpMethod.Get, pURI);
|
|
reqMsg.Headers.TryAddWithoutValidation("Accept", "application/json");
|
|
HttpResponseMessage response = null;
|
|
try
|
|
{
|
|
// response = await httpClient.SendAsync(reqMsg, _CancelTokenSource.Token);
|
|
response = await httpClient.SendAsync(reqMsg).ConfigureAwait(false);
|
|
|
|
if (response is not null && response.IsSuccessStatusCode)
|
|
{
|
|
string respStr = await response.Content.ReadAsStringAsync().ConfigureAwait(false);
|
|
ret = JanusMessageResp.FromJson(respStr);
|
|
// m_log.DebugFormat("{0} GetFromJanus: response {1}", LogHeader, respStr);
|
|
}
|
|
else
|
|
{
|
|
m_log.Error($"{LogHeader} GetFromJanus: response not successful {response}");
|
|
// m_log.ErrorFormat("{0} GetFromJanus: response not successful", LogHeader);
|
|
// if (m_log.IsDebugEnabled)
|
|
// m_log.DebugFormat("{0} GetFromJanus: response detail {1}", LogHeader, response);
|
|
ErrorResp eResp = new("GETERROR");
|
|
// Add the sessionId so the proper session can be shut down
|
|
eResp.AddSessionId(SessionId);
|
|
if (response is not null)
|
|
{
|
|
eResp.SetError((int)response.StatusCode, response.ReasonPhrase);
|
|
}
|
|
else
|
|
{
|
|
eResp.SetError(0, "Connection refused");
|
|
}
|
|
ret = eResp;
|
|
}
|
|
}
|
|
catch (TaskCanceledException e)
|
|
{
|
|
if (_MessageDetails) m_log.DebugFormat("{0} GetFromJanus: task canceled: {1}", LogHeader, e.Message);
|
|
// if (_MessageDetails) DebugLog("{0} GetFromJanus: task canceled: {1}", LogHeader, e.Message);
|
|
|
|
ErrorResp eResp = new("GETERROR");
|
|
eResp.SetError(499, "Task canceled");
|
|
ret = eResp;
|
|
}
|
|
catch (Exception e)
|
|
{
|
|
m_log.ErrorFormat("{0} GetFromJanus: exception {1}", LogHeader, e.Message);
|
|
ErrorResp eResp = new("GETERROR");
|
|
eResp.SetError(400, "Exception: " + e.Message);
|
|
ret = eResp;
|
|
}
|
|
}
|
|
catch (Exception e)
|
|
{
|
|
m_log.ErrorFormat("{0} GetFromJanus: exception {1}", LogHeader, e);
|
|
ErrorResp eResp = new("GETERROR");
|
|
eResp.SetError(400, "Exception: " + e.Message);
|
|
ret = eResp;
|
|
}
|
|
|
|
return ret;
|
|
}
|
|
|
|
// ====================================================================
|
|
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,CS0414
|
|
public event JanusEventHandler OnKeepAlive;
|
|
public event JanusEventHandler OnServerInfo;
|
|
public event JanusEventHandler OnTrickle;
|
|
public event JanusEventHandler OnHangup;
|
|
public event JanusEventHandler OnDetached;
|
|
public event JanusEventHandler OnError;
|
|
public event JanusEventHandler OnEvent;
|
|
public event JanusEventHandler OnMessage;
|
|
public event JanusEventHandler OnJoined;
|
|
public event JanusEventHandler OnLeaving;
|
|
public event JanusEventHandler OnDisconnect;
|
|
#pragma warning restore CS0067,CS0414
|
|
public void ClearEventSubscriptions()
|
|
{
|
|
OnKeepAlive = null;
|
|
OnServerInfo = null;
|
|
OnTrickle = null;
|
|
OnHangup = null;
|
|
OnDetached = null;
|
|
OnError = null;
|
|
OnEvent = null;
|
|
OnMessage = null;
|
|
OnJoined = null;
|
|
OnLeaving = null;
|
|
OnDisconnect = null;
|
|
}
|
|
// ====================================================================
|
|
/// <summary>
|
|
/// In the REST API, events are returned by a long poll. This
|
|
/// starts the poll and calls the registed event handler when
|
|
/// an event is received.
|
|
/// </summary>
|
|
private void StartLongPoll()
|
|
{
|
|
bool running = true;
|
|
|
|
m_log.Debug($"{LogHeader} EventLongPoll");
|
|
// DebugLog($"{LogHeader} EventLongPoll");
|
|
|
|
Task.Run(async () => {
|
|
while (running && IsConnected)
|
|
{
|
|
try
|
|
{
|
|
JanusMessageResp resp = await GetFromJanus(SessionUri, 60000);
|
|
if (resp is not null)
|
|
{
|
|
//_ = 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());
|
|
break;
|
|
case "server_info":
|
|
// Just info on the Janus instance
|
|
m_log.Debug($"{LogHeader} EventLongPoll: server_info {resp}");
|
|
break;
|
|
case "ack":
|
|
// 'ack' says the request was received and an event will follow
|
|
if (_MessageDetails) m_log.Debug($"{LogHeader} EventLongPoll: ack {resp}");
|
|
break;
|
|
case "success":
|
|
// success is a sync response that says the request was completed
|
|
if (_MessageDetails) m_log.Debug($"{LogHeader} EventLongPoll: success {resp}");
|
|
break;
|
|
case "trickle":
|
|
// got a trickle ICE candidate from Janus
|
|
// this is for reverse communication from Janus to the client and we don't do that
|
|
if (_MessageDetails) m_log.Debug($"{LogHeader} EventLongPoll: trickle {resp}");
|
|
|
|
OnTrickle?.Invoke(eventResp);
|
|
break;
|
|
case "webrtcup":
|
|
// ICE and DTLS succeeded, and so Janus correctly established a PeerConnection with the user/application;
|
|
// m_log.DebugFormat("{0} EventLongPoll: webrtcup {1}", LogHeader, resp.ToString());
|
|
string webrtcupSessionId = ExtractLogId(resp, "session_id");
|
|
string webrtcupSender = ExtractLogId(resp, "sender");
|
|
m_log.DebugFormat("{0} EventLongPoll: webrtcup session_id={1}, sender={2}",
|
|
LogHeader,
|
|
string.IsNullOrEmpty(webrtcupSessionId) ? "<none>" : webrtcupSessionId,
|
|
string.IsNullOrEmpty(webrtcupSender) ? "<none>" : webrtcupSender);
|
|
break;
|
|
case "hangup":
|
|
// The PeerConnection was closed, either by the user/application or by Janus itself;
|
|
// If one is in the room, when a "hangup" event happens, it means that the user left the room.
|
|
// m_log.DebugFormat("{0} EventLongPoll: hangup {1}", LogHeader, resp.ToString());
|
|
m_log.DebugFormat("{0} EventLongPoll: hangup session_id={1}, sender={2}, reason={3}, tx={4}",
|
|
LogHeader,
|
|
ExtractLogId(resp, "session_id"),
|
|
ExtractLogId(resp, "sender"),
|
|
resp.RawBody.TryGetString("reason", out string hangupReason) ? hangupReason : string.Empty,
|
|
FormatTransactionId(resp.TransactionId));
|
|
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());
|
|
m_log.DebugFormat("{0} EventLongPoll: detached session_id={1}, sender={2}, tx={3}",
|
|
LogHeader,
|
|
ExtractLogId(resp, "session_id"),
|
|
ExtractLogId(resp, "sender"),
|
|
FormatTransactionId(resp.TransactionId));
|
|
OnDetached?.Invoke(eventResp);
|
|
break;
|
|
case "media":
|
|
// Janus is receiving (receiving: true/false) audio/video (type: "audio/video") on this PeerConnection;
|
|
// m_log.DebugFormat("{0} EventLongPoll: media {1}", LogHeader, resp.ToString());
|
|
m_log.DebugFormat("{0} EventLongPoll: media session_id={1}, sender={2}, tx={3}",
|
|
LogHeader,
|
|
ExtractLogId(resp, "session_id"),
|
|
ExtractLogId(resp, "sender"),
|
|
FormatTransactionId(resp.TransactionId));
|
|
|
|
break;
|
|
case "slowlink":
|
|
// Janus detected a slowlink (uplink: true/false) on this PeerConnection;
|
|
// m_log.DebugFormat("{0} EventLongPoll: slowlink {1}", LogHeader, resp.ToString());
|
|
m_log.DebugFormat("{0} EventLongPoll: slowlink session_id={1}, sender={2}, tx={3}",
|
|
LogHeader,
|
|
ExtractLogId(resp, "session_id"),
|
|
ExtractLogId(resp, "sender"),
|
|
FormatTransactionId(resp.TransactionId));
|
|
break;
|
|
case "error":
|
|
m_log.DebugFormat("{0} EventLongPoll: error {1}", LogHeader, resp.ToString());
|
|
if (TryGetOutstandingRequest(resp.TransactionId, out OutstandingRequest outstandingRequest))
|
|
{
|
|
outstandingRequest.TaskCompletionSource.SetResult(resp);
|
|
}
|
|
else
|
|
{
|
|
OnError?.Invoke(eventResp);
|
|
m_log.Error($"{LogHeader} EventLongPoll: error with no transaction. {resp}");
|
|
}
|
|
break;
|
|
case "event":
|
|
if (_MessageDetails) m_log.Debug($"{LogHeader} EventLongPoll: event {resp}");
|
|
if (TryGetOutstandingRequest(resp.TransactionId, out OutstandingRequest outstandingRequest2))
|
|
{
|
|
// Someone is waiting for this event
|
|
outstandingRequest2.TaskCompletionSource.SetResult(resp);
|
|
}
|
|
else
|
|
{
|
|
m_log.ErrorFormat("{0} EventLongPoll: event no outstanding request {1}", LogHeader, resp.ToString());
|
|
// Janus often pushes plugin events without a transaction id (normal async flow).
|
|
// Keep unknown transaction ids visible, but do not treat missing transaction ids as errors.
|
|
if (string.IsNullOrEmpty(resp.TransactionId))
|
|
{
|
|
m_log.DebugFormat("{0} EventLongPoll: async event with no transaction {1}", LogHeader, resp.ToString());
|
|
// if (_MessageDetails) DebugLog("{0} EventLongPoll: async event with no transaction {1}", LogHeader, resp.ToString());
|
|
}
|
|
else
|
|
{
|
|
m_log.WarnFormat("{0} EventLongPoll: event with unknown transaction", LogHeader);
|
|
if (m_log.IsDebugEnabled)
|
|
m_log.DebugFormat("{0} EventLongPoll: unknown transaction detail {1}", LogHeader, resp.ToString());
|
|
}
|
|
|
|
OnEvent?.Invoke(eventResp);
|
|
}
|
|
break;
|
|
case "message":
|
|
m_log.DebugFormat("{0} EventLongPoll: message {1}", LogHeader, resp.ToString());
|
|
OnMessage?.Invoke(eventResp);
|
|
break;
|
|
case "timeout":
|
|
// Events for the audio bridge
|
|
m_log.DebugFormat("{0} EventLongPoll: timeout {1}", LogHeader, resp.ToString());
|
|
break;
|
|
case "joined":
|
|
// Events for the audio bridge
|
|
OnJoined?.Invoke(eventResp);
|
|
m_log.DebugFormat("{0} EventLongPoll: joined {1}", LogHeader, resp.ToString());
|
|
break;
|
|
case "leaving":
|
|
// Events for the audio bridge
|
|
OnLeaving?.Invoke(eventResp);
|
|
m_log.DebugFormat("{0} EventLongPoll: leaving {1}", LogHeader, resp.ToString());
|
|
break;
|
|
case "GETERROR":
|
|
// Special error response from the GET
|
|
ErrorResp errorResp = new(resp);
|
|
switch (errorResp.errorCode)
|
|
{
|
|
case 404:
|
|
// "Not found" means there is a Janus server but the session is gone
|
|
m_log.ErrorFormat("{0} EventLongPoll: GETERROR Not Found. URI={1}: {2}",
|
|
LogHeader, SessionUri, resp.ToString());
|
|
break;
|
|
case 400:
|
|
// "Bad request" means the session is gone
|
|
m_log.ErrorFormat("{0} EventLongPoll: Bad Request. URI={1}: {2}",
|
|
LogHeader, SessionUri, resp.ToString());
|
|
break;
|
|
case 499:
|
|
// "Task canceled" means the long poll was canceled
|
|
if (_MessageDetails) m_log.DebugFormat("{0} EventLongPoll: Task canceled. URI={1}", LogHeader, SessionUri);
|
|
break;
|
|
default:
|
|
m_log.DebugFormat("{0} EventLongPoll: unknown response. URI={1}: {2}",
|
|
LogHeader, SessionUri, resp.ToString());
|
|
break;
|
|
}
|
|
// This will cause the long poll to exit
|
|
running = false;
|
|
OnDisconnect?.Invoke(eventResp);
|
|
break;
|
|
default:
|
|
m_log.DebugFormat("{0} EventLongPoll: unknown response {1}", LogHeader, resp.ToString());
|
|
break;
|
|
}
|
|
}
|
|
// );
|
|
}
|
|
else
|
|
{
|
|
m_log.ErrorFormat("{0} EventLongPoll: failed. Response is null", LogHeader);
|
|
}
|
|
}
|
|
catch (Exception e)
|
|
{
|
|
// This will cause the long poll to exit
|
|
running = false;
|
|
m_log.ErrorFormat("{0} EventLongPoll: exception {1}", LogHeader, e);
|
|
}
|
|
}
|
|
if (_MessageDetails)
|
|
m_log.InfoFormat("{0} EventLongPoll: Exiting long poll loop", LogHeader);
|
|
});
|
|
}
|
|
|
|
private static string ExtractLogId(JanusMessageResp resp, string key)
|
|
{
|
|
if (resp?.RawBody is null || !resp.RawBody.TryGetValue(key, out OSD value) || value is null)
|
|
return string.Empty;
|
|
|
|
try
|
|
{
|
|
return value.Type switch
|
|
{
|
|
OSDType.Integer or OSDType.Binary or OSDType.Array => value.AsLong().ToString(),
|
|
_ => value.AsString(),
|
|
};
|
|
}
|
|
catch
|
|
{
|
|
return value.AsString();
|
|
}
|
|
}
|
|
|
|
private static string FormatTransactionId(string transactionId)
|
|
{
|
|
return string.IsNullOrEmpty(transactionId) ? "<event>" : transactionId;
|
|
}
|
|
}
|
|
} |