Skip to content

Instantly share code, notes, and snippets.

@ugjka
Last active July 5, 2019 22:29
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save ugjka/737da9f4d9c4695bb289dc77f0492ab4 to your computer and use it in GitHub Desktop.
Save ugjka/737da9f4d9c4695bb289dc77f0492ab4 to your computer and use it in GitHub Desktop.
I little script to play my favourite radio stations without buffer latency (uses MPV for playback)
package main
import (
"flag"
"fmt"
"io"
"log"
"net"
"net/http"
"os"
"os/exec"
"time"
)
func main() {
noskip := flag.Bool("noskip", false, "do not skip buffer")
flag.Usage = func() {
fmt.Println("help:")
fmt.Println("\tavailable stations")
fmt.Println("\t\t1, 2, 5, swh, heart")
fmt.Println("\t-noskip")
fmt.Println("\t\tdisables buffer skiping")
os.Exit(0)
}
flag.Parse()
stations := map[string]struct {
stream string
buffer int
bitrate int
codec string
}{
//Latvijas Radio 1,2,5
"1": {"http://lr1mp1.latvijasradio.lv:8010/", 15, 128, "aac"},
"2": {"http://lr2mp1.latvijasradio.lv:8000/", 15, 128, "aac"},
"5": {"http://live.pieci.lv/live19-hq.mp3", 3, 128, "mp3"},
//Radio SWH
"swh": {"http://87.110.219.34:8000/swhaac", 3, 128, "aac"},
//Heart London
"heart": {"http://media-ice.musicradio.com/HeartLondonMP3", 12, 128, "mp3"},
}
args := flag.Args()
// If no args, play LR2
station := stations["2"]
if len(args) > 0 {
if v, ok := stations[args[0]]; ok {
station = v
}
}
//Custom transport for IceCast protocol
tr := &http.Transport{
Dial: func(network, a string) (net.Conn, error) {
realConn, err := net.Dial(network, a)
if err != nil {
return nil, err
}
hijack := &IcyConnWrapper{
deadline: time.Second * 5,
Conn: realConn,
}
if *noskip {
hijack.deadline = time.Second * 15
}
return hijack, nil
},
}
client := &http.Client{
Transport: tr,
Timeout: time.Hour * 6,
}
http.DefaultClient = client
for {
stream, err := http.Get(station.stream)
if err != nil {
log.Println("GET:", err)
time.Sleep(time.Second * 5)
continue
}
pipe, err := getPipe(station.codec)
if err != nil {
log.Println("GETPIPE:", err)
time.Sleep(time.Second * 5)
continue
}
//Skip the buffer for more realtime playback
if !*noskip {
err := skipBytes(stream.Body, station.bitrate/8*1024*station.buffer)
if err != nil {
log.Println("SKIP BYTES:", err)
stream.Body.Close()
pipe.Close()
time.Sleep(time.Second * 5)
continue
}
}
//Fetch only half a second at a time
buf := make([]byte, station.bitrate/8*1024/2)
_, err = io.CopyBuffer(pipe, stream.Body, buf)
if err != nil {
log.Println("COPY BUFFER:", err)
stream.Body.Close()
pipe.Close()
time.Sleep(time.Second * 5)
}
}
}
//IcyConnWrapper makes IceCast to work with net/http package
type IcyConnWrapper struct {
net.Conn
haveReadAny bool
deadline time.Duration
}
func (i *IcyConnWrapper) Read(b []byte) (int, error) {
i.Conn.SetReadDeadline(time.Now().Add(i.deadline))
if i.haveReadAny {
return i.Conn.Read(b)
}
i.haveReadAny = true
//bounds checking ommitted. There are a few ways this can go wrong.
//always check array sizes and returned n.
n, err := i.Conn.Read(b[:3])
if err != nil {
return n, err
}
if string(b[:3]) == "ICY" {
//write Correct http response into buffer
copy(b, []byte("HTTP/1.1"))
return 8, nil
}
return n, nil
}
func skipBytes(i io.Reader, target int) error {
buf := make([]byte, 128)
total := 0
for {
n, err := i.Read(buf)
if err != nil {
return err
}
total += n
if total > target {
return nil
}
}
}
func getPipe(codec string) (pipe io.WriteCloser, err error) {
cmd := exec.Command("mpv", "-", "--profile=low-latency", "--quiet",
"--cache=no", "--demuxer-lavf-buffersize=512",
"--demuxer-lavf-format="+codec, "--demuxer=+lavf")
pipe, err = cmd.StdinPipe()
if err != nil {
return
}
statusOut, err := cmd.StdoutPipe()
if err != nil {
return
}
statusErr, err := cmd.StderrPipe()
if err != nil {
return
}
go func(src io.ReadCloser) {
io.Copy(os.Stdout, src)
}(statusOut)
go func(src io.ReadCloser) {
io.Copy(os.Stderr, src)
}(statusErr)
err = cmd.Start()
if err != nil {
return
}
go func(c *exec.Cmd) {
c.Wait()
log.Println("REALEASING MPV")
}(cmd)
return
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment