[vlc-commits] [Git][videolan/vlc][master] 5 commits: vlc_stream: add STREAM_CAN_DOWNLOAD control query

François Cartegnie (@fcartegnie) gitlab at videolan.org
Sun Sep 6 15:10:28 UTC 2026



François Cartegnie pushed to branch master at VideoLAN / VLC


Commits:
1cabf72b by Ayush Dey at 2026-09-06T17:00:57+02:00
vlc_stream: add STREAM_CAN_DOWNLOAD control query

To identify access modules that will support file downloading
via the future LibVLC downloader API. This query is intended
for a limited set of modules where download capability is applicable.

- - - - -
8b8ec1c5 by Ayush Dey at 2026-09-06T17:00:57+02:00
lib: add libvlc_downloader API

Used to download a file readable by VLC over a limited
set of protocols: http(s), ftp, file, nfs, smb, sftp

Refs #27971

- - - - -
4df10d52 by Ayush Dey at 2026-09-06T17:00:57+02:00
input: add vlctest_ protocol

Add vlctest_ protocol as a file type. This will be required
in the next commit for using a dummy access module with the
libvlc_downloader.

- - - - -
1143b94d by Ayush Dey at 2026-09-06T17:00:57+02:00
test: add tests for libvlc_downloader

- - - - -
2769a335 by Ayush Dey at 2026-09-06T17:00:57+02:00
doc: add samples_libvlc_downloader

- - - - -


23 changed files:

- NEWS
- doc/Makefile.am
- + doc/libvlc/downloader.c
- include/meson.build
- + include/vlc/libvlc_downloader.h
- include/vlc/vlc.h
- include/vlc_stream.h
- lib/Makefile.am
- + lib/downloader.c
- lib/libvlc.sym
- lib/meson.build
- modules/access/dsm/access.c
- modules/access/file.c
- modules/access/ftp.c
- modules/access/http.c
- modules/access/http/access.c
- modules/access/nfs.c
- modules/access/samba.c
- modules/access/sftp.c
- modules/access/smb2.c
- src/input/item.c
- test/Makefile.am
- + test/libvlc/downloader.c


Changes:

=====================================
NEWS
=====================================
@@ -364,6 +364,7 @@ LibVLC:
    events, deprecated track APIs, deprecated service-discovery/log/audio/video
    stubs, AGL support and libvlc_wait()
  * Remove libvlc_media_player_set_abloop() in favor of explicit AB loop APIs
+ * Add libvlc_downloader API that provides a bitstream of media to client applications
 
 
 Changes between 3.0.23 and 3.0.24-beta1:


=====================================
doc/Makefile.am
=====================================
@@ -18,6 +18,7 @@ LIBVLC_SAMPLES = \
 	libvlc/win_player.c \
 	libvlc/CMakeLists.txt \
 	libvlc/appkit_player.m \
+	libvlc/downloader.c \
 	libvlc/parser.c \
 	libvlc/media_discoverer.c \
 	libvlc/renderer_discoverer.c \
@@ -30,6 +31,7 @@ AM_LDFLAGS = -no-install $(LDFLAGS_vlc) -L../src/ -lvlccore -L../lib -lvlc
 
 samples_libvlc_example_SOURCES = libvlc/example.c
 samples_libvlc_thumbnailer_SOURCES = libvlc/thumbnailer.c
+samples_libvlc_downloader_SOURCES = libvlc/downloader.c
 samples_libvlc_parser_SOURCES = libvlc/parser.c
 samples_libvlc_media_discoverer_SOURCES = libvlc/media_discoverer.c
 samples_libvlc_renderer_discoverer_SOURCES = libvlc/renderer_discoverer.c
@@ -38,6 +40,7 @@ samples_libvlc_player_SOURCES = libvlc/player.c
 check_PROGRAMS = \
 	samples_libvlc_example \
 	samples_libvlc_thumbnailer \
+	samples_libvlc_downloader \
 	samples_libvlc_parser \
 	samples_libvlc_media_discoverer \
 	samples_libvlc_renderer_discoverer \


=====================================
doc/libvlc/downloader.c
=====================================
@@ -0,0 +1,245 @@
+/*****************************************************************************
+ * downloader.c:  libvlc downloader API sample usage
+ *****************************************************************************
+ * Copyright (C) 2026 VLC authors and VideoLAN
+ *
+ * This program is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston MA 02110-1301, USA.
+ *****************************************************************************/
+
+/*
+ * Program usage:
+ *   Usage: <total number of media to download> <MRLtodownload1> [MRLtodownload2]...
+ *
+ * Example:
+ *   ./doc/samples_libvlc_downloader 2 https://example.org/a.mp4 file:///home/user/b.mp3
+ */
+
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <locale.h>
+#include <semaphore.h>
+#include <inttypes.h>
+
+#include <vlc/vlc.h>
+#include <vlc/libvlc_downloader.h>
+
+struct request_ctx
+{
+    int index;
+    sem_t *done_sem;
+};
+
+static const char *dl_status_to_string(libvlc_downloader_status_t st)
+{
+    switch (st)
+    {
+        case libvlc_downloader_status_pending:   return "pending";
+        case libvlc_downloader_status_running:   return "running";
+        case libvlc_downloader_status_paused:    return "paused";
+        case libvlc_downloader_status_finished:  return "finished";
+        case libvlc_downloader_status_cancelled: return "cancelled";
+        case libvlc_downloader_status_error:     return "error";
+        default: return "unknown";
+    }
+}
+
+static ptrdiff_t on_buffer(void *opaque, libvlc_downloader_task *task, const uint8_t *buf,
+                           size_t len, uint64_t position, uint64_t total)
+{
+    (void)buf;
+    struct request_ctx *ctx = (struct request_ctx *)opaque;
+    /* Buffer received for media. User can process or save 'buf' here. */
+    fprintf(stdout, "[req %d] received buffer: %zu bytes\n", ctx->index, len);
+    double pct = (total > 0) ? (100.0 * (double)position / (double)total) : 0.0;
+    fprintf(stdout, "[req %d, id=%p] progress: %" PRIu64 "/%" PRIu64 " (%.1f%%)\n",
+            ctx->index, task, position, total, pct);
+    fflush(stdout);
+    return (ptrdiff_t)len;
+}
+
+static void on_state_update(void *opaque, libvlc_downloader_task *task,
+                            libvlc_downloader_status_t status)
+{
+    struct request_ctx *ctx = (struct request_ctx *)opaque;
+    fprintf(stdout, "[req %d, id=%p] state: %s\n",
+            ctx->index, task, dl_status_to_string(status));
+    fflush(stdout);
+
+    if (status == libvlc_downloader_status_finished ||
+        status == libvlc_downloader_status_cancelled ||
+        status == libvlc_downloader_status_error)
+    {
+        sem_post(ctx->done_sem);
+        libvlc_downloader_task_release(task);
+    }
+}
+
+static void on_subitems(void *opaque, libvlc_downloader_task *task, libvlc_media_list_t *subitems)
+{
+    (void)task;
+    struct request_ctx *ctx = (struct request_ctx *)opaque;
+    int count = libvlc_media_list_count(subitems);
+    fprintf(stdout, "[req %d] media has %d subitems (download will not proceed for playlists/directories)\n",
+            ctx->index, count);
+    fflush(stdout);
+}
+
+static void on_slaves(void *opaque, libvlc_downloader_task *task, libvlc_media_slave_t **slaves, size_t count)
+{
+    (void)task;
+    struct request_ctx *ctx = (struct request_ctx *)opaque;
+    fprintf(stdout, "[req %d] media has %zu slave(s)\n", ctx->index, count);
+    (void)slaves;
+    fflush(stdout);
+}
+
+static libvlc_downloader_t *create_downloader(const struct libvlc_downloader_cfg *cfg)
+{
+    static const char* const argv[] = {
+        "-vvv",
+        "--vout=dummy",
+        "--aout=dummy",
+        "--text-renderer=dummy",
+    };
+    libvlc_instance_t *inst = libvlc_new((int)(sizeof argv / sizeof *argv), argv);
+    if (inst == NULL)
+        return NULL;
+
+    libvlc_downloader_t *downloader = libvlc_downloader_new(inst, cfg);
+    /* downloader retains the instance, we can release our reference */
+    libvlc_release(inst);
+    return downloader;
+}
+
+static void print_usage(const char *name)
+{
+    fprintf(stderr,
+            "Usage: %s <total number of media to download> <mediatodownload1> [mediatodownload2]...\n",
+            name);
+}
+
+int main(int argc, const char **argv)
+{
+    setlocale(LC_ALL, "");
+
+    if (argc < 2)
+    {
+        print_usage(argv[0]);
+        return EXIT_FAILURE;
+    }
+
+    char *endp = NULL;
+    long nlong = strtol(argv[1], &endp, 10);
+    if (endp == argv[1] || nlong <= 0)
+    {
+        print_usage(argv[0]);
+        return EXIT_FAILURE;
+    }
+    int total = (int)nlong;
+
+    /* expect: prog N media1 ... mediaN */
+    if (argc != 2 + total)
+    {
+        print_usage(argv[0]);
+        return EXIT_FAILURE;
+    }
+
+    const char **media_uris = &argv[2];
+
+    const struct libvlc_downloader_cfg cfg = {
+        .version = 0,
+        .max_parser_threads = 1,
+    };
+    libvlc_downloader_t *downloader = create_downloader(&cfg);
+    if (downloader == NULL)
+    {
+        fprintf(stderr, "Failed to create downloader\n");
+        return EXIT_FAILURE;
+    }
+
+    static const struct libvlc_downloader_cbs cbs = {
+        .version = 0,
+        .on_state_update = on_state_update,
+        .on_buffer = on_buffer,
+        .on_subitems = on_subitems,
+        .on_slaves = on_slaves,
+    };
+
+    sem_t done_sem;
+    sem_init(&done_sem, 0, 0);
+
+    struct request_ctx *ctxs = calloc((size_t)total, sizeof(*ctxs));
+    if (ctxs == NULL)
+    {
+        fprintf(stderr, "Failed to create context\n");
+        libvlc_downloader_destroy(downloader);
+        sem_destroy(&done_sem);
+        return EXIT_FAILURE;
+    }
+
+    int queued = 0;
+
+    for (int i = 0; i < total; ++i)
+    {
+        const char *url = media_uris[i];
+        libvlc_media_t *media = NULL;
+        if (strstr(url, "://") != NULL)
+            media = libvlc_media_new_location(url);
+        else
+            media = libvlc_media_new_path(url);
+
+        if (media == NULL)
+        {
+            fprintf(stderr, "[req %d] failed to create media for '%s'\n", i, url);
+            continue;
+        }
+
+        ctxs[i].index = i;
+        ctxs[i].done_sem = &done_sem;
+
+        const libvlc_downloader_request_t req = {
+            .version = 0,
+            .media = media,
+        };
+
+        libvlc_downloader_task *task = libvlc_downloader_queue(downloader, &req, &cbs, &ctxs[i]);
+
+        libvlc_media_release(media);
+
+        if (task == NULL)
+        {
+            fprintf(stderr, "[req %d] failed to queue download for '%s'\n", i, url);
+            continue;
+        }
+
+        fprintf(stdout, "[req %d, id=%p] queued: %s\n",
+                i, task, url);
+        fflush(stdout);
+        queued++;
+    }
+
+    for (int i = 0; i < queued; ++i)
+        sem_wait(&done_sem);
+
+    fprintf(stdout, "All %d queued download(s) completed (finished/cancelled/error).\n", queued);
+    fflush(stdout);
+
+    libvlc_downloader_destroy(downloader);
+    sem_destroy(&done_sem);
+    free(ctxs);
+
+    return queued > 0 ? EXIT_SUCCESS : EXIT_FAILURE;
+}


