StemStage + Holyrics
Востаннє оновлено: 2026-10-10
«Транслювати в Holyrics» — StemStage на сценічному ноуті надсилає поточний слайд у Holyrics на ноуті в залі, і проєктор показує його вже Holyrics: вашими темами, шрифтами й фонами. На цій сторінці — усе, що треба налаштувати один раз: API Server і дозволи токена в Holyrics, підключення в StemStage, тема слайдів і кнопка-перемикач трансляції на панелі Holyrics.
Перевірено на Holyrics 2.29.1. Трансляція працює лише з десктопного застосунку StemStage — у браузерній версії її немає.
1. Holyrics: API Server і токен
- Holyrics → Налаштування (шестерня) → вкладка API Server.
- API Server – Local → Увімкнено. Порт за замовчуванням — 8091; якщо змінюєте, той самий порт потім вводите в StemStage.
- Керувати дозволами → створіть токен для StemStage і позначте йому дії з таблиці нижче. Скопіюйте токен — він знадобиться в розділі 2.
- На ноуті в залі порт API Server має бути відкритий у фаєрволі: запити йдуть зі сценічного ноута в зал.
Дозволи токена
Holyrics видає дозволи по одній дії — варіанта «всі дії» немає. Токену StemStage потрібні:
| Дія | Навіщо | Потрібна? |
|---|---|---|
ShowQuickPresentation |
показати слайд — основна дія трансляції | так |
CloseCurrentPresentation |
прибрати слайд: пауза, інструментал, вимкнення трансляції | так |
GetCurrentPresentation |
кнопка «Перевірити» в налаштуваннях StemStage (лише читання) | так |
GetInterfaceInput |
читає тему головного вікна — для скрипту теми (розділ 3, спосіб Б) | так, якщо ставите скрипт теми |
GetCurrentTheme |
діагностика: тема слайда, що зараз на екрані | ні |
GetThemes |
діагностика: список тем з id і назвами | ні |
Дії гортання (ActionNext, ActionPrevious тощо) не потрібні: StemStage щоразу надсилає готовий
слайд, а не гортає презентацію Holyrics.
2. StemStage: підключення
У десктопному застосунку на сценічному ноуті: Налаштування → «Мережа» → Holyrics — адреса ноута в залі, порт (8091), токен із розділу 1 → «Перевірити». Має з’явитися «Зв’язок є — останній запит пройшов».
Те саме з консолі stemstage (наприклад, по ssh на сценічний ноут):
echo "$TOKEN" | stemstage holyrics set --host 192.168.x.x --port 8091 --token -
stemstage holyrics check # перевірити зв'язок
stemstage holyrics on # увімкнути трансляцію (off — вимкнути)
stemstage holyrics # показати поточний стан
--token - читає токен рядком зі stdin (з переводом рядка — echo його додає). Просто в рядку
команди токен не передається навмисно: там його було б видно іншим користувачам системи.
Токен зберігається лише на цьому пристрої й не синхронізується через хмару.
Сама трансляція вмикається кнопкою «Транслювати в Holyrics» на вкладці «Показ» або на пульті
/control (на пульті — лише коли ввімкнена «Трансляція в локальну мережу»).
3. Тема слайдів
Без налаштування слайди зі StemStage ідуть першою темою зі списку Holyrics, хоч би яку тему вибрали в головному вікні. Причина: StemStage показує слайд як швидку презентацію, а вона бере тему не з головного вікна, а з власного вікна швидкої презентації (кнопка в лівому верхньому куті Holyrics, поруч із кнопкою переходу до Біблії). Поки там нічого не вибрано — це перша тема.
Є два способи це змінити — обидва в самому Holyrics.
Спосіб А: без скрипту — тема у вікні швидкої презентації
Відкрийте вікно швидкої презентації (кнопка в лівому верхньому куті) і виберіть там тему. Мінус: це окремий вибір — тема головного вікна на трансляцію не впливає, і змінювати тему доведеться в обох місцях.
Спосіб Б: скрипт — тема головного вікна (рекомендовано)
Невеликий скрипт API-сервера Holyrics перехоплює кожен слайд, що прийшов без теми, і показує його темою, вибраною в головному вікні. Оператор вибирає тему як завжди — і наступний слайд зі StemStage вже йде нею. Якщо щось піде не так, скрипт мовчки показує слайд як раніше: слайд на проєкторі буде завжди.
Куди вставляти
- Holyrics → Налаштування → вкладка API Server → кнопка JavaScript (нижній ряд, поруч із «Допомога» і «Log»). Відкриється редактор із двома полями коду.
- Ліве поле — основна функція
request(...): виділіть усе (Ctrl+A), видаліть шаблон і вставте ліву половину нижче. - Праве поле — «додаткові функції»: вставте праву половину. Копіюйте до останньої
}включно. - Зберегти внизу редактора → Ok у вікні налаштувань.
- Додайте токену StemStage дозвіл
GetInterfaceInput(розділ 1).
Якщо в лівому полі вже є ваш власний код (інша інтеграція), не видаляйте його — додайте лише рядки
case 'ShowQuickPresentation': ...у йогоswitch.return trueв кінці мусить лишитися: саме він каже Holyrics обробити всі інші запити як зазвичай.
Ліва половина
function request(action, headers, content, info) {
switch (action) {
case 'ShowQuickPresentation':
return ssShowWithMainTheme(content);
}
// Усі інші дії Holyrics виконує сам, як без скрипту.
return true;
}
Права половина
// Слова зі StemStage приходять як ShowQuickPresentation без теми. Підставляємо тему, вибрану
// в головному вікні Holyrics. Що б не пішло не так — повертаємо true: Holyrics покаже слайд
// як раніше (першою темою), але слайд на проєкторі буде завжди.
function ssShowWithMainTheme(content) {
// Тема вже є в запиті (інший клієнт API або повторний вхід) — нічого не чіпаємо.
if (content == null || content.theme != null) return true;
try {
var t = h.hly('GetInterfaceInput', { id: 'main_selected_theme' });
if (t == null || String(t.status) != 'ok' || !t.data) return true;
content.theme = { id: String(t.data) };
var r = h.hly('ShowQuickPresentation', content);
if (r != null && String(r.status) == 'ok') return r;
h.log('[stemstage] показ із темою не вдався: ' + JSON.stringify(r));
} catch (e) {
h.log('[stemstage] тема: ' + e);
}
// Запасний шлях: штатний показ без теми.
delete content.theme;
return true;
}
Скрипт діє на всі програми, що надсилають у Holyrics швидку презентацію без теми, не лише на StemStage, — вони теж підуть темою головного вікна. Запит, у якому тему вказано явно, скрипт не чіпає.
Перевірка
- У головному вікні Holyrics виберіть тему, помітно відмінну від першої в списку.
- Увімкніть «Транслювати в Holyrics» і запустіть пісню — слайд має йти вибраною темою.
- Змініть тему в головному вікні — наступний слайд піде новою (поточний не перемальовується, поки StemStage не надішле наступний).
- Не підхопилось — API Server → Log, рядки
[stemstage]. Помилка дозволу наGetInterfaceInput— додайте його токену.
Як прибрати
Видаліть обидва фрагменти (у лівому полі лишіть request лише з return true;) → Зберегти.
Трансляція повернеться до теми вікна швидкої презентації.
4. Кнопка «Слова зі StemStage» на панелі Holyrics
Кнопка на панелі швидкого доступу Holyrics, яка вмикає й вимикає «Транслювати в Holyrics» — ту
саму трансляцію, що й кнопка на вкладці «Показ» чи на пульті /control. Оператор у залі керує
словами, не відходячи від Holyrics. Дозволи токена для неї не потрібні: кнопка ходить не в API
Holyrics, а до StemStage на сцені.
- Кнопка підключається до LAN-сервера StemStage так само, як телефон із пультом
/control, — за PIN. - Перемикач один на всю систему: сцена, пульт і кнопка в Holyrics керують тим самим станом, і кнопка показує його чесно, хоч би хто його змінив — зелена «ON» (трансляція ввімкнена), «OFF» (вимкнена), сіра «(немає зв’язку)» (стану ще не отримано).
- Вимкнення трансляції закриває й поточний слайд у Holyrics.
Що потрібно
- StemStage на сценічному ноуті відкритий, LAN-сервер увімкнений (Налаштування → «Мережа»), і
пульт
http://<IP-сцени>:7373/controlвідкривається з ноута з Holyrics. Старий порт 47811 більше не використовується — якщо кнопку налаштовували раніше, поміняйте порт у приймачі і в скрипті. - PIN пульта (той, що вводите на
/control). - IP сценічного ноута — нижче в прикладах
192.168.2.3, замініть на свій. - Holyrics 2.23.0 або новіший.
Крок 1. Приймач
Holyrics 2.29.1 не приймає повну адресу ws://… у скрипті (receiver not found) — потрібен
приймач — окреме з’єднання, яке Holyrics налаштовує у вікні «Приймачі».
- «Приймачі» → «Створити», тип URL.
- Заповніть:
- Назва:
stemstage— саме так, скрипт шукає приймач за цією назвою. - URL/Host/ip:port:
ws://192.168.2.3:7373/lan/ws— між IP і портом двокрапка, не/. - Тип:
WebSocket.
- Назва:
- «Ок».
Крок 2. Кнопка-скрипт
- Панель швидкого доступу → додати елемент JavaScript.
- Назва — те, що оператор бачить на кнопці, наприклад «Слова зі StemStage». Стан (ON / OFF / «(немає зв’язку)») скрипт дописує сам, у назву його не пишіть.
- «JavaScript Code» — відкриється редактор із двома половинами:
- ліва — основний код; Holyrics вимагає, щоб він починався рядком
function scriptAction(obj) {, інакше при збереженні буде «‘function scriptAction(obj) {’ не знайдено»; - права — «додаткові функції» (Function Input / Item Status).
- ліва — основний код; Holyrics вимагає, щоб він починався рядком
- Вставте код нижче в обидві половини, у правій замініть
STEMSTAGE_HOSTіSTEMSTAGE_PINна свої. Копіюйте до останньої}включно — загублена дужка даєExpected } but found eof. - «Зберегти».
Ліва половина
function scriptAction(obj) {
var client = ssConnect();
var known = h.getGlobal('stemstage_holyrics_active', null);
if (client.isOpen() && h.getGlobal('stemstage_ready', false) && known !== null) {
ssSendCmd(client, !known);
} else {
h.setGlobal('stemstage_pending_toggle', true); // перемкне, щойно прийде стан
}
}
Права половина
// IP:порт сценічного ноута — для заголовка Origin (сама адреса з'єднання — у приймачі `stemstage`).
var STEMSTAGE_HOST = '192.168.2.3:7373';
// PIN LAN-пульта (той самий, що на /control).
var STEMSTAGE_PIN = '000000';
var CACHE_ID = 'stemstage-lan';
function hGetItemStatusData(obj) {
var active = h.getGlobal('stemstage_holyrics_active', null);
if (active === null) return { active: false, background: '5A5A5A', description: '(немає зв’язку)' };
if (active) return { active: true, description: 'ON' };
return { active: false, description: 'OFF' };
}
function ssLog(msg) { h.log('[stemstage] ' + msg); }
function ssSendCmd(client, active) {
client.send(JSON.stringify({ t: 'cmd', payload: { type: 'set-holyrics-broadcast', active: active } }));
}
function ssConnect() {
var url = 'stemstage'; // назва (або ID) приймача з кроку 1
var existing = h.ws(url, CACHE_ID);
if (existing != null && existing.isOpen()) return existing;
h.setGlobal('stemstage_ready', false);
var client = h.ws(url, CACHE_ID, {
// LAN-сервер пускає сокет лише коли Origin збігається з адресою, на яку дзвонять.
headers: { Origin: 'http://' + STEMSTAGE_HOST },
on_open: function (evt) {
evt.source.send(JSON.stringify({ t: 'hello', role: 'control', version: 1 }));
},
on_message: function (raw) {
var m;
try { m = JSON.parse(raw); } catch (e) { return; }
if (m.t === 'challenge') {
var proof = String(h.sha256Str(m.nonce + STEMSTAGE_PIN)).toLowerCase();
client.send(JSON.stringify({ t: 'auth', proof: proof }));
} else if (m.t === 'auth-failed') {
ssLog('невірний PIN або блокування: ' + m.reason);
} else if (m.t === 'no-host') {
h.setGlobal('stemstage_ready', false);
ssLog('StemStage не відкритий на сцені');
} else if (m.t === 'ready') {
h.setGlobal('stemstage_ready', true);
} else if (m.t === 'host' && m.payload) {
var p = m.payload;
if ((p.type === 'show-state' || p.type === 'snapshot') && typeof p.holyricsActive === 'boolean') {
h.setGlobal('stemstage_holyrics_active', p.holyricsActive);
if (h.getGlobal('stemstage_pending_toggle', false)) {
h.setGlobal('stemstage_pending_toggle', false);
ssSendCmd(client, !p.holyricsActive);
}
}
}
},
on_error: function (evt) { ssLog('помилка з’єднання: ' + evt.error); },
on_close: function (evt) {
h.setGlobal('stemstage_ready', false);
h.setGlobal('stemstage_holyrics_active', null);
}
});
return client;
}
Як користуватись
- Після запуску Holyrics кнопка сіра — з’єднання ще немає. Перше натискання підключається і одразу перемикає трансляцію (коли прийде поточний стан — за секунду-дві).
- Далі з’єднання тримається: кожне натискання — миттєве перемикання, а колір кнопки стежить за станом, навіть якщо трансляцію перемкнули зі сцени чи з телефона.
- Змінили PIN у StemStage — поміняйте
STEMSTAGE_PINу правій половині. - Змінився IP/порт сцени — поміняйте адресу в приймачі
stemstageіSTEMSTAGE_HOSTу скрипті.
Якщо кнопка не працює
Налагоджувальна кнопка — та сама кнопка, але з логом на кожному кроці
Окрема кнопка з тим самим протоколом, але з логом на кожному кроці. Має власне з’єднання й власні
змінні (ssdbg_*), тож основній кнопці не заважає; теж справді перемикає трансляцію —
перевіряйте поза служінням. Лог — вікно JavaScript Log (або кнопка «Тест» у редакторі); у самому приймачі є ще кнопка Log — сирий трафік з’єднання.
Натисніть її двічі з паузою 2–3 с і читайте ланцюжок: відкриваю → з’єднано → challenge →
PIN прийнято → готово → стан трансляції.
Ліва половина
function scriptAction(obj) {
try {
var client = dbgConnect();
var known = h.getGlobal('ssdbg_active', null);
var ready = h.getGlobal('ssdbg_ready', false);
dbgLog('клік: open=' + client.isOpen() + ' ready=' + ready + ' state=' + known);
if (client.isOpen() && ready && known !== null) {
dbgLog('надсилаю set-holyrics-broadcast active=' + !known);
dbgSendCmd(client, !known);
} else {
dbgLog('ще не готово — перемкну, коли прийде стан');
h.setGlobal('ssdbg_pending', true);
}
} catch (e) {
dbgLog('exception у кліку: ' + e);
}
}
Права половина
var DBG_HOST = '192.168.2.3:7373';
var DBG_PIN = '000000';
var DBG_CACHE_ID = 'stemstage-lan-debug';
function hGetItemStatusData(obj) {
var active = h.getGlobal('ssdbg_active', null);
if (active === null) return { active: false, background: '5A5A5A', description: '(немає зв’язку)' };
if (active) return { active: true, description: 'ON' };
return { active: false, description: 'OFF' };
}
function dbgLog(msg) { h.log('[stemstage-debug] ' + msg); }
function dbgSendCmd(client, active) {
client.send(JSON.stringify({ t: 'cmd', payload: { type: 'set-holyrics-broadcast', active: active } }));
}
function dbgConnect() {
var url = 'stemstage'; // назва (або ID) приймача
var existing = null;
try {
existing = h.ws(url, DBG_CACHE_ID);
} catch (e) {
dbgLog('exception при пошуку з’єднання: ' + e);
}
if (existing != null) {
dbgLog('знайдено з’єднання, open=' + existing.isOpen());
if (existing.isOpen()) return existing;
}
h.setGlobal('ssdbg_ready', false);
dbgLog('відкриваю приймач ' + url + ' з Origin http://' + DBG_HOST);
var client;
try {
client = h.ws(url, DBG_CACHE_ID, {
headers: { Origin: 'http://' + DBG_HOST },
on_open: function (evt) {
dbgLog('з’єднано, HTTP ' + evt.http_status + ' ' + evt.http_status_message);
evt.source.send(JSON.stringify({ t: 'hello', role: 'control', version: 1 }));
dbgLog('надіслано hello');
},
on_message: function (raw) {
var m;
try { m = JSON.parse(raw); } catch (e) { dbgLog('не JSON: ' + String(raw).substring(0, 200)); return; }
var p = m.payload;
dbgLog('отримано t=' + m.t + (p && p.type ? ' payload.type=' + p.type : ''));
if (m.t === 'challenge') {
var proof = String(h.sha256Str(m.nonce + DBG_PIN)).toLowerCase();
dbgLog('challenge, nonce=' + m.nonce + ' proof=' + proof);
client.send(JSON.stringify({ t: 'auth', proof: proof }));
} else if (m.t === 'auth-ok') {
dbgLog('PIN прийнято');
} else if (m.t === 'auth-failed') {
dbgLog('PIN відхилено: ' + m.reason + (m.retryAfterSec ? ', повтор через ' + m.retryAfterSec + ' с' : ''));
} else if (m.t === 'no-host') {
h.setGlobal('ssdbg_ready', false);
dbgLog('на сцені не відкритий StemStage (no-host)');
} else if (m.t === 'version-mismatch') {
dbgLog('різні версії протоколу: host=' + m.hostVersion + ' client=' + m.clientVersion);
} else if (m.t === 'ready') {
h.setGlobal('ssdbg_ready', true);
dbgLog('готово, чекаю стан');
} else if (m.t === 'host' && p) {
if ((p.type === 'show-state' || p.type === 'snapshot') && typeof p.holyricsActive === 'boolean') {
dbgLog('стан трансляції: ' + p.holyricsActive + ', health=' + p.holyricsHealth);
h.setGlobal('ssdbg_active', p.holyricsActive);
if (h.getGlobal('ssdbg_pending', false)) {
h.setGlobal('ssdbg_pending', false);
dbgLog('відкладене перемикання → ' + !p.holyricsActive);
dbgSendCmd(client, !p.holyricsActive);
}
}
}
},
on_error: function (evt) {
dbgLog('помилка з’єднання: ' + evt.error);
},
on_close: function (evt) {
dbgLog('з’єднання закрито: code=' + evt.code + ' reason=' + evt.reason);
h.setGlobal('ssdbg_ready', false);
h.setGlobal('ssdbg_active', null);
}
});
} catch (e) {
dbgLog('exception при відкритті: ' + e);
throw e;
}
return client;
}
Типові помилки
| Симптом | Причина | Що зробити |
|---|---|---|
| При збереженні: «‘function scriptAction(obj) {’ не знайдено» | Ліва половина не починається з цього рядка | Перший рядок лівої половини — рівно function scriptAction(obj) { |
При збереженні: Expected } but found eof |
При копіюванні загубилась остання } |
Докопіюйте кінець правої половини |
У лозі receiver not found: ws://… |
У скрипті повна адреса замість приймача | var url = 'stemstage' + приймач із кроку 1 |
| Кнопка весь час сіра, у лозі нічого | Приймач з помилкою в адресі (напр. /7373 замість :7373) |
Виправити URL приймача |
з’єднання закрито одразу після відкриваю |
LAN-сервер відхилив сокет: не той Origin або LAN-сервер вимкнений |
STEMSTAGE_HOST = той самий IP:порт, що в приймачі; перевірити, що /control відкривається |
PIN відхилено |
Невірний PIN (після кількох спроб — тимчасове блокування) | Виправити STEMSTAGE_PIN, зачекати retryAfterSec |
no-host |
На сцені не відкрита вкладка StemStage | Відкрити StemStage на сценічному ноуті |
| Після оновлення StemStage кнопка сіра, хоча слова приходять | У приймачі й скрипті лишився старий порт 47811 | Порт 7373 у приймачі stemstage і в STEMSTAGE_HOST (та DBG_HOST); на сцені відкрити його у фаєрволі: sudo firewall-cmd --permanent --add-port=7373/tcp && sudo firewall-cmd --reload |
| Слова в Holyrics приходять, а кнопка сіра | Це різні канали: слова йдуть HTTP API Holyrics, кнопка — WebSocket до сцени | Дивитись лог налагоджувальної кнопки |
5. Якщо не працює
Стан під кнопкою «Перевірити» в налаштуваннях StemStage:
| Повідомлення | Що це | Що робити |
|---|---|---|
| «Зв’язок є — останній запит пройшов.» | усе гаразд | — |
| «Holyrics не відповідає…» | ноут у залі вимкнений, невірна адреса чи порт, закритий фаєрвол, вимкнений API Server | розділ 1 (кроки 2 і 4), адреса й порт — розділ 2 |
| «Токен без потрібного дозволу…» | або токен невірний, або йому бракує дозволу на дію | перевірте токен і дозволи з розділу 1 |
| «Заповніть адресу, порт і токен нижче.» | підключення не задане | розділ 2 |
| «Немає звʼязку з локальним сервером…» | не працює локальний сервер застосунку | секція «Локальний сервер» у тих самих налаштуваннях |
Для технічної перевірки з будь-якого комп’ютера в мережі (тіло запиту й заголовок Content-Type
обов’язкові):
H=http://192.168.x.x:8091/api; T=<токен>
# зв'язок і токен:
curl -sS -X POST -H 'Content-Type: application/json' -d '{}' "$H/GetCurrentPresentation?token=$T"
# тема слайда, що зараз на екрані (дозвіл GetCurrentTheme):
curl -sS -X POST -H 'Content-Type: application/json' -d '{}' "$H/GetCurrentTheme?token=$T"
# тема головного вікна (дозвіл GetInterfaceInput):
curl -sS -X POST -H 'Content-Type: application/json' -d '{"id":"main_selected_theme"}' "$H/GetInterfaceInput?token=$T"
"unauthorized action" у відповіді — токену бракує дозволу на цю дію; "invalid token" — токен
невірний.