ICY-META v2.2 Protocol Specification

CasterClub Streaming Standards Initiative (CSSI) — Mcaster1DNAS / February 2026

Version 2.2 Open Spec / CSSI Backwards Compatible ICY 1.x Preserved

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.

All ICY2 fields are optional. Legacy ICY 1.x fields (icy-name, icy-genre, etc.) are never changed, never removed, and always parsed independently of ICY2 logic.

Protocol History

VersionOriginStatusKey Feature
ICY 1.xNullsoft SHOUTcast (1999)LEGACYicy-name, icy-genre, in-stream metadata blocks
ICY2 v2.0Icecast-KH extensionsLEGACYExtended HTTP-based source connect
ICY2 v2.1Mcaster1DNAS / CSSI (Feb 15, 2026)SUPERSEDEDStation ID, podcast, video, social, content flags (icy- prefix)
ICY2 v2.2Mcaster1DNAS / CSSI (Feb 2026)CURRENTTrack 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

HTTP
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

HTTP
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

HeaderTypeDescription
icy-nameStringStation or stream display name
icy-genreStringGenre or content type (e.g., Electronic, Rock)
icy-urlURLStation homepage URL
icy-pubBooleanPublic directory listing — 1 = yes, 0 = no
icy-brIntegerBitrate in kbps (e.g., 128, 320)
icy-metaintIntegerBytes of audio between in-stream metadata blocks (e.g., 8192)

Authentication

FieldDescription
Authorization: BasicBase64-encoded source:password (Icecast2)
passwordSource password sent inline (legacy SHOUTcast)
adminpasswordAdmin interface password
userOptional 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 byteBlock length indicator N. Actual block size = N × 16 bytes. If N = 0, no metadata follows.
N × 16 bytesNull-padded UTF-8 string in the format: StreamTitle='Artist - Title';
Example metadata block content
# Block length byte = 4  (4 × 16 = 64 bytes total)
# Content (null-padded to 64 bytes):
StreamTitle='Daft Punk - Get Lucky';StreamUrl='';
Only 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:

HTTP
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.

HTTP
icy-metadata-version: 2.2

Station Identity

HeaderTypeDescription
icy-meta-station-idStringUnique global station ID — alphanumeric, hyphens allowed. Permanent across sessions.
icy-meta-station-logoURLStation logo or branding image URL
icy-meta-certissuer-idStringCertificate authority ID for verification
icy-meta-cert-rootcaStringRoot CA hash or fingerprint
icy-meta-certificateStringBase64-encoded PEM certificate
icy-meta-ssh-pubkeyStringSSH public key for source authentication
icy-meta-verification-statusEnumunverified | pending | verified | gold

Programming / Show Scheduling

HeaderTypeDescription
icy-meta-show-titleStringCurrent show or program title
icy-meta-show-startISO8601Datetime the current show started
icy-meta-show-endISO8601Datetime the current show ends
icy-meta-next-showStringTitle of the next scheduled program
icy-meta-next-show-timeISO8601Scheduled start time of the next program
icy-meta-schedule-urlURLLink to the full station program schedule
icy-meta-autodjBoolean1 = AutoDJ/automation active, 0 = live human DJ
icy-meta-playlist-nameStringCurrent playlist or automation source name

DJ / Host

HeaderTypeDescription
icy-meta-dj-handleStringCurrent DJ or host social handle (e.g., @djsynthwave)
icy-meta-dj-bioStringShort DJ biography or tagline — max 280 characters
icy-meta-dj-genreStringDJ's genre set — comma-separated, max 5 values
icy-meta-dj-showratingEnumall-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.

HeaderTypeDescription
icy-meta-track-artworkURLAlbum or track artwork image URL
icy-meta-track-albumStringAlbum or release name
icy-meta-track-yearIntegerRelease year
icy-meta-track-labelStringRecord label
icy-meta-track-bpmIntegerBeats per minute
icy-meta-track-keyStringMusical key — Camelot or standard notation (e.g., 8B, Am)
icy-meta-track-genreStringPer-track genre — may differ from station genre
icy-meta-track-mbidUUIDMusicBrainz Recording ID for direct track lookup
icy-meta-track-isrcStringInternational Standard Recording Code — royalty/licensing tracking

Podcast

HeaderTypeDescription
icy-meta-podcast-hostStringPodcast creator or host name
icy-meta-podcast-ratingEnumall-ages | teen | mature | explicit
icy-meta-podcast-rssURLPodcast RSS feed URL
icy-meta-podcast-episodeStringEpisode title or ID (e.g., S4E1 – Decentralized Rights)
icy-meta-durationIntegerContent runtime in seconds — applies to audio, podcast, or video
icy-meta-languageStringISO 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.

HeaderTypeDescription
icy-meta-audio-codecEnummp3 | aac | aac-he | ogg | opus | flac
icy-meta-samplerateIntegerSample rate in Hz (e.g., 44100, 48000)
icy-meta-channelsInteger1 = mono, 2 = stereo, 6 = 5.1 surround
icy-meta-loudnessFloatIntegrated loudness in LUFS per EBU R128 (e.g., -14.0)
icy-meta-encoderStringEncoder software and version (e.g., Mcaster1DSP/1.2.0, BUTT/1.40)

