Jitsi IFrame in Use - A Shody Guide to Embeding Jitsi into Your Website
A guide to using Jitsi external API and managing its quirks.
What even is the external API?
It’s a small little API that turns a Jitsi meeting into an iFrame element, so you can use it in the html. It is the “go to” way of embeding Jitsi into your website and making it look like that was your plan all along. Mustache twirling will be examined in another article.
With the Jitsi iFrame API, you can include the meeting in your design like you would any iFrame component.
What is the guide about?
Chance would have it, recently I’ve got a request from a customer to design a Jitsi UI with rather distinct specifications. I’ve got to say, it is a nifty tool, but it has some shortcomings and oddities in the strangest of places. Mid-way through the project, I’ve decided to put together all the hurdles I’ve had to jump over into a neat little article. Now, without all the trademarked bells and whistles, the result looks startlingly close to a carrot you’ve purchased and forgot in the refrigerator. It is, to put it eloquently, veritably ugly. But it works without a hitch, and you can scavenge what wisdom you want without fear and flex your designer muscles on it. Go you.
Getting started with this Guide
You can find the entirety of the code on GitHub. I’ve tried to write it as openly as I could. If you’re in a hurry, you can download the html file and be done. But I would still read the rest, just in case.
I’ll explain how or why something works, something doesn’t and what can be done about it piece by piece.
Importing the script
All the scripting here takes place after the tag, but before, obviously, the tag. You should start by importing the external api script into the page;
<script src="https://your.jitsiserver.com/external_api.js"></script>
Once you are done, the rest of the API is available for your use.
Defining some constants
<script src="https://your.jitsiserver.com/external_api.js"></script> <script> const domain = "your.jitsiserver.com"; const options = { roomName: "ROOM_NAME", width: 800, height: 480, parentNode: document.querySelector('#ELEMENT_ID'),
configOverwrite: {},
interfaceConfigOverwrite: { TOOLBAR_BUTTONS: [ ] },
jwt: 'yourtokenhere' }; var isSteamOn = false;
What is going on in here?
The “domain” constant keeps track of which jitsi installation to stream from. That one is easy.
The options object however, contains some not-so-human-friendly properties. Let’s explain each of them, one by one.
roomName is the name of the room that this iframe joins.
width is the width of the iframe, while similarly height is its height. Dazzling.
parentNode is the bit that decides where to show the iframe. You can create an empty <div>, give it an ID, and pass that ID in here.
configOverwrite and interfaceConfigOverwrite can be used to overwrite the settings contained in the config and interface_config files, respectively. You can hide buttons, enable a prejoin screen and anything else you can normally do. For detailed information, take a look at the official guide.
jwt is the jwt token you pass for authorization. If you are using jitsi-token-moderation be careful that the token you pass has the ability to start a recording.
Okay. What's next?
const api = new JitsiMeetExternalAPI(domain, options);
At this point, it should work. However, in this configuration (see interfaceConfigOverwrite above), it displays no buttons on the video feed. Let’s add a few.
Buttons go in the <body> of your document. They are bogstandard html buttons. Here’s a few I’ve made.
<button onclick="api.executeCommand('toggleAudio')">Mute/Unmute Mic</button> <button onclick="api.executeCommand('muteEveryone')">Mute All</button> <button onclick="api.executeCommand('toggleVideo')">Stop/Start Cam</button> <button onclick="alert('This one is tricky and should be customized according to need')">Cam/Mic</button> <button onclick="api.executeCommand('toggleShareScreen')">Share Screen</button> <button id="stream-btn" onclick="streamHandler()">Start Stream</button>
Almost all of these commands are available to you out-of-the-box. You can see the whole list in the official guide. However, the last one, the streamHandler, I wrote myself. It changes between Start Stream and Stop Stream accordingly. For that, we’re going to need a few things.
Starting the stream
function streamHandler() {
try {
if (!isStreamOn) {
document.getElementById("streamingResponseMsg").innerHTML = "Starting streaming...";
//The function below starts the stream or recording, according to its "mode"
api.executeCommand('startRecording', {
mode: 'stream', //recording mode, either `file` or `stream`.
rtmpStreamKey: '', //This where you *should* put your favoured rtmp stream server along with your key, like "rtmp:\/\/some.address/norecord/stream-key"
youtubeStreamKey: 'rtmp:\/\/some.address/norecord/stream-key', //the youtube stream key.
});
} else {
document.getElementById("streamingResponseMsg").innerHTML = "Stopping streaming...";
//The function below stops the stream or recording, according to the string you pass. Official guide shows an object, while it should be a string
api.executeCommand('stopRecording', 'stream');
}
}
catch (e){
if (isStreamOn){
document.getElementById("streamingResponseMsg").innerHTML = "Error while stopping stream.";
console.log("Exception while stopping stream.", e);
}else{
document.getElementById("streamingResponseMsg").innerHTML = "Error while starting stream.";
console.log("Exception while starting stream.", e);
}
this.isStreamOn = false;
}
};
There are a few things I’d like to talk about that piece of code.
Firstly, notice how the rtmpStreamKey is an empty string, while the youtubeStreamKey is a generic rtmp key. The latest version of the iFrame supports the use of a custom rtmp in the “normal” way. However, the latest stable branch, at the time of this article, does not. This is a simple work around. You should try first to see if you can make it work.
Secondly, the startRecording function accepts the mode as an object, while the stopRecording function requires a string. The official guide misleads you on this, possibly because they are intending to fix it.
Thirdly, notice I don’t change what my buttons do in this step, and leave the value of isStreamOn well enough alone. That is done in the next step.
Recording the state of the stream
api.addEventListener("recordingStarted", () => {
document.getElementById("stream-btn").innerHTML="Stop Streaming";
document.getElementById("streamingResponseMsg").innerHTML = "Stream is on";
this.isStreamOn = true;
console.log("Example Stream On", this.isStreamOn);
});
api.addEventListener("recordingStopped", () => {
document.getElementById("stream-btn").innerHTML="Start Streaming";
document.getElementById("streamingResponseMsg").innerHTML = "Stream is off";
console.log("Example Stream Off", this.isStreamOn);
this.isStreamOn = false;
});
Alright. I got it. But what's this bit?
api.addEventListener(`videoConferenceJoined`, () => {
const listener = ({ enabled }) => {
api.removeEventListener(`tileViewChanged`, listener);
if (!enabled) {
api.executeCommand(`toggleTileView`);
}
};
});