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?

Do you want to embed Jitsi into your website page, and have it run through a UI of its own? Do you want the meeting function to be a small, secondary tidbit and have the rest of your website take the main stage, or, well, exist, at all? Meet Jitsi IFrame 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 embed a Jitsi meeting into your website design like you would any other IFrame.  

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

After importing the script, open a new <script> tag and define two constants. The API’s function will use them as parameters later. Your code should look like this;
<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;
  Now, you can see, I also have another variable here called “isStreamOn”. I use it to record the state of the stream, so I can change how my buttons work according to that. It’s not necessary, but go ahead and add it, if you want.

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?

Now that we’ve defined the parameters of our iFrame, let’s go ahead and actually construct it;
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

We’re once again in the <script> section of our page. We have to write a wrapper function for our streamHandling button.
 
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

To prevent unsavoury bugs, we should check first if the stream has actually started. Unfortunately, the iFrame API does not have a listener for that specific situation. You can find a solution here. Currently, it has been committed, but not yet approved and integrated into the API. After following the changes they’ve made and adding an event listener, you can use it in the following fashion;
 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?

Oh. You must be referring to this monstrosity;
api.addEventListener(`videoConferenceJoined`, () => {

        const listener = ({ enabled }) => {

            api.removeEventListener(`tileViewChanged`, listener);

            if (!enabled) {

                api.executeCommand(`toggleTileView`);

            }

        };

    });
The customer in mention wanted the Tile View to be the default view. There is an onload property you can pass in the options object. I first used that to run api.executeCommand(‘toggleTileView’). Unfortunately, it did not work for me. So I wrote this thing. It listens for anyone joining the room, and upon catching one, enables the Tile View. Talk about jank.

Too Long, Didn't Read

The code in the GitHub largely works. But you have to make some changes to Jitsi so you can catch when a stream starts and stops. Once again, you can find those here. If you have issues with rtmp key, that is probably because the workaround I’ve used is now deprecated. You can find the shiny new way of doing things in the official guide.
Now you have a, hopefully shiny, new Jitsi interface embeded in your website!
And if you need support for Jitsi do not hesitate to WhatsApp us. We are giving professional grade Jitsi consultation service including installation, integration, customisation and maintenance support. 
For your questions and comments please contribute below.

Leave a Comment

Your email address will not be published. Required fields are marked *