Video Streaming

HeaderTypeDescription
icy-meta-videotypeEnumlive | short | clip | trailer | ad
icy-meta-videoratingEnumall-ages | teen | mature | explicit
icy-meta-videolinkURLLink to the video content or stream page
icy-meta-videotitleStringTitle of the video
icy-meta-videoposterURLThumbnail or preview image URL
icy-meta-videochannelStringCreator/channel handle
icy-meta-videoplatformEnumyoutube | tiktok | twitch | kick | rumble | vimeo | custom
icy-meta-videostartISO8601Scheduled start datetime for the video
icy-meta-videoliveBoolean1 = currently live, 0 = pre-recorded
icy-meta-videocodecStringVideo codec in use (e.g., h264, vp9, av1)
icy-meta-videofpsIntegerFrames per second
icy-meta-videoresolutionStringe.g., 1080p, 4K, 720x1280
icy-meta-videonsfwBooleanVideo-specific NSFW indicator

Social, Discovery & Branding

HeaderTypeDescription
icy-meta-creator-handleStringPlatform-agnostic public creator or brand handle
icy-meta-social-twitterStringTwitter/X handle
icy-meta-social-twitchStringTwitch handle
icy-meta-social-igStringInstagram username
icy-meta-social-tiktokStringTikTok profile name
icy-meta-social-youtubeURLYouTube channel URL — static social presence link
icy-meta-social-facebook-pageURLFacebook page URL
icy-meta-social-linkedinURLLinkedIn profile URL
icy-meta-social-linktreeURLUnified profile link (Linktree, Beacons, etc.)
icy-meta-emojiStringMood or emotion indicators (e.g., 🎵🔥🎧)
icy-meta-hashtag-arrayJSON ArraySearchable tags (e.g., ["#electronic","#dj"])

Listener Engagement

HeaderTypeDescription
icy-meta-request-enabledBoolean1 = listener song requests currently open
icy-meta-request-urlURLURL for song requests or listener dedications
icy-meta-chat-urlURLLive listener chat room URL
icy-meta-tip-urlURLListener donation or tip URL (Ko-fi, Patreon, PayPal)
icy-meta-events-urlURLLink 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.

HeaderTypeDescription
icy-meta-crosspost-platformsStringComma-separated active live platforms (e.g., youtube,twitch,tiktok)
icy-meta-stream-session-idStringUnique ID for this broadcast session — distinct from permanent station-id
icy-meta-cdn-regionStringCDN or distribution region (e.g., us-east, eu-west)
icy-meta-relay-originURLOrigin 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.

HeaderTypeDescription
icy-meta-noticeStringGeneral listener notice or announcement text
icy-meta-notice-urlURLClick-through URL for more information
icy-meta-notice-expiresISO8601Datetime after which the notice should no longer display

Access, Authentication & Compliance

HeaderTypeDescription
icy-meta-auth-tokenJWTOptional Bearer JWT or custom access token
icy-meta-nsfwBoolean1 = explicit content — affects directory listings
icy-meta-ai-generatorBoolean1 = AI-generated or AI-assisted content
icy-meta-geo-regionStringTarget geographic region (e.g., US, EU, GLOBAL)
icy-meta-license-typeEnumcc-by | cc-by-sa | cc0 | pro-licensed | all-rights-reserved
icy-meta-royalty-freeBoolean1 = royalty-free content
icy-meta-license-territoryStringComma-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-idicy-meta-station-id
icy-podcast-hosticy-meta-podcast-host
icy-podcast-rssicy-meta-podcast-rss
icy-podcast-episodeicy-meta-podcast-episode
icy-durationicy-meta-duration
icy-languageicy-meta-language
icy-video-typeicy-meta-videotype
icy-video-linkicy-meta-videolink
icy-video-platformicy-meta-videoplatform
icy-dj-handleicy-meta-dj-handle
icy-social-twittericy-meta-social-twitter
icy-social-igicy-meta-social-ig
icy-social-tiktokicy-meta-social-tiktok
icy-emojiicy-meta-emoji
icy-hashtagsicy-meta-hashtag-array
icy-auth-tokenicy-meta-auth-token
icy-nsfwicy-meta-nsfw
icy-ai-generatedicy-meta-ai-generator
icy-geo-regionicy-meta-geo-region
icy-verification-statusicy-meta-verification-status

Use Cases & Examples

Live DJ Set

HTTP Headers
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

HTTP Headers
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

HTTP Headers
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)

HTTP Headers
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

HTTP Headers
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

These examples stream a local audio file. Replace audio-file.mp3, the server address, password, and mount as needed.

Basic ICY 1.x Legacy Test

bash
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

bash
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

Server log output
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.

C — ICY2 source client
#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.

Python 3
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.

Go
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.

Node.js
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 8.x
<?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.

C — Winamp in_mp3 plugin: parse icy response headers
/* 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.

C++ — Mixxx ShoutConnection extension sketch
/* 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.

C — Audacious neon plugin ICY2 header read
/* 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.

Swift — iOS/macOS Now Playing from ICY2 JSON status
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.

C++ — COM/WMP plugin ICY2 header parse via WinHTTP
#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

VersionDateChanges
2.2Feb 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.1Feb 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.x1999+ 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