@4players/odin-bot-sdk
TypeScript icon, indicating that this package has built-in type declarations

0.4.4 • Public • Published

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 methods
  • protected override async onPeerLeft(event: OdinPeerLeftEventPayload, user: IUser): Called when a new user joined the room
  • protected override async onPeerJoined(event: OdinPeerJoinedEventPayload, user: IUser): Called when a user left the room
  • protected override async onTextMessageReceived(event: OdinMessageReceivedEventPayload, message: IChatMessage, userData: IUserData): Called when a new text message was received
  • protected override async onUserDataChanged(event: OdinPeerUserDataChangedEventPayload, userData: IUserData): Called when the user data of a user changed
  • protected override async onRoomDataChanged(event: OdinRoomUserDataChangedEventPayload, roomData: IRoomData): Called when the room data changed
  • protected override async onRoomJoined(event: OdinJoinedEventPayload, roomData: IRoomData): Called when the bot joined a room
  • protected override async onRoomLeft(event: OdinLeftEventPayload): Called when the bot left a room
  • protected override onMediaActivity(event: OdinMediaActivityEventPayload, user: IUser, active: boolean): Called when the media activity of a user changed
  • protected override onMediaAdded(event: OdinMediaAddedEventPayload, user: IUser, mediaId: number): Called when a user added a new media stream
  • protected override onMediaRemoved(event: OdinMediaRemovedEventPayload, user: IUser, mediaId: number): Called when a user removed a media stream
  • protected 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 message
  • poke: A text message that notifies the user
  • rpc : 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));

Readme

Keywords

Package Sidebar

Install

npm i @4players/odin-bot-sdk

Weekly Downloads

46

Version

0.4.4

License

MIT

Unpacked Size

117 kB

Total Files

17

Last publish

Collaborators

  • svenpaulsen
  • pschuster