Added radio metadata improvements, changed HTTP headers
All checks were successful
continuous-integration/drone/push Build is passing
All checks were successful
continuous-integration/drone/push Build is passing
This commit is contained in:
parent
a2856ad82a
commit
5eda50e5b7
|
@ -31,6 +31,7 @@ func main() {
|
||||||
Addr: fmt.Sprintf(":%d", config.Radio.Port),
|
Addr: fmt.Sprintf(":%d", config.Radio.Port),
|
||||||
Handler: http.HandlerFunc(queue.HandleRadioRequest),
|
Handler: http.HandlerFunc(queue.HandleRadioRequest),
|
||||||
}
|
}
|
||||||
|
server.SetKeepAlivesEnabled(false)
|
||||||
|
|
||||||
//setup a timeout to prevent slow clients blocking. See https://github.com/golang/go/issues/16100
|
//setup a timeout to prevent slow clients blocking. See https://github.com/golang/go/issues/16100
|
||||||
timeoutListener, err := newListener("tcp", server.Addr, time.Second*15, time.Second*30)
|
timeoutListener, err := newListener("tcp", server.Addr, time.Second*15, time.Second*30)
|
||||||
|
|
|
@ -17,8 +17,12 @@ type Config struct {
|
||||||
ReplayGain bool `toml:"replaygain"`
|
ReplayGain bool `toml:"replaygain"`
|
||||||
} `toml:"queue"`
|
} `toml:"queue"`
|
||||||
Radio struct {
|
Radio struct {
|
||||||
Port int `toml:"port"`
|
Port int `toml:"port"`
|
||||||
Name string `toml:"name"`
|
Name string `toml:"name"`
|
||||||
|
Description string `toml:"description"`
|
||||||
|
URL string `toml:"url"`
|
||||||
|
Logo string `toml:"logo"`
|
||||||
|
Private bool `toml:"private"`
|
||||||
} `toml:"radio"`
|
} `toml:"radio"`
|
||||||
Streams []struct {
|
Streams []struct {
|
||||||
MountPath string `toml:"mount"`
|
MountPath string `toml:"mount"`
|
||||||
|
|
|
@ -51,6 +51,14 @@ replaygain=false
|
||||||
port=8001
|
port=8001
|
||||||
# Name of the stream.
|
# Name of the stream.
|
||||||
name="my radio"
|
name="my radio"
|
||||||
|
# Description of the stream.
|
||||||
|
description=""
|
||||||
|
# URL of the stream.
|
||||||
|
url=""
|
||||||
|
# Logo URL of the radio
|
||||||
|
logo=""
|
||||||
|
# Whether to make the radio "private" via headers
|
||||||
|
private=false
|
||||||
|
|
||||||
#
|
#
|
||||||
# A list of streams to make available at [radio.port]/*(mount) follows. The
|
# A list of streams to make available at [radio.port]/*(mount) follows. The
|
||||||
|
|
17
http.go
17
http.go
|
@ -13,12 +13,13 @@ import (
|
||||||
const rangeReaderBufferSize = 4096
|
const rangeReaderBufferSize = 4096
|
||||||
|
|
||||||
type RangeReadSeekCloser struct {
|
type RangeReadSeekCloser struct {
|
||||||
uri *url.URL
|
uri *url.URL
|
||||||
size int64
|
size int64
|
||||||
i int64
|
i int64
|
||||||
body io.ReadCloser
|
body io.ReadCloser
|
||||||
buf []byte
|
buf []byte
|
||||||
ib int64
|
ib int64
|
||||||
|
closed bool
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewRangeReadSeekCloser(uri string) (*RangeReadSeekCloser, error) {
|
func NewRangeReadSeekCloser(uri string) (*RangeReadSeekCloser, error) {
|
||||||
|
@ -43,6 +44,9 @@ func (r *RangeReadSeekCloser) GetURI() string {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *RangeReadSeekCloser) retryConnect() error {
|
func (r *RangeReadSeekCloser) retryConnect() error {
|
||||||
|
if r.closed {
|
||||||
|
return errors.New("already closed")
|
||||||
|
}
|
||||||
|
|
||||||
headers := make(http.Header)
|
headers := make(http.Header)
|
||||||
startOffset := r.i
|
startOffset := r.i
|
||||||
|
@ -163,6 +167,7 @@ func (r *RangeReadSeekCloser) Seek(offset int64, whence int) (int64, error) {
|
||||||
|
|
||||||
func (r *RangeReadSeekCloser) Close() error {
|
func (r *RangeReadSeekCloser) Close() error {
|
||||||
if r.body != nil {
|
if r.body != nil {
|
||||||
|
r.closed = true
|
||||||
defer func() {
|
defer func() {
|
||||||
r.body = nil
|
r.body = nil
|
||||||
}()
|
}()
|
||||||
|
|
2
mount.go
2
mount.go
|
@ -35,6 +35,7 @@ type StreamMount struct {
|
||||||
Mount string
|
Mount string
|
||||||
MimeType string
|
MimeType string
|
||||||
Packetizer packetizer.Packetizer
|
Packetizer packetizer.Packetizer
|
||||||
|
Options map[string]interface{}
|
||||||
SampleRate int
|
SampleRate int
|
||||||
MetadataQueue *goconcurrentqueue.FIFO
|
MetadataQueue *goconcurrentqueue.FIFO
|
||||||
listeners []*StreamListener
|
listeners []*StreamListener
|
||||||
|
@ -152,6 +153,7 @@ func NewStreamMount(source audio.Source, mount string, codec string, container s
|
||||||
MimeType: mimeType,
|
MimeType: mimeType,
|
||||||
Packetizer: packetizerEntry,
|
Packetizer: packetizerEntry,
|
||||||
SampleRate: sampleRate,
|
SampleRate: sampleRate,
|
||||||
|
Options: options,
|
||||||
MetadataQueue: goconcurrentqueue.NewFIFO(),
|
MetadataQueue: goconcurrentqueue.NewFIFO(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
60
queue.go
60
queue.go
|
@ -383,10 +383,58 @@ func (q *Queue) HandleRadioRequest(writer http.ResponseWriter, request *http.Req
|
||||||
writer.Header().Set("Server", "MeteorLight/radio")
|
writer.Header().Set("Server", "MeteorLight/radio")
|
||||||
writer.Header().Set("Content-Type", mount.MimeType)
|
writer.Header().Set("Content-Type", mount.MimeType)
|
||||||
writer.Header().Set("Accept-Ranges", "none")
|
writer.Header().Set("Accept-Ranges", "none")
|
||||||
writer.Header().Set("Connection", "keep-alive")
|
writer.Header().Set("Connection", "close")
|
||||||
writer.Header().Set("X-Audiocast-Name", q.config.Radio.Name)
|
writer.Header().Set("Cache-Control", "no-store, max-age=604800")
|
||||||
writer.Header().Set("Cache-Control", "no-store, max-age=0, no-transform")
|
|
||||||
writer.Header().Set("X-Content-Type-Options", "nosniff")
|
writer.Header().Set("X-Content-Type-Options", "nosniff")
|
||||||
|
writer.Header().Set("Access-Control-Allow-Origin", "*")
|
||||||
|
writer.Header().Set("Access-Control-Allow-Headers", "Accept, Content-Type, Icy-Metadata")
|
||||||
|
writer.Header().Set("Access-Control-Expose-Headers", "Accept-Ranges, Server, Content-Type, Icy-MetaInt")
|
||||||
|
writer.Header().Set("Vary", "*")
|
||||||
|
|
||||||
|
rangeHeader := request.Header.Get("range")
|
||||||
|
if rangeHeader != "" && rangeHeader != "bytes=0-" {
|
||||||
|
//TODO: maybe should fail in case bytes are requested
|
||||||
|
}
|
||||||
|
|
||||||
|
bitrate := 0
|
||||||
|
if value, ok := mount.Options["bitrate"]; ok {
|
||||||
|
if intValue, ok := value.(int); ok {
|
||||||
|
bitrate = intValue
|
||||||
|
} else if int64Value, ok := value.(int64); ok {
|
||||||
|
bitrate = int(int64Value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//set some audiocast/icy radio headers
|
||||||
|
writer.Header().Set("x-audiocast-name", q.config.Radio.Name)
|
||||||
|
writer.Header().Set("x-audiocast-bitrate", fmt.Sprintf("%d", bitrate))
|
||||||
|
writer.Header().Set("icy-name", q.config.Radio.Name)
|
||||||
|
writer.Header().Set("icy-version", "2")
|
||||||
|
writer.Header().Set("icy-index-metadata", "1")
|
||||||
|
if q.config.Radio.Description != "" {
|
||||||
|
writer.Header().Set("x-audiocast-description", q.config.Radio.Description)
|
||||||
|
writer.Header().Set("icy-description", q.config.Radio.Description)
|
||||||
|
}
|
||||||
|
if q.config.Radio.URL != "" {
|
||||||
|
writer.Header().Set("x-audiocast-url", q.config.Radio.URL)
|
||||||
|
writer.Header().Set("icy-url", q.config.Radio.URL)
|
||||||
|
}
|
||||||
|
if q.config.Radio.Logo != "" {
|
||||||
|
writer.Header().Set("icy-logo", q.config.Radio.Logo)
|
||||||
|
}
|
||||||
|
writer.Header().Set("icy-br", fmt.Sprintf("%d", bitrate))
|
||||||
|
writer.Header().Set("icy-sr", fmt.Sprintf("%d", mount.SampleRate))
|
||||||
|
writer.Header().Set("icy-audio-info", fmt.Sprintf("ice-channels=%d;ice-samplerate=%d;ice-bitrate=%d", 2, mount.SampleRate, bitrate))
|
||||||
|
if q.config.Radio.Private {
|
||||||
|
writer.Header().Set("icy-pub", "0")
|
||||||
|
writer.Header().Set("icy-do-not-index", "1")
|
||||||
|
writer.Header().Set("x-audiocast-public", "0")
|
||||||
|
writer.Header().Set("x-robots-tag", "noindex, nofollow")
|
||||||
|
} else {
|
||||||
|
writer.Header().Set("icy-pub", "1")
|
||||||
|
writer.Header().Set("icy-do-not-index", "0")
|
||||||
|
writer.Header().Set("x-audiocast-public", "1")
|
||||||
|
}
|
||||||
|
|
||||||
var packetWriteCallback func(packet packetizer.Packet) error
|
var packetWriteCallback func(packet packetizer.Packet) error
|
||||||
|
|
||||||
|
@ -398,12 +446,12 @@ func (q *Queue) HandleRadioRequest(writer http.ResponseWriter, request *http.Req
|
||||||
|
|
||||||
if numberValue, err := strconv.Atoi(request.Header.Get("icy-metadata")); err == nil && numberValue >= 1 {
|
if numberValue, err := strconv.Atoi(request.Header.Get("icy-metadata")); err == nil && numberValue >= 1 {
|
||||||
metadataToSend := make(map[string]string)
|
metadataToSend := make(map[string]string)
|
||||||
const icyInterval = 8192
|
const icyInterval = 8192 //weird clients might not support other numbers than this
|
||||||
icyCounter := 0
|
icyCounter := 0
|
||||||
writer.Header().Set("Icy-MetaInt", fmt.Sprintf("%d", icyInterval))
|
writer.Header().Set("icy-metaint", fmt.Sprintf("%d", icyInterval))
|
||||||
|
|
||||||
writeIcy := func() []byte {
|
writeIcy := func() []byte {
|
||||||
packetContent := make([]byte, 1, 4096)
|
packetContent := make([]byte, 1, 16*255+1)
|
||||||
for k, v := range metadataToSend {
|
for k, v := range metadataToSend {
|
||||||
packetContent = append(packetContent, []byte(fmt.Sprintf("%s='%s';", k, v))...)
|
packetContent = append(packetContent, []byte(fmt.Sprintf("%s='%s';", k, v))...)
|
||||||
delete(metadataToSend, k)
|
delete(metadataToSend, k)
|
||||||
|
|
Loading…
Reference in a new issue