Skip to content

Commit be35a7a

Browse files
committed
I'm not anti-social. I'm just not social. - Woody Allen
1 parent e90489d commit be35a7a

File tree

15 files changed

+1883
-0
lines changed

15 files changed

+1883
-0
lines changed

StreamPlayer/README.md

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
# Stream Player
2+
3+
A simple and efficient stream player written in Go that can play various audio streams using FFplay.
4+
5+
## Features
6+
7+
- Play various audio stream formats (MP3, AAC, etc.)
8+
- Volume control
9+
- Stream monitoring and error handling
10+
- Cross-platform support (Windows, macOS, Linux)
11+
- Graceful shutdown handling
12+
13+
## Prerequisites
14+
15+
1. Go 1.16 or later
16+
2. FFplay (part of FFmpeg)
17+
18+
### Installing FFmpeg
19+
20+
#### Windows
21+
1. Download FFmpeg from https://ffmpeg.org/download.html
22+
2. Extract the archive
23+
3. Add the FFmpeg bin directory to your system PATH
24+
25+
#### macOS
26+
```bash
27+
brew install ffmpeg
28+
```
29+
30+
#### Linux
31+
```bash
32+
sudo apt-get install ffmpeg # Ubuntu/Debian
33+
sudo dnf install ffmpeg # Fedora
34+
```
35+
36+
## Building
37+
38+
```bash
39+
cd StreamPlayer
40+
go build -o streamplayer
41+
```
42+
43+
## Usage
44+
45+
Basic usage:
46+
```bash
47+
./streamplayer -url "http://example.com/stream.mp3"
48+
```
49+
50+
Options:
51+
- `-url`: The URL of the stream to play (required)
52+
- `-volume`: Volume level (0-100, default: 100)
53+
- `-device`: Audio device to use (optional)
54+
- `-format`: Stream format (mp3, aac, etc., default: mp3)
55+
56+
Example with options:
57+
```bash
58+
./streamplayer -url "http://example.com/stream.mp3" -volume 80 -format mp3
59+
```
60+
61+
## Integration with Radio Browser
62+
63+
This player can be used as a backend for the Radio Browser application. The Radio Browser will call this player with the appropriate stream URL when a station is selected.
64+
65+
## Error Handling
66+
67+
The player includes built-in error handling for:
68+
- Invalid URLs
69+
- Stream connection issues
70+
- FFplay errors
71+
- System signals (Ctrl+C)
72+
73+
## License
74+
75+
MIT License

StreamPlayer/go.mod

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
module streamplayer
2+
3+
go 1.21

StreamPlayer/main.go

