diff options
author | Přemysl Janouch <p.janouch@gmail.com> | 2016-10-25 00:21:26 +0200 |
---|---|---|
committer | Přemysl Janouch <p@janouch.name> | 2018-10-11 16:30:37 +0200 |
commit | a50f35aac306bbbb164878e3530b8f1669c676de (patch) | |
tree | 6891e26c8073a3bd9c0ca1a8735f7671e124376a /bbc-on-ice.go | |
parent | 92dbdedf0c774edf7a1e544622ea7d542d6111f9 (diff) | |
download | bbc-on-ice-a50f35aac306bbbb164878e3530b8f1669c676de.tar.gz bbc-on-ice-a50f35aac306bbbb164878e3530b8f1669c676de.tar.xz bbc-on-ice-a50f35aac306bbbb164878e3530b8f1669c676de.zip |
Little improvements
Diffstat (limited to 'bbc-on-ice.go')
-rw-r--r-- | bbc-on-ice.go | 257 |
1 files changed, 0 insertions, 257 deletions
diff --git a/bbc-on-ice.go b/bbc-on-ice.go deleted file mode 100644 index 57f5539..0000000 --- a/bbc-on-ice.go +++ /dev/null @@ -1,257 +0,0 @@ -package main - -import ( - "context" - "encoding/json" - "errors" - "fmt" - "io" - "io/ioutil" - "log" - "net/http" - "os" - "path" - "regexp" - "strings" - "time" - "unicode/utf8" -) - -type meta struct { - title string // What's playing right now - timeout uint // Timeout for the next poll in ms -} - -// Retrieve and decode metadata information from an independent webservice -func getMeta(name string) (*meta, error) { - const metaBaseURI = "http://polling.bbc.co.uk/radio/nhppolling/" - resp, err := http.Get(metaBaseURI + name) - if err != nil { - return nil, err - } - b, err := ioutil.ReadAll(resp.Body) - resp.Body.Close() - if len(b) < 2 { - // There needs to be an enclosing () pair - return nil, errors.New("invalid metadata response") - } - - // TODO: also retrieve richtracks/is_now_playing, see example file - type broadcast struct { - Title string `json:"title"` // Title of the broadcast - Percentage int `json:"percentage"` // How far we're in - } - var v struct { - Packages struct { - OnAir struct { - Broadcasts []broadcast `json:"broadcasts"` - BroadcastNowIndex uint `json:"broadcastNowIndex"` - } `json:"on-air"` - } `json:"packages"` - Timeouts struct { - PollingTimeout uint `json:"polling_timeout"` - } `json:"timeouts"` - } - err = json.Unmarshal(b[1:len(b)-1], &v) - if err != nil { - return nil, errors.New("invalid metadata response") - } - onAir := v.Packages.OnAir - if onAir.BroadcastNowIndex >= uint(len(onAir.Broadcasts)) { - return nil, errors.New("no active broadcast") - } - return &meta{ - timeout: v.Timeouts.PollingTimeout, - title: onAir.Broadcasts[onAir.BroadcastNowIndex].Title, - }, nil -} - -// Resolve an M3U8 playlist to the first link that seems to be playable -func resolveM3U8(target string) (out []string, err error) { - resp, err := http.Get(target) - if err != nil { - return nil, err - } - b, err := ioutil.ReadAll(resp.Body) - resp.Body.Close() - if !utf8.Valid(b) { - return nil, errors.New("invalid UTF-8") - } - lines := strings.Split(string(b), "\n") - for _, line := range lines { - if strings.HasPrefix(line, "#") { - continue - } - if !strings.Contains(line, "/") { - // Seems to be a relative link, let's make it absolute - dir, _ := path.Split(target) - line = dir + line - } - if strings.HasSuffix(line, "m3u8") { - // The playlist seems to recurse, and so do we - return resolveM3U8(line) - } - out = append(out, line) - } - return out, nil -} - -func metaProc(ctx context.Context, name string, out chan<- string) { - var current, last string - var interval time.Duration - for { - meta, err := getMeta(name) - if err != nil { - current = "Error: " + err.Error() - interval = 30 * time.Second - } else { - current = meta.title - interval = time.Duration(meta.timeout) - } - // TODO: select on the send? Can't close it in the main proc. - // Also see https://blog.golang.org/pipelines - if current != last { - out <- current - last = current - } - - select { - case <-time.After(time.Duration(interval) * time.Millisecond): - case <-ctx.Done(): - return - } - } -} - -var pathRE = regexp.MustCompile(`^/(.*?)/(.*?)/(.*?)$`) - -func proxy(w http.ResponseWriter, req *http.Request) { - const targetURI = "http://a.files.bbci.co.uk/media/live/manifesto/" + - "audio/simulcast/hls/%s/%s/ak/%s.m3u8" - const metaint = 1 << 16 - m := pathRE.FindStringSubmatch(req.URL.Path) - if m == nil { - http.NotFound(w, req) - return - } - hijacker, ok := w.(http.Hijacker) - if !ok { - // We're not using TLS where HTTP/2 could have caused this - panic("cannot hijack connection") - } - - // E.g. `nonuk`, `sbr_low` `bbc_radio_one`, or `uk`, `sbr_high`, `bbc_1xtra` - region, quality, name := m[1], m[2], m[3] - // This validates the params as a side-effect - target, err := resolveM3U8(fmt.Sprintf(targetURI, region, quality, name)) - if err == nil && len(target) == 0 { - err = errors.New("cannot resolve playlist") - } - if err != nil { - http.Error(w, err.Error(), http.StatusInternalServerError) - return - } - - wantMeta := false - if icyMeta, ok := req.Header["Icy-MetaData"]; ok { - wantMeta = len(icyMeta) == 1 && icyMeta[0] == "1" - } - resp, err := http.Get(target[0]) - if err != nil { - http.Error(w, err.Error(), http.StatusInternalServerError) - return - } - - conn, bufrw, err := hijacker.Hijack() - if err != nil { - http.Error(w, err.Error(), http.StatusInternalServerError) - return - } - defer conn.Close() - - // TODO: retrieve some general information from somewhere? - // There's nothing interesting in the playlist files. - fmt.Fprintf(bufrw, "ICY 200 OK\r\n") - fmt.Fprintf(bufrw, "icy-name:%s\r\n", name) - // BBC marks this as a video type, maybe just force audio/mpeg - fmt.Fprintf(bufrw, "content-type:%s\r\n", resp.Header["Content-Type"][0]) - fmt.Fprintf(bufrw, "icy-pub:%d\r\n", 0) - if wantMeta { - fmt.Fprintf(bufrw, "icy-metaint: %d\r\n", metaint) - } - fmt.Fprintf(bufrw, "\r\n") - - metaChan := make(chan string) - go metaProc(req.Context(), name, metaChan) - - // TODO: move to a normal function - // FIXME: this will load a few seconds (one URL) and die - // - we can either try to implement this and hope for the best - // https://tools.ietf.org/html/draft-pantos-http-live-streaming-20 - // then like https://github.com/kz26/gohls/blob/master/main.go - // - or we can become more of a proxy, which complicates ICY - chunkChan := make(chan []byte) - go func() { - defer resp.Body.Close() - defer close(chunkChan) - for { - chunk := make([]byte, metaint) - n, err := io.ReadFull(resp.Body, chunk) - chunkChan <- chunk[:n] - if err != nil { - return - } - - select { - default: - case <-req.Context().Done(): - return - } - } - }() - - var queuedMeta []byte - makeMetaChunk := func() []byte { - meta := queuedMeta - queuedMeta = nil - for len(meta)%16 != 0 { - meta = append(meta, 0) - } - if len(meta) > 16*255 { - meta = meta[:16*255] - } - chunk := []byte{byte(len(meta) / 16)} - return append(chunk, meta...) - } - - for { - select { - case title := <-metaChan: - queuedMeta = []byte(fmt.Sprintf("StreamTitle='%s'", title)) - case chunk, connected := <-chunkChan: - if !connected { - return - } - if wantMeta { - chunk = append(chunk, makeMetaChunk()...) - } - if _, err := bufrw.Write(chunk); err != nil { - return - } - if err := bufrw.Flush(); err != nil { - return - } - } - } -} - -func main() { - // TODO: also try to support systemd socket activation - address := ":8000" - if len(os.Args) == 2 { - address = os.Args[1] - } - - http.HandleFunc("/", proxy) - log.Fatal(http.ListenAndServe(address, nil)) -} |