Listener voting, authoritative round management, and resilient Windows playout for RadioTEDU.
VoterTAI is RadioTEDU's listener-controlled radio system. Listeners vote for
the next song on the website or inside the mobile app, the web server keeps the
authoritative voting round, and a supervised Windows playout PC streams the
winning music continuously to the /ai radio mount.
This repository contains the complete voting stack:
- the public React voting website;
- the web-server voting API and real-time transport;
- the Windows voting and radio agent;
- the process supervision, startup, recovery, and stream diagnostics needed to keep the station running.
- What the project does
- Architecture
- Repository structure
- Requirements
- Quick start
- Voting and playback lifecycle
- Resilience model
- Mobile WebView integration
- Health and verification
- Security boundaries
- The Voting PC scans a local music directory and starts a continuous radio stream.
- While the current song plays, the agent selects eligible candidates and publishes a voting round to the backend over an outbound authenticated WebSocket.
- Browsers and the mobile WebView receive the active round and live vote totals from the web server.
- The backend locks and resolves the round authoritatively.
- The winning song is queued on the Voting PC and becomes the next track.
- If no winner is available, the agent continues with filler music. It never intentionally stops the audio encoder between tracks.
flowchart LR
PC["Voting PC<br/>music library + playout agent"]
API["RadioTEDU web server<br/>API + WSS + Socket.IO"]
ORIGIN["Icecast/TinyIce<br/>/ai origin + fallback"]
WEB["Voting website<br/>/vote/"]
MOBILE["Mobile app<br/>WebView"]
LISTENER["Radio listener"]
PC -- "authenticated outbound WSS<br/>rounds and results" --> API
PC -- "continuous AAC source<br/>/ai" --> ORIGIN
API -- "HTTPS + Socket.IO" --> WEB
API -- "HTTPS + WebView bridge" --> MOBILE
WEB -- "votes" --> API
MOBILE -- "votes" --> API
ORIGIN -- "HTTPS audio" --> LISTENER
The mobile app and public website never connect directly to the Voting PC. The PC requires no public inbound port: it initiates both the backend WebSocket and the Icecast source connection.
votertai/
├── backend/ RadioTEDU server and voting integration
│ ├── src/routes/nextSongVoting.ts Public voting HTTP routes
│ ├── src/services/
│ │ ├── nextSongVotingService.ts Authoritative voting-round logic
│ │ └── radioAgentTunnel.ts Authenticated Voting PC WebSocket tunnel
│ ├── src/votingWebRoutes.ts /vote/ static-site hosting
│ ├── src/db/migrations/ Voting database migration
│ ├── .env.example Server configuration template
│ └── Windows-Deployment.md Windows server deployment notes
│
├── voting-web/ Public browser and mobile WebView UI
│ ├── src/App.tsx Voting screen
│ ├── src/voting.ts API, socket, and stream configuration
│ ├── src/styles.css RadioTEDU voting design
│ ├── public/ Static assets
│ └── README.md WebView bridge and frontend contract
│
├── tools/local-voting-agent/ Windows playout/source application
│ ├── src/agent/
│ │ ├── backendClient.ts Outbound authenticated WSS client
│ │ ├── folderCatalog.ts Music-folder scanning and metadata
│ │ ├── roundEngine.ts Candidate, vote, lock, and winner logic
│ │ ├── localHttpStreamer.ts Continuous local MP3 stream
│ │ ├── icecastRelay.ts Continuous PCM-to-AAC Icecast source
│ │ └── processLock.ts Duplicate-process protection
│ ├── src/server/ Local API, dashboard, and automation
│ ├── src/web/ Local diagnostic dashboard
│ ├── scripts/
│ │ ├── voting-supervisor.mjs Health watchdog and process recovery
│ │ ├── install-voting-service.ps1
│ │ ├── install-voting-startup.ps1
│ │ ├── voting_windows_service.py
│ │ └── start-production-voting-round.ps1
│ ├── WEB_SERVER_AI_STREAM_HANDOFF.md
│ ├── .env.example Voting PC configuration template
│ └── README.md Detailed agent operations
│
├── .gitignore
└── README.md
Generated dist/, dist-server/, runtime logs, process locks, credentials,
and local .env files are not committed.
| Purpose | URL |
|---|---|
| Voting website | https://radiotedu.com/vote/ |
| Mobile WebView | https://radiotedu.com/vote/?embed=1 |
| Voting API base | https://radiotedu.com/jukebox/api/v1 |
| Socket.IO transport | https://radiotedu.com/jukebox/socket.io |
| Radio stream | https://stream.radiotedu.com/ai |
| Agent WebSocket | wss://radiotedu.com/jukebox/api/v1/next-song-voting/agent/connect |
- Node.js 20 or newer
- PostgreSQL
- Redis
- Icecast-compatible origin such as TinyIce
- Nginx or an equivalent TLS reverse proxy
- A server-local fallback audio source for zero-dead-air operation
- Windows 10 or 11
- Node.js 20.6 or newer
- FFmpeg and ffprobe
- A readable local music directory
- A dedicated backend agent identity and secret
- A dedicated Icecast source account for
/ai
cd voting-web
npm ci
npm test
npm run buildUse npm run dev for local frontend development. The development-only demo is
available at http://127.0.0.1:4321/?demo=1 and is not enabled in production.
cd backend
npm ci
Copy-Item .env.example .env
npm run db:migrate
npm run db:migrate:voting-agent
npm test
npm run buildThe backend must be deployed with its protected production environment. Do not use example database passwords, JWT secrets, agent secrets, or source credentials in production.
cd tools\local-voting-agent
npm ci
Copy-Item .env.example .env
npm test
npm run buildAt minimum, configure these values in the local .env:
MUSIC_LIBRARY_DIR=C:\Path\To\Music
YOUTUBE_COVER_SYNC_ENABLED=true
YOUTUBE_COVER_SYNC_MINUTES=15
VOTING_AGENT_PLAYBACK_MODE=live
LOCAL_HTTP_STREAM_ENABLED=true
LOCAL_HTTP_STREAM_PORT=4320
LOCAL_HTTP_STREAM_PATH=/ai
ICECAST_STREAM_ENABLED=true
ICECAST_SOURCE_URL=http://stream.radiotedu.com:11154/ai
ICECAST_SOURCE_USERNAME=<dedicated-source-user>
ICECAST_SOURCE_PASSWORD=<dedicated-source-password>
ICECAST_CODEC=mp3
ICECAST_SOURCE_TRANSPORT=http
ICECAST_LEGACY_SOURCE=false
BACKEND_SYNC_ENABLED=true
RADIO_AGENT_TRANSPORT=websocket
RADIO_AGENT_CONNECT_URL=wss://radiotedu.com/jukebox/api/v1/next-song-voting/agent/connect
RADIO_AGENT_ID=school-radio-pc
RADIO_AGENT_REQUEST_SECRET=<dedicated-agent-secret>For production, install the automatic Windows service from an elevated PowerShell window:
powershell -ExecutionPolicy Bypass -File .\scripts\install-voting-service.ps1The service starts during Windows boot as LocalSystem, before user sign-in.
It restores the Node supervisor when missing, verifies both the local API and
real audio bytes, and relies on the agent's indefinite WSS and Icecast retry
loops. Windows Service Recovery restarts the service after unexpected failures.
The process locks make an accidental manual or sign-in launch harmless.
When administrator access is unavailable, use
install-voting-startup.ps1 as a sign-in-only fallback.
Track starts
-> candidates are selected
-> round is published to the backend
-> listeners vote
-> backend locks and resolves the round
-> winner is queued
-> winner plays when the current track ends
-> next round opens
Recent tracks and the currently playing song are excluded from candidate selection when possible. No-vote rounds and ties have deterministic fallback resolution paths, so automation does not wait forever for a ballot.
The Windows agent is designed to recover without manual intervention:
- an exclusive process lock prevents two agents from racing for
/ai; - a separate supervisor lock prevents duplicate watchdogs;
- the music encoder remains alive across normal track transitions;
- short PCM silence frames cover decoder startup or recovery gaps;
- stalled decoders are terminated and replaced;
- FFmpeg pipe errors do not crash the Node process;
- backend WSS and Icecast connections retry indefinitely with bounded backoff;
- music files are rescanned periodically;
- media probes have hard timeouts, so a damaged file cannot block boot;
- the supervisor validates real local audio, not merely an open TCP port;
- logs report state changes rather than repeating every failed retry.
Zero dead air on the public URL also requires a server-local fallback mount.
The Voting PC cannot provide audio while it is powered off or disconnected, so
the Icecast/TinyIce origin must keep fallback audio active and automatically
return listeners to the primary /ai source after reconnection. See
WEB_SERVER_AI_STREAM_HANDOFF.md.
The mobile application embeds https://radiotedu.com/vote/?embed=1. It must not
place access tokens in the URL. After the page sends
radiotedu.voting.ready, the app injects the existing mobile session through
the in-memory bridge:
window.__RADIOTEDU_SET_AUTH__({
accessToken: '<mobile access token>',
user: {display_name: 'Listener', is_guest: false}
});
true;The page reports successful votes with radiotedu.voting.vote-recorded. See
voting-web/README.md for the complete navigation and
message contract.
Local agent health:
Invoke-RestMethod http://127.0.0.1:4317/api/health
Invoke-RestMethod http://127.0.0.1:4317/api/stateExpected health includes:
playbackState: "playing"backendConnection: "connected"- a non-error Icecast source state
- the detected catalog track count
Verify public audio by decoding it, not only by checking HTTP headers:
ffmpeg -hide_banner -loglevel error `
-t 30 `
-i https://stream.radiotedu.com/ai `
-f null NULRun all package checks before deployment:
cd voting-web
npm ci
npm test
npm run build
cd ..\backend
npm ci
npm test
npm run build
cd ..\tools\local-voting-agent
npm ci
npm test
npm run build- Never commit
.envfiles, passwords, tokens, local paths, runtime logs, or source URLs containing credentials. - Use a dedicated agent ID and secret for VoterTAI; do not reuse the Juke Local or BroadcastAI identity.
- The web server validates the agent handshake and remains authoritative for vote acceptance and resolution.
- The local control API binds to loopback only.
- Album art and candidate payloads are validated and size-limited before they reach the public backend.
- The mobile WebView accepts only the production voting origin and keeps auth tokens in memory.
/aibelongs exclusively to the VoterTAI voting channel. Other RadioTEDU mounts are outside this project's ownership.
- Voting website and WebView contract
- Windows voting agent operations
/aiweb-server handoff and acceptance tests- Backend Windows deployment
VoterTAI is a RadioTEDU project. Add the organization's chosen license before redistributing the code outside its intended deployment.

