MeteorLight/listener/icy/icy.go
DataHoarder b8610799c8
All checks were successful
continuous-integration/drone/push Build is passing
Refactor queue / mount sections and split into multiple files and interfaces
2022-09-03 16:26:45 +02:00

163 lines
4.1 KiB
Go

package icy
import (
"errors"
"fmt"
"git.gammaspectra.live/S.O.N.G/Kirika/audio/packetizer"
"git.gammaspectra.live/S.O.N.G/MeteorLight/listener"
"git.gammaspectra.live/S.O.N.G/MeteorLight/queue/metadata"
"git.gammaspectra.live/S.O.N.G/MeteorLight/util"
"strconv"
"sync/atomic"
)
// icyInterval weird clients might not support other numbers than this
const icyInterval = 8192
type Listener struct {
information listener.Information
started atomic.Bool
streamStartOffset int64
streamSamplesBuffer int64
counter int
ctx *util.RequestDone
sliceWriter chan []byte
offsetStart bool
pendingMetadata map[string]string
}
func NewListener(info listener.Information, ctx *util.RequestDone, sliceWriter chan []byte, samplesToBuffer int64, offsetStart bool) (*Listener, map[string]string) {
headers := make(map[string]string)
headers["icy-metaint"] = strconv.Itoa(icyInterval)
return &Listener{
information: info,
ctx: ctx,
sliceWriter: sliceWriter,
offsetStart: offsetStart,
streamSamplesBuffer: samplesToBuffer,
streamStartOffset: -1,
pendingMetadata: make(map[string]string),
}, headers
}
func (l *Listener) Identifier() string {
return l.information.Identifier
}
func (l *Listener) Information() *listener.Information {
return &l.information
}
func (l *Listener) HasStarted() bool {
return l.started.Load()
}
func (l *Listener) Start(packets []packetizer.Packet) error {
if l.started.Swap(true) {
return nil
}
if len(packets) > 0 {
sampleBufferMin := packets[len(packets)-1].GetStartSampleNumber() - l.streamSamplesBuffer
for _, p := range packets {
if p.KeepMode() != packetizer.Discard || p.GetEndSampleNumber() >= sampleBufferMin {
if err := l.Write(p); err != nil {
return err
}
}
}
}
return nil
}
func (l *Listener) writeIcy() []byte {
packetContent := make([]byte, 1, 16*255+1)
for k, v := range l.pendingMetadata {
packetContent = append(packetContent, []byte(fmt.Sprintf("%s='%s';", k, v))...)
delete(l.pendingMetadata, k)
//shouldn't send multiple properties in same packet if we want working single quotes, wait until next ICY frame
break
}
contentLength := len(packetContent) - 1
if contentLength > 16*255 {
//cannot send long titles
return make([]byte, 1)
}
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))...)
}
return packetContent
}
func (l *Listener) Write(packet packetizer.Packet) error {
if l.ctx.Done() {
return l.ctx.Error()
}
if metadataPacket, ok := packet.(*metadata.Packet); ok {
if len(metadataPacket.TrackEntry.Artist()) > 0 {
l.pendingMetadata["StreamTitle"] = fmt.Sprintf("%s - %s", metadataPacket.TrackEntry.Artist(), metadataPacket.TrackEntry.Title())
} else {
l.pendingMetadata["StreamTitle"] = metadataPacket.TrackEntry.Title()
}
if len(metadataPacket.TrackEntry.Metadata.Art) > 0 {
l.pendingMetadata["StreamURL"] = metadataPacket.TrackEntry.Metadata.Art
}
return nil
}
var p []byte
if offsetable, ok := packet.(packetizer.OffsetablePacket); l.offsetStart && ok {
if l.streamStartOffset <= -1 {
if offsetable.KeepMode() != packetizer.Keep {
l.streamStartOffset = offsetable.GetStartSampleNumber()
p = offsetable.GetDataOffset(l.streamStartOffset)
} else {
p = packet.GetData()
}
} else {
p = offsetable.GetDataOffset(l.streamStartOffset)
}
} else {
p = packet.GetData()
}
var data []byte
for len(p) > 0 {
length := icyInterval - l.counter
if length <= len(p) {
data = append(data, p[:length]...)
data = append(data, l.writeIcy()...)
l.counter = 0
p = p[length:]
} else {
data = append(data, p...)
l.counter += len(p)
p = p[:0]
}
}
if len(l.sliceWriter) >= (cap(l.sliceWriter) - 1) {
l.ctx.Fail(errors.New("client ran out of writer buffer"))
return l.ctx.Error()
}
l.sliceWriter <- data
return nil
}
func (l *Listener) Close() {
close(l.sliceWriter)
}