diff --git a/PACKET_STREAM.md b/PACKET_STREAM.md new file mode 100644 index 0000000..4010f89 --- /dev/null +++ b/PACKET_STREAM.md @@ -0,0 +1,37 @@ +# Audio, Timing and Metadata Packet Stream + + +## The current problems +There are no good tagging metadata formats for live content without re-starting the file decoding (for example, Ogg). This causes several issues. + +ICY metadata exists, but this has several problems. It works on regular byte intervals, where most audio formats / containers work on packets. Additionally, the data you can add is severely limited, although you could send patches for players to support more fields. + +For timing, there is no option other than decoding the audio or being aware of the container format. This can also have issues given the time the stream starts can be different for long-running streams. + +Partially decoding such streams is also hard, without being aware of the container format. For example, technically you should be able to decode any two seconds, given you have header + related packets available. + + +## The solution + +The solution presented here is to split the stream on distinct packets and tag them with multiple fields: +* **Frame type** `unsigned varint`: See next section +* **Category** `signed varint`: This can be used to differentiate across same types or groups. +* **Start Sample Number** `signed varint`: When the content applies from, counting in audio samples from the start of the stream (absolute, not relative to the player start). +* **Duration In Samples** `signed varint`: Duration of content in samples +* **Data Length** `unsigned varint` +* **Data** `bytes` + +Given we are sending packets with tagged types and classes, we can support these frame types: +* **Header**: Simple metadata about the stream. Payload contains little-endian `int64 Channels`, `int64 SampleRate`, `int32 len(MimeType)`, `bytes MimeType` +* **Audio Data**: raw header / audio frame or packet. Several subtypes exist: + * KeepLast: Keep the last of these frames per-category. + * Keep: Keep the all of these frames. + * GroupKeep: Keep frames in this group per-category. + * GroupDiscard: Discard last frames kept on the group per-category. + * Discard: This frame can be discarded after usage. +* **Identifier**: Identifier for the new track. The payload, in MeteorLight, contains the `queue_id` as a `signed varint`. +* **Metadata**: Metadata for the track. The payload contains a JSON-encoded _TrackEntry.Metadata_ entry. + +Even client code that is not aware of audio playback can receive and parse these frames easily. + +Specify `X-Audio-Packet-Stream: 1` on the HTTP request headers. You will receive a response with `X-Audio-Packet-Stream: 1` header set and `Content-Type: application/x-audio-packet-stream`, with the body being continous frames. \ No newline at end of file diff --git a/README.md b/README.md index c6aa7ab..e74b22c 100644 --- a/README.md +++ b/README.md @@ -17,7 +17,7 @@ Radio streamer ([kawa](https://github.com/Luminarys/kawa) drop-in compatible). * Can set custom sample rate / channel count / bitdepth / compression level per stream mount. * Can read and apply ReplayGain tags, or normalize audio loudness. * Can have audio sources over HTTP(s) URLs on `path` property, and supports seeking. -* Precise metadata and timing information packet stream, trigger via `x-audio-packet-stream: 1` HTTP header. +* [Precise metadata and timing information packet stream](PACKET_STREAM.md), trigger via `x-audio-packet-stream: 1` HTTP header. * Workaround to allow FLAC streaming under Safari ## Dependencies diff --git a/queue.go b/queue.go index e417997..472c624 100644 --- a/queue.go +++ b/queue.go @@ -394,10 +394,11 @@ func (q *Queue) GetListeners() (listeners []*ListenerInformation) { return } -type packetStreamType uint64 +type PacketStreamType uint64 +//PacketStreamType The order of these fields is important and set on-wire protocol const ( - Header = packetStreamType(iota) + Header = PacketStreamType(iota) DataKeepLast DataKeep DataGroupKeep @@ -408,7 +409,8 @@ const ( ) type packetStreamFrame struct { - Type packetStreamType + Type PacketStreamType + Category int64 StartSampleNumber int64 DurationInSamples int64 //automatically filled based on Data @@ -424,6 +426,9 @@ func (p *packetStreamFrame) Encode() []byte { n := binary.PutUvarint(varBuf, uint64(p.Type)) buf.Write(varBuf[:n]) + n = binary.PutUvarint(varBuf, uint64(p.Category)) + buf.Write(varBuf[:n]) + n = binary.PutVarint(varBuf, p.StartSampleNumber) buf.Write(varBuf[:n]) @@ -575,6 +580,9 @@ func (q *Queue) HandleRadioRequest(writer http.ResponseWriter, request *http.Req if numberValue, err := strconv.Atoi(request.Header.Get("x-audio-packet-stream")); err == nil && numberValue == 1 { //version 1 + writer.Header().Set("x-audio-packet-stream", "1") + writer.Header().Set("Content-Type", "application/x-audio-packet-stream") + packetWriteCallback = func(packet packetizer.Packet) error { if requestDone != nil { return requestDone @@ -592,6 +600,7 @@ func (q *Queue) HandleRadioRequest(writer http.ResponseWriter, request *http.Req writeChannel <- (&packetStreamFrame{ Type: TrackIdentifier, + Category: packet.Category(), StartSampleNumber: packet.GetStartSampleNumber(), DurationInSamples: packet.GetEndSampleNumber() - packet.GetStartSampleNumber(), Data: queueInfoBuf[:n], @@ -606,6 +615,7 @@ func (q *Queue) HandleRadioRequest(writer http.ResponseWriter, request *http.Req writeChannel <- (&packetStreamFrame{ Type: TrackMetadata, + Category: packet.Category(), StartSampleNumber: packet.GetStartSampleNumber(), DurationInSamples: packet.GetEndSampleNumber() - packet.GetStartSampleNumber(), Data: metadataBytes, @@ -622,7 +632,7 @@ func (q *Queue) HandleRadioRequest(writer http.ResponseWriter, request *http.Req //TODO: category - var frameType packetStreamType + var frameType PacketStreamType switch packet.KeepMode() { case packetizer.KeepLast: frameType = DataKeepLast @@ -640,6 +650,7 @@ func (q *Queue) HandleRadioRequest(writer http.ResponseWriter, request *http.Req writeChannel <- (&packetStreamFrame{ Type: frameType, + Category: packet.Category(), StartSampleNumber: packet.GetStartSampleNumber(), DurationInSamples: packet.GetEndSampleNumber() - packet.GetStartSampleNumber(), Data: packet.GetData(), @@ -656,6 +667,7 @@ func (q *Queue) HandleRadioRequest(writer http.ResponseWriter, request *http.Req writeChannel <- (&packetStreamFrame{ Type: Header, + Category: 0, StartSampleNumber: 0, DurationInSamples: 0, Data: headerBytes.Bytes(),