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

0

No one is streaming right now

Be the first to go live!

Go live however you want

Start instantly in your browser, or plug in OBS and ffmpeg for a full scene-based workflow. Then restream to Twitch, YouTube, Kick, and any RTMP server — all at once, all from one dashboard.

Claim your channel One sign-in through Hobo Network
Start broadcasting Browser, OBS, or ffmpeg
Restream everywhere Twitch, YouTube, Kick & more
Multi-platform restreaming Broadcast to Twitch, YouTube, Kick, RobotStreamer, and any custom RTMP server simultaneously
Live chat & emotes Real-time chat with custom emotes, viewer reactions, and chat commands
Clips & VODs Clip moments on the fly and auto-record VODs for every broadcast
Built-in game Viewers play HoboQuest together live on-stream with leaderboards and canvas
Global chat A sitewide chatroom that connects everyone — talk across streams or just hang out
Voice chat Drop into voice channels with other viewers and streamers — no extra app needed
Pastes & file sharing Share code snippets, text, and files with syntax highlighting — like Pastebin, built in
100% open source Every line of code is public on GitHub — no black boxes, no hidden trackers
Stream analytics Track viewers, watch time, peak concurrents, and chat activity across every broadcast
Chat moderation Message deletion, IP approval mode, relay user moderation, ban tools, and full audit logging
200+ free tools Image editing, audio conversion, PDF documents, video downloads, network diagnostics, developer tools, text generators, logo makers, and more at hobo.tools
Explore Hobo Tools

Recently Online

No recent streams

Recent Changes

View All
Origin story

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.

01
The Shed Era

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.

02
The Corporate Detour

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.

The Music Arc

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.

@FCMenus on YouTube The soundtrack to the corporate-burnout era.
03
The Build

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.

The not-for-profit plan

The goal is not maximum revenue; it’s long-term sustainability. Right now, servers, storage, bandwidth, and development all cost money. So the plan is to keep the network transparent, fund operations responsibly, and formalize it as a real not-for-profit/public-benefit project once it can stand on its own.

Phase 1 — Make it sustainable Use optional support, memberships, cosmetics, or tasteful monetization to cover hosting, bandwidth, storage, and development.
Phase 2 — Protect the mission No investors, no surprise corporate pivot, and no turning the community into a product. Any money goes to operations first.
Phase 3 — Formalize it Once the network is stable enough, file the not-for-profit paperwork and build this into the real community project it was always meant to be.

Redirecting to channel...

Videos

Clips

?

Channel

username

0 followers irl

Channel is offline

Check back later!

GLOBAL CHAT

Videos

No videos yet

Clips

No clips yet

0:00 / 0:00

Chat Replay

Chat messages will appear here as the video plays

Video

Clips from this stream

Comments

0:00 / 0:00

Chat Replay

Chat messages will appear here as the clip plays

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 Page

Connection

Interactive Controls


Per-Stream Controls (Live)
Control Settings
Minimum time between commands per user
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

0 ($0.00)

Hobo Nickels

0 Free loyalty currency

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

1
Stream Info Give your stream a title so viewers know what you're doing
This appears on your channel page and in the stream directory
Viewers will see an age verification gate before watching
2
Streaming Method How do you want to send your video to HoboStreamer?
WebRTC Browser or OBS (WHIP) Easiest — no software to install
RTMP OBS / Streamlabs / IRL Pro Best quality — use your streaming app
JSMPEG FFmpeg command line Lightweight — Raspberry Pi & headless
WebRTC lets you stream directly from this browser tab using your camera, microphone, or screen — no extra software needed. Just click Create Stream and you're live.

Stream straight from this browser, or send video from OBS using WebRTC (WHIP).

Browser No extra software
OBS (WHIP) Requires OBS 30+

Pick whether to stream your webcam/phone camera, or share your screen/window/tab.

Camera / Mic Webcam, phone cam, etc.
Screen Share Desktop, app, or browser tab
3
Configure Set up your camera, mic, or screen share

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?
WebRTC (Browser)
  • 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.

RTMP
  • 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.

JSMPEG
  • 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
My camera/mic isn't showing up

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.

Can I stream from my phone?

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.

How do I use OBS?

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.

What's WHIP?

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.

Can I share my screen and my face at the same time?

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.

Where is my stream link?

Your stream is always at hobostreamer.com/your-username. This link never changes — share it once, and it works every time you go live.

Can I stream to multiple platforms at once?

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.

My stream quality is bad / buffering

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

WebRTC Browser or OBS (WHIP)
RTMP OBS / IRL Pro
JSMPEG FFmpeg / Pi

Default Media Devices

Audio Settings

100%

Video Quality Defaults

Lower for slow connections, higher for better quality

Text-to-Speech Defaults

VOD Defaults

VODs can be toggled after streaming

Breaking News in Chat

Headlines from configured sources are injected into your chat at most once every 3 minutes. You can turn this off at any time.

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.

Enable RS Restream
Mirror RS Chat
RS Token
Robot ID / URL
Detected Robots

Configure your token and robot, then validate the connection.

