Room
public class Room : IRoom, IDisposable
Main Room
Discussion
The Room
class is the entry point of the low-level API of the ODIN SDK. It provides methods to join and leave a room,
send and receive messages, and manage media streams. The
Room
class is also responsible for managing the connection
state and the room configuration.
If don’t want to develop everything yourself, it is recommended to use OdinRoom instead which is a higher level API and is easier to use.
The basic flow is to use Room.Create to create a Room object, configure it with event handlers, and then join the room. Once joined, you can attach a media stream to the room and start sending and receiving media (i.e. microphone). You need to listen on the Room.OnMediaStarted event to receive media streams from other peers in the room and to create playback components for them.
See the example below for a basic usage of the Room class.
Constructors
Name | Description |
---|---|
Room(OdinConnectionPoolHandle, String, UInt32, Boolean) | Initialise dangling room |
Events
Name | Description |
---|---|
OnDatagram | Call on audio data |
OnRpc | Call on rpc data |
OnSendRpcResponse | Call on response to a rpc request |
OnConnectionStatusChanged | |
OnRoomJoined | |
OnPeerJoined | |
OnPeerLeft | |
OnMediaStarted | |
OnMediaStopped | |
OnUserDataChanged | |
OnMessageReceived |
Static Methods
Name | Description |
---|---|
Create | Initialise independent room |
Join | Create and join a Room |
Properties
Name | Description |
---|---|
Samplerate | Room default samplerate |
Stereo | Room default stereo flag |
EndPoint | Room server gateway endpoint |
Id | RoomId |
OwnPeerId | PeerId of self |
Name | Room name |
ConnectionStatus | RoomStatus |
IsConnected | IsJoined |
IsClosed | IsClosed |
PositionX | Inital position of self on join |
PositionY | Inital position of self on join |
PositionZ | Inital position of self on join |
RpcWriter | Msgpack writer for RPC |
RpcAckActive | Toggle message type for Room.SendRpc true is request and false is notification. Currently calls to "UpdatePeer" and "SetPeerPosition" needs to be requests! |
RpcTableThunk | Msgpack results to RPC requests |
RoomUserData | Odin UserData helper for marshal byte arrays on Room level |
RemotePeers | Conatiner of room peers |
Encoders | Container of room input medias |
Decoders | Elements of room output medias |
Token | Room joining token |
Parent | Default value null indicates root or not set |
AvailableEncoderIds | Available media ids that are reserved for the room |
Public Methods
Name | Description |
---|---|
GetBaseRoom | This will always return itself |
Room_OnDatagram | Default impl will push a datagram to all Room of the same mediaId in the current room. |
Room_OnRPC | Default impl will process all rpc packets. |
ProcessRPC | Will process all rpc packets. |
Join | Join a room with token |
GetRoomId | Retrieves the room id |
GetEncoder | Get a encoder from Room by id |
GetOrCreateEncoder | Get a encoder from Room. |
CreateEncoder | Create a new input media that will be added toRoom |
RemoveEncoder | Removes the input media from Room |
GetDecoder | Get a decoder from RemotePeers by id |
GetOrCreateDecoder | Get a decoder from RemotePeers by id. If the decoder is not found create a new one that will be added to the Peer |
CreateDecoder | Create a new output media that will be added to PeerEntity |
RemoveDecoder | Removes a output media from a remote peer. |
StartMedia | Send a "StartMedia" RPC to the server to start the encoder for input. |
StopMedia | Send a "StopMedia" to the server to stop the encoder. |
PauseMedia | Send a "PauseMedia" to the server to stop the decoder for output. |
ResumeMedia | Send a "ResumeMedia" to the server to start a stopped decoder for output. |
UpdateUserData | Update arbitrary userdata of self (note: Odin) |
SetPosition | Set the spatial position for server side culling. Other remote peers outside the boundary will appear as not in the room or leaving the room. |
SendMessage | Send "SendMessage" to the server to broadcast the message with default UTF8 encoding. |
SendRpc | Send registered RPCs to the server. (set by Room) |
SendAudio | Push the samples to all Room for pipeline processing and pop the result as datagrams to the server |
SendEncoderAudio | Pop all samples from the input media by id and send them to the server |
Close | Close the native room. (native dispose) |
Dispose | On dispose will free the room and all associated data |
Example
using OdinNative.Odin;
using OdinNative.Odin.Media;
using OdinNative.Odin.Peer;
using OdinNative.Odin.Room;
using OdinNative.Unity;
using OdinNative.Unity.Audio;
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using System.Text;
using UnityEngine;
/// <summary>
/// This sample is to showcase a basic version of the wrapper
/// </summary>
/// <remarks>For a GameObject/Component sample checkout the 2D or 3D versions</remarks>
public class SampleManagerBasic : MonoBehaviour
{
public Room _Room;
public string Gateway;
public string AccessKey;
public string RoomName;
public string UserId;
public MicrophoneReader _AudioInput;
public static readonly ConcurrentQueue<Action> UnityQueue = new ConcurrentQueue<Action>();
void Reset()
{
Gateway = "https://gateway.odin.4players.de";
RoomName = "Test";
UserId = "DummyUsername";
}
// Start is called before the first frame update
void Start()
{
// For persistent room with scene switches
//DontDestroyOnLoad(this.gameObject);
if (_Room == null)
{
_Room = Room.Create(Gateway, 48000, false);
// Do NOT use in any circumstances Unity functions in base event callbacks use a dispatch instead
// or use MonoBehaviour versions like OdinRoom that passthrough invokes to UnityEvents
// but this sample is to showcase a basic version; For a GameObject/Component sample checkout InstanceSample2D
_Room.OnRoomJoined += Example_OnRoomJoined;
_Room.OnMediaStarted += Example_OnMediaStarted;
_Room.OnMediaStopped += Example_OnMediaStopped;
_Room.OnMessageReceived += Example_OnMessageLog;
// most likely use some kind of remote token request like UnityWebRequest
// Create token local for showcase purpose only => ACCESSKEY SHOULD NOT BE CLIENT SIDE!
DateTime utc = DateTime.UtcNow;
_Room.Join(ExampleKey(new ExampleTokenBody()
{
rid = RoomName,
uid = UserId,
nbf = ((DateTimeOffset)utc).ToUnixTimeSeconds(),
exp = ((DateTimeOffset)utc.AddMinutes(5)).ToUnixTimeSeconds() // 5min valid
}.ToString(), AccessKey));
}
}
private void Example_OnRoomJoined(object sender, ulong ownPeerId, string name, byte[] roomUserData, ushort[] mediaIds, ReadOnlyCollection<PeerRpc> peers)
{
Debug.Log($"Joined room \"{name}\" with {peers.Count} peers");
if (_Room == null)
{
Debug.LogError($"Can not create encoder without a room");
return;
}
// each room has a limited number of ids reserved for encoders i.e audio input / capture
if (_Room.AvailableEncoderIds.TryDequeue(out ushort mediaId))
CreateCapture(mediaId);
else
Debug.LogError($"Can not create encoder without a encoder id that is free to use");
// add already existing medias
foreach (var peer in _Room.RemotePeers.Values)
foreach(var decoder in peer.Medias.Values)
CreatePlayback(decoder, peer);
}
private void Example_OnMediaStarted(object sender, ulong peerId, MediaRpc media)
{
Debug.Log($"Peer {peerId} started media {media.Id}");
if (_Room == null)
{
Debug.LogError($"Can not add media effects to pipeline without a room in {nameof(Example_OnMediaStarted)}");
return;
}
// in default setup the room creates decoders intern automatically on the event
// get the decoder corresponding to a peer
if (_Room.RemotePeers.TryGetValue(peerId, out PeerEntity peer))
if (peer.Medias.TryGetValue(media.Id, out MediaDecoder decoder))
CreatePlayback(decoder, peer);
}
private void Example_OnMediaStopped(object sender, ulong peerId, ushort mediaId)
{
Debug.Log($"Peer {peerId} removed media {mediaId}");
DispatchDestroyAudioSource($"OdinDecoder {mediaId}");
}
private void Example_OnMessageLog(object sender, ulong peerId, byte[] message)
{
Debug.Log($"Room \"{(sender as Room).Name}\" got message ({message.Length} bytes) from peer {peerId}: \"{Encoding.UTF8.GetString(message)}\"");
}
private void ProxyAudio(float[] buffer, int _, bool isSilent)
{
if (_Room == null) return;
// send audio to all encoders in the current room
// or pop/push audio directly on the encoder/decoder
foreach (var kvp in _Room.Encoders)
_Room.SendAudio(buffer, kvp.Value, isSilent);
}
public void CreateCapture(ushort mediaId)
{
if( _Room == null) return;
// create a encoder where to send audio data to
if (_Room.GetOrCreateEncoder(mediaId, out MediaEncoder encoder))
{
// created encoders have to be started with a customizable rpc call
_Room.StartMedia(encoder);
// set a callback for the MicrophoneReader and add sample effects to the pipeline
LinkEncoderToMicrophone(encoder);
}
}
private void LinkEncoderToMicrophone(MediaEncoder encoder)
{
UnityQueue.Enqueue(() =>
{
// this sample does not set PersistentListener direct or by prefab
if (_AudioInput && _AudioInput.OnAudioData?.GetPersistentEventCount() <= 0 && _Room != null)
_AudioInput.OnAudioData.AddListener(ProxyAudio);
MicrophoneReader microphone = GetComponent<MicrophoneReader>();
if (microphone != null)
{
// optionally add effects to the encoder (Input/Capture)
// add voice activity detection
OdinVadComponent vadComponent = microphone.gameObject.AddComponent<OdinVadComponent>();
vadComponent.Media = encoder;
// add a microphone boost
OdinVolumeBoostComponent volumeBoostComponent = microphone.gameObject.AddComponent<OdinVolumeBoostComponent>();
volumeBoostComponent.Media = encoder;
}
});
}
/// <summary>
/// Add show how
/// </summary>
public void CreatePlayback(MediaDecoder decoder, PeerEntity peer)
{
// EXAMPLE optionally add INTERNAL effects to the decoder (Output/Playback)
MediaPipeline pipeline = decoder.GetPipeline();
if (pipeline.AddVadEffect(out _))
Debug.Log($"added {nameof(VadEffect)} to \"OdinDecoder {decoder.Id}\" of peer {peer.Id}");
// Odin uses Unity to play the audio
DispatchCreateAudioSource(decoder, peer);
}
/// <summary>
/// Add OdinMedia that handles <see cref="AudioSource"/> and copy data from Odin to <see cref="AudioClip"/>
/// </summary>
/// <remarks>optionally <see cref="MediaDecoder.Pop"/> samples can be used with <see cref="AudioClip.SetData"/></remarks>
private void DispatchCreateAudioSource(MediaDecoder decoder, PeerEntity peer)
{
UnityQueue.Enqueue(() =>
{
GameObject container = new GameObject($"OdinDecoder {decoder.Id}");
container.transform.parent = transform;
OdinMedia mediaComponent = container.AddComponent<OdinMedia>();
mediaComponent.MediaDecoder = decoder; // set the decoder to copy data from
mediaComponent.Parent = peer; // the use of OdinMedia requires a parent else it is optional
mediaComponent.enabled = true;
// optionally add interal effects wrapped with Unity to the decoder (Output/Playback)
// for audio pipeline manipulation
// add a playback volume boost
OdinVolumeBoostComponent volumeBoostComponent = container.AddComponent<OdinVolumeBoostComponent>();
volumeBoostComponent.Media = mediaComponent;
// add a playback mute
OdinMuteAudioComponent muteComponent = container.AddComponent<OdinMuteAudioComponent>();
muteComponent.Media = mediaComponent;
// see other Effects or build one
// with CustomEffect (PipelineEffect)
// or with Unity helper class OdinCustomEffectUnityComponentBase
});
}
/// <summary>
/// Disposing objects is necessary to prevent memory leaks
/// </summary>
/// <remarks>To cleaup Unity versions of Odin components just destroy on gameObject to automatically dispose them <see cref="OnDestroy"/></remarks>
private void DispatchDestroyAudioSource(string gameObjectName)
{
UnityQueue.Enqueue(() =>
{
OdinMedia mediaComponent = this.gameObject
.GetComponentsInChildren<OdinMedia>()
.FirstOrDefault(component => component.name == gameObjectName);
if (mediaComponent != null)
Destroy(mediaComponent.gameObject);
});
}
private void Update()
{
if (UnityQueue.IsEmpty == false)
while (UnityQueue.TryDequeue(out var action))
action?.Invoke();
}
private void OnDestroy()
{
if( _Room != null )
{
_Room.Dispose();
_Room = null;
}
}
#region ExampleToken
[Serializable]
class ExampleTokenBody
{
public string rid;
public string uid;
public long nbf;
public long exp;
public override string ToString() => JsonUtility.ToJson(this);
}
private string ExampleKey(string body, string accesskey = "")
{
Debug.LogAssertion("The access key should never be used client side and is for showcase purpose only!");
if (string.IsNullOrEmpty(accesskey))
{
string currentKey = OdinClient.CreateAccessKey();
Debug.LogWarning($"Generated example key: \"{currentKey}\"");
return OdinClient.CreateToken(currentKey, body);
}
else
{
Debug.LogWarning($"Using example key: \"{accesskey}\"");
return OdinClient.CreateToken(accesskey, body);
}
}
#endregion ExampleToken
}