Docs · Custom Widget API
Виджеты для оверлеев
Кастом-виджет — это ваш HTML, CSS и JS, который платформа запускает в изолированном фрейме на странице оверлея, видит в OBS и редакторе, и кормит живыми событиями канала: донатами, подписками, фолловерами и сообщениями чата.
СОВМЕСТИМОСТЬ / COMPATIBILITY
Рантайм повторяет публичное API виджетов StreamElements: onWidgetLoad / onEventReceived / onSessionUpdate, SE_API.store, поле-кнопки, SE-формы данных и goal-модель сессии.
Виджеты, экспортированные из StreamElements Custom Widgets, запускаются на ByAnish Platform без изменений кода. Встраивать их можно прямо ZIP-экспортом или вставкой кода в редактор.
Обзор
Каждый виджет живёт внутри оверлея — документа с холстом 1920×1080 (по умолчанию), на котором владелец расставляет слои: виджеты, текст, картинки, видео. Оверлей открывается по публичной ссылке с токеном — её вставляют в OBS как Browser Source.
Ваш код исполняется в sandbox-iframe (allow-scripts, без same-origin) — виджеты изолированы друг от друга и от страницы-хоста. jQuery 3.7.1 уже вшит в документ: селекторы $() работают сразу, без тегов <script src>.
Платформа доставляет в фрейм события канала и их слушает ваш JS. Панель настроек в редакторе рисуется из описания полей FIELDS, а значения читаются в fieldData.
Быстрый старт
В редакторе оверлея: «Добавить виджет» → «Кастомный». В модалке кода пять вкладок — HTML, CSS, JS, FIELDS, DATA. Вставьте четыре части ниже и нажмите «Готово» — виджет уже работает в превью.
<!-- HTML --> <div id="root">Привет, чат!</div>
/* CSS */
#root {
font: 600 28px/1.2 var(--font, sans-serif);
color: #fff;
padding: 8px 14px;
}// JS
window.addEventListener('onWidgetLoad', function (event) {
// как у SE: данные — в detail события
document.getElementById('root').textContent =
event.detail.fieldData.title;
});[
{
"name": "title",
"type": "text",
"label": "Заголовок / Title",
"value": "Привет, чат!"
}
]Ссылку на оверлей (кнопка «Открыть» в списке) вставьте в OBS: Browser Source, ширина/высота — как у оверлея. Дальше всё живое: события приходят в виджет по WebSocket без перезагрузки.
Из чего состоит виджет
| часть | что это |
|---|---|
HTML | Разметка виджета. Вставляется в <body> документа фрейма как есть. |
CSS | Стили. Едут в <head> ДО кода виджета. Подстановки {{имя}} заменяются значениями полей при сборке документа. |
JS | Код. Исполняется последним, после jQuery и шима платформы. Подстановки {{имя}} работают и здесь. |
FIELDS | JSON-массив описаний полей — из него редактор рисует панель настроек. |
DATA | JSON-объект значений поверх дефолтов полей. Ключи без поля тоже доезжают до fieldData. |
Подстановки {{имя}} — для значений, вшиваемых в разметку/стили при сборке. Если значение должно меняться без перезагрузки фрейма — читайте его в JS из fieldData, а не вставляйте скобками.
Runtime: жизненный цикл
| событие | когда и что в detail |
|---|---|
onWidgetLoad | Документ загружен, или панель настроек прислала новые значения полей. detail: { fieldData, session, recents, currency, channel, overlay }. |
onEventReceived | Событие канала (донат, подписка, сообщение чата…). detail: { listener, event } — имена в таблице слушателей. |
onSessionUpdate | Счётчики канала изменились. detail: { session } — полный снимок; внутри события onEventReceived сессия едет отдельным полем и диспетчится этим же событием. |
onWidgetLoad
window.addEventListener('onWidgetLoad', function (event) {
// Как у SE: onWidgetLoad — CustomEvent, все данные в detail.
const obj = event.detail;
// fieldData: значения полей из панели настроек
const title = obj.fieldData.title;
// session: снимок счётчиков канала (data — SE-форма, см. «Модель данных»)
const tips = obj.session.data['tip-total'].amount;
// channel: id — Account ID владельца оверлея, username — Twitch-логин (или null)
const channelId = obj.channel.id;
// overlay: { isEditorMode: true } в превью редактора — гасите звук/анимации
const inEditor = obj.overlay.isEditorMode;
// recents и currency присутствуют для совместимости с SE (пустые/заглушки)
render(title, tips);
});События доставляются нативными CustomEvent на window — как в SE. detail содержит данные; session в onEventReceived снимается с события и диспетчится отдельным onSessionUpdate.
Правка поля в панели настроек вызывает повторный onWidgetLoad с новым fieldData — документ не перезагружается, состояние виджета (сообщения чата, накопленные числа) сохраняется. Исключение — значения, вшитые в CSS/HTML скобками {{имя}}: их правка пересобирает фрейм с задержкой ~0.5 с.
// Панель настроек прислала новые значения — документ НЕ перезагружается:
// накопленное состояние (сообщения чата, очередь алертов) живёт дальше.
window.addEventListener('onWidgetLoad', function (event) {
const fieldData = event.detail.fieldData;
document.documentElement.style.setProperty('--accent', fieldData.accent);
});
// Один и тот же обработчик вызывается и на загрузке, и на fields:update.События и слушатели
listener — имя события в onEventReceived. Форма event повторяет нормализованную SE-форму.
| listener | event | когда |
|---|---|---|
tip-latest | { name, amount, currency, message, platform?, goalId? } | донат с любой подключённой платформы |
follower-latest | { name, avatar } | новый фолловер |
subscriber-latest | { name, amount, tier, message } | новая подписка |
subscriber-gifted-latest | { name, amount, tier, sender } | подаренная подписка |
subscriber-resub-latest | { name, amount, tier, message } | продление подписки |
cheer-latest | { name, amount, message } | чир (биты) |
raid-latest | { name, amount } | рейд |
redemption-latest | { name, title } | погашение награды канала |
message | { displayName, message, data: { text, nick, badges, tags, emotes }, … } | сообщение чата |
delete-message | { msgId } | модератор удалил сообщение |
delete-messages | { userId } | модератор вычистил сообщения пользователя |
session.update | полный снимок сессии | после любого события, меняющего счётчики |
event:test | { field, value, listener: "widget-button" } | нажата тест-кнопка из панели настроек |
tip-latest
{
"listener": "tip-latest",
"event": {
"name": "ByANiSh",
"amount": 300,
"currency": "RUB",
"message": "на кофе",
"platform": "donatex", // метка источника, см. «Донаты»
"goalId": "cb6d4fc8-…" // только для дельт сборов Fetta
}
}window.addEventListener('onEventReceived', function (evt) {
const listener = evt.detail.listener; // имя события (таблица ниже)
const event = evt.detail.event; // данные события
if (listener === 'tip-latest') {
showDonation(event.name, event.amount, event.currency, event.message);
}
if (listener === 'message') {
// event.data — SE-форма чата: text, nick, badges, tags, emotes
appendMessage(event.data.nick, event.data.text);
}
});Донатное событие несёт platform — источник платежа: donationalerts, donatex, cloudtips, donatepay или fetta. Для дельт сборов Fetta дополнительно приезжает goalId — внутренний id сбора.
Модель данных сессии
session — снимок счётчиков и последних событий канала. Едет в onWidgetLoad, в каждом onEventReceived и отдельным onSessionUpdate. Две формы сразу: плоская (session.tips) и SE-форма session.data (goal-модель) — виджеты из SE читают вторую.
{
"follows": 128,
"subs": 12,
"bits": 0,
"tips": 1450,
"follower-latest": { "name": "View", "avatar": null },
"subscriber-latest": { "name": "Kuma", "amount": 3, "tier": "1000", "sender": null, "message": "" },
"cheer-latest": { "name": "Forge", "amount": 500, "message": "Hi!" },
"raid-latest": { "name": "StreamTeam", "amount": 24 },
"tip-latest": { "name": "ByANiSh", "amount": 300, "currency": "RUB", "message": "на кофе" },
"donationGoals": [
{
"id": "cb6d4fc8-…",
"platform": "fetta",
"title": "переезд и съем квартиры",
"currentAmount": 12500,
"goalAmount": 60000,
"currency": "RUB",
"imageUrl": null,
"endsAt": null
}
]
}session.data — goal-модель
// session.data — SE goal-model, выведена из плоских счётчиков:
{
"tip-total": { "amount": 1450 }, // = tips + сборы Fetta (native)
"tip-month": { "amount": 1450 }, // периоды сейчас указывают на тотал
"tip-week": { "amount": 1450 },
"tip-session": { "amount": 1450 },
"tip-goal": { "amount": 1450 },
"tip-tip-latest": { "amount": 300 },
"follower-total": { "count": 128 },
"follower-month": { "count": 128 },
"subscriber-total": { "count": 12 },
"subscriber-goal": { "amount": 12 },
"cheer-total": { "amount": 0 }
}window.addEventListener('onSessionUpdate', function (evt) {
const session = evt.detail.session; // полный снимок SessionData
const tips = session.data['tip-total'].amount;
const follows = session.data['follower-total'].count;
updateCounters(tips, follows);
});donationGoals — активные сборы владельца: Fetta-сборы импортируются с площадки, ручные и агрегатные создаются в дашборде «Сборы». id сбора = goalId в донатных событиях.
Поля настроек (FIELDS/DATA)
FIELDS — массив объектов с name, type, label и value-дефолтом. Редактор рисует по типу контрол, значение пишет в fieldData. Группа (group) сворачивает поля в секцию.
| type | контрол | value |
|---|---|---|
text | текстовый ввод | строка |
number | числовой ввод | число (min/max/step) |
slider | ползунок | число |
checkbox | чекбокс | boolean |
dropdown | селект (options: {значение: подпись}) | строка |
colorpicker | пипетка цветов, любые нотации CSS | строка "rgba(...)" / "#rrggbb" |
image / video / sound | загрузка файла в медиатеку платформы | URL /uploads/… |
googleFont | имя шрифта (подгружать самим) | строка |
button | кнопка-триггер тест-события | null |
hidden | не рисуется, значение едет в fieldData | любое |
goalPicker | наш тип: выбор сборов для виджета | JSON-строка |
Панель показывает только подключённые владельцем источники донатов. MEDIA: файлы из виджета загружаются только в собственный /uploads платформы — внешние URL не принимаются.
SE_API.store — хранение состояния
Виджет может сохранять состояние между перезагрузками: счётчики, настройки «на этом канале», прогресс. Значение хранится на канале владельца оверлея — общий стор для всех оверлеев канала, как у SE.
// Значение хранится под ключом на канале — переживает перезагрузки OBS. const KEY = 'my-widget:counter'; // чтение (null — ключ ещё не задан) const saved = await SE_API.store.get(KEY); let counter = typeof saved === 'number' ? saved : 0; // запись counter += 1; await SE_API.store.set(KEY, counter);
Ключ: до 200 символов, [\w.-]. Значение: до 64 КБ (сериализованный JSON). Ключ не задан → get возвращает null.
store живёт поверх HTTP-эндпоинтов /api/overlays/{token}/kv — токен оверлея непроживаемый и сам является креденциалом, шим ходит в него без вашего участия.
Донаты и источники
Донаты приходят событием tip-latest. Если код виджета реально обращается к донатной системе (listener tip-latest или ключи tip-* goal-модели), в настройках появляется выбор источников: DonationAlerts, DonateX, Fetta. Владелец может отметить конкретные сборы Fetta — тогда виджет увидит только их дельты.
Фильтрация адресная — на уровне доставки события в фрейм: виджет просто слушает tip-latest и получает только выбранное. Отдельная метка platform внутри event позволяет фильтровать и в своём JS, если нужно.
| platform | платформа | механика |
|---|---|---|
donationalerts | DonationAlerts | OAuth-подключение, живые донаты |
donatex | DonateX | токен-подключение, живые донаты |
fetta | Fetta | сборы: дельта суммы = событие с goalId |
cloudtips / donatepay | CloudTips / DonatePay | коннекторы в плане; значения уже валидны |
Лимиты и правила
| что | лимит |
|---|---|
html / css / js — каждый | 1 МБ (байты, UTF-8) |
js не может содержать "</script" | валидатор отвергнет на сохранении |
имя поля | 100 символов, уникально в виджете |
label поля | 300 символов |
виджетов в оверлее | 50 |
имена виджета | 80 символов |
текстовый слой | 500 символов |
тело запроса документа | 2 МБ |
SE_API.store значение | 64 КБ, ключ ≤ 200 символов |
картинки/видео/звук | только /uploads платформы |
Фрейм sandbox без allow-same-origin: сетевые запросы к чужим доменам блокируются политикой CSP оверлея — ассеты вшивайте data: URI или грузите из /uploads.
Полный пример: счётчик донатов
Соберём работающий счётчик «всего донатов» с настраиваемым шрифтом и тест-кнопкой. Пять частей вставляются в соответствующие вкладки редактора кода. Значение стартует из снапшота сессии и растёт от живых донатов.
<!-- HTML --> <div class="counter"> <span id="amount">0</span><span id="cur">₽</span> </div>
/* CSS */
.counter {
font-family: {{font}};
font-size: {{fontSize}}px;
font-weight: 600;
color: {{color}};
background: {{bg}};
padding: 10px 18px;
border-radius: {{radius}}px;
}
.counter #cur { opacity: 0.7; margin-left: 4px; }// JS
(function () {
var root = document.getElementById('amount');
var cur = document.getElementById('cur');
var total = 0;
var currency = '₽';
function render() {
root.textContent = Math.floor(total).toLocaleString('ru-RU');
cur.textContent = currency;
}
// стартовое число — из снапшота сессии (данные уже есть при загрузке).
// Как у SE: данные — в detail события.
window.addEventListener('onWidgetLoad', function (event) {
var obj = event.detail;
total = obj.session.data['tip-total'].amount;
currency = obj.fieldData.currency;
render();
});
// живые донаты двигают счётчик
window.addEventListener('onEventReceived', function (evt) {
if (evt.detail.listener === 'tip-latest') {
total += evt.detail.event.amount;
render();
}
});
})();[
{ "name": "font", "type": "googleFont", "label": "Шрифт / Font", "value": "Inter" },
{ "name": "fontSize", "type": "slider", "label": "Размер / Size", "value": 42, "min": 10, "max": 120, "step": 1 },
{ "name": "color", "type": "colorpicker","label": "Цвет текста / Text", "value": "#ffffff" },
{ "name": "bg", "type": "colorpicker","label": "Фон / Background", "value": "rgba(20,22,30,0.75)" },
{ "name": "radius", "type": "slider", "label": "Скругление / Radius", "value": 10, "min": 0, "max": 40, "step": 1 },
{ "name": "currency", "type": "text", "label": "Валюта / Currency", "value": "₽" },
{ "name": "testDonation", "type": "button", "label": "Тест / Test", "value": null }
]Совместимость со StreamElements
Публичное API виджетов ByAnish повторяет StreamElements один-в-один: события onWidgetLoad / onEventReceived / onSessionUpdate c теми же именами listeners и формами данных, SE_API.store с теми же лимитами (64 КБ), поле-кнопки event:test, цельная goal-модель session.data, jQuery в документе, подстановки {{field}} в CSS/JS.
Практически: виджет, экспортированный из SE (Custom Widget → Export), вставляется в наш редактор без правок. Известные отличия — только там, где SE держит сервисы, которых у нас нет: recents сейчас пустая заглушка, currency — статическая, периоды tip-month/tip-week указывают на общий тотал.
Если ваш виджет не завёлся — проверьте консоль браузера: шим пишет [widget] onWidgetLoad и [widget] onEventReceived: <listener> на каждое событие. «Доставлено в 0 из 0» значит, что событие не адресовано этому фрейму.