=====================================
include/meson.build
=====================================
@@ -3,6 +3,7 @@ install_headers(
     'vlc/vlc.h',
     'vlc/libvlc.h',
     'vlc/libvlc_dialog.h',
+    'vlc/libvlc_downloader.h',
     'vlc/libvlc_media.h',
     'vlc/libvlc_media_discoverer.h',
     'vlc/libvlc_media_list.h',


=====================================
include/vlc/libvlc_downloader.h
=====================================
@@ -0,0 +1,362 @@
+/*****************************************************************************
+ * libvlc_downloader.h:  LibVLC Downloader API
+ *****************************************************************************
+ * Copyright (C) 2026 VLC authors and VideoLAN
+ *
+ * This program is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston MA 02110-1301, USA.
+ *****************************************************************************/
+
+#ifndef VLC_LIBVLC_DOWNLOADER_H
+#define VLC_LIBVLC_DOWNLOADER_H 1
+
+#include <vlc/libvlc.h>
+#include <vlc/libvlc_media.h>
+#include <vlc/libvlc_media_list.h>
+
+# ifdef __cplusplus
+extern "C" {
+# endif
+
+/** \defgroup libvlc_downloader LibVLC downloader
+ * \ingroup libvlc
+ * @ref libvlc_downloader_t is an abstract representation of a downloader
+ * @{
+ * \file
+ * LibVLC downloader API
+ */
+typedef struct libvlc_downloader_t libvlc_downloader_t;
+
+/**
+ * A downloader request object
+ */
+typedef struct libvlc_downloader_request_t libvlc_downloader_request_t;
+
+/**
+ * Opaque handle of a downloader task.
+ *
+ * Identifies a task request submitted via libvlc_downloader_queue().
+ * It can be passed to libvlc_downloader_cancel() to cancel that request,
+ * or to libvlc_downloader_set_pause() to pause/resume that request.
+ *
+ * \note Validity starts when libvlc_downloader_queue() returns a non-NULL handle
+ * and ends with libvlc_downloader_task_release().
+ */
+typedef struct libvlc_downloader_task libvlc_downloader_task;
+
+/**
+ * Downloader status
+ */
+typedef enum libvlc_downloader_status_t
+{
+    /** download pending */
+    libvlc_downloader_status_pending,
+    /** active download in progress (not paused) */
+    libvlc_downloader_status_running,
+    /** download paused */
+    libvlc_downloader_status_paused,
+    /** download finished */
+    libvlc_downloader_status_finished,
+    /** download cancelled */
+    libvlc_downloader_status_cancelled,
+    /** download error occurred */
+    libvlc_downloader_status_error,
+} libvlc_downloader_status_t;
+
+/** Sentinel return value to signal an error in the download (to be returned by on_buffer callback) */
+#define LIBVLC_DOWNLOADER_CB_ERROR ((ptrdiff_t)-1)
+
+/** Sentinel return value to cancel the download (to be returned by on_buffer callback) */
+#define LIBVLC_DOWNLOADER_CB_CANCEL ((ptrdiff_t)-2)
+
+/**
+ * Downloader callbacks
+ */
+struct libvlc_downloader_cbs
+{
+    /**
+     * Version of struct libvlc_downloader_cbs
+     */
+    uint32_t version;
+
+    /**
+     * Called when a buffer of data is read.
+     *
+     * \note Mandatory (can't be NULL),
+     * available since version 0
+     *
+     * \warning Do not call any libvlc_downloader_* API from within this callback.
+     * Only libvlc_downloader_task_* APIs may be called on the provided task handle.
+     * And avoid blocking operations in this callback as it is invoked with the internal lock held.
+     *
+     * \param opaque user data
+     * \param task opaque handle returned by libvlc_downloader_queue()
+     * \param buf pointer to buffer (owned by downloader, only valid during callback)
+     * \param len size of buffer
+     * \param position total number of bytes read by the downloader so far
+     * \param total total size of the media in bytes (only medias with finite size are allowed to download)
+     *
+     * \return The number of bytes consumed by the user, or \ref LIBVLC_DOWNLOADER_CB_ERROR to
+     * terminate the download with an error, or \ref LIBVLC_DOWNLOADER_CB_CANCEL to cancel the download.
+     *
+     * If the returned number of bytes consumed is less than \p len (partial read), the downloader
+     * auto-pauses as a form of backpressure. The user must call `libvlc_downloader_set_pause(dl, task, false)`
+     * from the main thread to resume when ready to consume data again.
+     *
+     * \note User must copy the data if it needs to be accessed later.
+     * The maximum buffer size is limited to around 64 KB.
+     */
+    ptrdiff_t (*on_buffer)(void *opaque, libvlc_downloader_task *task,
+                           const uint8_t *buf, size_t len,
+                           uint64_t position, uint64_t total);
+
+    /**
+     * Called when the downloader state changes.
+     *
+     * \note Mandatory (can't be NULL),
+     * available since version 0
+     *
+     * Invoked when download starts, pauses, resumes, cancels, finishes
+     * or errors out. \ref libvlc_downloader_status_t
+     *
+     * \warning Do not call any libvlc_downloader_* API from within this callback.
+     * Only libvlc_downloader_task_* APIs may be called on the provided task handle.
+     * And avoid blocking operations in this callback as it is invoked with the internal lock held.
+     *
+     * \param opaque user data
+     * \param task opaque handle returned by libvlc_downloader_queue()
+     * \param status download status
+     */
+    void (*on_state_update)(void *opaque, libvlc_downloader_task *task,
+                            libvlc_downloader_status_t status);
+
+    /**
+     * Called when subitems of the media are available.
+     *
+     * \note Optional (can be NULL),
+     * available since version 0
+     *
+     * \param opaque user data
+     * \param task opaque handle returned by libvlc_downloader_queue()
+     * \param subitems media list of subitems (owned by LibVLC)
+     */
+    void (*on_subitems)(void *opaque, libvlc_downloader_task *task,
+                        libvlc_media_list_t *subitems);
+
+    /**
+     * Called when the parsed media has slaves.
+     *
+     * \note Optional (can be NULL),
+     * available since version 0
+     *
+     * \param opaque user data
+     * \param task opaque handle returned by libvlc_downloader_queue()
+     * \param slaves array of libvlc_media_slave_t* (owned by LibVLC)
+     * \param count number of slaves
+     */
+    void (*on_slaves)(void *opaque, libvlc_downloader_task *task,
+                      libvlc_media_slave_t **slaves, size_t count);
+};
+
+/**
+ * struct defining a downloader request
+ */
+struct libvlc_downloader_request_t
+{
+    /**
+     * Version of libvlc_downloader_request_t
+     */
+    uint32_t version;
+
+    /**
+     * Media to download
+     *
+     * \note Mandatory (can't be NULL),
+     * available since version 0
+     *
+     * - Only finite-size media are allowed to download.
+     *
+     * - If the media is a playlist or directory, the user will be notified of the
+     *   subitems via the on_subitems callback and the download will not proceed.
+     *
+     * - If the media is a livestream or unknown type, the download will error out.
+     */
+    libvlc_media_t *media;
+};
+
+/**
+ * struct defining downloader configuration
+ */
+struct libvlc_downloader_cfg
+{
+    /**
+     * Version of struct libvlc_downloader_cfg
+     */
+    uint32_t version;
+
+    /**
+     * The maximum number of threads used by the parser internally, 0 for default
+     * (1 thread)
+     *
+     * \note Optional (can be 0),
+     * available since version 0
+     */
+    uint32_t max_parser_threads;
+};
+
+/**
+ * Create a downloader instance.
+ *
+ * Supports downloading files over a limited set of protocols:
+ * http(s), ftp, file, nfs, smb, sftp
+ *
+ * The downloader must be released by calling libvlc_downloader_destroy()
+ * when it is no longer needed.
+ *
+ * \param inst LibVLC instance
+ * \param cfg a pointer to a valid downloader configuration struct
+ * \return downloader instance or NULL on error
+ * 
+ * \version LibVLC 4.0.0 or later
+ */
+LIBVLC_API libvlc_downloader_t *
+libvlc_downloader_new(libvlc_instance_t *inst, const struct libvlc_downloader_cfg *cfg);
+
+/**
+ * Download a media asynchronously.
+ *
+ * - The downloader first parses the media.
+ *
+ * - If the media has subitems, the user will be notified via the on_subitems callback.
+ *
+ * - If the media has slaves, the user will be notified via the on_slaves callback.
+ *
+ * - If the media is not a file type,
+ *   the download will not proceed. \see libvlc_media_type_t,
+ *   and the user will be notified via the on_state_update callback.
+ *
+ * - If the media is a file type with finite size, the download starts in a separate thread.
+ *
+ * \param downloader downloader instance
+ * \param req a pointer to a valid request struct
+ * \param cbs a pointer to a valid callbacks struct. The pointed struct
+ * must be kept alive (and not modified) by the caller until libvlc_downloader_cbs.on_state_update()
+ * is called for the returned task handle with a terminal state (finished/cancelled/error).
+ * \param cbs_opaque opaque pointer for callbacks
+ * \return NULL in case of error, or a valid handle if the request was
+ * scheduled for downloading.
+ *
+ * \note No callbacks will be invoked if the return value is NULL.
+ *
+ * \version LibVLC 4.0.0 or later
+ */
+LIBVLC_API libvlc_downloader_task *
+libvlc_downloader_queue(libvlc_downloader_t *downloader, const libvlc_downloader_request_t *req,
+                        const struct libvlc_downloader_cbs *cbs, void *cbs_opaque);
+
+/**
+ * Cancel an ongoing download.
+ *
+ * \param downloader downloader instance
+ * \param task a downloader task returned by libvlc_downloader_queue(), 
+ * or NULL to cancel all requests.
+ *
+ * \return the number of requests cancelled
+ *
+ * \note
+ * - This function is valid only if the request is in one of the following states:
+ *   pending, running, or paused.
+ *
+ * - When a request is cancelled, the `on_state_update` callback will be triggered
+ *   with the cancelled state.
+ *
+ * - If the request is already in a terminated state (finished, cancelled, or error),
+ *   the call is a no-op and no callback will be invoked.
+ *
+ * \version LibVLC 4.0.0 or later
+ */
+LIBVLC_API size_t libvlc_downloader_cancel(libvlc_downloader_t *downloader, libvlc_downloader_task *task);
+
+/**
+ * Toggle pause/resume for the download.
+ *
+ * \param downloader downloader instance
+ * \param task a valid downloader task returned by libvlc_downloader_queue()
+ * \param paused true to pause, false to resume
+ *
+ * \note This API is valid only when the download is in pending/running/paused state.
+ * And the on_state_update callback with paused/running state will be called only during these
+ * state changes. Else, for finished/cancelled/error states, it's a no-op and
+ * no callback will be called.
+ *
+ * \version LibVLC 4.0.0 or later
+ */
+LIBVLC_API void libvlc_downloader_set_pause(libvlc_downloader_t *downloader,
+                                            libvlc_downloader_task *task,
+                                            bool paused);
+
+/**
+ * Destroy a downloader and free resources.
+ * All pending, running and paused downloads are cancelled.
+ * Waits for all download threads to join.
+ *
+ * \param downloader downloader instance
+ *
+ * \version LibVLC 4.0.0 or later
+ */
+LIBVLC_API void libvlc_downloader_destroy(libvlc_downloader_t *downloader);
+
+/**
+ * Get the media associated with the downloader request handle.
+ *
+ * \param task opaque handle returned by libvlc_downloader_queue()
+ * \return the media associated with the request handle.
+ *
+ * \note The returned media is held by the task, it must not be
+ * released by the caller.
+ *
+ * \version LibVLC 4.0.0 or later
+ */
+LIBVLC_API libvlc_media_t *
+libvlc_downloader_task_get_media(libvlc_downloader_task *task);
+
+/**
+ * Release a downloader task handle.
+ *
+ * \param task the downloader task handle
+ *
+ * \note
+ * - The task handle is retained when returned by libvlc_downloader_queue().
+ *
+ * - Mandatory to call to avoid memory leaks.
+ *
+ * - It is safe to call this API from within the on_state_update callback, when it
+ *   reports a terminal state (finished, cancelled, error) \see libvlc_downloader_status_t.
+ *
+ * - The task handle should not be used after calling this function.
+ *
+ * - If called on an active task, it doesn't cancel the task,
+ *   use \ref libvlc_downloader_cancel() for that.
+ *
+ * \version LibVLC 4.0.0 or later
+ */
+LIBVLC_API void libvlc_downloader_task_release(libvlc_downloader_task *task);
+
+/** @}*/
+
+# ifdef __cplusplus
+}
+# endif
+
+#endif /* VLC_LIBVLC_DOWNLOADER_H */


=====================================
include/vlc/vlc.h
=====================================
@@ -45,6 +45,7 @@ extern "C" {
 #include "libvlc_media_list_player.h"
 #include "libvlc_media_discoverer.h"
 #include "libvlc_dialog.h"
+#include "libvlc_downloader.h"
 #include "libvlc_version.h"
 
 # ifdef __cplusplus


=====================================
include/vlc_stream.h
=====================================
@@ -255,6 +255,7 @@ enum stream_query_e
     STREAM_CAN_FASTSEEK,                    /**< arg1=(bool *) res=cannot fail */
     STREAM_CAN_PAUSE,                       /**< arg1=(bool *) res=cannot fail */
     STREAM_CAN_CONTROL_PACE,                /**< arg1=(bool *) res=cannot fail */
+    STREAM_CAN_DOWNLOAD,                    /**< arg1=(bool *) res=can fail */
     /* */
     STREAM_GET_SIZE=6,                      /**< arg1=(uint64_t *) res=can fail */
     STREAM_GET_MTIME,                       /**< arg1=(uint64_t *) res=can fail
@@ -468,6 +469,13 @@ VLC_USED static inline bool vlc_stream_CanPace(stream_t *s)
     return can_control_pace;
 }
 
+VLC_USED static inline bool vlc_stream_CanDownload(stream_t *s)
+{
+    bool can_download = false;
+    vlc_stream_Control(s, STREAM_CAN_DOWNLOAD, &can_download);
+    return can_download;
+}
+
 VLC_USED static inline vlc_tick_t vlc_stream_GetPtsDelay(stream_t *s)
 {
     vlc_tick_t pts_delay;


=====================================
lib/Makefile.am
=====================================
@@ -10,6 +10,7 @@ CLEANFILES = $(BUILT_SOURCES) $(pkgconfig_DATA)
 pkginclude_HEADERS = \
 	../include/vlc/libvlc.h \
 	../include/vlc/libvlc_dialog.h \
+	../include/vlc/libvlc_downloader.h \
 	../include/vlc/libvlc_media.h \
 	../include/vlc/libvlc_media_discoverer.h \
 	../include/vlc/libvlc_media_list.h \
@@ -34,6 +35,7 @@ libvlc_la_SOURCES = \
 	renderer_discoverer_internal.h \
 	core.c \
 	dialog.c \
+	downloader.c \
 	renderer_discoverer.c \
 	error.c \
 	log.c \


=====================================
lib/downloader.c
=====================================
@@ -0,0 +1,568 @@
+/*****************************************************************************
+ * downloader.c: LibVLC Downloader API
+ *****************************************************************************
+ * Copyright (C) 2026 VLC authors and VideoLAN
+ *
+ * This program is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston MA 02110-1301, USA.
+ *****************************************************************************/
+
+#ifdef HAVE_CONFIG_H
+#include "config.h"
+#endif
+
+#include <vlc/libvlc_downloader.h>
+
+#include <vlc_access.h>
+#include <vlc_interrupt.h>
+#include <vlc_stream.h>
+#include <vlc_threads.h>
+
+#include "libvlc_internal.h"
+
+#define BUFFER_SIZE 65536
+
+struct libvlc_downloader_thread
+{
+    /* Node of libvlc_downloader_t.threads list */
+    struct vlc_list node;
+
+    /* The task thread */
+    vlc_thread_t thread;
+
+    /* Thread termination state */
+    bool terminated;
+};
+
+struct libvlc_downloader_t
+{
+    libvlc_instance_t *libvlc;
+    vlc_mutex_t lock;
+    libvlc_parser_t *parser;
+
+    /* list of ongoing tasks (terminated tasks are removed) */
+    struct vlc_list submitted_tasks;
+
+    /* list of downloader threads (dead threads joined at libvlc_downloader_queue,
+       all threads are joined at libvlc_downloader_destroy) */
+    struct vlc_list threads;
+};
+
+struct libvlc_downloader_task
+{
+    libvlc_downloader_t *downloader;
+    struct vlc_list node;
+    libvlc_downloader_status_t status;
+
+    libvlc_parser_task *parser_task;
+
+    libvlc_media_t *media;
+    stream_t *s;
+
+    const struct libvlc_downloader_cbs *cbs;
+    void *cbs_opaque;
+
+    /* interrupt context (to interrupt blocking read) */
+    vlc_interrupt_t *interrupt;
+
+    /* required for pause/resume */
+    bool pause_requested;
+    vlc_cond_t interrupt_cond;
+
+    /* required for ongoing task abortion */
+    bool interrupted;
+
+    vlc_atomic_rc_t rc;
+
+    struct libvlc_downloader_thread *thread;
+};
+
+/* Create a new downloader task */
+static struct libvlc_downloader_task *
+DownloaderTaskNew(libvlc_downloader_t *downloader, libvlc_media_t *media,
+                  const struct libvlc_downloader_cbs *cbs, void *cbs_opaque)
+{
+    struct libvlc_downloader_task *task = malloc(sizeof(*task));
+    if (task == NULL)
+        return NULL;
+
+    task->interrupt = vlc_interrupt_create();
+    if (task->interrupt == NULL)
+    {
+        free(task);
+        return NULL;
+    }
+
+    task->downloader = downloader;
+    task->media = libvlc_media_retain(media);
+    task->s = NULL;
+    task->cbs = cbs;
+    task->cbs_opaque = cbs_opaque;
+    task->pause_requested = false;
+    vlc_cond_init(&task->interrupt_cond);
+    task->interrupted = false;
+    task->status = libvlc_downloader_status_pending;
+    task->parser_task = NULL;
+    task->thread = NULL;
+
+    vlc_atomic_rc_init(&task->rc);
+
+    return task;
+}
+
+/* Delete a downloader task */
+static void
+DownloaderTaskDestroy(struct libvlc_downloader_task *task)
+{
+    libvlc_media_release(task->media);
+    vlc_interrupt_destroy(task->interrupt);
+
+    if (task->parser_task)
+        libvlc_parser_task_release(task->parser_task);
+
+    if (task->s)
+        vlc_stream_Delete(task->s);
+
+    free(task);
+}
+
+/* Interrupt a running downloader task */
+static void
+DownloaderTaskInterruptLocked(struct libvlc_downloader_task *task)
+{
+    task->interrupted = true;
+    vlc_interrupt_kill(task->interrupt);
+    vlc_cond_signal(&task->interrupt_cond);
+}
+
+/* report downloader state changes */
+static void libvlc_downloader_task_ReportStateLocked(struct libvlc_downloader_task *task,
+                                                     libvlc_downloader_status_t new_status)
+{
+    bool status_changed = false;
+
+    if (task->interrupted)
+        new_status = libvlc_downloader_status_cancelled;
+
+    libvlc_downloader_status_t old_status = task->status;
+    if (old_status != new_status)
+    {
+        task->status = new_status;
+        status_changed = true;
+    }
+
+    /* report on_state_update only when the state changed */
+    if (status_changed)
+        task->cbs->on_state_update(task->cbs_opaque, task, new_status);
+}
+
+static void *
+downloaderThread(void *data)
+{
+    vlc_thread_set_name("downloader");
+    struct libvlc_downloader_task *task = data;
+    libvlc_downloader_t *downloader = task->downloader;
+    libvlc_downloader_status_t status = libvlc_downloader_status_error;
+    uint8_t *buf = NULL;
+
+    vlc_interrupt_set(task->interrupt);
+
+    char *mrl = libvlc_media_get_mrl(task->media);
+    if (mrl == NULL)
+        goto cleanup;
+
+    task->s = vlc_access_NewMRL(VLC_OBJECT(downloader->libvlc->p_libvlc_int), mrl);
+    free(mrl);
+
+    if (task->s == NULL || !vlc_stream_CanDownload(task->s))
+        goto cleanup;
+
+    uint64_t stream_size = 0;
+    /* only files with valid size allowed */
+    if (vlc_stream_GetSize(task->s, &stream_size) != VLC_SUCCESS)
+        goto cleanup;
+
+    buf = malloc(BUFFER_SIZE);
+    if (buf == NULL)
+        goto cleanup;
+
+    uint64_t total_read = 0;
+    size_t buf_len = 0;
+    size_t consumed_offset = 0;
+    status = libvlc_downloader_status_cancelled;
+
+    while (true)
+    {
+        vlc_mutex_lock(&downloader->lock);
+        if (task->interrupted)
+            goto cleanup_locked;
+
+        /* Pause handling */
+        if (task->pause_requested)
+        {
+            libvlc_downloader_task_ReportStateLocked(task, libvlc_downloader_status_paused);
+            vlc_cond_wait(&task->interrupt_cond, &downloader->lock);
+            vlc_mutex_unlock(&downloader->lock);
+            continue;
+        }
+
+        libvlc_downloader_task_ReportStateLocked(task, libvlc_downloader_status_running);
+
+        /* fetch next chunk only when the previous one is fully drained */
+        if (consumed_offset == buf_len)
+        {
+            vlc_mutex_unlock(&downloader->lock);
+
+            ssize_t read = vlc_stream_ReadPartial(task->s, buf, BUFFER_SIZE);
+
+            if (read < 0)
+                continue;
+
+            vlc_mutex_lock(&downloader->lock);
+            /* check whether task was interrupted during blocking read operation */
+            if (task->interrupted)
+                goto cleanup_locked;
+
+            if (read == 0) /* EOF */
+            {
+                status = libvlc_downloader_status_finished;
+                goto cleanup_locked;
+            }
+
+            buf_len = (size_t)read;
+            consumed_offset = 0;
+            total_read += (uint64_t)read;
+
+            /* update size in case it changed; a valid size is required throughout */
+            if (vlc_stream_GetSize(task->s, &stream_size) != VLC_SUCCESS)
+            {
+                status = libvlc_downloader_status_error;
+                goto cleanup_locked;
+            }
+
+            /* check whether the user requested pause during the blocking read. In that case,
+               the data is buffered and download is paused. */
+            if (task->pause_requested)
+            {
+                vlc_mutex_unlock(&downloader->lock);
+                continue;
+            }
+        }
+
+        /* deliver the residual (either just fetched or left over after a partial read) */
+        const uint8_t *chunk = buf + consumed_offset;
+        size_t remaining = buf_len - consumed_offset;
+
+        ptrdiff_t consumed = task->cbs->on_buffer(task->cbs_opaque, task, chunk,
+                                                  remaining, total_read, stream_size);
+
+        if (consumed == LIBVLC_DOWNLOADER_CB_CANCEL)
+            goto cleanup_locked;
+
+        if (consumed < 0 || (size_t)consumed > remaining)
+        {
+            status = libvlc_downloader_status_error;
+            goto cleanup_locked;
+        }
+
+        consumed_offset += (size_t)consumed;
+
+        /* partial read: auto-pause as backpressure */
+        if ((size_t)consumed < remaining)
+            task->pause_requested = true;
+
+        vlc_mutex_unlock(&downloader->lock);
+    }
+
+cleanup:
+    vlc_mutex_lock(&downloader->lock);
+cleanup_locked:
+    libvlc_downloader_task_ReportStateLocked(task, status);
+    vlc_list_remove(&task->node);
+    task->thread->terminated = true;
+    vlc_mutex_unlock(&downloader->lock);
+    free(buf);
+    libvlc_downloader_task_release(task);
+    return NULL;
+}
+
+libvlc_downloader_t *
+libvlc_downloader_new(libvlc_instance_t *inst, const struct libvlc_downloader_cfg *cfg)
+{
+    assert(inst != NULL);
+    assert(cfg != NULL);
+
+    /* No different versions to handle for now */
+    assert(cfg->version <= 0);
+
+    libvlc_downloader_t *downloader = malloc(sizeof(*downloader));
+    if (downloader == NULL)
+        return NULL;
+
+    const struct libvlc_parser_cfg parser_cfg = {
+        .version = 0,
+        .max_parser_threads = cfg->max_parser_threads,
+        .timeout = 0,
+    };
+
+    downloader->parser = libvlc_parser_new(inst, &parser_cfg);
+    if (downloader->parser == NULL)
+    {
+        free(downloader);
+        return NULL;
+    }
+
+    downloader->libvlc = libvlc_retain(inst);
+    vlc_mutex_init(&downloader->lock);
+    vlc_list_init(&downloader->submitted_tasks);
+    vlc_list_init(&downloader->threads);
+    return downloader;
+}
+
+static void notify_subitems(const struct libvlc_downloader_cbs *cbs, void *cbs_opaque,
+                            libvlc_downloader_task *task)
+{
+    libvlc_media_list_t *mlist = libvlc_media_subitems(task->media);
+    if (mlist == NULL)
+        return;
+
+    cbs->on_subitems(cbs_opaque, task, mlist);
+    libvlc_media_list_release(mlist);
+}
+
+static void notify_slaves(const struct libvlc_downloader_cbs *cbs, void *cbs_opaque,
+                          libvlc_downloader_task *task)
+{
+    libvlc_media_slave_t **slaves = NULL;
+    unsigned count = libvlc_media_slaves_get(task->media, &slaves);
+    if (count > 0 && slaves)
+    {
+        cbs->on_slaves(cbs_opaque, task, slaves, count);
+        libvlc_media_slaves_release(slaves, count);
+    }
+}
+
+static void parse_ended(void *opaque, libvlc_parser_task *parser_task, libvlc_parser_status_t status)
+{
+    (void) parser_task;
+    struct libvlc_downloader_task *task = opaque;
+    const struct libvlc_downloader_cbs *cbs = task->cbs;
+    void *cbs_opaque = task->cbs_opaque;
+    libvlc_downloader_t *downloader = task->downloader;
+    libvlc_downloader_status_t downloader_status = libvlc_downloader_status_error;
+    struct libvlc_downloader_thread *thread = NULL;
+
+    if (status == libvlc_parser_status_cancelled)
+    {
+        downloader_status = libvlc_downloader_status_cancelled;
+        goto cleanup;
+    }
+
+    if (status != libvlc_parser_status_done)
+        goto cleanup;
+
+    if (cbs->on_subitems)
+        notify_subitems(cbs, cbs_opaque, task);
+
+    if (cbs->on_slaves)
+        notify_slaves(cbs, cbs_opaque, task);
+
+    libvlc_media_type_t type = libvlc_media_get_type(task->media);
+    if (type != libvlc_media_type_file)
+        goto cleanup;
+
+    thread = malloc(sizeof(*thread));
+    if (thread == NULL)
+        goto cleanup;
+
+    vlc_mutex_lock(&downloader->lock);
+    /* interruption check before spawning downloader thread */
+    if (task->interrupted)
+        goto cleanup_locked;
+
+    thread->terminated = false;
+    vlc_list_append(&thread->node, &downloader->threads);
+    if (vlc_clone(&thread->thread, downloaderThread, task))
+    {
+        vlc_list_remove(&thread->node);
+        goto cleanup_locked;
+    }
+    task->thread = thread;
+    vlc_mutex_unlock(&downloader->lock);
+
+    return;
+
+cleanup:
+    vlc_mutex_lock(&downloader->lock);
+cleanup_locked:
+    libvlc_downloader_task_ReportStateLocked(task, downloader_status);
+    vlc_list_remove(&task->node);
+    vlc_mutex_unlock(&downloader->lock);
+    free(thread);
+    libvlc_downloader_task_release(task);
+}
+
+static const struct libvlc_parser_cbs parser_cbs = {
+    .version = 0,
+    .on_parsed = parse_ended,
+    .on_attachments_added = NULL,
+};
+
+libvlc_downloader_task *
+libvlc_downloader_queue(libvlc_downloader_t *downloader, const libvlc_downloader_request_t *req,
+                        const struct libvlc_downloader_cbs *cbs, void *cbs_opaque)
+{
+    assert(downloader != NULL);
+    assert(req != NULL && req->media != NULL);
+    assert(cbs != NULL && cbs->on_buffer != NULL && cbs->on_state_update != NULL);
+
+    /* No different versions to handle for now */
+    assert(req->version <= 0);
+    assert(cbs->version <= 0);
+
+    struct libvlc_downloader_task *task = DownloaderTaskNew(downloader, req->media,
+                                                            cbs, cbs_opaque);
+
+    if (task == NULL)
+        return NULL;
+
+    vlc_mutex_lock(&downloader->lock);
+    vlc_list_append(&task->node, &downloader->submitted_tasks);
+
+    /* clean up terminated threads */
+    struct libvlc_downloader_thread *thread;
+    vlc_list_foreach(thread, &downloader->threads, node)
+    {
+        if (thread->terminated)
+        {
+            vlc_join(thread->thread, NULL);
+            vlc_list_remove(&thread->node);
+            free(thread);
+        }
+    }
+    vlc_mutex_unlock(&downloader->lock);
+
+    libvlc_parser_request_t request = {
+        .version = 0,
+        .media = req->media,
+        .parse_flags = 0, /* default VLC_PREPARSER_TYPE_PARSE | VLC_PREPARSER_OPTION_SUBITEMS */
+    };
+
+    vlc_atomic_rc_inc(&task->rc);
+
+    libvlc_parser_task *parser_task = libvlc_parser_queue(downloader->parser, &request,
+                                                          &parser_cbs, task);
+    if (parser_task == NULL)
+    {
+        vlc_mutex_lock(&downloader->lock);
+        vlc_list_remove(&task->node);
+        vlc_mutex_unlock(&downloader->lock);
+        DownloaderTaskDestroy(task);
+        return NULL;
+    }
+
+    task->parser_task = parser_task;
+    return task;
+}
+
+size_t libvlc_downloader_cancel(libvlc_downloader_t *downloader, libvlc_downloader_task *task)
+{
+    assert(downloader != NULL);
+    size_t cancelled = 0;
+
+    vlc_mutex_lock(&downloader->lock);
+
+    struct libvlc_downloader_task *task_itr;
+    vlc_list_foreach(task_itr, &downloader->submitted_tasks, node)
+    {
+        if (task != NULL && task_itr != task)
+            continue;
+
+        cancelled++;
+        DownloaderTaskInterruptLocked(task_itr);
+
+        /* small optimisation in the likely case where the user cancel
+           only one task */
+        if (task != NULL)
+            break;
+    }
+
+    vlc_mutex_unlock(&downloader->lock);
+
+    if (task != NULL)
+        libvlc_parser_cancel_request(downloader->parser, task->parser_task);
+    else
+        libvlc_parser_cancel_request(downloader->parser, NULL);
+
+    return cancelled;
+}
+
+void libvlc_downloader_set_pause(libvlc_downloader_t *downloader, libvlc_downloader_task *task,
+                                 bool paused)
+{
+    assert(downloader != NULL);
+    assert(task != NULL);
+
+    vlc_mutex_lock(&downloader->lock);
+    task->pause_requested = paused;
+    if (!paused)
+        vlc_cond_signal(&task->interrupt_cond);
+    vlc_mutex_unlock(&downloader->lock);
+}
+
+void libvlc_downloader_destroy(libvlc_downloader_t *downloader)
+{
+    assert(downloader != NULL);
+    libvlc_downloader_cancel(downloader, NULL);
+
+    /* drain the parser. libvlc_parser_destroy() waits for all in-flight
+       parse_ended callbacks to complete. Every running task was interrupted
+       above, so none of the parse_ended callback can spawn a new download
+       thread and the threads list is stable once this returns.
+       A thread spawned just before the interruption is already in that list and
+       observes the interruption at its next iteration, so joining it below returns
+       promptly. */
+    libvlc_parser_destroy(downloader->parser);
+
+    struct libvlc_downloader_thread *thread;
+    vlc_list_foreach(thread, &downloader->threads, node)
+    {
+        vlc_join(thread->thread, NULL);
+        vlc_list_remove(&thread->node);
+        free(thread);
+    }
+
+    /* both lists must be empty by now */
+    assert(vlc_list_is_empty(&downloader->submitted_tasks));
+    assert(vlc_list_is_empty(&downloader->threads));
+
+    libvlc_release(downloader->libvlc);
+    free(downloader);
+}
+
+libvlc_media_t *
+libvlc_downloader_task_get_media(libvlc_downloader_task *task)
+{
+    assert(task != NULL);
+    return task->media;
+}
+
+void libvlc_downloader_task_release(libvlc_downloader_task *task)
+{
+    assert(task != NULL);
+    if (!vlc_atomic_rc_dec(&task->rc))
+        return;
+
+    DownloaderTaskDestroy(task);
+}


=====================================
lib/libvlc.sym
=====================================
@@ -119,6 +119,13 @@ libvlc_parser_queue_thumbnailing
 libvlc_parser_cancel_request
 libvlc_parser_task_get_media
 libvlc_parser_task_release
+libvlc_downloader_new
+libvlc_downloader_queue
+libvlc_downloader_cancel
+libvlc_downloader_set_pause
+libvlc_downloader_destroy
+libvlc_downloader_task_get_media
+libvlc_downloader_task_release
 libvlc_media_player_add_slave
 libvlc_media_player_can_pause
 libvlc_media_player_program_scrambled


=====================================
lib/meson.build
=====================================
@@ -1,6 +1,7 @@
 libvlc_sources = [
     'core.c',
     'dialog.c',
+    'downloader.c',
     'renderer_discoverer.c',
     'error.c',
     'log.c',


=====================================
modules/access/dsm/access.c
=====================================
@@ -733,6 +733,7 @@ static int Control( stream_t *p_access, int i_query, va_list args )
     case STREAM_CAN_SEEK:
     case STREAM_CAN_PAUSE:
     case STREAM_CAN_CONTROL_PACE:
+    case STREAM_CAN_DOWNLOAD:
         *va_arg( args, bool* ) = true;
         break;
 


=====================================
modules/access/file.c
=====================================
@@ -361,6 +361,11 @@ static int FileControl( stream_t *p_access, int i_query, va_list args )
             *pb_bool = p_sys->b_pace_control;
             break;
 
+        case STREAM_CAN_DOWNLOAD:
+            pb_bool = va_arg( args, bool * );
+            *pb_bool = true;
+            break;
+
         case STREAM_GET_SIZE:
         case STREAM_GET_MTIME:
         {


=====================================
modules/access/ftp.c
=====================================
@@ -1373,6 +1373,7 @@ static int Control( stream_t *p_access, int i_query, va_list args )
             *pb_bool = true;    /* FIXME */
             break;
         case STREAM_CAN_CONTROL_PACE:
+        case STREAM_CAN_DOWNLOAD:
             pb_bool = va_arg( args, bool * );
             *pb_bool = true;    /* FIXME */
             break;


=====================================
modules/access/http.c
=====================================
@@ -561,6 +561,7 @@ static int Control( stream_t *p_access, int i_query, va_list args )
             break;
         case STREAM_CAN_PAUSE:
         case STREAM_CAN_CONTROL_PACE:
+        case STREAM_CAN_DOWNLOAD:
             pb_bool = va_arg( args, bool* );
             *pb_bool = true;
             break;


=====================================
modules/access/http/access.c
=====================================
@@ -82,6 +82,7 @@ static int FileControl(stream_t *access, int query, va_list args)
 
         case STREAM_CAN_PAUSE:
         case STREAM_CAN_CONTROL_PACE:
+        case STREAM_CAN_DOWNLOAD:
             *va_arg(args, bool *) = true;
             break;
 


=====================================
modules/access/nfs.c
=====================================
@@ -289,6 +289,7 @@ FileControl(stream_t *p_access, int i_query, va_list args)
 
         case STREAM_CAN_PAUSE:
         case STREAM_CAN_CONTROL_PACE:
+        case STREAM_CAN_DOWNLOAD:
             *va_arg(args, bool *) = true;
             break;
 


=====================================
modules/access/samba.c
=====================================
@@ -217,6 +217,7 @@ static int Control( stream_t *p_access, int i_query, va_list args )
     case STREAM_CAN_SEEK:
     case STREAM_CAN_PAUSE:
     case STREAM_CAN_CONTROL_PACE:
+    case STREAM_CAN_DOWNLOAD:
         *va_arg( args, bool* ) = true;
         break;
 


=====================================
modules/access/sftp.c
=====================================
@@ -641,6 +641,7 @@ static int Control( stream_t* p_access, int i_query, va_list args )
 
     case STREAM_CAN_PAUSE:
     case STREAM_CAN_CONTROL_PACE:
+    case STREAM_CAN_DOWNLOAD:
         pb_bool = va_arg( args, bool * );
         *pb_bool = true;
         break;


=====================================
modules/access/smb2.c
=====================================
@@ -364,6 +364,7 @@ FileControl(stream_t *access, int i_query, va_list args)
 
         case STREAM_CAN_PAUSE:
         case STREAM_CAN_CONTROL_PACE:
+        case STREAM_CAN_DOWNLOAD:
             *va_arg(args, bool *) = true;
             break;
 


=====================================
src/input/item.c
=====================================
@@ -1242,6 +1242,7 @@ static enum input_item_type_e GuessType( const input_item_t *p_item, bool *p_net
         { "v4l",    ITEM_TYPE_CARD, false },
         { "vcd",    ITEM_TYPE_DISC, false },
         { "vdr",    ITEM_TYPE_STREAM, true },
+        { "vlctest_",ITEM_TYPE_FILE, false },
         { "wasapi", ITEM_TYPE_CARD, false },
         { "window", ITEM_TYPE_CARD, false },
     };


=====================================
test/Makefile.am
=====================================
@@ -32,6 +32,7 @@ player_programs = \
 noinst_PROGRAMS =
 check_PROGRAMS = \
 	test_libvlc_core \
+	test_libvlc_downloader \
 	test_libvlc_equalizer \
 	test_libvlc_media \
 	test_libvlc_media_callback \
@@ -169,6 +170,8 @@ LIBVLC = -L../lib -lvlc
 
 test_libvlc_core_SOURCES = libvlc/core.c
 test_libvlc_core_LDADD = $(LIBVLC)
+test_libvlc_downloader_SOURCES = libvlc/downloader.c
+test_libvlc_downloader_LDADD = $(LIBVLCCORE) $(LIBVLC)
 test_libvlc_equalizer_SOURCES = libvlc/equalizer.c
 test_libvlc_equalizer_LDADD = $(LIBVLC)
 test_libvlc_media_SOURCES = libvlc/media.c


=====================================
test/libvlc/downloader.c
=====================================
@@ -0,0 +1,512 @@
+/*****************************************************************************
+ * downloader.c: test for the LibVLC Downloader API
+ *****************************************************************************
+ * Copyright (C) 2026 VLC authors and VideoLAN
+ *
+ * This program is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published
+ * by the Free Software Foundation; either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston MA 02110-1301, USA.
+ *****************************************************************************/
+
+#ifdef HAVE_CONFIG_H
+# include "config.h"
+#endif
+
+/* Define a builtin module for mocked parts */
+#define MODULE_NAME test_downloader
+#undef VLC_DYNAMIC_PLUGIN
+
+#include "./test.h"
+#include <vlc_common.h>
+#include <vlc_demux.h>
+#include <vlc_plugin.h>
+#include <vlc_stream.h>
+
+#include <vlc/libvlc.h>
+
+#include <limits.h>
+
+const char vlc_module_name[] = MODULE_STRING;
+
+#define TEST_TOTAL_READS 20
+#define TEST_BUFFER_SIZE 65536
+#define TEST_TOTAL_BYTES ((uint64_t)TEST_TOTAL_READS * (uint64_t)TEST_BUFFER_SIZE)
+
+struct test_ctx_t
+{
+    vlc_sem_t terminated_sem;
+    vlc_sem_t progress_sem;
+    vlc_sem_t paused_sem;
+    int state_counts[libvlc_downloader_status_error + 1];
+    int buffer_cb_calls;
+    uint64_t total_bytes;
+    bool buffer_data_ok;
+};
+
+/* Dummy access module read counter */
+struct vlctest_access_sys_t
+{
+    int read_counter;
+};
+
+/* Dummy Read callback: fills buffer with 0xAB until EOF (simulated) */
+static ssize_t Read(stream_t *access, void *buf, size_t len)
+{
+    struct vlctest_access_sys_t *p_sys = access->p_sys;
+    assert(len <= SSIZE_MAX);
+    if (p_sys->read_counter >= TEST_TOTAL_READS)
+        return 0; /* EOF */
+
+    /* simulate some delay in reading */
+    vlc_tick_sleep(VLC_TICK_FROM_MS(10));
+    memset(buf, 0xAB, len);
+    p_sys->read_counter++;
+    return len;
+}
+
+static int Control(stream_t *access, int query, va_list args)
+{
+    (void)access;
+    switch (query)
+    {
+        case STREAM_CAN_DOWNLOAD:
+        {
+            bool *pb_bool = va_arg(args, bool *);
+            *pb_bool = true;
+            break;
+        }
+        case STREAM_GET_SIZE:
+        {
+            uint64_t *p_size = va_arg(args, uint64_t *);
+            *p_size = TEST_TOTAL_BYTES;
+            break;
+        }
+        default:
+            return VLC_EGENERIC;
+    }
+    return VLC_SUCCESS;
+}
+
+static void AccessClose(vlc_object_t *obj)
+{
+    stream_t *access = (stream_t *)obj;
+    free(access->p_sys);
+}
+
+static int AccessOpen(vlc_object_t *obj)
+{
+    stream_t *access = (stream_t *)obj;
+    struct vlctest_access_sys_t *p_sys = malloc(sizeof(*p_sys));
+    if (p_sys == NULL)
+        return VLC_ENOMEM;
+
+    p_sys->read_counter = 0;
+    access->p_sys = p_sys;
+    access->pf_read = Read;
+    access->pf_control = Control;
+    access->pf_seek = NULL;
+    return VLC_SUCCESS;
+}
+
+/* Dummy demux that opens any vlctest_:// stream and immediately reports EOF.
+   Selected only when explicitly forced via ":demux=vlctest_" on the media,
+   so the preparser succeeds without needing real demuxer probing. */
+static int DemuxDemux(demux_t *demux)
+{
+    (void)demux;
+    return VLC_DEMUXER_EOF;
+}
+
+static int DemuxControl(demux_t *demux, int query, va_list args)
+{
+    (void)demux;
+    (void)query;
+    (void)args;
+    return VLC_EGENERIC;
+}
+
+static int DemuxOpen(vlc_object_t *obj)
+{
+    demux_t *demux = (demux_t *)obj;
+    demux->pf_demux = DemuxDemux;
+    demux->pf_control = DemuxControl;
+    return VLC_SUCCESS;
+}
+
+static void DemuxClose(vlc_object_t *obj)
+{
+    (void)obj;
+}
+
+vlc_module_begin()
+    set_capability("access", 0)
+    add_shortcut("vlctest_")
+    set_callbacks(AccessOpen, AccessClose)
+
+    add_submodule()
+        set_capability("demux", 0)
+        add_shortcut("vlctest_")
+        set_callbacks(DemuxOpen, DemuxClose)
+vlc_module_end()
+
+VLC_EXPORT const vlc_plugin_cb vlc_static_modules[] = {
+    VLC_SYMBOL(vlc_entry),
+    NULL
+};
+
+static ptrdiff_t on_buffer(void *opaque, libvlc_downloader_task *task, const uint8_t *buf,
+                           size_t len, uint64_t position, uint64_t total)
+{
+    (void)task;
+    (void)total;
+    assert(len <= SSIZE_MAX);
+    struct test_ctx_t *ctx = opaque;
+    ctx->buffer_cb_calls++;
+    ctx->total_bytes += len;
+    /* check that all bytes are 0xAB as per Read() */
+    for (size_t i = 0; i < len; i++)
+    {
+        if (buf[i] != 0xAB)
+            ctx->buffer_data_ok = false;
+    }
+    /* progress should match total bytes downloaded so far */
+    assert(position == ctx->total_bytes);
+
+    /* notify main thread for synchronization */
+    vlc_sem_post(&ctx->progress_sem);
+    return (ptrdiff_t)len;
+}
+
+static void on_state_update(void *opaque, libvlc_downloader_task *task, libvlc_downloader_status_t status)
+{
+    struct test_ctx_t *ctx = opaque;
+    ctx->state_counts[status]++;
+    if (status == libvlc_downloader_status_paused)
+        vlc_sem_post(&ctx->paused_sem);
+    if (status == libvlc_downloader_status_finished ||
+        status == libvlc_downloader_status_cancelled ||
+        status == libvlc_downloader_status_error)
+    {
+        vlc_sem_post(&ctx->terminated_sem);
+        libvlc_downloader_task_release(task);
+    }
+}
+
+static void reset_ctx(struct test_ctx_t *ctx)
+{
+    memset(ctx, 0, sizeof(*ctx));
+    ctx->buffer_data_ok = true;
+    vlc_sem_init(&ctx->terminated_sem, 0);
+    vlc_sem_init(&ctx->progress_sem, 0);
+    vlc_sem_init(&ctx->paused_sem, 0);
+}
+
+static const struct libvlc_downloader_cbs cbs = {
+    .version = 0,
+    .on_buffer = on_buffer,
+    .on_state_update = on_state_update,
+    .on_subitems = NULL,
+    .on_slaves = NULL,
+};
+
+static void test_basic_download(libvlc_instance_t *vlc)
+{
+    fprintf(stderr, "test: 1/ checking basic download of two medias with one downloader\n");
+    struct test_ctx_t ctx1, ctx2;
+    reset_ctx(&ctx1);
+    reset_ctx(&ctx2);
+
+    const struct libvlc_downloader_cfg cfg = {
+        .version = 0,
+        .max_parser_threads = 1,
+    };
+    libvlc_downloader_t *downloader = libvlc_downloader_new(vlc, &cfg);
+    assert(downloader);
+
+    libvlc_media_t *media1 = libvlc_media_new_location("vlctest_://dummyone");
+    libvlc_media_add_option(media1, ":demux=vlctest_");
+    libvlc_media_t *media2 = libvlc_media_new_location("vlctest_://dummytwo");
+    libvlc_media_add_option(media2, ":demux=vlctest_");
+    assert(media1 && media2);
+
+    const libvlc_downloader_request_t req1 = {
+        .version = 0,
+        .media = media1,
+    };
+    const libvlc_downloader_request_t req2 = {
+        .version = 0,
+        .media = media2,
+    };
+
+    libvlc_downloader_task *task1 = libvlc_downloader_queue(downloader, &req1, &cbs, &ctx1);
+    libvlc_downloader_task *task2 = libvlc_downloader_queue(downloader, &req2, &cbs, &ctx2);
+
+    assert(task1 != NULL);
+    assert(task2 != NULL);
+    assert(task1 != task2);
+
+    /* wait for both downloads to reach a terminal state */
+    vlc_sem_wait(&ctx1.terminated_sem);
+    vlc_sem_wait(&ctx2.terminated_sem);
+
+    /* check state transitions for both */
+    assert(ctx1.state_counts[libvlc_downloader_status_running] > 0);
+    assert(ctx1.state_counts[libvlc_downloader_status_finished] == 1);
+    assert(ctx2.state_counts[libvlc_downloader_status_running] > 0);
+    assert(ctx2.state_counts[libvlc_downloader_status_finished] == 1);
+
+    /* check buffer and progress */
+    assert(ctx1.buffer_cb_calls > 0);
+    assert(ctx1.buffer_data_ok);
+    assert(ctx2.buffer_cb_calls > 0);
+    assert(ctx2.buffer_data_ok);
+
+    /* check total bytes per download */
+    assert(ctx1.total_bytes == TEST_TOTAL_BYTES);
+    assert(ctx2.total_bytes == TEST_TOTAL_BYTES);
+
+    libvlc_downloader_destroy(downloader);
+    libvlc_media_release(media1);
+    libvlc_media_release(media2);
+}
+
+static void test_pause_resume(libvlc_instance_t *vlc)
+{
+    fprintf(stderr, "test: 2/ checking pause and resume\n");
+    struct test_ctx_t ctx;
+    reset_ctx(&ctx);
+
+    const struct libvlc_downloader_cfg cfg = {
+        .version = 0,
+        .max_parser_threads = 1,
+    };
+    libvlc_downloader_t *downloader = libvlc_downloader_new(vlc, &cfg);
+    assert(downloader);
+
+    libvlc_media_t *media = libvlc_media_new_location("vlctest_://dummy");
+    assert(media);
+    libvlc_media_add_option(media, ":demux=vlctest_");
+
+    const libvlc_downloader_request_t req = {
+        .version = 0,
+        .media = media,
+    };
+
+    libvlc_downloader_task *task = libvlc_downloader_queue(downloader, &req, &cbs, &ctx);
+    assert(task != NULL);
+
+    /* wait for a couple of buffer callbacks before pausing */
+    for (int i = 0; i < 2; ++i)
+        vlc_sem_wait(&ctx.progress_sem);
+    int progress_before_pause = ctx.buffer_cb_calls;
+
+    libvlc_downloader_set_pause(downloader, task, true);
+
+    /* wait until paused state is reported */
+    vlc_sem_wait(&ctx.paused_sem);
+    assert(ctx.state_counts[libvlc_downloader_status_paused] >= 1);
+
+    /* resume and wait for terminal state */
+    libvlc_downloader_set_pause(downloader, task, false);
+    vlc_sem_wait(&ctx.terminated_sem);
+
+    assert(ctx.state_counts[libvlc_downloader_status_running] >= 1);
+    assert(ctx.state_counts[libvlc_downloader_status_finished] == 1);
+
+    /* ensure progress increased after resume */
+    assert(ctx.buffer_cb_calls > progress_before_pause);
+
+    /* buffer integrity and total size checks */
+    assert(ctx.buffer_data_ok);
+    assert(ctx.total_bytes == TEST_TOTAL_BYTES);
+
+    libvlc_downloader_destroy(downloader);
+    libvlc_media_release(media);
+}
+
+static void test_cancel_download(libvlc_instance_t *vlc)
+{
+    fprintf(stderr, "test: 3/ checking download cancellation\n");
+    struct test_ctx_t ctx;
+    reset_ctx(&ctx);
+
+    const struct libvlc_downloader_cfg cfg = {
+        .version = 0,
+        .max_parser_threads = 1,
+    };
+    libvlc_downloader_t *downloader = libvlc_downloader_new(vlc, &cfg);
+    assert(downloader);
+
+    libvlc_media_t *media = libvlc_media_new_location("vlctest_://dummy");
+    assert(media);
+    libvlc_media_add_option(media, ":demux=vlctest_");
+
+    const libvlc_downloader_request_t req = {
+        .version = 0,
+        .media = media,
+    };
+
+    libvlc_downloader_task *task = libvlc_downloader_queue(downloader, &req, &cbs, &ctx);
+    assert(task != NULL);
+
+    int progress_for_cancel = 2; /* cancel after 2 calls of buffer callback */
+
+    /* wait for the desired progress count, then cancel from main thread */
+    for (int i = 0; i < progress_for_cancel; ++i)
+        vlc_sem_wait(&ctx.progress_sem);
+
+    size_t cancelled = libvlc_downloader_cancel(downloader, task);
+    assert(cancelled == 1);
+
+    vlc_sem_wait(&ctx.terminated_sem);
+
+    assert(ctx.state_counts[libvlc_downloader_status_cancelled] == 1);
+
+    libvlc_downloader_destroy(downloader);
+    libvlc_media_release(media);
+}
+
+/* context for partial-read */
+struct partial_ctx_t
+{
+    vlc_sem_t terminated_sem; /* signal download termination */
+    vlc_sem_t first_buffer_sem; /* signal first buffer callback */
+    vlc_sem_t paused_sem; /* signal paused state */
+    int state_counts[libvlc_downloader_status_error + 1]; /* count of each state reported */
+    int buffer_cb_calls; /* count of buffer callback calls */
+    size_t first_len; /* length of the buffer in the first on_buffer callback invocation */
+    size_t second_len; /* length of the buffer in the second on_buffer callback invocation (residual) */
+};
+
+static void partial_ctx_init(struct partial_ctx_t *ctx)
+{
+    memset(ctx, 0, sizeof(*ctx));
+    vlc_sem_init(&ctx->terminated_sem, 0);
+    vlc_sem_init(&ctx->first_buffer_sem, 0);
+    vlc_sem_init(&ctx->paused_sem, 0);
+}
+
+static void partial_on_state(void *opaque, libvlc_downloader_task *task,
+                             libvlc_downloader_status_t status)
+{
+    struct partial_ctx_t *ctx = opaque;
+    ctx->state_counts[status]++;
+    if (status == libvlc_downloader_status_paused)
+        vlc_sem_post(&ctx->paused_sem);
+    if (status == libvlc_downloader_status_finished ||
+        status == libvlc_downloader_status_cancelled ||
+        status == libvlc_downloader_status_error)
+    {
+        vlc_sem_post(&ctx->terminated_sem);
+        libvlc_downloader_task_release(task);
+    }
+}
+
+static ptrdiff_t partial_on_buffer(void *opaque, libvlc_downloader_task *task,
+                                   const uint8_t *buf, size_t len,
+                                   uint64_t position, uint64_t total)
+{
+    (void)task; (void)total; (void)buf; (void)position;
+    struct partial_ctx_t *ctx = opaque;
+    ctx->buffer_cb_calls++;
+
+    if (ctx->buffer_cb_calls == 1)
+    {
+        /* accept half to trigger auto-pause as backpressure */
+        ctx->first_len = len;
+        vlc_sem_post(&ctx->first_buffer_sem);
+        return (ptrdiff_t)(len / 2);
+    }
+
+    if (ctx->buffer_cb_calls == 2)
+        ctx->second_len = len;
+
+    return (ptrdiff_t)len;
+}
+
+static void test_partial_read(libvlc_instance_t *vlc)
+{
+    fprintf(stderr, "test: 4/ checking partial read triggers pause and redelivers residual\n");
+    struct partial_ctx_t ctx;
+    partial_ctx_init(&ctx);
+
+    const struct libvlc_downloader_cfg cfg = {
+        .version = 0,
+        .max_parser_threads = 1,
+    };
+    libvlc_downloader_t *downloader = libvlc_downloader_new(vlc, &cfg);
+    assert(downloader);
+
+    static const struct libvlc_downloader_cbs cbs = {
+        .version = 0,
+        .on_buffer = partial_on_buffer,
+        .on_state_update = partial_on_state,
+        .on_subitems = NULL,
+        .on_slaves = NULL,
+    };
+
+    libvlc_media_t *media = libvlc_media_new_location("vlctest_://partial");
+    assert(media);
+    libvlc_media_add_option(media, ":demux=vlctest_");
+
+    const libvlc_downloader_request_t req = {
+        .version = 0,
+        .media = media,
+    };
+
+    libvlc_downloader_task *task = libvlc_downloader_queue(downloader, &req, &cbs, &ctx);
+    assert(task != NULL);
+
+    /* wait for the first (partial-accept) callback, then for auto-pause */
+    vlc_sem_wait(&ctx.first_buffer_sem);
+    vlc_sem_wait(&ctx.paused_sem);
+
+    assert(ctx.state_counts[libvlc_downloader_status_paused] >= 1);
+    /* no further on_buffer call should have happened while paused */
+    assert(ctx.buffer_cb_calls == 1);
+
+    /* resume, next on_buffer must deliver the residual of the same chunk */
+    libvlc_downloader_set_pause(downloader, task, false);
+
+    vlc_sem_wait(&ctx.terminated_sem);
+
+    assert(ctx.state_counts[libvlc_downloader_status_finished] == 1);
+
+    /* check that the second buffer contains the expected residual */
+    size_t expected_residual = ctx.first_len - (ctx.first_len / 2);
+    assert(ctx.second_len == expected_residual);
+
+    libvlc_downloader_destroy(downloader);
+    libvlc_media_release(media);
+}
+
+int main(int argc, char **argv)
+{
+    (void)argc; (void)argv;
+    test_init();
+
+    const char * const vlc_argv[] = {
+        "-vvv", "--vout=dummy", "--aout=dummy", "--text-renderer=dummy"
+    };
+
+    libvlc_instance_t *vlc = libvlc_new(ARRAY_SIZE(vlc_argv), vlc_argv);
+    assert(vlc);
+
+    test_basic_download(vlc);
+    test_pause_resume(vlc);
+    test_cancel_download(vlc);
+    test_partial_read(vlc);
+
+    libvlc_release(vlc);
+    return 0;
+}



View it on GitLab: https://code.videolan.org/videolan/vlc/-/compare/cacc4b02aefda52c0cff1b0039399bbfd9c38e95...2769a335d22528bbd15f1958ac93ca583e4c770b

-- 
View it on GitLab: https://code.videolan.org/videolan/vlc/-/compare/cacc4b02aefda52c0cff1b0039399bbfd9c38e95...2769a335d22528bbd15f1958ac93ca583e4c770b
You're receiving this email because of your account on code.videolan.org. Manage all notifications: https://code.videolan.org/-/profile/notifications | Help: https://code.videolan.org/help




More information about the vlc-commits mailing list