?
User

Account Info

Username
Cannot be changed
Display Name
Email
Bio
Profile Color

Password & Security

Your account is managed by the Hobo Network.

Manage Account on hobo.tools

Theme

Choose a built-in theme or create your own.

Loading themes...

Custom Theme Editor

Tweak individual colors. Changes preview live.

Share Your Theme

Submit your current colors to the community Theme Directory.

Theme Name
Description

Stream Key

Key
Keep secret! Anyone with this key can stream on your channel.

Default Broadcast Settings

Default Resolution
Default FPS
Codec Preference
Max Bitrate (kbps)
Min Bitrate (kbps)
On Disconnect

Default Audio

Auto Gain
Echo Cancellation
Noise Suppression

VOD & Clip Defaults

Set the default visibility for new VODs and clips on your channel.

VOD Visibility
New recordings will default to this visibility
Clip Visibility
Clips from your streams will default to this visibility
Record VODs
Enabled by default. Turn this off to stop creating new VOD files for your channel.

Channel Weather

Show local weather on your channel page. Your zip/postal code is never shared with viewers.

Zip / Postal Code
Used to look up weather — never shown to viewers. Hidden to protect your privacy while streaming.
Show Location
Off by default — enable to display your area name on the weather widget
Detail Level
Controls how much weather info viewers see on your channel

Loading streams...

Voice Channels

Global Chat

Live messages from all streams and the lobby

Loading themes...

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

MethodPathAuthBody / Notes
POST/api/auth/registerNo{ username, email, password }
POST/api/auth/loginNo{ username, password } → { token, user }
GET/api/auth/meYesReturns current user profile
PUT/api/auth/profileYesUpdate 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

MethodPathAuthDescription
GET/api/streamsNoList all live streams
POST/api/streamsYesGo live. Body: { title, description, category, protocol, tags }
GET/api/streams/:idNoStream details (endpoint, controls, cameras)
PUT/api/streams/:idYesUpdate title/description/category/tags/nsfw
DELETE/api/streams/:idYesEnd stream
GET/api/streams/:id/endpointYesGet WHIP URL, RTMP key, ffmpeg commands
POST/api/streams/:id/heartbeatYesKeep stream alive — call every 30s
GET/api/streams/channel/:usernameNoFull channel page data (streams, VODs, clips, panels)
GET/api/streams/channel/:username/liveNoLightweight live status for fast player init
PUT/api/streams/channelYesUpdate own channel settings
POST/api/streams/channel/:username/followYesFollow / unfollow
GET/api/streams/mineYesYour 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.

MethodPathNotes
POST/whip/:streamIdBody: raw SDP (Content-Type: application/sdp). Auth: Bearer JWT. Returns SDP answer + Location header.
PATCH/whip/:streamId/:resourceIdTrickle ICE (Content-Type: application/trickle-ice-sdpfrag)
DELETE/whip/:streamId/:resourceIdTerminate 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' }))

TypeDirKey FieldsDescription
welcomeS→CpeerId, role, streamId, viewerCount, iceServersSent immediately on connect
watchC→SViewer requests to start watching (triggers SFU consumer)
broadcaster-readyS→ViewerBroadcaster is live, safe to send watch
sfu-get-capabilitiesC→SGet Mediasoup RTP capabilities
sfu-create-transportC→Sdirection: send|recvCreate WebRTC transport
sfu-connect-transportC→StransportId, dtlsParametersConnect DTLS
sfu-produceC→StransportId, kind, rtpParametersStart producing audio/video
sfu-stop-produceC→SproducerIdStop a producer
offer / answerRelaysdp, targetPeerIdP2P SDP signaling fallback
ice-candidateRelaycandidate, targetPeerIdP2P ICE (SFU handles ICE internally)
viewer-countS→AllcountUpdated viewer count broadcast
stream-endedS→ViewersStream 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

TypePayloadDescription
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

TypeKey FieldsDescription
authauthenticated, username, role, user_id, slowmode_seconds, slur_filter_enabledIdentity confirmation. First message after connect.
chatusername, core_username, user_id, anon_id, role, message, stream_id, is_global, timestamp, id, reply_to?, avatar_url, profile_color, filteredA chat message. is_global=true means not tied to a stream.
systemmessage, timestampSystem notice (joins, events)
user-countcountUpdated viewer/chatter count
users-listusers: { logged: [{username, role, avatar_url}], anonCount }Full user list (after get-users)
errormessageError (banned, rate limited, etc.)
slur-blockedmessageYour message was blocked by the anti-slur filter
coin_earnedcoins, total, reasonHobo 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)

CommandDescription
/tts <text>Text-to-speech via TTS system
/color #hexSet your chat name color
!sr / !yt / !request <url>Media / song request
!queueShow media queue
!np / !nowplayingNow playing

User Roles

RoleDescription
adminPlatform admin
modPlatform moderator
streamerRegistered streamer
userRegistered viewer
anonAnonymous visitor (anonXXXXX identity)

Chat REST API

