Skip to main content

ODIN Voice Web SDK

ODIN is our cross-platform immersive voice SDK with best-in-class quality and noise suppression technologies. You can integrate it with ease in your TypeScript/JavaScript-based applications.

Source Code

The web SDK is developed in TypeScript and is open source. Check out the source in our public Github repository.

Installation

You can install the ODIN web SDK in your NodeJS project (you may want to build a bot in your NodeJS-based server that responds to messages sent by users). You may also add ODIN to your Angular, React, React Native, Svelte, or any other frontend framework based on NPM.

NPM

npm npm npm

Install the web SDK via npm in your project like this:

npm install --save @4players/odin

HTML

If you want to include ODIN in your website, you can add ODIN with this script tag to your website:


<script type="text/javascript" src="https://4npcdn.com/2355/odin/javascript/latest/odin.min.js"></script>

We also provide versioned URLs like this:


<script type="text/javascript" src="https://4npcdn.com/2355/odin/javascript/0.9.0/odin.min.js"></script>

If you want to host it yourself or make adjustments, you can also build it from source:

  1. Clone this repo on your workstation: https://github.com/4Players/odin-sdk-web
  2. Install modules: npm install
  3. Build vanilla JavaScript script: npm run bundle
  4. Congratulations! You now have a bundled script file named odin.min.js in the dist folder.

You can now use the code as shown in the sample below, with one exception:

In the NPM package, namespaces and modules exist to separate different APIs from each other so they don't share the same space and accidentally overwrite things from other APIs. As some browsers don't support modules, we group ODIN APIs in a global namespace named ODIN. The namespace is defined in the Rollup bundle settings (please refer to rollup.config.ts for details).

Therefore, whenever you see something in the docs that says OdinClient.initRoom you'll need to change that to ODIN.OdinClient.initRoom in vanilla JavaScript.

Interoperability

The ODIN web SDK is fully compatible with our other client SDKs, so you can easily communicate between your favorite web browser and any native app or game which integrates ODIN.

While next-gen browsers and native ODIN SDKs utilize WebTransport over HTTP/3 to talk to an ODIN server using a secure and reliable multiplexed transport, we've also implemented a fallback mechanism over WebRTC data channels for browsers without HTTP/3 support.

Event Handling

The ODIN server will automatically notify you about relevant updates, and the ODIN web SDK uses the JavaScript EventTarget API to dispatch custom events based on the

OdinEventTarget

. Use any of the provided addEventListener methods to set up a function that will be called whenever the specified event is delivered to the target.

Events are available in the following scopes:

This allows fine-grained control over which events you want to receive on any specific class instance, most notably OdinRoom. Please refer to the scopes listed above to find out which events are available.

anyOdinObjectInstance.addEventListener('<EVENT_NAME>', function (event) { /* do something */
});

Using Media objects

ODIN works with the concept of media objects that are attached to a room. A media is either remote (i.e., connected to the mic of someone else’s device) or a local media, which is linked to your own microphone or other input device.

After joining a room, you can create a media (linked to your local input device) and add it to the room. After joining a room, you are just a listener in the room, and you don't send anything to it (i.e., you are muted). If you want to unmute the local user, you need to attach a media to that room:

Creating a media object
// Create a new audio stream for our default capture device and append it to the room
const mediaStream = await navigator.mediaDevices.getUserMedia({
echoCancellation: true,
autoGainControl: true,
noiseSuppression: true,
});

// Create and append a new input media to the room
const inputMedia = await odinRoom.createMedia(mediaStream);

// Start transmitting voice data (this can also be done later in the event listener for 'MediaStarted')
inputMedia.start();

That's it. The navigator object's mediaDevices has many functions to identify the input devices available on the user's device, i.e., you could show users a list of devices with all mics attached to the user's device before attaching that media.

warning

Important: In our Web SDK, media objects are not started immediately. The objects are created, but they don't send any data yet. You need to call the start method on the media object to start sending (and receiving data).

If you also want to enable receiving voice from others, you'll need to start incoming media objects too. For this, add an event handler for the MediaStarted event like this:

Starting incoming media objects
// Listen to media started events in the room and start decoding its voice packets
odinRoom.addEventListener('MediaStarted', (event) => {
console.log(`Peer ${event.payload.peer.id} added a new media stream to the room`);
// Start the media stream to enable the speaker for this media stream.
event.payload.media.start();
});