Lines changed: 156 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,156 @@
1+
package main
2+
3+
import (
4+
"bufio"
5+
"flag"
6+
"fmt"
7+
"io"
8+
"net/http"
9+
"os"
10+
"os/exec"
11+
"os/signal"
12+
"path/filepath"
13+
"runtime"
14+
"syscall"
15+
"time"
16+
)
17+
18+
var (
19+
url = flag.String("url", "", "Stream URL to play")
20+
volume = flag.Int("volume", 100, "Volume level (0-100)")
21+
device = flag.String("device", "", "Audio device to use")
22+
format = flag.String("format", "mp3", "Stream format (mp3, aac, etc.)")
23+
player *exec.Cmd
24+
stopChan = make(chan struct{})
25+
)
26+
27+
func main() {
28+
flag.Parse()
29+
30+
if *url == "" {
31+
fmt.Println("Please provide a stream URL using -url flag")
32+
os.Exit(1)
33+
}
34+
35+
// Handle interrupt signals
36+
sigChan := make(chan os.Signal, 1)
37+
signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM)
38+
39+
// Start the stream
40+
go playStream()
41+
42+
// Wait for interrupt signal
43+
<-sigChan
44+
stopStream()
45+
}
46+
47+
func playStream() {
48+
// Determine the appropriate player command based on the OS
49+
var cmd *exec.Cmd
50+
switch runtime.GOOS {
51+
case "windows":
52+
cmd = exec.Command("ffplay", "-nodisp", "-autoexit", "-volume", fmt.Sprintf("%d", *volume), *url)
53+
case "darwin":
54+
cmd = exec.Command("ffplay", "-nodisp", "-autoexit", "-volume", fmt.Sprintf("%d", *volume), *url)
55+
case "linux":
56+
cmd = exec.Command("ffplay", "-nodisp", "-autoexit", "-volume", fmt.Sprintf("%d", *volume), *url)
57+
default:
58+
fmt.Printf("Unsupported operating system: %s\n", runtime.GOOS)
59+
os.Exit(1)
60+
}
61+
62+
// Set up pipes for communication
63+
stdout, err := cmd.StdoutPipe()
64+
if err != nil {
65+
fmt.Printf("Error setting up stdout pipe: %v\n", err)
66+
os.Exit(1)
67+
}
68+
69+
stderr, err := cmd.StderrPipe()
70+
if err != nil {
71+
fmt.Printf("Error setting up stderr pipe: %v\n", err)
72+
os.Exit(1)
73+
}
74+
75+
// Start the player
76+
if err := cmd.Start(); err != nil {
77+
fmt.Printf("Error starting player: %v\n", err)
78+
os.Exit(1)
79+
}
80+
81+
player = cmd
82+
83+
// Monitor the stream
84+
go monitorStream(stdout, stderr)
85+
86+
// Wait for the player to finish
87+
if err := cmd.Wait(); err != nil {
88+
fmt.Printf("Player finished with error: %v\n", err)
89+
}
90+
}
91+
92+
func monitorStream(stdout, stderr io.ReadCloser) {
93+
// Monitor stdout
94+
go func() {
95+
scanner := bufio.NewScanner(stdout)
96+
for scanner.Scan() {
97+
line := scanner.Text()
98+
fmt.Printf("Player output: %s\n", line)
99+
}
100+
}()
101+
102+
// Monitor stderr
103+
go func() {
104+
scanner := bufio.NewScanner(stderr)
105+
for scanner.Scan() {
106+
line := scanner.Text()
107+
fmt.Printf("Player error: %s\n", line)
108+
}
109+
}()
110+
111+
// Monitor the stream URL
112+
go func() {
113+
for {
114+
select {
115+
case <-stopChan:
116+
return
117+
default:
118+
resp, err := http.Get(*url)
119+
if err != nil {
120+
fmt.Printf("Error checking stream: %v\n", err)
121+
time.Sleep(5 * time.Second)
122+
continue
123+
}
124+
resp.Body.Close()
125+
time.Sleep(30 * time.Second)
126+
}
127+
}
128+
}()
129+
}
130+
131+
func stopStream() {
132+
if player != nil {
133+
close(stopChan)
134+
player.Process.Kill()
135+
player.Wait()
136+
}
137+
}
138+
139+
// Helper function to get the executable path
140+
func getExecutablePath() string {
141+
ex, err := os.Executable()
142+
if err != nil {
143+
return ""
144+
}
145+
return filepath.Dir(ex)
146+
}
147+
148+
// Helper function to check if a URL is valid
149+
func isValidURL(url string) bool {
150+
resp, err := http.Get(url)
151+
if err != nil {
152+
return false
153+
}
154+
defer resp.Body.Close()
155+
return resp.StatusCode == http.StatusOK
156+
}

StreamPlayer/streamplayer.exe

8.16 MB
Binary file not shown.

composeApp/compose-desktop.pro

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
-keep class kotlin.** { *; }
2+
-keep class kotlinx.coroutines.** { *; }
3+
-keep class org.jetbrains.skia.** { *; }
4+
-keep class org.jetbrains.skiko.** { *; }
5+
6+
-keep class org.koin.** { *; }
7+
-keep class MainKt { *; }
8+
-keep class main.presentation.** { *; }
9+
-keep class radio.** { *; }
10+
-keep class player.** { *; }
11+
12+
# Ktor rules
13+
-keep class io.ktor.** { *; }
14+
-keep class kotlinx.serialization.** { *; }
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
package di
2+
3+
import org.koin.dsl.module
4+
import radio.data.api.RadioBrowserApi
5+
import radio.data.repository.RadioImpl
6+
import radio.domain.repository.RadioRepository
7+
import radio.presentation.viewmodel.RadioViewModel
8+
9+
val appModule = module {
10+
single { RadioBrowserApi() }
11+
single<RadioRepository> { RadioImpl(get()) }
12+
single { RadioViewModel(get(), get()) }
13+
}
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
package main
2+
3+
import androidx.compose.ui.window.Window
4+
import androidx.compose.ui.window.application
5+
import di.appModule
6+
import org.koin.core.context.startKoin
7+
import org.koin.compose.koinInject
8+
import player.di.PlayerModule
9+
import main.presentation.App
10+
11+
fun main() = application {
12+
startKoin {
13+
modules(appModule, PlayerModule)
14+
}
15+
16+
Window(
17+
onCloseRequest = ::exitApplication,
18+
title = "Radio Browser"
19+
) {
20+
App()
21+
}
22+
}

0 commit comments

Comments
 (0)