Merge branch 'master' into lickx
This commit is contained in:
428
OpenSim/Addons/os-webrtc-janus/Janus/BHasher.cs
Normal file
428
OpenSim/Addons/os-webrtc-janus/Janus/BHasher.cs
Normal file
@@ -0,0 +1,428 @@
|
||||
/*
|
||||
* 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.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Security.Cryptography;
|
||||
|
||||
namespace osWebRtcVoice
|
||||
{
|
||||
|
||||
// There are several different hashing systems ranging from int's to SHA versions.
|
||||
// The model here is to create a hasher of the desired type, do Add's of things to
|
||||
// hash, and complete with a Finish() to return a BHash that contains the hash value.
|
||||
// Since some hash functions are incremental (doing Add's) while some are buffer
|
||||
// oriented (create a hash of a byte buffer), the interface is built to cover both.
|
||||
// Some optimizations are implemented internally (like not copying the buffer
|
||||
// for buffer based hashers if Finish(bytes) is used).
|
||||
//
|
||||
// var hasher = new BHashSHA256();
|
||||
// BHash bHash = hasher.Finish(buffer);
|
||||
// byte[] theHash = bHash.ToByte();
|
||||
//
|
||||
// Note that BHash has IEquatable and IComparible so it can be used in Dictionaries
|
||||
// and sorted Lists.
|
||||
//
|
||||
// The C# GetHashCode() method returns an int that is usually based on location.
|
||||
// Signatures should really be at least 64 bits so these routines generate
|
||||
// ulong's for hashes and fold them to make the int for GetHashCode().
|
||||
|
||||
// Create a BHasher, do a bunch of 'Add's, then Finish().
|
||||
public interface IBHasher {
|
||||
// Create a new object implementing BHasher, then Add values to be hashed
|
||||
void Add(byte c);
|
||||
void Add(ushort c);
|
||||
void Add(uint c);
|
||||
void Add(ulong c);
|
||||
void Add(float c);
|
||||
void Add(byte[] c, int offset, int len);
|
||||
void Add(string c);
|
||||
void Add(BHash c);
|
||||
|
||||
BHash Finish();
|
||||
// Finish and add byte array.
|
||||
// If no Add's before, can do the hashing without copying the byte array
|
||||
BHash Finish(byte[] c);
|
||||
BHash Finish(byte[] c, int offset, int len);
|
||||
// Get the hash code after doing a Finish()
|
||||
BHash Hash();
|
||||
}
|
||||
|
||||
// ======================================================================
|
||||
// BHasher computes a BHash which holds the hash value after Finish() is called.
|
||||
public abstract class BHash : IEquatable<BHash>, IComparable<BHash> {
|
||||
public abstract override string ToString();
|
||||
public abstract byte[] ToBytes();
|
||||
public abstract ulong ToULong(); // returns the hash of the hash if not int based hash
|
||||
public abstract bool Equals(BHash other);
|
||||
public abstract int CompareTo(BHash obj);
|
||||
// public abstract int Compare(BHash x, BHash y); // TODO: do we need this function?
|
||||
public abstract override int GetHashCode(); // to match the C# standard hash function
|
||||
}
|
||||
|
||||
// A hash that is an UInt64
|
||||
public class BHashULong : BHash {
|
||||
protected ulong _hash;
|
||||
|
||||
public BHashULong() {
|
||||
_hash = 5131;
|
||||
}
|
||||
public BHashULong(ulong initialHash) {
|
||||
_hash = initialHash;
|
||||
}
|
||||
// the .NET GetHashCode uses an int. Make conversion easy.
|
||||
public BHashULong(int initialHash) {
|
||||
_hash = (ulong)initialHash;
|
||||
}
|
||||
public override string ToString() {
|
||||
return _hash.ToString();
|
||||
}
|
||||
public override byte[] ToBytes() {
|
||||
return BitConverter.GetBytes(_hash);
|
||||
}
|
||||
public override ulong ToULong() {
|
||||
return _hash;
|
||||
}
|
||||
public override bool Equals(BHash other) {
|
||||
bool ret = false;
|
||||
if (other != null) {
|
||||
BHash bh = other as BHashULong;
|
||||
if (bh != null) {
|
||||
ret = _hash.Equals(bh.ToULong());
|
||||
}
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
public override int CompareTo(BHash other) {
|
||||
int ret = 1;
|
||||
if (other != null) {
|
||||
if (other is BHashULong bh) {
|
||||
ret = _hash.CompareTo(bh.ToULong());
|
||||
}
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
public override int GetHashCode() {
|
||||
ulong upper = (_hash >> 32) & 0xffffffff;
|
||||
ulong lower = _hash & 0xffffffff;
|
||||
return (int)(upper ^ lower);
|
||||
}
|
||||
}
|
||||
|
||||
// A hash that is an array of bytes
|
||||
public class BHashBytes : BHash {
|
||||
private readonly byte[] _hash;
|
||||
|
||||
public BHashBytes() {
|
||||
_hash = new byte[0];
|
||||
}
|
||||
public BHashBytes(byte[] initialHash) {
|
||||
_hash = initialHash;
|
||||
}
|
||||
public override string ToString() {
|
||||
// BitConverter puts a hyphen between each byte. Remove them
|
||||
return BitConverter.ToString(_hash).Replace("-", String.Empty);
|
||||
}
|
||||
public override byte[] ToBytes() {
|
||||
return _hash;
|
||||
}
|
||||
public override ulong ToULong() {
|
||||
return this.MakeHashCode();
|
||||
}
|
||||
public override bool Equals(BHash other) {
|
||||
bool ret = false;
|
||||
if (other != null) {
|
||||
BHash bh = other as BHashBytes;
|
||||
if (bh != null) {
|
||||
ret = _hash.Equals(bh.ToBytes());
|
||||
}
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
public override int CompareTo(BHash other) {
|
||||
int ret = 1;
|
||||
if (other != null) {
|
||||
BHash bh = other as BHashBytes;
|
||||
if (bh != null) {
|
||||
byte[] otherb = bh.ToBytes();
|
||||
if (_hash.Length != otherb.Length) {
|
||||
ret = _hash.Length.CompareTo(otherb.Length);
|
||||
}
|
||||
else {
|
||||
ret = 0; // start off assuming they are equal
|
||||
for (int ii = 0; ii < _hash.Length; ii++) {
|
||||
ret = _hash[ii].CompareTo(otherb[ii]);
|
||||
if (ret != 0) break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
public override int GetHashCode()
|
||||
{
|
||||
ulong hashhash = this.MakeHashCode();
|
||||
ulong upper = (hashhash >> 32 )& 0xffffffff;
|
||||
ulong lower = hashhash & 0xffffffff;
|
||||
return (int)(upper ^ lower);
|
||||
}
|
||||
public ulong MakeHashCode() {
|
||||
ulong h = 5381;
|
||||
for (int ii = 0; ii < _hash.Length; ii++) {
|
||||
h = ((h << 5) + h) + (ulong)(_hash[ii]);
|
||||
}
|
||||
return h;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// ======================================================================
|
||||
// ======================================================================
|
||||
public abstract class BHasher : IBHasher
|
||||
{
|
||||
public BHasher() {
|
||||
}
|
||||
|
||||
public abstract void Add(byte c);
|
||||
public abstract void Add(ushort c);
|
||||
public abstract void Add(uint c);
|
||||
public abstract void Add(ulong c);
|
||||
public abstract void Add(float c);
|
||||
public abstract void Add(byte[] c, int offset, int len);
|
||||
public abstract void Add(string c);
|
||||
public abstract void Add(BHash c);
|
||||
public abstract BHash Finish();
|
||||
public abstract BHash Finish(byte[] c);
|
||||
public abstract BHash Finish(byte[] c, int offset, int len);
|
||||
public abstract BHash Hash();
|
||||
}
|
||||
|
||||
// A hasher that builds up a buffer of bytes ('building') and then hashes over same
|
||||
public abstract class BHasherBytes : BHasher {
|
||||
protected byte[] building;
|
||||
protected int buildingLoc;
|
||||
protected int allocStep = 1024;
|
||||
|
||||
public BHasherBytes() : base() {
|
||||
building = new byte[allocStep];
|
||||
buildingLoc = 0;
|
||||
}
|
||||
|
||||
private byte[] oneByte = new byte[1];
|
||||
public override void Add(byte c) {
|
||||
oneByte[0] = c;
|
||||
AddBytes(oneByte, 0, 1);
|
||||
}
|
||||
|
||||
public override void Add(ushort c) {
|
||||
byte[] bytes = BitConverter.GetBytes(c);
|
||||
AddBytes(bytes, 0, bytes.Length);
|
||||
}
|
||||
|
||||
public override void Add(uint c) {
|
||||
byte[] bytes = BitConverter.GetBytes(c);
|
||||
AddBytes(bytes, 0, bytes.Length);
|
||||
}
|
||||
|
||||
public override void Add(ulong c) {
|
||||
byte[] bytes = BitConverter.GetBytes(c);
|
||||
AddBytes(bytes, 0, bytes.Length);
|
||||
}
|
||||
|
||||
public override void Add(float c) {
|
||||
byte[] bytes = BitConverter.GetBytes(c);
|
||||
AddBytes(bytes, 0, bytes.Length);
|
||||
}
|
||||
|
||||
public override void Add(byte[] c, int offset, int len) {
|
||||
AddBytes(c, offset, len);
|
||||
}
|
||||
|
||||
public override void Add(string c)
|
||||
{
|
||||
byte[] bytes = Encoding.UTF8.GetBytes(c);
|
||||
AddBytes(bytes, 0, bytes.Length);
|
||||
}
|
||||
|
||||
public override void Add(BHash c) {
|
||||
byte[] bytes = c.ToBytes();
|
||||
AddBytes(bytes, 0, bytes.Length);
|
||||
}
|
||||
|
||||
// Implemented by derived class
|
||||
// public abstract BHash Finish();
|
||||
|
||||
// Helper function for simple byte array
|
||||
public override BHash Finish(byte[] c) {
|
||||
return this.Finish(c, 0, c.Length);
|
||||
}
|
||||
|
||||
// Implemented by derived class
|
||||
// public abstract BHash Finish(byte[] c, int offset, int len);
|
||||
|
||||
// Implemented by derived class
|
||||
// public abstract BHash Hash();
|
||||
|
||||
// Add the given number of bytes to the byte array being built
|
||||
protected void AddBytes(byte[] addition, int offset, int len) {
|
||||
// byte[] tempBytes = new byte[len]; // DEBUG DEBUG
|
||||
// Array.Copy(addition, offset, tempBytes, 0, len); // DEBUG DEBUG
|
||||
// System.Console.WriteLine(String.Format("AddBytes: offset={0}, len={1}, bytes={2}", // DEBUG DEBUG
|
||||
// offset, len, BitConverter.ToString(tempBytes).Replace("-", String.Empty))); // DEBUG DEBUG
|
||||
if (len < 0 || offset < 0 || addition == null) {
|
||||
throw new ArgumentException(String.Format("BHasherBytes.AddBytes: Bad parameters. offset={0}, len={1}",
|
||||
offset, len));
|
||||
}
|
||||
if (offset + len > addition.Length) {
|
||||
throw new ArgumentException(String.Format("BHasherBytes.AddBytes: addition parameters off end of array. addition.len={0}, offset={1}, len={2}",
|
||||
addition.Length, offset, len));
|
||||
}
|
||||
if (len > 0) {
|
||||
if (buildingLoc + len > building.Length) {
|
||||
// New data requires expanding the data buffer
|
||||
byte[] newBuilding = new byte[buildingLoc + len + allocStep];
|
||||
Buffer.BlockCopy(building, 0, newBuilding, 0, buildingLoc);
|
||||
building = newBuilding;
|
||||
}
|
||||
Buffer.BlockCopy(addition, offset, building, buildingLoc, len);
|
||||
buildingLoc += len;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ======================================================================
|
||||
// ULong hash code taken from Meshmerizer
|
||||
public class BHasherMdjb2 : BHasherBytes, IBHasher {
|
||||
BHashULong hash = new BHashULong();
|
||||
|
||||
public BHasherMdjb2() : base() {
|
||||
}
|
||||
|
||||
public override BHash Finish() {
|
||||
hash = new BHashULong(ComputeMdjb2Hash(building, 0, buildingLoc));
|
||||
return hash;
|
||||
}
|
||||
|
||||
public override BHash Finish(byte[] c, int offset, int len) {
|
||||
if (building.Length > 0) {
|
||||
AddBytes(c, offset, len);
|
||||
hash = new BHashULong(ComputeMdjb2Hash(building, 0, buildingLoc));
|
||||
}
|
||||
else {
|
||||
// if no 'Add's were done, don't copy the input data
|
||||
hash = new BHashULong(ComputeMdjb2Hash(c, offset, len));
|
||||
}
|
||||
return hash;
|
||||
}
|
||||
|
||||
private ulong ComputeMdjb2Hash(byte[] c, int offset, int len) {
|
||||
ulong h = 5381;
|
||||
for (int ii = offset; ii < offset+len; ii++) {
|
||||
h = ((h << 5) + h) + (ulong)(c[ii]);
|
||||
}
|
||||
return h;
|
||||
}
|
||||
|
||||
public override BHash Hash() {
|
||||
return hash;
|
||||
}
|
||||
}
|
||||
|
||||
// ======================================================================
|
||||
public class BHasherMD5 : BHasherBytes, IBHasher {
|
||||
BHashBytes hash = new BHashBytes();
|
||||
|
||||
public BHasherMD5() : base() {
|
||||
}
|
||||
|
||||
public override BHash Finish() {
|
||||
MD5 md5 = MD5.Create();
|
||||
hash = new BHashBytes(md5.ComputeHash(building, 0, buildingLoc));
|
||||
return hash;
|
||||
}
|
||||
|
||||
public override BHash Finish(byte[] c) {
|
||||
return this.Finish(c, 0, c.Length);
|
||||
}
|
||||
|
||||
public override BHash Finish(byte[] c, int offset, int len) {
|
||||
MD5 md5 = MD5.Create();
|
||||
if (building.Length > 0) {
|
||||
AddBytes(c, offset, len);
|
||||
hash = new BHashBytes(md5.ComputeHash(building, 0, buildingLoc));
|
||||
}
|
||||
else {
|
||||
// if no 'Add's were done, don't copy the input data
|
||||
hash = new BHashBytes(md5.ComputeHash(c, offset, len));
|
||||
}
|
||||
return hash;
|
||||
}
|
||||
|
||||
public override BHash Hash() {
|
||||
return hash;
|
||||
}
|
||||
}
|
||||
|
||||
// ======================================================================
|
||||
public class BHasherSHA256 : BHasherBytes, IBHasher {
|
||||
BHashBytes hash = new BHashBytes();
|
||||
|
||||
public BHasherSHA256() : base() {
|
||||
}
|
||||
|
||||
public override BHash Finish() {
|
||||
using (SHA256 SHA256 = SHA256.Create()) {
|
||||
hash = new BHashBytes(SHA256.ComputeHash(building, 0, buildingLoc));
|
||||
}
|
||||
return hash;
|
||||
}
|
||||
|
||||
public override BHash Finish(byte[] c) {
|
||||
return this.Finish(c, 0, c.Length);
|
||||
}
|
||||
|
||||
public override BHash Finish(byte[] c, int offset, int len) {
|
||||
using (SHA256 SHA256 = SHA256.Create()) {
|
||||
if (buildingLoc > 0) {
|
||||
AddBytes(c, offset, len);
|
||||
hash = new BHashBytes(SHA256.ComputeHash(building, 0, buildingLoc));
|
||||
}
|
||||
else {
|
||||
// if no 'Add's were done, don't copy the input data
|
||||
hash = new BHashBytes(SHA256.ComputeHash(c, offset, len));
|
||||
}
|
||||
}
|
||||
return hash;
|
||||
}
|
||||
|
||||
public override BHash Hash() {
|
||||
return hash;
|
||||
}
|
||||
}
|
||||
}
|
||||
251
OpenSim/Addons/os-webrtc-janus/Janus/JanusAudioBridge.cs
Normal file
251
OpenSim/Addons/os-webrtc-janus/Janus/JanusAudioBridge.cs
Normal file
@@ -0,0 +1,251 @@
|
||||
/*
|
||||
* 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.Generic;
|
||||
using System.Reflection;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
using log4net;
|
||||
|
||||
namespace osWebRtcVoice
|
||||
{
|
||||
// Encapsulization of a Session to the Janus server
|
||||
public class JanusAudioBridge : JanusPlugin
|
||||
{
|
||||
private static readonly ILog m_log = LogManager.GetLogger(MethodBase.GetCurrentMethod().DeclaringType);
|
||||
private static readonly string LogHeader = "[JANUS AUDIO BRIDGE]";
|
||||
|
||||
// Wrapper around the session connection to Janus-gateway
|
||||
public JanusAudioBridge(JanusSession pSession) : base(pSession, "janus.plugin.audiobridge")
|
||||
{
|
||||
// m_log.DebugFormat("{0} JanusAudioBridge constructor", LogHeader);
|
||||
}
|
||||
|
||||
public override void Dispose()
|
||||
{
|
||||
if (IsConnected)
|
||||
{
|
||||
// Close the handle
|
||||
|
||||
}
|
||||
base.Dispose();
|
||||
}
|
||||
|
||||
public async Task<AudioBridgeResp> SendAudioBridgeMsg(PluginMsgReq pMsg)
|
||||
{
|
||||
AudioBridgeResp ret = null;
|
||||
try
|
||||
{
|
||||
ret = new AudioBridgeResp(await SendPluginMsg(pMsg));
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
m_log.ErrorFormat("{0} SendPluginMsg. Exception {1}", LogHeader, e);
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Create a room with the given criteria. This talks to Janus to create the room.
|
||||
/// If the room with this RoomId already exists, just return it.
|
||||
/// Janus could create and return the RoomId but this presumes that the Janus server
|
||||
/// is only being used for our voice service.
|
||||
/// </summary>
|
||||
/// <param name="pRoomId">integer room ID to create</param>
|
||||
/// <param name="pSpatial">boolean on whether room will be spatial or non-spatial</param>
|
||||
/// <param name="pRoomDesc">added as "description" to the created room</param>
|
||||
/// <returns></returns>
|
||||
public async Task<JanusRoom> CreateRoom(int pRoomId, bool pSpatial, string pRoomDesc)
|
||||
{
|
||||
JanusRoom ret = null;
|
||||
try
|
||||
{
|
||||
JanusMessageResp resp = await SendPluginMsg(new AudioBridgeCreateRoomReq(pRoomId, pSpatial, pRoomDesc));
|
||||
AudioBridgeResp abResp = new AudioBridgeResp(resp);
|
||||
|
||||
m_log.DebugFormat("{0} CreateRoom. ReturnCode: {1}", LogHeader, abResp.AudioBridgeReturnCode);
|
||||
switch (abResp.AudioBridgeReturnCode)
|
||||
{
|
||||
case "created":
|
||||
ret = new JanusRoom(this, pRoomId);
|
||||
break;
|
||||
case "event":
|
||||
if (abResp.AudioBridgeErrorCode == 486)
|
||||
{
|
||||
m_log.WarnFormat("{0} CreateRoom. Room {1} already exists. Reusing! {2}", LogHeader, pRoomId, abResp.ToString());
|
||||
// if room already exists, just use it
|
||||
ret = new JanusRoom(this, pRoomId);
|
||||
}
|
||||
else
|
||||
{
|
||||
m_log.ErrorFormat("{0} CreateRoom. XX Room creation failed: {1}", LogHeader, abResp.ToString());
|
||||
}
|
||||
break;
|
||||
default:
|
||||
m_log.ErrorFormat("{0} CreateRoom. YY Room creation failed: {1}", LogHeader, abResp.ToString());
|
||||
break;
|
||||
}
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
m_log.ErrorFormat("{0} CreateRoom. Exception {1}", LogHeader, e);
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
public async Task<bool> DestroyRoom(JanusRoom janusRoom)
|
||||
{
|
||||
bool ret = false;
|
||||
try
|
||||
{
|
||||
JanusMessageResp resp = await SendPluginMsg(new AudioBridgeDestroyRoomReq(janusRoom.RoomId));
|
||||
ret = true;
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
m_log.ErrorFormat("{0} DestroyRoom. Exception {1}", LogHeader, e);
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
// Constant used to denote that this is a spatial audio room for the region (as opposed to parcels)
|
||||
public const int REGION_ROOM_ID = -999;
|
||||
private Dictionary<int, JanusRoom> _rooms = new Dictionary<int, JanusRoom>();
|
||||
|
||||
// Calculate a room number for the given parameters. The room number is a hash of the parameters.
|
||||
// The attempt is to deterministicly create a room number so all regions will generate the
|
||||
// same room number across sessions and across the grid.
|
||||
// getHashCode() is not deterministic across sessions.
|
||||
public static int CalcRoomNumber(string pRegionId, string pChannelType, int pParcelLocalID, string pChannelID)
|
||||
{
|
||||
var hasher = new BHasherMdjb2();
|
||||
// If there is a channel specified it must be group
|
||||
switch (pChannelType)
|
||||
{
|
||||
case "local":
|
||||
// A "local" channel is unique to the region and parcel
|
||||
hasher.Add(pRegionId);
|
||||
hasher.Add(pChannelType);
|
||||
hasher.Add(pParcelLocalID);
|
||||
break;
|
||||
case "multiagent":
|
||||
// A "multiagent" channel is unique to the grid
|
||||
// should add a GridId here
|
||||
hasher.Add(pChannelID);
|
||||
hasher.Add(pChannelType);
|
||||
break;
|
||||
default:
|
||||
throw new Exception("Unknown channel type: " + pChannelType);
|
||||
}
|
||||
var hashed = hasher.Finish();
|
||||
// The "Abs()" is because Janus room number must be a positive integer
|
||||
// And note that this is the BHash.GetHashCode() and not Object.getHashCode().
|
||||
int roomNumber = Math.Abs(hashed.GetHashCode());
|
||||
return roomNumber;
|
||||
}
|
||||
public async Task<JanusRoom> SelectRoom(string pRegionId, string pChannelType, bool pSpatial, int pParcelLocalID, string pChannelID)
|
||||
{
|
||||
int roomNumber = CalcRoomNumber(pRegionId, pChannelType, pParcelLocalID, pChannelID);
|
||||
|
||||
// Should be unique for the given use and channel type
|
||||
m_log.DebugFormat("{0} SelectRoom: roomNumber={1}", LogHeader, roomNumber);
|
||||
|
||||
// Check to see if the room has already been created
|
||||
lock (_rooms)
|
||||
{
|
||||
if (_rooms.ContainsKey(roomNumber))
|
||||
{
|
||||
return _rooms[roomNumber];
|
||||
}
|
||||
}
|
||||
|
||||
// The room doesn't exist. Create it.
|
||||
string roomDesc = pRegionId + "/" + pChannelType + "/" + pParcelLocalID + "/" + pChannelID;
|
||||
JanusRoom ret = await CreateRoom(roomNumber, pSpatial, roomDesc);
|
||||
|
||||
JanusRoom existingRoom = null;
|
||||
if (ret is not null)
|
||||
{
|
||||
lock (_rooms)
|
||||
{
|
||||
if (_rooms.ContainsKey(roomNumber))
|
||||
{
|
||||
// If the room was created while we were waiting,
|
||||
existingRoom = _rooms[roomNumber];
|
||||
}
|
||||
else
|
||||
{
|
||||
// Our room is the first one created. Save it.
|
||||
_rooms[roomNumber] = ret;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (existingRoom is not null)
|
||||
{
|
||||
// The room we created was already created by someone else. Delete ours and use the existing one
|
||||
await DestroyRoom(ret);
|
||||
ret = existingRoom;
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
// Return the room with the given room ID or 'null' if no such room
|
||||
public JanusRoom GetRoom(int pRoomId)
|
||||
{
|
||||
JanusRoom ret = null;
|
||||
lock (_rooms)
|
||||
{
|
||||
_rooms.TryGetValue(pRoomId, out ret);
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
public override void Handle_Event(JanusMessageResp pResp)
|
||||
{
|
||||
base.Handle_Event(pResp);
|
||||
AudioBridgeResp abResp = new AudioBridgeResp(pResp);
|
||||
if (abResp is not null && abResp.AudioBridgeReturnCode == "event")
|
||||
{
|
||||
// An audio bridge event!
|
||||
m_log.DebugFormat("{0} Handle_Event. {1}", LogHeader, abResp.ToString());
|
||||
}
|
||||
|
||||
}
|
||||
public override void Handle_Message(JanusMessageResp pResp)
|
||||
{
|
||||
base.Handle_Message(pResp);
|
||||
AudioBridgeResp abResp = new AudioBridgeResp(pResp);
|
||||
if (abResp is not null && abResp.AudioBridgeReturnCode == "event")
|
||||
{
|
||||
// An audio bridge event!
|
||||
m_log.DebugFormat("{0} Handle_Event. {1}", LogHeader, abResp.ToString());
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
639
OpenSim/Addons/os-webrtc-janus/Janus/JanusMessages.cs
Normal file
639
OpenSim/Addons/os-webrtc-janus/Janus/JanusMessages.cs
Normal file
@@ -0,0 +1,639 @@
|
||||
/*
|
||||
* 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.TryGetValue("session_id", out OSD tmposd) ? OSDToLong(tmposd).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 static 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.TryGetOSDMap("data", out OSDMap osdm) ? osdm : 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)
|
||||
{
|
||||
}
|
||||
}
|
||||
// ==============================================================
|
||||
}
|
||||
161
OpenSim/Addons/os-webrtc-janus/Janus/JanusPlugin.cs
Normal file
161
OpenSim/Addons/os-webrtc-janus/Janus/JanusPlugin.cs
Normal file
@@ -0,0 +1,161 @@
|
||||
/*
|
||||
* 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 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 osWebRtcVoice
|
||||
{
|
||||
// Encapsulization of a Session to the Janus server
|
||||
public class JanusPlugin : IDisposable
|
||||
{
|
||||
private static readonly ILog m_log = LogManager.GetLogger(MethodBase.GetCurrentMethod().DeclaringType);
|
||||
private static readonly string LogHeader = "[JANUS PLUGIN]";
|
||||
|
||||
protected IConfigSource _Config;
|
||||
protected JanusSession _JanusSession;
|
||||
|
||||
public string PluginName { get; private set; }
|
||||
public string PluginId { get; private set; }
|
||||
public string PluginUri { get ; private set ; }
|
||||
|
||||
public bool IsConnected => !String.IsNullOrEmpty(PluginId);
|
||||
|
||||
// Wrapper around the session connection to Janus-gateway
|
||||
public JanusPlugin(JanusSession pSession, string pPluginName)
|
||||
{
|
||||
_JanusSession = pSession;
|
||||
PluginName = pPluginName;
|
||||
}
|
||||
|
||||
public virtual void Dispose()
|
||||
{
|
||||
if (IsConnected)
|
||||
{
|
||||
// Close the handle
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
public Task<JanusMessageResp> SendPluginMsg(OSDMap pParams)
|
||||
{
|
||||
return _JanusSession.SendToJanus(new PluginMsgReq(pParams), PluginUri);
|
||||
}
|
||||
public Task<JanusMessageResp> SendPluginMsg(PluginMsgReq pJMsg)
|
||||
{
|
||||
return _JanusSession.SendToJanus(pJMsg, PluginUri);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Make the create a handle to a plugin within the session.
|
||||
/// </summary>
|
||||
/// <returns>TRUE if handle was created successfully</returns>
|
||||
public async Task<bool> Activate(IConfigSource pConfig)
|
||||
{
|
||||
_Config = pConfig;
|
||||
|
||||
bool ret = false;
|
||||
try
|
||||
{
|
||||
var resp = await _JanusSession.SendToSession(new AttachPluginReq(PluginName));
|
||||
if (resp is not null && resp.isSuccess)
|
||||
{
|
||||
var handleResp = new AttachPluginResp(resp);
|
||||
PluginId = handleResp.pluginId;
|
||||
PluginUri = _JanusSession.SessionUri + "/" + PluginId;
|
||||
m_log.DebugFormat("{0} Activate. Plugin attached. ID={1}, URL={2}", LogHeader, PluginId, PluginUri);
|
||||
_JanusSession.PluginId = PluginId;
|
||||
_JanusSession.OnEvent += Handle_Event;
|
||||
_JanusSession.OnMessage += Handle_Message;
|
||||
ret = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
m_log.ErrorFormat("{0} Activate: failed to attach to plugin {1}", LogHeader, PluginName);
|
||||
}
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
m_log.ErrorFormat("{0} Activate: exception attaching to plugin {1}: {2}", LogHeader, PluginName, e);
|
||||
}
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
public virtual async Task<bool> Detach()
|
||||
{
|
||||
bool ret = false;
|
||||
if (!IsConnected || _JanusSession is null)
|
||||
{
|
||||
m_log.WarnFormat("{0} Detach. Not connected", LogHeader);
|
||||
return ret;
|
||||
}
|
||||
try
|
||||
{
|
||||
_JanusSession.OnEvent -= Handle_Event;
|
||||
_JanusSession.OnMessage -= Handle_Message;
|
||||
// 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;
|
||||
}
|
||||
|
||||
public virtual void Handle_Event(JanusMessageResp pResp)
|
||||
{
|
||||
m_log.DebugFormat("{0} Handle_Event: {1}", LogHeader, pResp.ToString());
|
||||
}
|
||||
public virtual void Handle_Message(JanusMessageResp pResp)
|
||||
{
|
||||
m_log.DebugFormat("{0} Handle_Message: {1}", LogHeader, pResp.ToString());
|
||||
}
|
||||
}
|
||||
}
|
||||
138
OpenSim/Addons/os-webrtc-janus/Janus/JanusRoom.cs
Normal file
138
OpenSim/Addons/os-webrtc-janus/Janus/JanusRoom.cs
Normal file
@@ -0,0 +1,138 @@
|
||||
/*
|
||||
* 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 OpenSim.Framework;
|
||||
using OpenSim.Services.Interfaces;
|
||||
using OpenSim.Services.Base;
|
||||
|
||||
using OpenMetaverse.StructuredData;
|
||||
using OpenMetaverse;
|
||||
|
||||
using Nini.Config;
|
||||
using log4net;
|
||||
using System.Threading.Tasks;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace osWebRtcVoice
|
||||
{
|
||||
// Encapsulization of a Session to the Janus server
|
||||
public class JanusRoom : IDisposable
|
||||
{
|
||||
private static readonly ILog m_log = LogManager.GetLogger(MethodBase.GetCurrentMethod().DeclaringType);
|
||||
private static readonly string LogHeader = "[JANUS ROOM]";
|
||||
|
||||
public int RoomId { get; private set; }
|
||||
|
||||
private JanusPlugin _AudioBridge;
|
||||
|
||||
// Wrapper around the session connection to Janus-gateway
|
||||
public JanusRoom(JanusPlugin pAudioBridge, int pRoomId)
|
||||
{
|
||||
_AudioBridge = pAudioBridge;
|
||||
RoomId = pRoomId;
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
// Close the room
|
||||
}
|
||||
|
||||
public async Task<bool> JoinRoom(JanusViewerSession pVSession)
|
||||
{
|
||||
bool ret = false;
|
||||
try
|
||||
{
|
||||
// m_log.DebugFormat("{0} JoinRoom. New joinReq for room {1}", LogHeader, RoomId);
|
||||
|
||||
// Discovered that AudioBridge doesn't care if the data portion is present
|
||||
// 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.ToString());
|
||||
// joinReq.SetJsep("offer", cleanSdp);
|
||||
joinReq.SetJsep("offer", pVSession.Offer);
|
||||
|
||||
JanusMessageResp resp = await _AudioBridge.SendPluginMsg(joinReq);
|
||||
AudioBridgeJoinRoomResp joinResp = new AudioBridgeJoinRoomResp(resp);
|
||||
|
||||
if (joinResp is not null && joinResp.AudioBridgeReturnCode == "joined")
|
||||
{
|
||||
pVSession.ParticipantId = joinResp.ParticipantId;
|
||||
pVSession.Answer = joinResp.Jsep;
|
||||
ret = true;
|
||||
m_log.DebugFormat("{0} JoinRoom. Joined room {1}. Participant={2}", LogHeader, RoomId, pVSession.ParticipantId);
|
||||
}
|
||||
else
|
||||
{
|
||||
m_log.ErrorFormat("{0} JoinRoom. Failed to join room {1}. Resp={2}", LogHeader, RoomId, joinResp.ToString());
|
||||
}
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
m_log.ErrorFormat("{0} JoinRoom. Exception {1}", LogHeader, e);
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
// TODO: this doesn't work.
|
||||
// Not sure if it is needed. Janus generates Hangup events when the viewer leaves.
|
||||
/*
|
||||
public async Task<bool> Hangup(JanusViewerSession pAttendeeSession)
|
||||
{
|
||||
bool ret = false;
|
||||
try
|
||||
{
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
m_log.ErrorFormat("{0} LeaveRoom. Exception {1}", LogHeader, e);
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
*/
|
||||
|
||||
public async Task<bool> LeaveRoom(JanusViewerSession pAttendeeSession)
|
||||
{
|
||||
bool ret = false;
|
||||
try
|
||||
{
|
||||
JanusMessageResp resp = await _AudioBridge.SendPluginMsg(
|
||||
new AudioBridgeLeaveRoomReq(RoomId, pAttendeeSession.ParticipantId));
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
m_log.ErrorFormat("{0} LeaveRoom. Exception {1}", LogHeader, e);
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
658
OpenSim/Addons/os-webrtc-janus/Janus/JanusSession.cs
Normal file
658
OpenSim/Addons/os-webrtc-janus/Janus/JanusSession.cs
Normal file
@@ -0,0 +1,658 @@
|
||||
/*
|
||||
* 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.Generic;
|
||||
using System.Net.Http;
|
||||
using System.Net.Mime;
|
||||
using System.Reflection;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
using OpenMetaverse.StructuredData;
|
||||
|
||||
using log4net;
|
||||
using log4net.Core;
|
||||
using System.Reflection.Metadata;
|
||||
using System.Threading;
|
||||
|
||||
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 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 CancellationTokenSource();
|
||||
private HttpClient _HttpClient = new HttpClient();
|
||||
|
||||
public bool IsConnected { get; set; }
|
||||
|
||||
// Wrapper around the session connection to Janus-gateway
|
||||
public JanusSession(string pServerURI, string pAPIToken, string pAdminURI, string pAdminToken, bool pDebugMessages = false)
|
||||
{
|
||||
m_log.DebugFormat("{0} JanusSession constructor", LogHeader);
|
||||
_JanusServerURI = pServerURI;
|
||||
_JanusAPIToken = pAPIToken;
|
||||
_JanusAdminURI = pAdminURI;
|
||||
_JanusAdminToken = pAdminToken;
|
||||
_MessageDetails = pDebugMessages;
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
ClearEventSubscriptions();
|
||||
if (IsConnected)
|
||||
{
|
||||
_ = DestroySession();
|
||||
}
|
||||
if (_HttpClient is not null)
|
||||
{
|
||||
_HttpClient.Dispose();
|
||||
_HttpClient = null;
|
||||
}
|
||||
}
|
||||
|
||||
/// <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
|
||||
{
|
||||
var resp = await SendToJanus(new CreateSessionReq());
|
||||
if (resp is not null && resp.isSuccess)
|
||||
{
|
||||
var sessionResp = new CreateSessionResp(resp);
|
||||
SessionId = sessionResp.returnedId;
|
||||
IsConnected = true;
|
||||
SessionUri = _JanusServerURI + "/" + SessionId;
|
||||
m_log.DebugFormat("{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.DebugFormat("{0} DestroySession. Destroyed", LogHeader);
|
||||
}
|
||||
else
|
||||
{
|
||||
if (resp.isError)
|
||||
{
|
||||
ErrorResp eResp = new ErrorResp(resp);
|
||||
switch (eResp.errorCode)
|
||||
{
|
||||
case 458:
|
||||
// This is the error code for a session that is already destroyed
|
||||
m_log.DebugFormat("{0} DestroySession: session already destroyed", LogHeader);
|
||||
break;
|
||||
case 459:
|
||||
// This is the error code for handle already destroyed
|
||||
m_log.DebugFormat("{0} DestroySession: Handle not found", LogHeader);
|
||||
break;
|
||||
default:
|
||||
m_log.ErrorFormat("{0} DestroySession: failed {1}", LogHeader, eResp.errorReason);
|
||||
break;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
m_log.ErrorFormat("{0} DestroySession: failed. Resp: {1}", LogHeader, resp.ToString());
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
m_log.ErrorFormat("{0} DestroySession: exception {1}", LogHeader, 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));
|
||||
}
|
||||
else
|
||||
{
|
||||
ret = await SendToJanusNoWait(new TrickleReq(pVSession), 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);
|
||||
}
|
||||
// ====================================================================
|
||||
// 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 Dictionary<string, OutstandingRequest> _OutstandingRequests = new Dictionary<string, OutstandingRequest>();
|
||||
|
||||
// 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)
|
||||
{
|
||||
AddJanusHeaders(pReq);
|
||||
// m_log.DebugFormat("{0} SendToJanus", LogHeader);
|
||||
if (_MessageDetails) m_log.DebugFormat("{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.Add(pReq.TransactionId, outReq);
|
||||
|
||||
string reqStr = pReq.ToJson();
|
||||
|
||||
HttpRequestMessage reqMsg = new HttpRequestMessage(HttpMethod.Post, pURI);
|
||||
reqMsg.Content = new StringContent(reqStr, System.Text.Encoding.UTF8, MediaTypeNames.Application.Json);
|
||||
reqMsg.Headers.Add("Accept", "application/json");
|
||||
HttpResponseMessage response = await _HttpClient.SendAsync(reqMsg, _CancelTokenSource.Token);
|
||||
|
||||
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.DebugFormat("{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
|
||||
}
|
||||
else
|
||||
{
|
||||
// If the response is not an ack, that means a synchronous request/response so return the response
|
||||
_OutstandingRequests.Remove(pReq.TransactionId);
|
||||
if (_MessageDetails) m_log.DebugFormat("{0} SendToJanus: response {1}", LogHeader, respStr);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
m_log.ErrorFormat("{0} SendToJanus: response not successful {1}", LogHeader, response);
|
||||
_OutstandingRequests.Remove(pReq.TransactionId);
|
||||
}
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
m_log.ErrorFormat("{0} SendToJanus: exception {1}", LogHeader, e.Message);
|
||||
}
|
||||
|
||||
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 JanusMessageResp();
|
||||
|
||||
AddJanusHeaders(pReq);
|
||||
|
||||
try {
|
||||
HttpRequestMessage reqMsg = new HttpRequestMessage(HttpMethod.Post, pURI);
|
||||
string reqStr = pReq.ToJson();
|
||||
reqMsg.Content = new StringContent(reqStr, System.Text.Encoding.UTF8, MediaTypeNames.Application.Json);
|
||||
reqMsg.Headers.Add("Accept", "application/json");
|
||||
HttpResponseMessage response = await _HttpClient.SendAsync(reqMsg);
|
||||
string respStr = await response.Content.ReadAsStringAsync();
|
||||
ret = JanusMessageResp.FromJson(respStr);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
m_log.ErrorFormat("{0} SendToJanusNoWait: exception {1}", LogHeader, 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)
|
||||
{
|
||||
// Authentication token
|
||||
if (!String.IsNullOrEmpty(_JanusAPIToken))
|
||||
{
|
||||
pReq.AddAPIToken(_JanusAPIToken);
|
||||
}
|
||||
// Transaction ID that matches responses to requests
|
||||
if (String.IsNullOrEmpty(pReq.TransactionId))
|
||||
{
|
||||
pReq.TransactionId = Guid.NewGuid().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;
|
||||
}
|
||||
|
||||
bool ret = false;
|
||||
lock (_OutstandingRequests)
|
||||
{
|
||||
if (_OutstandingRequests.TryGetValue(pTransactionId, out pOutstandingRequest))
|
||||
{
|
||||
_OutstandingRequests.Remove(pTransactionId);
|
||||
ret = true;
|
||||
}
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
public Task<JanusMessageResp> SendToJanusAdmin(JanusMessageReq pReq)
|
||||
{
|
||||
return SendToJanus(pReq, _JanusAdminURI);
|
||||
}
|
||||
|
||||
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)
|
||||
{
|
||||
if (!String.IsNullOrEmpty(_JanusAPIToken))
|
||||
{
|
||||
pURI += "?apisecret=" + _JanusAPIToken;
|
||||
}
|
||||
|
||||
JanusMessageResp ret = null;
|
||||
try
|
||||
{
|
||||
// m_log.DebugFormat("{0} GetFromJanus: URI = \"{1}\"", LogHeader, pURI);
|
||||
HttpRequestMessage reqMsg = new HttpRequestMessage(HttpMethod.Get, pURI);
|
||||
reqMsg.Headers.Add("Accept", "application/json");
|
||||
HttpResponseMessage response = null;
|
||||
try
|
||||
{
|
||||
response = await _HttpClient.SendAsync(reqMsg, _CancelTokenSource.Token);
|
||||
if (response is not null && response.IsSuccessStatusCode)
|
||||
{
|
||||
string respStr = await response.Content.ReadAsStringAsync();
|
||||
ret = JanusMessageResp.FromJson(respStr);
|
||||
// m_log.DebugFormat("{0} GetFromJanus: response {1}", LogHeader, respStr);
|
||||
}
|
||||
else
|
||||
{
|
||||
m_log.ErrorFormat("{0} GetFromJanus: response not successful {1}", LogHeader, response);
|
||||
var eResp = new ErrorResp("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)
|
||||
{
|
||||
m_log.DebugFormat("{0} GetFromJanus: task canceled: {1}", LogHeader, e.Message);
|
||||
var eResp = new ErrorResp("GETERROR");
|
||||
eResp.SetError(499, "Task canceled");
|
||||
ret = eResp;
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
m_log.ErrorFormat("{0} GetFromJanus: exception {1}", LogHeader, e.Message);
|
||||
var eResp = new ErrorResp("GETERROR");
|
||||
eResp.SetError(400, "Exception: " + e.Message);
|
||||
ret = eResp;
|
||||
}
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
m_log.ErrorFormat("{0} GetFromJanus: exception {1}", LogHeader, e);
|
||||
var eResp = new ErrorResp("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.DebugFormat("{0} EventLongPoll", LogHeader);
|
||||
Task.Run(async () => {
|
||||
while (running && IsConnected)
|
||||
{
|
||||
try
|
||||
{
|
||||
var resp = await GetFromJanus(SessionUri);
|
||||
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.DebugFormat("{0} EventLongPoll: server_info {1}", LogHeader, resp.ToString());
|
||||
break;
|
||||
case "ack":
|
||||
// 'ack' says the request was received and an event will follow
|
||||
if (_MessageDetails) m_log.DebugFormat("{0} EventLongPoll: ack {1}", LogHeader, resp.ToString());
|
||||
break;
|
||||
case "success":
|
||||
// success is a sync response that says the request was completed
|
||||
if (_MessageDetails) m_log.DebugFormat("{0} EventLongPoll: success {1}", LogHeader, resp.ToString());
|
||||
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.DebugFormat("{0} EventLongPoll: trickle {1}", LogHeader, resp.ToString());
|
||||
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());
|
||||
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());
|
||||
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(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());
|
||||
break;
|
||||
case "slowlink":
|
||||
// Janus detected a slowlink (uplink: true/false) on this PeerConnection;
|
||||
m_log.DebugFormat("{0} EventLongPoll: slowlink {1}", LogHeader, resp.ToString());
|
||||
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.ErrorFormat("{0} EventLongPoll: error with no transaction. {1}", LogHeader, resp.ToString());
|
||||
}
|
||||
break;
|
||||
case "event":
|
||||
if (_MessageDetails) m_log.DebugFormat("{0} EventLongPoll: event {1}", LogHeader, resp.ToString());
|
||||
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());
|
||||
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
|
||||
var errorResp = new ErrorResp(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
|
||||
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);
|
||||
}
|
||||
}
|
||||
m_log.InfoFormat("{0} EventLongPoll: Exiting long poll loop", LogHeader);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
109
OpenSim/Addons/os-webrtc-janus/Janus/JanusViewerSession.cs
Normal file
109
OpenSim/Addons/os-webrtc-janus/Janus/JanusViewerSession.cs
Normal file
@@ -0,0 +1,109 @@
|
||||
/*
|
||||
* 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.Reflection;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
using OMV = OpenMetaverse;
|
||||
using OpenMetaverse.StructuredData;
|
||||
|
||||
using log4net;
|
||||
|
||||
namespace osWebRtcVoice
|
||||
{
|
||||
public class JanusViewerSession : IVoiceViewerSession
|
||||
{
|
||||
protected static readonly ILog m_log = LogManager.GetLogger(MethodBase.GetCurrentMethod().DeclaringType);
|
||||
protected static readonly string LogHeader = "[JANUS VIEWER SESSION]";
|
||||
|
||||
// 'viewer_session' that is passed to and from the viewer
|
||||
// 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 OMV.UUID RegionId { get; set; }
|
||||
// IVoiceViewerSession.AgentId
|
||||
public OMV.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; }
|
||||
|
||||
public JanusViewerSession(IWebRtcVoiceService pVoiceService)
|
||||
{
|
||||
ViewerSessionID = OMV.UUID.Random().ToString();
|
||||
VoiceService = pVoiceService;
|
||||
m_log.DebugFormat("{0} JanusViewerSession created {1}", LogHeader, ViewerSessionID);
|
||||
}
|
||||
public JanusViewerSession(string pViewerSessionID, IWebRtcVoiceService pVoiceService)
|
||||
{
|
||||
ViewerSessionID = pViewerSessionID;
|
||||
VoiceService = pVoiceService;
|
||||
m_log.DebugFormat("{0} JanusViewerSession created {1}", LogHeader, ViewerSessionID);
|
||||
}
|
||||
|
||||
// Send the messages to the voice service to try and get rid of the session
|
||||
// IVoiceViewerSession.Shutdown
|
||||
public async Task Shutdown()
|
||||
{
|
||||
m_log.DebugFormat("{0} JanusViewerSession shutdown {1}", LogHeader, ViewerSessionID);
|
||||
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();
|
||||
s.Dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
465
OpenSim/Addons/os-webrtc-janus/Janus/WebRtcJanusService.cs
Normal file
465
OpenSim/Addons/os-webrtc-janus/Janus/WebRtcJanusService.cs
Normal file
@@ -0,0 +1,465 @@
|
||||
/*
|
||||
* 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 System.Threading.Tasks;
|
||||
|
||||
using OpenSim.Framework;
|
||||
using OpenSim.Services.Base;
|
||||
|
||||
using OpenMetaverse.StructuredData;
|
||||
using OpenMetaverse;
|
||||
|
||||
using Nini.Config;
|
||||
using log4net;
|
||||
|
||||
namespace osWebRtcVoice
|
||||
{
|
||||
public class WebRtcJanusService : ServiceBase, IWebRtcVoiceService
|
||||
{
|
||||
private static readonly ILog _log = LogManager.GetLogger(MethodBase.GetCurrentMethod().DeclaringType);
|
||||
private static readonly string LogHeader = "[JANUS WEBRTC SERVICE]";
|
||||
|
||||
private readonly IConfigSource _Config;
|
||||
private bool _Enabled = false;
|
||||
|
||||
private string _JanusServerURI = String.Empty;
|
||||
private string _JanusAPIToken = String.Empty;
|
||||
private string _JanusAdminURI = String.Empty;
|
||||
private string _JanusAdminToken = String.Empty;
|
||||
|
||||
private bool _MessageDetails = false;
|
||||
|
||||
// An extra "viewer session" that is created initially. Used to verify the service
|
||||
// is working and for a handle for the console commands.
|
||||
private JanusViewerSession _ViewerSession;
|
||||
|
||||
public WebRtcJanusService(IConfigSource pConfig) : base(pConfig)
|
||||
{
|
||||
Assembly assembly = Assembly.GetExecutingAssembly();
|
||||
string version = assembly.GetName().Version?.ToString() ?? "unknown";
|
||||
|
||||
_log.DebugFormat("{0} WebRtcJanusService version {1}", LogHeader, version);
|
||||
_Config = pConfig;
|
||||
IConfig webRtcVoiceConfig = _Config.Configs["WebRtcVoice"];
|
||||
|
||||
if (webRtcVoiceConfig is not null)
|
||||
{
|
||||
_Enabled = webRtcVoiceConfig.GetBoolean("Enabled", false);
|
||||
IConfig janusConfig = _Config.Configs["JanusWebRtcVoice"];
|
||||
if (_Enabled && janusConfig is not null)
|
||||
{
|
||||
_JanusServerURI = janusConfig.GetString("JanusGatewayURI", String.Empty);
|
||||
_JanusAPIToken = janusConfig.GetString("APIToken", String.Empty);
|
||||
_JanusAdminURI = janusConfig.GetString("JanusGatewayAdminURI", String.Empty);
|
||||
_JanusAdminToken = janusConfig.GetString("AdminAPIToken", String.Empty);
|
||||
// Debugging options
|
||||
_MessageDetails = janusConfig.GetBoolean("MessageDetails", false);
|
||||
|
||||
if (String.IsNullOrEmpty(_JanusServerURI) || String.IsNullOrEmpty(_JanusAPIToken) ||
|
||||
String.IsNullOrEmpty(_JanusAdminURI) || String.IsNullOrEmpty(_JanusAdminToken))
|
||||
{
|
||||
_log.ErrorFormat("{0} JanusWebRtcVoice configuration section missing required fields", LogHeader);
|
||||
_Enabled = false;
|
||||
}
|
||||
|
||||
if (_Enabled)
|
||||
{
|
||||
_log.DebugFormat("{0} Enabled", LogHeader);
|
||||
StartConnectionToJanus();
|
||||
RegisterConsoleCommands();
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
_log.ErrorFormat("{0} No JanusWebRtcVoice configuration section", LogHeader);
|
||||
_Enabled = false;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
_log.ErrorFormat("{0} No WebRtcVoice configuration section", LogHeader);
|
||||
_Enabled = false;
|
||||
}
|
||||
}
|
||||
|
||||
// Start a thread to do the connection to the Janus server.
|
||||
// Here an initial session is created and then a handle to the audio bridge plugin
|
||||
// is created for the console commands. Since webrtc PeerConnections that are created
|
||||
// my Janus are per-session, the other sessions will be created by the viewer requests.
|
||||
private void StartConnectionToJanus()
|
||||
{
|
||||
_log.DebugFormat("{0} StartConnectionToJanus", LogHeader);
|
||||
Task.Run(async () =>
|
||||
{
|
||||
_ViewerSession = new JanusViewerSession(this);
|
||||
await ConnectToSessionAndAudioBridge(_ViewerSession);
|
||||
});
|
||||
}
|
||||
|
||||
private async Task ConnectToSessionAndAudioBridge(JanusViewerSession pViewerSession)
|
||||
{
|
||||
JanusSession janusSession = new JanusSession(_JanusServerURI, _JanusAPIToken, _JanusAdminURI, _JanusAdminToken, _MessageDetails);
|
||||
if (await janusSession.CreateSession())
|
||||
{
|
||||
_log.DebugFormat("{0} JanusSession created", LogHeader);
|
||||
janusSession.OnDisconnect += Handle_Hangup;
|
||||
|
||||
// Once the session is created, create a handle to the plugin for rooms
|
||||
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);
|
||||
// Requests through the capabilities will create rooms
|
||||
}
|
||||
else
|
||||
{
|
||||
_log.ErrorFormat("{0} JanusPluginHandle not created", LogHeader);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
_log.ErrorFormat("{0} JanusSession not created", LogHeader);
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
DisconnectViewerSession(viewerSession as JanusViewerSession);
|
||||
}
|
||||
else
|
||||
{
|
||||
_log.DebugFormat("{0} Handle_Hangup: no session found. SessionId={1}", LogHeader, sessionId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Disconnect the viewer session. This is called when the viewer logs out or hangs up.
|
||||
private void DisconnectViewerSession(JanusViewerSession pViewerSession)
|
||||
{
|
||||
if (pViewerSession is not null)
|
||||
{
|
||||
Task.Run(() =>
|
||||
{
|
||||
VoiceViewerSession.RemoveViewerSession(pViewerSession.ViewerSessionID);
|
||||
// No need to wait for the session to be shutdown
|
||||
_ = pViewerSession.Shutdown();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 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.
|
||||
// IWebRtcVoiceService.ProvisionVoiceAccountRequest
|
||||
public async Task<OSDMap> ProvisionVoiceAccountRequest(IVoiceViewerSession pSession, OSDMap pRequest, UUID pUserID, UUID pSceneID)
|
||||
{
|
||||
OSDMap ret = null;
|
||||
string errorMsg = null;
|
||||
JanusViewerSession viewerSession = pSession as JanusViewerSession;
|
||||
if (viewerSession is not null)
|
||||
{
|
||||
if (viewerSession.Session is null)
|
||||
{
|
||||
// This is a new session so we must create a new session and handle to the audio bridge
|
||||
await ConnectToSessionAndAudioBridge(viewerSession);
|
||||
}
|
||||
|
||||
// TODO: need to keep count of users in a room to know when to close a room
|
||||
bool isLogout = pRequest.ContainsKey("logout") && pRequest["logout"].AsBoolean();
|
||||
if (isLogout)
|
||||
{
|
||||
// The client is logging out. Exit the room.
|
||||
if (viewerSession.Room is not null)
|
||||
{
|
||||
await viewerSession.Room.LeaveRoom(viewerSession);
|
||||
viewerSession.Room = null;
|
||||
}
|
||||
return new OSDMap
|
||||
{
|
||||
{ "response", "closed" }
|
||||
};
|
||||
}
|
||||
|
||||
// Get the parameters that select the room
|
||||
// To get here, voice_server_type has already been checked to be 'webrtc' and channel_type='local'
|
||||
int parcel_local_id = pRequest.ContainsKey("parcel_local_id") ? pRequest["parcel_local_id"].AsInteger() : JanusAudioBridge.REGION_ROOM_ID;
|
||||
string channel_id = pRequest.ContainsKey("channel_id") ? pRequest["channel_id"].AsString() : String.Empty;
|
||||
string channel_credentials = pRequest.ContainsKey("credentials") ? pRequest["credentials"].AsString() : String.Empty;
|
||||
string channel_type = pRequest["channel_type"].AsString();
|
||||
bool isSpatial = channel_type == "local";
|
||||
string voice_server_type = pRequest["voice_server_type"].AsString();
|
||||
|
||||
_log.DebugFormat("{0} ProvisionVoiceAccountRequest: parcel_id={1} channel_id={2} channel_type={3} voice_server_type={4}", LogHeader, parcel_local_id, channel_id, channel_type, voice_server_type);
|
||||
|
||||
if (pRequest.ContainsKey("jsep") && pRequest["jsep"] is OSDMap jsep)
|
||||
{
|
||||
// The jsep is the SDP from the client. This is the client's request to connect to the audio bridge.
|
||||
string jsepType = jsep["type"].AsString();
|
||||
string jsepSdp = jsep["sdp"].AsString();
|
||||
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);
|
||||
viewerSession.Room = await viewerSession.AudioBridge.SelectRoom(pSceneID.ToString(),
|
||||
channel_type, isSpatial, parcel_local_id, channel_id);
|
||||
if (viewerSession.Room is null)
|
||||
{
|
||||
errorMsg = "room selection failed";
|
||||
_log.ErrorFormat("{0} ProvisionVoiceAccountRequest: room selection failed", LogHeader);
|
||||
}
|
||||
else {
|
||||
viewerSession.Offer = jsepSdp;
|
||||
viewerSession.OfferOrig = jsepSdp;
|
||||
viewerSession.AgentId = pUserID;
|
||||
if (await viewerSession.Room.JoinRoom(viewerSession))
|
||||
{
|
||||
ret = new OSDMap
|
||||
{
|
||||
{ "jsep", viewerSession.Answer },
|
||||
{ "viewer_session", viewerSession.ViewerSessionID }
|
||||
};
|
||||
}
|
||||
else
|
||||
{
|
||||
errorMsg = "JoinRoom failed";
|
||||
_log.ErrorFormat("{0} ProvisionVoiceAccountRequest: JoinRoom failed", LogHeader);
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
errorMsg = "jsep type not offer";
|
||||
_log.ErrorFormat("{0} ProvisionVoiceAccountRequest: jsep type={1} not offer", LogHeader, jsepType);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
errorMsg = "no jsep";
|
||||
_log.DebugFormat("{0} ProvisionVoiceAccountRequest: no jsep. req={1}", LogHeader, pRequest.ToString());
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
errorMsg = "viewersession not JanusViewerSession";
|
||||
_log.ErrorFormat("{0} ProvisionVoiceAccountRequest: viewersession not JanusViewerSession", LogHeader);
|
||||
}
|
||||
|
||||
if (!String.IsNullOrEmpty(errorMsg) && ret is null)
|
||||
{
|
||||
// The provision failed so build an error messgage to return
|
||||
ret = new OSDMap
|
||||
{
|
||||
{ "response", "failed" },
|
||||
{ "error", errorMsg }
|
||||
};
|
||||
}
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
// IWebRtcVoiceService.VoiceAccountBalanceRequest
|
||||
public async Task<OSDMap> VoiceSignalingRequest(IVoiceViewerSession pSession, OSDMap pRequest, UUID pUserID, UUID pSceneID)
|
||||
{
|
||||
OSDMap ret = null;
|
||||
JanusViewerSession viewerSession = pSession as JanusViewerSession;
|
||||
JanusMessageResp resp = null;
|
||||
if (viewerSession is not null)
|
||||
{
|
||||
// The request should be an array of candidates
|
||||
if (pRequest.ContainsKey("candidate") && pRequest["candidate"] is OSDMap candidate)
|
||||
{
|
||||
if (candidate.ContainsKey("completed") && candidate["completed"].AsBoolean())
|
||||
{
|
||||
// The client has finished sending candidates
|
||||
resp = await viewerSession.Session.TrickleCompleted(viewerSession);
|
||||
_log.DebugFormat("{0} VoiceSignalingRequest: candidate completed", LogHeader);
|
||||
}
|
||||
else
|
||||
{
|
||||
}
|
||||
}
|
||||
else if (pRequest.ContainsKey("candidates") && pRequest["candidates"] is OSDArray candidates)
|
||||
{
|
||||
OSDArray candidatesArray = new OSDArray();
|
||||
foreach (OSDMap cand in candidates)
|
||||
{
|
||||
candidatesArray.Add(new OSDMap() {
|
||||
{ "candidate", cand["candidate"].AsString() },
|
||||
{ "sdpMid", cand["sdpMid"].AsString() },
|
||||
{ "sdpMLineIndex", cand["sdpMLineIndex"].AsLong() }
|
||||
});
|
||||
}
|
||||
resp = await viewerSession.Session.TrickleCandidates(viewerSession, candidatesArray);
|
||||
_log.DebugFormat("{0} VoiceSignalingRequest: {1} candidates", LogHeader, candidatesArray.Count);
|
||||
}
|
||||
else
|
||||
{
|
||||
_log.ErrorFormat("{0} VoiceSignalingRequest: no 'candidate' or 'candidates'", LogHeader);
|
||||
}
|
||||
}
|
||||
if (resp is null)
|
||||
{
|
||||
_log.ErrorFormat("{0} VoiceSignalingRequest: no response so returning error", LogHeader);
|
||||
ret = new OSDMap
|
||||
{
|
||||
{ "response", "error" }
|
||||
};
|
||||
}
|
||||
else
|
||||
{
|
||||
ret = resp.RawBody;
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
// This module should not be invoked with this signature
|
||||
// IWebRtcVoiceService.ProvisionVoiceAccountRequest
|
||||
public Task<OSDMap> ProvisionVoiceAccountRequest(OSDMap pRequest, UUID pUserID, UUID pSceneID)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
// This module should not be invoked with this signature
|
||||
// IWebRtcVoiceService.VoiceSignalingRequest
|
||||
public Task<OSDMap> VoiceSignalingRequest(OSDMap pRequest, UUID pUserID, UUID pSceneID)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
// The viewer session object holds all the connection information to Janus.
|
||||
// IWebRtcVoiceService.CreateViewerSession
|
||||
public IVoiceViewerSession CreateViewerSession(OSDMap pRequest, UUID pUserID, UUID pSceneID)
|
||||
{
|
||||
return new JanusViewerSession(this)
|
||||
{
|
||||
AgentId = pUserID,
|
||||
RegionId = pSceneID
|
||||
};
|
||||
}
|
||||
|
||||
// ======================================================================================================
|
||||
private void RegisterConsoleCommands()
|
||||
{
|
||||
if (_Enabled) {
|
||||
MainConsole.Instance.Commands.AddCommand("Webrtc", false, "janus info",
|
||||
"janus info",
|
||||
"Show Janus server information",
|
||||
HandleJanusInfo);
|
||||
MainConsole.Instance.Commands.AddCommand("Webrtc", false, "janus list rooms",
|
||||
"janus list rooms",
|
||||
"List the rooms on the Janus server",
|
||||
HandleJanusListRooms);
|
||||
// List rooms
|
||||
// List participants in a room
|
||||
}
|
||||
}
|
||||
|
||||
private async void HandleJanusInfo(string module, string[] cmdparms)
|
||||
{
|
||||
if (_ViewerSession is not null && _ViewerSession.Session is not null)
|
||||
{
|
||||
WriteOut("{0} Janus session: {1}", LogHeader, _ViewerSession.Session.SessionId);
|
||||
string infoURI = _ViewerSession.Session.JanusServerURI + "/info";
|
||||
var resp = await _ViewerSession.Session.GetFromJanus(infoURI);
|
||||
if (resp is not null)
|
||||
MainConsole.Instance.Output(resp.ToJson());
|
||||
}
|
||||
}
|
||||
|
||||
private async void HandleJanusListRooms(string module, string[] cmdparms)
|
||||
{
|
||||
if (_ViewerSession is not null && _ViewerSession.Session is not null && _ViewerSession.AudioBridge is not null)
|
||||
{
|
||||
var ab = _ViewerSession.AudioBridge;
|
||||
var resp = await ab.SendAudioBridgeMsg(new AudioBridgeListRoomsReq());
|
||||
if (resp is not null && resp.isSuccess)
|
||||
{
|
||||
if (resp.PluginRespData.TryGetValue("list", out OSD list))
|
||||
{
|
||||
MainConsole.Instance.Output("");
|
||||
MainConsole.Instance.Output(
|
||||
" {0,10} {1,15} {2,5} {3,10} {4,7} {5,7}",
|
||||
"Room", "Description", "Num", "SampleRate", "Spatial", "Recording");
|
||||
foreach (OSDMap room in list as OSDArray)
|
||||
{
|
||||
MainConsole.Instance.Output(
|
||||
" {0,10} {1,15} {2,5} {3,10} {4,7} {5,7}",
|
||||
room["room"], room["description"], room["num_participants"],
|
||||
room["sampling_rate"], room["spatial_audio"], room["record"]);
|
||||
var participantResp = await ab.SendAudioBridgeMsg(new AudioBridgeListParticipantsReq(room["room"].AsInteger()));
|
||||
if (participantResp is not null && participantResp.AudioBridgeReturnCode == "participants")
|
||||
{
|
||||
if (participantResp.PluginRespData.TryGetValue("participants", out OSD participants))
|
||||
{
|
||||
foreach (OSDMap participant in participants as OSDArray)
|
||||
{
|
||||
MainConsole.Instance.Output(" {0}/{1},muted={2},talking={3},pos={4}",
|
||||
participant["id"].AsLong(), participant["display"], participant["muted"],
|
||||
participant["talking"], participant["spatial_position"]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
MainConsole.Instance.Output("No rooms");
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
MainConsole.Instance.Output("Failed to get room list");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void WriteOut(string msg, params object[] args)
|
||||
{
|
||||
// m_log.InfoFormat(msg, args);
|
||||
MainConsole.Instance.Output(msg, args);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
390
OpenSim/Addons/os-webrtc-janus/LICENSE
Normal file
390
OpenSim/Addons/os-webrtc-janus/LICENSE
Normal file
@@ -0,0 +1,390 @@
|
||||
os-webrtc-janus original work, by Robert Adams, license changed to same BSD of rest of OpenSimulator
|
||||
By Robert Adams, for OpenSimulator
|
||||
|
||||
Original license:
|
||||
|
||||
// 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.
|
||||
|
||||
Mozilla Public License Version 2.0
|
||||
==================================
|
||||
|
||||
1. Definitions
|
||||
--------------
|
||||
|
||||
1.1. "Contributor"
|
||||
means each individual or legal entity that creates, contributes to
|
||||
the creation of, or owns Covered Software.
|
||||
|
||||
1.2. "Contributor Version"
|
||||
means the combination of the Contributions of others (if any) used
|
||||
by a Contributor and that particular Contributor's Contribution.
|
||||
|
||||
1.3. "Contribution"
|
||||
means Covered Software of a particular Contributor.
|
||||
|
||||
1.4. "Covered Software"
|
||||
means Source Code Form to which the initial Contributor has attached
|
||||
the notice in Exhibit A, the Executable Form of such Source Code
|
||||
Form, and Modifications of such Source Code Form, in each case
|
||||
including portions thereof.
|
||||
|
||||
1.5. "Incompatible With Secondary Licenses"
|
||||
means
|
||||
|
||||
(a) that the initial Contributor has attached the notice described
|
||||
in Exhibit B to the Covered Software; or
|
||||
|
||||
(b) that the Covered Software was made available under the terms of
|
||||
version 1.1 or earlier of the License, but not also under the
|
||||
terms of a Secondary License.
|
||||
|
||||
1.6. "Executable Form"
|
||||
means any form of the work other than Source Code Form.
|
||||
|
||||
1.7. "Larger Work"
|
||||
means a work that combines Covered Software with other material, in
|
||||
a separate file or files, that is not Covered Software.
|
||||
|
||||
1.8. "License"
|
||||
means this document.
|
||||
|
||||
1.9. "Licensable"
|
||||
means having the right to grant, to the maximum extent possible,
|
||||
whether at the time of the initial grant or subsequently, any and
|
||||
all of the rights conveyed by this License.
|
||||
|
||||
1.10. "Modifications"
|
||||
means any of the following:
|
||||
|
||||
(a) any file in Source Code Form that results from an addition to,
|
||||
deletion from, or modification of the contents of Covered
|
||||
Software; or
|
||||
|
||||
(b) any new file in Source Code Form that contains any Covered
|
||||
Software.
|
||||
|
||||
1.11. "Patent Claims" of a Contributor
|
||||
means any patent claim(s), including without limitation, method,
|
||||
process, and apparatus claims, in any patent Licensable by such
|
||||
Contributor that would be infringed, but for the grant of the
|
||||
License, by the making, using, selling, offering for sale, having
|
||||
made, import, or transfer of either its Contributions or its
|
||||
Contributor Version.
|
||||
|
||||
1.12. "Secondary License"
|
||||
means either the GNU General Public License, Version 2.0, the GNU
|
||||
Lesser General Public License, Version 2.1, the GNU Affero General
|
||||
Public License, Version 3.0, or any later versions of those
|
||||
licenses.
|
||||
|
||||
1.13. "Source Code Form"
|
||||
means the form of the work preferred for making modifications.
|
||||
|
||||
1.14. "You" (or "Your")
|
||||
means an individual or a legal entity exercising rights under this
|
||||
License. For legal entities, "You" includes any entity that
|
||||
controls, is controlled by, or is under common control with You. For
|
||||
purposes of this definition, "control" means (a) the power, direct
|
||||
or indirect, to cause the direction or management of such entity,
|
||||
whether by contract or otherwise, or (b) ownership of more than
|
||||
fifty percent (50%) of the outstanding shares or beneficial
|
||||
ownership of such entity.
|
||||
|
||||
2. License Grants and Conditions
|
||||
--------------------------------
|
||||
|
||||
2.1. Grants
|
||||
|
||||
Each Contributor hereby grants You a world-wide, royalty-free,
|
||||
non-exclusive license:
|
||||
|
||||
(a) under intellectual property rights (other than patent or trademark)
|
||||
Licensable by such Contributor to use, reproduce, make available,
|
||||
modify, display, perform, distribute, and otherwise exploit its
|
||||
Contributions, either on an unmodified basis, with Modifications, or
|
||||
as part of a Larger Work; and
|
||||
|
||||
(b) under Patent Claims of such Contributor to make, use, sell, offer
|
||||
for sale, have made, import, and otherwise transfer either its
|
||||
Contributions or its Contributor Version.
|
||||
|
||||
2.2. Effective Date
|
||||
|
||||
The licenses granted in Section 2.1 with respect to any Contribution
|
||||
become effective for each Contribution on the date the Contributor first
|
||||
distributes such Contribution.
|
||||
|
||||
2.3. Limitations on Grant Scope
|
||||
|
||||
The licenses granted in this Section 2 are the only rights granted under
|
||||
this License. No additional rights or licenses will be implied from the
|
||||
distribution or licensing of Covered Software under this License.
|
||||
Notwithstanding Section 2.1(b) above, no patent license is granted by a
|
||||
Contributor:
|
||||
|
||||
(a) for any code that a Contributor has removed from Covered Software;
|
||||
or
|
||||
|
||||
(b) for infringements caused by: (i) Your and any other third party's
|
||||
modifications of Covered Software, or (ii) the combination of its
|
||||
Contributions with other software (except as part of its Contributor
|
||||
Version); or
|
||||
|
||||
(c) under Patent Claims infringed by Covered Software in the absence of
|
||||
its Contributions.
|
||||
|
||||
This License does not grant any rights in the trademarks, service marks,
|
||||
or logos of any Contributor (except as may be necessary to comply with
|
||||
the notice requirements in Section 3.4).
|
||||
|
||||
2.4. Subsequent Licenses
|
||||
|
||||
No Contributor makes additional grants as a result of Your choice to
|
||||
distribute the Covered Software under a subsequent version of this
|
||||
License (see Section 10.2) or under the terms of a Secondary License (if
|
||||
permitted under the terms of Section 3.3).
|
||||
|
||||
2.5. Representation
|
||||
|
||||
Each Contributor represents that the Contributor believes its
|
||||
Contributions are its original creation(s) or it has sufficient rights
|
||||
to grant the rights to its Contributions conveyed by this License.
|
||||
|
||||
2.6. Fair Use
|
||||
|
||||
This License is not intended to limit any rights You have under
|
||||
applicable copyright doctrines of fair use, fair dealing, or other
|
||||
equivalents.
|
||||
|
||||
2.7. Conditions
|
||||
|
||||
Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted
|
||||
in Section 2.1.
|
||||
|
||||
3. Responsibilities
|
||||
-------------------
|
||||
|
||||
3.1. Distribution of Source Form
|
||||
|
||||
All distribution of Covered Software in Source Code Form, including any
|
||||
Modifications that You create or to which You contribute, must be under
|
||||
the terms of this License. You must inform recipients that the Source
|
||||
Code Form of the Covered Software is governed by the terms of this
|
||||
License, and how they can obtain a copy of this License. You may not
|
||||
attempt to alter or restrict the recipients' rights in the Source Code
|
||||
Form.
|
||||
|
||||
3.2. Distribution of Executable Form
|
||||
|
||||
If You distribute Covered Software in Executable Form then:
|
||||
|
||||
(a) such Covered Software must also be made available in Source Code
|
||||
Form, as described in Section 3.1, and You must inform recipients of
|
||||
the Executable Form how they can obtain a copy of such Source Code
|
||||
Form by reasonable means in a timely manner, at a charge no more
|
||||
than the cost of distribution to the recipient; and
|
||||
|
||||
(b) You may distribute such Executable Form under the terms of this
|
||||
License, or sublicense it under different terms, provided that the
|
||||
license for the Executable Form does not attempt to limit or alter
|
||||
the recipients' rights in the Source Code Form under this License.
|
||||
|
||||
3.3. Distribution of a Larger Work
|
||||
|
||||
You may create and distribute a Larger Work under terms of Your choice,
|
||||
provided that You also comply with the requirements of this License for
|
||||
the Covered Software. If the Larger Work is a combination of Covered
|
||||
Software with a work governed by one or more Secondary Licenses, and the
|
||||
Covered Software is not Incompatible With Secondary Licenses, this
|
||||
License permits You to additionally distribute such Covered Software
|
||||
under the terms of such Secondary License(s), so that the recipient of
|
||||
the Larger Work may, at their option, further distribute the Covered
|
||||
Software under the terms of either this License or such Secondary
|
||||
License(s).
|
||||
|
||||
3.4. Notices
|
||||
|
||||
You may not remove or alter the substance of any license notices
|
||||
(including copyright notices, patent notices, disclaimers of warranty,
|
||||
or limitations of liability) contained within the Source Code Form of
|
||||
the Covered Software, except that You may alter any license notices to
|
||||
the extent required to remedy known factual inaccuracies.
|
||||
|
||||
3.5. Application of Additional Terms
|
||||
|
||||
You may choose to offer, and to charge a fee for, warranty, support,
|
||||
indemnity or liability obligations to one or more recipients of Covered
|
||||
Software. However, You may do so only on Your own behalf, and not on
|
||||
behalf of any Contributor. You must make it absolutely clear that any
|
||||
such warranty, support, indemnity, or liability obligation is offered by
|
||||
You alone, and You hereby agree to indemnify every Contributor for any
|
||||
liability incurred by such Contributor as a result of warranty, support,
|
||||
indemnity or liability terms You offer. You may include additional
|
||||
disclaimers of warranty and limitations of liability specific to any
|
||||
jurisdiction.
|
||||
|
||||
4. Inability to Comply Due to Statute or Regulation
|
||||
---------------------------------------------------
|
||||
|
||||
If it is impossible for You to comply with any of the terms of this
|
||||
License with respect to some or all of the Covered Software due to
|
||||
statute, judicial order, or regulation then You must: (a) comply with
|
||||
the terms of this License to the maximum extent possible; and (b)
|
||||
describe the limitations and the code they affect. Such description must
|
||||
be placed in a text file included with all distributions of the Covered
|
||||
Software under this License. Except to the extent prohibited by statute
|
||||
or regulation, such description must be sufficiently detailed for a
|
||||
recipient of ordinary skill to be able to understand it.
|
||||
|
||||
5. Termination
|
||||
--------------
|
||||
|
||||
5.1. The rights granted under this License will terminate automatically
|
||||
if You fail to comply with any of its terms. However, if You become
|
||||
compliant, then the rights granted under this License from a particular
|
||||
Contributor are reinstated (a) provisionally, unless and until such
|
||||
Contributor explicitly and finally terminates Your grants, and (b) on an
|
||||
ongoing basis, if such Contributor fails to notify You of the
|
||||
non-compliance by some reasonable means prior to 60 days after You have
|
||||
come back into compliance. Moreover, Your grants from a particular
|
||||
Contributor are reinstated on an ongoing basis if such Contributor
|
||||
notifies You of the non-compliance by some reasonable means, this is the
|
||||
first time You have received notice of non-compliance with this License
|
||||
from such Contributor, and You become compliant prior to 30 days after
|
||||
Your receipt of the notice.
|
||||
|
||||
5.2. If You initiate litigation against any entity by asserting a patent
|
||||
infringement claim (excluding declaratory judgment actions,
|
||||
counter-claims, and cross-claims) alleging that a Contributor Version
|
||||
directly or indirectly infringes any patent, then the rights granted to
|
||||
You by any and all Contributors for the Covered Software under Section
|
||||
2.1 of this License shall terminate.
|
||||
|
||||
5.3. In the event of termination under Sections 5.1 or 5.2 above, all
|
||||
end user license agreements (excluding distributors and resellers) which
|
||||
have been validly granted by You or Your distributors under this License
|
||||
prior to termination shall survive termination.
|
||||
|
||||
************************************************************************
|
||||
* *
|
||||
* 6. Disclaimer of Warranty *
|
||||
* ------------------------- *
|
||||
* *
|
||||
* Covered Software is provided under this License on an "as is" *
|
||||
* basis, without warranty of any kind, either expressed, implied, or *
|
||||
* statutory, including, without limitation, warranties that the *
|
||||
* Covered Software is free of defects, merchantable, fit for a *
|
||||
* particular purpose or non-infringing. The entire risk as to the *
|
||||
* quality and performance of the Covered Software is with You. *
|
||||
* Should any Covered Software prove defective in any respect, You *
|
||||
* (not any Contributor) assume the cost of any necessary servicing, *
|
||||
* repair, or correction. This disclaimer of warranty constitutes an *
|
||||
* essential part of this License. No use of any Covered Software is *
|
||||
* authorized under this License except under this disclaimer. *
|
||||
* *
|
||||
************************************************************************
|
||||
|
||||
************************************************************************
|
||||
* *
|
||||
* 7. Limitation of Liability *
|
||||
* -------------------------- *
|
||||
* *
|
||||
* Under no circumstances and under no legal theory, whether tort *
|
||||
* (including negligence), contract, or otherwise, shall any *
|
||||
* Contributor, or anyone who distributes Covered Software as *
|
||||
* permitted above, be liable to You for any direct, indirect, *
|
||||
* special, incidental, or consequential damages of any character *
|
||||
* including, without limitation, damages for lost profits, loss of *
|
||||
* goodwill, work stoppage, computer failure or malfunction, or any *
|
||||
* and all other commercial damages or losses, even if such party *
|
||||
* shall have been informed of the possibility of such damages. This *
|
||||
* limitation of liability shall not apply to liability for death or *
|
||||
* personal injury resulting from such party's negligence to the *
|
||||
* extent applicable law prohibits such limitation. Some *
|
||||
* jurisdictions do not allow the exclusion or limitation of *
|
||||
* incidental or consequential damages, so this exclusion and *
|
||||
* limitation may not apply to You. *
|
||||
* *
|
||||
************************************************************************
|
||||
|
||||
8. Litigation
|
||||
-------------
|
||||
|
||||
Any litigation relating to this License may be brought only in the
|
||||
courts of a jurisdiction where the defendant maintains its principal
|
||||
place of business and such litigation shall be governed by laws of that
|
||||
jurisdiction, without reference to its conflict-of-law provisions.
|
||||
Nothing in this Section shall prevent a party's ability to bring
|
||||
cross-claims or counter-claims.
|
||||
|
||||
9. Miscellaneous
|
||||
----------------
|
||||
|
||||
This License represents the complete agreement concerning the subject
|
||||
matter hereof. If any provision of this License is held to be
|
||||
unenforceable, such provision shall be reformed only to the extent
|
||||
necessary to make it enforceable. Any law or regulation which provides
|
||||
that the language of a contract shall be construed against the drafter
|
||||
shall not be used to construe this License against a Contributor.
|
||||
|
||||
10. Versions of the License
|
||||
---------------------------
|
||||
|
||||
10.1. New Versions
|
||||
|
||||
Mozilla Foundation is the license steward. Except as provided in Section
|
||||
10.3, no one other than the license steward has the right to modify or
|
||||
publish new versions of this License. Each version will be given a
|
||||
distinguishing version number.
|
||||
|
||||
10.2. Effect of New Versions
|
||||
|
||||
You may distribute the Covered Software under the terms of the version
|
||||
of the License under which You originally received the Covered Software,
|
||||
or under the terms of any subsequent version published by the license
|
||||
steward.
|
||||
|
||||
10.3. Modified Versions
|
||||
|
||||
If you create software not governed by this License, and you want to
|
||||
create a new license for such software, you may create and use a
|
||||
modified version of this License if you rename the license and remove
|
||||
any references to the name of the license steward (except to note that
|
||||
such modified license differs from this License).
|
||||
|
||||
10.4. Distributing Source Code Form that is Incompatible With Secondary
|
||||
Licenses
|
||||
|
||||
If You choose to distribute Source Code Form that is Incompatible With
|
||||
Secondary Licenses under the terms of this version of the License, the
|
||||
notice described in Exhibit B of this License must be attached.
|
||||
|
||||
Exhibit A - Source Code Form License Notice
|
||||
-------------------------------------------
|
||||
|
||||
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/.
|
||||
|
||||
If it is not possible or desirable to put the notice in a particular
|
||||
file, then You may include the notice in a location (such as a LICENSE
|
||||
file in a relevant directory) where a recipient would be likely to look
|
||||
for such a notice.
|
||||
|
||||
You may add additional accurate notices of copyright ownership.
|
||||
|
||||
Exhibit B - "Incompatible With Secondary Licenses" Notice
|
||||
---------------------------------------------------------
|
||||
|
||||
This Source Code Form is "Incompatible With Secondary Licenses", as
|
||||
defined by the Mozilla Public License, v. 2.0.
|
||||
234
OpenSim/Addons/os-webrtc-janus/README.md
Normal file
234
OpenSim/Addons/os-webrtc-janus/README.md
Normal file
@@ -0,0 +1,234 @@
|
||||
# os-webrtc-janus
|
||||
|
||||
Addon-module for [OpenSimulator] to provide webrtc voice support
|
||||
using Janus-gateway.
|
||||
|
||||
For an explanation of background and architecture,
|
||||
this project was presented at the
|
||||
[OpenSimulator Community Conference] 2024
|
||||
in the presentation
|
||||
[WebRTC Voice for OpenSimulator](https://www.youtube.com/watch?v=nL78fieIFYg).
|
||||
|
||||
This addon works by taking viewer requests for voice service and
|
||||
using a separate, external [Janus-Gateway WebRTC server].
|
||||
This can be configured to allow local region spatial voice
|
||||
and grid-wide group and spatial voice. See the sections below.
|
||||
|
||||
For running that separate Janus server, check out
|
||||
[os-webrtc-janus-docker] which has instructions for running
|
||||
Janus-Gateway on Linux and Windows WSL using Docker.
|
||||
|
||||
Instructions for:
|
||||
|
||||
- [Building into OpenSimulator](#Building): Build OpenSimulator with WebRTC voice service
|
||||
- [Configuring Simulator for Voice Services](#Configure_Simulator)
|
||||
- [Configuring Robust Grid Service](#Configure_Robust)
|
||||
- [Configure Standalone Region](#Configure_Standalone)
|
||||
- [Managing Voice Service](#Managing_Voice) (console commands, etc)
|
||||
|
||||
**Note**: as of January 2024, this solution does not provide true spatial
|
||||
voice service using Janus. There are people working on additions to Janus
|
||||
to provide this but the existing solution provides only non-spatial
|
||||
voice services using the `AudioBridge` Janus plugin. Additionally,
|
||||
features like muting and individual avatar volume are not yet implemented.
|
||||
|
||||
<a id="Known_Issues"></a>
|
||||
## Known Issues
|
||||
|
||||
- No spatial audio
|
||||
- One can see your own "white dot" but you don't see other avatar's white dots
|
||||
- No muting
|
||||
- No individual volume control
|
||||
|
||||
And probably more found at [os-webrtc-janus issues](https://github.com/Misterblue/os-webrtc-janus/issues).
|
||||
|
||||
<a id="Building"></a>
|
||||
## Building Plugin into OpenSimulator
|
||||
|
||||
`os-webrtc-janus` is integrated as a source build into [OpenSimulator].
|
||||
It uses the [OpenSimulator] addon-module feature which makes the
|
||||
build as easy as cloning the `os-webrtc-janus` sources into the
|
||||
[OpenSimulator] source tree, running the build configuration script,
|
||||
and then building OpenSimulator.
|
||||
|
||||
The steps are:
|
||||
|
||||
```
|
||||
# Get the OpenSimulator sources
|
||||
git clone git://opensimulator.org/git/opensim
|
||||
cd opensim # cd into the top level OpenSim directory
|
||||
|
||||
# Fetch the WebRtc addon
|
||||
cd addon-modules
|
||||
git clone https://github.com/Misterblue/os-webrtc-janus.git
|
||||
cd ..
|
||||
|
||||
# Build the project files
|
||||
./runprebuild.sh
|
||||
|
||||
# Compile OpenSimulator with the webrtc addon
|
||||
./compile.sh
|
||||
|
||||
# Copy the INI file for webrtc into a config dir that is read at boot
|
||||
mkdir bin/config
|
||||
cp addon-modules/os-webrtc-janus/os-webrtc-janus.ini bin/config
|
||||
```
|
||||
|
||||
These building steps create several `.dll` files for `os-webrtc-janus`
|
||||
in `bin/WebRtc*.dll`. Some adventurous people have found that, rather
|
||||
than building the [OpenSimulator] sources, you can just copy the `.dll`s
|
||||
into an existing `/bin` directory. Just make sure the `WebRtc*.dll` files
|
||||
were built on the same version of [OpenSimulator] you are running.
|
||||
|
||||
<a id="Configure_Simulator"></a>
|
||||
## Configure a Region for Voice
|
||||
|
||||
The last step in [Building](#Building) copied `os-webrtc-janus.ini` into
|
||||
the `bin/config` directory. [OpenSimulator] reads all the `.ini` files
|
||||
in that directory so this copy operation adds the configuration for `os-webrtc-janus`
|
||||
and this is what needs to be configured for the simulator and region.
|
||||
|
||||
The sample `.ini` file has two sections: `[WebRtcVoice]` and `[JanusWebRtcVoice]`.
|
||||
The `WebRtcVoice` section configures the what services the simulator uses
|
||||
for WebRtc voice. The `[JanusWebRtcVoice]` section configures any connection
|
||||
the simulator makes to the Janus server. The latter section is only updated
|
||||
if this simulator is using a local Janus server for spatial voice.
|
||||
|
||||
The values for `SpatialVoiceService` and `NonSpatialVoiceService` point
|
||||
either directly to a Janus service or to a Robust grid server that is providing
|
||||
the grid voice service. Both these options are in the sample `os-webrtc-janus.ini`
|
||||
file and the proper one should be uncommented.
|
||||
|
||||
The viewer makes requests for either spatial voice (used in the region and parcels)
|
||||
or non-spatial voice (used for group chats or person-to-person voice conversations).
|
||||
`os-webrtc-janus` allows these two types of voice connections to be handled by
|
||||
different voice services. Thus there are two different configurations:
|
||||
|
||||
- all voice service is provided by the grid (both spatial and non-spatial point to a robust service), and
|
||||
- the region simulator provides a local Janus server for region spatial voice while the grid service is used for group chats
|
||||
|
||||
#### Grid Only Voice Services
|
||||
|
||||
The most common configuration will be for a simulator that uses the grid supplied
|
||||
voice services. For this configuration, `os-webrtc-janus.ini` would look like:
|
||||
|
||||
```
|
||||
[WebRtcVoice]
|
||||
Enabled = true
|
||||
SpatialVoiceService = WebRtcVoice.dll:WebRtcVoiceServiceConnector
|
||||
NonSpatialVoiceService = WebRtcVoice.dll:WebRtcVoiceServiceConnector
|
||||
WebRtcVoiceServerURI = ${Const|PrivURL}:${Const|PrivatePort}
|
||||
```
|
||||
|
||||
This directs both spatial and non-spatial voice to the grid service connector
|
||||
and `WebRtcVoiceServerURI` points to the configured Robust grid service.
|
||||
|
||||
There is no need for a `[JanusWebRtcVoice]` section because all that is handled by the grid services.
|
||||
|
||||
#### Local Simulator Janus Service
|
||||
|
||||
In a grid setup, there might be a need for a single simulator/region to use its own
|
||||
Janus server for either privacy or to off-load the grid voice service.
|
||||
In this configuration, spatial voice is directed to the local Janus service
|
||||
while the non-spatial voice goes to the grid services to allow grid wide group
|
||||
chat and region independent person-to-person chat.
|
||||
|
||||
This is done with a `os-webrtc-janus.ini` that looks like:
|
||||
```
|
||||
[WebRtcVoice]
|
||||
Enabled = true
|
||||
SpatialVoiceService = WebRtcJanusService.dll:WebRtcJanusService
|
||||
NonSpatialVoiceService = WebRtcVoice.dll:WebRtcVoiceServiceConnector
|
||||
WebRtcVoiceServerURI = ${Const|PrivURL}:${Const|PrivatePort}
|
||||
[JanusWebRtcVoice]
|
||||
JanusGatewayURI = http://janus.example.org:14223/voice
|
||||
APIToken = APITokenToNeverCheckIn
|
||||
JanusGatewayAdminURI = http://janus.example.org/admin
|
||||
AdminAPIToken = AdminAPITokenToNeverCheckIn
|
||||
```
|
||||
|
||||
Notice that, since the simulator has its own Janus service, it must configure the
|
||||
connection parameters to access that Janus service. The details of running and
|
||||
configuring a Janus service is provided at [os-webrtc-janus-docker] but, the configuration
|
||||
here needs to specify the URI to address the Janus server and the API keys
|
||||
to allow this simulator access to its interfaces. The example above
|
||||
contains just sample entries.
|
||||
|
||||
<a id="Configure_Robust"></a>
|
||||
## Configure Robust Server for WebRTC Voice
|
||||
|
||||
For the grid services side, `os-webrtc-janus` is configured as an additional service
|
||||
in the Robust OpenSimulator server. The additions to `Robust.ini` are:
|
||||
|
||||
```
|
||||
...
|
||||
[ServiceList]
|
||||
...
|
||||
VoiceServiceConnector = "${Const|PrivatePort}/WebRtcVoice.dll:WebRtcVoiceServerConnector"
|
||||
...
|
||||
|
||||
[WebRtcVoice]
|
||||
Enabled = true
|
||||
SpatialVoiceService = WebRtcJanusService.dll:WebRtcJanusService
|
||||
NonSpatialVoiceService = WebRtcJanusService.dll:WebRtcJanusService
|
||||
[JanusWebRtcVoice]
|
||||
JanusGatewayURI = http://janus.example.org:14223/voice
|
||||
APIToken = APITokenToNeverCheckIn
|
||||
JanusGatewayAdminURI = http://janus.example.org/admin
|
||||
AdminAPIToken = AdminAPITokenToNeverCheckIn
|
||||
...
|
||||
```
|
||||
|
||||
This adds `VoiceServiceConnector` to the list of services presented by this Robust server
|
||||
and adds the WebRtcVoice configuration that says to do both spatial and non-spatial voice
|
||||
using the Janus server, and the configuration for the Janus server itself.
|
||||
|
||||
One can configure multiple Robust services to distribute the load of services
|
||||
and a Robust server with only `VoiceServiceConnector` in its ServiceList is possible.
|
||||
|
||||
<a id="Configure_Standalone"></a>
|
||||
## Configure Standalone Region
|
||||
|
||||
[OpenSimulator] can be run "standalone" where all the grid services and regions are
|
||||
run in one simulator instance. Adding voice to this configuration is sometimes useful
|
||||
for very private meetings or testing. For this configuration, a Janus server is set up
|
||||
and the standalone simulator is configured to point all voice to that Janus server:
|
||||
|
||||
```
|
||||
[WebRtcVoice]
|
||||
Enabled = true
|
||||
SpatialVoiceService = WebRtcJanusService.dll:WebRtcJanusService
|
||||
NonSpatialVoiceService = WebRtcJanusService.dll:WebRtcJanusService
|
||||
WebRtcVoiceServerURI = ${Const|PrivURL}:${Const|PrivatePort}
|
||||
[JanusWebRtcVoice]
|
||||
JanusGatewayURI = http://janus.example.org:14223/voice
|
||||
APIToken = APITokenToNeverCheckIn
|
||||
JanusGatewayAdminURI = http://janus.example.org/admin
|
||||
AdminAPIToken = AdminAPITokenToNeverCheckIn
|
||||
```
|
||||
|
||||
This directs both spatial and non-spatial voice to the Janus server
|
||||
and configures the URI address of the Janus server and the API access
|
||||
keys for that server.
|
||||
|
||||
<a id="Managing_Voice"></a>
|
||||
## Managing Voice (Console commands)
|
||||
|
||||
There are a few console commands for checking on and controlling the voice system.
|
||||
The current list of commands for the simulator can be listed with the
|
||||
console command `help webrtc`.
|
||||
|
||||
This is a growing section and will be added to over time.
|
||||
|
||||
**webrtc list sessions** -- not implemented
|
||||
|
||||
**janus info** -- list many details of the Janus-Gateway configuration. Very ugly, non-formated JSON.
|
||||
|
||||
**janus list rooms** -- list the rooms that have been allocated in the `AudioBridge` Janus plugin
|
||||
|
||||
[SecondLife WebRTC Voice]: https://wiki.secondlife.com/wiki/WebRTC_Voice
|
||||
[OpenSimulator]: http://opensimulator.org
|
||||
[OpenSimulator Community Conference]: https://conference.opensimulator.org
|
||||
[os-webrtc-janus]: https://github.com/Misterblue/os-webrtc-janus
|
||||
[Janus-Gateway WebRTC server]: https://janus.conf.meetecho.com/
|
||||
[os-webrtc-janus-docker]: https://github.com/Misterblue/os-webrtc-janus-docker
|
||||
@@ -0,0 +1,54 @@
|
||||
/*
|
||||
* 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.Threading.Tasks;
|
||||
|
||||
using OMV = OpenMetaverse;
|
||||
|
||||
namespace osWebRtcVoice
|
||||
{
|
||||
/// <summary>
|
||||
/// This is the interface for the viewer session. It is used to store the
|
||||
/// state of the viewer session and to disconnect the session when needed.
|
||||
/// </summary>
|
||||
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 OMV.UUID RegionId { get; set; }
|
||||
|
||||
// The simulator has a GUID to identify the user
|
||||
public OMV.UUID AgentId { get; set; }
|
||||
|
||||
// Disconnect the connection to the voice service for this session
|
||||
public Task Shutdown();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
/*
|
||||
* 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 OpenSim.Framework;
|
||||
|
||||
using OpenMetaverse;
|
||||
using OpenMetaverse.StructuredData;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace osWebRtcVoice
|
||||
{
|
||||
/// <summary>
|
||||
/// This is the interface for the voice service. It is used to connect
|
||||
/// the user to the voice server and to handle the capability messages
|
||||
/// from the viewer.
|
||||
/// </summary>
|
||||
public interface IWebRtcVoiceService
|
||||
{
|
||||
// The user is requesting a voice connection. The message contains the offer
|
||||
// from the user and we must return the answer.
|
||||
// If there are problems, the returned map will contain an error message.
|
||||
|
||||
// Initial calls to the voice server to get the user connected
|
||||
public Task<OSDMap> ProvisionVoiceAccountRequest(OSDMap pRequest, UUID pUserID, UUID pScene);
|
||||
public Task<OSDMap> VoiceSignalingRequest(OSDMap pRequest, UUID pUserID, UUID pScene);
|
||||
|
||||
// Once connection state is looked up, the viewer session is passed in
|
||||
public Task<OSDMap> ProvisionVoiceAccountRequest(IVoiceViewerSession pVSession, OSDMap pRequest, UUID pUserID, UUID pScene);
|
||||
public Task<OSDMap> VoiceSignalingRequest(IVoiceViewerSession pVSession, OSDMap pRequest, UUID pUserID, UUID pScene);
|
||||
|
||||
// Create a viewer session with all the variables needed for the underlying implementation
|
||||
public IVoiceViewerSession CreateViewerSession(OSDMap pRequest, UUID pUserID, UUID pScene);
|
||||
}
|
||||
}
|
||||
132
OpenSim/Addons/os-webrtc-janus/WebRtcVoice/VoiceViewerSession.cs
Normal file
132
OpenSim/Addons/os-webrtc-janus/WebRtcVoice/VoiceViewerSession.cs
Normal file
@@ -0,0 +1,132 @@
|
||||
/*
|
||||
* 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.Linq;
|
||||
using System.Collections.Generic;
|
||||
|
||||
using OpenMetaverse;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace osWebRtcVoice
|
||||
{
|
||||
public class VoiceViewerSession : IVoiceViewerSession
|
||||
{
|
||||
|
||||
// A simple session structure that is used when the connection is actually in the
|
||||
// remote service.
|
||||
public VoiceViewerSession(IWebRtcVoiceService pVoiceService, UUID pRegionId, UUID pAgentId)
|
||||
{
|
||||
RegionId = pRegionId;
|
||||
AgentId = pAgentId;
|
||||
ViewerSessionID = UUID.Random().ToString();
|
||||
VoiceService = pVoiceService;
|
||||
|
||||
}
|
||||
public string ViewerSessionID { get; set; }
|
||||
public IWebRtcVoiceService VoiceService { get; set; }
|
||||
public string VoiceServiceSessionId
|
||||
{
|
||||
get => throw new System.NotImplementedException();
|
||||
set => throw new System.NotImplementedException();
|
||||
}
|
||||
public UUID RegionId { get; set; }
|
||||
public UUID AgentId { get; set; }
|
||||
|
||||
// =====================================================================
|
||||
// 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<string, IVoiceViewerSession> ViewerSessions = new Dictionary<string, IVoiceViewerSession>();
|
||||
// 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<KeyValuePair<string, IVoiceViewerSession>> 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);
|
||||
}
|
||||
}
|
||||
|
||||
// Update a ViewSession from one ID to another.
|
||||
// Remove the old session ID from the ViewerSessions collection, update the
|
||||
// sessionID value in the IVoiceViewerSession, and add the session back to the
|
||||
// collection.
|
||||
// This is used in the kludge to synchronize a region's ViewerSessionID with the
|
||||
// remote VoiceService's session ID.
|
||||
public static void UpdateViewerSessionId(IVoiceViewerSession pSession, string pNewSessionId)
|
||||
{
|
||||
lock (ViewerSessions)
|
||||
{
|
||||
ViewerSessions.Remove(pSession.ViewerSessionID);
|
||||
pSession.ViewerSessionID = pNewSessionId;
|
||||
ViewerSessions[pSession.ViewerSessionID] = pSession;
|
||||
}
|
||||
}
|
||||
|
||||
public Task Shutdown()
|
||||
{
|
||||
throw new System.NotImplementedException();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,171 @@
|
||||
/*
|
||||
* 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 OpenSim.Framework;
|
||||
using OpenSim.Framework.Servers.HttpServer;
|
||||
using OpenSim.Region.Framework.Interfaces;
|
||||
using OpenSim.Services.Interfaces;
|
||||
using OpenSim.Server.Base;
|
||||
using OpenSim.Server.Handlers.Base;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
using OpenMetaverse;
|
||||
using OpenMetaverse.StructuredData;
|
||||
|
||||
using log4net;
|
||||
using Nini.Config;
|
||||
|
||||
namespace osWebRtcVoice
|
||||
{
|
||||
// Class that provides the network interface to the WebRTC voice server.
|
||||
// This is used by the Robust server to receive requests from the region servers
|
||||
// and do the voice stuff on the WebRTC service (see WebRtcVoiceServiceConnector).
|
||||
public class WebRtcVoiceServerConnector : IServiceConnector
|
||||
{
|
||||
private static readonly ILog m_log = LogManager.GetLogger(MethodBase.GetCurrentMethod().DeclaringType);
|
||||
private static readonly string LogHeader = "[WEBRTC VOICE SERVER CONNECTOR]";
|
||||
|
||||
private bool m_Enabled = false;
|
||||
private bool m_MessageDetails = false;
|
||||
private IWebRtcVoiceService m_WebRtcVoiceService;
|
||||
|
||||
public WebRtcVoiceServerConnector(IConfigSource pConfig, IHttpServer pServer, string pConfigName)
|
||||
{
|
||||
IConfig moduleConfig = pConfig.Configs["WebRtcVoice"];
|
||||
|
||||
if (moduleConfig is not null)
|
||||
{
|
||||
m_Enabled = moduleConfig.GetBoolean("Enabled", false);
|
||||
if (m_Enabled)
|
||||
{
|
||||
m_log.InfoFormat("{0} WebRtcVoiceServerConnector enabled", LogHeader);
|
||||
m_MessageDetails = moduleConfig.GetBoolean("MessageDetails", false);
|
||||
|
||||
// This creates the local service that handles the requests.
|
||||
// The local service provides the IWebRtcVoiceService interface and directs the requests
|
||||
// to the WebRTC service.
|
||||
string localServiceModule = moduleConfig.GetString("LocalServiceModule", "WebRtcVoiceServiceModule.dll:WebRtcVoiceServiceModule");
|
||||
m_log.DebugFormat("{0} loading {1}", LogHeader, localServiceModule);
|
||||
|
||||
object[] args = new object[0];
|
||||
m_WebRtcVoiceService = ServerUtils.LoadPlugin<IWebRtcVoiceService>(localServiceModule, args);
|
||||
|
||||
// The WebRtcVoiceServiceModule is both an IWebRtcVoiceService and a ISharedRegionModule
|
||||
// so we can initialize it as if it was the region module.
|
||||
ISharedRegionModule sharedModule = m_WebRtcVoiceService as ISharedRegionModule;
|
||||
if (sharedModule is null)
|
||||
{
|
||||
m_log.ErrorFormat("{0} local service module does not implement ISharedRegionModule", LogHeader);
|
||||
m_Enabled = false;
|
||||
return;
|
||||
}
|
||||
sharedModule.Initialise(pConfig);
|
||||
|
||||
// Now that we have someone to handle the requests, we can set up the handlers
|
||||
pServer.AddJsonRPCHandler("provision_voice_account_request", Handle_ProvisionVoiceAccountRequest);
|
||||
pServer.AddJsonRPCHandler("voice_signaling_request", Handle_VoiceSignalingRequest);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private bool Handle_ProvisionVoiceAccountRequest(OSDMap pJson, ref JsonRpcResponse pResponse)
|
||||
{
|
||||
bool ret = false;
|
||||
m_log.DebugFormat("{0} Handle_ProvisionVoiceAccountRequest", LogHeader);
|
||||
if (m_MessageDetails) m_log.DebugFormat("{0} PVAR: req={1}", LogHeader, pJson.ToString());
|
||||
|
||||
if (pJson.ContainsKey("params") && pJson["params"] is OSDMap paramsMap)
|
||||
{
|
||||
OSDMap request = paramsMap.ContainsKey("request") ? paramsMap["request"] as OSDMap : null;
|
||||
UUID userID = paramsMap.ContainsKey("userID") ? paramsMap["userID"].AsUUID() : UUID.Zero;
|
||||
UUID sceneID = paramsMap.ContainsKey("scene") ? paramsMap["scene"].AsUUID() : UUID.Zero;
|
||||
|
||||
try
|
||||
{
|
||||
if (m_WebRtcVoiceService is null)
|
||||
{
|
||||
m_log.ErrorFormat("{0} PVAR: no local service", LogHeader);
|
||||
return false;
|
||||
}
|
||||
OSDMap resp = m_WebRtcVoiceService.ProvisionVoiceAccountRequest(request, userID, sceneID).Result;
|
||||
|
||||
pResponse = new JsonRpcResponse();
|
||||
pResponse.Result = resp;
|
||||
if (m_MessageDetails) m_log.DebugFormat("{0} PVAR: resp={1}", LogHeader, resp.ToString());
|
||||
ret = true;
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
m_log.ErrorFormat("{0} PVAR: exception {1}", LogHeader, e);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
m_log.ErrorFormat("{0} PVAR: missing parameters", LogHeader);
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
private bool Handle_VoiceSignalingRequest(OSDMap pJson, ref JsonRpcResponse pResponse)
|
||||
{
|
||||
bool ret = false;
|
||||
if (pJson.ContainsKey("params") && pJson["params"] is OSDMap paramsMap)
|
||||
{
|
||||
m_log.DebugFormat("{0} Handle_VoiceSignalingRequest", LogHeader);
|
||||
if (m_MessageDetails) m_log.DebugFormat("{0} VSR: req={1}", LogHeader, paramsMap.ToString());
|
||||
|
||||
OSDMap request = paramsMap.ContainsKey("request") ? paramsMap["request"] as OSDMap : null;
|
||||
UUID userID = paramsMap.ContainsKey("userID") ? paramsMap["userID"].AsUUID() : UUID.Zero;
|
||||
UUID sceneID = paramsMap.ContainsKey("scene") ? paramsMap["scene"].AsUUID() : UUID.Zero;
|
||||
|
||||
try
|
||||
{
|
||||
OSDMap resp = m_WebRtcVoiceService.VoiceSignalingRequest(request, userID, sceneID).Result;
|
||||
|
||||
pResponse = new JsonRpcResponse();
|
||||
pResponse.Result = resp;
|
||||
if (m_MessageDetails) m_log.DebugFormat("{0} VSR: resp={1}", LogHeader, resp.ToString());
|
||||
|
||||
ret = true;
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
m_log.ErrorFormat("{0} VSR: exception {1}", LogHeader, e);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
m_log.ErrorFormat("{0} VSR: missing parameters", LogHeader);
|
||||
}
|
||||
|
||||
return ret;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,217 @@
|
||||
/*
|
||||
* 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 System.Threading.Tasks;
|
||||
|
||||
using OpenSim.Framework;
|
||||
|
||||
using OpenMetaverse;
|
||||
using OpenMetaverse.StructuredData;
|
||||
|
||||
using log4net;
|
||||
using Nini.Config;
|
||||
using OSHttpServer;
|
||||
|
||||
namespace osWebRtcVoice
|
||||
{
|
||||
// Class that provides the local IWebRtcVoiceService interface to the JsonRPC Robust
|
||||
// server. This is used by the region servers to talk to the Robust server.
|
||||
public class WebRtcVoiceServiceConnector : IWebRtcVoiceService
|
||||
{
|
||||
private static readonly ILog m_log = LogManager.GetLogger(MethodBase.GetCurrentMethod().DeclaringType);
|
||||
private static readonly string LogHeader = "[WEBRTC VOICE SERVICE CONNECTOR]";
|
||||
private bool m_Enabled = false;
|
||||
private bool m_MessageDetails = false;
|
||||
private IConfigSource m_Config;
|
||||
|
||||
string m_serverURI = "http://localhost:8080";
|
||||
|
||||
public WebRtcVoiceServiceConnector(IConfigSource pConfig)
|
||||
{
|
||||
m_Config = pConfig;
|
||||
IConfig moduleConfig = m_Config.Configs["WebRtcVoice"];
|
||||
|
||||
if (moduleConfig is not null)
|
||||
{
|
||||
m_Enabled = moduleConfig.GetBoolean("Enabled", false);
|
||||
if (m_Enabled)
|
||||
{
|
||||
m_serverURI = moduleConfig.GetString("WebRtcVoiceServerURI", string.Empty);
|
||||
if (string.IsNullOrWhiteSpace(m_serverURI))
|
||||
{
|
||||
m_log.ErrorFormat("{0} WebRtcVoiceServiceConnector enabled but no WebRtcVoiceServerURI specified", LogHeader);
|
||||
m_Enabled = false;
|
||||
}
|
||||
else
|
||||
{
|
||||
m_log.InfoFormat("{0} WebRtcVoiceServiceConnector enabled", LogHeader);
|
||||
}
|
||||
|
||||
m_MessageDetails = moduleConfig.GetBoolean("MessageDetails", false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Create a local viewer session. This gets a local viewer session ID that is
|
||||
// later changed when the ProvisionVoiceAccountRequest response is returned
|
||||
// so that the viewer session ID is the same here as from the WebRTC service.
|
||||
public IVoiceViewerSession CreateViewerSession(OSDMap pRequest, UUID pUserID, UUID pSceneID)
|
||||
{
|
||||
m_log.DebugFormat("{0} CreateViewerSession", LogHeader);
|
||||
return new VoiceViewerSession(this, pUserID, pSceneID);
|
||||
}
|
||||
|
||||
public Task<OSDMap> ProvisionVoiceAccountRequest(OSDMap pRequest, UUID pUserID, UUID pSceneID)
|
||||
{
|
||||
m_log.DebugFormat("{0} ProvisionVoiceAccountRequest without ViewerSession. uID={1}, sID={2}", LogHeader, pUserID, pSceneID);
|
||||
return null;
|
||||
}
|
||||
|
||||
// Received a ProvisionVoiceAccountRequest from a viewer. Forward it to the WebRTC service.
|
||||
public async Task<OSDMap> ProvisionVoiceAccountRequest(IVoiceViewerSession pVSession, OSDMap pRequest, UUID pUserID, UUID pSceneID)
|
||||
{
|
||||
m_log.DebugFormat("{0} VoiceSignalingRequest. uID={1}, sID={2}", LogHeader, pUserID, pSceneID);
|
||||
OSDMap req = new OSDMap()
|
||||
{
|
||||
{ "request", pRequest },
|
||||
{ "userID", pUserID.ToString() },
|
||||
{ "scene", pSceneID.ToString() }
|
||||
};
|
||||
var resp = await JsonRpcRequest("provision_voice_account_request", m_serverURI, req);
|
||||
|
||||
// Kludge to sync the viewer session number in our IVoiceViewerSession with the one from the WebRTC service.
|
||||
if (resp.ContainsKey("viewer_session"))
|
||||
{
|
||||
string otherViewerSessionId = resp["viewer_session"].AsString();
|
||||
m_log.DebugFormat("{0} ProvisionVoiceAccountRequest: syncing viewSessionID. old={1}, new={2}",
|
||||
LogHeader, pVSession.ViewerSessionID, otherViewerSessionId);
|
||||
VoiceViewerSession.UpdateViewerSessionId(pVSession, otherViewerSessionId);
|
||||
}
|
||||
|
||||
return resp;
|
||||
}
|
||||
|
||||
public Task<OSDMap> VoiceSignalingRequest(OSDMap pRequest, UUID pUserID, UUID pSceneID)
|
||||
{
|
||||
m_log.DebugFormat("{0} VoiceSignalingRequest without ViewerSession. uID={1}, sID={2}", LogHeader, pUserID, pSceneID);
|
||||
return null;
|
||||
}
|
||||
|
||||
public Task<OSDMap> VoiceSignalingRequest(IVoiceViewerSession pVSession, OSDMap pRequest, UUID pUserID, UUID pSceneID)
|
||||
{
|
||||
m_log.DebugFormat("{0} VoiceSignalingRequest. uID={1}, sID={2}", LogHeader, pUserID, pSceneID);
|
||||
OSDMap req = new OSDMap()
|
||||
{
|
||||
{ "request", pRequest },
|
||||
{ "userID", pUserID.ToString() },
|
||||
{ "scene", pSceneID.ToString() }
|
||||
};
|
||||
return JsonRpcRequest("voice_signaling_request", m_serverURI, req);
|
||||
}
|
||||
|
||||
public Task<OSDMap> JsonRpcRequest(string method, string uri, OSDMap pParams)
|
||||
{
|
||||
string jsonId = UUID.Random().ToString();
|
||||
|
||||
if(string.IsNullOrWhiteSpace(uri))
|
||||
return null;
|
||||
|
||||
TaskCompletionSource<OSDMap> tcs = new TaskCompletionSource<OSDMap>();
|
||||
_ = Task.Run(() =>
|
||||
{
|
||||
OSDMap request = new()
|
||||
{
|
||||
{ "jsonrpc", OSD.FromString("2.0") },
|
||||
{ "id", OSD.FromString(jsonId) },
|
||||
{ "method", OSD.FromString(method) },
|
||||
{ "params", pParams }
|
||||
};
|
||||
|
||||
OSDMap outerResponse = null;
|
||||
try
|
||||
{
|
||||
if (m_MessageDetails) m_log.DebugFormat("{0}: request: {1}", LogHeader, request);
|
||||
outerResponse = WebUtil.PostToService(uri, request, 10000, true);
|
||||
if (m_MessageDetails) m_log.DebugFormat("{0}: response: {1}", LogHeader, outerResponse);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
m_log.ErrorFormat("{0}: JsonRpc request '{1}' to {2} failed: {3}", LogHeader, method, uri, e);
|
||||
m_log.DebugFormat("{0}: request: {1}", LogHeader, request);
|
||||
tcs.SetResult(new OSDMap()
|
||||
{
|
||||
{ "error", OSD.FromString(e.Message) }
|
||||
});
|
||||
}
|
||||
|
||||
OSD osdtmp;
|
||||
if (!outerResponse.TryGetValue("_Result", out osdtmp) || (osdtmp is not OSDMap))
|
||||
{
|
||||
string errm = String.Format("JsonRpc request '{0}' to {1} returned an invalid response: {2}",
|
||||
method, uri, OSDParser.SerializeJsonString(outerResponse));
|
||||
m_log.ErrorFormat(errm);
|
||||
tcs.SetResult(new OSDMap()
|
||||
{
|
||||
{ "error", errm }
|
||||
});
|
||||
}
|
||||
|
||||
OSDMap response = osdtmp as OSDMap;
|
||||
if (response.TryGetValue("error", out osdtmp))
|
||||
{
|
||||
string errm = String.Format("JsonRpc request '{0}' to {1} returned an error: {2}",
|
||||
method, uri, OSDParser.SerializeJsonString(osdtmp));
|
||||
m_log.ErrorFormat(errm);
|
||||
tcs.SetResult(new OSDMap()
|
||||
{
|
||||
{ "error", errm }
|
||||
});
|
||||
}
|
||||
|
||||
OSDMap resultmap = null;
|
||||
if (!response.TryGetValue("result", out osdtmp) || (osdtmp is not OSDMap))
|
||||
{
|
||||
string errm = String.Format("JsonRpc request '{0}' to {1} returned result as non-OSDMap: {2}",
|
||||
method, uri, OSDParser.SerializeJsonString(outerResponse));
|
||||
m_log.ErrorFormat(errm);
|
||||
tcs.SetResult(new OSDMap()
|
||||
{
|
||||
{ "error", errm }
|
||||
});
|
||||
}
|
||||
resultmap = osdtmp as OSDMap;
|
||||
|
||||
tcs.SetResult(resultmap);
|
||||
});
|
||||
|
||||
return tcs.Task;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,502 @@
|
||||
/*
|
||||
* 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.IO;
|
||||
using System.Net;
|
||||
using System.Text;
|
||||
using System.Collections.Generic;
|
||||
using System.Reflection;
|
||||
|
||||
using Mono.Addins;
|
||||
|
||||
using OpenSim.Framework;
|
||||
using OpenSim.Framework.Servers.HttpServer;
|
||||
using OpenSim.Region.Framework.Interfaces;
|
||||
using OpenSim.Region.Framework.Scenes;
|
||||
using Caps = OpenSim.Framework.Capabilities.Caps;
|
||||
|
||||
using OpenMetaverse;
|
||||
using OpenMetaverse.StructuredData;
|
||||
using OSDMap = OpenMetaverse.StructuredData.OSDMap;
|
||||
|
||||
using log4net;
|
||||
using Nini.Config;
|
||||
|
||||
[assembly: Addin("WebRtcVoiceRegionModule", "1.0")]
|
||||
[assembly: AddinDependency("OpenSim.Region.Framework", OpenSim.VersionInfo.VersionNumber)]
|
||||
|
||||
namespace osWebRtcVoice
|
||||
{
|
||||
/// <summary>
|
||||
/// This module provides the WebRTC voice interface for viewer clients..
|
||||
///
|
||||
/// In particular, it provides the following capabilities:
|
||||
/// ProvisionVoiceAccountRequest, VoiceSignalingRequest and limited ChatSessionRequest
|
||||
/// which are the user interface to the voice service.
|
||||
///
|
||||
/// Initially, when the user connects to the region, the region feature "VoiceServiceType" is
|
||||
/// set to "webrtc" and the capabilities that support voice are enabled.
|
||||
/// The capabilities then pass the user request information to the IWebRtcVoiceService interface
|
||||
/// that has been registered for the reqion.
|
||||
/// </summary>
|
||||
[Extension(Path = "/OpenSim/RegionModules", NodeName = "RegionModule", Id = "RegionVoiceModule")]
|
||||
public class WebRtcVoiceRegionModule : ISharedRegionModule
|
||||
{
|
||||
private static readonly ILog m_log = LogManager.GetLogger(MethodBase.GetCurrentMethod().DeclaringType);
|
||||
private static readonly string logHeader = "[REGION WEBRTC VOICE]";
|
||||
|
||||
private static byte[] llsdUndefAnswerBytes = Util.UTF8.GetBytes("<llsd><undef /></llsd>");
|
||||
private bool _MessageDetails = false;
|
||||
|
||||
// Control info
|
||||
private static bool m_Enabled = false;
|
||||
|
||||
private IConfig m_Config;
|
||||
|
||||
// ISharedRegionModule.Initialize
|
||||
public void Initialise(IConfigSource config)
|
||||
{
|
||||
m_Config = config.Configs["WebRtcVoice"];
|
||||
if (m_Config is not null)
|
||||
{
|
||||
m_Enabled = m_Config.GetBoolean("Enabled", false);
|
||||
if (m_Enabled)
|
||||
{
|
||||
_MessageDetails = m_Config.GetBoolean("MessageDetails", false);
|
||||
|
||||
m_log.Info($"{logHeader}: enabled");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ISharedRegionModule.PostInitialize
|
||||
public void PostInitialise()
|
||||
{
|
||||
}
|
||||
|
||||
// ISharedRegionModule.AddRegion
|
||||
public void AddRegion(Scene scene)
|
||||
{
|
||||
// todo register module to get parcels changes etc
|
||||
}
|
||||
|
||||
// ISharedRegionModule.RemoveRegion
|
||||
public void RemoveRegion(Scene scene)
|
||||
{
|
||||
}
|
||||
|
||||
// ISharedRegionModule.RegionLoaded
|
||||
public void RegionLoaded(Scene scene)
|
||||
{
|
||||
if (m_Enabled)
|
||||
{
|
||||
scene.EventManager.OnRegisterCaps += delegate (UUID agentID, Caps caps)
|
||||
{
|
||||
OnRegisterCaps(scene, agentID, caps);
|
||||
};
|
||||
|
||||
ISimulatorFeaturesModule simFeatures = scene.RequestModuleInterface<ISimulatorFeaturesModule>();
|
||||
simFeatures?.AddFeature("VoiceServerType", OSD.FromString("webrtc"));
|
||||
}
|
||||
}
|
||||
|
||||
// ISharedRegionModule.Close
|
||||
public void Close()
|
||||
{
|
||||
}
|
||||
|
||||
// ISharedRegionModule.Name
|
||||
public string Name
|
||||
{
|
||||
get { return "RegionVoiceModule"; }
|
||||
}
|
||||
|
||||
// ISharedRegionModule.ReplaceableInterface
|
||||
public Type ReplaceableInterface
|
||||
{
|
||||
get { return null; }
|
||||
}
|
||||
|
||||
// <summary>
|
||||
// OnRegisterCaps is invoked via the scene.EventManager
|
||||
// everytime OpenSim hands out capabilities to a client
|
||||
// (login, region crossing). We contribute three capabilities to
|
||||
// the set of capabilities handed back to the client:
|
||||
// ProvisionVoiceAccountRequest, VoiceSignalingRequest and limited ChatSessionRequest
|
||||
//
|
||||
// ProvisionVoiceAccountRequest allows the client to obtain
|
||||
// voice communication information the the avater.
|
||||
//
|
||||
// VoiceSignalingRequest: Used for trickling ICE candidates.
|
||||
//
|
||||
// ChatSessionRequest
|
||||
//
|
||||
// Note that OnRegisterCaps is called here via a closure
|
||||
// delegate containing the scene of the respective region (see
|
||||
// Initialise()).
|
||||
// </summary>
|
||||
public void OnRegisterCaps(Scene scene, UUID agentID, Caps caps)
|
||||
{
|
||||
m_log.Debug(
|
||||
$"{logHeader}: OnRegisterCaps called with agentID {agentID} caps {caps} in scene {scene.Name}");
|
||||
|
||||
caps.RegisterSimpleHandler("ProvisionVoiceAccountRequest",
|
||||
new SimpleStreamHandler("/" + UUID.Random(), (IOSHttpRequest httpRequest, IOSHttpResponse httpResponse) =>
|
||||
{
|
||||
ProvisionVoiceAccountRequest(httpRequest, httpResponse, agentID, scene);
|
||||
}));
|
||||
|
||||
caps.RegisterSimpleHandler("VoiceSignalingRequest",
|
||||
new SimpleStreamHandler("/" + UUID.Random(), (IOSHttpRequest httpRequest, IOSHttpResponse httpResponse) =>
|
||||
{
|
||||
VoiceSignalingRequest(httpRequest, httpResponse, agentID, scene);
|
||||
}));
|
||||
|
||||
caps.RegisterSimpleHandler("ChatSessionRequest",
|
||||
new SimpleStreamHandler("/" + UUID.Random(), (IOSHttpRequest httpRequest, IOSHttpResponse httpResponse) =>
|
||||
{
|
||||
ChatSessionRequest(httpRequest, httpResponse, agentID, scene);
|
||||
}));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Callback for a client request for Voice Account Details
|
||||
/// </summary>
|
||||
/// <param name="scene">current scene object of the client</param>
|
||||
/// <param name="request"></param>
|
||||
/// <param name="path"></param>
|
||||
/// <param name="param"></param>
|
||||
/// <param name="agentID"></param>
|
||||
/// <param name="caps"></param>
|
||||
/// <returns></returns>
|
||||
public void ProvisionVoiceAccountRequest(IOSHttpRequest request, IOSHttpResponse response, UUID agentID, Scene scene)
|
||||
{
|
||||
// Get the voice service. If it doesn't exist, return an error.
|
||||
IWebRtcVoiceService voiceService = scene.RequestModuleInterface<IWebRtcVoiceService>();
|
||||
if (voiceService is null)
|
||||
{
|
||||
m_log.Error($"{logHeader}[ProvisionVoice]: voice service not loaded");
|
||||
response.StatusCode = (int)HttpStatusCode.NotFound;
|
||||
return;
|
||||
}
|
||||
|
||||
if(request.HttpMethod != "POST")
|
||||
{
|
||||
m_log.DebugFormat($"[{logHeader}][ProvisionVoice]: Not a POST request. Agent={agentID}");
|
||||
response.StatusCode = (int)HttpStatusCode.NotFound;
|
||||
return;
|
||||
}
|
||||
|
||||
// Deserialize the request. Convert the LLSDXml to OSD for our use
|
||||
OSDMap map = BodyToMap(request, "ProvisionVoiceAccountRequest");
|
||||
if (map is null)
|
||||
{
|
||||
m_log.Error($"{logHeader}[ProvisionVoice]: No request data found. Agent={agentID}");
|
||||
response.StatusCode = (int)HttpStatusCode.NoContent;
|
||||
return;
|
||||
}
|
||||
|
||||
// Make sure the request is for WebRtc voice
|
||||
if (map.TryGetValue("voice_server_type", out OSD vstosd))
|
||||
{
|
||||
if (vstosd is OSDString vst && !((string)vst).Equals("webrtc", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
m_log.Warn($"{logHeader}[ProvisionVoice]: voice_server_type is not 'webrtc'. Request: {map}");
|
||||
response.RawBuffer = llsdUndefAnswerBytes;
|
||||
response.StatusCode = (int)HttpStatusCode.OK;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (_MessageDetails) m_log.DebugFormat($"{logHeader}[ProvisionVoice]: request: {map}");
|
||||
|
||||
if (map.TryGetString("channel_type", out string channelType))
|
||||
{
|
||||
//do fully not trust viewers voice parcel requests
|
||||
if (channelType == "local")
|
||||
{
|
||||
if (!scene.RegionInfo.EstateSettings.AllowVoice)
|
||||
{
|
||||
m_log.Debug($"{logHeader}[ProvisionVoice]:region \"{scene.Name}\": voice not enabled in estate settings");
|
||||
response.RawBuffer = llsdUndefAnswerBytes;
|
||||
response.StatusCode = (int)HttpStatusCode.NotImplemented;
|
||||
return;
|
||||
}
|
||||
if (scene.LandChannel == null)
|
||||
{
|
||||
m_log.Error($"{logHeader}[ProvisionVoice] region \"{scene.Name}\" land data not yet available");
|
||||
response.RawBuffer = llsdUndefAnswerBytes;
|
||||
response.StatusCode = (int)HttpStatusCode.NotImplemented;
|
||||
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);
|
||||
if (parcel == null)
|
||||
{
|
||||
response.RawBuffer = llsdUndefAnswerBytes;
|
||||
response.StatusCode = (int)HttpStatusCode.NotFound;
|
||||
return;
|
||||
}
|
||||
|
||||
LandData land = parcel.LandData;
|
||||
if (land == null)
|
||||
{
|
||||
response.RawBuffer = llsdUndefAnswerBytes;
|
||||
response.StatusCode = (int)HttpStatusCode.NotFound;
|
||||
return;
|
||||
}
|
||||
|
||||
if (!scene.RegionInfo.EstateSettings.TaxFree && (land.Flags & (uint)ParcelFlags.AllowVoiceChat) == 0)
|
||||
{
|
||||
m_log.Debug($"{logHeader}[ProvisionVoice]:parcel voice not allowed");
|
||||
response.RawBuffer = llsdUndefAnswerBytes;
|
||||
response.StatusCode = (int)HttpStatusCode.Forbidden;
|
||||
return;
|
||||
}
|
||||
|
||||
if ((land.Flags & (uint)ParcelFlags.UseEstateVoiceChan) != 0)
|
||||
{
|
||||
map.Remove("parcel_local_id"); // estate channel
|
||||
}
|
||||
else 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// The checks passed. Send the request to the voice service.
|
||||
OSDMap resp = voiceService.ProvisionVoiceAccountRequest(map, agentID, scene.RegionInfo.RegionID).Result;
|
||||
|
||||
if(resp is not null)
|
||||
{
|
||||
if (_MessageDetails) m_log.DebugFormat($"{logHeader}[ProvisionVoice]: response: {resp}");
|
||||
|
||||
// TODO: check for errors and package the response
|
||||
|
||||
// Convert the OSD to LLSDXml for the response
|
||||
string xmlResp = OSDParser.SerializeLLSDXmlString(resp);
|
||||
response.RawBuffer = Util.UTF8.GetBytes(xmlResp);
|
||||
response.StatusCode = (int)HttpStatusCode.OK;
|
||||
}
|
||||
else
|
||||
{
|
||||
m_log.DebugFormat($"{logHeader}[ProvisionVoice]: got null response");
|
||||
response.StatusCode = (int)HttpStatusCode.OK;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
public void VoiceSignalingRequest(IOSHttpRequest request, IOSHttpResponse response, UUID agentID, Scene scene)
|
||||
{
|
||||
IWebRtcVoiceService voiceService = scene.RequestModuleInterface<IWebRtcVoiceService>();
|
||||
if (voiceService is null)
|
||||
{
|
||||
m_log.ErrorFormat($"{logHeader}[VoiceSignalingRequest]: avatar \"{agentID}\": no voice service");
|
||||
response.StatusCode = (int)HttpStatusCode.NotFound;
|
||||
return;
|
||||
}
|
||||
|
||||
if(request.HttpMethod != "POST")
|
||||
{
|
||||
m_log.Error($"[{logHeader}][VoiceSignaling]: Not a POST request. Agent={agentID}");
|
||||
response.StatusCode = (int)HttpStatusCode.NotFound;
|
||||
return;
|
||||
}
|
||||
|
||||
// Deserialize the request. Convert the LLSDXml to OSD for our use
|
||||
OSDMap map = BodyToMap(request, "VoiceSignalingRequest");
|
||||
if (map is null)
|
||||
{
|
||||
m_log.ErrorFormat($"{logHeader}[VoiceSignalingRequest]: No request data found. Agent={agentID}");
|
||||
response.StatusCode = (int)HttpStatusCode.NoContent;
|
||||
return;
|
||||
}
|
||||
|
||||
// Make sure the request is for WebRTC voice
|
||||
if (map.TryGetValue("voice_server_type", out OSD vstosd))
|
||||
{
|
||||
if (vstosd is OSDString vst && !((string)vst).Equals("webrtc", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
response.RawBuffer = llsdUndefAnswerBytes;
|
||||
response.StatusCode = (int)HttpStatusCode.OK;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
OSDMap resp = voiceService.VoiceSignalingRequest(map, agentID, scene.RegionInfo.RegionID).Result;
|
||||
|
||||
if (_MessageDetails) m_log.Debug($"{logHeader}[VoiceSignalingRequest]: Response: {resp}");
|
||||
|
||||
// TODO: check for errors and package the response
|
||||
|
||||
response.RawBuffer = llsdUndefAnswerBytes;
|
||||
response.StatusCode = (int)HttpStatusCode.OK;
|
||||
return;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Callback for a client request for ChatSessionRequest.
|
||||
/// The viewer sends this request when the user tries to start a P2P text or voice session
|
||||
/// with another user. We need to generate a new session ID and return it to the client.
|
||||
/// </summary>
|
||||
/// <param name="request"></param>
|
||||
/// <param name="response"></param>
|
||||
/// <param name="agentID"></param>
|
||||
/// <param name="scene"></param>
|
||||
public void ChatSessionRequest(IOSHttpRequest request, IOSHttpResponse response, UUID agentID, Scene scene)
|
||||
{
|
||||
m_log.DebugFormat("{0}: ChatSessionRequest received for agent {1} in scene {2}", logHeader, agentID, scene.RegionInfo.RegionName);
|
||||
if (request.HttpMethod != "POST")
|
||||
{
|
||||
response.StatusCode = (int)HttpStatusCode.NotFound;
|
||||
return;
|
||||
}
|
||||
|
||||
if (!scene.TryGetScenePresence(agentID, out ScenePresence sp) || sp.IsDeleted)
|
||||
{
|
||||
m_log.Warn($"{logHeader} ChatSessionRequest: scene presence not found or deleted for agent {agentID}");
|
||||
response.StatusCode = (int)HttpStatusCode.NotFound;
|
||||
return;
|
||||
}
|
||||
|
||||
OSDMap reqmap = BodyToMap(request, "[ChatSessionRequest]");
|
||||
if (reqmap is null)
|
||||
{
|
||||
m_log.Warn($"{logHeader} ChatSessionRequest: message body not parsable in request for agent {agentID}");
|
||||
response.StatusCode = (int)HttpStatusCode.NoContent;
|
||||
return;
|
||||
}
|
||||
|
||||
m_log.Debug($"{logHeader} ChatSessionRequest");
|
||||
|
||||
if (!reqmap.TryGetString("method", out string method))
|
||||
{
|
||||
m_log.Warn($"{logHeader} ChatSessionRequest: missing required 'method' field in request for agent {agentID}");
|
||||
response.StatusCode = (int)HttpStatusCode.NotFound;
|
||||
return;
|
||||
}
|
||||
|
||||
if (!reqmap.TryGetUUID("session-id", out UUID sessionID))
|
||||
{
|
||||
m_log.Warn($"{logHeader} ChatSessionRequest: missing required 'session-id' field in request for agent {agentID}");
|
||||
response.StatusCode = (int)HttpStatusCode.NotFound;
|
||||
return;
|
||||
}
|
||||
|
||||
switch (method.ToLower())
|
||||
{
|
||||
// Several different method requests that we don't know how to handle.
|
||||
// Just return OK for now.
|
||||
case "decline p2p voice":
|
||||
case "decline invitation":
|
||||
case "start conference":
|
||||
case "fetch history":
|
||||
response.StatusCode = (int)HttpStatusCode.OK;
|
||||
break;
|
||||
// Asking to start a P2P voice session. We need to generate a new session ID and return
|
||||
// it to the client in a ChatterBoxSessionStartReply event.
|
||||
case "start p2p voice":
|
||||
UUID newSessionID;
|
||||
if (reqmap.TryGetUUID("params", out UUID otherID))
|
||||
newSessionID = new(otherID.ulonga ^ agentID.ulonga, otherID.ulongb ^ agentID.ulongb);
|
||||
else
|
||||
newSessionID = UUID.Random();
|
||||
|
||||
IEventQueue queue = scene.RequestModuleInterface<IEventQueue>();
|
||||
if (queue is null)
|
||||
{
|
||||
m_log.ErrorFormat("{0}: no event queue for scene {1}", logHeader, scene.RegionInfo.RegionName);
|
||||
response.StatusCode = (int)HttpStatusCode.InternalServerError;
|
||||
}
|
||||
else
|
||||
{
|
||||
queue.ChatterBoxSessionStartReply(
|
||||
newSessionID,
|
||||
sp.Name,
|
||||
2,
|
||||
false,
|
||||
true,
|
||||
sessionID,
|
||||
true,
|
||||
string.Empty,
|
||||
agentID);
|
||||
|
||||
response.StatusCode = (int)HttpStatusCode.OK;
|
||||
}
|
||||
break;
|
||||
default:
|
||||
response.StatusCode = (int)HttpStatusCode.BadRequest;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Convert the LLSDXml body of the request to an OSDMap for easier handling.
|
||||
/// Also logs the request if message details is enabled.
|
||||
/// </summary>
|
||||
/// <param name="request"></param>
|
||||
/// <param name="pCaller"></param>
|
||||
/// <returns>'null' if the request body is empty or cannot be deserialized</returns>
|
||||
private OSDMap BodyToMap(IOSHttpRequest request, string pCaller)
|
||||
{
|
||||
try
|
||||
{
|
||||
using Stream inputStream = request.InputStream;
|
||||
if (inputStream.Length > 0)
|
||||
{
|
||||
OSD tmp = OSDParser.DeserializeLLSDXml(inputStream);
|
||||
if (_MessageDetails)
|
||||
m_log.Debug($"{pCaller} BodyToMap: Request: {tmp}");
|
||||
if(tmp is OSDMap map)
|
||||
return map;
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
m_log.Debug($"{pCaller} BodyToMap: Fail to decode LLSDXml request");
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,295 @@
|
||||
/*
|
||||
* 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.Generic;
|
||||
using System.Reflection;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
using OpenSim.Region.Framework.Scenes;
|
||||
using OpenSim.Region.Framework.Interfaces;
|
||||
using OpenSim.Server.Base;
|
||||
|
||||
using OpenMetaverse;
|
||||
using OpenMetaverse.StructuredData;
|
||||
|
||||
using Mono.Addins;
|
||||
|
||||
using log4net;
|
||||
using Nini.Config;
|
||||
|
||||
[assembly: Addin("WebRtcVoiceServiceModule", "1.0")]
|
||||
[assembly: AddinDependency("OpenSim.Region.Framework", OpenSim.VersionInfo.VersionNumber)]
|
||||
|
||||
namespace osWebRtcVoice
|
||||
{
|
||||
/// <summary>
|
||||
/// Interface for the WebRtcVoiceService.
|
||||
/// An instance of this is registered as the IWebRtcVoiceService for this region.
|
||||
/// The function here is to direct the capability requests to the appropriate voice service.
|
||||
/// For the moment, there are separate voice services for spatial and non-spatial voice
|
||||
/// with the idea that a region could have a pre-region spatial voice service while
|
||||
/// the grid could have a non-spatial voice service for group chat, etc.
|
||||
/// Fancier configurations are possible.
|
||||
/// </summary>
|
||||
[Extension(Path = "/OpenSim/RegionModules", NodeName = "RegionModule", Id = "WebRtcVoiceServiceModule")]
|
||||
public class WebRtcVoiceServiceModule : ISharedRegionModule, IWebRtcVoiceService
|
||||
{
|
||||
private static readonly ILog m_log = LogManager.GetLogger(MethodBase.GetCurrentMethod().DeclaringType);
|
||||
private static string LogHeader = "[WEBRTC VOICE SERVICE MODULE]";
|
||||
|
||||
private static bool m_Enabled = false;
|
||||
private IConfigSource m_Config;
|
||||
|
||||
private IWebRtcVoiceService m_spatialVoiceService;
|
||||
private IWebRtcVoiceService m_nonSpatialVoiceService;
|
||||
|
||||
// =====================================================================
|
||||
|
||||
// ISharedRegionModule.Initialize
|
||||
// Get configuration and load the modules that will handle spatial and non-spatial voice.
|
||||
public void Initialise(IConfigSource pConfig)
|
||||
{
|
||||
m_Config = pConfig;
|
||||
IConfig moduleConfig = m_Config.Configs["WebRtcVoice"];
|
||||
|
||||
if (moduleConfig is not null)
|
||||
{
|
||||
m_Enabled = moduleConfig.GetBoolean("Enabled", false);
|
||||
if (m_Enabled)
|
||||
{
|
||||
// Get the DLLs for the two voice services
|
||||
string spatialDllName = moduleConfig.GetString("SpatialVoiceService", string.Empty);
|
||||
string nonSpatialDllName = moduleConfig.GetString("NonSpatialVoiceService", string.Empty);
|
||||
if (string.IsNullOrEmpty(spatialDllName) && string.IsNullOrEmpty(nonSpatialDllName))
|
||||
{
|
||||
m_log.Error($"{LogHeader} No VoiceService specified in configuration");
|
||||
m_Enabled = false;
|
||||
return;
|
||||
}
|
||||
|
||||
// Default non-spatial to spatial if not specified
|
||||
if (string.IsNullOrEmpty(nonSpatialDllName))
|
||||
{
|
||||
m_log.Debug($"{LogHeader} nonSpatialDllName not specified. Defaulting to spatialDllName");
|
||||
nonSpatialDllName = spatialDllName;
|
||||
}
|
||||
|
||||
// Load the two voice services
|
||||
m_log.Debug($"{LogHeader} Loading SpatialVoiceService from {spatialDllName}");
|
||||
m_spatialVoiceService = ServerUtils.LoadPlugin<IWebRtcVoiceService>(spatialDllName, [m_Config]);
|
||||
if (m_spatialVoiceService is null)
|
||||
{
|
||||
m_log.Error($"{LogHeader} Could not load SpatialVoiceService from {spatialDllName}, module disabled");
|
||||
m_Enabled = false;
|
||||
return;
|
||||
}
|
||||
|
||||
m_log.Debug($"{LogHeader} Loading NonSpatialVoiceService from {nonSpatialDllName}");
|
||||
if (spatialDllName != nonSpatialDllName)
|
||||
{
|
||||
m_nonSpatialVoiceService = ServerUtils.LoadPlugin<IWebRtcVoiceService>(nonSpatialDllName, [ m_Config ]);
|
||||
if (m_nonSpatialVoiceService is null)
|
||||
{
|
||||
m_log.Error("{LogHeader} Could not load NonSpatialVoiceService from {nonSpatialDllName}");
|
||||
m_Enabled = false;
|
||||
}
|
||||
}
|
||||
|
||||
if (m_Enabled)
|
||||
{
|
||||
m_log.Info($"{LogHeader} WebRtcVoiceService enabled");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ISharedRegionModule.PostInitialize
|
||||
public void PostInitialise()
|
||||
{
|
||||
}
|
||||
|
||||
// ISharedRegionModule.Close
|
||||
public void Close()
|
||||
{
|
||||
}
|
||||
|
||||
// ISharedRegionModule.ReplaceableInterface
|
||||
public Type ReplaceableInterface
|
||||
{
|
||||
get { return null; }
|
||||
}
|
||||
|
||||
// ISharedRegionModule.Name
|
||||
public string Name
|
||||
{
|
||||
get { return "WebRtcVoiceServiceModule"; }
|
||||
}
|
||||
|
||||
// ISharedRegionModule.AddRegion
|
||||
public void AddRegion(Scene scene)
|
||||
{
|
||||
if (m_Enabled)
|
||||
{
|
||||
m_log.Debug($"{LogHeader} Adding WebRtcVoiceService to region {scene.Name}");
|
||||
scene.RegisterModuleInterface<IWebRtcVoiceService>(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;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// ISharedRegionModule.RemoveRegion
|
||||
public void RemoveRegion(Scene scene)
|
||||
{
|
||||
if (m_Enabled)
|
||||
{
|
||||
scene.UnregisterModuleInterface<IWebRtcVoiceService>(this);
|
||||
}
|
||||
}
|
||||
|
||||
// ISharedRegionModule.RegionLoaded
|
||||
public void RegionLoaded(Scene scene)
|
||||
{
|
||||
}
|
||||
|
||||
// =====================================================================
|
||||
// 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<KeyValuePair<string, IVoiceViewerSession>> vSessions;
|
||||
if (VoiceViewerSession.TryGetViewerSessionByAgentId(pAgentID, out 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
// =====================================================================
|
||||
// IWebRtcVoiceService
|
||||
|
||||
// IWebRtcVoiceService.ProvisionVoiceAccountRequest
|
||||
public async Task<OSDMap> ProvisionVoiceAccountRequest(OSDMap pRequest, UUID pUserID, UUID pSceneID)
|
||||
{
|
||||
OSDMap response = null;
|
||||
IVoiceViewerSession vSession = null;
|
||||
if (pRequest.TryGetString("viewer_session", out string viewerSessionId))
|
||||
{
|
||||
// request has a viewer session. Use that to find the voice service
|
||||
if (!VoiceViewerSession.TryGetViewerSession(viewerSessionId, out vSession))
|
||||
{
|
||||
m_log.Error($"{0} ProvisionVoiceAccountRequest: viewer session {viewerSessionId} not found");
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// the request does not have a viewer session. See if it's an initial request
|
||||
if (pRequest.TryGetString("channel_type", out string channelType))
|
||||
{
|
||||
if (channelType == "local")
|
||||
{
|
||||
// TODO: check if this userId is making a new session (case that user is reconnecting)
|
||||
vSession = m_spatialVoiceService.CreateViewerSession(pRequest, pUserID, pSceneID);
|
||||
VoiceViewerSession.AddViewerSession(vSession);
|
||||
}
|
||||
else
|
||||
{
|
||||
// TODO: check if this userId is making a new session (case that user is reconnecting)
|
||||
vSession = m_nonSpatialVoiceService.CreateViewerSession(pRequest, pUserID, pSceneID);
|
||||
VoiceViewerSession.AddViewerSession(vSession);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
m_log.Error($"{LogHeader} ProvisionVoiceAccountRequest: no channel_type in request");
|
||||
}
|
||||
}
|
||||
if (vSession is not null)
|
||||
{
|
||||
response = await vSession.VoiceService.ProvisionVoiceAccountRequest(vSession, pRequest, pUserID, pSceneID);
|
||||
}
|
||||
return response;
|
||||
}
|
||||
|
||||
// IWebRtcVoiceService.VoiceSignalingRequest
|
||||
public async Task<OSDMap> VoiceSignalingRequest(OSDMap pRequest, UUID pUserID, UUID pSceneID)
|
||||
{
|
||||
OSDMap response = null;
|
||||
IVoiceViewerSession vSession = null;
|
||||
if (pRequest.TryGetString("viewer_session", out string viewerSessionId))
|
||||
{
|
||||
// request has a viewer session. Use that to find the voice service
|
||||
if (VoiceViewerSession.TryGetViewerSession(viewerSessionId, out vSession))
|
||||
{
|
||||
response = await vSession.VoiceService.VoiceSignalingRequest(vSession, pRequest, pUserID, pSceneID);
|
||||
}
|
||||
else
|
||||
{
|
||||
m_log.ErrorFormat("{0} VoiceSignalingRequest: viewer session {1} not found", LogHeader, viewerSessionId);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
m_log.ErrorFormat("{0} VoiceSignalingRequest: no viewer_session in request", LogHeader);
|
||||
}
|
||||
return response;
|
||||
}
|
||||
|
||||
// This module should never be called with this signature
|
||||
public Task<OSDMap> ProvisionVoiceAccountRequest(IVoiceViewerSession pVSession, OSDMap pRequest, UUID pUserID, UUID pSceneID)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
// This module should never be called with this signature
|
||||
public Task<OSDMap> VoiceSignalingRequest(IVoiceViewerSession pVSession, OSDMap pRequest, UUID pUserID, UUID pSceneID)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
public IVoiceViewerSession CreateViewerSession(OSDMap pRequest, UUID pUserID, UUID pSceneID)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -71,7 +71,7 @@ namespace OpenSim.Framework
|
||||
Flags = OSHTTPURIFlags.None;
|
||||
try
|
||||
{
|
||||
Uri m_checkuri = new Uri(uri);
|
||||
Uri m_checkuri = new(uri);
|
||||
|
||||
if(m_checkuri.Scheme != Uri.UriSchemeHttp && m_checkuri.Scheme != Uri.UriSchemeHttps)
|
||||
return;
|
||||
@@ -368,10 +368,11 @@ namespace OpenSim.Framework
|
||||
private string m_SearchURL = string.Empty;
|
||||
private string m_DestinationGuideURL = string.Empty;
|
||||
private string m_economyURL = string.Empty;
|
||||
private string[] m_StunServers = null;
|
||||
|
||||
public GridInfo (IConfigSource config, string defaultURI = "")
|
||||
{
|
||||
string[] sections = new string[] {"Const", "Startup", "Hypergrid"};
|
||||
string[] sections = ["Const", "Startup", "Hypergrid"];
|
||||
|
||||
string gatekeeper = Util.GetConfigVarFromSections<string>(config, "GatekeeperURI", sections, string.Empty);
|
||||
if (string.IsNullOrEmpty(gatekeeper))
|
||||
@@ -416,8 +417,7 @@ namespace OpenSim.Framework
|
||||
OSHHTPHost tmp = new OSHHTPHost(alias[i].Trim(), false);
|
||||
if (tmp.IsValidHost)
|
||||
{
|
||||
if (m_gateKeeperAlias == null)
|
||||
m_gateKeeperAlias = new HashSet<OSHHTPHost>();
|
||||
m_gateKeeperAlias ??= new HashSet<OSHHTPHost>();
|
||||
m_gateKeeperAlias.Add(tmp);
|
||||
}
|
||||
}
|
||||
@@ -431,6 +431,7 @@ namespace OpenSim.Framework
|
||||
m_gridUrlAlias[i++] = a.URI;
|
||||
}
|
||||
|
||||
|
||||
string home = Util.GetConfigVarFromSections<string>(config, "HomeURI", sections, string.Empty);
|
||||
|
||||
if (string.IsNullOrEmpty(home))
|
||||
@@ -458,14 +459,13 @@ namespace OpenSim.Framework
|
||||
OSHHTPHost tmp = new OSHHTPHost(alias[i].Trim(), false);
|
||||
if (tmp.IsValidHost)
|
||||
{
|
||||
if (m_homeURLAlias == null)
|
||||
m_homeURLAlias = new HashSet<OSHHTPHost>();
|
||||
m_homeURLAlias ??= new HashSet<OSHHTPHost>();
|
||||
m_homeURLAlias.Add(tmp);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
string[] namessections = new string[] { "Const", "GridInfo", "SimulatorFeatures" };
|
||||
string[] namessections = ["Const", "GridInfo", "SimulatorFeatures"];
|
||||
m_GridName = Util.GetConfigVarFromSections<string>(config, "GridName", namessections, string.Empty);
|
||||
if (string.IsNullOrEmpty(m_GridName))
|
||||
m_GridName = Util.GetConfigVarFromSections<string>(config, "gridname", namessections, string.Empty);
|
||||
@@ -504,7 +504,8 @@ namespace OpenSim.Framework
|
||||
m_DestinationGuideURL = tmpuri.URI;
|
||||
}
|
||||
|
||||
m_economyURL = Util.GetConfigVarFromSections<string>(config, "economy", new string[] { "Economy", "GridInfo" });
|
||||
|
||||
m_economyURL = Util.GetConfigVarFromSections<string>(config, "economy", ["Economy", "GridInfo"]);
|
||||
if (!string.IsNullOrEmpty(m_economyURL))
|
||||
{
|
||||
tmpuri = new OSHTTPURI(m_economyURL.Trim(), true);
|
||||
@@ -515,6 +516,20 @@ namespace OpenSim.Framework
|
||||
}
|
||||
m_economyURL = tmpuri.URI;
|
||||
}
|
||||
|
||||
string stunservers = Util.GetConfigVarFromSections<string>(config, "StunServers", ["Const", "Startup", "GridInfo", "SimulatorFeatures"], string.Empty);
|
||||
if (!string.IsNullOrWhiteSpace(stunservers))
|
||||
{
|
||||
string[] stuns = stunservers.Split(',');
|
||||
List<string> stunsarr = new List<string>(stuns.Length);
|
||||
for (int i = 0; i < stuns.Length; ++i)
|
||||
{
|
||||
OSHHTPHost tmp = new OSHHTPHost(stuns[i].Trim(), false);
|
||||
if (tmp.IsValidHost)
|
||||
stunsarr.Add(tmp.URI);
|
||||
}
|
||||
m_StunServers = stunsarr.Count > 0 ? stunsarr.ToArray() : null;
|
||||
}
|
||||
}
|
||||
|
||||
public bool HasHGConfig
|
||||
@@ -685,6 +700,7 @@ namespace OpenSim.Framework
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public string GridName
|
||||
{
|
||||
get { return m_GridName; }
|
||||
@@ -756,5 +772,27 @@ namespace OpenSim.Framework
|
||||
m_log.Error((tmp.IsValidHost ? "Could not resolve EconomyURL" : "EconomyURL is a invalid host ") + value ?? "");
|
||||
}
|
||||
}
|
||||
|
||||
public string[] StunServers
|
||||
{
|
||||
get { return m_StunServers; }
|
||||
set
|
||||
{
|
||||
if(value.Length > 0)
|
||||
{
|
||||
List<string> values = new List<string>(value.Length);
|
||||
for (int i = 0; i < value.Length; ++i)
|
||||
{
|
||||
OSHHTPHost tmp = new OSHHTPHost(value[i].Trim(), false);
|
||||
if (tmp.IsValidHost)
|
||||
values.Add(tmp.URI);
|
||||
}
|
||||
m_StunServers = values.Count > 0 ? values.ToArray() : null;
|
||||
}
|
||||
else
|
||||
m_StunServers = null;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,8 +25,9 @@
|
||||
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
*/
|
||||
|
||||
using OpenSim.Region.Framework.Scenes;
|
||||
using OpenMetaverse;
|
||||
using OpenSim.Region.Framework.Scenes;
|
||||
using static OpenMetaverse.Primitive.RenderMaterials;
|
||||
|
||||
namespace OpenSim.Region.Framework.Interfaces
|
||||
{
|
||||
@@ -36,5 +37,7 @@ namespace OpenSim.Region.Framework.Interfaces
|
||||
FaceMaterial GetMaterialCopy(UUID ID);
|
||||
UUID AddNewMaterial(FaceMaterial fm);
|
||||
void RemoveMaterial(UUID id);
|
||||
bool CleanMaterialOverrides(ref RenderMaterialOverrideEntry[] overrides, int side, bool removeTransforms = false);
|
||||
bool RemoveMaterialEntry(ref RenderMaterialEntry[] entries, int side);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1278,6 +1278,12 @@ namespace OpenSim.Region.Framework.Scenes
|
||||
if (!string.IsNullOrEmpty(SceneGridInfo.EconomyURL))
|
||||
fm.AddOpenSimExtraFeature("currency-base-uri", SceneGridInfo.EconomyURL);
|
||||
}
|
||||
|
||||
if (SceneGridInfo.StunServers is not null)
|
||||
{
|
||||
string stuns = string.Join(',', SceneGridInfo.StunServers);
|
||||
fm.AddFeature("stun-servers", stuns);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1025,7 +1025,8 @@ namespace OpenSim.Region.OptionalModules.Materials
|
||||
|
||||
public static readonly byte[] XMLkeyMaterialSucess = osUTF8.GetASCIIBytes("<llsd><map><key>success</key><integer>1</integer></map></llsd>\r\n");
|
||||
public static readonly byte[] XMLkeyMaterialFail = osUTF8.GetASCIIBytes("<llsd><map><key>success</key><integer>0</integer></map></llsd>\r\n");
|
||||
private static bool RemoveMaterialEntry(ref RenderMaterialEntry[] entries, int side)
|
||||
|
||||
public bool RemoveMaterialEntry(ref RenderMaterialEntry[] entries, int side)
|
||||
{
|
||||
if (entries is null || entries.Length == 0)
|
||||
return false;
|
||||
@@ -1386,5 +1387,90 @@ namespace OpenSim.Region.OptionalModules.Materials
|
||||
overrides[indx].data = data;
|
||||
return true;
|
||||
}
|
||||
|
||||
public static bool ClearOverrideData(OSDMap facemat, out OSDMap changedmat)
|
||||
{
|
||||
if(facemat is not null && facemat.TryGetOSDArray("ti", out OSDArray transforms))
|
||||
{
|
||||
changedmat = new OSDMap()
|
||||
{
|
||||
["ti"] = transforms
|
||||
};
|
||||
return facemat.Count > 1;
|
||||
}
|
||||
|
||||
changedmat = null;
|
||||
return facemat is not null;
|
||||
}
|
||||
|
||||
public bool CleanMaterialOverrides(ref RenderMaterialOverrideEntry[] overrides, int side, bool removeTransforms = false)
|
||||
{
|
||||
if (overrides is null || overrides.Length == 0)
|
||||
return false;
|
||||
|
||||
bool changed = false;
|
||||
if(removeTransforms && side < 0)
|
||||
{
|
||||
overrides = null;
|
||||
return true;
|
||||
}
|
||||
|
||||
List<RenderMaterialOverrideEntry> newoverrides = new(overrides.Length);
|
||||
|
||||
for(int indx = 0; indx < overrides.Length; indx++)
|
||||
{
|
||||
if(side >= 0 && side != overrides[indx].te_index)
|
||||
{
|
||||
newoverrides.Add(overrides[indx]);
|
||||
}
|
||||
else if (removeTransforms)
|
||||
{
|
||||
changed = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
if(string.IsNullOrEmpty(overrides[indx].data))
|
||||
{
|
||||
changed = true;
|
||||
continue;
|
||||
}
|
||||
|
||||
int tiindx = overrides[indx].data.IndexOf("ti");
|
||||
if(tiindx < 0)
|
||||
{
|
||||
changed = true;
|
||||
continue;
|
||||
}
|
||||
|
||||
StringBuilder sb = osStringBuilderCache.Acquire();
|
||||
sb.Append("{'");
|
||||
int brk = 0;
|
||||
do
|
||||
{
|
||||
char c = overrides[indx].data[tiindx];
|
||||
sb.Append(c);
|
||||
if(c=='[')
|
||||
brk++;
|
||||
else if(c==']')
|
||||
{
|
||||
brk--;
|
||||
if(brk == 0)
|
||||
break;
|
||||
}
|
||||
tiindx++;
|
||||
}
|
||||
while (tiindx < overrides[indx].data.Length);
|
||||
|
||||
sb.Append('}');
|
||||
string newdata = osStringBuilderCache.GetStringAndRelease(sb);
|
||||
changed = !newdata.Equals(overrides[indx].data);
|
||||
overrides[indx].data = newdata;
|
||||
newoverrides.Add(overrides[indx]);
|
||||
}
|
||||
}
|
||||
|
||||
overrides = newoverrides.Count > 0 ? [.. newoverrides] : null;
|
||||
return changed;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19657,6 +19657,123 @@ namespace OpenSim.Region.ScriptEngine.Shared.Api
|
||||
return 0;
|
||||
}
|
||||
|
||||
public void llSetRenderMaterial(LSL_String materialstr, LSL_Integer lsl_face)
|
||||
{
|
||||
if(m_materialsModule is null)
|
||||
return;
|
||||
|
||||
if(string.IsNullOrEmpty(materialstr.m_string))
|
||||
{
|
||||
Error("llSetRenderMaterial", "material \"\" not found");
|
||||
return;
|
||||
}
|
||||
|
||||
int face = lsl_face.value;
|
||||
bool changed;
|
||||
|
||||
if(UUID.ZeroString.Equals(materialstr.m_string, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
if(m_host.Shape.RenderMaterials is null || m_host.Shape.RenderMaterials.entries is null || m_host.Shape.RenderMaterials.entries.Length == 0)
|
||||
return;
|
||||
|
||||
changed = m_materialsModule.CleanMaterialOverrides(ref m_host.Shape.RenderMaterials.overrides, face);
|
||||
if(face == ScriptBaseClass.ALL_SIDES)
|
||||
{
|
||||
m_host.Shape.RenderMaterials.entries = null;
|
||||
changed = true;
|
||||
}
|
||||
else
|
||||
changed |= m_materialsModule.RemoveMaterialEntry(ref m_host.Shape.RenderMaterials.entries, face);
|
||||
|
||||
if(changed)
|
||||
{
|
||||
m_host.ParentGroup.HasGroupChanged = true;
|
||||
m_host.ScheduleUpdate(PrimUpdateFlags.MaterialOvr | PrimUpdateFlags.FullUpdate);
|
||||
m_host.TriggerScriptChangedEvent(Changed.MATERIAL);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
UUID matID = ScriptUtils.GetAssetIdFromItemName(m_host, materialstr.m_string, (int)AssetType.Material);
|
||||
if (matID.IsZero())
|
||||
{
|
||||
if (!UUID.TryParse(materialstr.m_string, out matID) || matID.IsZero())
|
||||
{
|
||||
Error("llSetRenderMaterial", $"material \"{materialstr.m_string}\" not found");
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
int nsides = GetNumberOfSides(m_host);
|
||||
if(face >= nsides)
|
||||
return;
|
||||
|
||||
m_host.Shape.RenderMaterials ??= new();
|
||||
m_host.Shape.RenderMaterials.entries ??= new Primitive.RenderMaterials.RenderMaterialEntry[1];
|
||||
|
||||
changed = m_materialsModule.CleanMaterialOverrides(ref m_host.Shape.RenderMaterials.overrides, face);
|
||||
if(face == ScriptBaseClass.ALL_SIDES)
|
||||
{
|
||||
if(m_host.Shape.RenderMaterials.entries is null || m_host.Shape.RenderMaterials.entries.Length != nsides)
|
||||
{
|
||||
m_host.Shape.RenderMaterials.entries = new Primitive.RenderMaterials.RenderMaterialEntry[nsides];
|
||||
for (int i = 0; i < m_host.Shape.RenderMaterials.entries.Length; i++)
|
||||
{
|
||||
m_host.Shape.RenderMaterials.entries[i] = new()
|
||||
{
|
||||
te_index = (byte)i,
|
||||
id = matID
|
||||
};
|
||||
}
|
||||
changed = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
for (int i = 0; i < m_host.Shape.RenderMaterials.entries.Length; i++)
|
||||
{
|
||||
if(matID.NotEqual(m_host.Shape.RenderMaterials.entries[i].id))
|
||||
{
|
||||
changed = true;
|
||||
m_host.Shape.RenderMaterials.entries[i].id = matID;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
int indx = 0;
|
||||
for( ; indx < m_host.Shape.RenderMaterials.entries.Length; indx++)
|
||||
{
|
||||
if (m_host.Shape.RenderMaterials.entries[indx].te_index == face)
|
||||
{
|
||||
if(matID.NotEqual(m_host.Shape.RenderMaterials.entries[indx].id))
|
||||
{
|
||||
changed = true;
|
||||
m_host.Shape.RenderMaterials.entries[indx].id = matID;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
if(indx == m_host.Shape.RenderMaterials.entries.Length)
|
||||
{
|
||||
Array.Resize(ref m_host.Shape.RenderMaterials.entries, m_host.Shape.RenderMaterials.entries.Length + 1);
|
||||
|
||||
m_host.Shape.RenderMaterials.entries[indx] = new()
|
||||
{
|
||||
te_index = (byte)face,
|
||||
id = matID
|
||||
};
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
if(changed)
|
||||
{
|
||||
m_host.ParentGroup.HasGroupChanged = true;
|
||||
m_host.ScheduleUpdate(PrimUpdateFlags.MaterialOvr | PrimUpdateFlags.FullUpdate);
|
||||
m_host.TriggerScriptChangedEvent(Changed.MATERIAL);
|
||||
}
|
||||
}
|
||||
|
||||
public LSL_Vector llWorldPosToHUD(LSL_Vector wp)
|
||||
{
|
||||
if(!m_host.ParentGroup.IsAttachment)
|
||||
|
||||
@@ -538,5 +538,6 @@ namespace OpenSim.Region.ScriptEngine.Shared.Api.Interfaces
|
||||
LSL_String llGetRenderMaterial(LSL_Integer face);
|
||||
LSL_Integer llIsLinkGLTFMaterial(LSL_Integer linknum, LSL_Integer face);
|
||||
LSL_Vector llWorldPosToHUD(LSL_Vector WorldPosition);
|
||||
void llSetRenderMaterial(LSL_String material, LSL_Integer face);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2905,5 +2905,11 @@ namespace OpenSim.Region.ScriptEngine.Shared.ScriptBase
|
||||
{
|
||||
return m_LSL_Functions.llWorldPosToHUD(WorldPosition);
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public void llSetRenderMaterial(LSL_String material, LSL_Integer face)
|
||||
{
|
||||
m_LSL_Functions.llSetRenderMaterial(material, face);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -76,6 +76,8 @@
|
||||
; from outside their networks, using firewalls
|
||||
PrivatePort = "8003"
|
||||
|
||||
|
||||
|
||||
[Startup]
|
||||
;# {ConsolePrompt} {} {ConsolePrompt} {} "Region (\R) "
|
||||
;; Console prompt
|
||||
@@ -368,6 +370,9 @@
|
||||
;; having both options true does provide encryption but with low security
|
||||
;; set both true if you don't care to use SSL, they are needed to contact regions or grids that do use it.
|
||||
|
||||
;; list of stun servers, for now to tell viewers for webrtc support
|
||||
StunServers = stun.l.google.com:19302, stun.cloudflare.com:3478
|
||||
|
||||
[AccessControl]
|
||||
;# {AllowedClients} {} {Bar (|) separated list of allowed clients} {}
|
||||
;; Bar (|) separated list of viewers which may gain access to the regions.
|
||||
|
||||
29
bin/config/os-webrtc-janus.ini.example
Normal file
29
bin/config/os-webrtc-janus.ini.example
Normal file
@@ -0,0 +1,29 @@
|
||||
[WebRtcVoice]
|
||||
Enabled=false
|
||||
; Module to use for spatial WebRtcVoice
|
||||
; The following is for a Janus service just for spatial voice for this region. Config in [JanusWebRtcVoice]
|
||||
SpatialVoiceService = WebRtcJanusService.dll:WebRtcJanusService
|
||||
; The following is for a grid spatial voice service for this region. Link is WebRtcVoiceServiceURI
|
||||
; SpatialVoiceService=WebRtcVoice.dll:WebRtcVoiceServiceConnector
|
||||
; Module to use for non-spatial WebRtcVoice. Local voice service. Config in [JanusWebRtcVoice]
|
||||
NonSpatialVoiceService = WebRtcJanusService.dll:WebRtcJanusService
|
||||
; The following is for a grid non-spatial voice service. For groups and IMs. Link is WebRtcVoiceServiceURI
|
||||
; NonSpatialVoiceService=WebRtcVoice.dll:WebRtcVoiceServiceConnector
|
||||
; URL for the grid service that is providing the WebRtcVoiceService. Used by WebRtcVoiceServiceConnector.
|
||||
;WebRtcVoiceServerURI = ${Const|PrivURL}:${Const|PrivatePort}
|
||||
; Debugging: output to log file messages sent and received from the viewer. Very verbose.
|
||||
MessageDetails = false
|
||||
|
||||
[JanusWebRtcVoice]
|
||||
; URI to access the Janus Gateway
|
||||
JanusGatewayURI = http://janus.example.org:14223/voice
|
||||
; APIKey to access the Janus Gateway. Must be set to the same value as the Janus Gateway.
|
||||
APIToken = APITokenToNeverCheckIn
|
||||
; URI to access the admin port on Janus Gateway
|
||||
JanusGatewayAdminURI = http://janus.example.org/admin
|
||||
; APIKey to access the admin port on the Janus Gateway. Must be set to the same value as the Janus Gateway.
|
||||
AdminAPIToken = AdminAPITokenToNeverCheckIn
|
||||
; Debugging: output to log file messages sent and received from Janus. Very verbose.
|
||||
MessageDetails = false
|
||||
|
||||
|
||||
190
prebuild.xml
190
prebuild.xml
@@ -2037,6 +2037,196 @@
|
||||
|
||||
</Project>
|
||||
|
||||
<Project
|
||||
name="WebRtcVoice"
|
||||
path="OpenSim/Addons/os-webrtc-janus/WebRtcVoice"
|
||||
type="Library">
|
||||
|
||||
<Configuration name="Debug">
|
||||
<Options>
|
||||
<OutputPath>../../../../bin/</OutputPath>
|
||||
<AllowUnsafe>true</AllowUnsafe>
|
||||
</Options>
|
||||
</Configuration>
|
||||
<Configuration name="Release">
|
||||
<Options>
|
||||
<OutputPath>../../../../bin/</OutputPath>
|
||||
<AllowUnsafe>true</AllowUnsafe>
|
||||
</Options>
|
||||
</Configuration>
|
||||
|
||||
<ReferencePath>../../../../bin/</ReferencePath>
|
||||
<Reference name="OpenSim.Framework"/>
|
||||
<Reference name="OpenSim.Framework.Console"/>
|
||||
<Reference name="OpenSim.Framework.Servers.HttpServer"/>
|
||||
<Reference name="OpenSim.Services.Interfaces"/>
|
||||
<Reference name="OpenSim.Services.Connectors"/>
|
||||
<Reference name="OpenSim.Services.Base"/>
|
||||
<Reference name="OpenSim.Server.Base"/>
|
||||
<Reference name="OpenSim.Server.Handlers"/>
|
||||
<Reference name="OpenSim.Data"/>
|
||||
|
||||
<Reference name="OpenMetaverse"/>
|
||||
<Reference name="OpenMetaverseTypes"/>
|
||||
<Reference name="OpenMetaverse.StructuredData"/>
|
||||
|
||||
<Reference name="Mono.Addins" path ="../../../../bin"/>
|
||||
|
||||
<Reference name="Nini"/>
|
||||
<Reference name="log4net"/>
|
||||
|
||||
<Files>
|
||||
<Match pattern="*.cs" recurse="true">
|
||||
<Exclude name="Tests" pattern="Tests"/>
|
||||
<Exclude name="Object" pattern="obj"/>
|
||||
</Match>
|
||||
</Files>
|
||||
|
||||
</Project>
|
||||
|
||||
<Project
|
||||
name="WebRtcVoiceRegionModule"
|
||||
path="OpenSim/Addons/os-webrtc-janus/WebRtcVoiceRegionModule"
|
||||
type="Library">
|
||||
|
||||
<Configuration name="Debug">
|
||||
<Options>
|
||||
<OutputPath>../../../../bin/</OutputPath>
|
||||
<AllowUnsafe>true</AllowUnsafe>
|
||||
</Options>
|
||||
</Configuration>
|
||||
<Configuration name="Release">
|
||||
<Options>
|
||||
<OutputPath>../../../../bin/</OutputPath>
|
||||
<AllowUnsafe>true</AllowUnsafe>
|
||||
</Options>
|
||||
</Configuration>
|
||||
|
||||
<ReferencePath>../../../../bin/</ReferencePath>
|
||||
<Reference name="OpenSim.Framework"/>
|
||||
<Reference name="OpenSim.Framework.Console"/>
|
||||
<Reference name="OpenSim.Framework.Servers.HttpServer"/>
|
||||
<Reference name="OpenSim.Services.Interfaces"/>
|
||||
<Reference name="OpenSim.Services.Connectors"/>
|
||||
<Reference name="OpenSim.Services.Base"/>
|
||||
<Reference name="OpenSim.Server.Base"/>
|
||||
<Reference name="OpenSim.Data"/>
|
||||
|
||||
<Reference name="OpenMetaverse"/>
|
||||
<Reference name="OpenMetaverseTypes"/>
|
||||
<Reference name="OpenMetaverse.StructuredData"/>
|
||||
|
||||
<Reference name="WebRtcVoice"/>
|
||||
|
||||
<Reference name="Mono.Addins" path ="../../../../bin"/>
|
||||
|
||||
<Reference name="Nini"/>
|
||||
<Reference name="log4net"/>
|
||||
|
||||
<Files>
|
||||
<Match pattern="*.cs" recurse="true">
|
||||
<Exclude name="Tests" pattern="Tests"/>
|
||||
<Exclude name="Object" pattern="obj"/>
|
||||
</Match>
|
||||
</Files>
|
||||
|
||||
</Project>
|
||||
|
||||
|
||||
<Project
|
||||
name="WebRtcJanusService"
|
||||
path="OpenSim/Addons/os-webrtc-janus/Janus"
|
||||
type="Library">
|
||||
|
||||
<Configuration name="Debug">
|
||||
<Options>
|
||||
<OutputPath>../../../../bin/</OutputPath>
|
||||
<AllowUnsafe>true</AllowUnsafe>
|
||||
</Options>
|
||||
</Configuration>
|
||||
<Configuration name="Release">
|
||||
<Options>
|
||||
<OutputPath>../../../../bin/</OutputPath>
|
||||
<AllowUnsafe>true</AllowUnsafe>
|
||||
</Options>
|
||||
</Configuration>
|
||||
|
||||
<ReferencePath>../../../../bin/</ReferencePath>
|
||||
<Reference name="OpenSim.Framework"/>
|
||||
<Reference name="OpenSim.Framework.Console"/>
|
||||
<Reference name="OpenSim.Framework.Servers.HttpServer"/>
|
||||
<Reference name="OpenSim.Services.Interfaces"/>
|
||||
<Reference name="OpenSim.Services.Connectors"/>
|
||||
<Reference name="OpenSim.Services.Base"/>
|
||||
<Reference name="OpenSim.Server.Base"/>
|
||||
<Reference name="OpenSim.Data"/>
|
||||
|
||||
<Reference name="OpenMetaverse"/>
|
||||
<Reference name="OpenMetaverseTypes"/>
|
||||
<Reference name="OpenMetaverse.StructuredData"/>
|
||||
|
||||
<Reference name="WebRtcVoice"/>
|
||||
|
||||
<Reference name="Mono.Addins" path ="../../../../bin"/>
|
||||
|
||||
<Reference name="Nini"/>
|
||||
<Reference name="log4net"/>
|
||||
|
||||
<Files>
|
||||
<Match pattern="*.cs" recurse="true">
|
||||
<Exclude name="Tests" pattern="Tests"/>
|
||||
<Exclude name="Object" pattern="obj"/>
|
||||
</Match>
|
||||
</Files>
|
||||
|
||||
</Project>
|
||||
|
||||
<Project
|
||||
name="WebRtcVoiceServiceModule"
|
||||
path="OpenSim/Addons/os-webrtc-janus/WebRtcVoiceServiceModule"
|
||||
type="Library">
|
||||
|
||||
<Configuration name="Debug">
|
||||
<Options>
|
||||
<OutputPath>../../../../bin/</OutputPath>
|
||||
</Options>
|
||||
</Configuration>
|
||||
<Configuration name="Release">
|
||||
<Options>
|
||||
<OutputPath>../../../../bin/</OutputPath>
|
||||
</Options>
|
||||
</Configuration>
|
||||
|
||||
<ReferencePath>../../../../bin/</ReferencePath>
|
||||
<Reference name="OpenSim.Framework"/>
|
||||
<Reference name="OpenSim.Framework.Console"/>
|
||||
<Reference name="OpenSim.Framework.Servers.HttpServer"/>
|
||||
<Reference name="OpenSim.Services.Interfaces"/>
|
||||
<Reference name="OpenSim.Services.Connectors"/>
|
||||
<Reference name="OpenSim.Services.Base"/>
|
||||
<Reference name="OpenSim.Server.Base"/>
|
||||
<Reference name="OpenSim.Data"/>
|
||||
|
||||
<Reference name="OpenMetaverse"/>
|
||||
<Reference name="OpenMetaverseTypes"/>
|
||||
<Reference name="OpenMetaverse.StructuredData"/>
|
||||
|
||||
<Reference name="WebRtcVoice"/>
|
||||
|
||||
<Reference name="Mono.Addins" path ="../../../../bin"/>
|
||||
|
||||
<Reference name="Nini"/>
|
||||
<Reference name="log4net"/>
|
||||
|
||||
<Files>
|
||||
<Match pattern="*.cs" recurse="true">
|
||||
<Exclude name="Tests" pattern="Tests"/>
|
||||
<Exclude name="Object" pattern="obj"/>
|
||||
</Match>
|
||||
</Files>
|
||||
|
||||
</Project>
|
||||
|
||||
<!-- Test Clients -->
|
||||
<!--
|
||||
<Project name="OpenSim.Tests.Clients.AssetClient" path="OpenSim/Tests/Clients/Assets" type="Exe">
|
||||
|
||||
Reference in New Issue
Block a user