Disconnect from Odin

To disconnect a player from an Odin room, you can simply call:

room.disconnect(); // room: OdinRoom

This function will stop all media objects from sending and receiving data. However, the browser may continue showing a microphone indicator on the tab, which can mistakenly suggest that the microphone is still being used. To remove this indicator and let the browser know that the microphone is no longer active, implement the clean-up code as shown in the example below:

Clean-up media tracks and hide microphone indicator
function stopMediaStream(ms: MediaStream) {
ms.getTracks().forEach((track) => {
track.stop();
ms.removeTrack(track);
});
}

Execute this clean-up function on the media stream that is capturing audio to ensure that all resources are properly released and the microphone indicator is removed.

Examples

We have prepared a couple of examples for various web frameworks.

Vanilla JS

This example shows how to join a room and how to add a media stream (i.e., microphone) to it. It also shows how to subscribe to important events and how to manage them. This can be useful for updating lists of users in the channel or showing activity indicators.

info

To join an ODIN room, you need to create an access token. This should not be done on the client side as the access key is very powerful and should not be exposed in a public client. We provide an easy-to-use and deploy token server via NodeJS that you can use to create ODIN tokens. You can also use our NPM package @4players/odin-tokens to add a route to your existing NodeJS-based server or cloud functions. Check out the token server here: Token Server NodeJS Example.

The code should be self-explanatory; however, the basic steps are:

  1. Receive an access token for the room default from our example token server.
  2. Join the room with that access token.
  3. Set up listeners to receive notifications if new users join the room or start and stop talking.
Vanilla JS Example
import {generateAccessKey, TokenGenerator} from '@4players/odin-tokens';
import {OdinClient, OdinMedia, OdinPeer, OdinRoom, uint8ArrayToValue, valueToUint8Array} from '@4players/odin';

import './style.css';

/**
* The address of an ODIN gateway or server to connect to.
*
* Please refer to our developer documentation for a list of public gateways available in your region:
* https://www.4players.io/odin/hosting/cloud/#available-gateways
*/
let serverAddress = 'gateway.odin.4players.io';

/**
* The local access key used in this example.
*
* ===== IMPORTANT =====
* Your access key is the unique authentication key to be used to generate room tokens for accessing the ODIN
* server network. Think of it as your individual username and password combination all wrapped up into a single
* non-comprehendible string of characters, and treat it with the same respect. For your own security, we strongly
* recommend that you NEVER put an access key in your client-side code.
*
* Please refer to our developer documentation to learn more about access keys:
* https://www.4players.io/odin/introduction/access-keys/
*/
let accessKey = '';

/**
* The identifier of the room we want to join.
*/
let roomId = 'default';

/**
* The identifier to set for your own peer during authentication.
*/
let userId = '';

/**
* The custom string to set as user data for your own peer (can be changed later).
*/
let userData = '';

/**
* 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, serverAddress, audioContext);

// Register room events
handleRoomEvents(odinRoom);

// Join the room and specify initial user data
const ownPeer = await odinRoom.join(valueToUint8Array(userData));
addOrUpdateUiPeer(ownPeer);

// 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();
resetUi();
}
}

/**
* Helper function to set event handlers for ODIN room events.
*/
function handleRoomEvents(room: OdinRoom) {
room.addEventListener('ConnectionStateChanged', (event) => {
console.log('Client connection status changed', event.payload.newState);

if (event.payload.newState !== 'disconnected') {
accessKeyInput?.setAttribute('disabled', 'disabled');
userIdInput?.setAttribute('disabled', 'disabled');
roomIdInput?.setAttribute('disabled', 'disabled');
generateAccessKeyBtn?.setAttribute('disabled', 'disabled');
if (toggleConnectionBtn) toggleConnectionBtn.innerHTML = 'Leave';
} else {
resetUi();
}

const title = app.querySelector('#room-title');
if (title) {
title.innerHTML =
event.payload.newState === 'connected' ? `Joined '${room.id}' on ${room.serverAddress}` : 'Not Connected';
}
});

// Handle peer join events to update our UI
room.addEventListener('PeerJoined', (event) => {
console.log(`Adding peer ${event.payload.peer.id}`);
addOrUpdateUiPeer(event.payload.peer);
});

// Handle peer left events to update our UI
room.addEventListener('PeerLeft', (event) => {
console.log(`Removing peer ${event.payload.peer.id}`);
removeUiPeer(event.payload.peer);
});

// Handle media started events to update our UI and start the audio decoder
room.addEventListener('MediaStarted', (event) => {
console.log(`Adding new media ${event.payload.media.id} owned by peer ${event.payload.peer.id}`);
event.payload.media.start();
addOrUpdateUiPeer(event.payload.peer);
});

// Handle media stopped events to update our UI and stop the audio decoder
room.addEventListener('MediaStopped', (event) => {
console.log(`Removing new media ${event.payload.media.id} owned by peer ${event.payload.peer.id}`);
event.payload.media.stop();
addOrUpdateUiPeer(event.payload.peer);
});

// Handle peer user data changes to update our UI
room.addEventListener('PeerUserDataChanged', (event) => {
console.log(`Received user data update for peer ${event.payload.peer.id}`);
addOrUpdateUiPeer(event.payload.peer);
});

// Handle media stopped events to update our UI and stop the audio decoder
room.addEventListener('MediaActivity', (event) => {
console.log(`Handle activity update on media ${event.payload.media.id}`, event.payload.media.active);
updateUiMediaActivity(event.payload.media);
});
}

