Base on the protocol description link (thank to Rauuun) I implemented this algorithm.
ICY_METADATA_INTERVAL = 16000 # bytes
ICY_BYTES_BLOCK_SIZE = 16 # bytes
ICY_METADATA_SIGNAL = "META_EVENT".encode()
async def stream_from_queue(queue: Queue, session_id: str):
buffer = b""
...
for chunk in <stream_queue>:
... # get an audio chunk from queue
if chunk == ICY_METADATA_SIGNAL: # if get a special signal we can send some metadata
# flush buffer padded with zeros to ICY_METADATA_INTERVAL length
yield buffer + (ICY_METADATA_INTERVAL - len(buffer)) * (0).to_bytes()
buffer = b""
# send a meta message
yield preprocess_metadata()
else: # send raw audio data
buffer += chunk
if len(buffer) < ICY_METADATA_INTERVAL:
continue
yield buffer[:ICY_METADATA_INTERVAL]
yield (0).to_bytes() # we have to send at least zero byte as metadata after every ICY_METADATA_INTERVAL
buffer = buffer[ICY_METADATA_INTERVAL:]
...
def preprocess_metadata(metadata: str = "META_EVENT") -> bytes:
icy_metadata_formatted = f"StreamTitle='{metadata}';".encode()
icy_metadata_block_length = len(icy_metadata_formatted) + 1
return (
# number of ICY_BYTES_BLOCK_SIZE blocks needed for this meta message (including this byte)
(1 + icy_metadata_block_length // ICY_BYTES_BLOCK_SIZE).to_bytes(1, "big")
# meta message encoded
+ icy_metadata_formatted
# zero-padded tail to fill the last ICY_BYTES_BLOCK_SIZE
+ (ICY_BYTES_BLOCK_SIZE - icy_metadata_block_length % ICY_BYTES_BLOCK_SIZE)
* (0).to_bytes(1, "big")
)