ByAnish PlatformКабинет

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
<!-- HTML -->
<div id="root">Привет, чат!</div>
CSS
/* CSS */
#root {
  font: 600 28px/1.2 var(--font, sans-serif);
  color: #fff;
  padding: 8px 14px;
}
JS
// JS
window.addEventListener('onWidgetLoad', function (event) {
  // как у SE: данные — в detail события
  document.getElementById('root').textContent =
    event.detail.fieldData.title;
});
FIELDSJSON
[
  {
    "name": "title",
    "type": "text",
    "label": "Заголовок / Title",
    "value": "Привет, чат!"
  }
]

Ссылку на оверлей (кнопка «Открыть» в списке) вставьте в OBS: Browser Source, ширина/высота — как у оверлея. Дальше всё живое: события приходят в виджет по WebSocket без перезагрузки.

Из чего состоит виджет

частьчто это
HTMLРазметка виджета. Вставляется в <body> документа фрейма как есть.
CSSСтили. Едут в <head> ДО кода виджета. Подстановки {{имя}} заменяются значениями полей при сборке документа.
JSКод. Исполняется последним, после jQuery и шима платформы. Подстановки {{имя}} работают и здесь.
FIELDSJSON-массив описаний полей — из него редактор рисует панель настроек.
DATAJSON-объект значений поверх дефолтов полей. Ключи без поля тоже доезжают до fieldData.
i

Подстановки {{имя}} — для значений, вшиваемых в разметку/стили при сборке. Если значение должно меняться без перезагрузки фрейма — читайте его в JS из fieldData, а не вставляйте скобками.

Runtime: жизненный цикл

событиекогда и что в detail
onWidgetLoadДокумент загружен, или панель настроек прислала новые значения полей. detail: { fieldData, session, recents, currency, channel, overlay }.
onEventReceivedСобытие канала (донат, подписка, сообщение чата…). detail: { listener, event } — имена в таблице слушателей.
onSessionUpdateСчётчики канала изменились. detail: { session } — полный снимок; внутри события onEventReceived сессия едет отдельным полем и диспетчится этим же событием.

onWidgetLoad

JS
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 с.

JSправки полей без перезагрузки
// Панель настроек прислала новые значения — документ НЕ перезагружается:
// накопленное состояние (сообщения чата, очередь алертов) живёт дальше.
window.addEventListener('onWidgetLoad', function (event) {
  const fieldData = event.detail.fieldData;
  document.documentElement.style.setProperty('--accent', fieldData.accent);
});
// Один и тот же обработчик вызывается и на загрузке, и на fields:update.

События и слушатели

listener — имя события в onEventReceived. Форма event повторяет нормализованную SE-форму.

listenereventкогда
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

eventJSON
{
  "listener": "tip-latest",
  "event": {
    "name": "ByANiSh",
    "amount": 300,
    "currency": "RUB",
    "message": "на кофе",
    "platform": "donatex",          // метка источника, см. «Донаты»
    "goalId": "cb6d4fc8-…"          // только для дельт сборов Fetta
  }
}
JS
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);
  }
});
i

Донатное событие несёт platform — источник платежа: donationalerts, donatex, cloudtips, donatepay или fetta. Для дельт сборов Fetta дополнительно приезжает goalId — внутренний id сбора.

Модель данных сессии

session — снимок счётчиков и последних событий канала. Едет в onWidgetLoad, в каждом onEventReceived и отдельным onSessionUpdate. Две формы сразу: плоская (session.tips) и SE-форма session.data (goal-модель) — виджеты из SE читают вторую.

sessionJSON
{
  "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.dataJSON
// 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 }
}
JS
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-строка
i

Панель показывает только подключённые владельцем источники донатов. MEDIA: файлы из виджета загружаются только в собственный /uploads платформы — внешние URL не принимаются.

SE_API.store — хранение состояния

Виджет может сохранять состояние между перезагрузками: счётчики, настройки «на этом канале», прогресс. Значение хранится на канале владельца оверлея — общий стор для всех оверлеев канала, как у SE.

JS
// Значение хранится под ключом на канале — переживает перезагрузки 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 — токен оверлея непроживаемый и сам является креденциалом, шим ходит в него без вашего участия.

Тест-кнопки и эмуляция

Поле типа button в FIELDS рисует в панели настроек кнопку. Нажатие публикует событие listener event:test с именем поля — виджету удобно тестировать алерты и анимации без живого доната.

Кроме кнопок, в редакторе есть «Эмуляция событий»: фолловер, подписка, рейд, чир и т.д. — платформа синтезирует событие и доставляет его в превью и в OBS одновременно. Счётчики канала при этом НЕ загрязняются: мок живёт только в событии и снимке сессии.

JS
// FIELDS: поле типа "button" — панель настроек рисует кнопку с этим label.
// Нажатие приходит обычным событием:
window.addEventListener('onEventReceived', function (evt) {
  if (evt.detail.listener === 'event:test') {
    // event.field — имя поля-кнопки из FIELDS, event.value — её значение
    if (evt.detail.event.field === 'testDonation') {
      showDonation('TestTip', 100, 'RUB', 'тест из редактора');
    }
  }
});

Донаты и источники

Донаты приходят событием tip-latest. Если код виджета реально обращается к донатной системе (listener tip-latest или ключи tip-* goal-модели), в настройках появляется выбор источников: DonationAlerts, DonateX, Fetta. Владелец может отметить конкретные сборы Fetta — тогда виджет увидит только их дельты.

Фильтрация адресная — на уровне доставки события в фрейм: виджет просто слушает tip-latest и получает только выбранное. Отдельная метка platform внутри event позволяет фильтровать и в своём JS, если нужно.

platformплатформамеханика
donationalertsDonationAlertsOAuth-подключение, живые донаты
donatexDonateXтокен-подключение, живые донаты
fettaFettaсборы: дельта суммы = событие с goalId
cloudtips / donatepayCloudTips / 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
<!-- HTML -->
<div class="counter">
  <span id="amount">0</span><span id="cur">₽</span>
</div>
CSS
/* 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
// 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();
    }
  });
})();
FIELDSJSON
[
  { "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» значит, что событие не адресовано этому фрейму.