Skip to content

Commit 55dfde7

Browse files
authored
Merge pull request #2 from script-php/playlist
Playlist
2 parents 2dc27c3 + 86555f1 commit 55dfde7

15 files changed

Lines changed: 1808 additions & 25 deletions

File tree

README.md

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ GoStream is a lightweight MP3 streaming server written in Go. It provides real-t
1111
- **Audio normalization** - Automatic FFmpeg-based audio normalization to standardized bitrate and sample rate for consistent streaming
1212
- **Stream metadata** - ID3 tag parsing for track title and artist information
1313
- **Icecast compatibility** - Stats endpoint compatible with Icecast format for player integration
14+
- **Icecast source input** - Accept live audio from DJ apps and other sources via Icecast protocol
1415
- **Configurable gap/silence** - Set custom silence duration between songs (default 500ms)
1516
- **CORS support** - Cross-Origin Resource Sharing enabled for browser-based streaming
1617
- **Debug mode** - Enhanced logging for troubleshooting
@@ -61,6 +62,7 @@ The server will start on port 8090 by default.
6162
- `-debug` - Enable debug mode for detailed logging
6263
- `-n string` - Server name (default: "GoStream")
6364
- `-gap int` - Gap/silence between songs in milliseconds (default: 500)
65+
- `-icecast-source-port int` - Port for Icecast source client connections (default: 0 = disabled)
6466
- `-c string` - Load configuration from JSON file or URL
6567
- `-h` - Show help information
6668

@@ -116,9 +118,72 @@ Create a `config.json` file:
116118
- `standard_sample_rate` (string) - Sample rate for normalized audio (e.g., "44100", "48000") - default: "44100"
117119
- `cache_dir` (string) - Directory to store cached normalized files - default: ".cache"
118120
- `cache_ttl_minutes` (int) - Cache time-to-live in minutes (files older than this are deleted, 0 = no cleanup) - default: 10
121+
- `icecast_source_port` (int) - Port for Icecast source client connections (0 = disabled) - default: 0
119122

120123
**Note:** Audio normalization is always enabled for consistent stream quality. All songs are automatically normalized to the standard bitrate and sample rate. Command-line arguments take precedence over config file values.
121124

125+
### Icecast Source Input (Live Audio)
126+
127+
GoStream supports accepting live audio from DJ applications and other sources via the **Icecast protocol**. This allows you to stream live audio without pre-recorded files.
128+
129+
#### Enabling Icecast Source Input
130+
131+
Add to your config file:
132+
133+
```json
134+
{
135+
"port": 8090,
136+
"icecast_source_port": 8001
137+
}
138+
```
139+
140+
Or use command-line flag:
141+
142+
```bash
143+
./gostream -icecast-source-port 8001
144+
```
145+
146+
#### Connecting a Source
147+
148+
Use any Icecast-compatible source client to push audio to your server:
149+
150+
```bash
151+
# Example with curl and ffmpeg
152+
ffmpeg -i input.mp3 -f mp3 -b:a 128k - | \
153+
curl -X SOURCE \
154+
-H "Content-Type: audio/mpeg" \
155+
--data-binary @- \
156+
http://localhost:8001/
157+
```
158+
159+
#### Features
160+
161+
- Accept audio from DJ apps, ffmpeg, OBS Studio, and standard Icecast source clients
162+
- Automatic failover to file playlist when source disconnects
163+
- Broadcast live audio to all connected listeners
164+
- Full HTTP compatibility
165+
166+
For detailed setup instructions and examples, see [ICECAST_SOURCE_GUIDE.md](release/ICECAST_SOURCE_GUIDE.md).
167+
168+
#### Testing Icecast Connection
169+
170+
Use the provided test scripts to verify your setup:
171+
172+
**Linux/macOS:**
173+
```bash
174+
./release/test-icecast-source.sh localhost 8001 your_audio.mp3
175+
```
176+
177+
**Windows (PowerShell):**
178+
```powershell
179+
.\release\test-icecast-source.ps1 localhost 8001 your_audio.mp3
180+
```
181+
182+
**Windows (Batch):**
183+
```cmd
184+
release\test-icecast-source.bat localhost 8001 your_audio.mp3
185+
```
186+
122187
### Cache Management
123188

