Jitsi 视频会议

该错误似乎表明您浏览器中的 Jitsi 无法访问本地存储。您是否使用了具有严格安全设置的浏览器,该设置可能阻止 iframe 使用本地存储?

2 个赞

我的控制台里出现了一些“有趣”的内容,还有一些错误,例如:

内容安全策略:页面设置阻止了在 eval 处加载资源(“script-src”)。来源:(function injected(eventName, injectedIntoContentWindow)
{

尽管它仍然能运行::thinking:

你可以尝试以下任意一个实例:
https://framatalk.org/accueil/fr/info/

1 个赞

是的——我将 Chrome 设置为不允许第三方 Cookie。禁用该设置即可解决此问题。如果能有一种方式提醒我们该原因就好了,不过我想这确实是个罕见的问题。

Jitsi 中是否有办法在新标签页或独立弹出窗口中打开视频通话?我担心用户在通话过程中可能会点击离开,以便在论坛中继续浏览或添加对主题的回复。

2 个赞

也许并非如此,因为这会使 about.json 中的 csp_extensions 变得复杂。

1 个赞

不,当前版本的主题组件已经将 Jitsi API 路径添加到了 CSP 源中。正如 @Benjamin_D 指出的,这是通过 about.json 中的这一行实现的:

1 个赞

@pmusaraj 这个较新的步骤可以在 Discourse 内部设置吗?

:smiley: 是的,我正在边做边摸索……
我仍然想知道为什么这个函数:injected(eventName, injectedIntoContentWindow) 等等……无法通过 CSP(内容安全策略)。

控制台

内容安全策略:页面的设置阻止了在 eval 处加载资源(“script-src”)。来源:(function injected(eventName, injectedIntoContentWindow)
{
let checkRequest;

/*

  • 帧上下文包装器
  • 在某些边缘情况下,Chrome 不会在帧内运行内容脚本。
  • 一些网站开始利用这一事实,通过帧的 contentWindow 访问未包装的 API(#4586, 5207)。
  • 因此,在 Chrome 能够一致地为所有帧运行内容脚本之前,我们必须确保在访问 contentWindow 时重新注入我们的包装器。
    */
    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”
);

// 显然,在旧版本的 Chrome(如 51)中,HTMLObjectElement.prototype.contentWindow 不存在。
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);

}

/*

// 如果我们是经由 contentWindow 注入到帧中的,那么我们可以直接获取父文档留给我们的 checkRequest 副本。
// 否则,我们需要现在就设置它,以及事件处理函数。
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}}));
};

}

// 仅应在页面代码之前调用,未进行强化处理。
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 有一个选项(media.peerconnection.enabled)可以禁用 WebRTC,
// 在这种情况下 RealRTCPeerConnection 为 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;
};

// 使用 .getConfiguration 方法从 RTCPeerConnection 实例获取标准化且安全的配置会容易得多。
// 不幸的是,截至 Chrome 不稳定版 59,该方法尚未实现。
// 参见 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 不会迭代 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)
    {
      // 如果已经关闭,调用 .close() 会抛出错误。
      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 不稳定版(使用 59 测试)已经实现了 setConfiguration,
// 因此如果该方法存在,我们也需要对其进行包装。
// 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);

    // 首先调用真实方法,以便为我们验证配置。
    // 此外,由于 checkRequest 本身就是异步的,这样做也无妨。
    realSetConfiguration(this, configuration);
    checkConfiguration(this, configuration);
  };
}

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

  let configuration = protectConfiguration(args[0]);

  // 由于旧的 webkitRTCPeerConnection 构造函数接受一个可选的第二个参数,
  // 我们需要确保将其传递过去。这对于旧版本的 Chrome(如 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

你启用了内容安全策略吗?
另外,请私信告诉我你是如何添加那个酷炫的 console 下拉菜单的。谢谢。

明白了!
那是广告拦截器的问题,与 Jitsi 无关……:sweat_smile:

1 个赞

@sunjam 没有新的步骤,该组件会自动将其需要白名单的脚本加入白名单。

1 个赞

目前还没有,但这将是一个很好的改进,我一有机会就会着手处理。

4 个赞

太棒了!有两个小建议——

在移动端,点击按钮开始活动后,“继续进入应用”的链接有时无法点击!在 Safari 中直接查看时正常,但在 Chrome 中不行,在 Discourse Hub iOS 应用中也不行……

我也附议这一点,提供一个快速自动生成房间 ID 的方式会非常实用。我认为对于还不熟悉 Jitsi 的用户来说,目前并不完全清楚在该字段中是需要输入一个已存在的房间,还是可以直接使用任意随机字符串来即时生成一个新房间。

2 个赞

发现得很及时!这是因为“继续进入应用”的链接会发起一个以 org.jitsi.meet:// 开头的自定义 URL 请求。我已在 DiscourseHub 中将该自定义 URL 方案加入白名单,这将在下一次应用发布时解决该问题。(遗憾的是,Chrome 中似乎没有解决办法。)

我已添加了一个随机生成的选项,用户只需留空 ID 字段,系统便会自动生成一个 ID。我还添加了一些说明文字:

请注意,该 ID 不会包含单词,而是字母和数字的随机组合。

4 个赞

请问您能优先考虑一下这个需求吗?我们一直在尝试在内部聊天中使用 Jitsi。

问题如下:

  1. 我们使用日历插件将某个帖子创建为“事件”,并在其中加入一个届时发起的 Jitsi 通话链接。有时我们会在同一个帖子中先进行 类似 Doodle 的投票 后再这样做。Jitsi 链接会放在首帖中。
  2. 通话启动,一切正常。
  3. 有人用同一个标签页去搜索内容,或在帖子中回复(例如实时撰写会议纪要)——结果导致通话中断。

让链接直接在新标签页打开而不是嵌入在 iframe 中,应该不难吧?虽然我并没有这方面的技术能力!

2 个赞

好吧,@nathank,如果你只是想要一个指向 Jitsi 房间的链接,这个组件有点大材小用了。你只需将 https://meet.jit.si/ROOMID 添加到链接中即可,无需使用什么花哨的主题组件。

不过,我确实为这个组件添加了该选项,你现在可以选择是否要在移动设备、桌面设备或两者上以 iframe 形式嵌入:

默认情况下,视频会议会在 iframe 中加载。如果取消勾选,则会在新窗口中直接打开视频通话。在 BigBlueButton 视频会议 中,这种做法在移动设备上尤其有用,因此我也在这里做了同样的处理。

另外请注意,指向完整窗口 Jitsi 房间的链接不会在新标签页中打开。如果你希望它在新标签页中打开,请使用带有 target="_blank" 属性的锚点链接。

8 个赞

对我来说没问题,但对于不太懂技术的用户来说就不太友好了!感谢你对 iframe 功能的增强,非常实用。我不确定将 iframe 设为移动端的默认选项是否合理,你是否考虑过更改这一点?

我在其他地方发现,通过在房间 ID 后添加 #config.disableDeepLinking=true,可以绕过移动端出现的“下载 Jitsi 应用”页面:
https://meet.jit.si/YourMeetingNameHere#config.disableDeepLinking=true

4 个赞

我刚刚尝试将这个主题组件添加到我们的 Discourse 2.7.0.beta3 实例中,但无论我怎么做,都无法在编辑器工具栏中看到用于链接到会议 ID 的额外图标。

主题组件配置中已勾选“在下拉选项中显示”,因此它应该是可见的。请问我该从哪里排查错误?

这是我在一个全新的 Discourse 站点上勾选该选项后看到的内容:

1 个赞

是的,问题已解决,可能是缓存问题。不过还是感谢你的回复。

1 个赞

我真的很喜欢这个主题组件的工作方式。我仍在处理 Jitsi 音频问题,否则一切都会顺利进行。我想知道是否有人目前在他们的论坛中使用 Jitsi 并且没有抱怨。我的音频问题可能是由于 WebRTC ---- 还有人有类似的经历吗?

1 个赞