/**
* Leaves the room and closes the connection to the ODIN server network.
*/
function disconnect() {
OdinClient.disconnect();
}

// ======================================================================================================================
// ======================================================================================================================
// ==================================== All the example UI related code starts below ====================================
// ======================================================================================================================
// ======================================================================================================================

/**
* Render some basic HTML.
*/
const app: HTMLDivElement = document.querySelector < HTMLDivElement > ('#app')
!;
app.innerHTML = `
<h1>ODIN TypeScript Example</h1>
<div id="login-form">
<label for="server-address">Server Address</label>
<input id="server-address" type="text" value="${serverAddress}" placeholder="The address of the ODIN server">
<select id="token-audience"><option>gateway</option><option>sfu</option></select>
<label for="access-key">Access Key</label>
<input id="access-key" type="text" value="${accessKey}" placeholder="A local access key for testing">
<button id="generate-access-key">Generate</button>
<label for="user-id">Peer User ID</label>
<input id="user-id" type="text" value="${userId}" placeholder="Optional identifier (e.g. nickname)">
<label for="user-data">Peer User Data</label>
<input id="user-data" type="text" value="${userData}" placeholder="Optional arbitrary data (e.g. JSON)">
<label for="room-id">Room ID</label>
<input id="room-id" type="text" value="${roomId}" placeholder="The name of the room to join">
<button id="toggle-connection">Join</button>
</div>
<fieldset id="room-container">
<legend id="room-title">Not Connected</legend>
</fieldset>
`;

/**
* Grab server address input and register event handlers to handle manual changes.
*/
const serverAddressInput = app.querySelector < HTMLInputElement > ('#server-address');
serverAddressInput?.addEventListener('change', (e: any) => {
if (!e.target) return;
serverAddress = (e.target
as
HTMLInputElement
).
value;
});

/**
* Grab access key input and register event handlers to handle manual changes.
*/
const accessKeyInput = app.querySelector < HTMLInputElement > ('#access-key');
accessKeyInput?.addEventListener('change', (e: any) => {
if (!e.target) return;
accessKey = (e.target
as
HTMLInputElement
).
value;
});

/**
* Grab user ID input and register event handlers to handle manual changes.
*/
const userIdInput = app.querySelector < HTMLInputElement > ('#user-id');
userIdInput?.addEventListener('change', (e: any) => {
if (!e.target) return;
userId = (e.target
as
HTMLInputElement
).
value;
});

/**
* Grab user data input and register event handlers to handle manual changes.
*/
const userDataInput = app.querySelector < HTMLInputElement > ('#user-data');
userDataInput?.addEventListener('change', (e: any) => {
if (!e.target) return;
userData = (e.target
as
HTMLInputElement
).
value;

// if we're connected, also send an update of our own user data to the server
if (OdinClient.connectionState === 'connected') {
const ownPeer = OdinClient.rooms[0].ownPeer;
ownPeer.data = valueToUint8Array(userData);
ownPeer.update(); // flush user data update
console.log('Sent updated peer user data to server', ownPeer.data);
addOrUpdateUiPeer(ownPeer);
}
});