124189
GoStream includes automatic cache cleanup to prevent unlimited disk usage when audio normalization is enabled.

TODO.md

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11

2-
playlist
3-
update the song list on upload
2+
playlist {cm:2026-02-27}
3+
update the song list on upload {cm:2026-02-28}
44
icy headers {cm:2026-02-26}
55
stable {cm:2026-02-25}
66
shoutcat protocol {cm:2026-02-26}
77
hash identification instead of index id {cm:2026-02-26}
8-
change next song with a custom one
8+
change next song with a custom one {cm:2026-02-27}
9+
broadcast

main.go

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,14 +5,104 @@ import (
55
"gostream/modules"
66
"gostream/routes"
77
"fmt"
8+
"time"
89

910
"github.com/labstack/echo/v4"
1011
"github.com/labstack/echo/v4/middleware"
1112
)
1213

14+
// icecastNormalizerFeeder reads chunks from Icecast source, normalizes them, and feeds to MusicReader
15+
// It automatically manages mode switching based on whether an Icecast source is connected
16+
func icecastNormalizerFeeder() {
17+
modules.Logger.Info("Icecast normalizer feeder started")
18+
var isIcecastProcessing bool
19+
var processorWaitCh chan struct{}
20+
21+
for {
22+
// Check if there's an active Icecast source connection
23+
hasSource := modules.IcecastSource.HasActiveSource()
24+
25+
// Transition: Source connected, start Icecast mode
26+
if hasSource && !isIcecastProcessing {
27+
modules.Logger.Info("Icecast source connected - switching to Icecast mode")
28+
29+
// Close current file and advance to next song before switching modes
30+
// This ensures: 1) current file is released (no lock for cleanup), 2) next song is ready when Icecast disconnects
31+
modules.MusicReader.SkipToNext()
32+
modules.Logger.Info("Advancing to next song in preparation for Icecast mode")
33+
34+
modules.MusicReader.EnableIcecastMode()
35+
36+
// Create a channel to signal when processor is done
37+
processorWaitCh = make(chan struct{})
38+
go func() {
39+
modules.MusicReader.ProcessIcecastStream()
40+
close(processorWaitCh)
41+
}()
42+
43+
isIcecastProcessing = true
44+
time.Sleep(200 * time.Millisecond) // Give processor time to start
45+
continue
46+
}
47+
48+
// Transition: Source disconnected, revert to file mode
49+
if !hasSource && isIcecastProcessing {
50+
modules.Logger.Info("Icecast source disconnected - reverting to file mode")
51+
modules.MusicReader.DisableIcecastMode()
52+
53+
// Wait for processor to exit (with timeout)
54+
select {
55+
case <-processorWaitCh:
56+
modules.Logger.Info("Icecast processor exited cleanly")
57+
case <-time.After(2 * time.Second):
58+
modules.Logger.Info("Icecast processor did not exit within 2s, continuing")
59+
}
60+
61+
isIcecastProcessing = false
62+
time.Sleep(200 * time.Millisecond) // Give StartLoop time to resume file feeding
63+
continue
64+
}
65+
66+
// No source and not processing - check periodically
67+
if !hasSource && !isIcecastProcessing {
68+
time.Sleep(500 * time.Millisecond)
69+
continue
70+
}
71+
72+
// Source is active and we're processing - get next chunk
73+
chunk, ok := modules.IcecastSource.GetAudioChunk()
74+
if !ok {
75+
time.Sleep(10 * time.Millisecond)
76+
continue
77+
}
78+
79+
// For live streaming, pass chunks through directly (no re-encoding)
80+
// Icecast already provides MP3 data from Mixxx
81+
// Skip FFmpeg normalization to avoid latency/jerkiness
82+
83+
// Feed to MusicReader buffer system
84+
err := modules.MusicReader.FeedIcecastChunk(chunk)
85+
if err != nil {
86+
modules.Logger.Debug("Failed to feed chunk: " + err.Error())
87+
}
88+
}
89+
}
90+
1391
func main() {
1492

1593
modules.InitReader()
94+
95+
// Initialize Icecast source server on port 8001
96+
modules.InitIcecastServer("8001")
97+
go func() {
98+
err := modules.IcecastSource.Start()
99+
if err != nil {
100+
modules.Logger.Error(fmt.Sprintf("Icecast server failed: %v", err))
101+
}
102+
}()
103+
104+
// Start Icecast normalizer feeder (runs continuously, checks mode)
105+
go icecastNormalizerFeeder()
16106

17107
e := echo.New()
18108

@@ -31,3 +121,4 @@ func main() {
31121
modules.Logger.Error(err)
32122
}
33123
}
124+

