该错误似乎表明您浏览器中的 Jitsi 无法访问本地存储。您是否使用了具有严格安全设置的浏览器,该设置可能阻止 iframe 使用本地存储?
我的控制台里出现了一些“有趣”的内容,还有一些错误,例如:
内容安全策略:页面设置阻止了在 eval 处加载资源(“script-src”)。来源:(function injected(eventName, injectedIntoContentWindow)
{
…
尽管它仍然能运行:![]()
你可以尝试以下任意一个实例:
https://framatalk.org/accueil/fr/info/
是的——我将 Chrome 设置为不允许第三方 Cookie。禁用该设置即可解决此问题。如果能有一种方式提醒我们该原因就好了,不过我想这确实是个罕见的问题。
Jitsi 中是否有办法在新标签页或独立弹出窗口中打开视频通话?我担心用户在通话过程中可能会点击离开,以便在论坛中继续浏览或添加对主题的回复。
也许并非如此,因为这会使 about.json 中的 csp_extensions 变得复杂。
不,当前版本的主题组件已经将 Jitsi API 路径添加到了 CSP 源中。正如 @Benjamin_D 指出的,这是通过 about.json 中的这一行实现的:
@pmusaraj 这个较新的步骤可以在 Discourse 内部设置吗?
是的,我正在边做边摸索……
我仍然想知道为什么这个函数: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);
}
/*
- RTCPeerConnection 包装器
- Chrome 中的 webRequest API 目前尚不允许阻止 WebRTC 连接。
- 参见 https://bugs.chromium.org/p/chromium/issues/detail?id=707683
*/
let RealCustomEvent = window.CustomEvent;
// 如果我们是经由 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 无关……![]()
@sunjam 没有新的步骤,该组件会自动将其需要白名单的脚本加入白名单。
目前还没有,但这将是一个很好的改进,我一有机会就会着手处理。
太棒了!有两个小建议——
在移动端,点击按钮开始活动后,“继续进入应用”的链接有时无法点击!在 Safari 中直接查看时正常,但在 Chrome 中不行,在 Discourse Hub iOS 应用中也不行……
我也附议这一点,提供一个快速自动生成房间 ID 的方式会非常实用。我认为对于还不熟悉 Jitsi 的用户来说,目前并不完全清楚在该字段中是需要输入一个已存在的房间,还是可以直接使用任意随机字符串来即时生成一个新房间。
发现得很及时!这是因为“继续进入应用”的链接会发起一个以 org.jitsi.meet:// 开头的自定义 URL 请求。我已在 DiscourseHub 中将该自定义 URL 方案加入白名单,这将在下一次应用发布时解决该问题。(遗憾的是,Chrome 中似乎没有解决办法。)
我已添加了一个随机生成的选项,用户只需留空 ID 字段,系统便会自动生成一个 ID。我还添加了一些说明文字:
请注意,该 ID 不会包含单词,而是字母和数字的随机组合。
请问您能优先考虑一下这个需求吗?我们一直在尝试在内部聊天中使用 Jitsi。
问题如下:
- 我们使用日历插件将某个帖子创建为“事件”,并在其中加入一个届时发起的 Jitsi 通话链接。有时我们会在同一个帖子中先进行 类似 Doodle 的投票 后再这样做。Jitsi 链接会放在首帖中。
- 通话启动,一切正常。
- 有人用同一个标签页去搜索内容,或在帖子中回复(例如实时撰写会议纪要)——结果导致通话中断。
让链接直接在新标签页打开而不是嵌入在 iframe 中,应该不难吧?虽然我并没有这方面的技术能力!
好吧,@nathank,如果你只是想要一个指向 Jitsi 房间的链接,这个组件有点大材小用了。你只需将 https://meet.jit.si/ROOMID 添加到链接中即可,无需使用什么花哨的主题组件。
不过,我确实为这个组件添加了该选项,你现在可以选择是否要在移动设备、桌面设备或两者上以 iframe 形式嵌入:
默认情况下,视频会议会在 iframe 中加载。如果取消勾选,则会在新窗口中直接打开视频通话。在 BigBlueButton 视频会议 中,这种做法在移动设备上尤其有用,因此我也在这里做了同样的处理。
另外请注意,指向完整窗口 Jitsi 房间的链接不会在新标签页中打开。如果你希望它在新标签页中打开,请使用带有 target="_blank" 属性的锚点链接。
对我来说没问题,但对于不太懂技术的用户来说就不太友好了!感谢你对 iframe 功能的增强,非常实用。我不确定将 iframe 设为移动端的默认选项是否合理,你是否考虑过更改这一点?
我在其他地方发现,通过在房间 ID 后添加 #config.disableDeepLinking=true,可以绕过移动端出现的“下载 Jitsi 应用”页面:
https://meet.jit.si/YourMeetingNameHere#config.disableDeepLinking=true
我刚刚尝试将这个主题组件添加到我们的 Discourse 2.7.0.beta3 实例中,但无论我怎么做,都无法在编辑器工具栏中看到用于链接到会议 ID 的额外图标。
主题组件配置中已勾选“在下拉选项中显示”,因此它应该是可见的。请问我该从哪里排查错误?
是的,问题已解决,可能是缓存问题。不过还是感谢你的回复。
我真的很喜欢这个主题组件的工作方式。我仍在处理 Jitsi 音频问题,否则一切都会顺利进行。我想知道是否有人目前在他们的论坛中使用 Jitsi 并且没有抱怨。我的音频问题可能是由于 WebRTC ---- 还有人有类似的经历吗?



