@4players/odin-bot-sdk
v0.4.4
Published
A simple SDK built on top of Odin NodeJS SDK and Odin Foundation to quickly build simple bots that welcome users, change rooms or adding AI bots to Odin based text chats.
Downloads
250
Readme
ODIN Bot SDK for NodeJS
This is a simple bot SDK built on top of the ODIN Web (JS/TS) SDK to make it easier to build bots for ODIN. It provides a simple interface to the ODIN Web SDK and handles the communication with the ODIN Web SDK.
Prerequisites
ODIN in its core is super flexiblen and allows you to build and use any data structures that you like. However, this
flexibility comes with a price: It makes interacting between different applications and bots a bit more difficult.
For this we built a set of simple core data structures bundled in the @4players/odin-foundation
package. It
contains interfaces for users and messages.
The ODIN Bot SDKs uses these data structures to build internal user representations and to send messages and RPCs.
If you want your application to interact with the Bot SDK make sure to use the same naving conventions as defined in the ODIN foundation.
If you have already built your own data structures, you can still make use of the Bot SDK. Just make a few
adjustments to the OdinBot
class to create data structures your application needs.
Installation
Add the bot SDK to your NodeJS project:
npm install @4players/odin-bot-sdk
Usage
You need to create you own bot class that extends the OdinBot
class. You can then add your own functionality by
overriding a few functions:
import {
OdinMessageReceivedEventPayload,
OdinPeerJoinedEventPayload,
OdinPeerLeftEventPayload
} from "@4players/odin-nodejs";
class MyBot extends OdinBot {
/** Override register RPC methods to register your own RPC methods */
protected override registerRPCMethods() {
this.registerRPCMethod("getUsers");
this.registerRPCMethod("getPing");
}
/** Override onPeerLeft to get notified when a new user left the room */
protected override async onPeerLeft(event: OdinPeerLeftEventPayload, user: IUser) {
await this.sendTextMessage(`${user.name} left the room`);
}
/** Override onPeerJoined to get notified when a new user joined the room */
protected override async onPeerJoined(event: OdinPeerJoinedEventPayload, user: IUser) {
if (this.isJoined) {
// Send a private text message to the individual user (this message cannot be seen by other peers in the room
await this.sendTextMessage(`Hello there ${user.name}! I am a bot and will store all messages that you
write so other can also read them. If you are not happy with that, please leave this channel`, event.peerId);
// Send a public text message to all users in the room
await this.sendTextMessage(`${user.name} joined the room`);
}
}
/** Override onTextMessageReceived to get notified when a new text message was received */
protected override async onTextMessageReceived(event: OdinMessageReceivedEventPayload, message: IChatMessage, userData: IUserData) {
console.log("Someone sent this message:", message, userData);
}
/** One of your own RPC methods */
protected async getUsers(senderPeerId: number) {
await this.sendTextMessage(`There are ${this.users.size} users in this room`, senderPeerId);
}
/** Another RPC method */
protected async getPing(senderPeerId: number, text: string) {
await this.sendTextMessage(`We got this text: ${text}`, senderPeerId);
}
}
Next, create an instance of the class by providing your access key and a unique bot id. This allows your application to distinguish between different bots. Finally, start the bot by providing the name of the room.
const main = async function() {
const bot = new MyBot("Ad4R7/hpCx1U5yGvC61oNBeJ/fWiW7dodvXWW7MEwrjg", "bot-00001");
// Set the sample rate and number of channels to use for audio capture (default is 48000 and 1)
const sampleRate = 48000;
const numChannels = 1;
await bot.start("Lobby", sampleRate, numChannels);
}
main()
.then(() => {
console.log("Bot started");
})
.catch((err) => {
console.error("Could not start bot", err);
});
Please note: The start
function provides
Functions you can override
You can override these functions of the OdinBot
class to adapt functionality:
protected override registerRPCMethods()
: Called when the bot is started. Use this to register your own RPC methodsprotected override async onPeerLeft(event: OdinPeerLeftEventPayload, user: IUser)
: Called when a new user joined the roomprotected override async onPeerJoined(event: OdinPeerJoinedEventPayload, user: IUser)
: Called when a user left the roomprotected override async onTextMessageReceived(event: OdinMessageReceivedEventPayload, message: IChatMessage, userData: IUserData)
: Called when a new text message was receivedprotected override async onUserDataChanged(event: OdinPeerUserDataChangedEventPayload, userData: IUserData)
: Called when the user data of a user changedprotected override async onRoomDataChanged(event: OdinRoomUserDataChangedEventPayload, roomData: IRoomData)
: Called when the room data changedprotected override async onRoomJoined(event: OdinJoinedEventPayload, roomData: IRoomData)
: Called when the bot joined a roomprotected override async onRoomLeft(event: OdinLeftEventPayload)
: Called when the bot left a roomprotected override onMediaActivity(event: OdinMediaActivityEventPayload, user: IUser, active: boolean)
: Called when the media activity of a user changedprotected override onMediaAdded(event: OdinMediaAddedEventPayload, user: IUser, mediaId: number)
: Called when a user added a new media streamprotected override onMediaRemoved(event: OdinMediaRemovedEventPayload, user: IUser, mediaId: number)
: Called when a user removed a media streamprotected override onAudioDataReceived(event: OdinAudioDataReceivedEventPayload, user: IUser)
: Called when new audio samples are available
Capturing audio
With the new Bot SDK (from version 0.2.0) you can record audio from each individual user. To do so, you need to start capturing audio:
this.startCaptureAudio();
If you want to capture audio right from the beginning, use the onBeforeJoin
callback function to start capturing:
class MyBot extends OdinBot {
// ...
protected override onBeforeJoin() {
this.startCaptureAudio();
}
// ...
}
Please note: Users typically don't expect to be recorded. So you should inform your users about that by sending a message or by showing a popup.
You will now receive onAudioDataReceived
events for each user that is talking every 20 milliseconds. In the event
payload you'll receive 16-bit samples ranging from -32768 to 32767 or 32-bit floats ranging from -1 to 1. The sample
rate is determined by the sample rate and channel count that you set when starting the bot. Different audio
libraries handle audio differently and require different samples. You should be fine with either the 16-bit or
32-bit samples.
This is a simple example of how to record audio using the wav
NPM package:
class MyBot extends OdinBot {
private fileRecorder: wav.FileWriter;
// ...
protected override onBeforeJoin() {
// Start capture audio (required to receive audio data)
this.startCaptureAudio();
this.fileRecorder = new wav.FileWriter("audio.wav", {
channels: 1,
sampleRate: 48000,
bitDepth: 16
});
}
protected override async onAudioDataReceived(event: OdinAudioDataReceivedEventPayload, user: IUser) {
// You can directly write the 16 bit samples to the WAV encoder
this.fileRecorder.wavEncoder.file.write(event.samples16, (error) => {
if (error) {
console.log("Failed to write audio file");
}
});
}
// ...
}
As you can see, it's super simple to receive audio data and working with them. In our example (see /examples folder) we show you how to leverage OpenAI to transcribe audio. As you receive individual audio files for each user, you can also use that for moderation purposes.
Sending audio
With the new Bot SDK (from version 0.2.0) you can also send audio to the room. To do so, you need to create an
OdinMedia
instance like this:
// Create a new audio stream with 44.1 kHz and 1 channel
const media = room.createAudioStream(44200, 1);
// Prepare our MP3 decoder and load the sample file
const audioBuffer = await decode(fs.readFileSync('./santa.mp3'));
// Create a stream that will match the settings of the file
const audioBufferStream = new AudioBufferStream({channels: 1, sampleRate: 44100, float: true, bitDepth: 32});
// Whenever the stream has data, send it to the media stream
audioBufferStream.on('data', (data) => {
const floats = new Float32Array(new Uint8Array(data).buffer)
media.sendAudioData(floats);
});
// Write the audio file to the stream. AudioBufferStream will read the file and send it as a media stream which will
// trigger the on('data') event which we use to just forward the samples to ODIN
audioBufferStream.write(audioBuffer);
}
We used the npm packages audio-buffer-stream
and audio-decode
for this example code. Of course there are other
ways to handle audio. It's just important to regularly (every 20 milliseconds) send audio data to the media stream.
ODIN requires 32-bit floats with values ranging from -1 to 1. The sample rate and channel count must match the
options used when creating the media object.
Samples
You can find a sample bot in the examples
folder. It's a simple bot that will send a message to the room when a
user joins or leaves and answers questions starting with @bot
using ChatGPT-API from OpenAI. It also records audio
and transcribes them using OpenAIs new whisper
model.
The @4players/odin-nodejs
package also contains some working samples for recording and playing audio.
RPC methods
The @4players/odin-foundation
packages provides data structures for different kind of messages:
message
: A simple text messagepoke
: A text message that notifies the userrpc
: A remote procedure call that can be used to trigger actions in the bot
You need to register a member function of your class as a RPC method by calling the registerRPCMethod
function.
Best place for that is to override the registerRPCMethods
function.
class MyBot extends OdinBot {
// ...
protected override registerRPCMethods() {
this.registerRPCMethod("getUsers");
this.registerRPCMethod("getPing");
}
// ...
}
RPC methods need to have this format:
export type OdinBotRPCMethod = (senderPeerId: number, ...args: any[]) => void;
The first parameter is the peer id of the sender (of the rpc call) and optionally additional parameters.
To send the RPC call from your application, you only need to send a message with this data structure to the bot. The bot will then call the registered RPC method.
const rpcPayload: IRPCPayload = {
method: 'getPing',
args: ['This is my text and expect the bot to send it back']
}
const message: IMessageTransferFormat = {
kind: 'rpc',
payload: rpcPayload
}
await this.room.sendMessage(this.encodeObjToUint8Array(message));