ICY-META v2.2 Protocol Specification
CasterClub Streaming Standards Initiative (CSSI) — Mcaster1DNAS / February 2026
Introduction
ICY-META v2.2 is an extended streaming metadata protocol built on top of the original SHOUTcast ICY 1.x protocol. It preserves full backwards compatibility with all legacy source encoders and listener clients while adding rich metadata support for modern streaming workflows: live DJ sets, podcasts, video simulcasts, social integration, track-level data, station programming, listener engagement, and content compliance.
The protocol is used by Mcaster1DNAS, mcaster1TagStack, mcaster1DSPEncoder, and mcaster1CastIt, and is published as an open specification by the CasterClub Streaming Standards Initiative (CSSI) for adoption by any streaming platform or client.
icy-name, icy-genre, etc.) are never changed, never removed, and always parsed independently of ICY2 logic.Protocol History
| Version | Origin | Status | Key Feature |
|---|---|---|---|
| ICY 1.x | Nullsoft SHOUTcast (1999) | LEGACY | icy-name, icy-genre, in-stream metadata blocks |
| ICY2 v2.0 | Icecast-KH extensions | LEGACY | Extended HTTP-based source connect |
| ICY2 v2.1 | Mcaster1DNAS / CSSI (Feb 15, 2026) | SUPERSEDED | Station ID, podcast, video, social, content flags (icy- prefix) |
| ICY2 v2.2 | Mcaster1DNAS / CSSI (Feb 2026) | CURRENT | Track metadata, artwork, show scheduling, audio technical, notices, engagement, distribution, PKI, licensing (icy-meta- prefix) |
ICY 1.x Legacy Protocol — Full Specification
The original SHOUTcast ICY protocol. These headers, formats, and behaviors are preserved exactly as-is and must never be altered by any ICY2 implementation. Any server claiming ICY2 support must continue to handle ICY 1.x clients without modification.
Source Connection & Headers
A source client connects to the server and sends its stream headers before audio data begins. Two connection formats exist:
Format A — SHOUTcast Legacy SOURCE
SOURCE /mountpoint HTTP/1.0
Authorization: Basic c291cmNlOnBhc3N3b3Jk
Content-Type: audio/mpeg
icy-name: My Radio Station
icy-genre: Electronic
icy-url: http://mystation.com
icy-pub: 1
icy-br: 128
icy-metaint: 8192
Format B — Icecast2 / Modern PUT
PUT /mountpoint.mp3 HTTP/1.1
Host: server.example.com:8000
Authorization: Basic c291cmNlOnBhc3N3b3Jk
Content-Type: audio/mpeg
icy-name: My Radio Station
icy-genre: Electronic
icy-url: http://mystation.com
icy-pub: 1
icy-br: 128
icy-metaint: 8192
ICY 1.x Source Headers
| Header | Type | Description |
|---|---|---|
icy-name | String | Station or stream display name |
icy-genre | String | Genre or content type (e.g., Electronic, Rock) |
icy-url | URL | Station homepage URL |
icy-pub | Boolean | Public directory listing — 1 = yes, 0 = no |
icy-br | Integer | Bitrate in kbps (e.g., 128, 320) |
icy-metaint | Integer | Bytes of audio between in-stream metadata blocks (e.g., 8192) |
Authentication
| Field | Description |
|---|---|
Authorization: Basic | Base64-encoded source:password (Icecast2) |
password | Source password sent inline (legacy SHOUTcast) |
adminpassword | Admin interface password |
user | Optional username for SHOUTcast v2 or Icecast2 |
In-Stream Metadata Block Format
ICY 1.x embeds track title changes directly inside the audio byte stream. Every metaint bytes of audio data, a metadata block is inserted:
| Byte(s) | Content |
|---|---|
| 1 byte | Block length indicator N. Actual block size = N × 16 bytes. If N = 0, no metadata follows. |
N × 16 bytes | Null-padded UTF-8 string in the format: StreamTitle='Artist - Title'; |
# Block length byte = 4 (4 × 16 = 64 bytes total)
# Content (null-padded to 64 bytes):
StreamTitle='Daft Punk - Get Lucky';StreamUrl='';
StreamTitle is universally supported. StreamUrl is optional. The full block is always a multiple of 16 bytes, padded with null bytes (\0).Server Response to Listener
When a listener client connects, the server responds with ICY headers before the audio stream begins:
ICY 200 OK
icy-notice1: SHOUTcast Distributed Network Audio Server
icy-notice2: (c) 1999-2004 Nullsoft, Inc.
icy-name: My Radio Station
icy-genre: Electronic
icy-url: http://mystation.com
icy-pub: 1
icy-br: 128
icy-metaint: 8192
Content-Type: audio/mpeg
[audio stream data with embedded metadata blocks follows]
ICY2 v2.2 — Protocol Detection
A source client signals ICY2 support by including the icy-metadata-version header. The server uses prefix matching — any 2.x value triggers ICY2 parsing. Legacy ICY 1.x fields are still parsed regardless.
icy-metadata-version: 2.2
- Value
2.x(2.0, 2.1, 2.2, etc.) → parse as ICY2 - Header absent or non-2.x → fall back to ICY 1.x parsing only
- ICY2 clients on ICY1 servers: unknown headers are silently ignored
Station Identity
| Header | Type | Description |
|---|---|---|
icy-meta-station-id | String | Unique global station ID — alphanumeric, hyphens allowed. Permanent across sessions. |
icy-meta-station-logo | URL | Station logo or branding image URL |
icy-meta-certissuer-id | String | Certificate authority ID for verification |
icy-meta-cert-rootca | String | Root CA hash or fingerprint |
icy-meta-certificate | String | Base64-encoded PEM certificate |
icy-meta-ssh-pubkey | String | SSH public key for source authentication |
icy-meta-verification-status | Enum | unverified | pending | verified | gold |
Programming / Show Scheduling
| Header | Type | Description |
|---|---|---|
icy-meta-show-title | String | Current show or program title |
icy-meta-show-start | ISO8601 | Datetime the current show started |
icy-meta-show-end | ISO8601 | Datetime the current show ends |
icy-meta-next-show | String | Title of the next scheduled program |
icy-meta-next-show-time | ISO8601 | Scheduled start time of the next program |
icy-meta-schedule-url | URL | Link to the full station program schedule |
icy-meta-autodj | Boolean | 1 = AutoDJ/automation active, 0 = live human DJ |
icy-meta-playlist-name | String | Current playlist or automation source name |
DJ / Host
| Header | Type | Description |
|---|---|---|
icy-meta-dj-handle | String | Current DJ or host social handle (e.g., @djsynthwave) |
icy-meta-dj-bio | String | Short DJ biography or tagline — max 280 characters |
icy-meta-dj-genre | String | DJ's genre set — comma-separated, max 5 values |
icy-meta-dj-showrating | Enum | all-ages | teen | mature | explicit |
Track Metadata
Per-track fields pushed on every track change, typically by a metadata injection system (e.g., mcaster1TagStack). icy-meta-track-artwork is the primary field for player album art display.
| Header | Type | Description |
|---|---|---|
icy-meta-track-artwork | URL | Album or track artwork image URL |
icy-meta-track-album | String | Album or release name |
icy-meta-track-year | Integer | Release year |
icy-meta-track-label | String | Record label |
icy-meta-track-bpm | Integer | Beats per minute |
icy-meta-track-key | String | Musical key — Camelot or standard notation (e.g., 8B, Am) |
icy-meta-track-genre | String | Per-track genre — may differ from station genre |
icy-meta-track-mbid | UUID | MusicBrainz Recording ID for direct track lookup |
icy-meta-track-isrc | String | International Standard Recording Code — royalty/licensing tracking |
Podcast
| Header | Type | Description |
|---|---|---|
icy-meta-podcast-host | String | Podcast creator or host name |
icy-meta-podcast-rating | Enum | all-ages | teen | mature | explicit |
icy-meta-podcast-rss | URL | Podcast RSS feed URL |
icy-meta-podcast-episode | String | Episode title or ID (e.g., S4E1 – Decentralized Rights) |
icy-meta-duration | Integer | Content runtime in seconds — applies to audio, podcast, or video |
icy-meta-language | String | ISO 639-1 language tag (e.g., en, en-US, es) |
Audio Technical
Reported by source encoders (e.g., mcaster1DSPEncoder). Allows directories and players to display and verify stream quality. icy-meta-loudness uses EBU R128 integrated LUFS measurement.
| Header | Type | Description |
|---|---|---|
icy-meta-audio-codec | Enum | mp3 | aac | aac-he | ogg | opus | flac |
icy-meta-samplerate | Integer | Sample rate in Hz (e.g., 44100, 48000) |
icy-meta-channels | Integer | 1 = mono, 2 = stereo, 6 = 5.1 surround |
icy-meta-loudness | Float | Integrated loudness in LUFS per EBU R128 (e.g., -14.0) |
icy-meta-encoder | String | Encoder software and version (e.g., Mcaster1DSP/1.2.0, BUTT/1.40) |
Video Streaming
| Header | Type | Description |
|---|---|---|
icy-meta-videotype | Enum | live | short | clip | trailer | ad |
icy-meta-videorating | Enum | all-ages | teen | mature | explicit |
icy-meta-videolink | URL | Link to the video content or stream page |
icy-meta-videotitle | String | Title of the video |
icy-meta-videoposter | URL | Thumbnail or preview image URL |
icy-meta-videochannel | String | Creator/channel handle |
icy-meta-videoplatform | Enum | youtube | tiktok | twitch | kick | rumble | vimeo | custom |
icy-meta-videostart | ISO8601 | Scheduled start datetime for the video |
icy-meta-videolive | Boolean | 1 = currently live, 0 = pre-recorded |
icy-meta-videocodec | String | Video codec in use (e.g., h264, vp9, av1) |
icy-meta-videofps | Integer | Frames per second |
icy-meta-videoresolution | String | e.g., 1080p, 4K, 720x1280 |
icy-meta-videonsfw | Boolean | Video-specific NSFW indicator |
Listener Engagement
| Header | Type | Description |
|---|---|---|
icy-meta-request-enabled | Boolean | 1 = listener song requests currently open |
icy-meta-request-url | URL | URL for song requests or listener dedications |
icy-meta-chat-url | URL | Live listener chat room URL |
icy-meta-tip-url | URL | Listener donation or tip URL (Ko-fi, Patreon, PayPal) |
icy-meta-events-url | URL | Link to upcoming station events or gigs page |
Broadcast Distribution
icy-meta-crosspost-platforms lists where the content is simultaneously live — distinct from icy-meta-social-youtube which is a static channel profile link.
| Header | Type | Description |
|---|---|---|
icy-meta-crosspost-platforms | String | Comma-separated active live platforms (e.g., youtube,twitch,tiktok) |
icy-meta-stream-session-id | String | Unique ID for this broadcast session — distinct from permanent station-id |
icy-meta-cdn-region | String | CDN or distribution region (e.g., us-east, eu-west) |
icy-meta-relay-origin | URL | Origin server URL if this mount is a relay |
Station Notices
Real-time listener announcements pushed via the stream for display in ICY2-aware players and notice boards. icy-meta-notice-expires is ISO8601 — players should hide the notice after this time.
| Header | Type | Description |
|---|---|---|
icy-meta-notice | String | General listener notice or announcement text |
icy-meta-notice-url | URL | Click-through URL for more information |
icy-meta-notice-expires | ISO8601 | Datetime after which the notice should no longer display |
Access, Authentication & Compliance
| Header | Type | Description |
|---|---|---|
icy-meta-auth-token | JWT | Optional Bearer JWT or custom access token |
icy-meta-nsfw | Boolean | 1 = explicit content — affects directory listings |
icy-meta-ai-generator | Boolean | 1 = AI-generated or AI-assisted content |
icy-meta-geo-region | String | Target geographic region (e.g., US, EU, GLOBAL) |
icy-meta-license-type | Enum | cc-by | cc-by-sa | cc0 | pro-licensed | all-rights-reserved |
icy-meta-royalty-free | Boolean | 1 = royalty-free content |
icy-meta-license-territory | String | Comma-separated ISO country codes (e.g., US,CA,EU) or GLOBAL |
ICY2 v2.1 Backwards Compatibility
The server accepts both the v2.2 icy-meta- prefix and the original v2.1 icy- prefix forms for all ICY2-specific fields. Both always work. Clients should prefer icy-meta- going forward.
| v2.1 Form (still accepted) | v2.2 Standard Form |
|---|---|
icy-station-id | icy-meta-station-id |
icy-podcast-host | icy-meta-podcast-host |
icy-podcast-rss | icy-meta-podcast-rss |
icy-podcast-episode | icy-meta-podcast-episode |
icy-duration | icy-meta-duration |
icy-language | icy-meta-language |
icy-video-type | icy-meta-videotype |
icy-video-link | icy-meta-videolink |
icy-video-platform | icy-meta-videoplatform |
icy-dj-handle | icy-meta-dj-handle |
icy-social-twitter | icy-meta-social-twitter |
icy-social-ig | icy-meta-social-ig |
icy-social-tiktok | icy-meta-social-tiktok |
icy-emoji | icy-meta-emoji |
icy-hashtags | icy-meta-hashtag-array |
icy-auth-token | icy-meta-auth-token |
icy-nsfw | icy-meta-nsfw |
icy-ai-generated | icy-meta-ai-generator |
icy-geo-region | icy-meta-geo-region |
icy-verification-status | icy-meta-verification-status |
Use Cases & Examples
Live DJ Set
icy-metadata-version: 2.2
icy-name: ChillZone FM
icy-genre: Electronic/House
icy-br: 320
icy-pub: 1
icy-meta-station-id: chillzone-fm-001
icy-meta-show-title: Late Night House Sessions
icy-meta-show-start: 2026-02-21T22:00:00Z
icy-meta-show-end: 2026-02-22T02:00:00Z
icy-meta-autodj: 0
icy-meta-dj-handle: @djsynthwave
icy-meta-dj-bio: Berlin-based electronic DJ — deep house, techno, and everything in between.
icy-meta-dj-genre: Electronic, House, Techno
icy-meta-dj-showrating: all-ages
icy-meta-track-artwork: https://cdn.example.com/art/track123.jpg
icy-meta-track-bpm: 124
icy-meta-track-key: 8B
icy-meta-track-mbid: 3a8e7c21-1234-5678-abcd-ef0123456789
icy-meta-audio-codec: mp3
icy-meta-samplerate: 44100
icy-meta-channels: 2
icy-meta-loudness: -14.0
icy-meta-request-enabled: 1
icy-meta-chat-url: https://chillzone.fm/chat
icy-meta-tip-url: https://ko-fi.com/djsynthwave
icy-meta-crosspost-platforms: youtube,twitch
icy-meta-notice: Tune in to our YouTube stream for the video feed tonight!
icy-meta-notice-expires: 2026-02-22T02:00:00Z
icy-meta-nsfw: 0
icy-meta-license-type: pro-licensed
Podcast Episode
icy-metadata-version: 2.2
icy-name: FutureTalks
icy-genre: Talk/Technology
icy-br: 128
icy-pub: 1
icy-meta-station-id: futuretalks-001
icy-meta-show-title: FutureTalks Podcast
icy-meta-podcast-host: Sasha Tran
icy-meta-podcast-rating: all-ages
icy-meta-podcast-episode: S4E1 – Decentralized Rights
icy-meta-podcast-rss: https://futuretalks.fm/feed.xml
icy-meta-duration: 3600
icy-meta-language: en
icy-meta-track-artwork: https://futuretalks.fm/episodes/s4e1.jpg
icy-meta-license-type: cc-by
icy-meta-royalty-free: 1
YouTube Simulcast
icy-metadata-version: 2.2
icy-name: ChillZone FM
icy-meta-videotype: live
icy-meta-videolink: https://youtube.com/watch?v=live543
icy-meta-videotitle: Synthwave All Night
icy-meta-videoplatform: youtube
icy-meta-videoresolution: 1080p
icy-meta-videocodec: h264
icy-meta-videofps: 60
icy-meta-videolive: 1
icy-meta-crosspost-platforms: youtube,twitch
icy-meta-stream-session-id: session-20260221-cz-001
TikTok Short-Form Push (No Audio Needed)
icy-metadata-version: 2.2
icy-name: @DropMaster
icy-meta-videotype: short
icy-meta-videolink: https://tiktok.com/@dropmaster/video/7739201
icy-meta-videochannel: @dropmaster
icy-meta-videoplatform: tiktok
icy-meta-videoposter: https://cdn.tiktok.com/posters/7739201.jpg
icy-meta-videoresolution: 720x1280
icy-meta-duration: 45
icy-meta-emoji: 🎵🔥🎥
icy-meta-hashtag-array: ["#beatdrop","#shorts","#music"]
AutoDJ with Notice
icy-metadata-version: 2.2
icy-name: ChillZone FM
icy-meta-autodj: 1
icy-meta-playlist-name: Top 40 Rotation
icy-meta-next-show: Morning Drive with DJ Kane
icy-meta-next-show-time: 2026-02-22T07:00:00Z
icy-meta-schedule-url: https://chillzone.fm/schedule
icy-meta-notice: Live show starts at 7am — DJ Kane in the morning!
icy-meta-notice-expires: 2026-02-22T07:00:00Z
Testing with cURL
audio-file.mp3, the server address, password, and mount as needed.Basic ICY 1.x Legacy Test
curl -k -X PUT \
-H "icy-name: Test Station" \
-H "icy-genre: Test" \
-H "icy-url: http://test.example.com" \
-H "icy-pub: 1" \
-H "icy-br: 128" \
-H "Authorization: Basic c291cmNlOnBhc3N3b3Jk" \
-H "Content-Type: audio/mpeg" \
--data-binary @audio-file.mp3 \
https://server.example.com:9443/test.mp3
ICY2 v2.2 Full Test
curl -k -X PUT \
-H "icy-metadata-version: 2.2" \
-H "icy-name: Test ICY2 Station" \
-H "icy-genre: Electronic" \
-H "icy-br: 128" \
-H "icy-pub: 1" \
-H "icy-meta-station-id: test-station-001" \
-H "icy-meta-show-title: Test Show" \
-H "icy-meta-autodj: 0" \
-H "icy-meta-dj-handle: @testdj" \
-H "icy-meta-track-artwork: https://example.com/art.jpg" \
-H "icy-meta-track-bpm: 128" \
-H "icy-meta-audio-codec: mp3" \
-H "icy-meta-samplerate: 44100" \
-H "icy-meta-channels: 2" \
-H "icy-meta-loudness: -14.0" \
-H "icy-meta-encoder: curl-test/1.0" \
-H "icy-meta-social-twitter: @teststation" \
-H "icy-meta-request-enabled: 1" \
-H "icy-meta-notice: Testing ICY2 v2.2 integration" \
-H "icy-meta-nsfw: 0" \
-H "icy-meta-ai-generator: 0" \
-H "icy-meta-geo-region: GLOBAL" \
-H "icy-meta-license-type: pro-licensed" \
-H "Authorization: Basic c291cmNlOnBhc3N3b3Jk" \
-H "Content-Type: audio/mpeg" \
--data-binary @audio-file.mp3 \
https://server.example.com:9443/test.mp3
Verify ICY2 Detection in Server Logs
INFO icy2/icy2_meta_is_icy2 Detected ICY-META version 2.2
INFO icy2/icy2_meta_parse_headers Parsed 18 ICY2 metadata fields for station-id: test-station-001
INFO icy2/icy2_meta_copy_to_stats ICY2 metadata copied to stats
C / C++ Implementation
Used by source clients such as Winamp DSP plugins, Edcast, BUTT, and any native streaming encoder. The source client opens a TCP socket, sends HTTP headers, then streams audio bytes.
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <netdb.h>
#include <unistd.h>
/* Base64 encode for Authorization header */
static const char *b64chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
void base64_encode(const char *in, char *out) {
int i = 0, j = 0;
unsigned char a3[3], a4[4];
int len = strlen(in);
while (len--) {
a3[i++] = *in++;
if (i == 3) {
a4[0] = (a3[0] & 0xfc) >> 2;
a4[1] = ((a3[0] & 0x03) << 4) + ((a3[1] & 0xf0) >> 4);
a4[2] = ((a3[1] & 0x0f) << 2) + ((a3[2] & 0xc0) >> 6);
a4[3] = a3[2] & 0x3f;
for (i = 0; i < 4; i++) out[j++] = b64chars[(int)a4[i]];
i = 0;
}
}
if (i) {
for (int k = i; k < 3; k++) a3[k] = '\0';
a4[0] = (a3[0] & 0xfc) >> 2;
a4[1] = ((a3[0] & 0x03) << 4) + ((a3[1] & 0xf0) >> 4);
a4[2] = ((a3[1] & 0x0f) << 2) + ((a3[2] & 0xc0) >> 6);
for (int k = 0; k < i + 1; k++) out[j++] = b64chars[(int)a4[k]];
while (i++ < 3) out[j++] = '=';
}
out[j] = '\0';
}
int icy2_connect(const char *host, int port, const char *mount,
const char *password, const char *station_name,
const char *station_id, const char *genre, int bitrate) {
struct hostent *he = gethostbyname(host);
if (!he) { fprintf(stderr, "DNS lookup failed\n"); return -1; }
int sock = socket(AF_INET, SOCK_STREAM, 0);
struct sockaddr_in addr = {0};
addr.sin_family = AF_INET;
addr.sin_port = htons(port);
addr.sin_addr = *((struct in_addr *)he->h_addr);
if (connect(sock, (struct sockaddr *)&addr, sizeof(addr)) < 0) {
perror("connect"); close(sock); return -1;
}
/* Build auth string */
char creds[256], auth[512];
snprintf(creds, sizeof(creds), "source:%s", password);
base64_encode(creds, auth);
/* Build and send ICY2 request */
char request[4096];
int n = snprintf(request, sizeof(request),
"PUT /%s HTTP/1.1\r\n"
"Host: %s:%d\r\n"
"Authorization: Basic %s\r\n"
"Content-Type: audio/mpeg\r\n"
"icy-metadata-version: 2.2\r\n"
"icy-name: %s\r\n"
"icy-genre: %s\r\n"
"icy-br: %d\r\n"
"icy-pub: 1\r\n"
"icy-meta-station-id: %s\r\n"
"icy-meta-audio-codec: mp3\r\n"
"icy-meta-encoder: MyClient/1.0\r\n"
"\r\n",
mount, host, port, auth,
station_name, genre, bitrate, station_id);
if (send(sock, request, n, 0) < 0) {
perror("send"); close(sock); return -1;
}
/* Read server response */
char resp[1024] = {0};
recv(sock, resp, sizeof(resp) - 1, 0);
if (strstr(resp, "200") == NULL) {
fprintf(stderr, "Server rejected: %s\n", resp);
close(sock); return -1;
}
printf("ICY2 connected. Ready to stream audio.\n");
return sock; /* caller sends audio bytes then close(sock) */
}
/* Send ICY 1.x in-stream title update */
void icy_send_metadata(int sock, int metaint,
const char *artist, const char *title) {
char meta_str[512];
snprintf(meta_str, sizeof(meta_str),
"StreamTitle='%s - %s';StreamUrl='';", artist, title);
int len = strlen(meta_str);
int blocks = (len / 16) + 1;
int total = blocks * 16;
unsigned char *buf = calloc(total + 1, 1);
buf[0] = (unsigned char)blocks;
memcpy(buf + 1, meta_str, len);
send(sock, buf, total + 1, 0);
free(buf);
}
Python Implementation
Suitable for mcaster1TagStack metadata pushers, automation scripts, and testing tools.
import socket
import base64
from dataclasses import dataclass, field
from typing import Optional
@dataclass
class ICY2Meta:
name: str = "My Station"
genre: str = "Various"
url: str = ""
bitrate: int = 128
public: int = 1
station_id: str = ""
show_title: str = ""
autodj: int = 0
dj_handle: str = ""
dj_bio: str = ""
track_artwork: str = ""
track_album: str = ""
track_bpm: int = 0
track_key: str = ""
track_mbid: str = ""
track_isrc: str = ""
audio_codec: str = "mp3"
samplerate: int = 44100
channels: int = 2
loudness: float = -14.0
encoder: str = "Python-ICY2/1.0"
notice: str = ""
notice_url: str = ""
request_url: str = ""
nsfw: int = 0
ai_generator: int = 0
license_type: str = "pro-licensed"
class ICY2Client:
def __init__(self, host: str, port: int, mount: str, password: str):
self.host = host
self.port = port
self.mount = mount.lstrip('/')
self.password = password
self.sock: Optional[socket.socket] = None
def _auth(self) -> str:
return base64.b64encode(f"source:{self.password}".encode()).decode()
def _build_request(self, meta: ICY2Meta) -> bytes:
lines = [
f"PUT /{self.mount} HTTP/1.1",
f"Host: {self.host}:{self.port}",
f"Authorization: Basic {self._auth()}",
"Content-Type: audio/mpeg",
"icy-metadata-version: 2.2",
f"icy-name: {meta.name}",
f"icy-genre: {meta.genre}",
f"icy-br: {meta.bitrate}",
f"icy-pub: {meta.public}",
]
if meta.url: lines.append(f"icy-url: {meta.url}")
if meta.station_id: lines.append(f"icy-meta-station-id: {meta.station_id}")
if meta.show_title: lines.append(f"icy-meta-show-title: {meta.show_title}")
lines.append(f"icy-meta-autodj: {meta.autodj}")
if meta.dj_handle: lines.append(f"icy-meta-dj-handle: {meta.dj_handle}")
if meta.dj_bio: lines.append(f"icy-meta-dj-bio: {meta.dj_bio}")
if meta.track_artwork:lines.append(f"icy-meta-track-artwork: {meta.track_artwork}")
if meta.track_album: lines.append(f"icy-meta-track-album: {meta.track_album}")
if meta.track_bpm: lines.append(f"icy-meta-track-bpm: {meta.track_bpm}")
if meta.track_key: lines.append(f"icy-meta-track-key: {meta.track_key}")
if meta.track_mbid: lines.append(f"icy-meta-track-mbid: {meta.track_mbid}")
if meta.track_isrc: lines.append(f"icy-meta-track-isrc: {meta.track_isrc}")
lines += [
f"icy-meta-audio-codec: {meta.audio_codec}",
f"icy-meta-samplerate: {meta.samplerate}",
f"icy-meta-channels: {meta.channels}",
f"icy-meta-loudness: {meta.loudness}",
f"icy-meta-encoder: {meta.encoder}",
]
if meta.notice: lines.append(f"icy-meta-notice: {meta.notice}")
if meta.notice_url: lines.append(f"icy-meta-notice-url: {meta.notice_url}")
if meta.request_url: lines.append(f"icy-meta-request-url: {meta.request_url}")
lines += [
f"icy-meta-nsfw: {meta.nsfw}",
f"icy-meta-ai-generator: {meta.ai_generator}",
f"icy-meta-license-type: {meta.license_type}",
]
return ("\r\n".join(lines) + "\r\n\r\n").encode("utf-8")
def connect(self, meta: ICY2Meta) -> bool:
self.sock = socket.create_connection((self.host, self.port), timeout=10)
self.sock.sendall(self._build_request(meta))
resp = self.sock.recv(1024).decode("utf-8", errors="replace")
return "200" in resp
def stream(self, data: bytes):
if self.sock:
self.sock.sendall(data)
def disconnect(self):
if self.sock:
self.sock.close()
self.sock = None
# Usage
if __name__ == "__main__":
client = ICY2Client("dnas.example.com", 9443, "/stream.mp3", "sourcepass")
meta = ICY2Meta(
name="ChillZone FM",
genre="Electronic",
bitrate=320,
station_id="chillzone-fm-001",
show_title="Late Night House Sessions",
dj_handle="@djsynthwave",
audio_codec="mp3",
encoder="Python-ICY2/1.0",
)
if client.connect(meta):
print("Connected. Streaming...")
with open("audio.mp3", "rb") as f:
while chunk := f.read(4096):
client.stream(chunk)
client.disconnect()
Go Implementation
Suitable for high-performance streaming relay agents, mcaster1CastIt distribution clients, and server-side tooling.
package main
import (
"encoding/base64"
"fmt"
"net"
"os"
"strings"
)
type ICY2Meta struct {
Name string
Genre string
URL string
Bitrate int
Public int
StationID string
ShowTitle string
AutoDJ int
DJHandle string
DJBio string
Artwork string
Album string
BPM int
Key string
MBID string
ISRC string
Codec string
SampleRate int
Channels int
Loudness float64
Encoder string
Notice string
NSFW int
License string
}
type ICY2Client struct {
Host string
Port int
Mount string
Password string
conn net.Conn
}
func (c *ICY2Client) buildRequest(m ICY2Meta) string {
auth := base64.StdEncoding.EncodeToString(
[]byte("source:" + c.Password))
var sb strings.Builder
w := func(s string) { sb.WriteString(s + "\r\n") }
w(fmt.Sprintf("PUT /%s HTTP/1.1", strings.TrimPrefix(c.Mount, "/")))
w(fmt.Sprintf("Host: %s:%d", c.Host, c.Port))
w("Authorization: Basic " + auth)
w("Content-Type: audio/mpeg")
w("icy-metadata-version: 2.2")
w("icy-name: " + m.Name)
w("icy-genre: " + m.Genre)
w(fmt.Sprintf("icy-br: %d", m.Bitrate))
w(fmt.Sprintf("icy-pub: %d", m.Public))
if m.URL != "" { w("icy-url: " + m.URL) }
if m.StationID != "" { w("icy-meta-station-id: " + m.StationID) }
if m.ShowTitle != "" { w("icy-meta-show-title: " + m.ShowTitle) }
w(fmt.Sprintf("icy-meta-autodj: %d", m.AutoDJ))
if m.DJHandle != "" { w("icy-meta-dj-handle: " + m.DJHandle) }
if m.DJBio != "" { w("icy-meta-dj-bio: " + m.DJBio) }
if m.Artwork != "" { w("icy-meta-track-artwork: " + m.Artwork) }
if m.Album != "" { w("icy-meta-track-album: " + m.Album) }
if m.BPM > 0 { w(fmt.Sprintf("icy-meta-track-bpm: %d", m.BPM)) }
if m.Key != "" { w("icy-meta-track-key: " + m.Key) }
if m.MBID != "" { w("icy-meta-track-mbid: " + m.MBID) }
if m.ISRC != "" { w("icy-meta-track-isrc: " + m.ISRC) }
w("icy-meta-audio-codec: " + m.Codec)
w(fmt.Sprintf("icy-meta-samplerate: %d", m.SampleRate))
w(fmt.Sprintf("icy-meta-channels: %d", m.Channels))
w(fmt.Sprintf("icy-meta-loudness: %.1f", m.Loudness))
w("icy-meta-encoder: " + m.Encoder)
if m.Notice != "" { w("icy-meta-notice: " + m.Notice) }
w(fmt.Sprintf("icy-meta-nsfw: %d", m.NSFW))
if m.License != "" { w("icy-meta-license-type: " + m.License) }
sb.WriteString("\r\n")
return sb.String()
}
func (c *ICY2Client) Connect(m ICY2Meta) error {
conn, err := net.Dial("tcp", fmt.Sprintf("%s:%d", c.Host, c.Port))
if err != nil { return err }
c.conn = conn
_, err = fmt.Fprint(conn, c.buildRequest(m))
if err != nil { return err }
buf := make([]byte, 1024)
n, _ := conn.Read(buf)
if !strings.Contains(string(buf[:n]), "200") {
return fmt.Errorf("server rejected: %s", string(buf[:n]))
}
return nil
}
func (c *ICY2Client) Stream(data []byte) error {
_, err := c.conn.Write(data)
return err
}
func (c *ICY2Client) Close() { c.conn.Close() }
func main() {
client := &ICY2Client{
Host: "dnas.example.com", Port: 9443,
Mount: "/stream.mp3", Password: "sourcepass",
}
meta := ICY2Meta{
Name: "ChillZone FM", Genre: "Electronic", Bitrate: 320,
StationID: "chillzone-001", ShowTitle: "Late Night Mix",
DJHandle: "@djsynthwave", Codec: "mp3",
SampleRate: 44100, Channels: 2, Loudness: -14.0,
Encoder: "Go-ICY2/1.0", License: "pro-licensed",
}
if err := client.Connect(meta); err != nil {
fmt.Println("Error:", err); os.Exit(1)
}
defer client.Close()
fmt.Println("Connected. Streaming...")
// client.Stream(audioBytes)
}
JavaScript / Node.js Implementation
Useful for web-based broadcast dashboards, streaming automation bots, and webhook-based metadata pushers.
const net = require('net');
const fs = require('fs');
class ICY2Client {
constructor({ host, port, mount, password }) {
this.host = host;
this.port = port;
this.mount = mount.replace(/^\//, '');
this.password = password;
this.socket = null;
}
_auth() {
return Buffer.from(`source:${this.password}`).toString('base64');
}
_buildRequest(meta = {}) {
const lines = [
`PUT /${this.mount} HTTP/1.1`,
`Host: ${this.host}:${this.port}`,
`Authorization: Basic ${this._auth()}`,
`Content-Type: audio/mpeg`,
`icy-metadata-version: 2.2`,
`icy-name: ${meta.name || 'My Station'}`,
`icy-genre: ${meta.genre || 'Various'}`,
`icy-br: ${meta.bitrate || 128}`,
`icy-pub: ${meta.pub ?? 1}`,
];
if (meta.url) lines.push(`icy-url: ${meta.url}`);
if (meta.stationId) lines.push(`icy-meta-station-id: ${meta.stationId}`);
if (meta.showTitle) lines.push(`icy-meta-show-title: ${meta.showTitle}`);
lines.push(`icy-meta-autodj: ${meta.autodj ? 1 : 0}`);
if (meta.djHandle) lines.push(`icy-meta-dj-handle: ${meta.djHandle}`);
if (meta.djBio) lines.push(`icy-meta-dj-bio: ${meta.djBio}`);
if (meta.artwork) lines.push(`icy-meta-track-artwork: ${meta.artwork}`);
if (meta.album) lines.push(`icy-meta-track-album: ${meta.album}`);
if (meta.bpm) lines.push(`icy-meta-track-bpm: ${meta.bpm}`);
if (meta.key) lines.push(`icy-meta-track-key: ${meta.key}`);
if (meta.mbid) lines.push(`icy-meta-track-mbid: ${meta.mbid}`);
if (meta.isrc) lines.push(`icy-meta-track-isrc: ${meta.isrc}`);
lines.push(`icy-meta-audio-codec: ${meta.codec || 'mp3'}`);
lines.push(`icy-meta-samplerate: ${meta.sampleRate || 44100}`);
lines.push(`icy-meta-channels: ${meta.channels || 2}`);
lines.push(`icy-meta-loudness: ${meta.loudness ?? -14.0}`);
lines.push(`icy-meta-encoder: ${meta.encoder || 'Node-ICY2/1.0'}`);
if (meta.notice) lines.push(`icy-meta-notice: ${meta.notice}`);
if (meta.noticeUrl) lines.push(`icy-meta-notice-url: ${meta.noticeUrl}`);
if (meta.requestUrl) lines.push(`icy-meta-request-url: ${meta.requestUrl}`);
lines.push(`icy-meta-nsfw: ${meta.nsfw ? 1 : 0}`);
lines.push(`icy-meta-license-type: ${meta.licenseType || 'pro-licensed'}`);
return lines.join('\r\n') + '\r\n\r\n';
}
connect(meta) {
return new Promise((resolve, reject) => {
this.socket = net.createConnection(this.port, this.host, () => {
this.socket.write(this._buildRequest(meta));
});
this.socket.once('data', (data) => {
if (data.toString().includes('200')) resolve(this.socket);
else reject(new Error('Server rejected: ' + data.toString()));
});
this.socket.once('error', reject);
});
}
stream(buffer) {
if (this.socket) this.socket.write(buffer);
}
disconnect() {
if (this.socket) { this.socket.destroy(); this.socket = null; }
}
}
// Usage
const client = new ICY2Client({
host: 'dnas.example.com', port: 9443,
mount: '/stream.mp3', password: 'sourcepass'
});
client.connect({
name: 'ChillZone FM', genre: 'Electronic', bitrate: 320,
stationId: 'chillzone-001', showTitle: 'Late Night Mix',
djHandle: '@djsynthwave', codec: 'mp3',
encoder: 'Node-ICY2/1.0', licenseType: 'pro-licensed'
}).then(sock => {
console.log('Connected. Streaming...');
const stream = fs.createReadStream('audio.mp3', { highWaterMark: 4096 });
stream.on('data', chunk => client.stream(chunk));
stream.on('end', () => client.disconnect());
}).catch(console.error);
PHP Implementation
Suitable for web-based streaming control panels, CMS-driven station managers, and server-side metadata injection.
<?php
class ICY2Client {
private string $host;
private int $port;
private string $mount;
private string $password;
private mixed $socket = null;
public function __construct(string $host, int $port, string $mount, string $password) {
$this->host = $host;
$this->port = $port;
$this->mount = ltrim($mount, '/');
$this->password = $password;
}
private function auth(): string {
return base64_encode("source:{$this->password}");
}
private function buildRequest(array $meta): string {
$name = $meta['name'] ?? 'My Station';
$genre = $meta['genre'] ?? 'Various';
$bitrate = $meta['bitrate'] ?? 128;
$pub = $meta['pub'] ?? 1;
$codec = $meta['codec'] ?? 'mp3';
$sampleRate= $meta['sampleRate'] ?? 44100;
$channels = $meta['channels'] ?? 2;
$loudness = $meta['loudness'] ?? -14.0;
$encoder = $meta['encoder'] ?? 'PHP-ICY2/1.0';
$license = $meta['license'] ?? 'pro-licensed';
$nsfw = $meta['nsfw'] ?? 0;
$autodj = $meta['autodj'] ?? 0;
$req = "PUT /{$this->mount} HTTP/1.1\r\n";
$req .= "Host: {$this->host}:{$this->port}\r\n";
$req .= "Authorization: Basic {$this->auth()}\r\n";
$req .= "Content-Type: audio/mpeg\r\n";
$req .= "icy-metadata-version: 2.2\r\n";
$req .= "icy-name: {$name}\r\n";
$req .= "icy-genre: {$genre}\r\n";
$req .= "icy-br: {$bitrate}\r\n";
$req .= "icy-pub: {$pub}\r\n";
if (!empty($meta['url'])) $req .= "icy-url: {$meta['url']}\r\n";
if (!empty($meta['stationId'])) $req .= "icy-meta-station-id: {$meta['stationId']}\r\n";
if (!empty($meta['showTitle'])) $req .= "icy-meta-show-title: {$meta['showTitle']}\r\n";
$req .= "icy-meta-autodj: {$autodj}\r\n";
if (!empty($meta['djHandle'])) $req .= "icy-meta-dj-handle: {$meta['djHandle']}\r\n";
if (!empty($meta['djBio'])) $req .= "icy-meta-dj-bio: {$meta['djBio']}\r\n";
if (!empty($meta['artwork'])) $req .= "icy-meta-track-artwork: {$meta['artwork']}\r\n";
if (!empty($meta['album'])) $req .= "icy-meta-track-album: {$meta['album']}\r\n";
if (!empty($meta['bpm'])) $req .= "icy-meta-track-bpm: {$meta['bpm']}\r\n";
if (!empty($meta['key'])) $req .= "icy-meta-track-key: {$meta['key']}\r\n";
if (!empty($meta['mbid'])) $req .= "icy-meta-track-mbid: {$meta['mbid']}\r\n";
if (!empty($meta['isrc'])) $req .= "icy-meta-track-isrc: {$meta['isrc']}\r\n";
$req .= "icy-meta-audio-codec: {$codec}\r\n";
$req .= "icy-meta-samplerate: {$sampleRate}\r\n";
$req .= "icy-meta-channels: {$channels}\r\n";
$req .= "icy-meta-loudness: {$loudness}\r\n";
$req .= "icy-meta-encoder: {$encoder}\r\n";
if (!empty($meta['notice'])) $req .= "icy-meta-notice: {$meta['notice']}\r\n";
if (!empty($meta['noticeUrl'])) $req .= "icy-meta-notice-url: {$meta['noticeUrl']}\r\n";
$req .= "icy-meta-nsfw: {$nsfw}\r\n";
$req .= "icy-meta-license-type: {$license}\r\n";
$req .= "\r\n";
return $req;
}
public function connect(array $meta): bool {
$this->socket = fsockopen($this->host, $this->port, $errno, $errstr, 10);
if (!$this->socket) throw new RuntimeException("Connection failed: {$errstr} ({$errno})");
fwrite($this->socket, $this->buildRequest($meta));
$response = fgets($this->socket, 1024);
return str_contains($response, '200');
}
public function stream(string $data): void {
if ($this->socket) fwrite($this->socket, $data);
}
public function disconnect(): void {
if ($this->socket) { fclose($this->socket); $this->socket = null; }
}
}
// Usage
$client = new ICY2Client('dnas.example.com', 9443, '/stream.mp3', 'sourcepass');
if ($client->connect([
'name' => 'ChillZone FM',
'genre' => 'Electronic',
'bitrate' => 320,
'stationId' => 'chillzone-001',
'showTitle' => 'Late Night Mix',
'djHandle' => '@djsynthwave',
'codec' => 'mp3',
'encoder' => 'PHP-ICY2/1.0',
])) {
echo "Connected. Streaming...\n";
$fp = fopen('audio.mp3', 'rb');
while (!feof($fp)) $client->stream(fread($fp, 4096));
fclose($fp);
}
$client->disconnect();
Media Client Integration
The following notes cover how ICY-META v2.2 relates to common media players — both as source clients (broadcasting) and listener clients (receiving and parsing ICY headers and in-stream metadata).
Winamp
Winamp receives ICY streams as a listener. It parses the ICY 200 OK response headers and reads in-stream StreamTitle metadata blocks. For broadcasting, the SHOUTcast DSP and Edcast plugins act as source clients sending ICY 1.x headers. To add ICY2 support to a Winamp plugin, extend the source client handshake (see C implementation above) and parse icy-meta-track-artwork via HTTP for album art display.
/* Typical Winamp in_mp3 / in_http plugin header parsing pattern */
void parse_icy_response(const char *headers) {
const char *p;
if ((p = strstr(headers, "icy-name:")) != NULL)
strncpy(station_name, p + 9, sizeof(station_name));
if ((p = strstr(headers, "icy-genre:")) != NULL)
strncpy(station_genre, p + 10, sizeof(station_genre));
if ((p = strstr(headers, "icy-metaint:")) != NULL)
metaint = atoi(p + 12);
/* ICY2 v2.2 extensions */
if ((p = strstr(headers, "icy-meta-track-artwork:")) != NULL)
fetch_album_art(p + 23); /* fetch and display in Now Playing */
if ((p = strstr(headers, "icy-meta-show-title:")) != NULL)
strncpy(show_title, p + 20, sizeof(show_title));
}
/* Parse in-stream metadata block */
void parse_meta_block(const unsigned char *buf, int len) {
/* Find StreamTitle='...'; */
const char *start = strstr((char*)buf + 1, "StreamTitle='");
if (!start) return;
start += 13;
const char *end = strstr(start, "';");
if (!end) return;
int title_len = end - start;
char title[512] = {0};
strncpy(title, start, title_len);
update_now_playing(title); /* e.g., "Artist - Track Title" */
}
Mixxx
Mixxx is an open-source DJ application with built-in live broadcasting via Shoutcast/Icecast (DlgLivebroadcasting). To add ICY2 support, extend the ShoutConnection class in src/broadcast/ to include the icy-metadata-version: 2.2 header and additional ICY2 fields when connecting. Mixxx can also read BPM and key from its track database and map them to icy-meta-track-bpm and icy-meta-track-key.
/* In src/broadcast/shoutconnection.cpp — extend header build */
void ShoutConnection::buildHeaders() {
shout_set_name(m_pShout, m_settings.name().toUtf8().constData());
shout_set_genre(m_pShout, m_settings.genre().toUtf8().constData());
shout_set_url(m_pShout, m_settings.url().toUtf8().constData());
shout_set_public(m_pShout, m_settings.publicStream() ? 1 : 0);
/* ICY2 v2.2 extension headers */
shout_set_meta(m_pShout, "icy-metadata-version", "2.2");
shout_set_meta(m_pShout, "icy-meta-station-id",
m_settings.stationId().toUtf8().constData());
shout_set_meta(m_pShout, "icy-meta-autodj", "0");
shout_set_meta(m_pShout, "icy-meta-encoder", "Mixxx/2.5");
/* Map Mixxx track data to ICY2 track fields */
if (m_pCurrentTrack) {
double bpm = m_pCurrentTrack->getBpm();
QString key = m_pCurrentTrack->getKeyText();
shout_set_meta(m_pShout, "icy-meta-track-bpm",
QString::number((int)bpm).toUtf8().constData());
shout_set_meta(m_pShout, "icy-meta-track-key",
key.toUtf8().constData());
QString mbid = m_pCurrentTrack->getMusicBrainzRecordingId();
if (!mbid.isEmpty())
shout_set_meta(m_pShout, "icy-meta-track-mbid",
mbid.toUtf8().constData());
}
}
Audacious
Audacious is a Linux/Unix audio player with ICY stream support via the neon or aud_http input plugin. It parses ICY response headers and in-stream metadata. To consume ICY2 fields, extend the header parser in src/libaudcore/ or the input plugin to read icy-meta-track-artwork for album art and icy-meta-show-title for the Now Playing display.
/* Extend ne_request_dispatch callback for ICY2 fields */
static void header_callback(void *userdata, const char *name, const char *value) {
StreamInfo *info = (StreamInfo *)userdata;
if (!strcasecmp(name, "icy-name"))
info->station_name = g_strdup(value);
if (!strcasecmp(name, "icy-genre"))
info->genre = g_strdup(value);
if (!strcasecmp(name, "icy-metaint"))
info->metaint = atoi(value);
/* ICY2 v2.2 */
if (!strcasecmp(name, "icy-meta-track-artwork"))
audgui_pixbuf_request(value); /* fetch album art */
if (!strcasecmp(name, "icy-meta-show-title"))
info->show_title = g_strdup(value);
if (!strcasecmp(name, "icy-meta-dj-handle"))
info->dj_handle = g_strdup(value);
}
iTunes / Apple Music
iTunes and Apple Music support ICY streams through HTTP Live Streaming (HLS) and direct MPEG stream URLs. They read the icy-name and in-stream StreamTitle metadata. ICY2 v2.2 fields are not natively supported but can be consumed by a proxy layer or companion app that polls the server's JSON status endpoint (/status-json.xsl) and exposes metadata via native macOS/iOS MPNowPlayingInfoCenter APIs.
import MediaPlayer
func updateNowPlaying(from icy2Status: [String: Any]) {
var info = [String: Any]()
info[MPMediaItemPropertyTitle] = icy2Status["title"] as? String ?? ""
info[MPMediaItemPropertyArtist] = icy2Status["artist"] as? String ?? ""
info[MPMediaItemPropertyAlbumTitle] = icy2Status["icy2-track-album"] as? String ?? ""
info[MPNowPlayingInfoPropertyIsLiveStream] = true
if let artworkURL = icy2Status["icy2-track-artwork"] as? String,
let url = URL(string: artworkURL) {
URLSession.shared.dataTask(with: url) { data, _, _ in
if let data = data, let img = UIImage(data: data) {
info[MPMediaItemPropertyArtwork] =
MPMediaItemArtwork(boundsSize: img.size) { _ in img }
MPNowPlayingInfoCenter.default().nowPlayingInfo = info
}
}.resume()
} else {
MPNowPlayingInfoCenter.default().nowPlayingInfo = info
}
}
Windows Media Player
Windows Media Player supports Shoutcast/ICY streams natively, reading icy-name and StreamTitle metadata. For ICY2 integration in a WMP plugin (COM/DirectShow), parse the extended ICY2 response headers from the HTTP source filter and forward them to a companion UI component or system tray overlay.
#include <winhttp.h>
#pragma comment(lib, "winhttp.lib")
struct ICY2Info {
std::wstring stationName, genre, showTitle, djHandle, artwork;
int metaint = 0;
};
ICY2Info ParseICY2Headers(HINTERNET hRequest) {
ICY2Info info;
WCHAR buf[1024]; DWORD len;
auto getHeader = [&](const WCHAR *name) -> std::wstring {
len = sizeof(buf);
if (WinHttpQueryHeaders(hRequest, WINHTTP_QUERY_CUSTOM,
name, buf, &len, NULL))
return std::wstring(buf, len / sizeof(WCHAR));
return {};
};
info.stationName = getHeader(L"icy-name");
info.genre = getHeader(L"icy-genre");
std::wstring mi = getHeader(L"icy-metaint");
if (!mi.empty()) info.metaint = _wtoi(mi.c_str());
/* ICY2 v2.2 extensions */
info.showTitle = getHeader(L"icy-meta-show-title");
info.djHandle = getHeader(L"icy-meta-dj-handle");
info.artwork = getHeader(L"icy-meta-track-artwork");
return info;
}
Changelog
| Version | Date | Changes |
|---|---|---|
| 2.2 | Feb 2026 | Added track metadata (artwork, album, BPM, key, MBID, ISRC), show scheduling, DJ bio/genre/rating, audio technical (codec, samplerate, channels, loudness, encoder), full video fields (poster, channel, duration, start, live, codec, fps, rating, nsfw), social (twitch, youtube, facebook, linkedin, creator-handle), listener engagement (request, chat, tip, events), broadcast distribution (crosspost-platforms, session-id, cdn-region, relay-origin), station notices, content licensing, PKI fields (certissuer, cert-rootca, certificate, ssh-pubkey). Renamed prefix to icy-meta-. v2.1 icy- prefix aliases retained. |
| 2.1 | Feb 15, 2026 | Initial ICY2 release. Station ID, podcast metadata, video type/link/platform/resolution, DJ handle, social handles (twitter, ig, tiktok), emoji, hashtags, NSFW, AI-generated, geo-region, certificate-verify, verification-status. |
| 1.x | 1999+ | Original SHOUTcast ICY protocol. icy-name, icy-genre, icy-url, icy-pub, icy-br, icy-metaint, StreamTitle in-stream blocks. Preserved unchanged. |
CasterClub Streaming Standards Initiative (CSSI)
Contact: specs@casterclub.com |
Web: casterclub.com/specs/icy-2
License: Open Specification / Attribution Preferred
Implementation: Mcaster1DNAS — GNU GPL v2