add Robert Adams os-webrtc-janus experimental module. License changed to same BSD as rest of OpenSimulator by Robert, for OpenSimulator use
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 WebRtcVoice
|
||||||
|
{
|
||||||
|
|
||||||
|
// 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 WebRtcVoice
|
||||||
|
{
|
||||||
|
// 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());
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
638
OpenSim/Addons/os-webrtc-janus/Janus/JanusMessages.cs
Normal file
638
OpenSim/Addons/os-webrtc-janus/Janus/JanusMessages.cs
Normal file
@@ -0,0 +1,638 @@
|
|||||||
|
/*
|
||||||
|
* 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 WebRtcVoice
|
||||||
|
{
|
||||||
|
|
||||||
|
/// <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.ContainsKey("transaction") ? m_message["transaction"] : null; }
|
||||||
|
set { m_message["transaction"] = value; }
|
||||||
|
}
|
||||||
|
public string Sender {
|
||||||
|
get { return m_message.ContainsKey("sender") ? m_message["sender"] : null; }
|
||||||
|
set { m_message["sender"] = value; }
|
||||||
|
}
|
||||||
|
public OSDMap Jsep {
|
||||||
|
get { return m_message.ContainsKey("jsep") ? (m_message["jsep"] as OSDMap) : null; }
|
||||||
|
set { m_message["jsep"] = value; }
|
||||||
|
}
|
||||||
|
public void SetJsep(string pOffer, string pSdp)
|
||||||
|
{
|
||||||
|
m_message["jsep"] = new OSDMap()
|
||||||
|
{
|
||||||
|
{ "type", pOffer },
|
||||||
|
{ "sdp", pSdp }
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public void AddAPIToken(string pToken)
|
||||||
|
{
|
||||||
|
m_message["apisecret"] = pToken;
|
||||||
|
}
|
||||||
|
// Note that the session_id is a long number in the JSON so we convert the string.
|
||||||
|
public string sessionId {
|
||||||
|
get { return m_message.ContainsKey("session_id") ? OSDToLong(m_message["session_id"]).ToString() : String.Empty; }
|
||||||
|
set { m_message["session_id"] = long.Parse(value); }
|
||||||
|
}
|
||||||
|
public bool hasSessionId { get { return m_message.ContainsKey("session_id"); } }
|
||||||
|
public void AddSessionId(string pToken)
|
||||||
|
{
|
||||||
|
AddSessionId(long.Parse(pToken));
|
||||||
|
}
|
||||||
|
public void AddSessionId(long pToken)
|
||||||
|
{
|
||||||
|
m_message["session_id"] = pToken;
|
||||||
|
}
|
||||||
|
public bool hasHandleId { get { return m_message.ContainsKey("handle_id"); } }
|
||||||
|
public void AddHandleId(string pToken)
|
||||||
|
{
|
||||||
|
m_message["handle_id"] = long.Parse(pToken);
|
||||||
|
}
|
||||||
|
public string sender {
|
||||||
|
get { return m_message.ContainsKey("sender") ? m_message["sender"] : String.Empty; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public virtual string ToJson()
|
||||||
|
{
|
||||||
|
return m_message.ToString();
|
||||||
|
}
|
||||||
|
public override string ToString()
|
||||||
|
{
|
||||||
|
return m_message.ToString();
|
||||||
|
}
|
||||||
|
// Utility function to convert an OSD object to an long. The OSD object can be an OSDInteger
|
||||||
|
// or an OSDArray of 4 or 8 integers.
|
||||||
|
// This exists because the JSON to OSD parser can return an OSDArray for a long number
|
||||||
|
// since there is not an OSDLong type.
|
||||||
|
// The design of the OSD conversion functions kinda needs one to know how the number
|
||||||
|
// is stored in order to extract it. Like, if it's stored as a long value (8 bytes)
|
||||||
|
// and one fetches it with .AsInteger(), it will return the first 4 bytes as an integer
|
||||||
|
// and not the long value. So this function looks at the type of the OSD object and
|
||||||
|
// extracts the number appropriately.
|
||||||
|
public long OSDToLong(OSD pIn)
|
||||||
|
{
|
||||||
|
long ret = 0;
|
||||||
|
switch (pIn.Type)
|
||||||
|
{
|
||||||
|
case OSDType.Integer:
|
||||||
|
ret = (long)(pIn as OSDInteger).AsInteger();
|
||||||
|
break;
|
||||||
|
case OSDType.Binary:
|
||||||
|
byte[] value = (pIn as OSDBinary).value;
|
||||||
|
if (value.Length == 4)
|
||||||
|
{
|
||||||
|
ret = (long)(pIn as OSDBinary).AsInteger();
|
||||||
|
}
|
||||||
|
if (value.Length == 8)
|
||||||
|
{
|
||||||
|
ret = (pIn as OSDBinary).AsLong();
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case OSDType.Array:
|
||||||
|
if ((pIn as OSDArray).Count == 4)
|
||||||
|
{
|
||||||
|
ret = (long)pIn.AsInteger();
|
||||||
|
}
|
||||||
|
if ((pIn as OSDArray).Count == 8)
|
||||||
|
{
|
||||||
|
ret = pIn.AsLong();
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// ==============================================================
|
||||||
|
// A Janus request message is a basic Janus message with an API token
|
||||||
|
public class JanusMessageReq : JanusMessage
|
||||||
|
{
|
||||||
|
public JanusMessageReq(string pType) : base(pType)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==============================================================
|
||||||
|
// Janus message response is a basic Janus message with the response data
|
||||||
|
// {
|
||||||
|
// "janus": "success",
|
||||||
|
// "transaction": "baefcec8-70c5-4e79-b2c1-d653b9617dea", // ID of the requesting message
|
||||||
|
// "data": { ... } // the response data
|
||||||
|
// "error": { "code": 123, "reason": "..." } // if there was an error
|
||||||
|
// }
|
||||||
|
// The "janus" return code changes depending on the request. The above is for
|
||||||
|
// a successful response. Could be
|
||||||
|
// "event": for an event message (See JanusEventResp)
|
||||||
|
// "keepalive": for a keepalive event
|
||||||
|
public class JanusMessageResp : JanusMessage
|
||||||
|
{
|
||||||
|
public JanusMessageResp() : base()
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public JanusMessageResp(string pType) : base(pType)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public JanusMessageResp(OSDMap pMap) : base()
|
||||||
|
{
|
||||||
|
m_message = pMap;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static JanusMessageResp FromJson(string pJson)
|
||||||
|
{
|
||||||
|
var newBody = OSDParser.DeserializeJson(pJson) as OSDMap;
|
||||||
|
return new JanusMessageResp(newBody);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return the "data" portion of the response as an OSDMap or null if there is none
|
||||||
|
public OSDMap dataSection { get { return m_message.ContainsKey("data") ? (m_message["data"] as OSDMap) : null; } }
|
||||||
|
|
||||||
|
// Check if a successful response code is in the response
|
||||||
|
public virtual bool isSuccess { get { return CheckReturnCode("success"); } }
|
||||||
|
public virtual bool isEvent { get { return CheckReturnCode("event"); } }
|
||||||
|
public virtual bool isError { get { return CheckReturnCode("error"); } }
|
||||||
|
public virtual bool CheckReturnCode(string pCode)
|
||||||
|
{
|
||||||
|
return ReturnCode == pCode;
|
||||||
|
}
|
||||||
|
public virtual string ReturnCode { get {
|
||||||
|
string ret = String.Empty;
|
||||||
|
if (m_message is not null && m_message.ContainsKey("janus"))
|
||||||
|
{
|
||||||
|
ret = m_message["janus"].AsString();
|
||||||
|
}
|
||||||
|
return ret;
|
||||||
|
} }
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==============================================================
|
||||||
|
// An error response is a Janus response with an error code and reason.
|
||||||
|
// {
|
||||||
|
// "janus": "error",
|
||||||
|
// "transaction": "baefcec8-70c5-4e79-b2c1-d653b9617dea", // ID of the requesting message
|
||||||
|
// "error": { "code": 123, "reason": "..." } // if there was an error
|
||||||
|
// }
|
||||||
|
public class ErrorResp : JanusMessageResp
|
||||||
|
{
|
||||||
|
public ErrorResp() : base("error")
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public ErrorResp(string pType) : base(pType)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public ErrorResp(JanusMessageResp pResp) : base(pResp.RawBody)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public void SetError(int pCode, string pReason)
|
||||||
|
{
|
||||||
|
m_message["error"] = new OSDMap()
|
||||||
|
{
|
||||||
|
{ "code", pCode },
|
||||||
|
{ "reason", pReason }
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Dig through the response to get the error code or 0 if there is none
|
||||||
|
public int errorCode { get {
|
||||||
|
int ret = 0;
|
||||||
|
if (m_message.ContainsKey("error"))
|
||||||
|
{
|
||||||
|
var err = m_message["error"];
|
||||||
|
if (err is OSDMap)
|
||||||
|
ret = (int)OSDToLong((err as OSDMap)["code"]);
|
||||||
|
}
|
||||||
|
return ret;
|
||||||
|
}}
|
||||||
|
|
||||||
|
// Dig through the response to get the error reason or empty string if there is none
|
||||||
|
public string errorReason { get {
|
||||||
|
string ret = String.Empty;
|
||||||
|
if (m_message.ContainsKey("error"))
|
||||||
|
{
|
||||||
|
var err = m_message["error"];
|
||||||
|
if (err is OSDMap)
|
||||||
|
ret = (err as OSDMap)["reason"];
|
||||||
|
}
|
||||||
|
// return ((m_message["error"] as OSDMap)?["reason"]) ?? String.Empty;
|
||||||
|
return ret;
|
||||||
|
}}
|
||||||
|
}
|
||||||
|
// ==============================================================
|
||||||
|
// Create session request and response
|
||||||
|
public class CreateSessionReq : JanusMessageReq
|
||||||
|
{
|
||||||
|
public CreateSessionReq() : base("create")
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
|
public class CreateSessionResp : JanusMessageResp
|
||||||
|
{
|
||||||
|
public CreateSessionResp(JanusMessageResp pResp) : base(pResp.RawBody)
|
||||||
|
{ }
|
||||||
|
public string returnedId { get {
|
||||||
|
// The JSON response gives a long number (not a string)
|
||||||
|
// and the ODMap conversion interprets it as a long (OSDLong).
|
||||||
|
// If one just does a "ToString()" on the OSD object, you
|
||||||
|
// get an interpretation of the binary value.
|
||||||
|
return dataSection.ContainsKey("id") ? OSDToLong(dataSection["id"]).ToString() : String.Empty;
|
||||||
|
}}
|
||||||
|
}
|
||||||
|
// ==============================================================
|
||||||
|
public class DestroySessionReq : JanusMessageReq
|
||||||
|
{
|
||||||
|
public DestroySessionReq() : base("destroy")
|
||||||
|
{
|
||||||
|
// Doesn't include the session ID because it is the URI
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// ==============================================================
|
||||||
|
public class TrickleReq : JanusMessageReq
|
||||||
|
{
|
||||||
|
// An empty trickle request is used to signal the end of the trickle
|
||||||
|
public TrickleReq(JanusViewerSession pVSession) : base("trickle")
|
||||||
|
{
|
||||||
|
m_message["candidate"] = new OSDMap()
|
||||||
|
{
|
||||||
|
{ "completed", true },
|
||||||
|
};
|
||||||
|
|
||||||
|
}
|
||||||
|
public TrickleReq(JanusViewerSession pVSession, OSD pCandidates) : base("trickle")
|
||||||
|
{
|
||||||
|
m_message["viewer_session"] = pVSession.ViewerSessionID;
|
||||||
|
if (pCandidates is OSDArray)
|
||||||
|
m_message["candidates"] = pCandidates;
|
||||||
|
else
|
||||||
|
m_message["candidate"] = pCandidates;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// ==============================================================
|
||||||
|
public class AttachPluginReq : JanusMessageReq
|
||||||
|
{
|
||||||
|
public AttachPluginReq(string pPlugin) : base("attach")
|
||||||
|
{
|
||||||
|
m_message["plugin"] = pPlugin;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
public class AttachPluginResp : JanusMessageResp
|
||||||
|
{
|
||||||
|
public AttachPluginResp(JanusMessageResp pResp) : base(pResp.RawBody)
|
||||||
|
{ }
|
||||||
|
public string pluginId { get {
|
||||||
|
return dataSection.ContainsKey("id") ? OSDToLong(dataSection["id"]).ToString() : String.Empty;
|
||||||
|
}}
|
||||||
|
}
|
||||||
|
// ==============================================================
|
||||||
|
public class DetachPluginReq : JanusMessageReq
|
||||||
|
{
|
||||||
|
public DetachPluginReq() : base("detach")
|
||||||
|
{
|
||||||
|
// Doesn't include the session ID or plugin ID because it is the URI
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// ==============================================================
|
||||||
|
public class HangupReq : JanusMessageReq
|
||||||
|
{
|
||||||
|
public HangupReq() : base("hangup")
|
||||||
|
{
|
||||||
|
// Doesn't include the session ID or plugin ID because it is the URI
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// ==============================================================
|
||||||
|
// Plugin messages are defined here as wrappers around OSDMap.
|
||||||
|
// The ToJson() method is overridden to put the OSDMap into the
|
||||||
|
// message body.
|
||||||
|
// A plugin request is formatted like:
|
||||||
|
// {
|
||||||
|
// "janus": "message",
|
||||||
|
// "transaction": "baefcec8-70c5-4e79-b2c1-d653b9617dea",
|
||||||
|
// "body": {
|
||||||
|
// "request": "create",
|
||||||
|
// "room": 10,
|
||||||
|
// "is_private": false,
|
||||||
|
// }
|
||||||
|
public class PluginMsgReq : JanusMessageReq
|
||||||
|
{
|
||||||
|
private OSDMap m_body = new OSDMap();
|
||||||
|
|
||||||
|
// Note that the passed OSDMap is placed in the "body" section of the message
|
||||||
|
public PluginMsgReq(OSDMap pBody) : base("message")
|
||||||
|
{
|
||||||
|
m_body = pBody;
|
||||||
|
}
|
||||||
|
public void AddStringToBody(string pKey, string pValue)
|
||||||
|
{
|
||||||
|
m_body[pKey] = pValue;
|
||||||
|
}
|
||||||
|
public void AddIntToBody(string pKey, int pValue)
|
||||||
|
{
|
||||||
|
m_body[pKey] = pValue;
|
||||||
|
}
|
||||||
|
public void AddBoolToBody(string pKey, bool pValue)
|
||||||
|
{
|
||||||
|
m_body[pKey] = pValue;
|
||||||
|
}
|
||||||
|
public void AddOSDToBody(string pKey, OSD pValue)
|
||||||
|
{
|
||||||
|
m_body[pKey] = pValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
public override string ToJson()
|
||||||
|
{
|
||||||
|
m_message["body"] = m_body;
|
||||||
|
return base.ToJson();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// A plugin response is formatted like:
|
||||||
|
// {
|
||||||
|
// "janus": "success",
|
||||||
|
// "session_id": 5645225333294848,
|
||||||
|
// "transaction": "baefcec8-70c5-4e79-b2c1-d653b9617dea",
|
||||||
|
// "sender": 6969906757968657,
|
||||||
|
// "plugindata": {
|
||||||
|
// "plugin": "janus.plugin.audiobridge",
|
||||||
|
// "data": {
|
||||||
|
// "audiobridge": "created",
|
||||||
|
// "room": 10,
|
||||||
|
// "permanent": false
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
public class PluginMsgResp : JanusMessageResp
|
||||||
|
{
|
||||||
|
public OSDMap m_pluginData;
|
||||||
|
public OSDMap m_data;
|
||||||
|
public PluginMsgResp(JanusMessageResp pResp) : base(pResp.RawBody)
|
||||||
|
{
|
||||||
|
if (m_message is not null && m_message.ContainsKey("plugindata"))
|
||||||
|
{
|
||||||
|
// Move the plugin data up into the m_data var so it is easier to get to
|
||||||
|
m_pluginData = m_message["plugindata"] as OSDMap;
|
||||||
|
if (m_pluginData is not null && m_pluginData.ContainsKey("data"))
|
||||||
|
{
|
||||||
|
m_data = m_pluginData["data"] as OSDMap;
|
||||||
|
// m_log.DebugFormat("{0} AudioBridgeResp. Found both plugindata and data: data={1}", LogHeader, m_data.ToString());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public OSDMap PluginRespData { get { return m_data; } }
|
||||||
|
|
||||||
|
// Get an integer value for a key in the response data or zero if not there
|
||||||
|
public int PluginRespDataInt(string pKey)
|
||||||
|
{
|
||||||
|
if (m_data is null)
|
||||||
|
return 0;
|
||||||
|
return m_data.ContainsKey(pKey) ? (int)OSDToLong(m_data[pKey]) : 0;
|
||||||
|
}
|
||||||
|
// Get a string value for a key in the response data or empty string if not there
|
||||||
|
public string PluginRespDataString(string pKey)
|
||||||
|
{
|
||||||
|
if (m_data is null)
|
||||||
|
return String.Empty;
|
||||||
|
return m_data.ContainsKey(pKey) ? m_data[pKey].AsString() : String.Empty;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// ==============================================================
|
||||||
|
// Plugin messages for the audio bridge.
|
||||||
|
// Audiobridge responses are formatted like:
|
||||||
|
// {
|
||||||
|
// "janus": "success",
|
||||||
|
// "session_id": 5645225333294848,
|
||||||
|
// "transaction": "baefcec8-70c5-4e79-b2c1-d653b9617dea",
|
||||||
|
// "sender": 6969906757968657,
|
||||||
|
// "plugindata": {
|
||||||
|
// "plugin": "janus.plugin.audiobridge",
|
||||||
|
// "data": {
|
||||||
|
// "audiobridge": "created",
|
||||||
|
// "room": 10,
|
||||||
|
// "permanent": false
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
public class AudioBridgeResp: PluginMsgResp
|
||||||
|
{
|
||||||
|
public AudioBridgeResp(JanusMessageResp pResp) : base(pResp)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
public override bool isSuccess { get { return PluginRespDataString("audiobridge") == "success"; } }
|
||||||
|
// Return the return code if it is in the response or empty string if not
|
||||||
|
public string AudioBridgeReturnCode { get { return PluginRespDataString("audiobridge"); } }
|
||||||
|
// Return the error code if it is in the response or zero if not
|
||||||
|
public int AudioBridgeErrorCode { get { return PluginRespDataInt("error_code"); } }
|
||||||
|
// Return the room ID if it is in the response or zero if not
|
||||||
|
public int RoomId { get { return PluginRespDataInt("room"); } }
|
||||||
|
}
|
||||||
|
// ==============================================================
|
||||||
|
public class AudioBridgeCreateRoomReq : PluginMsgReq
|
||||||
|
{
|
||||||
|
public AudioBridgeCreateRoomReq(int pRoomId) : this(pRoomId, false, null)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
public AudioBridgeCreateRoomReq(int pRoomId, bool pSpatial, string pDesc) : base(new OSDMap() {
|
||||||
|
{ "room", pRoomId },
|
||||||
|
{ "request", "create" },
|
||||||
|
{ "is_private", false },
|
||||||
|
{ "permanent", false },
|
||||||
|
{ "sampling_rate", 48000 },
|
||||||
|
{ "spatial_audio", pSpatial },
|
||||||
|
{ "denoise", false },
|
||||||
|
{ "record", false }
|
||||||
|
})
|
||||||
|
{
|
||||||
|
if (!String.IsNullOrEmpty(pDesc))
|
||||||
|
AddStringToBody("description", pDesc);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// ==============================================================
|
||||||
|
public class AudioBridgeDestroyRoomReq : PluginMsgReq
|
||||||
|
{
|
||||||
|
public AudioBridgeDestroyRoomReq(int pRoomId) : base(new OSDMap() {
|
||||||
|
{ "request", "destroy" },
|
||||||
|
{ "room", pRoomId },
|
||||||
|
{ "permanent", true }
|
||||||
|
})
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// ==============================================================
|
||||||
|
public class AudioBridgeJoinRoomReq : PluginMsgReq
|
||||||
|
{
|
||||||
|
public AudioBridgeJoinRoomReq(int pRoomId, string pAgentName) : base(new OSDMap() {
|
||||||
|
{ "request", "join" },
|
||||||
|
{ "room", pRoomId },
|
||||||
|
{ "display", pAgentName }
|
||||||
|
})
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// A successful response contains the participant ID and the SDP
|
||||||
|
public class AudioBridgeJoinRoomResp : AudioBridgeResp
|
||||||
|
{
|
||||||
|
public AudioBridgeJoinRoomResp(JanusMessageResp pResp) : base(pResp)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
public int ParticipantId { get { return PluginRespDataInt("id"); } }
|
||||||
|
}
|
||||||
|
// ==============================================================
|
||||||
|
public class AudioBridgeConfigRoomReq : PluginMsgReq
|
||||||
|
{
|
||||||
|
// TODO:
|
||||||
|
public AudioBridgeConfigRoomReq(int pRoomId, string pSdp) : base(new OSDMap() {
|
||||||
|
{ "request", "configure" },
|
||||||
|
})
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
|
public class AudioBridgeConfigRoomResp : AudioBridgeResp
|
||||||
|
{
|
||||||
|
// TODO:
|
||||||
|
public AudioBridgeConfigRoomResp(JanusMessageResp pResp) : base(pResp)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// ==============================================================
|
||||||
|
public class AudioBridgeLeaveRoomReq : PluginMsgReq
|
||||||
|
{
|
||||||
|
public AudioBridgeLeaveRoomReq(int pRoomId, int pAttendeeId) : base(new OSDMap() {
|
||||||
|
{ "request", "leave" },
|
||||||
|
{ "room", pRoomId },
|
||||||
|
{ "id", pAttendeeId }
|
||||||
|
})
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// ==============================================================
|
||||||
|
public class AudioBridgeListRoomsReq : PluginMsgReq
|
||||||
|
{
|
||||||
|
public AudioBridgeListRoomsReq() : base(new OSDMap() {
|
||||||
|
{ "request", "list" }
|
||||||
|
})
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// ==============================================================
|
||||||
|
public class AudioBridgeListParticipantsReq : PluginMsgReq
|
||||||
|
{
|
||||||
|
public AudioBridgeListParticipantsReq(int pRoom) : base(new OSDMap() {
|
||||||
|
{ "request", "listparticipants" },
|
||||||
|
{ "room", pRoom }
|
||||||
|
})
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// ==============================================================
|
||||||
|
public class AudioBridgeEvent : AudioBridgeResp
|
||||||
|
{
|
||||||
|
public AudioBridgeEvent(JanusMessageResp pResp) : base(pResp)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// ==============================================================
|
||||||
|
// The LongPoll request returns events from the plugins. These are formatted
|
||||||
|
// like the other responses but are not responses to requests.
|
||||||
|
// They are formatted like:
|
||||||
|
// {
|
||||||
|
// "janus": "event",
|
||||||
|
// "sender": 6969906757968657,
|
||||||
|
// "transaction": "baefcec8-70c5-4e79-b2c1-d653b9617dea",
|
||||||
|
// "plugindata": {
|
||||||
|
// "plugin": "janus.plugin.audiobridge",
|
||||||
|
// "data": {
|
||||||
|
// "audiobridge": "event",
|
||||||
|
// "room": 10,
|
||||||
|
// "participants": 1,
|
||||||
|
// "participants": [
|
||||||
|
// {
|
||||||
|
// "id": 1234,
|
||||||
|
// "display": "John Doe",
|
||||||
|
// "audio_level": 0.0,
|
||||||
|
// "video_room": false,
|
||||||
|
// "video_muted": false,
|
||||||
|
// "audio_muted": false,
|
||||||
|
// "feed": 1234
|
||||||
|
// }
|
||||||
|
// ]
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
public class EventResp : JanusMessageResp
|
||||||
|
{
|
||||||
|
public EventResp() : base()
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public EventResp(string pType) : base(pType)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public EventResp(JanusMessageResp pResp) : base(pResp.RawBody)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// ==============================================================
|
||||||
|
}
|
||||||
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 WebRtcVoice
|
||||||
|
{
|
||||||
|
// 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 WebRtcVoice
|
||||||
|
{
|
||||||
|
// 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 WebRtcVoice
|
||||||
|
{
|
||||||
|
// 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 WebRtcVoice
|
||||||
|
{
|
||||||
|
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 WebRtcVoice
|
||||||
|
{
|
||||||
|
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 WebRtcVoice
|
||||||
|
{
|
||||||
|
/// <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 WebRtcVoice
|
||||||
|
{
|
||||||
|
/// <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 WebRtcVoice
|
||||||
|
{
|
||||||
|
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,172 @@
|
|||||||
|
/*
|
||||||
|
* 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.Net;
|
||||||
|
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 WebRtcVoice
|
||||||
|
{
|
||||||
|
// 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,218 @@
|
|||||||
|
/*
|
||||||
|
* 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 WebRtcVoice
|
||||||
|
{
|
||||||
|
// 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,597 @@
|
|||||||
|
/*
|
||||||
|
* 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 WebRtcVoice
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// This module provides the WebRTC voice interface for viewer clients..
|
||||||
|
///
|
||||||
|
/// In particular, it provides the following capabilities:
|
||||||
|
/// ProvisionVoiceAccountRequest, VoiceSignalingRequest, and ParcelVoiceInfoRequest.
|
||||||
|
/// 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 bool _MessageDetails = false;
|
||||||
|
|
||||||
|
// Control info
|
||||||
|
private static bool m_Enabled = false;
|
||||||
|
|
||||||
|
private readonly Dictionary<string, string> m_UUIDName = new Dictionary<string, string>();
|
||||||
|
private Dictionary<string, string> m_ParcelAddress = new Dictionary<string, string>();
|
||||||
|
|
||||||
|
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)
|
||||||
|
{
|
||||||
|
if (m_Enabled)
|
||||||
|
{
|
||||||
|
// Get the hook that means Capbibilities are being registered
|
||||||
|
scene.EventManager.OnRegisterCaps += (UUID agentID, Caps caps) =>
|
||||||
|
{
|
||||||
|
OnRegisterCaps(scene, agentID, caps);
|
||||||
|
};
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ISharedRegionModule.RemoveRegion
|
||||||
|
public void RemoveRegion(Scene scene)
|
||||||
|
{
|
||||||
|
var sfm = scene.RequestModuleInterface<ISimulatorFeaturesModule>();
|
||||||
|
sfm.OnSimulatorFeaturesRequest -= OnSimulatorFeatureRequestHandler;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ISharedRegionModule.RegionLoaded
|
||||||
|
public void RegionLoaded(Scene scene)
|
||||||
|
{
|
||||||
|
if (m_Enabled)
|
||||||
|
{
|
||||||
|
// Register for the region feature reporting so we can add 'webrtc'
|
||||||
|
var sfm = scene.RequestModuleInterface<ISimulatorFeaturesModule>();
|
||||||
|
sfm.OnSimulatorFeaturesRequest += OnSimulatorFeatureRequestHandler;
|
||||||
|
m_log.DebugFormat("{0}: registering OnSimulatorFeatureRequestHandler", logHeader);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ISharedRegionModule.Close
|
||||||
|
public void Close()
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
// ISharedRegionModule.Name
|
||||||
|
public string Name
|
||||||
|
{
|
||||||
|
get { return "RegionVoiceModule"; }
|
||||||
|
}
|
||||||
|
|
||||||
|
// ISharedRegionModule.ReplaceableInterface
|
||||||
|
public Type ReplaceableInterface
|
||||||
|
{
|
||||||
|
get { return null; }
|
||||||
|
}
|
||||||
|
|
||||||
|
// Called when the simulator features are being constructed.
|
||||||
|
// Add the flag that says we support WebRtc voice.
|
||||||
|
private void OnSimulatorFeatureRequestHandler(UUID agentID, ref OSDMap features)
|
||||||
|
{
|
||||||
|
m_log.DebugFormat("{0}: setting VoiceServerType=webrtc for agent {1}", logHeader, agentID);
|
||||||
|
features["VoiceServerType"] = "webrtc";
|
||||||
|
}
|
||||||
|
|
||||||
|
// <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 ParcelVoiceInfoRequest.
|
||||||
|
//
|
||||||
|
// ProvisionVoiceAccountRequest allows the client to obtain
|
||||||
|
// voice communication information the the avater.
|
||||||
|
//
|
||||||
|
// VoiceSignalingRequest: Used for trickling ICE candidates.
|
||||||
|
//
|
||||||
|
// ParcelVoiceInfoRequest is invoked whenever the client
|
||||||
|
// changes from one region or parcel to another.
|
||||||
|
//
|
||||||
|
// 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.DebugFormat(
|
||||||
|
"{0}: OnRegisterCaps() called with agentID {1} caps {2} in scene {3}",
|
||||||
|
logHeader, agentID, caps, scene.RegionInfo.RegionName);
|
||||||
|
|
||||||
|
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("ParcelVoiceInfoRequest",
|
||||||
|
new SimpleStreamHandler("/" + UUID.Random(), (IOSHttpRequest httpRequest, IOSHttpResponse httpResponse) =>
|
||||||
|
{
|
||||||
|
ParcelVoiceInfoRequest(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)
|
||||||
|
{
|
||||||
|
if (request.HttpMethod != "POST")
|
||||||
|
{
|
||||||
|
m_log.DebugFormat("[{0}][ProvisionVoice]: Not a POST request. Agent={1}", logHeader, agentID.ToString());
|
||||||
|
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.ErrorFormat("{0}[ProvisionVoice]: No request data found. Agent={1}", logHeader, agentID.ToString());
|
||||||
|
response.StatusCode = (int)HttpStatusCode.NoContent;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the voice service. If it doesn't exist, return an error.
|
||||||
|
IWebRtcVoiceService voiceService = scene.RequestModuleInterface<IWebRtcVoiceService>();
|
||||||
|
if (voiceService is null)
|
||||||
|
{
|
||||||
|
m_log.ErrorFormat("{0}[ProvisionVoice]: avatar \"{1}\": no voice service", logHeader, agentID);
|
||||||
|
response.StatusCode = (int)HttpStatusCode.NotFound;
|
||||||
|
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.WarnFormat("{0}[ProvisionVoice]: voice_server_type is not 'webrtc'. Request: {1}", logHeader, map.ToString());
|
||||||
|
response.RawBuffer = Util.UTF8.GetBytes("<llsd><undef /></llsd>");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// The checks passed. Send the request to the voice service.
|
||||||
|
OSDMap resp = voiceService.ProvisionVoiceAccountRequest(map, agentID, scene.RegionInfo.RegionID).Result;
|
||||||
|
|
||||||
|
if (_MessageDetails) m_log.DebugFormat("{0}[ProvisionVoice]: response: {1}", logHeader, resp.ToString());
|
||||||
|
|
||||||
|
// TODO: check for errors and package the response
|
||||||
|
|
||||||
|
// Convert the OSD to LLSDXml for the response
|
||||||
|
string xmlResp = OSDParser.SerializeLLSDXmlString(resp);
|
||||||
|
|
||||||
|
response.StatusCode = (int)HttpStatusCode.OK;
|
||||||
|
response.RawBuffer = Util.UTF8.GetBytes(xmlResp);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void VoiceSignalingRequest(IOSHttpRequest request, IOSHttpResponse response, UUID agentID, Scene scene)
|
||||||
|
{
|
||||||
|
if (request.HttpMethod != "POST")
|
||||||
|
{
|
||||||
|
m_log.ErrorFormat("[{0}][VoiceSignaling]: Not a POST request. Agent={1}", logHeader, agentID.ToString());
|
||||||
|
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("{0}[VoiceSignalingRequest]: No request data found. Agent={1}", logHeader, agentID.ToString());
|
||||||
|
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 = Util.UTF8.GetBytes("<llsd><undef /></llsd>");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
IWebRtcVoiceService voiceService = scene.RequestModuleInterface<IWebRtcVoiceService>();
|
||||||
|
if (voiceService is null)
|
||||||
|
{
|
||||||
|
m_log.ErrorFormat("{0}[VoiceSignalingRequest]: avatar \"{1}\": no voice service", logHeader, agentID);
|
||||||
|
response.StatusCode = (int)HttpStatusCode.NotFound;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
OSDMap resp = voiceService.VoiceSignalingRequest(map, agentID, scene.RegionInfo.RegionID).Result;
|
||||||
|
if (_MessageDetails) m_log.DebugFormat("{0}[VoiceSignalingRequest]: Response: {1}", logHeader, resp);
|
||||||
|
|
||||||
|
// TODO: check for errors and package the response
|
||||||
|
|
||||||
|
response.StatusCode = (int)HttpStatusCode.OK;
|
||||||
|
response.RawBuffer = Util.UTF8.GetBytes("<llsd><undef /></llsd>");
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// NOTE NOTE!! This is code from the FreeSwitch module. It is not clear if this is correct for WebRtc.
|
||||||
|
/// <summary>
|
||||||
|
/// Callback for a client request for ParcelVoiceInfo
|
||||||
|
/// </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 ParcelVoiceInfoRequest(IOSHttpRequest request, IOSHttpResponse response, UUID agentID, Scene scene)
|
||||||
|
{
|
||||||
|
if (request.HttpMethod != "POST")
|
||||||
|
{
|
||||||
|
response.StatusCode = (int)HttpStatusCode.NotFound;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
response.StatusCode = (int)HttpStatusCode.OK;
|
||||||
|
|
||||||
|
m_log.DebugFormat(
|
||||||
|
"{0}[PARCELVOICE]: ParcelVoiceInfoRequest() on {1} for {2}",
|
||||||
|
logHeader, scene.RegionInfo.RegionName, agentID);
|
||||||
|
|
||||||
|
ScenePresence avatar = scene.GetScenePresence(agentID);
|
||||||
|
if (avatar == null)
|
||||||
|
{
|
||||||
|
response.RawBuffer = Util.UTF8.GetBytes("<llsd>undef</llsd>");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
string avatarName = avatar.Name;
|
||||||
|
|
||||||
|
// - check whether we have a region channel in our cache
|
||||||
|
// - if not:
|
||||||
|
// create it and cache it
|
||||||
|
// - send it to the client
|
||||||
|
// - send channel_uri: as "sip:regionID@m_sipDomain"
|
||||||
|
try
|
||||||
|
{
|
||||||
|
string channelUri;
|
||||||
|
|
||||||
|
if (null == scene.LandChannel)
|
||||||
|
{
|
||||||
|
m_log.ErrorFormat("region \"{0}\": avatar \"{1}\": land data not yet available",
|
||||||
|
scene.RegionInfo.RegionName, avatarName);
|
||||||
|
response.RawBuffer = Util.UTF8.GetBytes("<llsd>undef</llsd>");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// get channel_uri: check first whether estate
|
||||||
|
// settings allow voice, then whether parcel allows
|
||||||
|
// voice, if all do retrieve or obtain the parcel
|
||||||
|
// voice channel
|
||||||
|
LandData land = scene.GetLandData(avatar.AbsolutePosition);
|
||||||
|
|
||||||
|
// TODO: EstateSettings don't seem to get propagated...
|
||||||
|
if (!scene.RegionInfo.EstateSettings.AllowVoice)
|
||||||
|
{
|
||||||
|
m_log.DebugFormat("{0}[PARCELVOICE]: region \"{1}\": voice not enabled in estate settings",
|
||||||
|
logHeader, scene.RegionInfo.RegionName);
|
||||||
|
channelUri = String.Empty;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
|
||||||
|
if (!scene.RegionInfo.EstateSettings.TaxFree && (land.Flags & (uint)ParcelFlags.AllowVoiceChat) == 0)
|
||||||
|
{
|
||||||
|
channelUri = String.Empty;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
channelUri = ChannelUri(scene, land);
|
||||||
|
}
|
||||||
|
|
||||||
|
// fast foward encode
|
||||||
|
osUTF8 lsl = LLSDxmlEncode2.Start(512);
|
||||||
|
LLSDxmlEncode2.AddMap(lsl);
|
||||||
|
LLSDxmlEncode2.AddElem("parcel_local_id", land.LocalID, lsl);
|
||||||
|
LLSDxmlEncode2.AddElem("region_name", scene.Name, lsl);
|
||||||
|
LLSDxmlEncode2.AddMap("voice_credentials", lsl);
|
||||||
|
LLSDxmlEncode2.AddElem("channel_uri", channelUri, lsl);
|
||||||
|
//LLSDxmlEncode2.AddElem("channel_credentials", channel_credentials, lsl);
|
||||||
|
LLSDxmlEncode2.AddEndMap(lsl);
|
||||||
|
LLSDxmlEncode2.AddEndMap(lsl);
|
||||||
|
|
||||||
|
response.RawBuffer = LLSDxmlEncode2.EndToBytes(lsl);
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
m_log.ErrorFormat("{0}[PARCELVOICE]: region \"{1}\": avatar \"{2}\": {3}, retry later",
|
||||||
|
logHeader, scene.RegionInfo.RegionName, avatarName, e.Message);
|
||||||
|
m_log.DebugFormat("{0}[PARCELVOICE]: region \"{1}\": avatar \"{2}\": {3} failed",
|
||||||
|
logHeader, scene.RegionInfo.RegionName, avatarName, e.ToString());
|
||||||
|
|
||||||
|
response.RawBuffer = Util.UTF8.GetBytes("<llsd>undef</llsd>");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// NOTE NOTE!! This is code from the FreeSwitch module. It is not clear if this is correct for WebRtc.
|
||||||
|
// Not sure what this Uri is for. Is this FreeSwitch specific?
|
||||||
|
// TODO: is this useful for WebRtc?
|
||||||
|
private string ChannelUri(Scene scene, LandData land)
|
||||||
|
{
|
||||||
|
string channelUri = null;
|
||||||
|
|
||||||
|
string landUUID;
|
||||||
|
string landName;
|
||||||
|
|
||||||
|
// Create parcel voice channel. If no parcel exists, then the voice channel ID is the same
|
||||||
|
// as the directory ID. Otherwise, it reflects the parcel's ID.
|
||||||
|
|
||||||
|
lock (m_ParcelAddress)
|
||||||
|
{
|
||||||
|
if (m_ParcelAddress.ContainsKey(land.GlobalID.ToString()))
|
||||||
|
{
|
||||||
|
m_log.DebugFormat("{0}: parcel id {1}: using sip address {2}",
|
||||||
|
logHeader, land.GlobalID, m_ParcelAddress[land.GlobalID.ToString()]);
|
||||||
|
return m_ParcelAddress[land.GlobalID.ToString()];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (land.LocalID != 1 && (land.Flags & (uint)ParcelFlags.UseEstateVoiceChan) == 0)
|
||||||
|
{
|
||||||
|
landName = String.Format("{0}:{1}", scene.RegionInfo.RegionName, land.Name);
|
||||||
|
landUUID = land.GlobalID.ToString();
|
||||||
|
m_log.DebugFormat("{0}: Region:Parcel \"{1}\": parcel id {2}: using channel name {3}",
|
||||||
|
logHeader, landName, land.LocalID, landUUID);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
landName = String.Format("{0}:{1}", scene.RegionInfo.RegionName, scene.RegionInfo.RegionName);
|
||||||
|
landUUID = scene.RegionInfo.RegionID.ToString();
|
||||||
|
m_log.DebugFormat("{0}: Region:Parcel \"{1}\": parcel id {2}: using channel name {3}",
|
||||||
|
logHeader, landName, land.LocalID, landUUID);
|
||||||
|
}
|
||||||
|
|
||||||
|
// slvoice handles the sip address differently if it begins with confctl, hiding it from the user in
|
||||||
|
// the friends list. however it also disables the personal speech indicators as well unless some
|
||||||
|
// siren14-3d codec magic happens. we dont have siren143d so we'll settle for the personal speech indicator.
|
||||||
|
channelUri = String.Format("sip:conf-{0}@{1}",
|
||||||
|
"x" + Convert.ToBase64String(Encoding.ASCII.GetBytes(landUUID)),
|
||||||
|
/*m_freeSwitchRealm*/ "webRTC");
|
||||||
|
|
||||||
|
lock (m_ParcelAddress)
|
||||||
|
{
|
||||||
|
if (!m_ParcelAddress.ContainsKey(land.GlobalID.ToString()))
|
||||||
|
{
|
||||||
|
m_ParcelAddress.Add(land.GlobalID.ToString(), channelUri);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return channelUri;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <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)
|
||||||
|
{
|
||||||
|
OSDMap? map = null;
|
||||||
|
using (Stream inputStream = request.InputStream)
|
||||||
|
{
|
||||||
|
if (inputStream.Length > 0)
|
||||||
|
{
|
||||||
|
OSD tmp = OSDParser.DeserializeLLSDXml(inputStream);
|
||||||
|
if (_MessageDetails) m_log.DebugFormat("{0} BodyToMap: Request: {1}", pCaller, tmp.ToString());
|
||||||
|
map = tmp as OSDMap;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return map;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,303 @@
|
|||||||
|
/*
|
||||||
|
* 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.Linq;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Reflection;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
using OpenSim.Framework;
|
||||||
|
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 WebRtcVoice
|
||||||
|
{
|
||||||
|
/// <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.ErrorFormat("{0} No SpatialVoiceService or NonSpatialVoiceService specified in configuration", LogHeader);
|
||||||
|
m_Enabled = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Default non-spatial to spatial if not specified
|
||||||
|
if (String.IsNullOrEmpty(nonSpatialDllName))
|
||||||
|
{
|
||||||
|
m_log.DebugFormat("{0} nonSpatialDllName not specified. Defaulting to spatialDllName", LogHeader);
|
||||||
|
nonSpatialDllName = spatialDllName;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load the two voice services
|
||||||
|
m_log.DebugFormat("{0} Loading SpatialVoiceService from {1}", LogHeader, spatialDllName);
|
||||||
|
m_spatialVoiceService = ServerUtils.LoadPlugin<IWebRtcVoiceService>(spatialDllName, new object[] { m_Config });
|
||||||
|
if (m_spatialVoiceService is null)
|
||||||
|
{
|
||||||
|
m_log.ErrorFormat("{0} Could not load SpatialVoiceService from {1}", LogHeader, spatialDllName);
|
||||||
|
m_Enabled = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
m_log.DebugFormat("{0} Loading NonSpatialVoiceService from {1}", LogHeader, nonSpatialDllName);
|
||||||
|
if (spatialDllName == nonSpatialDllName)
|
||||||
|
{
|
||||||
|
m_log.DebugFormat("{0} NonSpatialVoiceService is same as SpatialVoiceService", LogHeader);
|
||||||
|
m_nonSpatialVoiceService = m_spatialVoiceService;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
m_nonSpatialVoiceService = ServerUtils.LoadPlugin<IWebRtcVoiceService>(nonSpatialDllName, new object[] { m_Config });
|
||||||
|
if (m_nonSpatialVoiceService is null)
|
||||||
|
{
|
||||||
|
m_log.ErrorFormat("{0} Could not load NonSpatialVoiceService from {1}", LogHeader, nonSpatialDllName);
|
||||||
|
m_Enabled = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (m_Enabled)
|
||||||
|
{
|
||||||
|
m_log.InfoFormat("{0} WebRtcVoiceService enabled", LogHeader);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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.DebugFormat("{0} Adding WebRtcVoiceService to region {1}", LogHeader, 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.ContainsKey("viewer_session"))
|
||||||
|
{
|
||||||
|
// request has a viewer session. Use that to find the voice service
|
||||||
|
string viewerSessionId = pRequest["viewer_session"].AsString();
|
||||||
|
if (!VoiceViewerSession.TryGetViewerSession(viewerSessionId, out vSession))
|
||||||
|
{
|
||||||
|
m_log.ErrorFormat("{0} ProvisionVoiceAccountRequest: viewer session {1} not found", LogHeader, viewerSessionId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// the request does not have a viewer session. See if it's an initial request
|
||||||
|
if (pRequest.ContainsKey("channel_type"))
|
||||||
|
{
|
||||||
|
string channelType = pRequest["channel_type"].AsString();
|
||||||
|
if (channelType == "local")
|
||||||
|
{
|
||||||
|
// TODO: check if this userId is making a new session (case that user is reconnecting)
|
||||||
|
vSession = m_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.ErrorFormat("{0} ProvisionVoiceAccountRequest: no channel_type in request", LogHeader);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
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.ContainsKey("viewer_session"))
|
||||||
|
{
|
||||||
|
// request has a viewer session. Use that to find the voice service
|
||||||
|
string viewerSessionId = pRequest["viewer_session"].AsString();
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
29
bin/config/os-webrtc-janus.ini
Normal file
29
bin/config/os-webrtc-janus.ini
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>
|
||||||
|
|
||||||
|
<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 -->
|
<!-- Test Clients -->
|
||||||
<!--
|
<!--
|
||||||
<Project name="OpenSim.Tests.Clients.AssetClient" path="OpenSim/Tests/Clients/Assets" type="Exe">
|
<Project name="OpenSim.Tests.Clients.AssetClient" path="OpenSim/Tests/Clients/Assets" type="Exe">
|
||||||
|
|||||||
Reference in New Issue
Block a user