en

Виджет

Виджет использует именованные шаблоны: например, для разных сцен OBS. Автор разрабатывает интерфейс настроек и само отображение виджета. Кабинет управляет шаблонами и умеет получать и сохранять их данные через обмен сообщениями с окном виджета.

Готовый проект с настройками, страницей OBS и подписками на события разобран в примере счётчика зрителей. Для нового интерфейса React доступен UI Kit; пример счётчика использует его предыдущую версию.

Настроить приложение

Создайте приложение типа Виджет и выберите нужные разрешения. Откройте Настройки, задайте URL Widget и сохраните. Для отдельной среды есть адрес develop и режим разработки. Заполните информацию и выдайте доступ учебному проекту.

Виджет. 1. задайте URL Widget

Создать шаблон

В проекте установите приложение из каталога и нажмите Запустить. Откроется список шаблонов. Нажмите Добавить, задайте название и подтвердите создание.

Виджет. 1. задайте название

Откройте строку созданного шаблона: его настройки загружаются во встроенном окне. Значок копирования справа открывает Копировать шаблон и после подтверждения создаёт отдельную копию. Он не копирует ссылку для OBS. Удаление шаблона также требует подтверждения.

Параметры запуска

При открытии шаблона StreamVi добавляет к URL Widget параметры из таблицы. Задавайте в настройках базовый URL страницы без собственной query-строки и #: параметры запуска добавляет кабинет.

ПараметрЗначение
project_idВнутренний числовой ID проекта, переданный строкой в URL
project_external_idВнешний ID того же проекта
template_idID открытого шаблона
tokenAPI-токен установки приложения в этом проекте
signПодпись параметров запуска

На сервере своей страницы проверьте подпись до использования параметров. Храните токен установки в серверной сессии, если странице нужны запросы к API. Для запросов /method/ его можно передавать в Authorization: Bearer ...; доступ ограничен проектом и разрешениями токена.

token из URL предоставляет доступ к API. Поле token в сообщениях streamvi:cabinet:init и streamvi:widget:update ниже используется только для сеанса обмена с iframe. Эти значения нельзя подменять друг другом.

Проверить подпись

Текущий формат подписи - MD5 от сцепленных значений параметров и секретного ключа приложения. Для виджета порядок такой:

project_id + project_external_id + template_id + token + secret_key

Для встроенной страницы порядок другой:

project_id + project_external_id + token + secret_key

Если встроенной странице не передан token, он не участвует в подписи. Используйте декодированные строковые значения из URL без сортировки, разделителей и повторного URL-кодирования. Результат MD5 представлен 32 шестнадцатеричными символами.

Общий серверный пример на Node.js для обоих типов:

import { createHash, timingSafeEqual } from 'node:crypto';

function verifyLaunchQuery(params, secretKey, type) {
  if (!secretKey || !['widget', 'embedded'].includes(type)) return false;
  const fields = type === 'widget'
    ? ['project_id', 'project_external_id', 'template_id', 'token']
    : ['project_id', 'project_external_id', ...(params.has('token') ? ['token'] : [])];
  if ([...fields, 'sign'].some((key) => params.getAll(key).length !== 1)) return false;
  if (fields.some((key) => !params.get(key))) return false;
  const sign = params.get('sign');
  if (!/^[a-f0-9]{32}$/i.test(sign)) return false;

  const values = fields.map((key) => params.get(key)).join('');
  const expected = createHash('md5').update(values + secretKey).digest();
  return timingSafeEqual(expected, Buffer.from(sign, 'hex'));
}

Передайте в params значение new URL(requestUrl).searchParams, в secretKey секрет из серверной конфигурации, а в type ожидаемый тип вашего приложения. При false отклоните запуск. Не переносите эту проверку вместе с секретом в браузер. Собственные дополнительные параметры не входят в эту подпись. Подпись не делает ссылку одноразовой: не публикуйте полный адрес запуска с токеном.

Обмен настройками с кабинетом

Для типа Виджет кабинет загружает данные выбранного шаблона и обменивается сообщениями с его iframe через window.postMessage. Для типа Встроенная страница этот механизм не запускается.

СообщениеНаправлениеНазначение
streamvi:cabinet:initКабинет → виджетПередаёт данные шаблона и начинает сеанс обмена
streamvi:widget:readyВиджет → кабинетПодтверждает готовность, возвращая данные и реквизиты сеанса
streamvi:cabinet:syncКабинет → виджетПередаёт актуальные данные, язык и тему после начала обмена
streamvi:widget:updateВиджет → кабинетПередаёт изменения данных шаблона для сохранения

Каждое сообщение содержит type, token, template_id, project_id и payload. Верните token, строковый template_id и числовой project_id из полученного сообщения без изменения. Это реквизиты текущего окна; token здесь не является API-токеном или секретным ключом приложения.

payload - объект с данными шаблона. Отправляйте полный актуальный объект, сохраняя поля, которые не меняете. Входящие payload.language и payload.theme задаёт кабинет. Тема принимает light или dark; эти два служебных поля не сохраняются как настройки шаблона.

Подключите обработчик сообщений сразу при загрузке страницы. Кабинет повторяет init ограниченное число раз; обработайте повтор безопасно и ответьте ready. После sync обновляйте интерфейс без обратного update, иначе получится цикл изменений. Проверяйте event.origin, event.source и форму сообщения, указывайте точный targetOrigin. Правила postMessage в MDN.

