Janus Video Conferencing
Tags: typescript, janus, react, webrtc
Published: 2020-11-01 13:36:20

Hi everyone, recently I was building a video conferencing app with **Janus**. If you are not familiar with Janus,

>Janus is a WebRTC Server developed by Meetecho conceived to be a general-purpose one. As such, it doesn't provide any functionality per se other than implementing the means to set up a WebRTC media communication with a browser, exchanging JSON messages with it, and relaying RTP/RTCP and messages between browsers and the server-side application logic they're attached to. Any specific feature/application is provided by server-side plugins, that browsers can then contact via Janus to take advantage of the functionality they provide.

Check out the actual documentation on [Janus Documentation](https://janus.conf.meetecho.com/). But the problem lies there **_THE JANUS DOCUMENTATION_**, it is pretty elaborate but lacks examples which in my opinion makes this brilliant technology daunting and difficult to use at first glance. So, today I thought I should share my experience and to help others using this excellent open-source project.

####What are we going to do?
I am going to walk you through, building some general utility functions of Janus to build a video conferencing app. We will just be using typescript and Janus library.

####How Janus works?
Janus provides us with basic WebRTC methods like `createOffer()` and `createAnswer()` but it also provides something even better, ***_Plugins_***. _Plugins_ are like extensions that can be attached to Janus which makes our task even simpler. In this tutorial, we will be using the VideoRoom Plugin and the TextRoom Plugin. The VideoRoom Plugin will be used for video-audio data transmission and the TextRoom plugin will be used for web socket communication.

####Enough talk, Let's start...
1. Firstly we need to setup Janus so we can use it as a module. So for react developers, there is already a blog on [Janus Setup](https://medium.com/good-robot/wrangling-janus-and-your-robot-server-into-a-react-app-4d9278f79eda). For Angular and Vue developers I am sure there is some other way.
1. Now let's create a file called _janusAdapter.ts_ and import Janus into it.
```typescript
import Janus from "janus"; // from janus.js
```
1. Now we need to declare the JanusAdapter class and initialize the variables we will be needing.
```typescript
interface InitConfig {
  room: number;
  id: number;
  onData?: Function;
  onLocalStream?: Function;
}
class JanusAdapter {
  // store the janusInstance to be used in other functions
  private janusInstance: Janus | null = null;
  // the configurations of the janusInstance
  private janusConfig: InitConfig | null = null;
  // store the VideoRoom plugin instance
  private publisherSfu: any;
  // store the TextRoom plugin instance
  private textroom: any;
  private const SERVER_URL = _YOUR JANUS SERVER URL_;
}
```
_Note: You can use a constructor to initialize the variables._
1. We will now define the first utility function `init()` to get a Janus instance and store it to `janusInstance` variable.
```typescript
public init(config: InitConfig): Promise<void> {
    return new Promise((resolve, reject) => {
      Janus.init({
        callback: () => {
          const janus = new Janus({
            server: SERVER_URL,
            success: () => {
              this.janusInstance = janus;
              this.janusConfig = config;
              if (typeof config.debug === "undefined")
                this.janusConfig.debug = false;
              this.debug("Janus initialized successfully!");
              this.debug(this.janusConfig);
              resolve();
            },
            error: (err: string) => {
              console.log(err);
              console.error("Janus Initialization failed! Exiting...", err);
              reject();
            },
          });
        },
      });
    });
  }
```
1. The VideoRoom plugin expects us to specify whether we want to be a "***publisher***", broadcasting our video and audio feed or a "***subscriber***", receive someone's video and audio feed. If we want both then we have to attach two VideoRoom plugin instances to the `janusInstance`. So let's breakdown _publishing_ and _subscribing_ into two different methods. First comes the publish method -
```typescript
public publish(stream: MediaStream): Promise<void> {
    return new Promise((resolve, reject) => {
      // Attach the videoroom plugin
      this.janusInstance!.attach!({
        plugin: "janus.plugin.videoroom",
        opaqueId: Janus.randomString(12),
        success: (pluginHandle: any) => {
          this.debug("Publisher plugin attached!");
          this.debug(pluginHandle);
          // Set the SFU object
          this.publisherSfu = pluginHandle;

          // Request to join the room
          let request: { [key: string]: any } = {
            request: "join",
            room: this.janusConfig!.room,
            ptype: "publisher",
            id: this.janusConfig!.pubId
          };
          if (this.janusConfig!.display)
            request.display = this.janusConfig!.display;

          pluginHandle.send({ message: request });
        },
        onmessage: async (message: any, jsep: string) => {
          if (jsep) {
            this.debug({ message, jsep });
          } else {
            this.debug(message);
          }

          if (message.videoroom === "joined") {
            // Joined successfully, create SDP Offer with our stream
            this.debug("Joined room! Creating offer...");

            if (this.janusConfig!.onJoined) this.janusConfig!.onJoined(message.description);

            let mediaConfig = {};

            if (stream === null || typeof stream === "undefined") {
              mediaConfig = {
                audioSend: false,
                videoSend: false
              };
            } else {
              mediaConfig = {
                audioSend: true,
                videoSend: true
              };
            }

            if (typeof this.janusConfig!.onData === "function") {
              mediaConfig = { ...mediaConfig, data: true };
            }

            this.debug("Media Configuration for Publisher set! ->");
            this.debug(mediaConfig);

            this.publisherSfu.createOffer({
              media: mediaConfig,
              stream: stream ? stream : undefined,
              success: (sdpAnswer: string) => {
                // SDP Offer answered, publish our stream
                this.debug("Offer answered! Start publishing...");
                let publish = {
                  request: "configure",
                  audio: true,
                  video: true,
                  data: true
                };
                this.publisherSfu.send({ message: publish, jsep: sdpAnswer });
              },
            });
          } else if (message.videoroom === "destroyed") {
            // Room has been destroyed, time to leave...
            this.debug("Room destroyed! Time to leave...");
            if(this.janusConfig!.onDestroy)
              this.janusConfig!.onDestroy();
            resolve();
          }

          if (message.unpublished) {
            // We've gotten unpublished (disconnected, maybe?), leaving...
            if (message.unpublished === "ok") {
              this.debug("We've gotten disconnected, hanging up...");
              this.publisherSfu.hangup();
            } else {
              if (this.janusConfig!.onLeave)
                this.janusConfig!.onLeave(message.unpublished);
            }
            resolve();
          }

          if (jsep) {
            this.debug("Handling remote JSEP SDP");
            this.debug(jsep);
            this.publisherSfu.handleRemoteJsep({ jsep: jsep });
          }
        },
        onlocalstream: (localStream: MediaStream) => {
          this.debug("Successfully published local stream: " + localStream.id);
          if (this.janusConfig!.onLocalStream)
            this.janusConfig!.onLocalStream(localStream);
        },
        error: (err: string) => {
          this.debug("Publish: Janus VideoRoom Plugin Error!", true);
          this.debug(err, true);
          reject();
        },
      });
    });
  }
```
Here we first attach a VideoRoom plugin to the janusInstance and on successfully receiving a `pluginHandle` we set it to `publisherSfu`. Then we make a request to join the room with the `pluginHandle`. The meat and potatoes of the code are in the `onmessage` callback. Here we handle the different types of responses from Janus according to our needs(check the official docs to see all the responses). I have just written a few of them, the main one being the ***"joined"*** event in which we have to create a offer on successful join with the desired stream we want to publish.
1. We need the `subscribe()` method now.
```typescript
public subscribe(id: number): Promise<MediaStream> {
    return new Promise((resolve, reject) => {
      let sfu: any = null;

      this.janusInstance!.attach!({
        plugin: "janus.plugin.videoroom",
        opaqueId: Janus.randomString(12),
        success: (pluginHandle: any) => {
          this.debug("Remote Stream Plugin attached.");
          this.debug(pluginHandle);

          sfu = pluginHandle;
          sfu.send({
            message: {
              request: "join",
              room: this.janusConfig!.room,
              feed: id,
              ptype: "subscriber",
            },
          });
        },
        onmessage: (message: any, jsep: string) => {
          if (message.videoroom === "attached" && jsep) {
            this.debug(
              "Attached as subscriber and got SDP Offer! \nCreating answer..."
            );

            sfu.createAnswer({
              jsep: jsep,
              media: { audioSend: false, videoSend: false, data: true },
              success: (answer: string) => {
                sfu.send({
                  message: { request: "start", room: this.janusConfig!.room },
                  jsep: answer,
                  success: () => {
                    this.debug("Answer sent successfully!");
                  },
                  error: (err: string) => {
                    this.debug("Error answering to received SDP offer...");
                    this.debug(err, true);
                  },
                });
              },
            });
          }
        },
        onerror: (err: string) => {
          this.debug("Remote Feed: Janus VideoRoom Plugin Error!", true);
          this.debug(err, true);
          reject(err);
        },
      });
    });
  }
```
This method is a bit less intimidating than the `publish()` one 😄😄😄. Here also we are first attaching the VideoRoom plugin to the `janusInstance` and then joining the room as a subscriber and mentioning which feed we want to listen to(basically we have to pass the id of the publisher whose video and audio stream we need). When the plugin is attached successfully we are creating an answer boom!!! We should get the feed of the one we subscribed to.
7. The TextRoom part is left which is also similar to the above methods.
```typescript
public joinTextRoom(){
    return new Promise((resolve, reject) => {
      this.janusInstance!.attach!({
        plugin: "janus.plugin.textroom",
        opaqueId: Janus.randomString(12),
        success: (pluginHandle: any) => {
          this.textroom = pluginHandle;
          this.debug("Plugin attached! (" + this.textroom.getPlugin() + ", id=" + this.textroom.getId() + ")");
          // Setup the DataChannel
          var body = { request: "setup" };
          this.debug("Sending message:");
          this.debug(body)
          this.textroom.send({ message: body });
        },
        onmessage: (message: any, jsep: string) => {
          this.debug(message);
          if(jsep) {
            // Answer
            this.textroom.createAnswer(
              {
                jsep: jsep,
                media: { audio: false, video: false, data: true },	// We only use datachannels
                success: (jsep: string) => {
                  this.debug("Got SDP!");
                  this.debug(jsep);
                  var body = { request: "ack" };
                  this.textroom.send({ 
                    message: body, jsep: jsep,
                    success: () => {
                      let request: { [key: string]: any } = {
                        textroom: "join",
                        room: this.janusConfig!.room,
                        transaction: Janus.randomString(12),
                        display: this.janusConfig!.display,
                        username: this.janusConfig!.display
                      };
                      if (this.janusConfig!.display)
                        request.display = this.janusConfig!.display;

                        this.textroom.data({ 
                          text: JSON.stringify(request),
                          error: (err: string) => this.debug(err)
                      });
                    } 
                  });
                  resolve();
                },
                error: (error: string) => {
                  this.debug("WebRTC error:");
                  this.debug(error);
                  reject();
                }
              });
          }
        },
        ondata: (data: string) => {
          this.debug("Mesage Received on data");
          this.debug(data);
          if (this.janusConfig!.onData) this.janusConfig!.onData(data);
        },
        error: (err: string) => {
          this.debug(err);
          reject();
        }
      })
    })
  }
```
I think now you got the hang of it what is happening, right? Yes we are attaching a TextRoom plugin to the `janusInstance` set up the data channel with a "setup" request on success we create an answer and we are connected to everyone in the room ready to exchange messages.

###Conclusion
I hope you can now understand the basic working of Janus from this example. Janus is a really powerful library and becomes very simple if you get the hang of it. To wrap it up once again -
`create Janus instance -> attach plugin -> join room -> createOffer/createAnswer -> write callbacks as needed.`
That's it... Looking forward to seeing your video conferencing app in the future. And this was my first dev.to post so pardon me for any mistakes and hope you liked it😀.