MethodPathAuthDescription
POST/api/chat/sendYesSend message via REST (fallback)
GET/api/chat/:streamId/historyNoChat history for a stream
GET/api/chat/:streamId/usersNoUsers in a stream's chat
GET/api/chat/user/:username/profileNoUser 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.

MethodPathAuthDescription
GET/api/controls/configsYesList your configs (with button count)
POST/api/controls/configsYesCreate config { name, description }
GET/api/controls/configs/:idYesConfig with full buttons array
PUT/api/controls/configs/:idYesUpdate name / description
DELETE/api/controls/configs/:idYesDelete config
POST/api/controls/configs/:id/buttonsYesAdd button to config
PUT/api/controls/configs/:id/buttons/:btnIdYesUpdate button
DELETE/api/controls/configs/:id/buttons/:btnIdYesDelete button
POST/api/controls/configs/:id/activateYesSet as channel default config
POST/api/controls/configs/deactivateYesClear channel default
POST/api/controls/configs/:id/apply/:streamIdYesCopy config buttons onto a live stream
GET/api/controls/configs/:id/bridge-script?type=generic|cozmoYesDownload 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_typeViewer InteractionMessages Sent
buttonSingle click or key tapcommand
keyboardHold mouse/touch/key → continuous. Release → stopkey_down on press, key_up on release
toggleClick to toggle on/offcommand (on) / command (off)
dpadD-pad style directionalcommand

Channel Control Settings

MethodPathDescription
GET/api/controls/settings/channelGet settings
PUT/api/controls/settings/channelUpdate 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)

MethodPathDescription
GET/api/controls/:streamIdGet all controls + settings for a stream
POST/api/controls/:streamIdAdd a button directly to active stream
PUT/api/controls/:streamId/:idUpdate control
DELETE/api/controls/:streamId/:idDelete control
POST/api/controls/:streamId/presets/cozmoApply 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

TypePayload FieldsWhen
connectedmessageImmediately on connect — confirms auth
commandcommand, control_id, from_user, timestampViewer clicked a button (button/toggle/dpad type)
key_downcommand, control_id, from_user, timestampViewer started holding a keyboard-type button
key_upcommand, control_id, from_user, timestampViewer released the button
video_clickx (0-1), y (0-1), from_user, timestampViewer clicked on the video feed. 0,0 = top-left, 1,1 = bottom-right

Hardware Can Send

TypePayloadEffect
status{ any fields }Relayed to all viewers as hardware_status message

Viewer Sends These Messages

TypePayloadDescription
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

TypeKey FieldsDescription
controlscontrols[], settings{}Initial buttons + channel settings on connect
command_executedcommand, bySomeone pressed a button (activity feed)
key_heldcommand, bySomeone started holding a key
key_releasedcommand, bySomeone released a key
video_click_activityx, y, bySomeone clicked the video
hardware_statusanyStatus update from hardware bridge
errormessageDenied / not live / banned
cooldownmessageRate limited

Permission Model

control_modeWho can send commands
openAny viewer (anon allowed if anon_controls_enabled=true)
whitelistStream owner + explicitly whitelisted users only
disabledNobody — 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)

MethodPathDescription
POST/api/controls/api-keyGenerate API key (shown once — save it)
GET/api/controls/api-keysList keys (masked)
GET/api/controls/cozmo-scriptDownload 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).

TypeDescription
genericPrints 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.
cozmoPre-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:

IndicatorMeaning
⚫ (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.

MethodPathDescription
POST/api/onvif/discoverAuto-discover ONVIF cameras on local network
POST/api/onvif/camerasAdd camera { host, port, username, password, name }
GET/api/onvif/camerasList cameras
PUT/api/onvif/cameras/:idUpdate camera
DELETE/api/onvif/cameras/:idDelete camera
GET/api/onvif/cameras/:id/presetsList presets
POST/api/onvif/cameras/:id/presetsCreate 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

MethodPathDescription
GET/api/vodsList VODs (query: username, page, limit)
GET/api/vods/:idVOD details + playback URL
GET/api/clipsList clips
POST/api/clipsCreate clip { streamId, startTime, duration }
GET/api/clips/:idClip details
GET/api/pastesList public pastes
POST/api/pastesCreate paste { title, content, syntax, visibility, expiry }
GET/api/pastes/:slugGet paste by slug

9. MISC API

MethodPathDescription
GET/api/healthServer health: status, uptime, active connections
GET/api/updatesRecent changelog (git commits)
GET/api/emotesAll emotes (global + per-channel)
POST/api/emotes/uploadUpload custom emote (multipart)
GET/api/meta/link-preview?url=Link preview / OEmbed data

Rate Limits

ScopeWindowLimit
GET / HEAD requests1 minute900
POST / PUT / DELETE1 minute180
Auth (login/register)15 minutes20
File uploads15 minutes40
Chat messagesper message1000ms default; slowmode configurable
Control commandsper buttoncooldown_ms set per button
key_down / key_up eventsper event100ms minimum

HoboStreamer is open source. Contribute at github.com/HoboStreamer/HoboStreamer.com. Self-host it, fork it, vibe code on top of it.

Inventory