Jitsi video conference component

That error seems to indicate that Jitsi in your browser does not have access to local storage. Are you using a browser with restrictive security settings that could block iframes from using local storage?

2 Likes

something “funny” in my console, a few errors too, such as :

Content Security Policy: The page’s settings blocked the loading of a resource at eval (“script-src”). Source: (function injected(eventName, injectedIntoContentWindow)
{

although it works :thinking:

you may try any of these instances :

1 Like

Yes - I have chrome set up to not allow third party cookies. Disabling that setting fixes this problem. Would be nice to have some way to be alerted to that cause, but I guess this is a rare enough issue.

Is there a way in jitsi to open a video call in a new tab or separate popup? I wonder if people will click away during the call to keep browsing in the forum while talking or to add a reply to the topic.

2 Likes

perhaps not, as it would complicate the csp_extensions in about.json

1 Like

No, the current version of the theme component already adds the Jitsi API path to the CSP sources. It’s done by this line of about.json as @Benjamin_D noted:

1 Like

@pmusaraj Is that newer step possible to set within Discourse itself?

:smiley: yes i’m figuring it out as I go…
I’m still wondering why this function: injected(eventName, injectedIntoContentWindow) etc… doesn’t pass the csp

console

Content Security Policy: The page’s settings blocked the loading of a resource at eval (“script-src”). Source: (function injected(eventName, injectedIntoContentWindow)
{
let checkRequest;

/*

  • Frame context wrapper
  • For some edge-cases Chrome will not run content scripts inside of frames.
  • Website have started to abuse this fact to access unwrapped APIs via a
  • frame’s contentWindow (#4586, 5207). Therefore until Chrome runs content
  • scripts consistently for all frames we must take care to (re)inject our
  • wrappers when the contentWindow is accessed.
    */
    let injectedToString = Function.prototype.toString.bind(injected);
    let injectedFrames = new WeakSet();
    let injectedFramesAdd = WeakSet.prototype.add.bind(injectedFrames);
    let injectedFramesHas = WeakSet.prototype.has.bind(injectedFrames);

function injectIntoContentWindow(contentWindow)
{
if (contentWindow && !injectedFramesHas(contentWindow))
{
injectedFramesAdd(contentWindow);
try
{
contentWindow[eventName] = checkRequest;
contentWindow.eval(
“(” + injectedToString() + “)(’” + eventName + “’, true);”
);
delete contentWindow[eventName];
}
catch (e) {}
}
}

for (let element of [HTMLFrameElement, HTMLIFrameElement, HTMLObjectElement])
{
let contentDocumentDesc = Object.getOwnPropertyDescriptor(
element.prototype, “contentDocument”
);
let contentWindowDesc = Object.getOwnPropertyDescriptor(
element.prototype, “contentWindow”
);

// Apparently in HTMLObjectElement.prototype.contentWindow does not exist
// in older versions of Chrome such as 51.
if (!contentWindowDesc)
  continue;

let getContentDocument = Function.prototype.call.bind(
  contentDocumentDesc.get
);
let getContentWindow = Function.prototype.call.bind(
  contentWindowDesc.get
);

contentWindowDesc.get = function()
{
  let contentWindow = getContentWindow(this);
  injectIntoContentWindow(contentWindow);
  return contentWindow;
};
contentDocumentDesc.get = function()
{
  injectIntoContentWindow(getContentWindow(this));
  return getContentDocument(this);
};
Object.defineProperty(element.prototype, "contentWindow",
                      contentWindowDesc);
Object.defineProperty(element.prototype, "contentDocument",
                      contentDocumentDesc);

}

/*

// If we’ve been injected into a frame via contentWindow then we can simply
// grab the copy of checkRequest left for us by the parent document. Otherwise
// we need to set it up now, along with the event handling functions.
if (injectedIntoContentWindow)
checkRequest = window[eventName];
else
{
let addEventListener = document.addEventListener.bind(document);
let dispatchEvent = document.dispatchEvent.bind(document);
let removeEventListener = document.removeEventListener.bind(document);
checkRequest = (url, callback) =>
{
let incomingEventName = eventName + “-” + url;

  function listener(event)
  {
    callback(event.detail);
    removeEventListener(incomingEventName, listener);
  }
  addEventListener(incomingEventName, listener);

  dispatchEvent(new RealCustomEvent(eventName, {detail: {url}}));
};

}

// Only to be called before the page’s code, not hardened.
function copyProperties(src, dest, properties)
{
for (let name of properties)
{
if (Object.prototype.hasOwnProperty.call(src, name))
{
Object.defineProperty(dest, name,
Object.getOwnPropertyDescriptor(src, name));
}
}
}

let RealRTCPeerConnection = window.RTCPeerConnection ||
window.webkitRTCPeerConnection;

// Firefox has the option (media.peerconnection.enabled) to disable WebRTC
// in which case RealRTCPeerConnection is undefined.
if (typeof RealRTCPeerConnection != “undefined”)
{
let closeRTCPeerConnection = Function.prototype.call.bind(
RealRTCPeerConnection.prototype.close
);
let RealArray = Array;
let RealString = String;
let {create: createObject, defineProperty} = Object;

let normalizeUrl = url =>
{
  if (typeof url != "undefined")
    return RealString(url);
};

let safeCopyArray = (originalArray, transform) =>
{
  if (originalArray == null || typeof originalArray != "object")
    return originalArray;

  let safeArray = RealArray(originalArray.length);
  for (let i = 0; i < safeArray.length; i++)
  {
    defineProperty(safeArray, i, {
      configurable: false, enumerable: false, writable: false,
      value: transform(originalArray[i])
    });
  }
  defineProperty(safeArray, "length", {
    configurable: false, enumerable: false, writable: false,
    value: safeArray.length
  });
  return safeArray;
};

// It would be much easier to use the .getConfiguration method to obtain
// the normalized and safe configuration from the RTCPeerConnection
// instance. Unfortunately its not implemented as of Chrome unstable 59.
// See https://www.chromestatus.com/feature/5271355306016768
let protectConfiguration = configuration =>
{
  if (configuration == null || typeof configuration != "object")
    return configuration;

  let iceServers = safeCopyArray(
    configuration.iceServers,
    iceServer =>
    {
      let {url, urls} = iceServer;

      // RTCPeerConnection doesn't iterate through pseudo Arrays of urls.
      if (typeof urls != "undefined" && !(urls instanceof RealArray))
        urls = [urls];

      return createObject(iceServer, {
        url: {
          configurable: false, enumerable: false, writable: false,
          value: normalizeUrl(url)
        },
        urls: {
          configurable: false, enumerable: false, writable: false,
          value: safeCopyArray(urls, normalizeUrl)
        }
      });
    }
  );

  return createObject(configuration, {
    iceServers: {
      configurable: false, enumerable: false, writable: false,
      value: iceServers
    }
  });
};

let checkUrl = (peerconnection, url) =>
{
  checkRequest(url, blocked =>
  {
    if (blocked)
    {
      // Calling .close() throws if already closed.
      try
      {
        closeRTCPeerConnection(peerconnection);
      }
      catch (e) {}
    }
  });
};

let checkConfiguration = (peerconnection, configuration) =>
{
  if (configuration && configuration.iceServers)
  {
    for (let i = 0; i < configuration.iceServers.length; i++)
    {
      let iceServer = configuration.iceServers[i];
      if (iceServer)
      {
        if (iceServer.url)
          checkUrl(peerconnection, iceServer.url);

        if (iceServer.urls)
        {
          for (let j = 0; j < iceServer.urls.length; j++)
            checkUrl(peerconnection, iceServer.urls[j]);
        }
      }
    }
  }
};

// Chrome unstable (tested with 59) has already implemented
// setConfiguration, so we need to wrap that if it exists too.
// https://www.chromestatus.com/feature/5596193748942848
if (RealRTCPeerConnection.prototype.setConfiguration)
{
  let realSetConfiguration = Function.prototype.call.bind(
    RealRTCPeerConnection.prototype.setConfiguration
  );

  RealRTCPeerConnection.prototype.setConfiguration = function(configuration)
  {
    configuration = protectConfiguration(configuration);

    // Call the real method first, so that validates the configuration for
    // us. Also we might as well since checkRequest is asynchronous anyway.
    realSetConfiguration(this, configuration);
    checkConfiguration(this, configuration);
  };
}

let WrappedRTCPeerConnection = function(...args)
{
  if (!(this instanceof WrappedRTCPeerConnection))
    return RealRTCPeerConnection();

  let configuration = protectConfiguration(args[0]);

  // Since the old webkitRTCPeerConnection constructor takes an optional
  // second argument we need to take care to pass that through. Necessary
  // for older versions of Chrome such as 51.
  let constraints = undefined;
  if (args.length > 1)
    constraints = args[1];

  let peerconnection = new RealRTCPeerConnection(configuration,
                                                 constraints);
  checkConfiguration(peerconnection, configuration);
  return peerconnection;
};

WrappedRTCPeerConnection.prototype = RealRTCPeerConnection.prototype;

let boundWrappedRTCPeerConnection = WrappedRTCPeerConnection.bind();
copyProperties(RealRTCPeerConnection, boundWrappedRTCPeerConnection,
               ["generateCertificate", "name", "prototype"]);
RealRTCPeerConnection.prototype.constructor = boundWrappedRTCPeerConnection;

if ("RTCPeerConnection" in window)
  window.RTCPeerConnection = boundWrappedRTCPeerConnection;
if ("webkitRTCPeerConnection" in window)
  window.webkitRTCPeerConnection = boundWrappedRTCPeerConnection;

}
})(‘abp-request-o6i81ij12x’, true);. bd833f87-4c58-41b0-a0cd-15b978834599:27:22

Did you enable your content security policy?
Also, please dm me how you added that slick console drop down. Gracias.

Got it !
that’s adblock, nothing to do with jitsi… :sweat_smile:

1 Like

@sunjam there is no new step, the component automatically whitelists the script it needs to whitelist.

1 Like

Currently no, but this would be a good improvement, I’ll work on it when I get a chance.

4 Likes

Very cool! Couple quick comments—

For mobile, after clicking the button to start the event, the “Continue to the app” link is sometimes non-clickable! It works when viewing directly in Safari, but not in Chrome, and not in the Discourse Hub iOS app…

Seconding this as well, a quick way to auto-generate a room ID could be quite useful. I think particularly for anyone not already super familiar with Jitsi, it’s not totally clear if you need to enter an existing room in that field, or if you’re generating one on the fly just by using any arbitrary random string.

1 Like

Nice catch! This is because the “Continue to the app” link makes a custom URL request that starts with org.jitsi.meet://. I have whitelisted that custom query scheme in DiscourseHub, which should fix the issue there on the next app release. (I don’t think there is a fix for this in Chrome, alas.)

I have added a randomly-generated option, users just need to leave the ID field empty, and an ID will be generated. I also added some copy to explain this:

Note that the ID won’t have words, it will be a random mix of letters and numbers.

4 Likes

Any chance you could prioritise this? We’ve been playing with using Jitsi for our internal chats.

The problem is this:

  1. we start a thread as an Event using the Calendar plugin, and include a Jitsi call to go at that time. Sometimes do this after a Doodle-esque poll in the same thread. The Jitsi link goes in the OP.
  2. The call launches, all good.
  3. Someone uses that same tab to search for something, or reply in the thread (for example to write minutes on the fly) - and they crash out of the call.

It shouldn’t be too hard to make the link go to Blank rather than being in an iframe, eh? Not that I have the skills!

1 Like

Well, @nathankershaw this component is a bit of overkill if you want just a link to a jitsi room. You can add https://meet.jit.si/ROOMID to a link, and that should do the job without a fancy-shmancy theme component.

However, I did add this option to the component, you can now choose whether you want the iframed for mobile or desktop or both:

By default, the video conference is loaded in an iframe. When unchecked, it will go to the video call in the full window. With the BigBlueButton video conference this was desirable especially on mobile, so I’ve done the same here.

Note also that the link to the full window Jitsi room does not open in a new tab. If you want that, use an anchor with target="_blank".

5 Likes