middlewares/auth.go

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
package middlewares
2+
3+
import (
4+
"gostream/modules"
5+
"net/http"
6+
7+
"github.com/labstack/echo/v4"
8+
)
9+
10+
// BasicAuth middleware checks HTTP Basic Authentication
11+
func BasicAuth(next echo.HandlerFunc) echo.HandlerFunc {
12+
return func(ctx echo.Context) error {
13+
// Skip auth if credentials are not configured
14+
if modules.Config.Username == "" || modules.Config.Password == "" {
15+
return next(ctx)
16+
}
17+
18+
username, password, ok := ctx.Request().BasicAuth()
19+
20+
// Check if credentials match
21+
if !ok || username != modules.Config.Username || password != modules.Config.Password {
22+
ctx.Response().Header().Set("WWW-Authenticate", "Basic realm=\"GoStream\"")
23+
return ctx.JSON(http.StatusUnauthorized, map[string]string{
24+
"error": "Unauthorized",
25+
"message": "Invalid username or password",
26+
})
27+
}
28+
29+
return next(ctx)
30+
}
31+
}
32+

modules/config.go

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,9 @@ type IConfig struct {
3535
Notice1 string // Shoutcast notice 1 (icy-notice1 header)
3636
Notice2 string // Shoutcast notice 2 (icy-notice2 header)
3737
MetaInterval int // Metadata interval in bytes (default 8192)
38+
// Authentication
39+
Username string // Username for API authentication
40+
Password string // Password for API authentication
3841
}
3942

4043
var Config *IConfig
@@ -58,6 +61,9 @@ type JSONConfig struct {
5861
Notice1 string `json:"notice1"`
5962
Notice2 string `json:"notice2"`
6063
MetaInterval int `json:"meta_interval"`
64+
// Authentication
65+
Username string `json:"username"`
66+
Password string `json:"password"`
6167
}
6268

6369
// LoadConfigFromFile loads configuration from a local JSON file
@@ -135,6 +141,8 @@ func init() {
135141
var notice1 string = ""
136142
var notice2 string = ""
137143
var metaInterval int = 8192
144+
var username string = ""
145+
var password string = ""
138146

139147
flag.StringVar(&name, "n", "GoStream", "server name")
140148
flag.IntVar(&port, "p", 8090, "server port number")
@@ -206,6 +214,12 @@ func init() {
206214
if jsonConfig.MetaInterval > 0 {
207215
metaInterval = jsonConfig.MetaInterval
208216
}
217+
if jsonConfig.Username != "" {
218+
username = jsonConfig.Username
219+
}
220+
if jsonConfig.Password != "" {
221+
password = jsonConfig.Password
222+
}
209223

210224
// Boolean flags - only override if they're true in config
211225
if jsonConfig.Random {
@@ -241,6 +255,8 @@ func init() {
241255
Notice1: notice1,
242256
Notice2: notice2,
243257
MetaInterval: metaInterval,
258+
Username: username,
259+
Password: password,
244260
}
245261
}
246262

0 commit comments

Comments
 (0)