[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