Vanilla JS
This comprehensive guide demonstrates the essential steps to integrate the ODIN SDK into your website, including:
- How to properly include and initialize the SDK
- Steps to join voice chat rooms
- Techniques for handling real-time events
For a complete implementation with additional features and best practices, explore our VanillaJS Example on GitHub.
Initializing the plugin
To interact with the ODIN Web SDK
, the first step is to include both, the ODIN Web SDK
and ODIN Web Plugin
via one of the following methods:
Web Plugin
- IIFE (link to CDN)
- ESM (link to CDN)
Web SDK
- IIFE (link to CDN)
- ESM (link to CDN)
After both, the ODIN Plugin
and ODIN Web SDK
is included in the app, the Web SDK gets initialized as the examples below demonstrate.
info
The primary difference between the IIFE
and ESM
approaches lies in how the ODIN Plugin
and ODIN Web SDK
are included in your app. Otherwise, the rest of this example is the same for both.
IIFE intializing example
<body>
<div id="app"></div>
<script type="text/javascript" src="https://cdn.odin.4players.io/client/js/sdk/1.0.0-alpha.51/odin-sdk.js"></script>
<script type="text/javascript" src="https://cdn.odin.4players.io/client/js/sdk/1.0.0-alpha.57/odin-plugin.js"></script>
<script>
let audioPlugin;
async function initPlugin() {
if (audioPlugin) return audioPlugin;
audioPlugin = ODIN_PLUGIN.createPlugin(async (sampleRate) => {
const audioContext = new AudioContext({ sampleRate });
await audioContext.resume();
return audioContext;
});
ODIN.init(audioPlugin);
return audioPlugin;
}
initPlugin().then(_ => {
console.log('index.html initPlugin initialized');
});
</script>
<script type="module" src="/src/main.js"></script>
</body>
ESM intializing example
import * as ODIN_PLUGIN from './assets/odin-plugin-web-esm_1.0.0-alpha.37.js';
import * as ODIN from './assets/odin-sdk-esm_1.0.0-alpha.37.js';
let audioPlugin;
async function initPlugin() {
if (audioPlugin) return audioPlugin;
audioPlugin = ODIN_PLUGIN.default.createPlugin(async (sampleRate) => {
const audioContext = new AudioContext({ sampleRate });
await audioContext.resume();
return audioContext;
});
ODIN.init(audioPlugin);
return audioPlugin;
}
initPlugin().then(_ => {
console.log('main.js initPlugin initialized');
});
Full example
import './style.css';
import * as ODIN_PLUGIN from 'https://cdn.odin.4players.io/client/js/sdk/1.0.0-alpha.51/odin-sdk.esm.js';
import * as ODIN from 'https://cdn.odin.4players.io/client/js/sdk/1.0.0-alpha.57/odin-plugin.esm.js';
import { generateAccessKey, TokenGenerator } from '@4players/odin-tokens';
/**
* 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-comprehensible 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 = 'Foo';
/**
* 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 = '';
let room;
let audioInput;
let videoInput;
let audioPlugin;
let muted;
/**
* 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) {
try {
// Set the default Audio device (non-blocking);
audioPlugin.setOutputDevice({}).then();
room = new ODIN.Room();
handleRoomEvents(room);
await room.join(token, {
gateway: serverAddress,
roomId,
transport: 'H3',
cipher: {
type: 'OdinCrypto',
password: roomId,
},
});
// Activating Audio Capture
if (!audioInput) {
ODIN.DeviceManager.createAudioInput()
.then(async (input) => {
audioInput = input;
await room?.addAudioInput(audioInput);
showDeviceSettings(audioInput);
})
.catch((error) => {
console.error(error);
});
}
} catch (e) {
console.error('Failed to join room', e);
alert(e);
disconnect();
resetUi();
}
}
async function initPlugin() {
if (audioPlugin) return audioPlugin;
audioPlugin = ODIN_PLUGIN.default.createPlugin(async (sampleRate) => {
console.log('audioPlugin', audioPlugin);
const audioContext = new AudioContext({ sampleRate });
await audioContext.resume();
return audioContext;
});
ODIN.init(audioPlugin);
return audioPlugin;
}
/**
* Helper function to set event handlers for ODIN room events.
*/
function handleRoomEvents(room) {
// Handle the UI depending on the status of the room.
room.onStatusChanged = (payload) => {
console.info('Client connection status changed', payload.newState);
if (payload.newState.status !== '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 =
payload.newState.status === 'joined'
? `Joined '${room.id}' on ${room.address}`
: 'Not Connected';
}
};
// Handle peer join events to update the UI.
room.onPeerJoined = (payload) => addOrUpdateUiPeer(payload.peer);
// Handle peer left events to update the UI.
room.onPeerLeft = (payload) => {
removeUiPeer(payload.peer);
};
// Handle media started events to update the UI.
room.onMediaStarted = (payload) => addOrUpdateUiPeer(payload.peer);
// Handle media stopped events to update the UI.
room.onMediaStopped = (payload) => addOrUpdateUiPeer(payload.peer);
// Handle when a remote video was started
room.onVideoOutputStarted = (payload) => onVideoStarted(payload);
// Handle when a remote video was stopped
room.onVideoOutputStopped = (payload) => onVideoStopped(payload);
// Handle peer user data changes to update the UI.
room.onUserDataChanged = (payload) => addOrUpdateUiPeer(payload.peer);
}
/**
* Leaves the room and closes the connection to the ODIN server network.
*/
function disconnect() {
room?.leave();
room = undefined;
audioInput = undefined;
}
// ======================================================================================================================
// ======================================================================================================================
// ==================================== All the example UI related code starts below ====================================
// ======================================================================================================================
// ======================================================================================================================
/**
* Render some basic HTML.
*/
const app = document.querySelector('#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>
<div id="device-settings">
</div>
<fieldset id="room-container">
<legend id="room-title">Not Connected</legend>
</fieldset>
<div id="local-video"></div>
<div id="remote-video"></div>
`;
/**
* Grab server address input and register event handlers to handle manual changes.
*/
const serverAddressInput =
app.querySelector('#server-address');
serverAddressInput?.addEventListener('change', (e) => {
if (!e.target) return;
serverAddress = e.target.value;
});
/**
* Grab access key input and register event handlers to handle manual changes.
*/
const accessKeyInput = app.querySelector('#access-key');
accessKeyInput?.addEventListener('change', (e) => {
if (!e.target) return;
accessKey = e.target.value;
});
/**
* Grab user ID input and register event handlers to handle manual changes.
*/
const userIdInput = app.querySelector('#user-id');
userIdInput?.addEventListener('change', (e) => {
if (!e.target) return;
userId = e.target.value;
});
/**
* Grab user data input and register event handlers to handle manual changes.
*/
const userDataInput = app.querySelector('#user-data');
userDataInput?.addEventListener('change', (e) => {
if (!e.target) return;
userData = e.target.value;
// if we're connected, also send an update of our own user data to the server
const ownPeer = room?.ownPeer;
if (room && room.status.status === 'joined' && ownPeer) {
ownPeer.data = ODIN.valueToUint8Array(userData);
ownPeer.update(); // flush user data update
console.info('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('#room-id');
roomIdInput?.addEventListener('change', (e) => {
if (!e.target) return;
roomId = e.target.value;
});
/**
* Grab the 'Generate' button and register an event handler to update the access key if necessary.
*/
const generateAccessKeyBtn = document.querySelector(
'#generate-access-key'
);
generateAccessKeyBtn?.addEventListener('click', () => {
accessKey = generateAccessKey();
if (accessKeyInput) accessKeyInput.value = accessKey;
console.info('Generated a new local access key', accessKey);
});
/**
* Grab the 'Join / Leave' button and register and register an event handler to connect/disconnect.
*/
const toggleConnectionBtn =
document.querySelector('#toggle-connection');
toggleConnectionBtn?.addEventListener('click', async (e) => {
if (!room) {
try {
const tokenGenerator = new TokenGenerator(accessKey);
const audience = (app.querySelector('#token-audience')
?.value ?? 'gateway');
const token = tokenGenerator.createToken(roomId, userId, {
audience,
customer: '<no customer>',
});
console.info('Generated a new signed JWT to join room', token);
await connect(token);
} catch (e) {
console.error('Failed to generate token', e);
alert(e);
}
} else {
disconnect();
}
});
const voiceGateLevelInput = document.createElement('input');
voiceGateLevelInput.id = 'voice-gate-input';
voiceGateLevelInput.type = 'text';
/**
* Handles rendering of a few device-related things, for example, muting the audio
* or starting/stopping the video stream.
* @param audioInput
*/
function showDeviceSettings(audioInput) {
const container = document.querySelector('#device-settings');
if (!container) return;
const startStopMediasContainer = document.createElement('div');
startStopMediasContainer.id = 'start-stop-medias';
const volumeGateContainer = document.createElement('div');
volumeGateContainer.id = 'volume-gate';
const toggleAudioInputBtn = document.createElement('button');
toggleAudioInputBtn.id = 'toggle-audio';
toggleAudioInputBtn.textContent = 'Mute AudioInput';
const toggleVideoInputBtn = document.createElement('button');
toggleVideoInputBtn.id = 'toggle-video';
toggleVideoInputBtn.textContent = 'Start Video';
const inputDeviceSelect = document.createElement('select');
inputDeviceSelect.id = 'input-device';
const outputDeviceSelect = document.createElement('select');
outputDeviceSelect.id = 'output-device';
const voiceGateLevelBtn = document.createElement('button');
voiceGateLevelBtn.innerText = 'Apply VoiceGate';
voiceGateLevelBtn.onclick = () => {
const value = parseInt(voiceGateLevelInput.value);
if (audioInput) {
audioInput.setInputSettings({ volumeGate: isNaN(value) ? -40 : value });
}
};
startStopMediasContainer.appendChild(toggleAudioInputBtn);
startStopMediasContainer.appendChild(toggleVideoInputBtn);
container.appendChild(startStopMediasContainer);
container.appendChild(inputDeviceSelect);
container.appendChild(outputDeviceSelect);
volumeGateContainer.append(voiceGateLevelInput);
volumeGateContainer.append(voiceGateLevelBtn);
container.appendChild(volumeGateContainer);
toggleAudioInputBtn?.addEventListener('click', async (e) => {
if (!room || !audioInput) return;
muted = !muted;
if (muted) {
audioInput.setVolume(0);
toggleAudioInputBtn.innerHTML = 'Unmute AudioInput';
} else {
audioInput.setVolume(1);
toggleAudioInputBtn.innerHTML = 'Mute AudioInput';
}
});
toggleVideoInputBtn?.addEventListener('click', async (e) => {
if (!room) return;
if (!videoInput) {
toggleVideoInputBtn.disabled = true;
const ms = await navigator.mediaDevices.getUserMedia({
video: true,
});
videoInput = await ODIN.DeviceManager.createVideoInput(ms);
room.addVideoInput(videoInput);
toggleVideoInputBtn.innerHTML = 'Stop Video';
toggleVideoInputBtn.disabled = false;
const videoElement = createVideoElement(
videoInput.mediaStream,
room.ownPeerId
);
const localVideoContainer = document.querySelector('#local-video');
localVideoContainer?.appendChild(videoElement);
} else {
const videoElement = document.querySelector(`#video-${room.ownPeerId}`);
videoElement?.remove();
room.removeVideoInput(videoInput);
const ms = videoInput.mediaStream;
ms?.getTracks().forEach((track) => {
track.stop();
ms.removeTrack(track);
});
videoInput = undefined;
toggleVideoInputBtn.innerHTML = 'Start Video';
}
});
ODIN.DeviceManager.listAudioInputs().then((devices) => {
devices.forEach((device) => {
const option = document.createElement('option');
option.value = device.name;
option.label = device.name;
if (inputDeviceSelect) {
inputDeviceSelect.append(option);
}
});
});
inputDeviceSelect?.addEventListener('click', async (event) => {
const device = await ODIN.DeviceManager.getInputDeviceByName(
event.target.value
);
audioInput?.setDevice({ device });
});
ODIN.DeviceManager.listAudioOutputs().then((devices) => {
devices.forEach((device) => {
const option = document.createElement('option');
option.value = device.name;
option.label = device.name;
if (outputDeviceSelect) {
outputDeviceSelect.append(option);
}
});
});
outputDeviceSelect?.addEventListener('click', async (event) => {
const device = await ODIN.DeviceManager.getOutputDeviceByName(
event.target?.value
);
audioPlugin?.setOutputDevice({ device });
});
}
/**
* Helper function to add a peer node to the UI.
*/
function addOrUpdateUiPeer(peer) {
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');
// Handle media stopped events of the peer to update our UI and stop the audio decoder
peer.onAudioActivity = (payload) => {
const active = payload.media.isActive;
if (active) {
peerItem.setAttribute('class', 'talking');
} else {
peerItem.classList.remove('talking');
}
};
const decodedData = ODIN.uint8ArrayToValue(peer.data);
let userData = '';
if (typeof decodedData === 'object') {
userData = JSON.stringify(decodedData);
} else {
userData = String(decodedData);
}
peerItem.setAttribute('id', `peer-${peer.id}`);
peerItem.innerHTML = `Peer(${peer.id}) ${!peer.isRemote ? '(You)' : ''} <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);
if (peer.isRemote) {
const mediaItem = document.createElement('li');
mediaItem.setAttribute('class', 'media');
mediaItem.innerHTML = `AudioOutputs: ${peer.audioOutputs.length}`;
mediaList.append(mediaItem);
}
if (!peer.isRemote) {
const mediaItem = document.createElement('li');
mediaItem.setAttribute('class', 'media');
mediaItem.innerHTML = `AudioInputs: ${peer.audioInputs.length}`;
mediaList.append(mediaItem);
}
}
/**
* Helper function to remove a peer node from the UI.
*/
function removeUiPeer(peer) {
app.querySelector(`#peer-${peer.id}`)?.remove();
}
/**
* Handles the UI and behavior when a remote video was added to the room.
* @param payload
*/
async function onVideoStarted(payload) {
const video = payload.media;
await video.start();
if (!video.mediaStream) return;
const videoElement = createVideoElement(video.mediaStream, payload.peer.id);
const remoteVideoContainer = document.querySelector('#remote-video');
remoteVideoContainer?.appendChild(videoElement);
}
/**
* Handles the UI and behavior when a remote video was removed from the room.
* @param payload
*/
async function onVideoStopped(payload) {
const videoElement = document.querySelector(`#video-${payload.peer.id}`);
videoElement?.remove();
const video = payload.media;
payload.room.stopVideoOutput(video);
}
/**
* Helper function to reset UI to its original state.
*/
function resetUi() {
accessKeyInput?.removeAttribute('disabled');
const localVideoContainer = app.querySelector('#local-video');
const remoteVideoContainer = app.querySelector('#remote-video');
const deviceSettingsContainer = app.querySelector('#device-settings');
if (localVideoContainer) localVideoContainer.innerHTML = '';
if (remoteVideoContainer) remoteVideoContainer.innerHTML = '';
if (deviceSettingsContainer) deviceSettingsContainer.innerHTML = '';
if (videoInput) {
stopMediaStream(videoInput.mediaStream);
}
roomIdInput?.removeAttribute('disabled');
userIdInput?.removeAttribute('disabled');
generateAccessKeyBtn?.removeAttribute('disabled');
if (toggleConnectionBtn) toggleConnectionBtn.innerHTML = 'Join';
app.querySelector('#peer-container')?.remove();
}
/**
* Creates a <video> Element and attaches the given stream at it.
* @param ms
* @param peerId
*/
function createVideoElement(ms, peerId) {
const video = document.createElement('video');
video.id = `video-${peerId}`;
video.preload = 'none';
video.autoplay = true;
video.playsInline = true;
video.muted = true;
video.srcObject = ms;
return video;
}
/**
* Stops the given MediaStream.
* @param ms
*/
function stopMediaStream(ms) {
ms.getVideoTracks().forEach((track) => {
track.stop();
ms.removeTrack(track);
});
}
/**
* Initializing the Odin Plugin.
*/
initPlugin().then(() => {
console.info('Initialized plugin');
});