/**
* Grab room ID input and register event handlers to handle manual changes.
*/
const roomIdInput = app.querySelector < HTMLInputElement > ('#room-id');
roomIdInput?.addEventListener('change', (e: any) => {
if (!e.target) return;
roomId = (e.target
as
HTMLInputElement
).
value;
});

/**
* Grab 'Generate' button and register an event handler to update the access key if necessary.
*/
const generateAccessKeyBtn = document.querySelector < HTMLButtonElement > ('#generate-access-key');
generateAccessKeyBtn?.addEventListener('click', () => {
accessKey = generateAccessKey();
if (accessKeyInput) accessKeyInput.value = accessKey;
console.log('Generated a new local access key', accessKey);
});

/**
* Grab 'Join/Leave' button and register and register an event handler to connect/disconnect.
*/
const toggleConnectionBtn = document.querySelector < HTMLButtonElement > ('#toggle-connection');
toggleConnectionBtn?.addEventListener('click', (e: any) => {
if (OdinClient.connectionState === 'disconnected' || OdinClient.connectionState === 'error') {
try {
const tokenGenerator = new TokenGenerator(accessKey);
const audience = (app.querySelector < HTMLSelectElement > ('#token-audience')?.value ?? 'gateway')
as
| 'gateway'
| 'sfu';
const token = tokenGenerator.createToken(roomId, userId, {audience, customer: '<no customer>'});
console.log('Generated a new signed JWT to join room', token);
connect(token);
} catch (e) {
console.error('Failed to generate token', e);
alert(e);
}
} else {
disconnect();
}
});

/**
* Helper function to add a peer node to the UI.
*/
function addOrUpdateUiPeer(peer: OdinPeer) {
let container = app.querySelector('#peer-container');
if (!container) {
container = document.createElement('ul');
container.setAttribute('id', 'peer-container');
app.querySelector('#room-container')?.append(container);
}

const peerItem = app.querySelector(`#peer-${peer.id}`) ?? document.createElement('li');

const decodedData: unknown = uint8ArrayToValue(peer.data);
let userData: string = '';
if (typeof decodedData === 'object') {
userData = JSON.stringify(decodedData);
} else {
userData = String(decodedData);
}

peerItem.setAttribute('id', `peer-${peer.id}`);
peerItem.innerHTML = `Peer(${peer.id}) <div> User ID: ${peer.userId} <br> User Data: ${userData} <div>`;
container.append(peerItem);

const mediaList = document.createElement('ul');
mediaList.setAttribute('id', `peer-${peer.id}-medias`);
peerItem.append(mediaList);

peer.medias.forEach((media) => {
const mediaItem = document.createElement('li');
const toggleIdx = `media-${media.id}-toggle`;
const toggleStr = media.remote ? `[<a href="#" id="${toggleIdx}" data-id="${media.id}">toggle</a>]` : '';

mediaItem.setAttribute('id', `media-${media.id}`);
mediaItem.innerHTML = `Media(${media.id}) <div> Paused: ${media.paused} ${toggleStr} <div>`;
mediaList.append(mediaItem);

document.querySelector < HTMLButtonElement > (`#${toggleIdx}`)?.addEventListener('click', async (e: any) => {
if (media.paused) {
await media.resume();
} else {
await media.pause();
}
addOrUpdateUiPeer(peer);
});
});
}

/**
* Helper function to remove a peer node from the UI.
*/
function removeUiPeer(peer: OdinPeer) {
app.querySelector(`#peer-${peer.id}`)?.remove();
}

/**
* Helper function to highlight media activity in the UI.
*/
function updateUiMediaActivity(media: OdinMedia) {
if (media.active) {
app.querySelector(`#media-${media.id}`)?.setAttribute('class', 'talking');
} else {
app.querySelector(`#media-${media.id}`)?.removeAttribute('class');
}
}

/**
* Helper function to reset UI to its original state.
*/
function resetUi() {
accessKeyInput?.removeAttribute('disabled');
roomIdInput?.removeAttribute('disabled');
userIdInput?.removeAttribute('disabled');
generateAccessKeyBtn?.removeAttribute('disabled');
if (toggleConnectionBtn) toggleConnectionBtn.innerHTML = 'Join';
app.querySelector('#peer-container')?.remove();
}

Angular

We have created a simple Angular application that you can use as a starting point for your own Angular application or an integration into existing applications.

We provide the source code in our Github repository: Angular Demo for ODIN.