WebSocket w czasie rzeczywistym

Połącz się z wss://cliqer.io/ws, aby sterować pokojem Cliqer z własnego oprogramowania lub systemu show control jak pilotem do prezentacji: zmieniaj slajdy, steruj timerem hosta, wskazuj kropką lasera i wysyłaj wiadomości do hosta. Twoja aplikacja dołącza do pokoju jako prezenter; hostem pokoju jest zawsze aplikacja desktopowa Cliqer.
session_token (przeglądarki wysyłają je automatycznie), parametru zapytania ?token= lub nagłówka Authorization: Bearer. Token jest sprawdzany przy nawiązywaniu połączenia, a nie przy każdym poleceniu, więc gdy wygaśnie, otwórz nowe połączenie ze świeżym tokenem i dołącz ponownie. Bez tokenu możesz się połączyć, ale nie możesz korzystać z API. Klucze API zespołu (cliqer_...) uwierzytelniają wyłącznie endpointy REST; na tym gnieździe klucz nie jest tokenem sesji.Połączenie
# WebSocket connections require a WebSocket client
# Using websocat for testing:
websocat "$WS_URL/ws?token=YOUR_ACCESS_TOKEN"
const ws = new WebSocket('$WS_URL/ws?token=' + accessToken)
ws.onopen = () => {
console.log('Connected to Cliqer WebSocket')
}
ws.onmessage = (event) => {
const message = JSON.parse(event.data)
console.log('Received:', message)
}
ws.onerror = (error) => {
console.error('WebSocket error:', error)
}
import websocket
import json
ws = websocket.WebSocketApp(
f"$WS_URL/ws?token={access_token}",
on_message=lambda ws, msg: print(json.loads(msg)),
on_error=lambda ws, err: print(f"Error: {err}"),
on_open=lambda ws: print("Connected")
)
ws.run_forever()
import (
"github.com/gorilla/websocket"
)
url := "$WS_URL/ws?token=" + accessToken
conn, _, err := websocket.DefaultDialer.Dial(url, nil)
if err != nil {
log.Fatal("dial:", err)
}
defer conn.Close()
for {
_, message, err := conn.ReadMessage()
if err != nil {
break
}
fmt.Printf("Received: %s\n", message)
}
use WebSocket\Client;
$client = new Client("$WS_URL/ws?token={$accessToken}");
while (true) {
$message = $client->receive();
$data = json_decode($message, true);
print_r($data);
}
use tokio_tungstenite::{connect_async, tungstenite::Message};
use futures_util::{StreamExt, SinkExt};
let url = format!("$WS_URL/ws?token={}", access_token);
let (mut ws_stream, _) = connect_async(&url).await?;
while let Some(msg) = ws_stream.next().await {
match msg? {
Message::Text(text) => {
let data: serde_json::Value = serde_json::from_str(&text)?;
println!("Received: {:?}", data);
}
_ => {}
}
}
Serwer wysyła connected z Twoim clientId, gdy tylko skonfiguruje połączenie. Zaczekaj na to zdarzenie, zanim cokolwiek wyślesz: wiadomość, która dotrze wcześniej, zostaje odrzucona z error, a połączenie zostaje zamknięte.
Połączenie i przebieg pracy w pokoju
Nawigacja po slajdach
Steruj slajdami w pokoju z własnego oprogramowania, jak pilotem do prezentacji. Twoja aplikacja dołącza do pokoju jako prezenter, więc host widzi ją na liście prezenterów pod nazwą presenterName i steruje nią jak każdym innym prezenterem: operator może wyłączyć jej kliknięcia, usunąć ją lub zablokować.
- Połącz się, używając tokenu sesji (
?token=, nagłówkaAuthorization: Bearerlub ciasteczkasession_token). Klucze API zespołu działają tylko na endpointach REST, nie na tym gnieździe. - Plan właściciela pokoju musi obejmować Client API: dodatek API w planach Presenter, Studio HD i Studio 4K, w cenie planów Studio 8K i Enterprise.
- Polecenia slajdów i timera działają tylko wtedy, gdy host zezwala na kliknięcia z Twojego połączenia.
Dołączanie do pokoju
{ "type": "joinRoom", "payload": { "room": "ABC123", "presenterName": "Show control", "api": true } }
- Wyślij tę wiadomość po zdarzeniu
connected: serwer odrzuca wszystko, co dotrze wcześniej. presenterNameto nazwa, którą widzi host (domyślnieAPI). Twoja aplikacja jest w pokoju zawsze prezenterem, nigdy jego hostem.- Zanim wyślesz polecenia, zaczekaj na zdarzenie
clicksEnabledz payloademtrue. Host wysyła je, gdy skonfiguruje Twoje połączenie, i ponownie za każdym razem, gdy operator włączy lub wyłączy Twoje kliknięcia. - Zachowaj
presenterKeyzjoinedRoomi wysyłaj go wjoinRoomprzy ponownym połączeniu, aby host zachował Twoje uprawnienie do klikania. - Co 15 sekund wysyłaj ramkę tekstową
ping, aby utrzymać połączenie. Serwer odpowiada ramką tekstowąpong(nie JSON).
Wysyłanie poleceń
{ "type": "webAction", "payload": { "action": "goto", "slide": 5, "id": "a3" } }
action | Dodatkowe pole | Działanie |
|---|---|---|
next | Następny slajd (lub krok animacji) | |
prev | Poprzedni slajd (lub krok animacji) | |
first | Pierwszy slajd | |
last | Ostatni slajd | |
goto | slide: liczba całkowita od 1 do 9999 | Przejście do wskazanego slajdu |
blackout | on: true lub false | Wygaszenie ekranu na czarno lub ponowne jego wyświetlenie |
idjest opcjonalne: od 1 do 64 liter, cyfr, znaków_,.,:lub-. Wraca w wyniku. Używaj nowegoiddla każdego polecenia: host odrzuca polecenie, któregoidotrzymał już z Twojego połączenia w ciągu ostatnich 5 sekund, więc ponowienie tego samego polecenia wykona się tylko raz.first,last,gotoiblackoutdziałają tylko na hoście, którego najnowsze zdarzeniecontrolswymienia je wnav. Host, który nie wysyła zdarzeniacontrols, obsługuje tylkonextiprev. Gdy w pokoju jest kilku hostów, każdy z nich wykonuje polecenia, które obsługuje.nextlubprevprzy wygaszonym ekranie przywraca obraz bez zmiany slajdu.gotopoza ostatni slajd jest ignorowane.- Wysyłaj maksymalnie 10 poleceń na sekundę, łącznie polecenia slajdów i timera. Host odrzuca też powtórzenie tego samego polecenia w ciągu 50 milisekund.
Co obecnie obsługuje każda aplikacja do prezentacji:
| Aplikacja do prezentacji | next, prev | first, last | goto | blackout |
|---|---|---|---|---|
| PowerPoint w systemie Windows | Tak | Tak | Tak | Tak |
| PowerPoint na Mac | Tak | Tak | Nie | Nie |
| Keynote | Tak | Nie | Nie | Tak |
| Google Slides (z podłączonym rozszerzeniem przeglądarki) | Tak | Tak | Nie | Nie |
| vMix | Tak | Nie | Nie | Nie |
| Aktywne okno | Tak | Nie | Nie | Nie |
Zdarzenie controls to aktualna informacja o hoście, którym sterujesz: odczytuj nav z niego, a nie z tej tabeli.
# One connection: wait for `connected`, join as an API client, give the host a
# moment to set it up, then send commands. Replies print as they arrive.
( sleep 2
echo '{"type":"joinRoom","payload":{"room":"ABC123","presenterName":"Show control","api":true}}'
sleep 3
echo '{"type":"webAction","payload":{"action":"next","id":"a1"}}'
sleep 1
echo '{"type":"webAction","payload":{"action":"prev","id":"a2"}}'
sleep 1 ) | websocat "$WS_URL/ws?token=YOUR_SESSION_TOKEN"
const ws = new WebSocket('$WS_URL/ws?token=' + accessToken)
const send = (message: object) => ws.send(JSON.stringify(message))
let ready = false
ws.onmessage = (event) => {
if (event.data === 'pong') return
const msg = JSON.parse(event.data)
if (msg.type === 'connected') {
send({ type: 'joinRoom', payload: { room: 'ABC123', presenterName: 'Show control', api: true } })
setInterval(() => ws.send('ping'), 15000) // keeps the connection open
}
if (msg.type === 'clicksEnabled' && msg.payload === true && !ready) {
ready = true
send({ type: 'webAction', payload: { action: 'next', id: 'a1' } })
// Also: { action: 'prev' }, { action: 'first' }, { action: 'last' },
// { action: 'goto', slide: 5 }, { action: 'blackout', on: true }
}
if (msg.type === 'controls') console.log('Commands this host supports:', ['next', 'prev', ...msg.payload.nav])
if (msg.type === 'webActionResult') console.log(`${msg.payload.action} delivered to ${msg.payload.hosts} host(s)`)
if (msg.type === 'slide') console.log('Slide:', msg.payload?.number ?? 'no deck open')
if (msg.type === 'error') console.error(msg.error, msg.payload?.message)
}
import json
import threading
import time
import websocket
ready = False
def send(ws, message):
ws.send(json.dumps(message))
def keepalive(ws): # keeps the connection open
while ws.sock and ws.sock.connected:
time.sleep(15)
ws.send("ping")
def on_message(ws, message):
global ready
if message == "pong":
return
msg = json.loads(message)
if msg["type"] == "connected":
send(ws, {"type": "joinRoom", "payload": {"room": "ABC123", "presenterName": "Show control", "api": True}})
threading.Thread(target=keepalive, args=(ws,), daemon=True).start()
elif msg["type"] == "clicksEnabled" and msg["payload"] is True and not ready:
ready = True
send(ws, {"type": "webAction", "payload": {"action": "next", "id": "a1"}})
# Also: {"action": "goto", "slide": 5}, {"action": "blackout", "on": True}, ...
elif msg["type"] == "webActionResult":
print(f"{msg['payload']['action']} delivered to {msg['payload']['hosts']} host(s)")
elif msg["type"] == "slide":
print("Slide:", msg["payload"]["number"] if msg["payload"] else "no deck open")
elif msg["type"] == "error":
print("Error:", msg["error"], msg["payload"]["message"])
ws = websocket.WebSocketApp(f"$WS_URL/ws?token={access_token}", on_message=on_message)
ws.run_forever()
import (
"encoding/json"
"fmt"
"log"
"sync"
"time"
"github.com/gorilla/websocket"
)
conn, _, err := websocket.DefaultDialer.Dial("$WS_URL/ws?token="+accessToken, nil)
if err != nil {
log.Fatal("dial:", err)
}
defer conn.Close()
var writeMu sync.Mutex // one writer at a time
send := func(message interface{}) {
writeMu.Lock()
defer writeMu.Unlock()
conn.WriteJSON(message)
}
go func() { // keeps the connection open
for range time.Tick(15 * time.Second) {
writeMu.Lock()
conn.WriteMessage(websocket.TextMessage, []byte("ping"))
writeMu.Unlock()
}
}()
ready := false
for {
_, data, err := conn.ReadMessage()
if err != nil {
break
}
if string(data) == "pong" {
continue
}
var msg struct {
Type string `json:"type"`
Error string `json:"error"`
Payload json.RawMessage `json:"payload"`
}
if json.Unmarshal(data, &msg) != nil {
continue
}
switch msg.Type {
case "connected":
send(map[string]interface{}{
"type": "joinRoom",
"payload": map[string]interface{}{"room": "ABC123", "presenterName": "Show control", "api": true},
})
case "clicksEnabled":
if string(msg.Payload) == "true" && !ready {
ready = true
send(map[string]interface{}{
"type": "webAction",
"payload": map[string]interface{}{"action": "next", "id": "a1"},
})
// Also: {"action": "goto", "slide": 5}, {"action": "blackout", "on": true}, ...
}
case "webActionResult", "slide", "error":
fmt.Printf("%s %s %s\n", msg.Type, msg.Error, msg.Payload)
}
}
use WebSocket\Client;
use WebSocket\TimeoutException;
// A 15 second read timeout doubles as the keepalive timer.
$client = new Client("$WS_URL/ws?token={$accessToken}", ['timeout' => 15]);
$ready = false;
while (true) {
try {
$message = $client->receive();
} catch (TimeoutException $e) {
$client->text('ping'); // keeps the connection open
continue;
}
if ($message === null) break; // closed
if ($message === 'pong') continue;
$msg = json_decode($message, true);
if ($msg['type'] === 'connected') {
$client->text(json_encode([
'type' => 'joinRoom',
'payload' => ['room' => 'ABC123', 'presenterName' => 'Show control', 'api' => true]
]));
} elseif ($msg['type'] === 'clicksEnabled' && $msg['payload'] === true && !$ready) {
$ready = true;
$client->text(json_encode([
'type' => 'webAction',
'payload' => ['action' => 'next', 'id' => 'a1']
]));
// Also: ['action' => 'goto', 'slide' => 5], ['action' => 'blackout', 'on' => true], ...
} elseif (in_array($msg['type'], ['webActionResult', 'slide', 'error'], true)) {
echo json_encode($msg), PHP_EOL;
}
}
use futures_util::{SinkExt, StreamExt};
use serde_json::{json, Value};
use tokio_tungstenite::{connect_async, tungstenite::Message};
let url = format!("$WS_URL/ws?token={}", access_token);
let (ws_stream, _) = connect_async(&url).await?;
let (mut write, mut read) = ws_stream.split();
let every = std::time::Duration::from_secs(15);
let mut keepalive = tokio::time::interval_at(tokio::time::Instant::now() + every, every);
let mut ready = false;
loop {
tokio::select! {
_ = keepalive.tick() => write.send(Message::Text("ping".into())).await?, // keeps the connection open
frame = read.next() => {
let Some(frame) = frame else { break };
let Message::Text(text) = frame? else { continue };
if text.as_str() == "pong" { continue }
let msg: Value = serde_json::from_str(&text)?;
match msg["type"].as_str() {
Some("connected") => {
let join = json!({"type": "joinRoom", "payload": {"room": "ABC123", "presenterName": "Show control", "api": true}});
write.send(Message::Text(join.to_string().into())).await?;
}
Some("clicksEnabled") if msg["payload"] == true && !ready => {
ready = true;
let next = json!({"type": "webAction", "payload": {"action": "next", "id": "a1"}});
write.send(Message::Text(next.to_string().into())).await?;
// Also: {"action": "goto", "slide": 5}, {"action": "blackout", "on": true}, ...
}
Some("webActionResult") | Some("slide") | Some("error") => println!("{}", msg),
_ => {}
}
}
}
}
Wyniki i zdarzenia
Na każde polecenie przychodzi odpowiedź webActionResult lub error.
{ "type": "webActionResult", "payload": { "action": "next", "id": "a1", "hosts": 1 } }
hosts to liczba hostów, do których polecenie zostało dostarczone. Dowodem jego wykonania jest kolejne zdarzenie slide od hosta; blackout zmienia też wartość blackout w kolejnym zdarzeniu controls hosta. Polecenie, którego host nie wykona (kliknięcia wyłączone, nieobsługiwane polecenie, goto poza ostatni slajd), otrzymuje wynik, ale nie wywołuje zdarzenia slide.
error | Kiedy |
|---|---|
auth_required | joinRoom z api: true na połączeniu bez tokenu sesji |
not_api_client | Polecenie z połączenia, które nie dołączyło do pokoju z api: true lub już go opuściło |
invalid_web_action | Nieznana wartość action albo brakujące lub nieprawidłowe slide, on lub id |
invalid_timer_action | Nieznana wartość action timera albo brakujące lub nieprawidłowe minutes lub id |
invalid_message | Wiadomość bez text lub dłuższa niż 1000 znaków |
invalid_presenter_mouse | Pozycja wskaźnika poza zakresem od 0 do 1 |
rate_limited | Ponad 10 poleceń slajdów i timera w ciągu jednej sekundy lub ponad 5 wiadomości w ciągu 10 sekund |
no_host | Z pokojem nie jest połączony żaden host |
host_update_required | Każdy host w pokoju używa starszej wersji Cliqer, która nie przyjmuje poleceń z API |
api_not_in_plan | Plan właściciela pokoju nie obejmuje Client API |
Błędy zawierają czytelny komunikat message oraz, jeśli polecenie było poprawnie sformułowane, jego action i id:
{ "type": "error", "error": "invalid_web_action", "payload": { "message": "Unknown or incomplete webAction. Use next, prev, first, last, goto with slide, or blackout with on.", "action": "goto", "id": "a3" } }
Zdarzenia od hosta zawierają jego identyfikator w hostId:
| Zdarzenie | Payload |
|---|---|
clicksEnabled | true, gdy host przyjmuje Twoje polecenia, false, gdy operator wyłączył Twoje kliknięcia |
controls | { app, nav, blackout, timer }: aplikacja do prezentacji, dodatkowe obsługiwane przez nią polecenia slajdów (first, last, goto, blackout), informacja, czy ekran jest wygaszony, oraz czy host przyjmuje polecenia timera |
slide | { number, current, total, title, notes } po każdym poleceniu wykonanym przez hosta (number to "4/12", total ma wartość null, gdy aplikacja go nie podaje) lub null, gdy host nie ma otwartej prezentacji |
countdown | Czas wyświetlany przez timer hosta, np. "4:59" ("-0:12" po przekroczeniu czasu), co sekundę, gdy timer działa, lub null, gdy się zatrzyma |
countdownPhase | { phase, blink }: phase to idle, talk, warning (zostało mniej niż minuta), overtime lub paused, a blink określa, czy timer hosta miga |
messageFromHost | Odpowiedź od operatora hosta, zobacz Wiadomości do hosta |
Otrzymujesz też własne zdarzenia pokoju (clientJoined, clientLeft i inne); ignoruj te, których nie używasz. Jeśli operator usunie lub zablokuje Twoje połączenie, otrzymasz logout z reason: "kicked".
Timer
Steruj timerem w aplikacji Cliqer hosta za pomocą tych samych poleceń, które wysyła Stream Deck lub Bitfocus Companion.
{ "type": "timerAction", "payload": { "action": "set", "minutes": 25, "id": "t1" } }
action | Działanie |
|---|---|
start | Uruchamia timer lub wznawia go po stop |
stop | Wstrzymuje timer |
reset | Zatrzymuje timer i przywraca ustawiony czas |
plus1, plus2, plus5 | Dodaje 1, 2 lub 5 minut |
minus1, minus2, minus5 | Odejmuje 1, 2 lub 5 minut |
preset5, preset10, preset15, preset20 | Zatrzymuje timer i ustawia 5, 10, 15 lub 20 minut |
set | Zatrzymuje timer i ustawia minutes, liczbę całkowitą od 1 do 999 |
- Na każde polecenie przychodzi odpowiedź
timerActionResult({ action, id, hosts },hostsjak w przypadku slajdów) luberror. Dowodem wykonania jest kolejne zdarzeniecountdownlubcountdownPhaseod hosta. - Timer działa tylko na hoście, którego najnowsze zdarzenie
controlszawieratimer: true. Hosty ze starszą wersją Cliqer tego nie ogłaszają i ignorują polecenia timera. - Polecenia timera podlegają tym samym zasadom co polecenia slajdów: host wykonuje je tylko wtedy, gdy zezwala na kliknięcia z Twojego połączenia, a limit 10 poleceń na sekundę jest dla nich wspólny.
- Ustawienia timera hosta obowiązują bez zmian: timer ustawiony na liczenie w górę liczy w górę, a timer powiązany z podłączonym timerem sprzętowym pobiera czas z tego urządzenia.
Wskaźnik laserowy
Wyświetlaj kropkę lasera na ekranie udostępnianym przez hosta, tak jak robi to telefon prezentera.
{ "type": "presenterMouse", "payload": { "mouse": { "x": 0.5, "y": 0.25 } } }
xiyto pozycja na udostępnianym ekranie liczona od jego lewego górnego rogu, od0do1.- Wyślij
{ "pressed": false }jako payload, aby ukryć kropkę. W przeciwnym razie zniknie ona od 30 do 90 sekund po wysłaniu ostatniej pozycji. - Kropka jest widoczna tylko wtedy, gdy host udostępnia ekran i ma włączony Wskaźnik laserowy. Jej kolor zależy od Twojego
presenterName. - Wysyłaj maksymalnie 30 pozycji na sekundę. Nadmiarowe pozycje są odrzucane bez błędu i nie ma zdarzenia wyniku.
Wiadomości do hosta
Wyślij wiadomość do operatora hosta. Pojawi się ona w czacie prezenterów u hosta, tak samo jak wiadomość z telefonu prezentera, a odpowiedzi operatora wracają jako messageFromHost.
# Join as an API client, then send a message. Replies print as they arrive.
( sleep 2
echo '{"type":"joinRoom","payload":{"room":"ABC123","presenterName":"Show control","api":true}}'
sleep 3
echo '{"type":"messageToHosts","payload":{"text":"Two minutes to the break","id":"m1"}}'
sleep 30 ) | websocat "$WS_URL/ws?token=YOUR_SESSION_TOKEN"
// After joining (see Slide Navigation): send a message, print the replies.
send({ type: 'messageToHosts', payload: { text: 'Two minutes to the break', id: 'm1' } })
ws.addEventListener('message', (event) => {
if (event.data === 'pong') return
const msg = JSON.parse(event.data)
if (msg.type === 'messageToHostsResult') console.log(`Delivered to ${msg.payload.hosts} host(s)`)
if (msg.type === 'messageFromHost') console.log(`${msg.payload.fromPresenterName}: ${msg.payload.originalPayload.text}`)
})
# After joining (see Slide Navigation): send a message.
send(ws, {"type": "messageToHosts", "payload": {"text": "Two minutes to the break", "id": "m1"}})
# In on_message, print the replies:
# elif msg["type"] == "messageFromHost":
# print(f"{msg['payload']['fromPresenterName']}: {msg['payload']['originalPayload']['text']}")
// After joining (see Slide Navigation): send a message.
send(map[string]interface{}{
"type": "messageToHosts",
"payload": map[string]interface{}{"text": "Two minutes to the break", "id": "m1"},
})
// In the read loop, print the replies:
// case "messageFromHost":
// fmt.Printf("%s\n", msg.Payload)
// After joining (see Slide Navigation): send a message.
$client->text(json_encode([
'type' => 'messageToHosts',
'payload' => ['text' => 'Two minutes to the break', 'id' => 'm1']
]));
// In the read loop, print the replies:
// } elseif ($msg['type'] === 'messageFromHost') {
// echo $msg['payload']['originalPayload']['text'], PHP_EOL;
// After joining (see Slide Navigation): send a message.
let message = json!({"type": "messageToHosts", "payload": {"text": "Two minutes to the break", "id": "m1"}});
write.send(Message::Text(message.to_string().into())).await?;
// In the read loop, print the replies:
// Some("messageFromHost") => println!("{}", msg["payload"]["originalPayload"]["text"]),
Odpowiedź od hosta:
{
"type": "messageFromHost",
"payload": {
"fromClientId": "host_abc123",
"fromPresenterName": "Stage PC",
"originalPayload": { "text": "Thanks, got it" }
},
"hostId": "host_abc123"
}
textma od 1 do 1000 znaków. Wysyłaj maksymalnie 5 wiadomości na 10 sekund.- Na każdą wiadomość przychodzi odpowiedź
messageToHostsResult({ id, hosts }) luberror. - Operator widzi wiadomość pod Twoją nazwą
presenterName, a host wyświetla powiadomienie, gdy ma włączone ustawienie Powiadomienia.
Lista zdarzeń
Zdarzenia wychodzące (klient → serwer)
| Zdarzenie | Opis |
|---|---|
joinRoom | Dołączenie do pokoju jako klient API z api: true (zobacz Nawigacja po slajdach) |
webAction | Sterowanie slajdami (zobacz Nawigacja po slajdach) |
timerAction | Sterowanie timerem (zobacz Timer) |
presenterMouse | Pozycja wskaźnika laserowego (zobacz Wskaźnik laserowy) |
messageToHosts | Wysłanie wiadomości do operatora hosta (zobacz Wiadomości do hosta) |
logout | Opuszczenie pokoju; połączenie pozostaje otwarte i może ponownie dołączyć |
Zdarzenia przychodzące (serwer → klient)
| Zdarzenie | Opis |
|---|---|
connected | Wysyłane po otwarciu połączenia z { clientId, userId, isAuthenticated } |
joinedRoom | Pomyślnie dołączono do pokoju |
clientJoined | Ktoś dołączył do pokoju |
clientLeft | Ktoś opuścił pokój |
webActionResult | webAction zostało dostarczone do hostów pokoju |
timerActionResult | timerAction zostało dostarczone do hostów pokoju |
messageToHostsResult | messageToHosts zostało dostarczone do hostów pokoju |
clicksEnabled | Czy host przyjmuje Twoje polecenia |
controls | Co obsługują aplikacja do prezentacji i timer hosta |
slide | Bieżący slajd hosta po poleceniu |
countdown | Czas na timerze hosta |
countdownPhase | Faza timera hosta |
messageFromHost | Wiadomość od operatora hosta |
logout | Operator usunął lub zablokował Twoje połączenie (reason: "kicked") |
error | Komunikat o błędzie |