mirror of https://github.com/mpv-player/mpv
client API: add stream_cb API for user-defined stream implementations
Based on #2630. Some heavy changes by committer. Signed-off-by: wm4 <wm4@nowhere>
This commit is contained in:
parent
52a0cbe456
commit
7ca4a453e0
|
@ -299,7 +299,7 @@ typedef enum mpv_error {
|
|||
*/
|
||||
MPV_ERROR_COMMAND = -12,
|
||||
/**
|
||||
* Generic error on loading (used with mpv_event_end_file.error).
|
||||
* Generic error on loading (usually used with mpv_event_end_file.error).
|
||||
*/
|
||||
MPV_ERROR_LOADING_FAILED = -13,
|
||||
/**
|
||||
|
@ -1610,7 +1610,7 @@ typedef enum mpv_sub_api {
|
|||
* Will return NULL if unavailable (if OpenGL support was not compiled in).
|
||||
* See opengl_cb.h for details.
|
||||
*/
|
||||
MPV_SUB_API_OPENGL_CB = 1
|
||||
MPV_SUB_API_OPENGL_CB = 1,
|
||||
} mpv_sub_api;
|
||||
|
||||
/**
|
||||
|
|
|
@ -37,6 +37,7 @@ mpv_set_property
|
|||
mpv_set_property_async
|
||||
mpv_set_property_string
|
||||
mpv_set_wakeup_callback
|
||||
mpv_stream_cb_add_ro
|
||||
mpv_suspend
|
||||
mpv_terminate_destroy
|
||||
mpv_unobserve_property
|
||||
|
|
|
@ -0,0 +1,230 @@
|
|||
/* Permission to use, copy, modify, and/or distribute this software for any
|
||||
* purpose with or without fee is hereby granted, provided that the above
|
||||
* copyright notice and this permission notice appear in all copies.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
||||
* WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||
* MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
||||
* ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
||||
* WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
||||
* ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
||||
* OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||
*/
|
||||
|
||||
/*
|
||||
* Note: the client API is licensed under ISC (see above) to ease
|
||||
* interoperability with other licenses. But keep in mind that the
|
||||
* mpv core is still mostly GPLv2+. It's up to lawyers to decide
|
||||
* whether applications using this API are affected by the GPL.
|
||||
* One argument against this is that proprietary applications
|
||||
* using mplayer in slave mode is apparently tolerated, and this
|
||||
* API is basically equivalent to slave mode.
|
||||
*/
|
||||
|
||||
#ifndef MPV_CLIENT_API_STREAM_CB_H_
|
||||
#define MPV_CLIENT_API_STREAM_CB_H_
|
||||
|
||||
#include "client.h"
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
/**
|
||||
* Warning: this API is not stable yet.
|
||||
*
|
||||
* Overview
|
||||
* --------
|
||||
*
|
||||
* This API can be used to make mpv read from a stream with a custom
|
||||
* implementation. This interface is inspired by funopen on BSD and
|
||||
* fopencookie on linux. The stream is backed by user-defined callbacks
|
||||
* which can implement customized open, read, seek, size and close behaviors.
|
||||
*
|
||||
* Usage
|
||||
* -----
|
||||
*
|
||||
* Register your stream callbacks with the mpv_stream_cb_add_ro() function. You
|
||||
* have to provide a mpv_stream_cb_open_ro_fn callback to it (open_fn argument).
|
||||
*
|
||||
* Once registered, you can `loadfile myprotocol://myfile`. Your open_fn will be
|
||||
* invoked with the URI and you must fill out the provided mpv_stream_cb_info
|
||||
* struct. This includes your stream callbacks (like read_fn), and an opaque
|
||||
* cookie, which will be passed as the first argument to all the remaining
|
||||
* stream callbacks.
|
||||
*
|
||||
* Note that your custom callbacks must not invoke libmpv APIs as that would
|
||||
* cause a deadlock.
|
||||
*
|
||||
* Stream lifetime
|
||||
* ---------------
|
||||
*
|
||||
* A stream remains valid until its close callback has been called. It's up to
|
||||
* libmpv to call the close callback, and the libmpv user cannot close it
|
||||
* directly with the stream_cb API.
|
||||
*
|
||||
* For example, if you consider your custom stream to become suddenly invalid
|
||||
* (maybe because the underlying stream died), libmpv will continue using your
|
||||
* stream. All you can do is returning errors from each callback, until libmpv
|
||||
* gives up and closes it.
|
||||
*
|
||||
* Protocol registration and lifetime
|
||||
* ----------------------------------
|
||||
*
|
||||
* Protocols remain registered until the mpv instance is terminated. This means
|
||||
* in particular that it can outlive the mpv_handle that was used to register
|
||||
* it, but once mpv_terminate_destroy() is called, your registered callbacks
|
||||
* will not be called again.
|
||||
*
|
||||
* Protocol unregistration is finished after the mpv core has been destroyed
|
||||
* (e.g. after mpv_terminate_destroy() has returned).
|
||||
*
|
||||
* If you do not call mpv_terminate_destroy() yourself (e.g. plugin-style code),
|
||||
* you will have to deal with the registration or even streams outliving your
|
||||
* code. Here are some possible ways to do this:
|
||||
* - call mpv_terminate_destroy(), which destroys the core, and will make sure
|
||||
* all streams are closed once this function returns
|
||||
* - you refcount all resources your stream "cookies" reference, so that it
|
||||
* doesn't matter if streams live longer than expected
|
||||
* - create "cancellation" semantics: after your protocol has been unregistered,
|
||||
* notify all your streams that are still opened, and make them drop all
|
||||
* referenced resources - then return errors from the stream callbacks as
|
||||
* long as the stream is still opened
|
||||
*
|
||||
*/
|
||||
|
||||
/**
|
||||
* Read callback used to implement a custom stream. The semantics of the
|
||||
* callback match read(2) in blocking mode. Short reads are allowed (you can
|
||||
* return less bytes than requested, and libmpv will retry reading the rest
|
||||
* with a nother call). If no data can be immediately read, the callback must
|
||||
* block until there is new data. A return of 0 will be interpreted as final
|
||||
* EOF, although libmpv might retry the read, or seek to a different position.
|
||||
*
|
||||
* @param cookie opaque cookie identifying the stream,
|
||||
* returned from mpv_stream_cb_open_fn
|
||||
* @param buf buffer to read data into
|
||||
* @param size of the buffer
|
||||
* @return number of bytes read into the buffer
|
||||
* @return 0 on EOF
|
||||
* @return -1 on error
|
||||
*/
|
||||
typedef int64_t (*mpv_stream_cb_read_fn)(void *cookie, char *buf, uint64_t nbytes);
|
||||
|
||||
/**
|
||||
* Seek callback used to implement a custom stream.
|
||||
*
|
||||
* Note that mpv will issue a seek to position 0 immediately after opening. This
|
||||
* is used to test whether the stream is seekable (since seekability might
|
||||
* depend on the URI contents, not just the protocol). Return
|
||||
* MPV_ERROR_UNSUPPORTED if seeking is not implemented for this stream. This
|
||||
* seek also servies to establish the fact that streams start at position 0.
|
||||
*
|
||||
* This callback can be NULL, in which it behaves as if always returning
|
||||
* MPV_ERROR_UNSUPPORTED.
|
||||
*
|
||||
* @param cookie opaque cookie identifying the stream,
|
||||
* returned from mpv_stream_cb_open_fn
|
||||
* @param offset target absolut stream position
|
||||
* @return the resulting offset of the stream
|
||||
* MPV_ERROR_UNSUPPORTED or MPV_ERROR_GENERIC if the seek failed
|
||||
*/
|
||||
typedef int64_t (*mpv_stream_cb_seek_fn)(void *cookie, int64_t offset);
|
||||
|
||||
/**
|
||||
* Size callback used to implement a custom stream.
|
||||
*
|
||||
* Return MPV_ERROR_UNSUPPORTED if no size is known.
|
||||
*
|
||||
* This callback can be NULL, in which it behaves as if always returning
|
||||
* MPV_ERROR_UNSUPPORTED.
|
||||
*
|
||||
* @param cookie opaque cookie identifying the stream,
|
||||
* returned from mpv_stream_cb_open_fn
|
||||
* @return the total size in bytes of the stream
|
||||
*/
|
||||
typedef int64_t (*mpv_stream_cb_size_fn)(void *cookie);
|
||||
|
||||
/**
|
||||
* Close callback used to implement a custom stream.
|
||||
*
|
||||
* @param cookie opaque cookie identifying the stream,
|
||||
* returned from mpv_stream_cb_open_fn
|
||||
*/
|
||||
typedef void (*mpv_stream_cb_close_fn)(void *cookie);
|
||||
|
||||
/**
|
||||
* Values for the mpv_stream_cb_control_fn cmd argument.
|
||||
*/
|
||||
typedef struct mpv_stream_cb_info {
|
||||
/**
|
||||
* Opaque user-provided value, which will be passed to the other callbacks.
|
||||
* The close callback will be called to release the cookie. It is not
|
||||
* interpreted by mpv. It doesn't even need to be a valid pointer.
|
||||
*
|
||||
* The user sets this in the mpv_stream_cb_open_ro_fn callback.
|
||||
*/
|
||||
void *cookie;
|
||||
|
||||
/**
|
||||
* Callbacks set by the user in the mpv_stream_cb_open_ro_fn callback. Some
|
||||
* of them are optional, and can be left unset.
|
||||
*
|
||||
* The following callbacks are mandatory: read_fn, close_fn
|
||||
*/
|
||||
mpv_stream_cb_read_fn read_fn;
|
||||
mpv_stream_cb_seek_fn seek_fn;
|
||||
mpv_stream_cb_size_fn size_fn;
|
||||
mpv_stream_cb_close_fn close_fn;
|
||||
} mpv_stream_cb_info;
|
||||
|
||||
/**
|
||||
* Open callback used to implement a custom read-only (ro) stream. The user
|
||||
* must set the callback fields in the passed info struct. The cookie field
|
||||
* also can be set to store state associated to the stream instance.
|
||||
*
|
||||
* Note that the info struct is valid only for the duration of this callback.
|
||||
* You can't change the callbacks or the pointer to the cookie at a later point.
|
||||
*
|
||||
* Each stream instance created by the open callback can have different
|
||||
* callbacks.
|
||||
*
|
||||
* The close_fn callback will terminate the stream instance. The pointers to
|
||||
* your callbacks and cookie will be discarded, and the callbacks will not be
|
||||
* called again.
|
||||
*
|
||||
* @param user_data opaque user data provided via mpv_stream_cb_add()
|
||||
* @param uri name of the stream to be opened (with protocol prefix)
|
||||
* @param info fields which the user should fill
|
||||
* @return opaque cookie identifing the newly opened stream
|
||||
* @return 0 on success, MPV_ERROR_LOADING_FAILED if the URI cannot be opened.
|
||||
*/
|
||||
typedef int (*mpv_stream_cb_open_ro_fn)(void *user_data, char *uri,
|
||||
mpv_stream_cb_info *info);
|
||||
|
||||
/**
|
||||
* Add a custom stream protocol. This will register a protocol handler under
|
||||
* the given protocol prefix, and invoke the given callbacks if an URI with the
|
||||
* matching protocol prefix is opened.
|
||||
*
|
||||
* The "ro" is for read-only - only read-only streams can be registered with
|
||||
* this function.
|
||||
*
|
||||
* The callback remains registered until the mpv core is registered.
|
||||
*
|
||||
* If a custom stream with the same name is already registered, then the
|
||||
* MPV_ERROR_INVALID_PARAMETER error is returned.
|
||||
*
|
||||
* @param protocol protocol prefix, for example "foo" for "foo://" URIs
|
||||
* @param user_data opaque pointer passed into the mpv_stream_cb_open_fn
|
||||
* callback.
|
||||
* @return error code
|
||||
*/
|
||||
int mpv_stream_cb_add_ro(mpv_handle *ctx, const char *protocol, void *user_data,
|
||||
mpv_stream_cb_open_ro_fn open_fn);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif
|
|
@ -24,6 +24,7 @@
|
|||
#include "common/global.h"
|
||||
#include "common/msg.h"
|
||||
#include "common/msg_control.h"
|
||||
#include "common/global.h"
|
||||
#include "input/input.h"
|
||||
#include "input/cmd_list.h"
|
||||
#include "misc/ctype.h"
|
||||
|
@ -62,9 +63,13 @@ struct mp_client_api {
|
|||
pthread_mutex_t lock;
|
||||
|
||||
// -- protected by lock
|
||||
|
||||
struct mpv_handle **clients;
|
||||
int num_clients;
|
||||
uint64_t event_masks; // combined events of all clients, or 0 if unknown
|
||||
|
||||
struct mp_custom_protocol *custom_protocols;
|
||||
int num_custom_protocols;
|
||||
};
|
||||
|
||||
struct observe_property {
|
||||
|
@ -1723,3 +1728,59 @@ void *mpv_get_sub_api(mpv_handle *ctx, mpv_sub_api sub_api)
|
|||
unlock_core(ctx);
|
||||
return res;
|
||||
}
|
||||
|
||||
struct mp_custom_protocol {
|
||||
char *protocol;
|
||||
void *user_data;
|
||||
mpv_stream_cb_open_ro_fn open_fn;
|
||||
};
|
||||
|
||||
int mpv_stream_cb_add_ro(mpv_handle *ctx, const char *protocol, void *user_data,
|
||||
mpv_stream_cb_open_ro_fn open_fn)
|
||||
{
|
||||
if (!open_fn)
|
||||
return MPV_ERROR_INVALID_PARAMETER;
|
||||
|
||||
struct mp_client_api *clients = ctx->clients;
|
||||
int r = 0;
|
||||
pthread_mutex_lock(&clients->lock);
|
||||
for (int n = 0; n < clients->num_custom_protocols; n++) {
|
||||
struct mp_custom_protocol *proto = &clients->custom_protocols[n];
|
||||
if (strcmp(proto->protocol, protocol) == 0) {
|
||||
r = MPV_ERROR_INVALID_PARAMETER;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (stream_has_proto(protocol))
|
||||
r = MPV_ERROR_INVALID_PARAMETER;
|
||||
if (r >= 0) {
|
||||
struct mp_custom_protocol proto = {
|
||||
.protocol = talloc_strdup(clients, protocol),
|
||||
.user_data = user_data,
|
||||
.open_fn = open_fn,
|
||||
};
|
||||
MP_TARRAY_APPEND(clients, clients->custom_protocols,
|
||||
clients->num_custom_protocols, proto);
|
||||
}
|
||||
pthread_mutex_unlock(&clients->lock);
|
||||
return r;
|
||||
}
|
||||
|
||||
bool mp_streamcb_lookup(struct mpv_global *g, const char *protocol,
|
||||
void **out_user_data, mpv_stream_cb_open_ro_fn *out_fn)
|
||||
{
|
||||
struct mp_client_api *clients = g->client_api;
|
||||
bool found = false;
|
||||
pthread_mutex_lock(&clients->lock);
|
||||
for (int n = 0; n < clients->num_custom_protocols; n++) {
|
||||
struct mp_custom_protocol *proto = &clients->custom_protocols[n];
|
||||
if (strcmp(proto->protocol, protocol) == 0) {
|
||||
*out_user_data = proto->user_data;
|
||||
*out_fn = proto->open_fn;
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
pthread_mutex_unlock(&clients->lock);
|
||||
return found;
|
||||
}
|
||||
|
|
|
@ -5,6 +5,7 @@
|
|||
#include <stdbool.h>
|
||||
|
||||
#include "libmpv/client.h"
|
||||
#include "libmpv/stream_cb.h"
|
||||
|
||||
struct MPContext;
|
||||
struct mpv_handle;
|
||||
|
@ -46,4 +47,7 @@ struct mpv_opengl_cb_context *mp_opengl_create(struct mpv_global *g,
|
|||
struct mp_client_api *client_api);
|
||||
void kill_video(struct mp_client_api *client_api);
|
||||
|
||||
bool mp_streamcb_lookup(struct mpv_global *g, const char *protocol,
|
||||
void **out_user_data, mpv_stream_cb_open_ro_fn *out_fn);
|
||||
|
||||
#endif
|
||||
|
|
|
@ -75,6 +75,7 @@ extern const stream_info_t stream_info_bdnav;
|
|||
extern const stream_info_t stream_info_rar;
|
||||
extern const stream_info_t stream_info_edl;
|
||||
extern const stream_info_t stream_info_libarchive;
|
||||
extern const stream_info_t stream_info_cb;
|
||||
|
||||
static const stream_info_t *const stream_list[] = {
|
||||
#if HAVE_CDDA
|
||||
|
@ -115,6 +116,7 @@ static const stream_info_t *const stream_list[] = {
|
|||
&stream_info_edl,
|
||||
&stream_info_rar,
|
||||
&stream_info_file,
|
||||
&stream_info_cb,
|
||||
NULL
|
||||
};
|
||||
|
||||
|
@ -243,6 +245,9 @@ static stream_t *new_stream(void)
|
|||
|
||||
static const char *match_proto(const char *url, const char *proto)
|
||||
{
|
||||
if (strcmp(proto, "*") == 0)
|
||||
return url;
|
||||
|
||||
int l = strlen(proto);
|
||||
if (l > 0) {
|
||||
if (strncasecmp(url, proto, l) == 0 && strncmp("://", url + l, 3) == 0)
|
||||
|
@ -1111,3 +1116,17 @@ void stream_print_proto_list(struct mp_log *log)
|
|||
talloc_free(list);
|
||||
mp_info(log, "\nTotal: %d protocols\n", count);
|
||||
}
|
||||
|
||||
bool stream_has_proto(const char *proto)
|
||||
{
|
||||
for (int i = 0; stream_list[i]; i++) {
|
||||
const stream_info_t *stream_info = stream_list[i];
|
||||
|
||||
for (int j = 0; stream_info->protocols && stream_info->protocols[j]; j++) {
|
||||
if (strcmp(stream_info->protocols[j], proto) == 0)
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
|
|
@ -299,5 +299,6 @@ void mp_setup_av_network_options(struct AVDictionary **dict,
|
|||
|
||||
void stream_print_proto_list(struct mp_log *log);
|
||||
char **stream_get_proto_list(void);
|
||||
bool stream_has_proto(const char *proto);
|
||||
|
||||
#endif /* MPLAYER_STREAM_H */
|
||||
|
|
|
@ -0,0 +1,108 @@
|
|||
#include "config.h"
|
||||
|
||||
#include <stdio.h>
|
||||
#include <sys/types.h>
|
||||
#include <sys/stat.h>
|
||||
#include <fcntl.h>
|
||||
#include <unistd.h>
|
||||
#include <errno.h>
|
||||
|
||||
#include "osdep/io.h"
|
||||
|
||||
#include "common/common.h"
|
||||
#include "common/msg.h"
|
||||
#include "common/global.h"
|
||||
#include "stream.h"
|
||||
#include "options/m_option.h"
|
||||
#include "options/path.h"
|
||||
#include "player/client.h"
|
||||
#include "libmpv/stream_cb.h"
|
||||
|
||||
struct priv {
|
||||
mpv_stream_cb_info info;
|
||||
};
|
||||
|
||||
static int fill_buffer(stream_t *s, char *buffer, int max_len)
|
||||
{
|
||||
struct priv *p = s->priv;
|
||||
return (int)p->info.read_fn(p->info.cookie, buffer, (size_t)max_len);
|
||||
}
|
||||
|
||||
static int seek(stream_t *s, int64_t newpos)
|
||||
{
|
||||
struct priv *p = s->priv;
|
||||
return (int)p->info.seek_fn(p->info.cookie, newpos) >= 0;
|
||||
}
|
||||
|
||||
static int control(stream_t *s, int cmd, void *arg)
|
||||
{
|
||||
struct priv *p = s->priv;
|
||||
switch (cmd) {
|
||||
case STREAM_CTRL_GET_SIZE: {
|
||||
if (!p->info.size_fn)
|
||||
break;
|
||||
int64_t size = p->info.size_fn(p->info.cookie);
|
||||
if (size >= 0) {
|
||||
*(int64_t *)arg = size;
|
||||
return 1;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
return STREAM_UNSUPPORTED;
|
||||
}
|
||||
|
||||
static void s_close(stream_t *s)
|
||||
{
|
||||
struct priv *p = s->priv;
|
||||
p->info.close_fn(p->info.cookie);
|
||||
}
|
||||
|
||||
static int open_cb(stream_t *stream)
|
||||
{
|
||||
struct priv *p = talloc_ptrtype(stream, p);
|
||||
stream->priv = p;
|
||||
|
||||
bstr bproto = mp_split_proto(bstr0(stream->url), NULL);
|
||||
char *proto = bstrto0(stream, bproto);
|
||||
|
||||
void *user_data;
|
||||
mpv_stream_cb_open_ro_fn open_fn;
|
||||
|
||||
if (!mp_streamcb_lookup(stream->global, proto, &user_data, &open_fn))
|
||||
return STREAM_UNSUPPORTED;
|
||||
|
||||
mpv_stream_cb_info info = {0};
|
||||
|
||||
int r = open_fn(user_data, stream->url, &info);
|
||||
if (r < 0) {
|
||||
if (r != MPV_ERROR_LOADING_FAILED)
|
||||
MP_WARN(stream, "unknown error from user callback\n");
|
||||
return STREAM_ERROR;
|
||||
}
|
||||
|
||||
if (!info.read_fn || !info.close_fn) {
|
||||
MP_FATAL(stream, "required read_fn or close_fn callbacks not set.\n");
|
||||
return STREAM_ERROR;
|
||||
}
|
||||
|
||||
p->info = info;
|
||||
|
||||
if (p->info.seek_fn && p->info.seek_fn(p->info.cookie, 0) >= 0) {
|
||||
stream->seek = seek;
|
||||
stream->seekable = true;
|
||||
}
|
||||
stream->fast_skip = true;
|
||||
stream->fill_buffer = fill_buffer;
|
||||
stream->control = control;
|
||||
stream->read_chunk = 64 * 1024;
|
||||
stream->close = s_close;
|
||||
|
||||
return STREAM_OK;
|
||||
}
|
||||
|
||||
const stream_info_t stream_info_cb = {
|
||||
.name = "stream_callback",
|
||||
.open = open_cb,
|
||||
.protocols = (const char*const[]){ "*", NULL },
|
||||
};
|
|
@ -247,6 +247,7 @@ def build(ctx):
|
|||
( "stream/stream_dvdnav.c", "dvdnav" ),
|
||||
( "stream/stream_edl.c" ),
|
||||
( "stream/stream_file.c" ),
|
||||
( "stream/stream_cb.c" ),
|
||||
( "stream/stream_lavf.c" ),
|
||||
( "stream/stream_libarchive.c", "libarchive" ),
|
||||
( "stream/stream_memory.c" ),
|
||||
|
@ -548,7 +549,7 @@ def build(ctx):
|
|||
PRIV_LIBS = get_deps(),
|
||||
)
|
||||
|
||||
headers = ["client.h", "qthelper.hpp", "opengl_cb.h"]
|
||||
headers = ["client.h", "qthelper.hpp", "opengl_cb.h", "stream_cb.h"]
|
||||
for f in headers:
|
||||
ctx.install_as(ctx.env.INCDIR + '/mpv/' + f, 'libmpv/' + f)
|
||||
|
||||
|
|
Loading…
Reference in New Issue