Getting Started — Vibe Coding Guide
What is this?
HoboStreamer is a self-hosted, open-source live streaming platform with interactive controls, chat bots, TTS, soundboards, and hardware bridges. This documentation is designed for vibe coding — copy the section you need into your LLM and start building.
Quick Architecture Overview
| Layer | Tech | Notes |
| Backend | Node.js + Express | All routes return JSON |
| Database | SQLite (better-sqlite3) | WAL mode, foreign keys |
| WebRTC SFU | Mediasoup | Video/audio routing |
| RTMP | Node-Media-Server | OBS classic ingest |
| Chat/Control | WebSocket (ws) | Real-time messaging |
| Frontend | Vanilla JS SPA | No framework |
| Auth | RS256 JWT via hobo.tools SSO | Shared across Hobo Network |
Which doc to copy for which task?
| Building a... | Copy this tab |
| Chat bot / overlay | Chat — WS protocol, bot examples, commands |
| Robot/hardware bridge | Control WS — full Python + Node.js bridge examples |
| Custom stream player | WebRTC — WHIP, SFU, SDP protocol |
| Stream management tool | Streams — CRUD, go-live, channels |
| TTS / soundboard bot | TTS & Audio — voice catalog, audio queue, 101soundboards |
| Discord/push integration | Integrations — events, webhooks, push API |
| Control panel / buttons | Controls — profiles, button schema, settings |
| Admin tooling | API Ref — full endpoint list, rate limits |
How the Hobo Network is split
| Service | URL | Owns |
| HoboStreamer.com | hobostreamer.com | Streaming, chat, TTS, controls, VODs, soundboard |
| hobo.tools | hobo.tools / my.hobo.tools | SSO, accounts, notifications, push, Discord bot, admin |
| hobo.quest | hobo.quest | Games (RPG, canvas) |
Base URL & Auth
Base URL: https://hobostreamer.com (replace with your instance).
Auth: Authorization: Bearer <JWT> header, or ?token=JWT query param on WebSocket URLs.
API tokens: hbt_ prefix, scoped permissions, created from Dashboard → API Tokens.
Network Architecture
Service Map
┌──────────────────────────────────────────────────────┐
│ hobo.tools (port 3100) — Central Hub │
│ ├─ /api/auth — SSO (RS256 JWT) │
│ ├─ /api/notifications — Notification CRUD │
│ ├─ /api/push — Web Push (VAPID) │
│ ├─ /api/admin — Unified admin panel │
│ ├─ /api/admin/discord — Discord bot config │
│ ├─ /internal — Server-to-server API │
│ └─ Discord bot — Live alerts, system alerts │
├──────────────────────────────────────────────────────┤
│ HoboStreamer.com (port 3000) — Streaming Platform │
│ ├─ /api/streams — Stream CRUD + go-live │
│ ├─ /api/chat — Chat REST API │
│ ├─ /api/tts — TTS voice config │
│ ├─ /api/controls — Interactive control profiles │
│ ├─ /ws/chat — Real-time chat │
│ ├─ /ws/broadcast — WebRTC signaling │
│ ├─ /ws/control — Hardware command relay │
│ ├─ /whip/:id — WHIP WebRTC ingestion │
│ └─ RTMP server — rtmp://host/live │
└──────────────────────────────────────────────────────┘
Event Flow: Stream Goes Live
1. Streamer calls POST /api/streams (HoboStreamer)
2. HoboStreamer creates stream record, starts media endpoint
3. HoboStreamer calls POST /internal/events/stream-live (hobo.tools)
4. hobo.tools Discord bot sends embed to #alerts channel (with dedupe)
5. hobo.tools creates STREAM_LIVE notifications for:
- All followers of the streamer
- Users opted into "all live" alerts
6. Push notifications fire via Web Push (VAPID)
7. Fallback: HoboStreamer sends direct webhook if hobo.tools unreachable
WebSocket Services
| Path | Purpose | Auth |
| /ws/chat | Chat rooms (per-stream or global) | Optional (anon allowed) |
| /ws/broadcast | WebRTC SFU signaling | JWT required |
| /ws/control | Interactive controls relay (viewer ↔ hardware) | JWT or API key |
| /ws/call | Group voice/video calls | JWT required |
Admin Proxy Architecture
hobo.tools admin panel proxies requests to HoboStreamer:
hobo.tools /api/admin/streamer/* → HoboStreamer /api/admin/*
hobo.tools /api/admin/streamer-tts/* → HoboStreamer /api/tts/*
hobo.tools /api/admin/streamer-mod/* → HoboStreamer /api/mod/*
Protected by X-Internal-Key header for server-to-server calls.
1. AUTHENTICATION
REST Endpoints
| Method | Path | Auth | Body / Notes |
| POST | /api/auth/register | No | { username, email, password } |
| POST | /api/auth/login | No | { username, password } → { token, user } |
| GET | /api/auth/me | Yes | Returns current user profile |
| PUT | /api/auth/profile | Yes | Update avatar, bio, profile_color, etc. |
API Tokens (hbt_ prefix)
For bots and integrations that need long-lived auth without JWTs:
| Method | Path | Description |
| POST | /api/auth/tokens | Create token { name, scopes[], expires_in_days? } |
| GET | /api/auth/tokens | List your tokens (masked) |
| DELETE | /api/auth/tokens/:id | Revoke token |
Use in headers: Authorization: Bearer hbt_xxxxxxxxxxxx
Example: Login and get token
const res = await fetch('https://hobostreamer.com/api/auth/login', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ username: 'yourname', password: 'yourpass' })
});
const { token, user } = await res.json();
// Use token in all subsequent requests
2. STREAMS & CHANNELS
Stream REST API
| Method | Path | Auth | Description |
| GET | /api/streams | No | List all live streams |
| POST | /api/streams | Yes | Go live. Body: { title, description, category, protocol, tags } |
| GET | /api/streams/:id | No | Stream details (endpoint, controls, cameras) |
| PUT | /api/streams/:id | Yes | Update title/description/category/tags/nsfw |
| DELETE | /api/streams/:id | Yes | End stream |
| GET | /api/streams/:id/endpoint | Yes | Get WHIP URL, RTMP key, ffmpeg commands |
| POST | /api/streams/:id/heartbeat | Yes | Keep stream alive — call every 30s |
| GET | /api/streams/channel/:username | No | Full channel page data (streams, VODs, clips, panels) |
| GET | /api/streams/channel/:username/live | No | Lightweight live status for fast player init |
| PUT | /api/streams/channel | Yes | Update own channel settings |
| POST | /api/streams/channel/:username/follow | Yes | Follow / unfollow |
| GET | /api/streams/mine | Yes | Your streams (active and recent) |
Supported Ingestion Protocols
- webrtc — Mediasoup SFU. Lowest latency (~200ms). Use browser or WHIP encoder (OBS 30+).
- rtmp — Traditional RTMP ingest (OBS/ffmpeg). Plays back as HTTP-FLV.
- jsmpeg — MPEG1 over WebSocket. Runs on Raspberry Pi and low-power hardware.
Example: Create a stream and get the WHIP URL
const stream = await fetch('https://hobostreamer.com/api/streams', {
method: 'POST',
headers: { 'Authorization': `Bearer ${token}`, 'Content-Type': 'application/json' },
body: JSON.stringify({ title: 'Robot cam', protocol: 'webrtc', category: 'Technology' })
}).then(r => r.json());
const endpoint = await fetch(`https://hobostreamer.com/api/streams/${stream.id}/endpoint`, {
headers: { 'Authorization': `Bearer ${token}` }
}).then(r => r.json());
console.log(endpoint.whip_url); // POST SDP offer here
// Remember to call /heartbeat every 30s to keep stream alive
3. WEBRTC BROADCAST PROTOCOL
WHIP Ingestion (OBS / ffmpeg / custom encoder)
RFC 9725 compliant. POST a raw SDP offer, receive SDP answer.
| Method | Path | Notes |
| POST | /whip/:streamId | Body: raw SDP (Content-Type: application/sdp). Auth: Bearer JWT. Returns SDP answer + Location header. |
| PATCH | /whip/:streamId/:resourceId | Trickle ICE (Content-Type: application/trickle-ice-sdpfrag) |
| DELETE | /whip/:streamId/:resourceId | Terminate WHIP session |
Broadcast WebSocket — /ws/broadcast
Connect: wss://hobostreamer.com/ws/broadcast?streamId=ID&role=broadcaster|viewer&token=JWT
All messages are JSON. Send: ws.send(JSON.stringify({ type: 'watch' }))
| Type | Dir | Key Fields | Description |
| welcome | S→C | peerId, role, streamId, viewerCount, iceServers | Sent immediately on connect |
| watch | C→S | — | Viewer requests to start watching (triggers SFU consumer) |
| broadcaster-ready | S→Viewer | — | Broadcaster is live, safe to send watch |
| sfu-get-capabilities | C→S | — | Get Mediasoup RTP capabilities |
| sfu-create-transport | C→S | direction: send|recv | Create WebRTC transport |
| sfu-connect-transport | C→S | transportId, dtlsParameters | Connect DTLS |
| sfu-produce | C→S | transportId, kind, rtpParameters | Start producing audio/video |
| sfu-stop-produce | C→S | producerId | Stop a producer |
| offer / answer | Relay | sdp, targetPeerId | P2P SDP signaling fallback |
| ice-candidate | Relay | candidate, targetPeerId | P2P ICE (SFU handles ICE internally) |
| viewer-count | S→All | count | Updated viewer count broadcast |
| stream-ended | S→Viewers | — | Stream has ended |
RTMP Ingest (OBS)
Server: rtmp://hobostreamer.com/live
Key: YOUR_STREAM_KEY (get from Dashboard → Stream Key)
# Playback proxy for browser
GET /api/streams/rtmp-proxy/:streamId.flv
4. CHAT SYSTEM — CHATBOT GUIDE
Connect to the chat WebSocket to read and send messages. Perfect for building chat bots, overlays, alert systems, and integrations.
Chat WebSocket — /ws/chat
Connect: wss://hobostreamer.com/ws/chat?stream=STREAM_ID&token=JWT
Both params are optional: omit stream for global chat; omit token to connect anonymously (assigned anonXXXXX identity).
Client → Server Messages
| Type | Payload | Description |
| chat | { message, reply_to_id?, auto_delete_minutes? } | Send a message (max 500 chars) |
| join_stream | { streamId } | Join a specific stream's chat room |
| leave_stream | {} | Leave current stream chat |
| get-users | {} | Request current user list |
Server → Client Messages
| Type | Key Fields | Description |
| auth | authenticated, username, role, user_id, slowmode_seconds, slur_filter_enabled | Identity confirmation. First message after connect. |
| chat | username, core_username, user_id, anon_id, role, message, stream_id, is_global, timestamp, id, reply_to?, avatar_url, profile_color, filtered | A chat message. is_global=true means not tied to a stream. |
| system | message, timestamp | System notice (joins, events) |
| user-count | count | Updated viewer/chatter count |
| users-list | users: { logged: [{username, role, avatar_url}], anonCount } | Full user list (after get-users) |
| error | message | Error (banned, rate limited, etc.) |
| slur-blocked | message | Your message was blocked by the anti-slur filter |
| coin_earned | coins, total, reason | Hobo Coins earned notification |
Chat Bot — Complete Example (Node.js)
const WebSocket = require('ws');
const WS_URL = 'wss://hobostreamer.com/ws/chat?stream=123&token=YOUR_JWT';
const ws = new WebSocket(WS_URL);
ws.on('open', () => console.log('Bot connected'));
ws.on('message', (raw) => {
const msg = JSON.parse(raw);
if (msg.type === 'auth') {
console.log('Logged in as:', msg.username);
}
if (msg.type === 'chat') {
console.log(`[${msg.username}] ${msg.message}`);
// Respond to commands
if (msg.message.startsWith('!hello')) {
ws.send(JSON.stringify({
type: 'chat',
message: `Hey @${msg.username}! 👋`
}));
}
if (msg.message.startsWith('!time')) {
ws.send(JSON.stringify({
type: 'chat',
message: `The time is ${new Date().toLocaleTimeString()}`
}));
}
}
});
ws.on('close', () => setTimeout(() => reconnect(), 5000));
Chat Bot — Complete Example (Python)
import json, websocket, time
TOKEN = 'YOUR_JWT_TOKEN'
STREAM_ID = '123'
WS_URL = f'wss://hobostreamer.com/ws/chat?stream={STREAM_ID}&token={TOKEN}'
def send(ws, message):
ws.send(json.dumps({ 'type': 'chat', 'message': message }))
def on_message(ws, raw):
msg = json.loads(raw)
if msg.get('type') == 'auth':
print(f"Connected as {msg.get('username')}")
if msg.get('type') == 'chat':
user = msg.get('username', '?')
text = msg.get('message', '')
print(f'[{user}] {text}')
if text.startswith('!hello'):
send(ws, f'Hey @{user}!')
if text.startswith('!uptime'):
send(ws, f'Bot uptime: {int(time.time() - start_time)}s')
def on_open(ws): print('Connected')
def on_close(ws, *a): print('Disconnected — reconnecting...')
def on_error(ws, e): print(f'Error: {e}')
start_time = time.time()
while True:
ws = websocket.WebSocketApp(WS_URL, on_message=on_message,
on_open=on_open, on_close=on_close, on_error=on_error)
ws.run_forever()
time.sleep(5)
Chat Commands (built-in)
| Command | Description |
| /tts <text> | Text-to-speech via TTS system |
| /color #hex | Set your chat name color |
| !sr / !yt / !request <url> | Media / song request |
| !queue | Show media queue |
| !np / !nowplaying | Now playing |
User Roles
| Role | Description |
| admin | Platform admin |
| mod | Platform moderator |
| streamer | Registered streamer |
| user | Registered viewer |
| anon | Anonymous visitor (anonXXXXX identity) |
Chat REST API
| Method | Path | Auth | Description |
| POST | /api/chat/send | Yes | Send message via REST (fallback) |
| GET | /api/chat/:streamId/history | No | Chat history for a stream |
| GET | /api/chat/:streamId/users | No | Users in a stream's chat |
| GET | /api/chat/user/:username/profile | No | User profile card data |
5. INTERACTIVE CONTROL SYSTEM — ROBOT/HARDWARE GUIDE
The control system lets streamers expose interactive buttons that viewers press to control physical hardware (robots, cameras, servo rigs, etc.). Three interaction types: single-press buttons, keyboard hold (continuous drive), and video-click (x/y coordinates on the live video feed).
Control Config Profiles
Streamers create reusable control configs. Each config holds buttons. When going live, a config is applied to the stream — its buttons get copied to that stream session.
| Method | Path | Auth | Description |
| GET | /api/controls/configs | Yes | List your configs (with button count) |
| POST | /api/controls/configs | Yes | Create config { name, description } |
| GET | /api/controls/configs/:id | Yes | Config with full buttons array |
| PUT | /api/controls/configs/:id | Yes | Update name / description |
| DELETE | /api/controls/configs/:id | Yes | Delete config |
| POST | /api/controls/configs/:id/buttons | Yes | Add button to config |
| PUT | /api/controls/configs/:id/buttons/:btnId | Yes | Update button |
| DELETE | /api/controls/configs/:id/buttons/:btnId | Yes | Delete button |
| POST | /api/controls/configs/:id/activate | Yes | Set as channel default config |
| POST | /api/controls/configs/deactivate | Yes | Clear channel default |
| POST | /api/controls/configs/:id/apply/:streamId | Yes | Copy config buttons onto a live stream |
| GET | /api/controls/configs/:id/bridge-script?type=generic|cozmo | Yes | Download a Python hardware bridge script pre-configured with the profile's buttons |
Button Schema
{
"label": "Forward", // Display name shown to viewers (max 50)
"command": "forward", // String sent to your hardware bridge (max 100)
"icon": "fa-arrow-up", // FontAwesome icon class (without fa-solid prefix)
"control_type": "keyboard", // "button" | "keyboard" | "toggle" | "dpad"
"key_binding": "w", // Optional keyboard shortcut for viewers (max 20)
"cooldown_ms": 100, // Per-press cooldown in ms (0-30000)
"sort_order": 0, // Display order (ascending)
"btn_color": "#ffffff", // Button text color (CSS color — safe values only)
"btn_bg": "#1a1a2e", // Button background color
"btn_border_color": "#6366f1", // Button border color
"is_enabled": 1 // 1=shown, 0=hidden
}
Control Type Behaviour
| control_type | Viewer Interaction | Messages Sent |
| button | Single click or key tap | command |
| keyboard | Hold mouse/touch/key → continuous. Release → stop | key_down on press, key_up on release |
| toggle | Click to toggle on/off | command (on) / command (off) |
| dpad | D-pad style directional | command |
Channel Control Settings
| Method | Path | Description |
| GET | /api/controls/settings/channel | Get settings |
| PUT | /api/controls/settings/channel | Update settings |
{
"control_mode": "open", // "open" | "whitelist" | "disabled"
"anon_controls_enabled": true, // Allow anonymous viewers to use controls
"control_rate_limit_ms": 100, // Global rate limit per user (100-30000)
"video_click_enabled": true, // Enable video click input
"video_click_rate_limit_ms": 0 // Per-user minimum time between video clicks; 0 = use global rate limit
}
Per-Stream Controls (Legacy / Direct)
| Method | Path | Description |
| GET | /api/controls/:streamId | Get all controls + settings for a stream |
| POST | /api/controls/:streamId | Add a button directly to active stream |
| PUT | /api/controls/:streamId/:id | Update control |
| DELETE | /api/controls/:streamId/:id | Delete control |
| POST | /api/controls/:streamId/presets/cozmo | Apply Cozmo robot preset buttons |
6. CONTROL WEBSOCKET — HARDWARE BRIDGE
This is how your code receives viewer commands. Connect as mode=hardware and listen for messages. Your bridge translates them into robot/hardware actions.
Connection URLs
# Hardware bridge (your robot/device — receives commands)
wss://hobostreamer.com/ws/control?mode=hardware&stream_key=YOUR_STREAM_KEY
# Viewer (browser/bot — sends commands)
wss://hobostreamer.com/ws/control?mode=viewer&token=JWT&stream=STREAM_ID
Hardware Receives These Messages
| Type | Payload Fields | When |
| connected | message | Immediately on connect — confirms auth |
| command | command, control_id, from_user, timestamp | Viewer clicked a button (button/toggle/dpad type) |
| key_down | command, control_id, from_user, timestamp | Viewer started holding a keyboard-type button |
| key_up | command, control_id, from_user, timestamp | Viewer released the button |
| video_click | x (0-1), y (0-1), from_user, timestamp | Viewer clicked on the video feed. 0,0 = top-left, 1,1 = bottom-right |
Hardware Can Send
| Type | Payload | Effect |
| status | { any fields } | Relayed to all viewers as hardware_status message |
Viewer Sends These Messages
| Type | Payload | Description |
| command | { command, control_id } | Single button press |
| key_down | { command, control_id } | Start holding (keyboard type) |
| key_up | { command, control_id } | Release hold |
| video_click | { x: 0-1, y: 0-1 } | Click on video feed |
Viewer Receives These Messages
| Type | Key Fields | Description |
| controls | controls[], settings{} | Initial buttons + channel settings on connect |
| command_executed | command, by | Someone pressed a button (activity feed) |
| key_held | command, by | Someone started holding a key |
| key_released | command, by | Someone released a key |
| video_click_activity | x, y, by | Someone clicked the video |
| hardware_status | any | Status update from hardware bridge |
| error | message | Denied / not live / banned |
| cooldown | message | Rate limited |
Permission Model
| control_mode | Who can send commands |
| open | Any viewer (anon allowed if anon_controls_enabled=true) |
| whitelist | Stream owner + explicitly whitelisted users only |
| disabled | Nobody — controls are turned off |
Hardware Bridge — Python (Full Example)
#!/usr/bin/env python3
"""
HoboStreamer Hardware Bridge — generic template.
pip install websocket-client
"""
import json, time, threading, websocket
STREAM_KEY = 'YOUR_STREAM_KEY'
WS_URL = f'wss://hobostreamer.com/ws/control?mode=hardware&stream_key={STREAM_KEY}'
held_keys = set() # Track currently held keyboard commands
def do_command(cmd):
"""Map command strings to your hardware."""
print(f'Command: {cmd}')
# Example: call GPIO, serial, HTTP, etc.
# if cmd == 'forward': robot.drive(speed=100)
def do_start_hold(cmd):
"""Called when a keyboard-type button starts being held."""
held_keys.add(cmd)
print(f'Hold start: {cmd}')
def do_stop_hold(cmd):
"""Called when a keyboard-type button is released."""
held_keys.discard(cmd)
print(f'Hold stop: {cmd}')
if not held_keys:
print('All keys released — stop movement')
# robot.stop()
def do_video_click(x, y):
"""Called when a viewer clicks the video feed. x,y are 0-1 normalized."""
print(f'Video click at ({x:.2f}, {y:.2f})')
# Navigate toward click: x < 0.4 = turn left, x > 0.6 = turn right
# y close to 0 = far away (drive more), y close to 1 = near (drive less)
def on_message(ws, raw):
try:
msg = json.loads(raw)
t = msg.get('type')
if t == 'connected':
print('Hardware bridge authenticated!')
elif t == 'command':
do_command(msg['command'])
elif t == 'key_down':
do_start_hold(msg['command'])
elif t == 'key_up':
do_stop_hold(msg['command'])
elif t == 'video_click':
do_video_click(msg['x'], msg['y'])
except Exception as e:
print(f'Error: {e}')
def on_open(ws): print('Connected')
def on_close(ws, *a): print('Disconnected')
def on_error(ws, e): print(f'WS error: {e}')
while True:
ws = websocket.WebSocketApp(WS_URL, on_message=on_message,
on_open=on_open, on_close=on_close, on_error=on_error)
ws.run_forever(ping_interval=30)
print('Reconnecting in 5s...')
time.sleep(5)
Hardware Bridge — Node.js (Full Example)
const WebSocket = require('ws');
const STREAM_KEY = 'YOUR_STREAM_KEY';
const WS_URL = `wss://hobostreamer.com/ws/control?mode=hardware&stream_key=${STREAM_KEY}`;
const heldKeys = new Set();
function connect() {
const ws = new WebSocket(WS_URL);
ws.on('message', (raw) => {
const msg = JSON.parse(raw);
switch (msg.type) {
case 'connected':
console.log('Hardware bridge authenticated!');
break;
case 'command':
console.log(`Button: ${msg.command} by ${msg.from_user}`);
doCommand(msg.command);
break;
case 'key_down':
heldKeys.add(msg.command);
console.log(`Hold start: ${msg.command}`);
startContinuous(msg.command);
break;
case 'key_up':
heldKeys.delete(msg.command);
console.log(`Hold stop: ${msg.command}`);
if (heldKeys.size === 0) stopAll();
break;
case 'video_click':
console.log(`Click at (${msg.x.toFixed(2)}, ${msg.y.toFixed(2)}) by ${msg.from_user}`);
navigateToClick(msg.x, msg.y);
break;
}
});
ws.on('close', () => setTimeout(connect, 5000));
ws.on('error', (e) => console.error(e));
}
function doCommand(cmd) { /* implement hardware control */ }
function startContinuous(cmd) { /* start motors etc. */ }
function stopAll() { /* stop all motors */ }
function navigateToClick(x, y) { /* steer toward x,y */ }
connect();
API Keys (for hardware bridges without JWT)
| Method | Path | Description |
| POST | /api/controls/api-key | Generate API key (shown once — save it) |
| GET | /api/controls/api-keys | List keys (masked) |
| GET | /api/controls/cozmo-script | Download pre-built Cozmo robot bridge Python script |
Per-Profile Bridge Script Generator
Download Python bridge scripts that come pre-configured with your control profile's buttons and your stream key. Available from the Dashboard (Hardware Bridge Scripts section) or the Broadcast page (Script button next to profile selector).
| Type | Description |
| generic | Prints all commands to console. Edit handle_command() / handle_key_down() / handle_key_up() to control your hardware. Works with any device — GPIO, serial, HTTP, etc. |
| cozmo | Pre-mapped to pycozmo actions (drive, lift, head, animations). Includes COMMAND_MAP dict so you can remap your custom button commands to Cozmo movements. |
# Download via API:
GET /api/controls/configs/42/bridge-script?type=generic
GET /api/controls/configs/42/bridge-script?type=cozmo
# Both return a .py file download with:
# - Your stream key (auto-connects as hardware bridge)
# - All enabled buttons from the profile listed as BUTTONS
# - Skeleton handler functions ready to fill in
Hardware Status Indicator
When a hardware bridge connects, viewers see a live connection indicator:
| Indicator | Meaning |
| ⚫ (grey) | No controls configured for this stream |
| 🟡 (yellow) | Controls present but no hardware bridge connected |
| 🟢 (green) | Hardware bridge is online — commands will be received |
Hardware bridges send { type: "status", connected: true } on connect. The server broadcasts this to all viewers as a hardware_status message. Send a status with connected: true to turn the indicator green.
ONVIF Camera Control
Control PTZ cameras via ONVIF. Cameras appear as controls in the stream control panel.
| Method | Path | Description |
| POST | /api/onvif/discover | Auto-discover ONVIF cameras on local network |
| POST | /api/onvif/cameras | Add camera { host, port, username, password, name } |
| GET | /api/onvif/cameras | List cameras |
| PUT | /api/onvif/cameras/:id | Update camera |
| DELETE | /api/onvif/cameras/:id | Delete camera |
| GET | /api/onvif/cameras/:id/presets | List presets |
| POST | /api/onvif/cameras/:id/presets | Create preset |
ONVIF movements (send via control WS with isOnvif: true, cameraId: X, movement: Y):
pan_left, pan_right, tilt_up, tilt_down, zoom_in, zoom_out
TTS & AUDIO SYSTEM
Text-to-Speech
TTS reads chat messages, donation alerts, and custom text aloud on stream. Viewers hear it in the broadcast audio mix. Streamers configure which voice, volume, rate, and message types trigger TTS.
| Method | Path | Description |
| GET | /api/tts/settings | Current TTS config (voice, volume, rate, filters) |
| PUT | /api/tts/settings | Update TTS config |
| GET | /api/tts/voices | List available TTS voices |
| POST | /api/tts/speak | Queue a message for TTS { text, voice?, rate? } |
| POST | /api/tts/skip | Skip current TTS message |
TTS Settings Schema
{
"tts_enabled": true,
"tts_voice": "en-US-Standard-D",
"tts_volume": 0.8,
"tts_rate": 1.0,
"tts_read_chat": true,
"tts_read_username": true,
"tts_min_bits": 0,
"tts_max_length": 200,
"tts_filter_links": true,
"tts_filter_emotes": false
}
101Soundboards Integration
Viewers paste 101soundboards.com URLs in chat to play sound effects. The server fetches, caches, and routes audio through the TTS queue with optional pitch/speed modifiers.
Chat Syntax
# Basic — just paste the URL
https://www.101soundboards.com/sounds/12345
# With pitch modifier (cents — 100 = one semitone up)
https://www.101soundboards.com/sounds/12345 100p
# Negative pitch (down)
https://www.101soundboards.com/sounds/12345 -200p
# With speed modifier (1.0 = normal, 0.5-3.0 range)
https://www.101soundboards.com/sounds/12345 2.5
# Both pitch and speed
https://www.101soundboards.com/sounds/12345 150p 2
Channel Soundboard Settings
| Setting | Type | Default | Description |
| soundboard_enabled | boolean | true | Enable/disable soundboard in this channel |
| soundboard_allow_pitch | boolean | true | Allow pitch modifiers |
| soundboard_allow_speed | boolean | true | Allow speed modifiers |
| soundboard_banned_ids | string | "" | Comma-separated list of banned sound IDs |
Rate Limits
| Scope | Limit |
| Per user | One sound every 8 seconds |
| Per stream | 15 sounds per minute |
Audio Queue
TTS and soundboard audio share a single playback queue. Messages play in FIFO order. The audio element routes through a GainNode for volume control. Soundboard clips support playbackRate for pitch/speed modification.
INTEGRATIONS — DISCORD, PUSH, WEBHOOKS
Discord Bot (via hobo.tools)
The Discord bot runs on hobo.tools (not HoboStreamer). When a stream goes live, HoboStreamer fires an internal event, and the bot posts an embed in the configured alerts channel.
| Feature | Description |
| Live alerts | Rich embed with stream title, thumbnail, streamer, and link |
| Dedupe | Per-streamer cooldown (configurable, default 15 min) prevents spam |
| System alerts | Admin-triggered system notifications to a separate channel |
| Account linking | Users can link their Discord account via OAuth2 on my.hobo.tools |
Discord Admin API (hobo.tools)
| Method | Path | Description |
| GET | /api/admin/discord | Bot status + config (tokens masked) |
| PUT | /api/admin/discord | Update bot/OAuth settings |
| POST | /api/admin/discord/test | Send test system alert |
| POST | /api/admin/discord/test-live | Send test live alert |
| POST | /api/admin/discord/reinit | Reconnect Discord bot |
Discord Account Linking
| Method | Path | Description |
| GET | /api/auth/discord/link | Initiate Discord OAuth2 (redirects to Discord) |
| GET | /api/auth/discord/callback | OAuth callback (exchanges code, links account) |
| DELETE | /api/auth/discord/link | Unlink Discord account |
| GET | /api/auth/discord/status | Check if Discord is linked |
Web Push Notifications
Push notifications use VAPID (Web Push API). Users subscribe via their browser, and receive alerts for stream go-live events.
| Method | Path | Description |
| GET | /api/push/vapid-key | Get VAPID public key for subscription |
| POST | /api/push/subscribe | Register push subscription { endpoint, keys } |
| DELETE | /api/push/unsubscribe | Remove subscription |
| GET | /api/push/status | Subscription count + notification preferences |
| PUT | /api/push/live-preferences | Toggle "all live" vs "followed only" notifications |
Notification Preferences
| Preference | Values | Description |
| stream (per-follow) | enabled/disabled | Get notified when a followed streamer goes live |
| stream_live_all | enabled/disabled | Get notified when ANY streamer goes live |
Internal Events API (server-to-server)
HoboStreamer emits events to hobo.tools via internal API. Protected by X-Internal-Key header.
| Method | Path | Description |
| POST | /internal/events/stream-live | Notify: stream went live { streamer, stream } |
| POST | /internal/push-single | Send push to one user { userId, title, body, url } |
| POST | /internal/push-bulk | Send push to multiple users { userIds[], title, body, url } |
Fallback Webhook (HoboStreamer direct)
If hobo.tools is unreachable, HoboStreamer falls back to its built-in Discord webhook sender. Configure the webhook URL in HoboStreamer site settings (discord_webhook_url).
8. VODS, CLIPS & PASTES
| Method | Path | Description |
| GET | /api/vods | List VODs (query: username, page, limit) |
| GET | /api/vods/:id | VOD details + playback URL |
| GET | /api/clips | List clips |
| POST | /api/clips | Create clip { streamId, startTime, duration } |
| GET | /api/clips/:id | Clip details |
| GET | /api/pastes | List public pastes |
| POST | /api/pastes | Create paste { title, content, syntax, visibility, expiry } |
| GET | /api/pastes/:slug | Get paste by slug |
9. MISC API
| Method | Path | Description |
| GET | /api/health | Server health: status, uptime, active connections |
| GET | /api/updates | Recent changelog (git commits) |
| GET | /api/emotes | All emotes (global + per-channel) |
| POST | /api/emotes/upload | Upload custom emote (multipart) |
| GET | /api/meta/link-preview?url= | Link preview / OEmbed data |
Rate Limits
| Scope | Window | Limit |
| GET / HEAD requests | 1 minute | 900 |
| POST / PUT / DELETE | 1 minute | 180 |
| Auth (login/register) | 15 minutes | 20 |
| File uploads | 15 minutes | 40 |
| Chat messages | per message | 1000ms default; slowmode configurable |
| Control commands | per button | cooldown_ms set per button |
| key_down / key_up events | per event | 100ms minimum |
HoboStreamer is open source. Contribute at github.com/HoboStreamer/HoboStreamer.com. Self-host it, fork it, vibe code on top of it.
Comments