Видеоконференция Jitsi

Похоже, эта ошибка указывает на то, что Jitsi в вашем браузере не имеет доступа к локальному хранилищу. Используете ли вы браузер с ограничительными настройками безопасности, которые могут блокировать использование локального хранилища фреймами (iframes)?

2 лайка

что-то «забавное» в моём консоли, также несколько ошибок, например:

Content Security Policy: настройки страницы заблокировали загрузку ресурса по адресу eval («script-src»). Источник: (function injected(eventName, injectedIntoContentWindow)
{

хотя всё работает :thinking:

вы можете попробовать любые из этих инстансов:
https://framatalk.org/accueil/fr/info/

1 лайк

Да — в Chrome у меня настроено запрещение сторонних файлов cookie. Отключение этой настройки решает проблему. Было бы неплохо иметь возможность получать уведомление о такой причине, но, полагаю, это довольно редкий случай.

Есть ли в Jitsi способ открыть видеозвонок в новой вкладке или отдельном всплывающем окне? Интересно, будут ли пользователи переключаться во время звонка, чтобы продолжать просматривать форум во время разговора или добавить ответ к теме.

2 лайка

Возможно, нет, так как это усложнит csp_extensions в about.json

1 лайк

Нет, текущая версия компонента темы уже добавляет путь к API Jitsi в источники CSP. Это сделано в этой строке файла about.json, как отметил @Benjamin_D:

1 лайк

@pmusaraj Возможно ли настроить этот новый шаг непосредственно в Discourse?

:smiley: да, я разбираюсь по ходу дела…
Я всё ещё не понимаю, почему эта функция: injected(eventName, injectedIntoContentWindow) и т.д. не проходит проверку CSP

консоль

Политика безопасности контента: настройки страницы заблокировали загрузку ресурса по адресу eval (“script-src”). Источник: (function injected(eventName, injectedIntoContentWindow)
{
let checkRequest;

/*

  • Обёртка для контекста фрейма
  • В некоторых крайних случаях Chrome не запускает скрипты контента внутри фреймов.
  • Сайты начали злоупотреблять этим фактом для доступа к API без обёрток через
  • contentWindow фрейма (#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);

}

/*

// Если мы были внедрены во фрейм через 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 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. Gracias.

Понял!
Это блокировщик рекламы, к Jitsi это не имеет никакого отношения… :sweat_smile:

1 лайк

@sunjam нового шага нет, компонент автоматически добавляет в белый список необходимый ему скрипт.

1 лайк

В настоящее время нет, но это было бы хорошим улучшением. Я займусь этим, как только появится возможность.

4 лайка

Очень круто! Несколько быстрых замечаний —

На мобильных устройствах после нажатия кнопки для начала события ссылка «Перейти в приложение» иногда неактивна! Она работает при просмотре напрямую в Safari, но не работает в Chrome и не работает в приложении Discourse Hub для iOS…

Поддерживаю эту идею. Быстрый способ автоматически сгенерировать ID комнаты был бы весьма полезен. Я думаю, особенно для тех, кто не очень хорошо знаком с Jitsi, не совсем понятно, нужно ли вводить существующую комнату в это поле или можно генерировать её на лету, используя любую произвольную случайную строку.

2 лайка

Отличное замечание! Это связано с тем, что ссылка «Перейти в приложение» выполняет запрос к пользовательскому URL, начинающемуся с org.jitsi.meet://. Я добавил этот пользовательский схему URL в белый список в DiscourseHub, что должно исправить проблему в следующем обновлении приложения. (К сожалению, я не думаю, что для этого есть исправление в 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 по умолчанию для мобильных устройств; не могли бы вы это изменить?

Я обнаружил в другом месте, что можно обойти страницу «Скачать приложение Jitsi», которая появляется на мобильных устройствах, добавив #config.disableDeepLinking=true к ID комнаты:
https://meet.jit.si/YourMeetingNameHere#config.disableDeepLinking=true

4 лайка

Только что попробовал добавить этот компонент темы к нашему экземпляру Discourse 2.7.0.beta3, но что бы я ни делал, я не вижу дополнительной иконки в панели композера для ссылки на идентификатор конференции.

В конфигурации компонента темы опция «Показывать в выпадающем списке параметров» отмечена, поэтому она должна быть видна. Есть ли какие-либо идеи, где можно поискать ошибку?

Вот что я вижу на свежем сайте Discourse с включённой этой опцией:

1 лайк

Да, проблема решена, возможно, это было связано с кэшированием.
Тем не менее, спасибо за ваш ответ.

1 лайк

Мне очень нравится, как работает этот компонент темы. В остальном всё должно было работать гладко, но я всё ещё борюсь с проблемами звука в Jitsi. Я хотел бы узнать, использует ли кто-нибудь сейчас Jitsi на своём форуме и не сталкивается ли с жалобами на него. Возможно, мои проблемы со звуком связаны с WebRTC — у кого-нибудь был похожий опыт?

1 лайк