HoboStreamer
Live streaming for
Free & open source — community-first, no investors, and built to become a sustainable not-for-profit project over time. Full API docs for vibe coding chat bots, robot streams, and more.
Live Now
0No one is streaming right now
Be the first to go live!
The Hobo Network
One account, 14 services — streaming, games, maps, food, image tools, audio tools, text generators, logo makers, PDF & document tools, network diagnostics, developer tools, video downloads, and more.
Recently Online
No recent streams
Recent Changes
Built from a shed, burnout,
and pure stubbornness.
No investors. No polished startup mythology. Just one builder trying to make the internet feel human again.
Yes, it literally started in a shed.
Not a studio. Not a founder retreat. A shed with Wi‑Fi, a webcam, and enough stubbornness to stay live even when the setup looked ridiculous. People roasted it. The stream stayed on anyway. That ended up being the whole blueprint: imperfect gear, real people, zero waiting for permission.
Tried the normal route. Hated it.
There was a detour through Verizon and then Amazon corporate as a data analyst. Safe on paper. Spiritually awful. The bills got paid, but every spare hour still went back into streaming, building, and trying to imagine a better version of the internet.
So the burnout turned into songs.
A truly normal amount of career angst ended up on YouTube as a pile of weird, honest songs about corporate life, depression, and trying not to become a spreadsheet with a pulse.
So I quit and built the thing I wanted to exist.
HoboStreamer grew out of that decision. Then came hobo.tools. Then hobo.quest. The point of all of it is simple: useful, creative, community-first internet software that does not feel disposable, extractive, or shaped by investor pressure.
If you’re here early, you’re helping prove the internet still has room for something honest.
I’m not trying to build the next ad machine. I’m trying to build something useful, weird, and genuinely good for people.
Redirecting to channel...
Videos
Clips
Channel
username
Videos
No videos yet
Clips
No clips yet
Video
Clips from this stream
Clip Title
Comments
Streamer Dashboard
Broadcast
Create and manage streams, configure your broadcast setup, and go live from the dedicated Broadcast page.
Go to Broadcast PageConnection
Interactive Controls
Per-Stream Controls (Live)
Control Settings
Hardware Bridge Scripts
Download Python scripts to connect hardware (robots, GPIO, servos) to your control profiles. Each script is pre-configured with your stream key and the buttons from the selected profile.
Camera Controls (ONVIF PTZ)
Add ONVIF-compatible cameras (Hikvision, Axis, Dahua, etc.) for pan/tilt/zoom viewer control.
Donation Goals
My Videos
Videos are public by default. You can set individual videos to private.
My Emotes
Upload custom emotes for your channel. Supported: PNG, GIF (animated), WebP, AVIF. Max 256 KB each.
My Clips
Clips you've taken from other streams. New clips are unlisted until the streamer makes them public.
Clips of My Stream
Clips viewers have taken from your streams. You can publish or delete them.
Hobo Bucks
Hobo Nickels
Viewers earn Hobo Nickels by watching your stream and chatting. They can spend them on rewards you create below.
Coin Rewards
Create rewards your viewers can redeem with Hobo Nickels. Think TTS, sound effects, chat highlights, streamer challenges, etc.
Redemption Queue
Viewers who redeemed rewards. Fulfill or reject them.
Admin Panel
Go Live
Set up your stream in 3 easy steps — pick a method, configure your devices, and hit Go Live.
Active Streams
Create New Stream
Use your saved defaults from Broadcast Settings
Stream straight from this browser, or send video from OBS using WebRTC (WHIP).
Pick whether to stream your webcam/phone camera, or share your screen/window/tab.
Your browser needs camera & microphone access to list available devices and go live.
Click the button above — your browser will ask for permission. This is required to stream.
Your stream will appear at hobostreamer.com/you — this link stays the same every time you go live.
Which streaming method should I use?
- Easiest option — works right in your browser
- No software to install
- Use your webcam, phone camera, or screen share
- Ultra-low latency (under 1 second)
- Great for casual streaming, IRL, and quick screen shares
Choose this if you just want to go live fast without installing anything.
- Best video quality — uses OBS, Streamlabs, or a mobile app
- Full control over scenes, overlays, transitions
- Supports OBS Studio, Streamlabs, IRL Pro (Android), and more
- Just paste the Server URL and Stream Key into your app
- Best for gaming, desktop content, and professional-looking streams
Choose this if you use OBS or a mobile streaming app like IRL Pro.
- Lightweight — just a single FFmpeg command
- Perfect for Raspberry Pi, security cameras, embedded devices
- Works on headless servers (no GUI needed)
- Lower quality than RTMP (mpeg1 codec), but very low CPU usage
- Great for 24/7 unattended streams and IoT projects
Choose this if you're streaming from a Pi, Linux server, or anything command-line.
Need help? Common questions
Click "Allow Camera & Mic" and accept the browser permission popup. If you accidentally denied it, click the lock/camera icon in your browser's address bar to re-enable permissions, then refresh the page.
Yes! Use WebRTC → Browser → Camera/Mic to stream directly from your phone's browser. Or install IRL Pro (Android) and use the RTMP method — there's a setup guide on the RTMP instructions page.
Select RTMP as your method and click Create Stream. You'll get a Server URL and Stream Key — paste those into OBS under Settings → Stream → Custom. Then click "Start Streaming" in OBS. Alternatively, select WebRTC → OBS (WHIP) if you have OBS 30+ for ultra-low latency.
WHIP (WebRTC HTTP Ingest Protocol) is a new standard that lets OBS send video via WebRTC instead of RTMP. It gives you sub-second latency. Requires OBS Studio 30.0 or newer.
Yes! Choose Screen Share, then check the "Include Camera (PiP overlay)" checkbox. Your webcam will appear as a small picture-in-picture over your screen share.
Your stream is always at hobostreamer.com/your-username. This link never changes — share it once, and it works every time you go live.
Yes! After you go live, scroll down to the Restreams section. You can add YouTube, Twitch, Kick, or any custom RTMP server as a restream destination.
Try lowering the resolution to 480p or 360p, or reduce the max bitrate. If on mobile data, use 1000–1500 kbps. Make sure you have a stable internet connection — WiFi is better than cellular for streaming.
Your Streams
Loading...
Past Streams
Browse and watch your recorded VODs. Click a VOD to view it with chat replay.
Loading VODs...
Broadcast Settings
Configure your default broadcast preferences. These settings will auto-populate when you create a new stream.
Default Streaming Method
Default Media Devices
Audio Settings
Video Quality Defaults
Text-to-Speech Defaults
VOD Defaults
Breaking News in Chat
Restream Destinations
Configure where your stream is relayed. Destinations with Auto-Start will begin restreaming automatically when you go live.
Restream to YouTube, Twitch, Kick, or any custom RTMP server. RTMP uses codec copy (zero CPU); JSMPEG and WebRTC are re-encoded to H.264/AAC.
RobotStreamer
Restream your broadcast to RobotStreamer via WebRTC. Requires browser-based streaming.
Configure your token and robot, then validate the connection.
Account Info
Theme
Choose a built-in theme or create your own.
Custom Theme Editor
Tweak individual colors. Changes preview live.
Share Your Theme
Submit your current colors to the community Theme Directory.
Stream Key
Default Broadcast Settings
Default Audio
VOD & Clip Defaults
Set the default visibility for new VODs and clips on your channel.
Channel Weather
Show local weather on your channel page. Your zip/postal code is never shared with viewers.
Loading streams...
Global Chat
Live messages from all streams and the lobby
Theme Directory
Browse and download themes created by the community.
Updates
Recent changes and patch notes from the GitHub repository.
Pastes & Images
Share code, notes, prompts, and images with the community.
HoboStreamer API Docs
Built for vibe coders. Copy everything below, paste into your LLM, and build whatever you want.
HOBOSTREAMER PLATFORM — COMPLETE DEVELOPER REFERENCE
HoboStreamer is a self-hosted, open-source live streaming platform. Stack: Node.js + Express + SQLite (better-sqlite3) + Mediasoup WebRTC SFU. Frontend: vanilla JS SPA. All API endpoints return JSON. WebSocket endpoints on same host/port.
Base URL: https://hobostreamer.com (replace with your instance). All authenticated endpoints require: Authorization: Bearer <JWT> header, or ?token=JWT query param on WebSocket URLs.
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. |
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": 500, // Global rate limit per user (100-30000)
"video_click_enabled": true // Enable video click input
}
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.
7. 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
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