12 KiB
CL-Streamer Standalone Library Refactor
- Overview
- Phase 1: Define the Protocol (CLOS Generic Functions)
- Phase 2: Clean Package Boundaries
- Phase 3: Refactor Stream Server to iolib
- Phase 4: Separate Repository (Future)
- Task Checklist
- Notes
Overview
Extract cl-streamer from its current role as an embedded subsystem of Asteroid Radio
into a standalone, reusable Common Lisp audio streaming library. This aligns with Fade's
architecture vision: logically separated concerns with a stream daemon, a queue manager,
and the application layer communicating through well-defined protocols.
The goal is a library that any CL application could use to stream audio over HTTP, without any Asteroid-specific knowledge baked in.
Current State
cl-streamer already lives in its own directory (cl-streamer/) with its own ASDF
system definitions and packages. However, it has implicit coupling to Asteroid through:
- The glue layer (
stream-harmony.lisp) lives in the:asteroidpackage - Encoder creation and lifecycle managed by Asteroid, not cl-streamer
- Mount configuration hardcoded in Asteroid's startup
- Playlist loading, scheduling, and resume logic all in Asteroid
- DJ session control reaches directly into Harmony internals
Architecture Vision (per Fade)
"I want the basic architecture of the station to stay roughly the same. The concerns are logically separated, with a daemon holding up the streams, and a queue manager standing between the asteroid application and the stream daemon. This allows scaling horizontally in an unintrusive way."
Target architecture:
┌─────────────────────────┐ ┌──────────────────────────────┐
│ Asteroid Application │ │ cl-streamer (standalone) │
│ │ │ │
│ Playlist Scheduler ────┼──>──┤ Pipeline Protocol │
│ DJ Session ────────────┼──>──┤ (generic functions / CLOS) │
│ Admin APIs ────────────┼──>──┤ │
│ Track Metadata ────────┼──>──┤ Stream Server (iolib) │
│ │ │ Encoders (MP3, AAC) │
│ Frontend/ParenScript │ │ Broadcast Buffer │
│ │ │ Harmony Backend │
│ Rate Limiting │ │ ICY Protocol │
│ User Management │ │ │
└─────────────────────────┘ └──────────────────────────────┘
Application Library / Daemon
Phase 1: Define the Protocol (CLOS Generic Functions)
The key deliverable is a protocol layer that decouples Asteroid from cl-streamer internals.
1.1 Streaming Protocol Classes
Define protocol classes that cl-streamer exports and Asteroid programs against:
;; Protocol classes — no implementation, just interface
(defclass stream-pipeline () ()
(:documentation "Protocol class for an audio streaming pipeline."))
(defclass stream-encoder () ()
(:documentation "Protocol class for an audio encoder (MP3, AAC, etc)."))
(defclass stream-server () ()
(:documentation "Protocol class for the HTTP stream server."))
1.2 Generic Functions (the Protocol)
These replace the current direct function calls:
;;; Pipeline lifecycle
(defgeneric pipeline-start (pipeline))
(defgeneric pipeline-stop (pipeline))
(defgeneric pipeline-running-p (pipeline))
;;; Playback control
(defgeneric pipeline-play-file (pipeline file-path))
(defgeneric pipeline-play-list (pipeline file-list &key on-track-change on-playlist-change))
(defgeneric pipeline-skip (pipeline))
(defgeneric pipeline-queue-files (pipeline files))
(defgeneric pipeline-clear-queue (pipeline))
(defgeneric pipeline-get-queue (pipeline))
;;; State queries
(defgeneric pipeline-current-track (pipeline))
(defgeneric pipeline-listener-count (pipeline &optional mount))
;;; Metadata
(defgeneric pipeline-update-metadata (pipeline mount title &optional url))
;;; Encoder protocol
(defgeneric encoder-encode (encoder pcm-data &key start end))
(defgeneric encoder-flush (encoder))
(defgeneric encoder-close (encoder))
;;; Server protocol
(defgeneric server-start (server))
(defgeneric server-stop (server))
(defgeneric server-add-mount (server path &key content-type bitrate name))
(defgeneric server-remove-mount (server path))
(defgeneric server-write-audio (server mount-path data &key start end))
1.3 Callback Protocol
Replace the current setf slot approach with a proper callback/hook system:
(defgeneric pipeline-add-hook (pipeline event function))
(defgeneric pipeline-remove-hook (pipeline event function))
;; Events: :track-change, :playlist-change, :pipeline-start, :pipeline-stop
1.4 Files to Create/Modify
| File | Action | Description |
|---|---|---|
cl-streamer/protocol.lisp |
NEW | Protocol classes and generic functions |
cl-streamer/package.lisp |
MODIFY | Export protocol symbols |
cl-streamer/cl-streamer.asd |
MODIFY | Add protocol.lisp to all systems |
cl-streamer/harmony-backend.lisp |
MODIFY | Implement protocol for Harmony backend |
Phase 2: Clean Package Boundaries
2.1 Move Asteroid-Specific Code Out of cl-streamer
Currently cl-streamer is clean. The coupling is in Asteroid's stream-harmony.lisp
which directly references cl-streamer internals. This file should become a thin adapter:
| Current (stream-harmony.lisp) | Target |
|---|---|
| Creates encoders explicitly | Calls (make-pipeline :mp3 :aac) |
| Manages mount points manually | Pipeline auto-creates from config |
| Stores encoder instances as globals | Pipeline owns encoder lifecycle |
| Track-change callback via setf | Uses pipeline-add-hook |
| Docker path conversion | Stays in Asteroid (app-specific) |
| Playback state persistence | Stays in Asteroid (app-specific) |
| Playlist scheduling | Stays in Asteroid (app-specific) |
2.2 Declarative Pipeline Configuration (DSL)
Replace the imperative setup in start-harmony-streaming with a declarative form:
;; Target API — Asteroid calls this at startup
(setf *harmony-pipeline*
(cl-streamer:make-pipeline
'(:input (:harmony :channels 2 :samplerate 44100)
:outputs ((:mp3 :mount "/asteroid.mp3" :bitrate 128
:content-type "audio/mpeg" :name "Asteroid Radio MP3")
(:aac :mount "/asteroid.aac" :bitrate 128
:content-type "audio/aac" :name "Asteroid Radio AAC"))
:server (:port 8000))))
This keeps the pipeline definition in Asteroid but all the wiring inside cl-streamer.
2.3 Remove Global State
Current globals in cl-streamer.lisp (*server*, *default-port*) should move to
pipeline-scoped state. Multiple pipelines should be possible (needed for shuffle channel).
Phase 3: Refactor Stream Server to iolib
Per Fade's recommendation, replace sb-bsd-sockets / usocket with iolib for:
- Nonblocking I/O with event loop (epoll/kqueue)
- Better stale socket detection (SO_KEEPALIVE, TCP_NODELAY)
- Portability across CL implementations
- Cleaner connection lifecycle management
3.1 Changes Required
| File | Change |
|---|---|
cl-streamer/stream-server.lisp |
Rewrite socket handling to use iolib |
cl-streamer/cl-streamer.asd |
Replace usocket dependency with iolib |
cl-streamer/package.lisp |
Update exports if socket API changes |
3.2 Key iolib Features to Use
iolib:make-socketwith:connect :passivefor the listeneriolib:accept-connectionfor new clientsiomux:event-dispatchor per-client threads with iolib socketsiolib:socket-optionforSO_KEEPALIVE,TCP_NODELAY- Proper
:timeouton write operations to detect stale clients
3.3 Risk Mitigation
- iolib requires
libfixposix— ensure it's available on VPS - Test with multiple concurrent listeners
- Benchmark against current implementation (should be equivalent or better)
Phase 4: Separate Repository (Future)
Once cl-streamer is fully standalone, it could be extracted to its own git repo and loaded via Quicklisp/Ultralisp. This is optional and depends on whether other projects want to use it.
4.1 What Stays in Asteroid
stream-harmony.lisp— glue layer / adapterstream-control.lisp— queue management, playlist generationplaylist-scheduler.lisp— cron-based playlist switchingdj-session.lisp— DJ console logic (uses protocol)- All ParenScript/frontend code
- Docker path conversion, playback state persistence
4.2 What Goes with cl-streamer
cl-streamer/directory (all files)libfdkaac-shim.soandfdkaac-shim.c- Protocol definitions
- Encoder implementations (LAME, FDK-AAC)
- Harmony backend
- Stream server
- Broadcast buffer
- ICY protocol
Task Checklist
Phase 1: Protocol Definition
- Create
cl-streamer/protocol.lispwith protocol classes and generic functions - Update
cl-streamer/package.lispto export protocol symbols - Implement protocol in
harmony-backend.lisp(methods on existing classes) - Update
stream-harmony.lispto use protocol generics instead of direct calls - Verify:
make 2>&1compiles, audio plays, metadata updates
Phase 2: Clean Boundaries
- Implement
make-pipelineDSL for declarative pipeline creation - Move encoder lifecycle into pipeline (pipeline owns encoders)
- Move mount creation into pipeline configuration
- Remove global
*server*— pipeline-scoped server - Add hook system for callbacks (
pipeline-add-hook/pipeline-remove-hook) - Update
dj-session.lispto use protocol generics - Verify: full integration test (auto-playlist + DJ console + skip + metadata)
Phase 3: iolib Refactor
- Install iolib and libfixposix on dev machine
- Rewrite
stream-server.lispsocket layer to use iolib - Add SO_KEEPALIVE and TCP_NODELAY to client sockets
- Add write timeouts for stale client detection
- Update
cl-streamer.asddependencies - Stress test: multiple concurrent listeners, client disconnect scenarios
- Test on VPS (ensure libfixposix available)
Phase 4: Extraction (Future / Optional)
- Create separate git repository for cl-streamer
- Write proper README with usage examples
- Register on Ultralisp for Quicklisp distribution
- Update Asteroid to load cl-streamer as external dependency
Notes
- Branch:
glenneth/cl-streamer-standalone(offexperimental/cl-streaming) - The current
experimental/cl-streamingbranch remains the stable working version - Each phase should end with a working, tested system
- Phase 1 is the highest priority — it establishes the protocol without breaking anything
- Phase 3 (iolib) can be done independently of Phase 2
- The C shim for FDK-AAC (
libfdkaac-shim.so) travels with cl-streamer — it's needed to avoid SBCL signal handler conflicts with FDK-AAC internals