Минимальный пример

Разместите этот фрагмент на странице настроек своего виджета. В production cabinetOrigin - https://streamvi.io; для проверки локального кабинета замените его на http://localhost:3005. Адрес самого виджета может отличаться. Пример отправляет поле title вместе с остальными данными шаблона и применяет тему и язык кабинета.

<label>Title <input id="title"></label>
<button id="send" disabled>Send changes</button>
<output id="status"></output>
<script>
  const cabinetOrigin = 'https://streamvi.io';
  const titleInput = document.getElementById('title');
  const sendButton = document.getElementById('send');
  const status = document.getElementById('status');
  let session;
  let settings = {};

  function send(type) {
    if (!session) return;
    window.parent.postMessage({
      type,
      token: session.token,
      template_id: session.template_id,
      project_id: session.project_id,
      payload: settings,
    }, cabinetOrigin);
  }

  window.addEventListener('message', event => {
    if (event.origin !== cabinetOrigin || event.source !== window.parent) return;
    const message = event.data;
    if (!message || !['streamvi:cabinet:init', 'streamvi:cabinet:sync'].includes(message.type)) return;
    if (typeof message.token !== 'string' || typeof message.template_id !== 'string' ||
        !Number.isFinite(message.project_id) || !message.payload ||
        typeof message.payload !== 'object' || Array.isArray(message.payload)) return;
    if (message.type === 'streamvi:cabinet:sync' && (!session ||
        message.token !== session.token || message.template_id !== session.template_id ||
        message.project_id !== session.project_id)) return;
    if (message.type === 'streamvi:cabinet:init' && session?.token === message.token) {
      send('streamvi:widget:ready');
      return;
    }
    session = {
      token: message.token,
      template_id: message.template_id,
      project_id: message.project_id,
    };
    const { language, theme, ...stored } = message.payload;
    settings = stored;
    document.documentElement.lang = language === 'ru' ? 'ru' : 'en';
    document.documentElement.style.colorScheme = theme === 'dark' ? 'dark' : 'light';
    titleInput.value = typeof settings.title === 'string' ? settings.title : '';
    sendButton.disabled = false;
    if (message.type === 'streamvi:cabinet:init') send('streamvi:widget:ready');
  });

  sendButton.addEventListener('click', () => {
    settings = { ...settings, title: titleInput.value };
    send('streamvi:widget:update');
    status.textContent = 'Changes sent to Cabinet';
  });
</script>

Проверить сохранение

Откройте шаблон, измените заголовок и нажмите Send changes в примере. Затем проверьте отсутствие ошибки в окне кабинета, закройте шаблон и откройте его снова: заголовок должен загрузиться из сохранённых данных. Повторите проверку для другого шаблона и при смене языка или темы.

Кабинет отправляет данные на сервер при изменившемся payload. Отдельного сообщения «успешно сохранено» в этом протоколе нет: postMessage подтверждает только отправку сообщения. Поэтому пример показывает Changes sent to Cabinet, а не обещает сохранение. Пока предыдущий запрос выполняется, новые сообщения сохранения могут быть пропущены; не используйте отправку на каждое нажатие клавиши как гарантию сохранения. После ошибки проверьте данные повторным открытием шаблона.

Использовать в OBS

Адрес для OBS и отображение виджета реализует автор приложения. Создайте отдельную страницу отображения, которая получает сохранённые настройки нужного шаблона через ваш сервер. Предусмотрите ссылку на неё в интерфейсе настроек и объясните пользователю, где её скопировать. В OBS добавьте источник Браузер и укажите адрес и нужные размеры.

Для получения сохранённых данных шаблона есть GET /method/integration-template/widget/get, версия 1. Именно этому методу нужно разрешение integration:read; это не обязательное право для создания плагина. Для работы с проектами, каналами, видео и другими сущностями выбирайте права используемых методов API. В запросе ниже используйте внутренний project_id, template_id и API-токен из параметров проверенного запуска:

curl --fail-with-body --get \
  'https://api-v2.streamvi.io/method/integration-template/widget/get' \
  --header "Authorization: Bearer $STREAMVI_ACCESS_TOKEN" \
  --data-urlencode 'v=1' \
  --data-urlencode "project_id=$STREAMVI_PROJECT_ID" \
  --data-urlencode "template_id=$STREAMVI_TEMPLATE_ID"

Пример ответа после сохранения заголовка из примера выше:

{
  "payload": {
    "title": "My broadcast"
  }
}

payload может быть null, если у шаблона ещё нет сохранённых данных. Отобразите начальное состояние своего виджета. language и theme, которые кабинет добавляет при обмене сообщениями, в сохранённые настройки не входят. Другой проект не может прочитать шаблон этим запросом.

Страница в OBS работает вне кабинета и не получает streamvi:cabinet:init или sync. Автор приложения определяет, когда его страница перечитывает настройки; этот метод возвращает текущий объект и не создаёт подписку на изменения.

API-токен установки оставьте на своём сервере. Для ссылки OBS используйте отдельный ключ вашего приложения с доступом только к отображению нужного шаблона. Не публикуйте рабочую ссылку; перед удалением шаблона проверьте сцены, в которых он используется. Для самостоятельного OAuth-подключения см. OAuth.