DEV Community

Sayan Bhattacharya
Sayan Bhattacharya

Posted on

Janus Video Conferencing

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. 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. For Angular and Vue developers I am sure there is some other way.
  2. Now let's create a file called janusAdapter.ts and import Janus into it.
import Janus from "janus"; // from janus.js
Enter fullscreen mode Exit fullscreen mode
  1. Now we need to declare the JanusAdapter class and initialize the variables we will be needing.
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_;
}
Enter fullscreen mode Exit fullscreen mode

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.
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();
            },
          });
        },
      });
    });
  }
Enter fullscreen mode Exit fullscreen mode
  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 -
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();
        },
      });
    });
  }
Enter fullscreen mode Exit fullscreen mode

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.
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);
        },
      });
    });
  }
Enter fullscreen mode Exit fullscreen mode

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.

  1. The TextRoom part is left which is also similar to the above methods.
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();
        }
      })
    })
  }
Enter fullscreen mode Exit fullscreen mode

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๐Ÿ˜€.

Top comments (4)

Collapse
 
novienzi profile image
Novienzi

Hey, I just curious, is this will be come one page between videoroom and chat room? Thanks.

Collapse
 
thunder80 profile image
Sayan Bhattacharya

Sorry for the late reply, the videoroom, and the chatroom can be subscribed from separate pages, so if you want to chat and video calling on different pages you can call the respective methods on the required pages. Hope this answers your question.

Collapse
 
dimadmytruk23 profile image
dima-dmytruk23

Can you give link to github example pls?

Collapse
 
kaushal_gohil profile image
kaushal gohil

can you give me GitHub repo please ?