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 }