해당 오류는 브라우저의 Jitsi가 로컬 스토리지에 접근할 수 없음을 시사합니다. iframe이 로컬 스토리지를 사용하는 것을 차단할 수 있는 제한적인 보안 설정이 적용된 브라우저를 사용 중입니까?
콘솔에 뭔가 “이상한” 게 있고, 몇 가지 오류도 있습니다. 예를 들어:
Content Security Policy: 페이지 설정이 eval(“script-src”)의 리소스 로딩을 차단했습니다. 소스: (function injected(eventName, injectedIntoContentWindow)
{
…
그런데도 작동은 합니다 ![]()
아래 인스턴스 중 하나를 시도해 볼 수 있습니다:
https://framatalk.org/accueil/fr/info/
네 - Chrome에서 제3자 쿠키를 허용하지 않도록 설정되어 있습니다. 해당 설정을 비활성화하면 문제가 해결됩니다. 원인이 무엇인지 알리는 알림 기능이 있으면 좋겠지만, 이 문제는 비교적 드문 경우가 아니므로 그렇다고 생각합니다.
Jitsi에서 새 탭이나 별도의 팝업으로 영상 통화를 여는 방법이 있을까요? 통화 중에도 포럼을 계속 둘러보거나 주제에 답글을 남기려고 자리를 뜨는 사람이 있을지 궁금합니다.
아마도 아닐 것입니다. 그렇게 하면 about.json의 csp_extensions가 복잡해지거든요.
아니요, 현재 버전의 테마 컴포넌트는 이미 Jitsi API 경로를 CSP 소스에 추가하고 있습니다. @Benjamin_D가 지적한 대로, 이는 about.json의 이 줄에서 수행됩니다:
@pmusaraj 그 새로운 단계를 Discourse 자체에서 설정할 수 있나요?
네, 진행하면서 하나씩 파악하고 있어요…
injected(eventName, injectedIntoContentWindow) 같은 이 함수가 왜 CSP를 통과하지 못하는지 계속 궁금하네요.
console
Content Security Policy: 페이지 설정이 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”
);
// HTMLObjectElement.prototype.contentWindow는 Chrome 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);
}
/*
- 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는 WebRTC를 비활성화하는 옵션(media.peerconnection.enabled)이 있으며,
// 이 경우 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;
};
// RTCPeerConnection 인스턴스에서 정규화되고 안전한 구성을 가져오려면
// .getConfiguration 메서드를 사용하는 것이 훨씬 쉬울 것입니다.
// 불행히도 Chrome unstable 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 unstable(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 드롭다운을 어떻게 추가하셨는지 DM으로 알려주세요. 감사합니다.
알겠습니다!
아블록 때문이네요. jitsi와는 전혀 관련이 없군요… ![]()
@sunjam 새로운 단계는 없으며, 해당 컴포넌트는 화이트리스트에 추가해야 할 스크립트를 자동으로 화이트리스트에 추가합니다.
현재는 불가능하지만, 좋은 개선 사항이므로 기회가 되면 작업하겠습니다.
정말 멋지네요! 몇 가지 간단한 코멘트를 남길게요.
모바일에서 이벤트를 시작하는 버튼을 클릭한 후, “앱으로 계속” 링크가 클릭되지 않는 경우가 있습니다! Safari에서 직접 열면 작동하지만, Chrome에서는 작동하지 않으며, Discourse Hub iOS 앱에서도 작동하지 않습니다…
이 의견에 동의합니다. 방 ID를 자동으로 생성하는 간단한 방법은 꽤 유용할 수 있습니다. 특히 Jitsi에 이미 익숙하지 않은 사용자의 경우, 해당 필드에 기존 방을 입력해야 하는지, 아니면 아무런 임의의 문자열을 사용해서 방을 즉석에서 생성하는 것인지가 명확하지 않은 경우가 많습니다.
좋은 지적입니다! 이는 “앱으로 계속” 링크가 org.jitsi.meet://로 시작하는 커스텀 URL 요청을 수행하기 때문입니다. DiscourseHub에서 해당 커스텀 쿼리 스킴을 화이트리스트에 추가했으며, 다음 앱 릴리스 시 해당 문제가 수정될 것입니다. (불행히도 Chrome에 대한 수정 방법은 없는 것으로 보입니다.)
무작위로 생성되는 옵션을 추가했습니다. 사용자는 ID 필드를 비워 두기만 하면 ID가 생성됩니다. 이를 설명하는 문구도 추가했습니다:
ID에는 단어가 포함되지 않으며, 문자와 숫자의 무작위 조합이 됩니다.
이 부분을 우선순위로 다룰 수 있을까요? 내부 채팅에 Jitsi를 사용해보고 있습니다.
문제는 다음과 같습니다:
- 캘린더 플러그인을 사용하여 이벤트를 스레드로 시작하고, 해당 시간에 Jitsi 통화를 포함합니다. 때로는 같은 스레드에서 Doodle 유사한 투표를 진행한 후 이렇게 하기도 합니다. Jitsi 링크는 OP(첫 번째 게시글)에 삽입됩니다.
- 통화가 시작되고, 모든 것이 정상입니다.
- 누군가 같은 탭에서 무언가를 검색하거나 스레드에 답글을 남기면(예: 회의록을 실시간으로 작성하는 경우) 통화에서 강제로 나갑니다.
링크가 iframe 대신 새 탭(Blank)으로 열리도록 만드는 것은 그렇게 어렵지 않을 것 같은데, 어때요? 물론 저에게는 그 기술이 없긴 합니다만!
음, @nathank 님, 단순히 Jitsi 방 링크만 필요하다면 이 컴포넌트는 좀 과할 수 있습니다. 링크에 https://meet.jit.si/ROOMID를 추가하면 화려한 테마 컴포넌트 없이도 충분합니다.
다만, 해당 옵션을 컴포넌트에 추가해 두었으니 이제 모바일용, 데스크톱용, 또는 둘 다 iframe으로 표시할지 선택할 수 있습니다:
기본적으로 화상 회의는 iframe으로 로드됩니다. 체크를 해제하면 전체 창으로 화상 통화를 열립니다. BigBlueButton 화상 회의의 경우 특히 모바일에서 이것이 바람직했기 때문에 여기에서도 동일하게 처리했습니다.
참고로, 전체 창 Jitsi 방 링크는 새 탭에서 열리지 않습니다. 새 탭에서 열려면 target="_blank" 속성을 가진 앵커를 사용하세요.
저에게는 괜찮지만, 기술에 익숙하지 않은 사용자들에게는 그렇지 않네요! iframe 기능 개선에 감사드립니다. 정말 유용합니다. 모바일에서 iframe을 기본값으로 설정하는 것이 적절한지 확실하지 않습니다. 이 부분을 변경해 주실 수 있을까요?
다른 곳에서 모바일에서 나타나는 ‘Jitsi 앱 다운로드’ 페이지를 우회하는 방법을 발견했습니다. 회의 ID에 #config.disableDeepLinking=true를 추가하면 됩니다:
https://meet.jit.si/YourMeetingNameHere#config.disableDeepLinking=true
방금 이 테마 컴포넌트를 discourse 2.7.0.beta3 인스턴스에 추가해 보았는데, 어떻게 해도 회의 ID를 링크할 수 있는 아이콘이 작성자 바(composer bar)에 나타나지 않습니다.
테마 컴포넌트 설정에서 “옵션 드롭다운에 표시” 항목이 체크되어 있으므로 표시되어야 하는데, 오류를 어디에서 찾아볼 수 있는지 아이디어가 있을까요?
네, 문제가 이제 해결됐어요. 캐시 문제였던 것 같네요.
그래도 답변 주셔서 감사합니다.
이 테마 컴포넌트가 어떻게 작동하는지 정말 마음에 듭니다. 다만 Jitsi 오디오 문제 때문에 여전히 고생 중이라, 이 문제를 해결할 수만 있다면 모든 것이 원활하게 돌아갔을 텐데 아쉽습니다. 혹시 현재 포럼에서 Jitsi를 사용하면서 불만이 전혀 없는 분이 계신지 궁금합니다. 제 오디오 문제는 WebRTC 때문일 수도 있는데, 비슷한 경험을 하신 분이 있으신가요?



