diff options
author | xegineering <me@xegineering.eu> | 2024-11-08 20:56:56 +0100 |
---|---|---|
committer | xegineering <me@xegineering.eu> | 2024-11-10 15:53:44 +0100 |
commit | 36a2fda37e837bc39c88a2fa38678b0f3bded041 (patch) | |
tree | 892a68802b2df5ab474417964d1512027a5eb9df | |
parent | e30a1b0768d35ec70e5d2c00e6ae87c0f267b881 (diff) | |
download | soundbox-go-36a2fda37e837bc39c88a2fa38678b0f3bded041.tar soundbox-go-36a2fda37e837bc39c88a2fa38678b0f3bded041.tar.zst soundbox-go-36a2fda37e837bc39c88a2fa38678b0f3bded041.zip |
Use Go code for output network stream
Calling the external program `ffmpeg` should be avoided completely in
the future to make soundbox-go a pure Go code base. `ffmpeg` provides
the following functionality to soundbox-go:
- web radio input stream transport
- re-encoding of the audio stream
- output stream transport to soundbox devices
The last part should be replaced with this commit as a first step.
-rw-r--r-- | soundbox/interfaces.go | 2 | ||||
-rw-r--r-- | soundbox/stream.go | 84 |
2 files changed, 69 insertions, 17 deletions
diff --git a/soundbox/interfaces.go b/soundbox/interfaces.go index 2854b1a..04839c7 100644 --- a/soundbox/interfaces.go +++ b/soundbox/interfaces.go @@ -17,7 +17,7 @@ func getInterface() (net.Interface, error) { } for _, iface := range all { - if iface.Flags & net.FlagUp == 0 { + if iface.Flags&net.FlagUp == 0 { continue } addresses, err := iface.Addrs() diff --git a/soundbox/stream.go b/soundbox/stream.go index 4b08840..0d6fe5f 100644 --- a/soundbox/stream.go +++ b/soundbox/stream.go @@ -2,15 +2,22 @@ package soundbox import ( "context" + "errors" "fmt" - "os/exec" + "io" "net" + "os/exec" + "time" ) // streamingPort is the default network port a soundbox is listening to for // incoming audio stream data. const streamingPort = 5316 +const bufferSize = 20 + +const writeTimeout = 1 * time.Second + // StreamURLContext streams audio from a given URL to one or multiple soundbox // devices. The devices are referenced via their MAC addresses given by the // targets argument. The ctx argument is passed to cancel the streaming. @@ -20,29 +27,74 @@ func StreamURLContext(ctx context.Context, url string, targets []net.HardwareAdd return err } + ips := make([]net.IP, 0) + for _, target := range targets { + ip, err := toLinkLocal(target) + if err != nil { + return err + } + ips = append(ips, ip) + } + + conns := make([]net.Conn, 0) + for _, ip := range ips { + var d net.Dialer + conn, err := d.DialContext( + ctx, + "tcp6", + fmt.Sprintf("[%s%%%s]:%d", ip, iface.Name, streamingPort), + ) + if err != nil { + return err + } + conns = append(conns, conn) + } + defer func() { + for _, conn := range conns { + conn.Close() + } + }() + args := []string{ "-re", "-i", url, + "-acodec", + "flac", + "-f", + "ogg", + "-", } - for _, target := range targets { - ip, err := toLinkLocal(target) + cmd := exec.CommandContext(ctx, "ffmpeg", args...) + stdout, err := cmd.StdoutPipe() + if err != nil { + return err + } + + err = cmd.Start() + if err != nil { + return err + } + + for { + buffer := make([]byte, bufferSize) + i, err := stdout.Read(buffer) if err != nil { - return err + if errors.Is(err, io.EOF) { + break + } else { + return err + } + } + for _, conn := range conns { + conn.SetDeadline(time.Now().Add(writeTimeout)) + _, err = conn.Write(buffer[:i]) + if err != nil { + return err + } } - - args = append(args, "-acodec") - args = append(args, "flac") - args = append(args, "-f") - args = append(args, "ogg") - args = append(args, fmt.Sprintf( - "tcp://[%s%%%s]:%d", - ip, - iface.Name, - streamingPort, - )) } - return exec.CommandContext(ctx, "ffmpeg", args...).Run() + return cmd.Wait() } |