diff --git a/Makefile b/Makefile index b4a4dcf..018b82b 100644 --- a/Makefile +++ b/Makefile @@ -7,8 +7,8 @@ GO_OPTIONS=-buildmode=default all: radiospiral -radiospiral: main.go bundle.go - $(GO) -o radiospiral $(GO_OPTIONS) main.go bundle.go +radiospiral: main.go radioplayer.go bundle.go + $(GO) -o radiospiral $(GO_OPTIONS) main.go radioplayer.go bundle.go # It's a phony so we can always call it and regenerate the file .PHONY: generate diff --git a/main.go b/main.go index 54a5321..3ba6a6b 100644 --- a/main.go +++ b/main.go @@ -44,13 +44,11 @@ import ( "net/http" "net/url" "os" - "os/exec" "path/filepath" "runtime" "strings" "time" - "github.com/ebitengine/oto/v3" "github.com/muesli/cancelreader" "fyne.io/fyne/v2" @@ -69,7 +67,6 @@ const RADIOSPIRAL_JSON_ENDPOINT = "https://radiospiral.net/wp-json/radio/broadca const ( Loading int = iota Playing - Paused Stopped ) @@ -122,165 +119,6 @@ type HostsData struct { Name string `json:"name"` } -// Radio player interface -type RadioPlayer interface { - Load(stream_url string) - IsPlaying() bool - Play() - Mute() - Pause() - IncVolume() - DecVolume() - Close() -} - -// StreamPlayer -type StreamPlayer struct { - player_name string - stream_url string - command *exec.Cmd - in io.WriteCloser - out io.ReadCloser - audio io.ReadCloser - pipe_chan chan io.ReadCloser - otoContext *oto.Context - otoPlayer *oto.Player - currentVolume float64 - paused bool -} - -func (player *StreamPlayer) IsPlaying() bool { - if player.otoPlayer == nil { - log.Println("Player not loaded!") - return false - } - - return player.otoPlayer.IsPlaying() -} - -func (player *StreamPlayer) Load(stream_url string) { - if (player.otoPlayer == nil) || (!player.otoPlayer.IsPlaying()) { - var err error - is_playlist := strings.HasSuffix(stream_url, ".m3u") || strings.HasSuffix(stream_url, ".pls") - if is_playlist { - // TODO: Check ffmpeg's ability to deal with playlists - // player.command = exec.Command(player.player_name, "-quiet", "-playlist", stream_url) - player.command = exec.Command(player.player_name, "-nodisp", "-loglevel", "verbose", "-playlist", stream_url) - } else { - player.command = exec.Command(player.player_name, "-loglevel", "verbose", "-i", stream_url, "-f", "wav", "-") - } - - // In to send things over stdin to ffmpeg - player.in, err = player.command.StdinPipe() - check(err) - // Out will be the wave data we will read and play - player.audio, err = player.command.StdoutPipe() - check(err) - // Err is the output of ffmpeg, used to get stream title - player.out, err = player.command.StderrPipe() - check(err) - - log.Println("Starting ffmpeg") - err = player.command.Start() - check(err) - - player.stream_url = stream_url - - op := &oto.NewContextOptions{ - SampleRate: 44100, - ChannelCount: 2, - Format: oto.FormatSignedInt16LE, - } - - if player.otoContext == nil { - otoContext, readyChan, err := oto.NewContext(op) - player.otoContext = otoContext - if err != nil { - log.Fatal(err) - } - <-readyChan - } - - player.otoPlayer = player.otoContext.NewPlayer(player.audio) - // Save current volume for the mute function - player.currentVolume = player.otoPlayer.Volume() - - player.paused = false - - go func() { - player.pipe_chan <- player.out - }() - } -} - -func (player *StreamPlayer) Play() { - if player.otoPlayer == nil { - log.Println("Stream not loaded") - return - } - - if !player.otoPlayer.IsPlaying() { - if player.command == nil { - player.Load(player.stream_url) - } - player.otoPlayer.Play() - } -} - -func (player *StreamPlayer) Close() { - if player.IsPlaying() { - err := player.otoPlayer.Close() - if err != nil { - log.Println(err) - } - player.in.Close() - player.out.Close() - player.audio.Close() - - player.stream_url = "" - } -} - -func (player *StreamPlayer) Mute() { - if player.IsPlaying() { - if player.otoPlayer.Volume() > 0 { - player.currentVolume = player.otoPlayer.Volume() - player.otoPlayer.SetVolume(0.0) - } else { - player.otoPlayer.SetVolume(player.currentVolume) - } - } -} - -func (player *StreamPlayer) Pause() { - if player.IsPlaying() { - if !player.paused { - player.paused = true - player.otoPlayer.Pause() - } - } -} - -func (player *StreamPlayer) IncVolume() { - if player.IsPlaying() { - player.currentVolume += 0.05 - if player.currentVolume >= 1.0 { - player.currentVolume = 1.0 - } - player.otoPlayer.SetVolume(player.currentVolume) - } -} - -func (player *StreamPlayer) DecVolume() { - if player.IsPlaying() { - player.currentVolume -= 0.05 - if player.currentVolume <= 0.0 { - player.currentVolume = 0.0 - } - player.otoPlayer.SetVolume(player.currentVolume) - } -} - // Load images from URLs func loadImageURL(url string) image.Image { parts := strings.Split(url, "?") @@ -380,21 +218,21 @@ func main() { // appearance anytime it is clicked. We make the player start playing // or pause. if !streamPlayer.IsPlaying() && !streamPlayer.paused { - playButton.SetIcon(theme.MediaPauseIcon()) + playButton.SetIcon(theme.MediaStopIcon()) playButton.SetText("(Buffering)") streamPlayer.Load(RADIOSPIRAL_STREAM) streamPlayer.Play() playStatus = Loading } else { if playStatus == Playing { - playStatus = Paused + playStatus = Stopped playButton.SetIcon(theme.MediaPlayIcon()) streamPlayer.Pause() } else { reader.Cancel() playStatus = Loading playButton.SetText("(Buffering)") - playButton.SetIcon(theme.MediaPauseIcon()) + playButton.SetIcon(theme.MediaStopIcon()) streamPlayer.Load(RADIOSPIRAL_STREAM) streamPlayer.Play() } diff --git a/radioplayer.go b/radioplayer.go new file mode 100644 index 0000000..5634f71 --- /dev/null +++ b/radioplayer.go @@ -0,0 +1,169 @@ +package main + +import ( + "io" + "log" + "os/exec" + "strings" + + "github.com/ebitengine/oto/v3" +) + +// Radio player interface +type RadioPlayer interface { + Load(stream_url string) + IsPlaying() bool + Play() + Mute() + Pause() + IncVolume() + DecVolume() + Close() +} + +// StreamPlayer +type StreamPlayer struct { + player_name string + stream_url string + command *exec.Cmd + in io.WriteCloser + out io.ReadCloser + audio io.ReadCloser + pipe_chan chan io.ReadCloser + otoContext *oto.Context + otoPlayer *oto.Player + currentVolume float64 + paused bool +} + +func (player *StreamPlayer) IsPlaying() bool { + if player.otoPlayer == nil { + log.Println("Player not loaded!") + return false + } + + return player.otoPlayer.IsPlaying() +} + +func (player *StreamPlayer) Load(stream_url string) { + if (player.otoPlayer == nil) || (!player.otoPlayer.IsPlaying()) { + var err error + is_playlist := strings.HasSuffix(stream_url, ".m3u") || strings.HasSuffix(stream_url, ".pls") + if is_playlist { + // TODO: Check ffmpeg's ability to deal with playlists + // player.command = exec.Command(player.player_name, "-quiet", "-playlist", stream_url) + player.command = exec.Command(player.player_name, "-nodisp", "-loglevel", "verbose", "-playlist", stream_url) + } else { + player.command = exec.Command(player.player_name, "-loglevel", "verbose", "-i", stream_url, "-f", "wav", "-") + } + + // In to send things over stdin to ffmpeg + player.in, err = player.command.StdinPipe() + check(err) + // Out will be the wave data we will read and play + player.audio, err = player.command.StdoutPipe() + check(err) + // Err is the output of ffmpeg, used to get stream title + player.out, err = player.command.StderrPipe() + check(err) + + log.Println("Starting ffmpeg") + err = player.command.Start() + check(err) + + player.stream_url = stream_url + + op := &oto.NewContextOptions{ + SampleRate: 44100, + ChannelCount: 2, + Format: oto.FormatSignedInt16LE, + } + + if player.otoContext == nil { + otoContext, readyChan, err := oto.NewContext(op) + player.otoContext = otoContext + if err != nil { + log.Fatal(err) + } + <-readyChan + } + + player.otoPlayer = player.otoContext.NewPlayer(player.audio) + // Save current volume for the mute function + player.currentVolume = player.otoPlayer.Volume() + + player.paused = false + + go func() { + player.pipe_chan <- player.out + }() + } +} + +func (player *StreamPlayer) Play() { + if player.otoPlayer == nil { + log.Println("Stream not loaded") + return + } + + if !player.otoPlayer.IsPlaying() { + if player.command == nil { + player.Load(player.stream_url) + } + player.otoPlayer.Play() + } +} + +func (player *StreamPlayer) Close() { + if player.IsPlaying() { + err := player.otoPlayer.Close() + if err != nil { + log.Println(err) + } + player.in.Close() + player.out.Close() + player.audio.Close() + + player.stream_url = "" + } +} + +func (player *StreamPlayer) Mute() { + if player.IsPlaying() { + if player.otoPlayer.Volume() > 0 { + player.currentVolume = player.otoPlayer.Volume() + player.otoPlayer.SetVolume(0.0) + } else { + player.otoPlayer.SetVolume(player.currentVolume) + } + } +} + +func (player *StreamPlayer) Pause() { + if player.IsPlaying() { + if !player.paused { + player.paused = true + player.otoPlayer.Pause() + } + } +} + +func (player *StreamPlayer) IncVolume() { + if player.IsPlaying() { + player.currentVolume += 0.05 + if player.currentVolume >= 1.0 { + player.currentVolume = 1.0 + } + player.otoPlayer.SetVolume(player.currentVolume) + } +} + +func (player *StreamPlayer) DecVolume() { + if player.IsPlaying() { + player.currentVolume -= 0.05 + if player.currentVolume <= 0.0 { + player.currentVolume = 0.0 + } + player.otoPlayer.SetVolume(player.currentVolume) + } +}