[PR] libcurl http: Add support for ICY metadata (PR #24302)
Romain Beauxis via ffmpeg-devel <[email protected]>
| Newsgroups | gmane.comp.video.ffmpeg.devel |
|---|---|
| Message-ID | <[email protected]> |
PR #24302 opened by Romain Beauxis (toots) URL: https://code.ffmpeg.org/FFmpeg/FFmpeg/pulls/24302 Patch URL: https://code.ffmpeg.org/FFmpeg/FFmpeg/pulls/24302.patch This PR adds support for ICY metadata updates. These are in-band, admittedly hacky ad-hoc metadata updates sent by icecast and shoutcast servers to update metadata while streaming for formats that don't support them natively, mostly mp3 and aac (adts) but potentially any format declaring the proper header. Functionality is offered by the client via an explicit opt-in header and confirmed by the server with a byte interval. Once confirmed, metadata are inserted after each byte interval (potentially with no update). The metadata payload is not properly documented and obsolete. It contains a full stream title and url with no explicit generic label/value pairing. It also used to be expected to be latin1. Most clients send utf8 now but this is not guaranteed. Nonetheless, this is an important feature when reading popular icecast/shoutcast streams. Bonus: support for shoutcast's legacy `ICY` request responses are also implemented via an opt-in parameter. Such responses use a non-standard `ICY 200 OK` response status. They were inadvertently supported in the native `http` parser due to a response status parsing bug. This was fixed and removed in https://code.ffmpeg.org/FFmpeg/FFmpeg/pulls/24232. `ICY` response status being non-standard and limited in use, it makes sense to keep it opt-in so that the `libcurl` http response parser is fully HTTP compliant by default. >From 40a65f87787d3df071a2508e5589d9037b8b578d Mon Sep 17 00:00:00 2001 From: Romain Beauxis <[email protected]> Date: Tue, 18 Aug 2026 16:58:13 -0500 Subject: [PATCH 1/7] avformat/http: move the ICY packet parser to http.h The libcurl protocol needs the same "Key='Value';" splitting, and http.h is already the seam it shares ff_http_averror() through. --- libavformat/http.c | 30 +----------------------------- libavformat/http.h | 33 +++++++++++++++++++++++++++++++++ 2 files changed, 34 insertions(+), 29 deletions(-) diff --git a/libavformat/http.c b/libavformat/http.c index fff0f25e36..36a1dafe06 100644 --- a/libavformat/http.c +++ b/libavformat/http.c @@ -1982,34 +1982,6 @@ static int http_read_stream_all(URLContext *h, uint8_t *buf, int size) return pos; } -static void update_metadata(URLContext *h, char *data) -{ - char *key; - char *val; - char *end; - char *next = data; - HTTPContext *s = h->priv_data; - - while (*next) { - key = next; - val = strstr(key, "='"); - if (!val) - break; - end = strstr(val, "';"); - if (!end) - break; - - *val = '\0'; - *end = '\0'; - val += 2; - - av_dict_set(&s->metadata, key, val, 0); - av_log(h, AV_LOG_VERBOSE, "Metadata update for %s: %s\n", key, val); - - next = end + 2; - } -} - static int store_icy(URLContext *h, int size) { HTTPContext *s = h->priv_data; @@ -2039,7 +2011,7 @@ static int store_icy(URLContext *h, int size) data[len] = 0; if ((ret = av_opt_set(s, "icy_metadata_packet", data, 0)) < 0) return ret; - update_metadata(h, data); + ff_http_parse_icy_packet(h, &s->metadata, data); } s->icy_data_read = 0; remaining = s->icy_metaint; diff --git a/libavformat/http.h b/libavformat/http.h index dd2c15789e..42979c910c 100644 --- a/libavformat/http.h +++ b/libavformat/http.h @@ -22,7 +22,11 @@ #ifndef AVFORMAT_HTTP_H #define AVFORMAT_HTTP_H +#include <string.h> + +#include "libavutil/dict.h" #include "libavutil/error.h" +#include "libavutil/log.h" #include "url.h" #define HTTP_HEADERS_SIZE 4096 @@ -48,6 +52,35 @@ typedef struct HTTPStatusLine { int ff_http_parse_status_line(void *logctx, const char *line, HTTPStatusLine *st); +/** + * Split an in-band ICY metadata packet into its "Key='Value';" pairs. + * + * @param logctx context used for logging, may be NULL + * @param metadata dictionary the pairs are stored in + * @param data NUL terminated packet, split in place + */ +static inline void ff_http_parse_icy_packet(void *logctx, + AVDictionary **metadata, char *data) +{ + char *next = data; + + while (*next) { + char *key = next, *val, *end; + + if (!(val = strstr(key, "='")) || !(end = strstr(val, "';"))) + break; + + *val = '\0'; + *end = '\0'; + val += 2; + + av_dict_set(metadata, key, val, 0); + av_log(logctx, AV_LOG_VERBOSE, "Metadata update for %s: %s\n", key, val); + + next = end + 2; + } +} + /** * Initialize the authentication state based on another HTTP URLContext. * This can be used to pre-initialize the authentication parameters if -- 2.52.0 >From 8a12afd140fbe3aba13f9ce9b6c97eb6bc663113 Mon Sep 17 00:00:00 2001 From: Romain Beauxis <[email protected]> Date: Tue, 18 Aug 2026 16:58:33 -0500 Subject: [PATCH 2/7] avformat/libcurl: unpause the transfer on every read exit path A read that returns no data still frees FIFO space, so evaluating the unpause condition only on the data path can leave a paused transfer stalled. --- libavformat/libcurl.c | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/libavformat/libcurl.c b/libavformat/libcurl.c index bb5fa0f002..30144ec6b3 100644 --- a/libavformat/libcurl.c +++ b/libavformat/libcurl.c @@ -1054,23 +1054,17 @@ static int libcurl_read(URLContext *h, unsigned char *buf, int size) { CurlContext *c = h->priv_data; int nonblock = h->flags & AVIO_FLAG_NONBLOCK; - int ret; + int unpause, ret; pthread_mutex_lock(&c->mutex); while (1) { size_t avail = av_fifo_can_read(c->fifo); if (avail) { - int n = FFMIN(avail, (size_t)size); - int unpause; - av_fifo_read(c->fifo, buf, n); - /* Resume a paused transfer once the FIFO is at least half empty. */ - unpause = c->paused && av_fifo_can_write(c->fifo) * 2 >= c->buffer_size; - c->logical_pos += n; - pthread_mutex_unlock(&c->mutex); - if (unpause) - curl_dispatch(c->loop, CMD_UNPAUSE, c, 0, 0); - return n; + ret = FFMIN(avail, (size_t)size); + av_fifo_read(c->fifo, buf, ret); + c->logical_pos += ret; + break; } if (c->error) { ret = c->error; @@ -1088,8 +1082,14 @@ static int libcurl_read(URLContext *h, unsigned char *buf, int size) /* Return to the avio layer so it can poll the interrupt callback. */ nonblock = 1; } + /* Resume a paused transfer once the FIFO is at least half empty, on every + * exit path since a read is not guaranteed to drain anything. */ + unpause = c->paused && av_fifo_can_write(c->fifo) * 2 >= c->buffer_size; pthread_mutex_unlock(&c->mutex); + if (unpause) + curl_dispatch(c->loop, CMD_UNPAUSE, c, 0, 0); + return ret; } -- 2.52.0 >From 99ba326c3e108f416ebde5540a8afa290fd12abf Mon Sep 17 00:00:00 2001 From: Romain Beauxis <[email protected]> Date: Tue, 18 Aug 2026 16:59:09 -0500 Subject: [PATCH 3/7] avformat/libcurl: request and export ICY response headers Send Icy-MetaData: 1 unless the user's headers option already sets it, and collect the reply's Icy-* headers into the icy_metadata_headers and metadata options, matching the native http protocol. The headers are staged per response block so that a redirect hop's Icy-* headers do not reach the caller, and committed before probed is set so that only the caller thread touches the exported metadata afterwards. --- libavformat/libcurl.c | 79 ++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 78 insertions(+), 1 deletion(-) diff --git a/libavformat/libcurl.c b/libavformat/libcurl.c index 30144ec6b3..8a7b09c5ce 100644 --- a/libavformat/libcurl.c +++ b/libavformat/libcurl.c @@ -29,6 +29,7 @@ #include "libavutil/avstring.h" #include "libavutil/bprint.h" +#include "libavutil/dict.h" #include "libavutil/error.h" #include "libavutil/fifo.h" #include "libavutil/log.h" @@ -51,6 +52,9 @@ * callback. */ #define CURL_WAIT_US 100000 +/* Cap on the accumulated "Icy-*" reply headers exported to the caller. */ +#define ICY_MAX_HEADERS 65536 + typedef struct CurlContext CurlContext; enum cmd_kind { @@ -121,6 +125,9 @@ struct CurlContext { int64_t request_size; int64_t initial_request_size; int max_retries; + int icy; + char *icy_metadata_headers; /* "Icy-*" reply headers (output) */ + AVDictionary *metadata; /* ICY metadata (output) */ int64_t logical_pos; /* next byte url_read() will return, caller side */ @@ -138,6 +145,7 @@ struct CurlContext { int64_t hdr_content_start; /* inclusive start, or -1 */ int64_t hdr_content_end; /* inclusive end, or -1 */ int64_t hdr_content_total; /* if known, or -1 */ + AVDictionary *hdr_icy; /* "Icy-*" headers of this block */ /* Probe result. Set by the loop thread, read by url_open() once probed. */ int probed; @@ -256,6 +264,55 @@ static void parse_content_range(CurlContext *c, const char *v) c->hdr_content_total = parse_offset(slash + 1); } +/* Store one "Tag: value" reply header, tolerating a missing space after the + * colon and the CRLF curl leaves on the line. */ +static void store_icy_header(AVDictionary **dict, const char *ptr, size_t len) +{ + const char *colon = memchr(ptr, ':', len); + const char *val, *end = ptr + len; + char *key, *value; + + if (!colon) + return; + + val = colon + 1; + while (val < end && av_isspace(*val)) + val++; + while (end > val && (end[-1] == '\r' || end[-1] == '\n')) + end--; + + key = av_strndup(ptr, colon - ptr); + value = av_strndup(val, end - val); + /* Multikey keeps repeated headers in arrival order, as http.c reports them. */ + if (key && value) + av_dict_set(dict, key, value, AV_DICT_DONT_STRDUP_KEY | + AV_DICT_DONT_STRDUP_VAL | AV_DICT_MULTIKEY); + else { + av_free(key); + av_free(value); + } +} + +/* Export the reply's "Icy-*" headers. Called once, before probed is set, so + * that from then on only the caller thread touches the exported metadata. */ +static void commit_icy_headers(CurlContext *c) +{ + const AVDictionaryEntry *e = NULL; + AVBPrint bp; + + if (!c->hdr_icy) + return; + + av_bprint_init(&bp, 0, ICY_MAX_HEADERS); + while ((e = av_dict_iterate(c->hdr_icy, e))) + av_bprintf(&bp, "%s: %s\n", e->key, e->value); + + av_freep(&c->icy_metadata_headers); + if (av_bprint_finalize(&bp, &c->icy_metadata_headers) < 0) + c->icy_metadata_headers = NULL; + av_dict_copy(&c->metadata, c->hdr_icy, 0); +} + static size_t header_callback(char *ptr, size_t size, size_t nitems, void *userdata) { CurlContext *c = userdata; @@ -269,6 +326,7 @@ static size_t header_callback(char *ptr, size_t size, size_t nitems, void *userd c->hdr_content_start = -1; c->hdr_content_end = -1; c->hdr_content_total = -1; + av_dict_free(&c->hdr_icy); return len; } if (av_strncasecmp(ptr, "Accept-Ranges:", 14) == 0) { @@ -283,6 +341,12 @@ static size_t header_callback(char *ptr, size_t size, size_t nitems, void *userd parse_content_range(c, ptr + 14); return len; } + /* Collected per block so that headers from a redirect hop, which the + * interim early return below discards, do not reach the caller. */ + if (av_strncasecmp(ptr, "icy-", 4) == 0) { + store_icy_header(&c->hdr_icy, ptr, len); + return len; + } /* Otherwise act only on the blank line that terminates the header block. */ while (n && (ptr[n - 1] == '\r' || ptr[n - 1] == '\n')) @@ -329,6 +393,7 @@ static size_t header_callback(char *ptr, size_t size, size_t nitems, void *userd c->location = dup; } } + commit_icy_headers(c); } /* A compressed body is addressed in encoded form, so byte offsets are * meaningless: not seekable. Note that we prefer compression over @@ -801,6 +866,7 @@ static int debug_callback(CURL *easy, curl_infotype type, char *data, static struct curl_slist *build_headers(CurlContext *c) { struct curl_slist *list = NULL; + int user_set_icy = 0; if (c->referer && c->referer[0]) { char *h = av_asprintf("Referer: %s", c->referer); @@ -814,11 +880,18 @@ static struct curl_slist *build_headers(CurlContext *c) char *line, *saveptr = NULL; if (copy) { for (line = av_strtok(copy, "\r\n", &saveptr); line; - line = av_strtok(NULL, "\r\n", &saveptr)) + line = av_strtok(NULL, "\r\n", &saveptr)) { + if (!av_strncasecmp(line, "Icy-MetaData:", 13)) + user_set_icy = 1; list = curl_slist_append(list, line); + } av_free(copy); } } + /* libcurl does not deduplicate the list, so only add ours if the user + * did not already ask for one. */ + if (c->icy && !user_set_icy) + list = curl_slist_append(list, "Icy-MetaData: 1"); return list; } @@ -1161,6 +1234,7 @@ static int libcurl_close(URLContext *h) if (c->header_list) curl_slist_free_all(c->header_list); + av_dict_free(&c->hdr_icy); av_fifo_freep2(&c->fifo); pthread_cond_destroy(&c->cond); pthread_mutex_destroy(&c->mutex); @@ -1189,6 +1263,9 @@ static const AVOption options[] = { { "max_redirects", "maximum number of redirects to follow", OFFSET(max_redirects), AV_OPT_TYPE_INT, { .i64 = 16 }, 0, INT_MAX, D }, { "multiple_requests", "reuse the connection across requests (HTTP keep-alive)", OFFSET(multiple_requests), AV_OPT_TYPE_BOOL, { .i64 = 1 }, 0, 1, D | E }, { "max_retries", "maximum number of retries after a recoverable error", OFFSET(max_retries), AV_OPT_TYPE_INT, { .i64 = 5 }, 0, INT_MAX, D }, + { "icy", "request ICY metadata", OFFSET(icy), AV_OPT_TYPE_BOOL, { .i64 = 1 }, 0, 1, D }, + { "icy_metadata_headers", "return ICY metadata headers", OFFSET(icy_metadata_headers), AV_OPT_TYPE_STRING, { .str = NULL }, 0, 0, AV_OPT_FLAG_EXPORT }, + { "metadata", "metadata read from the bitstream", OFFSET(metadata), AV_OPT_TYPE_DICT, {0}, 0, 0, AV_OPT_FLAG_EXPORT }, { "buffer_size", "receive buffer size in bytes", OFFSET(buffer_size), AV_OPT_TYPE_INT64, { .i64 = CURL_DEFAULT_BUFFER_SIZE }, CURL_MAX_WRITE_SIZE, INT_MAX, D }, { "request_size", "split a transfer into ranged requests of at most this many bytes (0 = unlimited)", OFFSET(request_size), AV_OPT_TYPE_INT64, { .i64 = 0 }, 0, INT64_MAX, D }, { "initial_request_size", "size (in bytes) of initial requests made during probing / header parsing", OFFSET(initial_request_size), AV_OPT_TYPE_INT64, { .i64 = 0 }, 0, INT64_MAX, D }, -- 2.52.0 >From ffe7c1fb6c066009cdf650f7efdfb0f528237155 Mon Sep 17 00:00:00 2001 From: Romain Beauxis <[email protected]> Date: Tue, 18 Aug 2026 17:00:06 -0500 Subject: [PATCH 4/7] avformat/libcurl: de-interleave in-band ICY metadata Strip the metadata blocks a server interleaves every icy-metaint bytes and export each one through icy_metadata_packet and the metadata dictionary. The block is accumulated across reads rather than waited for in one piece: a read cannot block, and refusing to drain a partially buffered block can leave too little room for the next write, pausing the transfer for good. A reply carrying in-band metadata is forced non-seekable, since its byte offsets no longer address the payload. That also keeps on_done() from restarting a request without draining the FIFO, which would desync the interleave counter. --- libavformat/libcurl.c | 140 +++++++++++++++++++++++++++++++++++++++++- 1 file changed, 138 insertions(+), 2 deletions(-) diff --git a/libavformat/libcurl.c b/libavformat/libcurl.c index 8a7b09c5ce..08eafe6c7f 100644 --- a/libavformat/libcurl.c +++ b/libavformat/libcurl.c @@ -55,6 +55,9 @@ /* Cap on the accumulated "Icy-*" reply headers exported to the caller. */ #define ICY_MAX_HEADERS 65536 +/* Largest in-band metadata block: the length byte counts 16 byte units. */ +#define ICY_MAX_BLOCK (255 * 16) + typedef struct CurlContext CurlContext; enum cmd_kind { @@ -127,9 +130,14 @@ struct CurlContext { int max_retries; int icy; char *icy_metadata_headers; /* "Icy-*" reply headers (output) */ + char *icy_metadata_packet; /* last in-band block (output) */ AVDictionary *metadata; /* ICY metadata (output) */ int64_t logical_pos; /* next byte url_read() will return, caller side */ + int64_t icy_data_read; /* payload bytes since the last block, caller side */ + int icy_block_len; /* -1 while the length byte is pending */ + int icy_block_filled; + uint8_t icy_block[ICY_MAX_BLOCK + 1]; /* Producer bookkeeping, touched only by the loop thread. */ int active; /* currently added to the multi */ @@ -146,6 +154,7 @@ struct CurlContext { int64_t hdr_content_end; /* inclusive end, or -1 */ int64_t hdr_content_total; /* if known, or -1 */ AVDictionary *hdr_icy; /* "Icy-*" headers of this block */ + int64_t hdr_icy_metaint; /* in-band metadata interval, or -1 */ /* Probe result. Set by the loop thread, read by url_open() once probed. */ int probed; @@ -161,6 +170,7 @@ struct CurlContext { int eof; /* producer delivered all data */ int error; /* AVERROR for an unrecoverable failure, or 0 */ int aborted; /* transfer should stop (open was interrupted) */ + int64_t icy_metaint; /* in-band metadata interval, 0 if none */ }; /* Guards lazy creation of a format context's shared loop. */ @@ -264,6 +274,26 @@ static void parse_content_range(CurlContext *c, const char *v) c->hdr_content_total = parse_offset(slash + 1); } +/* Parse a decimal header value, bounded by len since curl does not promise a + * NUL terminated header buffer. Returns -1 if absent, malformed or too large. */ +static int64_t parse_metaint(const char *p, size_t len) +{ + int64_t v = 0; + size_t i = 0; + + while (i < len && av_isspace(p[i])) + i++; + if (i == len || !av_isdigit(p[i])) + return -1; + + for (; i < len && av_isdigit(p[i]); i++) { + if (v > (INT_MAX - (p[i] - '0')) / 10) + return -1; + v = v * 10 + (p[i] - '0'); + } + return v; +} + /* Store one "Tag: value" reply header, tolerating a missing space after the * colon and the CRLF curl leaves on the line. */ static void store_icy_header(AVDictionary **dict, const char *ptr, size_t len) @@ -326,6 +356,7 @@ static size_t header_callback(char *ptr, size_t size, size_t nitems, void *userd c->hdr_content_start = -1; c->hdr_content_end = -1; c->hdr_content_total = -1; + c->hdr_icy_metaint = -1; av_dict_free(&c->hdr_icy); return len; } @@ -341,6 +372,10 @@ static size_t header_callback(char *ptr, size_t size, size_t nitems, void *userd parse_content_range(c, ptr + 14); return len; } + if (av_strncasecmp(ptr, "icy-metaint:", 12) == 0) { + c->hdr_icy_metaint = parse_metaint(ptr + 12, len - 12); + return len; + } /* Collected per block so that headers from a redirect hop, which the * interim early return below discards, do not reach the caller. */ if (av_strncasecmp(ptr, "icy-", 4) == 0) { @@ -423,6 +458,26 @@ static size_t header_callback(char *ptr, size_t size, size_t nitems, void *userd * follow-up reply doesn't clobber it. */ if (c->seekable_opt >= 0) c->seekable = c->seekable_opt; + + c->icy_metaint = 0; + if (c->hdr_icy_metaint > 0) { + if (c->hdr_compressed) { + av_log(c->h, AV_LOG_WARNING, "Ignoring icy-metaint on a reply " + "with a Content-Encoding, the combination is not " + "supported\n"); + } else { + c->icy_metaint = c->hdr_icy_metaint; + if (c->seekable_opt > 0) + av_log(c->h, AV_LOG_WARNING, "Ignoring seekable=1, the " + "reply carries in-band ICY metadata\n"); + /* In-band metadata makes byte offsets meaningless. Staying + * non-seekable also keeps on_done() from restarting a request + * without draining the FIFO, which would desync the + * interleave. */ + c->seekable = 0; + c->content_size = -1; + } + } } else { c->stream_ok = 0; if (!c->error) @@ -1067,6 +1122,7 @@ static int libcurl_open(URLContext *h, const char *url, int flags, c->request_end = -1; c->logical_pos = c->off; c->is_initial = 1; + c->icy_block_len = -1; /* Report the request URL until header_callback replaces it post-redirect. */ av_strstart(eff_url, "libcurl:", &eff_url); @@ -1123,6 +1179,55 @@ fail: return ret; } +/* Export the metadata block, which the packet parser splits in place. */ +static int update_icy_metadata(CurlContext *c) +{ + av_freep(&c->icy_metadata_packet); + c->icy_metadata_packet = av_strdup((char *)c->icy_block); + if (!c->icy_metadata_packet) + return AVERROR(ENOMEM); + + ff_http_parse_icy_packet(c->h, &c->metadata, (char *)c->icy_block); + return 0; +} + +/* Consume the metadata block at the current interleave boundary, with the + * mutex held. Returns 1 once a block is complete and ready to export. */ +static int drain_icy_block(CurlContext *c) +{ + size_t avail; + + while ((avail = av_fifo_can_read(c->fifo))) { + int n; + + if (c->icy_block_len < 0) { + uint8_t units; + av_fifo_read(c->fifo, &units, 1); + c->icy_block_len = units * 16; + c->icy_block_filled = 0; + /* A zero length byte means the metadata did not change. */ + if (!c->icy_block_len) { + c->icy_block_len = -1; + c->icy_data_read = 0; + return 0; + } + continue; + } + + n = FFMIN(avail, (size_t)(c->icy_block_len - c->icy_block_filled)); + av_fifo_read(c->fifo, c->icy_block + c->icy_block_filled, n); + c->icy_block_filled += n; + if (c->icy_block_filled < c->icy_block_len) + return 0; + + c->icy_block[c->icy_block_len] = 0; + c->icy_block_len = -1; + c->icy_data_read = 0; + return 1; + } + return 0; +} + static int libcurl_read(URLContext *h, unsigned char *buf, int size) { CurlContext *c = h->priv_data; @@ -1131,12 +1236,34 @@ static int libcurl_read(URLContext *h, unsigned char *buf, int size) pthread_mutex_lock(&c->mutex); while (1) { - size_t avail = av_fifo_can_read(c->fifo); + size_t avail; + + if (c->icy_metaint > 0) { + if (c->icy_data_read > c->icy_metaint) { + if (!c->error) + c->error = AVERROR_INVALIDDATA; + ret = c->error; + pthread_cond_broadcast(&c->cond); + break; + } + if (c->icy_data_read == c->icy_metaint && drain_icy_block(c) && + (ret = update_icy_metadata(c)) < 0) { + if (!c->error) + c->error = ret; + pthread_cond_broadcast(&c->cond); + break; + } + } + + avail = av_fifo_can_read(c->fifo); + if (c->icy_metaint > 0) + avail = FFMIN(avail, (size_t)(c->icy_metaint - c->icy_data_read)); if (avail) { ret = FFMIN(avail, (size_t)size); av_fifo_read(c->fifo, buf, ret); - c->logical_pos += ret; + c->icy_data_read += ret; + c->logical_pos += ret; break; } if (c->error) { @@ -1144,6 +1271,9 @@ static int libcurl_read(URLContext *h, unsigned char *buf, int size) break; } if (c->eof) { + if (c->icy_block_len >= 0) + av_log(h, AV_LOG_WARNING, + "Stream ended inside an ICY metadata block\n"); ret = AVERROR_EOF; break; } @@ -1211,6 +1341,11 @@ static int64_t libcurl_seek(URLContext *h, int64_t pos, int whence) * surfaces on the following url_read(). */ curl_dispatch(c->loop, CMD_SEEK, c, newpos, 1); c->logical_pos = newpos; + /* The new reply restarts its interleave, and the sync dispatch above has + * already drained the FIFO of everything framed by the old one. */ + c->icy_data_read = 0; + c->icy_block_len = -1; + c->icy_block_filled = 0; return newpos; } @@ -1265,6 +1400,7 @@ static const AVOption options[] = { { "max_retries", "maximum number of retries after a recoverable error", OFFSET(max_retries), AV_OPT_TYPE_INT, { .i64 = 5 }, 0, INT_MAX, D }, { "icy", "request ICY metadata", OFFSET(icy), AV_OPT_TYPE_BOOL, { .i64 = 1 }, 0, 1, D }, { "icy_metadata_headers", "return ICY metadata headers", OFFSET(icy_metadata_headers), AV_OPT_TYPE_STRING, { .str = NULL }, 0, 0, AV_OPT_FLAG_EXPORT }, + { "icy_metadata_packet", "return current ICY metadata packet", OFFSET(icy_metadata_packet), AV_OPT_TYPE_STRING, { .str = NULL }, 0, 0, AV_OPT_FLAG_EXPORT }, { "metadata", "metadata read from the bitstream", OFFSET(metadata), AV_OPT_TYPE_DICT, {0}, 0, 0, AV_OPT_FLAG_EXPORT }, { "buffer_size", "receive buffer size in bytes", OFFSET(buffer_size), AV_OPT_TYPE_INT64, { .i64 = CURL_DEFAULT_BUFFER_SIZE }, CURL_MAX_WRITE_SIZE, INT_MAX, D }, { "request_size", "split a transfer into ranged requests of at most this many bytes (0 = unlimited)", OFFSET(request_size), AV_OPT_TYPE_INT64, { .i64 = 0 }, 0, INT64_MAX, D }, -- 2.52.0 >From 73b6135207dab5f6160ecb3dee48f506f9602673 Mon Sep 17 00:00:00 2001 From: Romain Beauxis <[email protected]> Date: Tue, 18 Aug 2026 17:00:18 -0500 Subject: [PATCH 5/7] avformat/libcurl: reset the response header scratch on any status line Keying the reset on a literal "HTTP/" prefix cannot recognise a status line of another syntax, leaving the scratch of the previous reply in place. Track the header block boundary instead. --- libavformat/libcurl.c | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/libavformat/libcurl.c b/libavformat/libcurl.c index 08eafe6c7f..fb2d0c1702 100644 --- a/libavformat/libcurl.c +++ b/libavformat/libcurl.c @@ -141,6 +141,7 @@ struct CurlContext { /* Producer bookkeeping, touched only by the loop thread. */ int active; /* currently added to the multi */ + int hdr_in_block; /* a status line has been seen */ int64_t request_start; /* absolute offset the current request began at */ int64_t request_received;/* bytes delivered in the current request */ int64_t request_end; /* expected end of request, or -1 if unknown */ @@ -350,7 +351,9 @@ static size_t header_callback(char *ptr, size_t size, size_t nitems, void *userd size_t n = len; long status = 0; - if (av_strncasecmp(ptr, "HTTP/", 5) == 0) { + /* The first line of a block is its status line, whatever its syntax. */ + if (!c->hdr_in_block) { + c->hdr_in_block = 1; c->hdr_accept_ranges = 0; c->hdr_compressed = 0; c->hdr_content_start = -1; @@ -388,6 +391,7 @@ static size_t header_callback(char *ptr, size_t size, size_t nitems, void *userd n--; if (n) return len; + c->hdr_in_block = 0; curl_easy_getinfo(c->easy, CURLINFO_RESPONSE_CODE, &status); @@ -531,6 +535,8 @@ static void start_request(CurlContext *c) c->request_received = 0; c->request_end = -1; c->active = 1; + /* A transfer that died mid-header-block leaves this set. */ + c->hdr_in_block = 0; CURLMcode res = curl_multi_add_handle(c->loop->multi, c->easy); if (res != CURLM_OK) { av_log(c->h, AV_LOG_ERROR, "curl_multi_add_handle: %s\n", -- 2.52.0 >From 4e2c3a67a2478b088aff4e9917a490e0ebe2e9a1 Mon Sep 17 00:00:00 2001 From: Romain Beauxis <[email protected]> Date: Tue, 18 Aug 2026 17:00:39 -0500 Subject: [PATCH 6/7] avformat/libcurl: optionally accept ICY status lines Shoutcast v1 answers "ICY 200 OK", which curl treats as HTTP/0.9 and rejects before any header reaches the callback. CURLOPT_HTTP200ALIASES makes curl accept it. The alias turns any reply whose status line starts with "ICY" into a 200, so it is gated behind the new icy_status option, off by default. --- libavformat/libcurl.c | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/libavformat/libcurl.c b/libavformat/libcurl.c index fb2d0c1702..7edac5b4f9 100644 --- a/libavformat/libcurl.c +++ b/libavformat/libcurl.c @@ -105,6 +105,7 @@ struct CurlContext { int private_loop; /* loop is owned by this context (not shared) */ CURL *easy; struct curl_slist *header_list; + struct curl_slist *alias_list; /* AVOptions. */ char *user_agent; @@ -129,6 +130,7 @@ struct CurlContext { int64_t initial_request_size; int max_retries; int icy; + int icy_status; char *icy_metadata_headers; /* "Icy-*" reply headers (output) */ char *icy_metadata_packet; /* last in-band block (output) */ AVDictionary *metadata; /* ICY metadata (output) */ @@ -1072,6 +1074,15 @@ static void setup_curl(CurlContext *c) c->header_list = build_headers(c); if (c->header_list) curl_easy_setopt(e, CURLOPT_HTTPHEADER, c->header_list); + + /* Shoutcast v1 answers "ICY 200 OK", which curl would otherwise reject as + * HTTP/0.9 before any header reaches header_callback(). Opt-in, since it + * makes curl accept any reply whose status line starts with "ICY". */ + if (c->icy_status) { + c->alias_list = curl_slist_append(NULL, "ICY"); + if (c->alias_list) + curl_easy_setopt(e, CURLOPT_HTTP200ALIASES, c->alias_list); + } } static void curl_cond_wait(CurlContext *c) @@ -1375,6 +1386,8 @@ static int libcurl_close(URLContext *h) if (c->header_list) curl_slist_free_all(c->header_list); + if (c->alias_list) + curl_slist_free_all(c->alias_list); av_dict_free(&c->hdr_icy); av_fifo_freep2(&c->fifo); pthread_cond_destroy(&c->cond); @@ -1405,6 +1418,7 @@ static const AVOption options[] = { { "multiple_requests", "reuse the connection across requests (HTTP keep-alive)", OFFSET(multiple_requests), AV_OPT_TYPE_BOOL, { .i64 = 1 }, 0, 1, D | E }, { "max_retries", "maximum number of retries after a recoverable error", OFFSET(max_retries), AV_OPT_TYPE_INT, { .i64 = 5 }, 0, INT_MAX, D }, { "icy", "request ICY metadata", OFFSET(icy), AV_OPT_TYPE_BOOL, { .i64 = 1 }, 0, 1, D }, + { "icy_status", "accept legacy Shoutcast \"ICY 200 OK\" status lines", OFFSET(icy_status), AV_OPT_TYPE_BOOL, { .i64 = 0 }, 0, 1, D }, { "icy_metadata_headers", "return ICY metadata headers", OFFSET(icy_metadata_headers), AV_OPT_TYPE_STRING, { .str = NULL }, 0, 0, AV_OPT_FLAG_EXPORT }, { "icy_metadata_packet", "return current ICY metadata packet", OFFSET(icy_metadata_packet), AV_OPT_TYPE_STRING, { .str = NULL }, 0, 0, AV_OPT_FLAG_EXPORT }, { "metadata", "metadata read from the bitstream", OFFSET(metadata), AV_OPT_TYPE_DICT, {0}, 0, 0, AV_OPT_FLAG_EXPORT }, -- 2.52.0 >From b440e30c3aed408455af8dd4c84930bd3267bf07 Mon Sep 17 00:00:00 2001 From: Romain Beauxis <[email protected]> Date: Tue, 18 Aug 2026 17:00:39 -0500 Subject: [PATCH 7/7] doc/protocols: document the libcurl ICY options Also note where the libcurl protocol differs from the native one once in-band metadata is active. --- doc/protocols.texi | 33 +++++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/doc/protocols.texi b/doc/protocols.texi index f7e65b1c9d..7d8c0946a3 100644 --- a/doc/protocols.texi +++ b/doc/protocols.texi @@ -1169,8 +1169,41 @@ by default. @item max_retries Maximum number of retries after a recoverable error on a seekable transfer. Default is @code{5}. + +@item icy +If set to 1 request ICY (SHOUTcast) metadata from the server. If the server +supports this, the metadata has to be retrieved by the application by reading +the @option{icy_metadata_headers} and @option{icy_metadata_packet} options. +The default is 1. + +@item icy_status +If set to 1 accept the legacy Shoutcast v1 @code{ICY 200 OK} status line in +place of an HTTP one. Any reply whose status line starts with @code{ICY} is +then treated as a successful HTTP 200, including error replies such as +@code{ICY 404}, so this is off by default. + +@item icy_metadata_headers +If the server supports ICY metadata, this contains the ICY-specific HTTP reply +headers, separated by newline characters. + +@item icy_metadata_packet +If the server supports ICY metadata, and @option{icy} was set to 1, this +contains the last non-empty metadata packet sent by the server. It should be +polled in regular intervals by applications interested in mid-stream metadata +updates. + +@item metadata +Set an exported dictionary containing Icecast metadata from the bitstream, if present. +Only useful with the C API. @end table +A stream carrying in-band ICY metadata is always treated as non-seekable, since +its byte offsets do not address the payload. @option{seekable}, +@option{request_size} and @option{max_retries} therefore have no effect on such +a stream. A reply that carries both a @code{Content-Encoding} and an +@code{icy-metaint} header is rejected as unsupported: the metadata is ignored +and the body is passed through unchanged. + For more information see: @url{https://curl.se/libcurl/}. @section libsmbclient -- 2.52.0 _______________________________________________ ffmpeg-devel mailing list -- [email protected] To unsubscribe send an email to [email protected]