Introducing ODIN
Empowering Real-Time Solutions for Gaming and Business
Explore our cutting-edge technologies, delivering high-quality solutions for multiplayer games and business solutions. Elevate your experience with top-tier features and services.
Made with ❤️ by 4Players in Germany 🇩🇪

Try it Out
Sign in to your ODIN account and try out these features. Here are some common use cases to get you started.
update-gameserver-on-commit.sh
#!/bin/bash
# Define the config ID that will be updated
# Use `odin fleet configs list` to find its id
CONFIG_ID=123456
# Create a new image and get its ID
IMAGE_ID=$(odin fleet images create \
--name="Development Server" \
--version="$CI_VERSION_TAG" \
--os="linux" \
--type="steam" \
--steam-app-id=123456 \
--branch="internal" \
--password="idontknow" \
--command="sh Server.sh -server" \
--steamcmd-username="mysteamusername" \
--steamcmd-password="mysteampw" \
--runtime="sniper" \
--unpublished \
--format="value(id)" \
--force)
echo "Created image with ID: $IMAGE_ID"
# Monitor the image status until it's ready
STATUS="processing"
while [[ "$STATUS" != "ready" ]]; do
STATUS=$(odin fleet images get --image-id=$IMAGE_ID --format="value(status)")
echo "Current status of image $IMAGE_ID: $STATUS"
# Check if the status is 'error', in which case we should exit with a failure
if [[ "$STATUS" == "error" ]]; then
echo "Error: Image creation failed. Exiting."
exit 1
fi
# Wait for a few seconds before polling again
sleep 5
done
echo "Image $IMAGE_ID is ready."
# Update the config with the new image ID
odin fleet configs update --config-id=$CONFIG_ID --binary-id=$IMAGE_ID --force
# All running servers using that config will be restarted with the new version
echo "Config $CONFIG_ID has been updated to use image $IMAGE_ID."
realtime-voice-chat.ts
import { generateAccessKey, TokenGenerator } from '@4players/odin-tokens';
import { OdinClient, OdinMedia, OdinPeer, OdinRoom, uint8ArrayToValue, valueToUint8Array } from '@4players/odin';
/**
* Connects to the ODIN server network by authenticating with the specified
* room token, joins the room, configures our own microphone input stream and
* registers a few callbacks to handle the most important server events.
*/
async function connect(token: string) {
try {
// Create an audio context (must happen after user interaction due to
// browser privacy features)
const audioContext = new AudioContext();
// Authenticate and initialize the room
const odinRoom = await OdinClient.initRoom(token, 'gateway.odin.4players.io', audioContext);
// Register room events
handleRoomEvents(odinRoom);
// Join the room and specify initial user data
const ownPeer = await odinRoom.join(valueToUint8Array('{ "name": "John Doe", "userId": "12345" }'));
// Create a new audio stream for the default capture device and
// append it to the room
navigator.mediaDevices
.getUserMedia({
audio: {
echoCancellation: true,
autoGainControl: true,
noiseSuppression: true,
sampleRate: 48000,
},
})
.then((mediaStream) => {
odinRoom.createMedia(mediaStream);
});
} catch (e) {
console.error('Failed to join room', e);
alert(e);
disconnect();
}
}
/**
* Helper function to set event handlers for ODIN room events.
*/
function handleRoomEvents(room: OdinRoom) {
// Handle media started events to update our UI and start the audio decoder
room.addEventListener('MediaStarted', (event) => {
event.payload.media.start();
});
// Handle media stopped events to update our UI and stop the audio decoder
room.addEventListener('MediaStopped', (event) => {
event.payload.media.stop();
});
// Handle media update events to show talking indicator
room.addEventListener('MediaActivity', (event) => {
console.log('Media activity:', event.payload);
});
}
/**
* Leaves the room and closes the connection to the ODIN server network.
*/
function disconnect() {
OdinClient.disconnect();
}
unity-proximity-chat.cs
using OdinNative.Unity;
using UnityEngine;
/// <summary>
/// OdinPeerManager is a Unity component that manages the connection between
/// Odin Voice and the Unity game objects. It is multiplayer framework
/// agnostic and can be used with any multiplayer framework.
/// </summary>
public class OdinPeerManager : MonoBehaviour
{
/// <summary>
/// Attach the Odin playback component to the player object.
/// All audio from the media stream will be played through the
/// player's AudioSource.
/// </summary>
private void AttachOdinPlaybackToPlayer(
PlayerScript player,
Room room,
ulong peerId,
int mediaId
)
{
var playback = OdinHandler.Instance.AddPlaybackComponent(
player.gameObject,
room.Config.Name,
peerId,
mediaId
);
// Set the spatialBlend to 1 for full 3D audio.
playback.PlaybackSource.spatialBlend = 1.0f;
}
/// <summary>
/// Find the player object in the scene that corresponds to the given
/// OdinPeer.
/// </summary>
public PlayerScript GetPlayerForOdinPeer(CustomUserDataJsonFormat userData)
{
if (userData.seed == null) return null;
Debug.Log("Player has network Id: " + userData.seed);
PlayerScript[] players = FindObjectsOfType<PlayerScript>();
foreach (var player in players)
{
if (player.odinSeed == userData.seed)
{
Debug.Log("Found PlayerScript with seed " + userData.seed);
if (!player.isLocalPlayer)
{
return player;
}
}
}
return null;
}
/// <summary>
/// Remove the Odin playback component from the players object
/// </summary>
public void RemoveOdinPlaybackFromPlayer(PlayerScript player)
{
OdinHandler.Instance.DestroyPlaybackComponent(player.gameObject);
}
/// <summary>
/// Callback for when a player removed its media stream from the room.
/// Detach the media stream from the player.
/// </summary>
public void OnMediaRemoved(object sender, MediaRemovedEventArgs eventArgs)
{
Room room = sender as Room;
CustomUserDataJsonFormat userData =
CustomUserDataJsonFormat.FromUserData(eventArgs.Peer.UserData);
PlayerScript player = GetPlayerForOdinPeer(userData);
if (player)
{
RemoveOdinPlaybackFromPlayer(player);
}
}
/// <summary>
/// Callback for when a new media stream is added to the room.
/// Attach the media stream to the correct player game object.
/// </summary>
public void OnMediaAdded(object sender, MediaAddedEventArgs eventArgs)
{
Room room = sender as Room;
CustomUserDataJsonFormat userData =
CustomUserDataJsonFormat.FromUserData(eventArgs.Peer.UserData);
PlayerScript player = GetPlayerForOdinPeer(userData);
if (player)
{
AttachOdinPlaybackToPlayer(
player,
room,
eventArgs.PeerId,
eventArgs.Media.Id
);
}
}
}
transcribe-audio-to-text.ts
class MyBot extends OdinBot {
private isJoined = false;
private openai: OpenAIApi;
private _fileRecorders = new Map<number, wav.FileWriter>();
private _flacEncoders = new Map<number, Encoder>();
constructor(botId: string) {
super(botId);
const configuration = new Configuration({
apiKey: openAIKey,
});
this.openai = new OpenAIApi(configuration);
this.botUserData.name = 'Chat GPT Bot';
}
/**
* Start Capture Audio in this bot
*/
protected override onBeforeJoin() {
this.startCaptureAudio();
}
/**
* Whenever a user starts talking we start recording the audio to a FLAC file.
* Once the user stops talking we stop the recording and transcribe the audio
* using OpenAIs whisper model.
*/
protected override async onMediaActivity(event: OdinMediaActivityEventPayload, user: IUser, active: boolean) {
// User started talking, start recording
if (active) {
if (!this._fileRecorders.has(user.peerId)) {
const timer = new Date().getTime();
const fileName = `./${user.peerId}_${event.mediaId}_${timer}.wav`;
this._fileRecorders.set(user.peerId, {
wavEncoder: new wav.FileWriter(fileName, {
channels: 1,
sampleRate: 48000,
bitDepth: 16,
}),
fileName: fileName,
user: user,
});
this._flacEncoders.set(
user.peerId,
new Encoder(Flac, {
channels: 1,
sampleRate: 48000,
bitsPerSample: 16,
verify: false,
compression: 0,
})
);
} else {
const fileRecorder = this._fileRecorders.get(user.peerId);
if (fileRecorder.timer) {
clearTimeout(fileRecorder.timer);
delete fileRecorder.timer;
}
}
} else {
// User stopped talking, wait 2 seconds and then transcribe the audio
if (this._fileRecorders.has(user.peerId)) {
const fileRecorder = this._fileRecorders.get(user.peerId);
if (!fileRecorder.timer) {
fileRecorder.timer = setTimeout(() => {
fileRecorder.wavEncoder.file.close();
try {
const file = fs.createReadStream(fileRecorder.fileName);
this.openai.createTranscription(file, 'whisper-1').then((response) => {
console.log('User said: ', fileRecorder.user.name, response.data.text);
});
} catch (e) {
console.log('Failed to transcribe: ', e);
}
this._fileRecorders.delete(user.peerId);
// Write the flac file
const encoder = this._flacEncoders.get(user.peerId);
if (encoder) {
encoder.encode();
//get the encoded data:
const encData = encoder.getSamples();
const metadata = encoder.metadata;
const flacFileName = (fileRecorder.fileName = fileRecorder.fileName.replace('.wav', '.flac'));
fs.writeFileSync(flacFileName, encData);
}
}, 2000);
}
}
}
}
/**
* Convert the audio samples from Odin to the FLAC encoder format
*/
private convertAudioSamples(samples32: Uint8Array, bitDepth: number): Int32Array {
const floats = new Float32Array(samples32.buffer);
// Calculate the scale to convert the floats to integers
const scale = Math.pow(2, bitDepth - 1) - 1;
// Convert ODIN Floats to integers required for the FLAC encoder
const intArray = new Int32Array(floats.length);
for (let i = 0; i < floats.length; i++) {
intArray[i] = Math.round(floats[i] * scale);
}
return intArray;
}
/**
* Write data from Odin to the audio file
*/
protected override async onAudioDataReceived(event: OdinAudioDataReceivedEventPayload, user: IUser) {
if (this._fileRecorders.has(user.peerId)) {
const fileRecorder = this._fileRecorders.get(user.peerId);
fileRecorder.wavEncoder.file.write(event.samples16, (error) => {
if (error) {
console.log('Failed to write audio file');
}
});
}
if (this._flacEncoders.has(user.peerId)) {
const encoder = this._flacEncoders.get(user.peerId);
const intArray = this.convertAudioSamples(event.samples32, 16);
encoder.encode(intArray);
}
}
}
const main = async function () {
const bot = new MyBot('bot-ai-chat');
await bot.startWithAccessKey('__ACCESS_KEY__', 'My Room', 'gateway.odin.4players.io', 48000, 1);
process.on('SIGINT', () => {
process.exit();
});
};
main()
.then(() => {
console.log('Bot started');
})
.catch((err) => {
console.error('Could not start bot', err);
});
What Others Say
ODIN was one of the best decisions we made regarding voice as it integrates with Unreal Engine’s built-in spatialization & occlusion systems, compared to competitors, this provided a much more immersive experience for our player-base. Their team was extremely helpful and quick in sorting out any issues we had during our setup process.
We have been the first to go live with ODIN onboard and what can we say? It was worth the risk, the ODIN integration was smooth and the ODIN dev team always supported us in the best manner. We are happy for the mutually beneficial and friend-like partnerships we share with 4Players.
ODIN is by far the best professional approach for a multiplayer cross platform voice chat system for your video game in-between Steam Decks, macOS and Windows.
I’ve been using ODIN since the beta release with a multiplayer VR game and it is working flawlessly! It’s very easy to set up and integrate into the project. You can do all sorts of cool things, it’s basically just an actor sound component that you can use and apply effects to like any other sound in Unreal. This is a 3rd-party service, it will work across your game servers in case you want that.
I have been working with ODIN both in Unreal and Unity for quite some time now and massively appreciate the easy integration and the flexibility it provides. I can adapt ODIN to any use case I can find, it’s massively scalable and it supports all the fancy features like 3D audio, audio occlusion and voice filters. From now on my first choice for any application that requires Voice Chat.
Supported Platforms
Different platforms, same code, equal performance. We support all major platforms, including iOS, Android, Windows, Mac, Linux, and more.