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 🇩🇪
Introducing ODIN

Trusted by developers and studios all around the world.

Aerosoft
ESL FACEIT Group
BMW Group
Ghosts of Tabor
HTX Labs
Madfinger Games

Real-Time Voice and Server Solutions for Gaming

ODIN Voice Chat

Trusted Communication Made Simple

ODIN Voice Chat

ODIN Fleet

Server Management at Scale

ODIN Fleet

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

Supported Platforms

Different platforms, same code, equal performance. We support all major platforms, including iOS, Android, Windows, Mac, Linux, and more.

Windows Apple Android PlayStation Nintendo Switch XBox Linux Unity Unreal Engine