This is an automated email from the git hooks/post-receive script.

Git pushed a commit to branch master
in repository ffmpeg.

commit 631d435b77512d8e353bd46a61ce9d83aa131c02
Author:     Romain Beauxis <[email protected]>
AuthorDate: Tue Aug 18 16:59:09 2026 -0500
Commit:     Romain Beauxis <[email protected]>
CommitDate: Fri Sep 25 16:04:08 2026 +0000

    avformat/libcurl: request, export and de-interleave ICY metadata
    
    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.
    
    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 | 206 +++++++++++++++++++++++++++++++++++++++++++++++++-
 1 file changed, 202 insertions(+), 4 deletions(-)

diff --git a/libavformat/libcurl.c b/libavformat/libcurl.c
index c4be36ff76..be8c4d2d97 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,12 @@
  * callback. */
 #define CURL_WAIT_US 100000
 
+/* 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 {
@@ -122,8 +129,16 @@ struct CurlContext {
     int64_t         initial_request_size;
     int64_t         short_seek_size;
     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 */
@@ -140,6 +155,8 @@ 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 */
+    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;
@@ -154,6 +171,7 @@ struct CurlContext {
     int             paused;      /* write callback paused, FIFO was full */
     int             status;      /* current stream status (AVERROR code) */
     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. */
@@ -262,6 +280,82 @@ 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');
+    }
+
+    // Reject 4junk
+    while (i < len && av_isspace(p[i]))
+        i++;
+    if (i != len)
+        return -1;
+
+    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)
+{
+    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;
@@ -275,6 +369,8 @@ 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;
     }
     if (av_strncasecmp(ptr, "Accept-Ranges:", 14) == 0) {
@@ -289,6 +385,16 @@ 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) {
+        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'))
@@ -336,6 +442,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
@@ -365,6 +472,20 @@ 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) {
+            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->loop->num_errors++;
         c->stream_ok = 0;
@@ -845,6 +966,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);
@@ -858,11 +980,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;
 }
 
@@ -1038,6 +1167,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);
@@ -1094,6 +1224,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;
@@ -1102,15 +1281,29 @@ 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;
+
+        /* A failed export loses one metadata update but leaves the interleave
+         * in sync, since the block is fully consumed either way. */
+        if (c->icy_metaint > 0 && c->icy_data_read == c->icy_metaint &&
+            drain_icy_block(c) && (ret = update_icy_metadata(c)) < 0)
+            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->status) {
+            if (c->status == AVERROR_EOF && c->icy_block_len >= 0)
+                av_log(h, AV_LOG_WARNING,
+                       "Stream ended inside an ICY metadata block\n");
             ret = c->status;
             break;
         }
@@ -1123,7 +1316,7 @@ static int libcurl_read(URLContext *h, unsigned char 
*buf, int size)
         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. */
+     * exit path because ICY framing can be drained without returning media. */
     unpause = c->paused && av_fifo_can_write(c->fifo) * 2 >= c->buffer_size;
     pthread_mutex_unlock(&c->mutex);
 
@@ -1201,6 +1394,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);
@@ -1237,6 +1431,10 @@ 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 },
+    { "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 },
     { "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 },

-- 
To stop receiving notification emails like this one, please contact
[email protected].
_______________________________________________
ffmpeg-cvslog mailing list -- [email protected]
To unsubscribe send an email to [email protected]

Reply via email to