Implement ICY metadata
All checks were successful
continuous-integration/drone/push Build is passing

This commit is contained in:
DataHoarder 2022-03-04 13:25:59 +01:00
parent 7f8274f431
commit 4f354bd813
6 changed files with 224 additions and 81 deletions

View file

@ -2,20 +2,16 @@
Radio streamer ([kawa](https://github.com/Luminarys/kawa) drop-in compatible). Radio streamer ([kawa](https://github.com/Luminarys/kawa) drop-in compatible).
This project is a Work in Progress.
`TODO: ICY metadata`
# Improvements / differences from Kawa # Improvements / differences from Kawa
* Does not use libav ([see supported formats/codecs on Kirika](https://git.gammaspectra.live/S.O.N.G/Kirika#codecs-supported)) * Does not use libav ([see supported formats/codecs on Kirika](https://git.gammaspectra.live/S.O.N.G/Kirika#codecs-supported))
* No Vorbis support. * No Vorbis support.
* Supports HTTP clients that have more than 16 HTTP request headers. * Supports HTTP clients that have more than 16 HTTP request headers or longer than 64 bytes per header.
* Does not restart stream per-track, instead being a continuous stream without parameter changes. * Does not restart stream per-track, instead being a continuous stream without parameter changes.
* Normalized channels / sample rates for mounts. * Normalized channels / sample rates for mounts.
* Implements ICY metadata * Implements ICY metadata (artist, title, url).
* Uses sample/timed packet buffers, instead of kawa byte buffers, which caused wild differences between endpoints. Mounts usually align within 0.2s of each other, depending on client. * Uses sample/timed packet buffers, instead of kawa byte buffers, which caused wild differences between endpoints. Mounts usually align within 0.2s of each other, depending on client.
* Use `queue.buffer_size` to specify number of seconds to buffer * Use `queue.buffer_size` to specify number of seconds to buffer.
* Implements `queue.nr` and `/random` (To be Deprecated/Changed) * Implements `queue.nr` and `/random` (to be deprecated/changed)
# Future improvements # Future improvements
* Allow playback of files by URL, not just by path * Allow playback of files by URL, not just by path

View file

@ -36,7 +36,7 @@ fallback="/tmp/in.flac"
# buffer_len=4096 # buffer_len=4096
# Duration in seconds of buffer to maintain. Set 0 for automatic mode depending on requesting client. # Duration in seconds of buffer to maintain. Set 0 for automatic mode depending on requesting client.
# Maximum 10 seconds. # Maximum 10 seconds.
# Do note buffer is counted from start of frame, not end, for removal purposes. This depends on format and can be a second or so at times. # Do note buffer is counted from end of frame, not start, for removal purposes. This depends on format and can be a second or so at times.
buffer_duration=0 buffer_duration=0
[radio] [radio]

3
go.mod
View file

@ -3,8 +3,9 @@ module git.gammaspectra.live/S.O.N.G/MeteorLight
go 1.18 go 1.18
require ( require (
git.gammaspectra.live/S.O.N.G/Kirika v0.0.0-20220303134137-d0976eac62c4 git.gammaspectra.live/S.O.N.G/Kirika v0.0.0-20220304112513-f1e808b8f144
github.com/BurntSushi/toml v1.0.0 github.com/BurntSushi/toml v1.0.0
github.com/enriquebris/goconcurrentqueue v0.6.3
) )
require ( require (

10
go.sum
View file

@ -1,5 +1,5 @@
git.gammaspectra.live/S.O.N.G/Kirika v0.0.0-20220303134137-d0976eac62c4 h1:2o+FJxxoN4pv2h/ZnhY7p0Cnr79tVDocD3UycH3Dmmk= git.gammaspectra.live/S.O.N.G/Kirika v0.0.0-20220304112513-f1e808b8f144 h1:2bJiVqiDQw36CKp6taE9MIl9tJ0j0E7Hp9n/QNgTdLo=
git.gammaspectra.live/S.O.N.G/Kirika v0.0.0-20220303134137-d0976eac62c4/go.mod h1:NYC/3wOINygtTYvAqEtMfgWBeJ/9Gfv0NvDxnWmg+yA= git.gammaspectra.live/S.O.N.G/Kirika v0.0.0-20220304112513-f1e808b8f144/go.mod h1:NYC/3wOINygtTYvAqEtMfgWBeJ/9Gfv0NvDxnWmg+yA=
git.gammaspectra.live/S.O.N.G/go-fdkaac v0.0.0-20220228131722-e9cb84c52f48 h1:MaKiBfXQl0keyfdCi1PxGOKRTiWhIs8PqCal5GhKDi0= git.gammaspectra.live/S.O.N.G/go-fdkaac v0.0.0-20220228131722-e9cb84c52f48 h1:MaKiBfXQl0keyfdCi1PxGOKRTiWhIs8PqCal5GhKDi0=
git.gammaspectra.live/S.O.N.G/go-fdkaac v0.0.0-20220228131722-e9cb84c52f48/go.mod h1:pkWt//S9hLVEQaJDPu/cHHPk8vPpo/0+zHy0me4LIP4= git.gammaspectra.live/S.O.N.G/go-fdkaac v0.0.0-20220228131722-e9cb84c52f48/go.mod h1:pkWt//S9hLVEQaJDPu/cHHPk8vPpo/0+zHy0me4LIP4=
git.gammaspectra.live/S.O.N.G/go-pus v0.0.0-20220227175608-6cc027f24dba h1:JEaxCVgdr3XXAuDCPAx7ttLFZaaHzTEzG+oRnVUtUKU= git.gammaspectra.live/S.O.N.G/go-pus v0.0.0-20220227175608-6cc027f24dba h1:JEaxCVgdr3XXAuDCPAx7ttLFZaaHzTEzG+oRnVUtUKU=
@ -12,10 +12,13 @@ github.com/BurntSushi/toml v1.0.0 h1:dtDWrepsVPfW9H/4y7dDgFc2MBUSeJhlaDtK13CxFlU
github.com/BurntSushi/toml v1.0.0/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ= github.com/BurntSushi/toml v1.0.0/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ=
github.com/cocoonlife/testify v0.0.0-20160218172820-792cc1faeb64 h1:LjPYdzoFSAJ5Tr/ElL8kzTJghXgpnOjJVbgd1UvZB1o= github.com/cocoonlife/testify v0.0.0-20160218172820-792cc1faeb64 h1:LjPYdzoFSAJ5Tr/ElL8kzTJghXgpnOjJVbgd1UvZB1o=
github.com/d4l3k/messagediff v1.2.2-0.20190829033028-7e0a312ae40b/go.mod h1:Oozbb1TVXFac9FtSIxHBMnBCq2qeH/2KkEQxENCrlLo= github.com/d4l3k/messagediff v1.2.2-0.20190829033028-7e0a312ae40b/go.mod h1:Oozbb1TVXFac9FtSIxHBMnBCq2qeH/2KkEQxENCrlLo=
github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8=
github.com/dh1tw/gosamplerate v0.1.2 h1:oyqtZk67xB9B4l+vIZCZ3F0RYV/z66W58VOah11/ktI= github.com/dh1tw/gosamplerate v0.1.2 h1:oyqtZk67xB9B4l+vIZCZ3F0RYV/z66W58VOah11/ktI=
github.com/dh1tw/gosamplerate v0.1.2/go.mod h1:zooTyHpoR7hE+FLfdE3yjLHb2QA2NpMusNfuaZqEACM= github.com/dh1tw/gosamplerate v0.1.2/go.mod h1:zooTyHpoR7hE+FLfdE3yjLHb2QA2NpMusNfuaZqEACM=
github.com/edgeware/mp4ff v0.26.1 h1:tH+TIesZZmrA8BN5HuiKWp3sv5NF4N1A2cFxTSCNL8E= github.com/edgeware/mp4ff v0.26.1 h1:tH+TIesZZmrA8BN5HuiKWp3sv5NF4N1A2cFxTSCNL8E=
github.com/edgeware/mp4ff v0.26.1/go.mod h1:6VHE5CTkpDseIg775+rh8BfnTvqjMnVbz5EDU4QwSdc= github.com/edgeware/mp4ff v0.26.1/go.mod h1:6VHE5CTkpDseIg775+rh8BfnTvqjMnVbz5EDU4QwSdc=
github.com/enriquebris/goconcurrentqueue v0.6.3 h1:+ma7EEEFMDmJBIS6Q4KNJruChctgwYQFqlxdveIoEE4=
github.com/enriquebris/goconcurrentqueue v0.6.3/go.mod h1:OZ+KC2BcRYzjg0vgoUs1GFqdAjkD9mz2Ots7Jbm1yS4=
github.com/go-audio/audio v1.0.0/go.mod h1:6uAu0+H2lHkwdGsAY+j2wHPNPpPoeg5AaEFh9FlA+Zs= github.com/go-audio/audio v1.0.0/go.mod h1:6uAu0+H2lHkwdGsAY+j2wHPNPpPoeg5AaEFh9FlA+Zs=
github.com/go-audio/riff v1.0.0/go.mod h1:l3cQwc85y79NQFCRB7TiPoNiaijp6q8Z0Uv38rVG498= github.com/go-audio/riff v1.0.0/go.mod h1:l3cQwc85y79NQFCRB7TiPoNiaijp6q8Z0Uv38rVG498=
github.com/go-audio/wav v1.0.0/go.mod h1:3yoReyQOsiARkvPl3ERCi8JFjihzG6WhjYpZCf5zAWE= github.com/go-audio/wav v1.0.0/go.mod h1:3yoReyQOsiARkvPl3ERCi8JFjihzG6WhjYpZCf5zAWE=
@ -34,10 +37,13 @@ github.com/mewkiz/flac v1.0.7/go.mod h1:yU74UH277dBUpqxPouHSQIar3G1X/QIclVbFahSd
github.com/mewkiz/pkg v0.0.0-20190919212034-518ade7978e2 h1:EyTNMdePWaoWsRSGQnXiSoQu0r6RS1eA557AwJhlzHU= github.com/mewkiz/pkg v0.0.0-20190919212034-518ade7978e2 h1:EyTNMdePWaoWsRSGQnXiSoQu0r6RS1eA557AwJhlzHU=
github.com/mewkiz/pkg v0.0.0-20190919212034-518ade7978e2/go.mod h1:3E2FUC/qYUfM8+r9zAwpeHJzqRVVMIYnpzD/clwWxyA= github.com/mewkiz/pkg v0.0.0-20190919212034-518ade7978e2/go.mod h1:3E2FUC/qYUfM8+r9zAwpeHJzqRVVMIYnpzD/clwWxyA=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/sssgun/mp3 v0.0.0-20170810093403-85f2ec632081 h1:Qo/HswJzVywl0podyXMD62HIohsj/Ij2oXbD26aUIxM= github.com/sssgun/mp3 v0.0.0-20170810093403-85f2ec632081 h1:Qo/HswJzVywl0podyXMD62HIohsj/Ij2oXbD26aUIxM=
github.com/sssgun/mp3 v0.0.0-20170810093403-85f2ec632081/go.mod h1:ExwZtltybPz8zLO8c2lKRfpPk1HAxhrkp038QIBs+yg= github.com/sssgun/mp3 v0.0.0-20170810093403-85f2ec632081/go.mod h1:ExwZtltybPz8zLO8c2lKRfpPk1HAxhrkp038QIBs+yg=
github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
github.com/viert/go-lame v0.0.0-20201108052322-bb552596b11d h1:LptdD7GTUZeklomtW5vZ1AHwBvDBUCZ2Ftpaz7uEI7g= github.com/viert/go-lame v0.0.0-20201108052322-bb552596b11d h1:LptdD7GTUZeklomtW5vZ1AHwBvDBUCZ2Ftpaz7uEI7g=
github.com/viert/go-lame v0.0.0-20201108052322-bb552596b11d/go.mod h1:EqTcYM7y4JlSfeTI47pmNu3EZQuCuLQefsQyg1Imlz8= github.com/viert/go-lame v0.0.0-20201108052322-bb552596b11d/go.mod h1:EqTcYM7y4JlSfeTI47pmNu3EZQuCuLQefsQyg1Imlz8=
golang.org/x/image v0.0.0-20190220214146-31aff87c08e9/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= golang.org/x/image v0.0.0-20190220214146-31aff87c08e9/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=

131
mount.go
View file

@ -8,6 +8,7 @@ import (
"git.gammaspectra.live/S.O.N.G/Kirika/audio/format/mp3" "git.gammaspectra.live/S.O.N.G/Kirika/audio/format/mp3"
"git.gammaspectra.live/S.O.N.G/Kirika/audio/format/opus" "git.gammaspectra.live/S.O.N.G/Kirika/audio/format/opus"
"git.gammaspectra.live/S.O.N.G/Kirika/audio/packetizer" "git.gammaspectra.live/S.O.N.G/Kirika/audio/packetizer"
"github.com/enriquebris/goconcurrentqueue"
"io" "io"
"log" "log"
"sync" "sync"
@ -31,11 +32,11 @@ type StreamListener struct {
Close func() Close func()
} }
type StreamMount struct { type StreamMount struct {
Mount string Mount string
MimeType string MimeType string
Packetizer packetizer.Packetizer Packetizer packetizer.Packetizer
SampleRate int SampleRate int
MetadataQueue *goconcurrentqueue.FIFO
listeners []*StreamListener listeners []*StreamListener
listenersLock sync.Mutex listenersLock sync.Mutex
keepBuffer []packetizer.Packet keepBuffer []packetizer.Packet
@ -147,41 +148,42 @@ func NewStreamMount(source audio.Source, mount string, codec string, container s
} }
return &StreamMount{ return &StreamMount{
Mount: mount, Mount: mount,
MimeType: mimeType, MimeType: mimeType,
Packetizer: packetizerEntry, Packetizer: packetizerEntry,
SampleRate: sampleRate, SampleRate: sampleRate,
MetadataQueue: goconcurrentqueue.NewFIFO(),
} }
} }
func (m *StreamMount) removeDiscard(sampleNumber int64) { func (m *StreamMount) removeDiscard(sampleNumber int64) {
for i, p := range m.keepBuffer { for i, p := range m.keepBuffer {
if p.KeepMode() == packetizer.Discard && p.GetSampleNumber() <= sampleNumber { if p.KeepMode() == packetizer.Discard && p.GetEndSampleNumber() <= sampleNumber {
m.keepBuffer = append(m.keepBuffer[:i], m.keepBuffer[i+1:]...) m.keepBuffer = append(m.keepBuffer[:i], m.keepBuffer[i+1:]...)
m.removeDiscard(sampleNumber) m.removeDiscard(sampleNumber)
break break
} else if p.GetSampleNumber() > sampleNumber { } else if p.GetEndSampleNumber() > sampleNumber {
//they are placed in order //they are placed in order
break break
} }
} }
} }
func (m *StreamMount) removeKeepLast() { func (m *StreamMount) removeKeepLast(category int64) {
for i, p := range m.keepBuffer { for i, p := range m.keepBuffer {
if p.KeepMode() == packetizer.KeepLast { if p.Category() == category && p.KeepMode() == packetizer.KeepLast {
m.keepBuffer = append(m.keepBuffer[:i], m.keepBuffer[i+1:]...) m.keepBuffer = append(m.keepBuffer[:i], m.keepBuffer[i+1:]...)
m.removeKeepLast() m.removeKeepLast(category)
break break
} }
} }
} }
func (m *StreamMount) removeGroupKeep() { func (m *StreamMount) removeGroupKeep(category int64) {
for i, p := range m.keepBuffer { for i, p := range m.keepBuffer {
if p.KeepMode() == packetizer.GroupKeep { if p.Category() == category && p.KeepMode() == packetizer.GroupKeep {
m.keepBuffer = append(m.keepBuffer[:i], m.keepBuffer[i+1:]...) m.keepBuffer = append(m.keepBuffer[:i], m.keepBuffer[i+1:]...)
m.removeGroupKeep() m.removeGroupKeep(category)
break break
} }
} }
@ -203,6 +205,52 @@ func (m *StreamMount) GetListeners() (entries []*ListenerInformation) {
return return
} }
func (m *StreamMount) handlePacket(packet packetizer.Packet) {
var toRemove []int
//TODO: do this via goroutine messaging?
for i, l := range m.listeners {
if l.Start != nil {
l.Start(m.keepBuffer)
l.Start = nil
}
if l.Write(packet) != nil {
toRemove = append(toRemove, i)
}
}
if len(toRemove) > 0 {
m.listenersLock.Lock()
//TODO: remove more than one per iteration
for _, i := range toRemove {
l := m.listeners[i]
m.listeners = append(m.listeners[:i], m.listeners[i+1:]...)
l.Close()
break
}
m.listenersLock.Unlock()
toRemove = toRemove[:0]
}
sampleLimit := packet.GetEndSampleNumber() - int64(maxBufferSize*m.SampleRate)
m.removeDiscard(sampleLimit) //always remove discards
switch packet.KeepMode() {
case packetizer.KeepLast:
m.removeKeepLast(packet.Category())
fallthrough
case packetizer.Keep:
m.keepBuffer = append(m.keepBuffer, packet)
case packetizer.GroupKeep:
m.keepBuffer = append(m.keepBuffer, packet)
case packetizer.GroupDiscard:
m.removeGroupKeep(packet.Category())
case packetizer.Discard:
m.keepBuffer = append(m.keepBuffer, packet)
}
}
func (m *StreamMount) Process(group *sync.WaitGroup) { func (m *StreamMount) Process(group *sync.WaitGroup) {
defer group.Done() defer group.Done()
defer func() { defer func() {
@ -213,55 +261,24 @@ func (m *StreamMount) Process(group *sync.WaitGroup) {
m.listeners = m.listeners[:0] m.listeners = m.listeners[:0]
m.listenersLock.Unlock() m.listenersLock.Unlock()
}() }()
var toRemove []int
for { for {
packet := m.Packetizer.GetPacket() packet := m.Packetizer.GetPacket()
if packet == nil { if packet == nil {
return return
} }
//TODO: do this via goroutine messaging? if item, err := m.MetadataQueue.Get(0); err == nil {
for i, l := range m.listeners { if metadataPacket, ok := item.(*QueueMetadataPacket); ok {
if l.Start != nil { if packet.GetEndSampleNumber() > metadataPacket.GetStartSampleNumber() {
l.Start(m.keepBuffer) m.MetadataQueue.Dequeue()
l.Start = nil m.handlePacket(metadataPacket)
} }
if l.Write(packet) != nil {
toRemove = append(toRemove, i)
} }
} }
if len(toRemove) > 0 { m.handlePacket(packet)
m.listenersLock.Lock()
//TODO: remove more than one per iteration
for _, i := range toRemove {
l := m.listeners[i]
m.listeners = append(m.listeners[:i], m.listeners[i+1:]...)
l.Close()
break
}
m.listenersLock.Unlock()
toRemove = toRemove[:0]
}
sampleLimit := packet.GetSampleNumber() - int64(maxBufferSize*m.SampleRate)
m.removeDiscard(sampleLimit) //always remove discards
switch packet.KeepMode() {
case packetizer.KeepLast:
m.removeKeepLast()
fallthrough
case packetizer.Keep:
m.keepBuffer = append(m.keepBuffer, packet)
case packetizer.GroupKeep:
m.keepBuffer = append(m.keepBuffer, packet)
case packetizer.GroupDiscard:
m.removeGroupKeep()
case packetizer.Discard:
m.keepBuffer = append(m.keepBuffer, packet)
}
} }

147
queue.go
View file

@ -13,6 +13,7 @@ import (
"net/http" "net/http"
"os" "os"
"path" "path"
"strconv"
"strings" "strings"
"sync" "sync"
"time" "time"
@ -33,9 +34,35 @@ type QueueTrackEntry struct {
original map[string]interface{} original map[string]interface{}
} }
type QueueMetadataPacket struct {
sampleNumber int64
TrackEntry *QueueTrackEntry
}
func (p *QueueMetadataPacket) KeepMode() packetizer.KeepMode {
return packetizer.KeepLast
}
func (p *QueueMetadataPacket) GetStartSampleNumber() int64 {
return p.sampleNumber
}
func (p *QueueMetadataPacket) GetEndSampleNumber() int64 {
return p.sampleNumber
}
func (p *QueueMetadataPacket) Category() int64 {
return -1
}
func (p *QueueMetadataPacket) GetData() []byte {
return nil
}
type Queue struct { type Queue struct {
NowPlaying chan *QueueTrackEntry NowPlaying chan *QueueTrackEntry
QueueEmpty chan *QueueTrackEntry QueueEmpty chan *QueueTrackEntry
Duration time.Duration
audioQueue *audio.Queue audioQueue *audio.Queue
mounts []*StreamMount mounts []*StreamMount
queue []*QueueTrackEntry queue []*QueueTrackEntry
@ -109,6 +136,13 @@ func (q *Queue) AddTrack(entry *QueueTrackEntry, tail bool) error {
log.Printf("now playing %s\n", f.Name()) log.Printf("now playing %s\n", f.Name())
if e := q.Get(entry.Identifier); e != nil { if e := q.Get(entry.Identifier); e != nil {
q.NowPlaying <- e q.NowPlaying <- e
for _, mount := range q.mounts {
mount.MetadataQueue.Enqueue(&QueueMetadataPacket{
//TODO: carry error
sampleNumber: int64(q.Duration * time.Duration(queue.GetSampleRate()) / time.Second),
TrackEntry: e,
})
}
} }
} }
@ -118,6 +152,12 @@ func (q *Queue) AddTrack(entry *QueueTrackEntry, tail bool) error {
removeCallback := func(queue *audio.Queue, entry *audio.QueueEntry) { removeCallback := func(queue *audio.Queue, entry *audio.QueueEntry) {
defer f.Close() defer f.Close()
q.mutex.Lock()
//TODO: carry error
q.Duration += (time.Second * time.Duration(entry.ReadSamples)) / time.Duration(entry.Source.SampleRate)
q.mutex.Unlock()
q.Remove(entry.Identifier) q.Remove(entry.Identifier)
q.HandleQueue() q.HandleQueue()
} }
@ -227,28 +267,97 @@ func (q *Queue) Remove(identifier audio.QueueIdentifier) bool {
return false return false
} }
type httpWriter struct { type httpAudioWriter struct {
timeout time.Duration timeout time.Duration
writer http.ResponseWriter writer http.ResponseWriter
metadataToSend struct {
Title string
URL string
}
icyInterval int
icyCounter int
} }
func (h *httpWriter) Write(p []byte) (n int, err error) { func (h *httpAudioWriter) writeIcy() error {
packetContent := make([]byte, 1, 4096)
if len(h.metadataToSend.Title) > 0 {
//TODO: quote quotes
packetContent = append(packetContent, []byte(fmt.Sprintf("StreamTitle='%s';", strings.ReplaceAll(h.metadataToSend.Title, "'", "")))...)
h.metadataToSend.Title = ""
}
if len(h.metadataToSend.URL) > 0 {
//TODO: quote quotes
packetContent = append(packetContent, []byte(fmt.Sprintf("StreamURL='%s';", strings.ReplaceAll(h.metadataToSend.URL, "'", "")))...)
h.metadataToSend.URL = ""
}
contentLength := len(packetContent) - 1
if contentLength > 16*255 {
//cannot send long titles
_, err := h.writer.Write(make([]byte, 1))
return err
}
if (contentLength % 16) == 0 { //already padded
packetContent[0] = byte(contentLength / 16)
} else {
packetContent[0] = byte(contentLength/16) + 1
packetContent = append(packetContent, make([]byte, 16-(contentLength%16))...)
}
_, err := h.writer.Write(packetContent)
return err
}
func (h *httpAudioWriter) Write(p []byte) (n int, err error) {
if h.writer != nil { if h.writer != nil {
n, err = h.writer.Write(p) if h.icyInterval > 0 {
if err != nil { var i int
h.writer = nil for len(p) > 0 {
l := h.icyInterval - h.icyCounter
if l <= len(p) {
i, err = h.writer.Write(p[:l])
n += i
if err != nil {
h.writer = nil
break
}
if err = h.writeIcy(); err != nil {
h.writer = nil
break
}
h.icyCounter = 0
p = p[l:]
} else {
i, err = h.writer.Write(p)
n += i
if err != nil {
h.writer = nil
break
}
h.icyCounter += i
p = p[:0]
}
}
} else {
n, err = h.writer.Write(p)
if err != nil {
h.writer = nil
}
} }
return return
} }
return 0, io.EOF return 0, io.EOF
} }
func (h *httpWriter) Close() (err error) { func (h *httpAudioWriter) Close() (err error) {
h.writer = nil h.writer = nil
return nil return nil
} }
func (h *httpWriter) Flush() { func (h *httpAudioWriter) Flush() {
if h.writer != nil { if h.writer != nil {
//TODO: not deadline aware? //TODO: not deadline aware?
/*if flusher, ok := h.writer.(http.Flusher); ok { /*if flusher, ok := h.writer.(http.Flusher); ok {
@ -280,11 +389,25 @@ func (q *Queue) HandleRadioRequest(writer http.ResponseWriter, request *http.Req
writer.Header().Set("Cache-Control", "no-store, max-age=0, no-transform") 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")
byteWriter := &httpWriter{writer: writer, timeout: time.Second * 2} byteWriter := &httpAudioWriter{writer: writer, timeout: time.Second * 2}
if numberValue, err := strconv.Atoi(request.Header.Get("icy-metadata")); err == nil && numberValue >= 1 {
byteWriter.icyInterval = 8192
writer.Header().Set("Icy-MetaInt", fmt.Sprintf("%d", byteWriter.icyInterval))
}
var wgClient sync.WaitGroup var wgClient sync.WaitGroup
writeCallback := func(packet packetizer.Packet) error { writeCallback := func(packet packetizer.Packet) error {
if metadataPacket, ok := packet.(*QueueMetadataPacket); ok {
if len(metadataPacket.TrackEntry.Metadata.Artist) > 0 {
byteWriter.metadataToSend.Title = fmt.Sprintf("%s - %s", metadataPacket.TrackEntry.Metadata.Artist, metadataPacket.TrackEntry.Metadata.Title)
} else {
byteWriter.metadataToSend.Title = metadataPacket.TrackEntry.Metadata.Title
}
byteWriter.metadataToSend.URL = metadataPacket.TrackEntry.Metadata.Art
return nil
}
//TODO: icy //TODO: icy
/* /*
select { select {
@ -354,9 +477,9 @@ func (q *Queue) HandleRadioRequest(writer http.ResponseWriter, request *http.Req
}, },
Start: func(packets []packetizer.Packet) error { Start: func(packets []packetizer.Packet) error {
if len(packets) > 0 { if len(packets) > 0 {
sampleBufferMin := packets[len(packets)-1].GetSampleNumber() - sampleBufferLimit sampleBufferMin := packets[len(packets)-1].GetStartSampleNumber() - sampleBufferLimit
for _, p := range packets { for _, p := range packets {
if p.KeepMode() != packetizer.Discard || p.GetSampleNumber() >= sampleBufferMin { if p.KeepMode() != packetizer.Discard || p.GetEndSampleNumber() >= sampleBufferMin {
if err := writeCallback(p); err != nil { if err := writeCallback(p); err != nil {
return err return err
} }