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...
- 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.
- Now let's create a file called janusAdapter.ts and import Janus into it.
import Janus from "janus"; // from janus.js
- 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_;
}
Note: You can use a constructor to initialize the variables.
- We will now define the first utility function
init()
to get a Janus instance and store it tojanusInstance
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();
},
});
},
});
});
}
- 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();
},
});
});
}
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.
- 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);
},
});
});
}
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.
- 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();
}
})
})
}
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)
Hey, I just curious, is this will be come one page between videoroom and chat room? Thanks.
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.
Can you give link to github example pls?
can you give me GitHub repo please ?