Виджет
Виджет использует именованные шаблоны: например, для разных сцен OBS. Автор разрабатывает интерфейс настроек и само отображение виджета. Кабинет управляет шаблонами и умеет получать и сохранять их данные через обмен сообщениями с окном виджета.
Готовый проект с настройками, страницей OBS и подписками на события разобран в примере счётчика зрителей. Для нового интерфейса React доступен UI Kit; пример счётчика использует его предыдущую версию.
Настроить приложение
Создайте приложение типа Виджет и выберите нужные разрешения. Откройте Настройки, задайте URL Widget и сохраните. Для отдельной среды есть адрес develop и режим разработки. Заполните информацию и выдайте доступ учебному проекту.

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

Откройте строку созданного шаблона: его настройки загружаются во встроенном окне. Значок копирования справа открывает Копировать шаблон и после подтверждения создаёт отдельную копию. Он не копирует ссылку для OBS. Удаление шаблона также требует подтверждения.
Параметры запуска
При открытии шаблона StreamVi добавляет к URL Widget параметры из таблицы. Задавайте в настройках базовый URL страницы без собственной query-строки и #: параметры запуска добавляет кабинет.
| Параметр | Значение |
|---|---|
project_id | Внутренний числовой ID проекта, переданный строкой в URL |
project_external_id | Внешний ID того же проекта |
template_id | ID открытого шаблона |
token | API-токен установки приложения в этом проекте |
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.