openvibers/openvibe.live Commits

Kiosk companion and hardware scripts moved to OpenVibe.Extensions (ADR-023, W18): browser-extension/ and hardware/ keep only redirect READMEs pointing at OpenVibe.Extensions kiosk-companion/ and hardware/ (moved unchanged at 9c5aa64)

OP OpenVibers committed 24 Sept 2026 · ce6a7332cf · 11 files, +12 −925


Changes

ddiff --git a/AGENTS.md b/AGENTS.md
iindex d027a148..fac0f963 100644
---- a/AGENTS.md
++++ b/AGENTS.md
@@@ -87,4 +87,4 @@ Every file below is also served on the site at `/docs/<name>` (rendered by `serv
- [docs/performance-audit.md](docs/performance-audit.md) — Measurements and tools
- [SETUP.md](SETUP.md) — Full deployment guide
- [SECURITY_AUDIT.md](SECURITY_AUDIT.md) — Security audit findings
--- [hardware/README.md](hardware/README.md) — Raspberry Pi integration
++- [hardware/README.md](hardware/README.md) — Raspberry Pi integration (moved to OpenVibe.Extensions `hardware/`; this is a redirect)
ddiff --git a/browser-extension/README.md b/browser-extension/README.md
nnew file mode 100644
iindex 00000000..b3b0ecc8
---- /dev/null
++++ b/browser-extension/README.md
@@@ -0,0 +1,6 @@
++# Moved: the kiosk companion extension
++
++The OpenVibe Kiosk companion (Firefox new-tab page and history bridge for `/kiosk`) now lives in
++[OpenVibe.Extensions `kiosk-companion/`](https://github.com/OpenVibers/OpenVibe.Extensions/tree/main/kiosk-companion),
++the home of OpenVibe's first-party client surfaces (ADR-023). It moved unchanged; its history before
++the move is in this repository's git log under `browser-extension/openvibe-kiosk-companion/`.
ddiff --git a/browser-extension/openvibe-kiosk-companion/README.md b/browser-extension/openvibe-kiosk-companion/README.md
ddeleted file mode 100644
iindex e22e9dca..00000000
---- a/browser-extension/openvibe-kiosk-companion/README.md
++++ /dev/null
@@@ -1,56 +0,0 @@
--# OpenVibe Kiosk Companion (Firefox)
--
--A tiny Firefox extension that gives the OpenVibe.Live **`/kiosk#input`** address bar
--access to your Firefox **history** so it can autocomplete like a real browser bar.
--It can also set the kiosk as your **new-tab page**.
--
--## What it does
--
--- Injects a content script only on `openvibe.live/kiosk` that bridges the page to a
-- background script which queries `browser.history` and returns your top matches
-- (ranked by visit count + recency). History never leaves your browser.
--- Overrides the new-tab page to open `https://openvibe.live/kiosk#input` **with the
-- kiosk's omnibar focused instead of the browser address bar**. It does this by opening
-- the kiosk in a fresh tab and closing the temporary new-tab page (Firefox focuses the web
-- page on a real tab navigation, but keeps focus on the URL bar if you merely update the
-- new-tab tab). (Don't want the new-tab override? Delete the `chrome_url_overrides` block
-- from `manifest.json`.)
--
--The kiosk page detects the companion automatically — no configuration. Without the
--extension the omnibar still works, it just falls back to its own on-page history.
--
--## Permissions
--
--- `history` — to read your browsing history for autocomplete.
--- Runs its content script only on `*://openvibe.live/kiosk*`.
--
--## Install — quick test (temporary, gone on restart)
--
--1. Open `about:debugging#/runtime/this-firefox`.
--2. **Load Temporary Add-on…** → pick this folder's `manifest.json`.
--3. Open a new tab (or visit `openvibe.live/kiosk#input`) and start typing —
-- history suggestions appear.
--
--## Install — permanent
--
--Firefox only runs signed extensions permanently. Sign it (free) with Mozilla:
--
--```bash
--npm install --global web-ext
--cd browser-extension/openvibe-kiosk-companion
--# Get API credentials at https://addons.mozilla.org/developers/addon/api/key/
--web-ext sign --api-key=YOUR_JWT_ISSUER --api-secret=YOUR_JWT_SECRET --channel=unlisted
--```
--
--This produces a signed `.xpi` in `web-ext-artifacts/`. Install it via
--`about:addons` → gear icon → **Install Add-on From File…**.
--
--(Alternatively, Firefox Developer Edition / ESR / Nightly can run unsigned
--extensions by setting `xpinstall.signatures.required = false` in `about:config`.)
--
--## Files
--
--- `manifest.json` — MV3 manifest (Firefox).
--- `background.js` — queries `browser.history`, ranks results.
--- `history-bridge.js` — content script that relays page ⇄ background.
--- `newtab.html` / `newtab.js` — new-tab override → kiosk.
ddiff --git a/browser-extension/openvibe-kiosk-companion/background.js b/browser-extension/openvibe-kiosk-companion/background.js
ddeleted file mode 100644
iindex c2bbc2f3..00000000
---- a/browser-extension/openvibe-kiosk-companion/background.js
++++ /dev/null
@@@ -1,33 +0,0 @@
--/**
-- * Background script — the only context allowed to touch browser.history.
-- * The content script (history-bridge.js) relays the kiosk page's queries here,
-- * we search history, and return ranked matches. Nothing leaves the browser.
-- */
--const api = (typeof browser !== "undefined") ? browser : chrome;
--
--api.runtime.onMessage.addListener((msg) => {
-- if (!msg || msg.type !== "openvibe-history-query") return;
-- const q = String(msg.q || "").trim();
-- if (!q) return Promise.resolve({ items: [] });
--
-- return api.history.search({
-- text: q,
-- maxResults: 30,
-- startTime: 0 // search all of history
-- }).then((results) => {
-- const seen = new Set();
-- const items = (results || [])
-- .filter((r) => r.url && /^https?:/i.test(r.url))
-- .map((r) => ({
-- url: r.url,
-- title: r.title || "",
-- visitCount: r.visitCount || 0,
-- lastVisit: r.lastVisitTime || 0
-- }))
-- // Rank by how often + how recently you've visited.
-- .sort((a, b) => (b.visitCount - a.visitCount) || (b.lastVisit - a.lastVisit))
-- .filter((r) => (seen.has(r.url) ? false : (seen.add(r.url), true)))
-- .slice(0, 8);
-- return { items };
-- }).catch(() => ({ items: [] }));
--});
ddiff --git a/browser-extension/openvibe-kiosk-companion/history-bridge.js b/browser-extension/openvibe-kiosk-companion/history-bridge.js
ddeleted file mode 100644
iindex 6250aa07..00000000
---- a/browser-extension/openvibe-kiosk-companion/history-bridge.js
++++ /dev/null
@@@ -1,38 +0,0 @@
--/**
-- * Content script injected into openvibe.live/kiosk. It is the bridge between
-- * the page (which can't read history) and the background script (which can):
-- *
-- * page --postMessage(history-query)--> this --runtime.sendMessage--> background
-- * page <--postMessage(history-result)-- this <--response-------------- background
-- *
-- * Only same-origin page messages are honoured, and replies are posted back with
-- * the page's own origin as targetOrigin, so history never leaks to other sites.
-- */
--const api = (typeof browser !== "undefined") ? browser : chrome;
--const ORIGIN = window.location.origin;
--
--function announce() {
-- window.postMessage({ source: "openvibe-kiosk-ext", type: "ready" }, ORIGIN);
--}
--// Announce both immediately and once the page's listener is surely attached.
--announce();
--window.addEventListener("DOMContentLoaded", announce);
--
--window.addEventListener("message", (event) => {
-- if (event.source !== window) return;
-- const d = event.data;
-- if (!d || d.source !== "openvibe-kiosk") return;
--
-- if (d.type === "ping") { announce(); return; }
--
-- if (d.type === "history-query") {
-- const { q, id } = d;
-- api.runtime.sendMessage({ type: "openvibe-history-query", q })
-- .then((resp) => {
-- window.postMessage({ source: "openvibe-kiosk-ext", type: "history-result", id, q, items: (resp && resp.items) || [] }, ORIGIN);
-- })
-- .catch(() => {
-- window.postMessage({ source: "openvibe-kiosk-ext", type: "history-result", id, q, items: [] }, ORIGIN);
-- });
-- }
--});
ddiff --git a/browser-extension/openvibe-kiosk-companion/manifest.json b/browser-extension/openvibe-kiosk-companion/manifest.json
ddeleted file mode 100644
iindex 8c471c7a..00000000
---- a/browser-extension/openvibe-kiosk-companion/manifest.json
++++ /dev/null
@@@ -1,26 +0,0 @@
--{
-- "manifest_version": 3,
-- "name": "OpenVibe Kiosk Companion",
-- "version": "1.1.0",
-- "description": "Lets the OpenVibe.Live /kiosk address bar autocomplete from your Firefox history, and (optionally) sets the kiosk as your new-tab page.",
-- "browser_specific_settings": {
-- "gecko": {
-- "id": "[email protected]",
-- "strict_min_version": "115.0"
-- }
-- },
-- "permissions": ["history"],
-- "background": {
-- "scripts": ["background.js"]
-- },
-- "content_scripts": [
-- {
-- "matches": ["*://openvibe.live/kiosk*", "*://www.openvibe.live/kiosk*"],
-- "js": ["history-bridge.js"],
-- "run_at": "document_start"
-- }
-- ],
-- "chrome_url_overrides": {
-- "newtab": "newtab.html"
-- }
--}
ddiff --git a/browser-extension/openvibe-kiosk-companion/newtab.html b/browser-extension/openvibe-kiosk-companion/newtab.html
ddeleted file mode 100644
iindex 8d992164..00000000
---- a/browser-extension/openvibe-kiosk-companion/newtab.html
++++ /dev/null
@@@ -1,11 +0,0 @@
--<!doctype html>
--<html>
--<head>
--<meta charset="utf-8">
--<title>New Tab</title>
--<style>html,body{margin:0;height:100%;background:#08090c}</style>
--</head>
--<body>
--<script src="newtab.js"></script>
--</body>
--</html>
ddiff --git a/browser-extension/openvibe-kiosk-companion/newtab.js b/browser-extension/openvibe-kiosk-companion/newtab.js
ddeleted file mode 100644
iindex bcaac25a..00000000
---- a/browser-extension/openvibe-kiosk-companion/newtab.js
++++ /dev/null
@@@ -1,33 +0,0 @@
--// Open the kiosk with the PAGE focused instead of the address bar.
--//
--// The trick (same one the "Custom New Tab" extensions use): simply navigating this
--// temporary new-tab tab to a URL leaves focus on the address bar. But opening the URL
--// in a FRESH tab and then closing this temporary tab makes Firefox focus the web page —
--// so the kiosk's omnibar input gets the caret. Falls back to a plain redirect if the
--// tabs API isn't available.
--const api = (typeof browser !== "undefined") ? browser : chrome;
--const TARGET = "https://openvibe.live/kiosk#input";
--
--(async () => {
-- try {
-- const tab = await api.tabs.getCurrent();
-- if (!tab || tab.id == null) { location.replace(TARGET); return; }
--
-- const created = await api.tabs.create({
-- url: TARGET,
-- index: tab.index,
-- windowId: tab.windowId,
-- active: true
-- });
--
-- // Keep the replacement in the same tab group, if any (best-effort).
-- if (api.tabs.group && tab.groupId != null && tab.groupId > -1) {
-- try { await api.tabs.group({ groupId: tab.groupId, tabIds: created.id }); } catch (e) { /* ignore */ }
-- }
--
-- await api.tabs.remove(tab.id);
-- try { api.history.deleteUrl({ url: location.href }); } catch (e) { /* ignore */ }
-- } catch (e) {
-- location.replace(TARGET); // fallback: at least land on the kiosk
-- }
--})();
ddiff --git a/hardware/README.md b/hardware/README.md
iindex d1e81ad1..f73f8161 100644
---- a/hardware/README.md
++++ b/hardware/README.md
@@@ -1,273 +1,6 @@
--# OpenVibe.Live — Hardware Integration
++# Moved: hardware integration
--Scripts for streaming from and controlling hardware devices (Raspberry Pi, single-board computers, etc.) with OpenVibe.Live.
--
--## Requirements
--
--- **Python 3.7+**
--- **FFmpeg** — `sudo apt install ffmpeg`
--- **websocket-client** — `pip3 install websocket-client`
--- For TTS: **espeak** — `sudo apt install espeak`
--- For GPIO: **gpiozero** — `pip3 install gpiozero` (Raspberry Pi only)
--- For PulseAudio/PipeWire audio: **pulseaudio-utils** — `sudo apt install pulseaudio-utils`
--
--## Quick Start (Bash Script)
--
--The easiest way to stream. Supports camera, screen capture, OBS virtual camera, and audio-only modes.
--
--```bash
--# Basic camera stream
--./scripts/start-stream.sh YOUR_STREAM_KEY localhost 9710
--
--# Screen capture
--MODE=screen ./scripts/start-stream.sh YOUR_KEY localhost 9710
--
--# OBS Virtual Camera → JSMPEG
--MODE=obs ./scripts/start-stream.sh YOUR_KEY localhost 9710
--
--# Audio only (no video)
--MODE=audioonly ./scripts/start-stream.sh YOUR_KEY localhost 9711
--
--# HD 720p with PulseAudio
--RESOLUTION=1280x720 BITRATE=1200k AUDIO=pulse FPS=30 ./scripts/start-stream.sh YOUR_KEY localhost 9710
--
--# Disable audio
--NOAUDIO=1 ./scripts/start-stream.sh YOUR_KEY localhost 9710
--
--# Custom camera device
--VIDEO_DEV=/dev/video2 ./scripts/start-stream.sh YOUR_KEY localhost 9710
--```
--
--### Environment Variables
--
--| Variable | Default | Description |
--|----------|---------|-------------|
--| `MODE` | camera | `camera`, `screen`, `obs`, or `audioonly` |
--| `RESOLUTION` | 640x480 | Video resolution |
--| `FPS` | 24 | Frames per second |
--| `BITRATE` | 800k | Video bitrate |
--| `AUDIO_BITRATE` | 128k | Audio bitrate |
--| `AUDIO` | alsa | `alsa` or `pulse` |
--| `NOAUDIO` | 0 | Set to `1` to disable audio |
--| `VIDEO_DEV` | auto | Camera device path (e.g. `/dev/video0`) |
--| `SCREEN_RES` | 1920x1080 | Screen capture resolution (screen mode only) |
--| `DISPLAY_ID` | :0.0 | X11 display (screen mode only) |
--
--## Finding Your Devices
--
--### Camera / Video
--
--```bash
--# List all video devices
--v4l2-ctl --list-devices
--
--# List supported formats and resolutions
--v4l2-ctl -d /dev/video0 --list-formats-ext
--
--# Test camera with a snapshot
--ffmpeg -f v4l2 -i /dev/video0 -frames 1 test.jpg
--
--# macOS: list devices
--ffmpeg -f avfoundation -list_devices true -i ""
--```
--
--### Audio / Microphone
--
--```bash
--# List ALSA audio devices
--arecord -l
--
--# List PulseAudio / PipeWire sources
--pactl list short sources
--
--# Test microphone (record 5 seconds, play back)
--arecord -d 5 -f cd test.wav && aplay test.wav
--
--# PipeWire users — check audio sources
--pw-cli list-objects | grep -A2 "node.name"
--```
--
--### OBS Virtual Camera
--
--```bash
--# 1. Start OBS Studio
--# 2. Go to Tools → Start Virtual Camera
--# 3. Check which device it created:
--v4l2-ctl --list-devices
--# Usually appears as /dev/video2 or /dev/video4
--
--# 4. Stream it (with PulseAudio for desktop audio):
--ffmpeg -f v4l2 -i /dev/video2 -f pulse -i default \
-- -f mpegts -codec:v mpeg1video -s 640x480 -b:v 500k -bf 0 \
-- -codec:a mp2 -b:a 128k -ar 44100 -ac 1 -muxdelay 0.001 \
-- http://SERVER:PORT/KEY/640/480/
--```
--
--### Raspberry Pi Camera (libcamera)
--
--```bash
--# Test if camera works
--libcamera-hello -t 5000
--
--# Stream via pipe to FFmpeg
--libcamera-vid -t 0 --inline --codec mjpeg --width 640 --height 480 -o - | \
-- ffmpeg -i pipe:0 -f mpegts -codec:v mpeg1video -s 640x480 -b:v 350k -bf 0 \
-- -muxdelay 0.001 http://SERVER:PORT/KEY/640/480/
--
--# With audio
--libcamera-vid -t 0 --inline --codec mjpeg --width 640 --height 480 -o - | \
-- ffmpeg -i pipe:0 -f alsa -i default \
-- -f mpegts -codec:v mpeg1video -s 640x480 -b:v 350k -bf 0 \
-- -codec:a mp2 -b:a 128k -ar 44100 -ac 1 -muxdelay 0.001 \
-- http://SERVER:PORT/KEY/640/480/
--```
--
--## FFmpeg Command Reference
--
--### JSMPEG Constraints
--
--JSMPEG uses canvas-based MPEG1 decoding. You **must** use:
--- Video codec: `mpeg1video` (not h264/h265)
--- Audio codec: `mp2` (not aac/opus)
--- Container: `mpegts`
--- No B-frames: `-bf 0`
--
--### Common FFmpeg Flags
--
--| Flag | Description | Example |
--|------|-------------|---------|
--| `-f v4l2` | Linux camera input | `-f v4l2 -i /dev/video0` |
--| `-f avfoundation` | macOS camera input | `-f avfoundation -i "0:0"` |
--| `-f x11grab` | Screen capture | `-f x11grab -s 1920x1080 -i :0.0` |
--| `-f alsa` | ALSA audio input | `-f alsa -i default` |
--| `-f pulse` | PulseAudio input | `-f pulse -i default` |
--| `-s WxH` | Output resolution | `-s 640x480` |
--| `-b:v` | Video bitrate | `-b:v 800k` |
--| `-b:a` | Audio bitrate | `-b:a 128k` |
--| `-r` | Frame rate | `-r 30` |
--| `-ar` | Audio sample rate | `-ar 44100` |
--| `-ac` | Audio channels | `-ac 1` (mono), `-ac 2` (stereo) |
--| `-bf 0` | Disable B-frames | Required for JSMPEG |
--| `-q:v` | Quality (2=best, 31=worst) | `-q:v 5` |
--| `-muxdelay` | Mux delay (lower = less latency) | `-muxdelay 0.001` |
--| `-framerate` | Input capture FPS | `-framerate 30` |
--| `-video_size` | Input capture resolution | `-video_size 1280x720` |
--
--### Recommended Presets
--
--| Device | Resolution | Video Bitrate | Audio | Notes |
--|--------|-----------|---------------|-------|-------|
--| Pi Zero | 320x240 | 200k | 64k mono | Use `-r 15` for stability |
--| Pi 3/4 | 640x480 | 350–500k | 128k mono | Good balance |
--| Desktop | 1280x720 | 800k–1.5M | 128k stereo | Smooth 30fps |
--| High Quality | 1920x1080 | 2–4M | 192k stereo | Requires good upload |
--
--## Streamer Script (Python)
--
--Captures video (USB camera / Pi Camera) and audio (ALSA mic), then pushes to OpenVibe.Live via JSMPEG or RTMP.
--
--```bash
--# JSMPEG streaming (recommended for low-latency)
--python3 streamer.py --key YOUR_STREAM_KEY --server openvibe.live
--
--# RTMP streaming (for OBS compatibility)
--python3 streamer.py --key YOUR_STREAM_KEY --server openvibe.live --protocol rtmp
--
--# Custom settings
--python3 streamer.py --key YOUR_KEY --server openvibe.live \
-- --resolution 1280x720 --fps 30 --bitrate 1500 \
-- --video-device /dev/video0 --audio-device default
--```
--
--### Options
--
--| Flag | Default | Description |
--|------|---------|-------------|
--| `--key` | (required) | Stream key from dashboard |
--| `--server` | localhost | OpenVibe.Live server |
--| `--protocol` | jsmpeg | `jsmpeg` or `rtmp` |
--| `--resolution` | 640x480 | Video resolution |
--| `--fps` | 24 | Frames per second |
--| `--bitrate` | 800 | Video bitrate (kbps) |
--| `--no-audio` | false | Disable audio capture |
--| `--dry-run` | false | Print command without running |
--
--## Controller Script
--
--Connects to OpenVibe.Live control WebSocket and executes viewer commands on the hardware (motors, servos, LEDs, TTS).
--
--```bash
--# Basic (simulation mode — prints commands)
--python3 controller.py --key YOUR_STREAM_KEY --server openvibe.live
--
--# With GPIO enabled (Raspberry Pi)
--python3 controller.py --key YOUR_KEY --server openvibe.live \
-- --enable-gpio --config gpio-config.json
--```
--
--### GPIO Config Example
--
--Create `gpio-config.json`:
--
--```json
--{
-- "gpio": {
-- "motor_left": { "forward": 17, "backward": 18 },
-- "motor_right": { "forward": 22, "backward": 23 },
-- "servos": { "tilt": 12, "pan": 13 },
-- "leds": { "main": 25, "status": 24 }
-- }
--}
--```
--
--### Supported Commands
--
--| Command | Action | Hardware |
--|---------|--------|----------|
--| `forward` | Move forward 0.5s | Motors |
--| `backward` | Move backward 0.5s | Motors |
--| `left` | Turn left 0.3s | Motors |
--| `right` | Turn right 0.3s | Motors |
--| `stop` | Stop all motors | Motors |
--| `tts` | Text-to-speech | espeak |
--| `led_on` | Turn LED on | GPIO |
--| `led_off` | Turn LED off | GPIO |
--| `horn` | Play horn sound | Speaker |
--| `servo_up` | Tilt servo up | Servo |
--| `servo_down` | Tilt servo down | Servo |
--| `servo_center` | Center servo | Servo |
--
--## Raspberry Pi Setup
--
--```bash
--# Install system dependencies
--sudo apt update
--sudo apt install ffmpeg espeak python3-pip pulseaudio-utils
--
--# Install Python packages
--pip3 install websocket-client gpiozero
--
--# Enable camera
--sudo raspi-config # Interface Options → Camera → Enable
--
--# Test camera
--ffmpeg -f v4l2 -i /dev/video0 -frames 1 test.jpg
--
--# Run both scripts
--python3 streamer.py --key YOUR_KEY --server openvibe.live &
--python3 controller.py --key YOUR_KEY --server openvibe.live --enable-gpio
--```
--
--## Troubleshooting
--
--| Problem | Solution |
--|---------|----------|
--| No video device | Run `ls /dev/video*`. If empty, check camera connection. Try `sudo modprobe uvcvideo`. |
--| No audio device | Run `arecord -l`. If empty, plug in a USB mic or check `pulseaudio --check`. |
--| Device busy | Close other camera apps (OBS, Cheese, etc.) and retry. |
--| Connection refused | Check server is running. Check firewall: `sudo ufw allow 9710`. |
--| High latency | Lower resolution, reduce bitrate, ensure `-muxdelay 0.001`. |
--| Choppy video | Upload bandwidth too low for bitrate. Reduce `-b:v` or resolution. |
--| Permission denied | `sudo usermod -aG video $USER` then log out/in. |
--| macOS camera | Use `-f avfoundation -i "0:0"` instead of `-f v4l2 -i /dev/video0`. |
--| PipeWire no sound | Use `-f pulse -i default` (PipeWire is PulseAudio-compatible). |
++The Raspberry Pi / single-board-computer scripts (`streamer.py`, `controller.py` and their guide)
++now live in [OpenVibe.Extensions `hardware/`](https://github.com/OpenVibers/OpenVibe.Extensions/tree/main/hardware),
++the home of OpenVibe's first-party client surfaces (ADR-023). They moved unchanged; their history
++before the move is in this repository's git log under `hardware/`.
ddiff --git a/hardware/controller.py b/hardware/controller.py
ddeleted file mode 100644
iindex d816f4a4..00000000
---- a/hardware/controller.py
++++ /dev/null
@@@ -1,278 +0,0 @@
--#!/usr/bin/env python3
--"""
--OpenVibe.Live — Hardware Controller Script
--Connects to OpenVibe.Live control WebSocket and executes commands
--on Raspberry Pi GPIO, servos, motors, TTS, etc.
--
--Usage:
-- python3 controller.py --key YOUR_STREAM_KEY --server openvibe.live
--
--Requirements:
-- - websocket-client: pip3 install websocket-client
-- - For TTS: espeak or pico2wave (apt install espeak)
-- - For GPIO: RPi.GPIO or gpiozero (pip3 install gpiozero)
--"""
--
--import argparse
--import json
--import subprocess
--import sys
--import signal
--import time
--import threading
--
--try:
-- import websocket
--except ImportError:
-- print("[ERROR] websocket-client not installed. Run: pip3 install websocket-client")
-- sys.exit(1)
--
--# Try to import GPIO (Raspberry Pi only)
--GPIO_AVAILABLE = False
--try:
-- from gpiozero import Motor, Servo, LED
-- GPIO_AVAILABLE = True
--except ImportError:
-- pass
--
--DEFAULT_SERVER = "localhost"
--DEFAULT_PORT = 3000
--
--# ═══════════════════════════════════════════════════════════════
--# Command Handlers
--# ═══════════════════════════════════════════════════════════════
--
--class HardwareController:
-- """Handles incoming commands and maps them to hardware actions."""
--
-- def __init__(self, config=None):
-- self.config = config or {}
-- self.motors = {}
-- self.servos = {}
-- self.leds = {}
--
-- if GPIO_AVAILABLE and self.config.get("enable_gpio"):
-- self._init_gpio()
--
-- def _init_gpio(self):
-- """Initialize GPIO devices based on config."""
-- gpio = self.config.get("gpio", {})
--
-- # Motors (L298N or similar H-bridge)
-- if "motor_left" in gpio:
-- pins = gpio["motor_left"]
-- self.motors["left"] = Motor(pins["forward"], pins["backward"])
-- if "motor_right" in gpio:
-- pins = gpio["motor_right"]
-- self.motors["right"] = Motor(pins["forward"], pins["backward"])
--
-- # Servos
-- for name, pin in gpio.get("servos", {}).items():
-- self.servos[name] = Servo(pin)
--
-- # LEDs
-- for name, pin in gpio.get("leds", {}).items():
-- self.leds[name] = LED(pin)
--
-- print(f"[GPIO] Motors: {list(self.motors.keys())}, Servos: {list(self.servos.keys())}, LEDs: {list(self.leds.keys())}")
--
-- def handle_command(self, command, data=None):
-- """Route a command to the appropriate handler."""
-- handlers = {
-- "forward": self.cmd_forward,
-- "backward": self.cmd_backward,
-- "left": self.cmd_left,
-- "right": self.cmd_right,
-- "stop": self.cmd_stop,
-- "tts": self.cmd_tts,
-- "led_on": self.cmd_led_on,
-- "led_off": self.cmd_led_off,
-- "horn": self.cmd_horn,
-- "servo_up": self.cmd_servo_up,
-- "servo_down": self.cmd_servo_down,
-- "servo_center": self.cmd_servo_center,
-- }
--
-- handler = handlers.get(command)
-- if handler:
-- print(f"[CMD] {command} {data or ''}")
-- handler(data)
-- else:
-- print(f"[CMD] Unknown command: {command}")
--
-- # Movement commands
-- def cmd_forward(self, data=None):
-- duration = (data or {}).get("duration", 0.5)
-- if self.motors:
-- for m in self.motors.values():
-- m.forward()
-- threading.Timer(duration, self.cmd_stop).start()
-- else:
-- print(" → [SIM] Moving forward")
--
-- def cmd_backward(self, data=None):
-- duration = (data or {}).get("duration", 0.5)
-- if self.motors:
-- for m in self.motors.values():
-- m.backward()
-- threading.Timer(duration, self.cmd_stop).start()
-- else:
-- print(" → [SIM] Moving backward")
--
-- def cmd_left(self, data=None):
-- duration = (data or {}).get("duration", 0.3)
-- if "left" in self.motors and "right" in self.motors:
-- self.motors["left"].backward()
-- self.motors["right"].forward()
-- threading.Timer(duration, self.cmd_stop).start()
-- else:
-- print(" → [SIM] Turning left")
--
-- def cmd_right(self, data=None):
-- duration = (data or {}).get("duration", 0.3)
-- if "left" in self.motors and "right" in self.motors:
-- self.motors["left"].forward()
-- self.motors["right"].backward()
-- threading.Timer(duration, self.cmd_stop).start()
-- else:
-- print(" → [SIM] Turning right")
--
-- def cmd_stop(self, data=None):
-- for m in self.motors.values():
-- m.stop()
-- print(" → [SIM] Stopped" if not self.motors else " → Motors stopped")
--
-- # TTS
-- def cmd_tts(self, data=None):
-- text = (data or {}).get("text", "hello from OpenVibe.Live")
-- try:
-- subprocess.Popen(["espeak", "-s", "150", text],
-- stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
-- print(f" → TTS: {text}")
-- except FileNotFoundError:
-- print(f" → [SIM] TTS (espeak not installed): {text}")
--
-- # LED
-- def cmd_led_on(self, data=None):
-- name = (data or {}).get("name", "main")
-- if name in self.leds:
-- self.leds[name].on()
-- print(f" → LED {name} ON")
--
-- def cmd_led_off(self, data=None):
-- name = (data or {}).get("name", "main")
-- if name in self.leds:
-- self.leds[name].off()
-- print(f" → LED {name} OFF")
--
-- # Horn/buzzer
-- def cmd_horn(self, data=None):
-- duration = (data or {}).get("duration", 0.5)
-- try:
-- subprocess.Popen(["aplay", "-d", str(duration), "/usr/share/sounds/alsa/Front_Center.wav"],
-- stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
-- except FileNotFoundError:
-- print(" → [SIM] Horn!")
--
-- # Servo
-- def cmd_servo_up(self, data=None):
-- name = (data or {}).get("name", "tilt")
-- if name in self.servos:
-- self.servos[name].value = min(1, self.servos[name].value + 0.2)
-- print(f" → Servo {name} UP")
--
-- def cmd_servo_down(self, data=None):
-- name = (data or {}).get("name", "tilt")
-- if name in self.servos:
-- self.servos[name].value = max(-1, self.servos[name].value - 0.2)
-- print(f" → Servo {name} DOWN")
--
-- def cmd_servo_center(self, data=None):
-- name = (data or {}).get("name", "tilt")
-- if name in self.servos:
-- self.servos[name].mid()
-- print(f" → Servo {name} CENTER")
--
--
--# ═══════════════════════════════════════════════════════════════
--# WebSocket Client
--# ═══════════════════════════════════════════════════════════════
--
--def connect(args, controller):
-- """Connect to OpenVibe.Live control WebSocket."""
-- protocol = "wss" if args.ssl else "ws"
-- url = f"{protocol}://{args.server}:{args.port}/ws/control"
-- reconnect_delay = 5
--
-- def on_open(ws):
-- print(f"[WS] Connected to {url}")
-- ws.send(json.dumps({
-- "type": "join",
-- "role": "hardware",
-- "streamKey": args.key,
-- }))
--
-- def on_message(ws, message):
-- try:
-- msg = json.loads(message)
-- if msg.get("type") == "command":
-- controller.handle_command(msg["command"], msg.get("data"))
-- elif msg.get("type") == "error":
-- print(f"[WS ERROR] {msg.get('message')}")
-- elif msg.get("type") == "ping":
-- ws.send(json.dumps({"type": "pong"}))
-- except json.JSONDecodeError:
-- print(f"[WS] Invalid message: {message}")
--
-- def on_error(ws, error):
-- print(f"[WS ERROR] {error}")
--
-- def on_close(ws, code, reason):
-- print(f"[WS] Disconnected (code={code}, reason={reason})")
-- if not args.no_reconnect:
-- print(f"[WS] Reconnecting in {reconnect_delay}s...")
-- time.sleep(reconnect_delay)
-- connect(args, controller)
--
-- ws = websocket.WebSocketApp(url,
-- on_open=on_open,
-- on_message=on_message,
-- on_error=on_error,
-- on_close=on_close)
--
-- ws.run_forever()
--
--def main():
-- parser = argparse.ArgumentParser(description="OpenVibe.Live — Hardware Controller")
-- parser.add_argument("--key", required=True, help="Your OpenVibe.Live stream key")
-- parser.add_argument("--server", default=DEFAULT_SERVER, help=f"Server hostname (default: {DEFAULT_SERVER})")
-- parser.add_argument("--port", type=int, default=DEFAULT_PORT, help=f"Server port (default: {DEFAULT_PORT})")
-- parser.add_argument("--ssl", action="store_true", help="Use WSS")
-- parser.add_argument("--no-reconnect", action="store_true", help="Disable auto-reconnect")
-- parser.add_argument("--enable-gpio", action="store_true", help="Enable GPIO (Raspberry Pi only)")
-- parser.add_argument("--config", help="Path to GPIO config JSON file")
-- args = parser.parse_args()
--
-- config = {"enable_gpio": args.enable_gpio}
-- if args.config:
-- try:
-- with open(args.config) as f:
-- config = json.load(f)
-- config["enable_gpio"] = args.enable_gpio
-- except (FileNotFoundError, json.JSONDecodeError) as e:
-- print(f"[WARN] Failed to load config: {e}")
--
-- controller = HardwareController(config)
--
-- print(f"\n🏕️ OpenVibe.Live — Hardware Controller")
-- print(f" Server: {args.server}:{args.port}")
-- print(f" GPIO : {'enabled' if args.enable_gpio and GPIO_AVAILABLE else 'simulation mode'}")
-- print(f" Press Ctrl+C to stop.\n")
--
-- signal.signal(signal.SIGINT, lambda s, f: sys.exit(0))
-- signal.signal(signal.SIGTERM, lambda s, f: sys.exit(0))
--
-- connect(args, controller)
--
--if __name__ == "__main__":
-- main()
ddiff --git a/hardware/streamer.py b/hardware/streamer.py
ddeleted file mode 100644
iindex 40224434..00000000
---- a/hardware/streamer.py
++++ /dev/null
@@@ -1,177 +0,0 @@
--#!/usr/bin/env python3
--"""
--OpenVibe.Live — FFmpeg Streamer Script
--Captures video/audio from Raspberry Pi camera + mic and streams
--to OpenVibe.Live via JSMPEG (HTTP POST of MPEG-TS) or RTMP.
--
--Usage:
-- python3 streamer.py --key YOUR_STREAM_KEY --server openvibe.live
-- python3 streamer.py --key YOUR_STREAM_KEY --protocol rtmp --server openvibe.live
--
--Requirements:
-- - FFmpeg installed (apt install ffmpeg)
-- - Camera: USB cam or Pi Camera (v4l2)
-- - Microphone: USB mic or built-in (ALSA)
--"""
--
--import argparse
--import subprocess
--import sys
--import signal
--import os
--
--DEFAULT_SERVER = "localhost"
--DEFAULT_VIDEO_PORT = 9710
--DEFAULT_RTMP_PORT = 1935
--
--def get_video_device():
-- """Find available video device."""
-- for dev in ["/dev/video0", "/dev/video1"]:
-- if os.path.exists(dev):
-- return dev
-- return None
--
--def get_audio_device():
-- """Find available ALSA audio device."""
-- try:
-- result = subprocess.run(["arecord", "-l"], capture_output=True, text=True)
-- if "card" in result.stdout.lower():
-- return "default"
-- except FileNotFoundError:
-- pass
-- return None
--
--def build_jsmpeg_command(args):
-- """Build FFmpeg command for JSMPEG streaming (MPEG-TS over HTTP POST)."""
-- video_dev = args.video_device or get_video_device()
-- audio_dev = args.audio_device or get_audio_device()
--
-- if not video_dev:
-- print("[ERROR] No video device found. Specify with --video-device")
-- sys.exit(1)
--
-- cmd = ["ffmpeg"]
--
-- # Input: video
-- cmd += ["-f", "v4l2", "-framerate", str(args.fps), "-video_size", args.resolution, "-i", video_dev]
--
-- # Input: audio (if available)
-- if audio_dev and not args.no_audio:
-- cmd += ["-f", "alsa", "-i", audio_dev]
--
-- # Video encoding: MPEG1 for JSMPEG
-- cmd += [
-- "-c:v", "mpeg1video",
-- "-b:v", f"{args.bitrate}k",
-- "-r", str(args.fps),
-- "-bf", "0",
-- ]
--
-- # Audio encoding
-- if audio_dev and not args.no_audio:
-- cmd += ["-c:a", "mp2", "-ar", "44100", "-ac", "1", "-b:a", "64k"]
-- else:
-- cmd += ["-an"]
--
-- # Output: HTTP POST to JSMPEG relay
-- url = f"http://{args.server}:{args.video_port}/{args.key}"
-- cmd += ["-f", "mpegts", url]
--
-- return cmd
--
--def build_rtmp_command(args):
-- """Build FFmpeg command for RTMP streaming."""
-- video_dev = args.video_device or get_video_device()
-- audio_dev = args.audio_device or get_audio_device()
--
-- if not video_dev:
-- print("[ERROR] No video device found. Specify with --video-device")
-- sys.exit(1)
--
-- cmd = ["ffmpeg"]
--
-- # Input: video
-- cmd += ["-f", "v4l2", "-framerate", str(args.fps), "-video_size", args.resolution, "-i", video_dev]
--
-- # Input: audio
-- if audio_dev and not args.no_audio:
-- cmd += ["-f", "alsa", "-i", audio_dev]
--
-- # Video encoding: H.264
-- cmd += [
-- "-c:v", "libx264",
-- "-preset", "ultrafast",
-- "-tune", "zerolatency",
-- "-b:v", f"{args.bitrate}k",
-- "-maxrate", f"{args.bitrate}k",
-- "-bufsize", f"{args.bitrate * 2}k",
-- "-g", str(args.fps * 2),
-- ]
--
-- # Audio encoding: AAC
-- if audio_dev and not args.no_audio:
-- cmd += ["-c:a", "aac", "-ar", "44100", "-ac", "1", "-b:a", "96k"]
-- else:
-- cmd += ["-an"]
--
-- # Output: RTMP
-- url = f"rtmp://{args.server}:{args.rtmp_port}/live/{args.key}"
-- cmd += ["-f", "flv", url]
--
-- return cmd
--
--def main():
-- parser = argparse.ArgumentParser(description="OpenVibe.Live — Stream from Raspberry Pi / Linux")
-- parser.add_argument("--key", required=True, help="Your OpenVibe.Live stream key")
-- parser.add_argument("--server", default=DEFAULT_SERVER, help=f"Server hostname (default: {DEFAULT_SERVER})")
-- parser.add_argument("--protocol", choices=["jsmpeg", "rtmp"], default="jsmpeg", help="Streaming protocol")
-- parser.add_argument("--video-device", help="Video device path (e.g. /dev/video0)")
-- parser.add_argument("--audio-device", help="ALSA audio device (e.g. default, hw:1,0)")
-- parser.add_argument("--resolution", default="640x480", help="Video resolution (default: 640x480)")
-- parser.add_argument("--fps", type=int, default=24, help="Frames per second (default: 24)")
-- parser.add_argument("--bitrate", type=int, default=800, help="Video bitrate in kbps (default: 800)")
-- parser.add_argument("--video-port", type=int, default=DEFAULT_VIDEO_PORT, help=f"JSMPEG video port (default: {DEFAULT_VIDEO_PORT})")
-- parser.add_argument("--rtmp-port", type=int, default=DEFAULT_RTMP_PORT, help=f"RTMP port (default: {DEFAULT_RTMP_PORT})")
-- parser.add_argument("--no-audio", action="store_true", help="Disable audio")
-- parser.add_argument("--dry-run", action="store_true", help="Print FFmpeg command without running")
-- args = parser.parse_args()
--
-- if args.protocol == "jsmpeg":
-- cmd = build_jsmpeg_command(args)
-- else:
-- cmd = build_rtmp_command(args)
--
-- print(f"\n🏕️ OpenVibe.Live — {args.protocol.upper()} Streaming")
-- print(f" Server : {args.server}")
-- print(f" Video : {args.resolution} @ {args.fps}fps, {args.bitrate}kbps")
-- print(f" Audio : {'disabled' if args.no_audio else 'enabled'}")
-- print(f" Command: {' '.join(cmd)}\n")
--
-- if args.dry_run:
-- print("[DRY RUN] Command not executed.")
-- return
--
-- # Run FFmpeg
-- proc = None
-- def signal_handler(sig, frame):
-- print("\n[INFO] Stopping stream...")
-- if proc:
-- proc.terminate()
-- sys.exit(0)
--
-- signal.signal(signal.SIGINT, signal_handler)
-- signal.signal(signal.SIGTERM, signal_handler)
--
-- print("[INFO] Starting stream... Press Ctrl+C to stop.")
-- try:
-- proc = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
-- _, stderr = proc.communicate()
-- if proc.returncode != 0:
-- print(f"[ERROR] FFmpeg exited with code {proc.returncode}")
-- print(stderr.decode()[-500:] if stderr else "")
-- except FileNotFoundError:
-- print("[ERROR] FFmpeg not found. Install with: sudo apt install ffmpeg")
-- sys.exit(1)
--
--if __name__ == "__main__":
-- main()