package soundbox import ( "context" "errors" "fmt" "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. func StreamURLContext(ctx context.Context, url string, targets []net.HardwareAddr) error { iface, err := getInterface() if err != nil { 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() } }() cmd := exec.CommandContext( ctx, "ffmpeg", "-re", "-i", url, "-acodec", "flac", "-f", "ogg", "-", ) 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 { 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 } } } return cmd.Wait() }