640 lines
24 KiB
C#
640 lines
24 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.Reflection;
|
|
|
|
using OpenMetaverse.StructuredData;
|
|
using OpenMetaverse;
|
|
|
|
using log4net;
|
|
|
|
namespace osWebRtcVoice
|
|
{
|
|
|
|
/// <summary>
|
|
/// Wrappers around the Janus requests and responses.
|
|
/// Since the messages are JSON and, because of the libraries we are using,
|
|
/// the internal structure is an OSDMap, these routines hold all the logic
|
|
/// to getting and setting the values in the JSON.
|
|
/// </summary>
|
|
public class JanusMessage
|
|
{
|
|
protected static readonly ILog m_log = LogManager.GetLogger(MethodBase.GetCurrentMethod().DeclaringType);
|
|
protected static readonly string LogHeader = "[JANUS MESSAGE]";
|
|
|
|
protected OSDMap m_message = new OSDMap();
|
|
|
|
public JanusMessage()
|
|
{
|
|
}
|
|
// A basic Janus message is:
|
|
// {
|
|
// "janus": "operation",
|
|
// "transaction": "baefcec8-70c5-4e79-b2c1-d653b9617dea",
|
|
// "session_id": 5645225333294848, // optional, gives the session ID
|
|
// "handle_id": 6969906757968657 // optional, gives the plugin handle ID
|
|
// "sender": 6969906757968657 // optional, gives the ID of the sending subsystem
|
|
// "jsep": { "type": "offer", "sdp": "..." } // optional, gives the SDP
|
|
// }
|
|
public JanusMessage(string pType) : this()
|
|
{
|
|
m_message["janus"] = pType;
|
|
m_message["transaction"] = UUID.Random().ToString();
|
|
}
|
|
public OSDMap RawBody => m_message;
|
|
|
|
public string TransactionId {
|
|
get { return m_message.TryGetString("transaction", out string tid) ? tid : null; }
|
|
set { m_message["transaction"] = value; }
|
|
}
|
|
public string Sender {
|
|
get { return m_message.TryGetString("sender", out string tid) ? tid : null; }
|
|
set { m_message["sender"] = value; }
|
|
}
|
|
public OSDMap Jsep {
|
|
get { return m_message.TryGetOSDMap("jsep", out OSDMap jsep) ? jsep : null; }
|
|
set { m_message["jsep"] = value; }
|
|
}
|
|
public void SetJsep(string pOffer, string pSdp)
|
|
{
|
|
m_message["jsep"] = new OSDMap()
|
|
{
|
|
{ "type", pOffer },
|
|
{ "sdp", pSdp }
|
|
};
|
|
}
|
|
|
|
public void AddAPIToken(string pToken)
|
|
{
|
|
m_message["apisecret"] = pToken;
|
|
}
|
|
// Note that the session_id is a long number in the JSON so we convert the string.
|
|
public string sessionId {
|
|
get { return m_message.ContainsKey("session_id") ? OSDToLong(m_message["session_id"]).ToString() : String.Empty; }
|
|
set { m_message["session_id"] = long.Parse(value); }
|
|
}
|
|
public bool hasSessionId { get { return m_message.ContainsKey("session_id"); } }
|
|
public void AddSessionId(string pToken)
|
|
{
|
|
AddSessionId(long.Parse(pToken));
|
|
}
|
|
public void AddSessionId(long pToken)
|
|
{
|
|
m_message["session_id"] = pToken;
|
|
}
|
|
public bool hasHandleId { get { return m_message.ContainsKey("handle_id"); } }
|
|
public void AddHandleId(string pToken)
|
|
{
|
|
m_message["handle_id"] = long.Parse(pToken);
|
|
}
|
|
public string sender
|
|
{
|
|
get { return m_message.TryGetString("sender", out string str) ? str : string.Empty; }
|
|
}
|
|
|
|
public virtual string ToJson()
|
|
{
|
|
return m_message.ToString();
|
|
}
|
|
public override string ToString()
|
|
{
|
|
return m_message.ToString();
|
|
}
|
|
// Utility function to convert an OSD object to an long. The OSD object can be an OSDInteger
|
|
// or an OSDArray of 4 or 8 integers.
|
|
// This exists because the JSON to OSD parser can return an OSDArray for a long number
|
|
// since there is not an OSDLong type.
|
|
// The design of the OSD conversion functions kinda needs one to know how the number
|
|
// is stored in order to extract it. Like, if it's stored as a long value (8 bytes)
|
|
// and one fetches it with .AsInteger(), it will return the first 4 bytes as an integer
|
|
// and not the long value. So this function looks at the type of the OSD object and
|
|
// extracts the number appropriately.
|
|
public long OSDToLong(OSD pIn)
|
|
{
|
|
long ret = 0;
|
|
switch (pIn.Type)
|
|
{
|
|
case OSDType.Integer:
|
|
ret = (long)(pIn as OSDInteger).AsInteger();
|
|
break;
|
|
case OSDType.Binary:
|
|
byte[] value = (pIn as OSDBinary).value;
|
|
if (value.Length == 4)
|
|
{
|
|
ret = (long)(pIn as OSDBinary).AsInteger();
|
|
}
|
|
if (value.Length == 8)
|
|
{
|
|
ret = (pIn as OSDBinary).AsLong();
|
|
}
|
|
break;
|
|
case OSDType.Array:
|
|
if ((pIn as OSDArray).Count == 4)
|
|
{
|
|
ret = (long)pIn.AsInteger();
|
|
}
|
|
if ((pIn as OSDArray).Count == 8)
|
|
{
|
|
ret = pIn.AsLong();
|
|
}
|
|
break;
|
|
}
|
|
return ret;
|
|
}
|
|
}
|
|
// ==============================================================
|
|
// A Janus request message is a basic Janus message with an API token
|
|
public class JanusMessageReq : JanusMessage
|
|
{
|
|
public JanusMessageReq(string pType) : base(pType)
|
|
{
|
|
}
|
|
}
|
|
|
|
// ==============================================================
|
|
// Janus message response is a basic Janus message with the response data
|
|
// {
|
|
// "janus": "success",
|
|
// "transaction": "baefcec8-70c5-4e79-b2c1-d653b9617dea", // ID of the requesting message
|
|
// "data": { ... } // the response data
|
|
// "error": { "code": 123, "reason": "..." } // if there was an error
|
|
// }
|
|
// The "janus" return code changes depending on the request. The above is for
|
|
// a successful response. Could be
|
|
// "event": for an event message (See JanusEventResp)
|
|
// "keepalive": for a keepalive event
|
|
public class JanusMessageResp : JanusMessage
|
|
{
|
|
public JanusMessageResp() : base()
|
|
{
|
|
}
|
|
|
|
public JanusMessageResp(string pType) : base(pType)
|
|
{
|
|
}
|
|
|
|
public JanusMessageResp(OSDMap pMap) : base()
|
|
{
|
|
m_message = pMap;
|
|
}
|
|
|
|
public static JanusMessageResp FromJson(string pJson)
|
|
{
|
|
var newBody = OSDParser.DeserializeJson(pJson) as OSDMap;
|
|
return new JanusMessageResp(newBody);
|
|
}
|
|
|
|
// Return the "data" portion of the response as an OSDMap or null if there is none
|
|
public OSDMap dataSection { get { return m_message.ContainsKey("data") ? (m_message["data"] as OSDMap) : null; } }
|
|
|
|
// Check if a successful response code is in the response
|
|
public virtual bool isSuccess { get { return CheckReturnCode("success"); } }
|
|
public virtual bool isEvent { get { return CheckReturnCode("event"); } }
|
|
public virtual bool isError { get { return CheckReturnCode("error"); } }
|
|
public virtual bool CheckReturnCode(string pCode)
|
|
{
|
|
return ReturnCode == pCode;
|
|
}
|
|
public virtual string ReturnCode { get {
|
|
string ret = String.Empty;
|
|
if (m_message is not null && m_message.ContainsKey("janus"))
|
|
{
|
|
ret = m_message["janus"].AsString();
|
|
}
|
|
return ret;
|
|
} }
|
|
}
|
|
|
|
// ==============================================================
|
|
// An error response is a Janus response with an error code and reason.
|
|
// {
|
|
// "janus": "error",
|
|
// "transaction": "baefcec8-70c5-4e79-b2c1-d653b9617dea", // ID of the requesting message
|
|
// "error": { "code": 123, "reason": "..." } // if there was an error
|
|
// }
|
|
public class ErrorResp : JanusMessageResp
|
|
{
|
|
public ErrorResp() : base("error")
|
|
{
|
|
}
|
|
|
|
public ErrorResp(string pType) : base(pType)
|
|
{
|
|
}
|
|
|
|
public ErrorResp(JanusMessageResp pResp) : base(pResp.RawBody)
|
|
{
|
|
}
|
|
|
|
public void SetError(int pCode, string pReason)
|
|
{
|
|
m_message["error"] = new OSDMap()
|
|
{
|
|
{ "code", pCode },
|
|
{ "reason", pReason }
|
|
};
|
|
}
|
|
|
|
// Dig through the response to get the error code or 0 if there is none
|
|
public int errorCode { get {
|
|
int ret = 0;
|
|
if (m_message.ContainsKey("error"))
|
|
{
|
|
var err = m_message["error"];
|
|
if (err is OSDMap)
|
|
ret = (int)OSDToLong((err as OSDMap)["code"]);
|
|
}
|
|
return ret;
|
|
}}
|
|
|
|
// Dig through the response to get the error reason or empty string if there is none
|
|
public string errorReason { get {
|
|
string ret = String.Empty;
|
|
if (m_message.ContainsKey("error"))
|
|
{
|
|
var err = m_message["error"];
|
|
if (err is OSDMap)
|
|
ret = (err as OSDMap)["reason"];
|
|
}
|
|
// return ((m_message["error"] as OSDMap)?["reason"]) ?? String.Empty;
|
|
return ret;
|
|
}}
|
|
}
|
|
// ==============================================================
|
|
// Create session request and response
|
|
public class CreateSessionReq : JanusMessageReq
|
|
{
|
|
public CreateSessionReq() : base("create")
|
|
{
|
|
}
|
|
}
|
|
public class CreateSessionResp : JanusMessageResp
|
|
{
|
|
public CreateSessionResp(JanusMessageResp pResp) : base(pResp.RawBody)
|
|
{ }
|
|
public string returnedId { get {
|
|
// The JSON response gives a long number (not a string)
|
|
// and the ODMap conversion interprets it as a long (OSDLong).
|
|
// If one just does a "ToString()" on the OSD object, you
|
|
// get an interpretation of the binary value.
|
|
return dataSection.ContainsKey("id") ? OSDToLong(dataSection["id"]).ToString() : String.Empty;
|
|
}}
|
|
}
|
|
// ==============================================================
|
|
public class DestroySessionReq : JanusMessageReq
|
|
{
|
|
public DestroySessionReq() : base("destroy")
|
|
{
|
|
// Doesn't include the session ID because it is the URI
|
|
}
|
|
}
|
|
// ==============================================================
|
|
public class TrickleReq : JanusMessageReq
|
|
{
|
|
// An empty trickle request is used to signal the end of the trickle
|
|
public TrickleReq(JanusViewerSession pVSession) : base("trickle")
|
|
{
|
|
m_message["candidate"] = new OSDMap()
|
|
{
|
|
{ "completed", true },
|
|
};
|
|
|
|
}
|
|
public TrickleReq(JanusViewerSession pVSession, OSD pCandidates) : base("trickle")
|
|
{
|
|
m_message["viewer_session"] = pVSession.ViewerSessionID;
|
|
if (pCandidates is OSDArray)
|
|
m_message["candidates"] = pCandidates;
|
|
else
|
|
m_message["candidate"] = pCandidates;
|
|
}
|
|
}
|
|
// ==============================================================
|
|
public class AttachPluginReq : JanusMessageReq
|
|
{
|
|
public AttachPluginReq(string pPlugin) : base("attach")
|
|
{
|
|
m_message["plugin"] = pPlugin;
|
|
}
|
|
}
|
|
public class AttachPluginResp : JanusMessageResp
|
|
{
|
|
public AttachPluginResp(JanusMessageResp pResp) : base(pResp.RawBody)
|
|
{ }
|
|
public string pluginId { get {
|
|
return dataSection.ContainsKey("id") ? OSDToLong(dataSection["id"]).ToString() : String.Empty;
|
|
}}
|
|
}
|
|
// ==============================================================
|
|
public class DetachPluginReq : JanusMessageReq
|
|
{
|
|
public DetachPluginReq() : base("detach")
|
|
{
|
|
// 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.
|
|
// A plugin request is formatted like:
|
|
// {
|
|
// "janus": "message",
|
|
// "transaction": "baefcec8-70c5-4e79-b2c1-d653b9617dea",
|
|
// "body": {
|
|
// "request": "create",
|
|
// "room": 10,
|
|
// "is_private": false,
|
|
// }
|
|
public class PluginMsgReq : JanusMessageReq
|
|
{
|
|
private OSDMap m_body = new OSDMap();
|
|
|
|
// Note that the passed OSDMap is placed in the "body" section of the message
|
|
public PluginMsgReq(OSDMap pBody) : base("message")
|
|
{
|
|
m_body = pBody;
|
|
}
|
|
public void AddStringToBody(string pKey, string pValue)
|
|
{
|
|
m_body[pKey] = pValue;
|
|
}
|
|
public void AddIntToBody(string pKey, int pValue)
|
|
{
|
|
m_body[pKey] = pValue;
|
|
}
|
|
public void AddBoolToBody(string pKey, bool pValue)
|
|
{
|
|
m_body[pKey] = pValue;
|
|
}
|
|
public void AddOSDToBody(string pKey, OSD pValue)
|
|
{
|
|
m_body[pKey] = pValue;
|
|
}
|
|
|
|
public override string ToJson()
|
|
{
|
|
m_message["body"] = m_body;
|
|
return base.ToJson();
|
|
}
|
|
}
|
|
// A plugin response is formatted like:
|
|
// {
|
|
// "janus": "success",
|
|
// "session_id": 5645225333294848,
|
|
// "transaction": "baefcec8-70c5-4e79-b2c1-d653b9617dea",
|
|
// "sender": 6969906757968657,
|
|
// "plugindata": {
|
|
// "plugin": "janus.plugin.audiobridge",
|
|
// "data": {
|
|
// "audiobridge": "created",
|
|
// "room": 10,
|
|
// "permanent": false
|
|
// }
|
|
// }
|
|
public class PluginMsgResp : JanusMessageResp
|
|
{
|
|
public OSDMap m_pluginData;
|
|
public OSDMap m_data;
|
|
public PluginMsgResp(JanusMessageResp pResp) : base(pResp.RawBody)
|
|
{
|
|
if (m_message is not null && m_message.ContainsKey("plugindata"))
|
|
{
|
|
// Move the plugin data up into the m_data var so it is easier to get to
|
|
m_pluginData = m_message["plugindata"] as OSDMap;
|
|
if (m_pluginData is not null && m_pluginData.ContainsKey("data"))
|
|
{
|
|
m_data = m_pluginData["data"] as OSDMap;
|
|
// m_log.DebugFormat("{0} AudioBridgeResp. Found both plugindata and data: data={1}", LogHeader, m_data.ToString());
|
|
}
|
|
}
|
|
}
|
|
|
|
public OSDMap PluginRespData { get { return m_data; } }
|
|
|
|
// Get an integer value for a key in the response data or zero if not there
|
|
public int PluginRespDataInt(string pKey)
|
|
{
|
|
if (m_data is null)
|
|
return 0;
|
|
return m_data.ContainsKey(pKey) ? (int)OSDToLong(m_data[pKey]) : 0;
|
|
}
|
|
// Get a string value for a key in the response data or empty string if not there
|
|
public string PluginRespDataString(string pKey)
|
|
{
|
|
if (m_data is null)
|
|
return String.Empty;
|
|
return m_data.ContainsKey(pKey) ? m_data[pKey].AsString() : String.Empty;
|
|
}
|
|
}
|
|
// ==============================================================
|
|
// Plugin messages for the audio bridge.
|
|
// Audiobridge responses are formatted like:
|
|
// {
|
|
// "janus": "success",
|
|
// "session_id": 5645225333294848,
|
|
// "transaction": "baefcec8-70c5-4e79-b2c1-d653b9617dea",
|
|
// "sender": 6969906757968657,
|
|
// "plugindata": {
|
|
// "plugin": "janus.plugin.audiobridge",
|
|
// "data": {
|
|
// "audiobridge": "created",
|
|
// "room": 10,
|
|
// "permanent": false
|
|
// }
|
|
// }
|
|
public class AudioBridgeResp: PluginMsgResp
|
|
{
|
|
public AudioBridgeResp(JanusMessageResp pResp) : base(pResp)
|
|
{
|
|
}
|
|
public override bool isSuccess { get { return PluginRespDataString("audiobridge") == "success"; } }
|
|
// Return the return code if it is in the response or empty string if not
|
|
public string AudioBridgeReturnCode { get { return PluginRespDataString("audiobridge"); } }
|
|
// Return the error code if it is in the response or zero if not
|
|
public int AudioBridgeErrorCode { get { return PluginRespDataInt("error_code"); } }
|
|
// Return the room ID if it is in the response or zero if not
|
|
public int RoomId { get { return PluginRespDataInt("room"); } }
|
|
}
|
|
// ==============================================================
|
|
public class AudioBridgeCreateRoomReq : PluginMsgReq
|
|
{
|
|
public AudioBridgeCreateRoomReq(int pRoomId) : this(pRoomId, false, null)
|
|
{
|
|
}
|
|
public AudioBridgeCreateRoomReq(int pRoomId, bool pSpatial, string pDesc) : base(new OSDMap() {
|
|
{ "room", pRoomId },
|
|
{ "request", "create" },
|
|
{ "is_private", false },
|
|
{ "permanent", false },
|
|
{ "sampling_rate", 48000 },
|
|
{ "spatial_audio", pSpatial },
|
|
{ "denoise", false },
|
|
{ "record", false }
|
|
})
|
|
{
|
|
if (!String.IsNullOrEmpty(pDesc))
|
|
AddStringToBody("description", pDesc);
|
|
}
|
|
}
|
|
// ==============================================================
|
|
public class AudioBridgeDestroyRoomReq : PluginMsgReq
|
|
{
|
|
public AudioBridgeDestroyRoomReq(int pRoomId) : base(new OSDMap() {
|
|
{ "request", "destroy" },
|
|
{ "room", pRoomId },
|
|
{ "permanent", true }
|
|
})
|
|
{
|
|
}
|
|
}
|
|
// ==============================================================
|
|
public class AudioBridgeJoinRoomReq : PluginMsgReq
|
|
{
|
|
public AudioBridgeJoinRoomReq(int pRoomId, string pAgentName) : base(new OSDMap() {
|
|
{ "request", "join" },
|
|
{ "room", pRoomId },
|
|
{ "display", pAgentName }
|
|
})
|
|
{
|
|
}
|
|
}
|
|
// A successful response contains the participant ID and the SDP
|
|
public class AudioBridgeJoinRoomResp : AudioBridgeResp
|
|
{
|
|
public AudioBridgeJoinRoomResp(JanusMessageResp pResp) : base(pResp)
|
|
{
|
|
}
|
|
public int ParticipantId { get { return PluginRespDataInt("id"); } }
|
|
}
|
|
// ==============================================================
|
|
public class AudioBridgeConfigRoomReq : PluginMsgReq
|
|
{
|
|
// TODO:
|
|
public AudioBridgeConfigRoomReq(int pRoomId, string pSdp) : base(new OSDMap() {
|
|
{ "request", "configure" },
|
|
})
|
|
{
|
|
}
|
|
}
|
|
public class AudioBridgeConfigRoomResp : AudioBridgeResp
|
|
{
|
|
// TODO:
|
|
public AudioBridgeConfigRoomResp(JanusMessageResp pResp) : base(pResp)
|
|
{
|
|
}
|
|
}
|
|
// ==============================================================
|
|
public class AudioBridgeLeaveRoomReq : PluginMsgReq
|
|
{
|
|
public AudioBridgeLeaveRoomReq(int pRoomId, int pAttendeeId) : base(new OSDMap() {
|
|
{ "request", "leave" },
|
|
{ "room", pRoomId },
|
|
{ "id", pAttendeeId }
|
|
})
|
|
{
|
|
}
|
|
}
|
|
// ==============================================================
|
|
public class AudioBridgeListRoomsReq : PluginMsgReq
|
|
{
|
|
public AudioBridgeListRoomsReq() : base(new OSDMap() {
|
|
{ "request", "list" }
|
|
})
|
|
{
|
|
}
|
|
}
|
|
// ==============================================================
|
|
public class AudioBridgeListParticipantsReq : PluginMsgReq
|
|
{
|
|
public AudioBridgeListParticipantsReq(int pRoom) : base(new OSDMap() {
|
|
{ "request", "listparticipants" },
|
|
{ "room", pRoom }
|
|
})
|
|
{
|
|
}
|
|
}
|
|
// ==============================================================
|
|
public class AudioBridgeEvent : AudioBridgeResp
|
|
{
|
|
public AudioBridgeEvent(JanusMessageResp pResp) : base(pResp)
|
|
{
|
|
}
|
|
}
|
|
// ==============================================================
|
|
// The LongPoll request returns events from the plugins. These are formatted
|
|
// like the other responses but are not responses to requests.
|
|
// They are formatted like:
|
|
// {
|
|
// "janus": "event",
|
|
// "sender": 6969906757968657,
|
|
// "transaction": "baefcec8-70c5-4e79-b2c1-d653b9617dea",
|
|
// "plugindata": {
|
|
// "plugin": "janus.plugin.audiobridge",
|
|
// "data": {
|
|
// "audiobridge": "event",
|
|
// "room": 10,
|
|
// "participants": 1,
|
|
// "participants": [
|
|
// {
|
|
// "id": 1234,
|
|
// "display": "John Doe",
|
|
// "audio_level": 0.0,
|
|
// "video_room": false,
|
|
// "video_muted": false,
|
|
// "audio_muted": false,
|
|
// "feed": 1234
|
|
// }
|
|
// ]
|
|
// }
|
|
// }
|
|
public class EventResp : JanusMessageResp
|
|
{
|
|
public EventResp() : base()
|
|
{
|
|
}
|
|
|
|
public EventResp(string pType) : base(pType)
|
|
{
|
|
}
|
|
|
|
public EventResp(JanusMessageResp pResp) : base(pResp.RawBody)
|
|
{
|
|
}
|
|
}
|
|
// ==============================================================
|
|
}
|