Last active
July 5, 2019 22:29
-
-
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)
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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