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) }