mirror of
https://github.com/owntone/owntone-server.git
synced 2024-12-27 15:45:56 -05:00
Merge branch 'castfix2'
This commit is contained in:
commit
e3308a464b
@ -317,6 +317,10 @@ audio {
|
||||
# Chromecast settings
|
||||
# (make sure you get the capitalization of the device name right)
|
||||
#chromecast "My Chromecast device" {
|
||||
# forked-daapd's volume goes to 11! If that's more than you can handle
|
||||
# you can set a lower value here
|
||||
# max_volume = 11
|
||||
|
||||
# Enable this option to exclude a particular device from the speaker
|
||||
# list
|
||||
# exclude = false
|
||||
|
137
src/artwork.c
137
src/artwork.c
@ -60,8 +60,7 @@
|
||||
*
|
||||
* An artwork source handler must return one of the following:
|
||||
*
|
||||
* ART_FMT_JPEG (positive) Found a jpeg
|
||||
* ART_FMT_PNG (positive) Found a png
|
||||
* ART_FMT_XXXX (positive) An image, see possible formats in artwork.h
|
||||
* ART_E_NONE (zero) No artwork found
|
||||
* ART_E_ERROR (negative) An error occurred while searching for artwork
|
||||
* ART_E_ABORT (negative) Caller should abort artwork search (may be returned by cache)
|
||||
@ -81,6 +80,14 @@ enum artwork_cache
|
||||
ON_FAILURE = 2, // Cache if artwork not found (so we don't keep asking)
|
||||
};
|
||||
|
||||
// Input data to handlers, requested width, height and format. Can be set to
|
||||
// zero then source is returned.
|
||||
struct artwork_req_params {
|
||||
int max_w;
|
||||
int max_h;
|
||||
int format;
|
||||
};
|
||||
|
||||
/* This struct contains the data available to the handler, as well as a char
|
||||
* buffer where the handler should output the path to the artwork (if it is
|
||||
* local - otherwise the buffer can be left empty). The purpose of supplying the
|
||||
@ -93,9 +100,9 @@ struct artwork_ctx {
|
||||
// Handler should output artwork data to this evbuffer
|
||||
struct evbuffer *evbuf;
|
||||
|
||||
// Input data to handler, requested width and height
|
||||
int max_w;
|
||||
int max_h;
|
||||
// Requested size and format
|
||||
struct artwork_req_params req_params;
|
||||
|
||||
// Input data to handler, did user configure to look for individual artwork
|
||||
int individual;
|
||||
|
||||
@ -576,26 +583,26 @@ rescale_calculate(int *target_w, int *target_h, int width, int height, int max_w
|
||||
* @out evbuf Image data (rescaled if needed)
|
||||
* @in path Path to the artwork file (alternative to inbuf)
|
||||
* @in inbuf Buffer with the artwork (alternative to path)
|
||||
* @in max_w Requested width
|
||||
* @in max_h Requested height
|
||||
* @in is_embedded Whether the artwork in file is embedded or raw jpeg/png
|
||||
* @in data_kind Used by the transcode module to determine e.g. probe size
|
||||
* @in req_params Requested max size/format
|
||||
* @return ART_FMT_* on success, ART_E_ERROR on error
|
||||
*/
|
||||
static int
|
||||
artwork_get(struct evbuffer *evbuf, char *path, struct evbuffer *inbuf, int max_w, int max_h, bool is_embedded, enum data_kind data_kind)
|
||||
artwork_get(struct evbuffer *evbuf, char *path, struct evbuffer *inbuf, bool is_embedded, enum data_kind data_kind, struct artwork_req_params req_params)
|
||||
{
|
||||
struct decode_ctx *xcode_decode;
|
||||
struct encode_ctx *xcode_encode;
|
||||
void *frame;
|
||||
int width;
|
||||
int height;
|
||||
int target_w;
|
||||
int target_h;
|
||||
int format_ok;
|
||||
int src_width;
|
||||
int src_height;
|
||||
int src_format;
|
||||
int dst_width;
|
||||
int dst_height;
|
||||
int dst_format;
|
||||
int ret;
|
||||
|
||||
DPRINTF(E_SPAM, L_ART, "Getting artwork (max destination width %d height %d)\n", max_w, max_h);
|
||||
DPRINTF(E_SPAM, L_ART, "Getting artwork (max destination width %d height %d)\n", req_params.max_w, req_params.max_h);
|
||||
|
||||
xcode_decode = transcode_decode_setup(XCODE_JPEG, NULL, data_kind, path, inbuf, 0); // Covers XCODE_PNG too
|
||||
if (!xcode_decode)
|
||||
@ -608,9 +615,9 @@ artwork_get(struct evbuffer *evbuf, char *path, struct evbuffer *inbuf, int max_
|
||||
}
|
||||
|
||||
if (transcode_decode_query(xcode_decode, "is_jpeg"))
|
||||
format_ok = ART_FMT_JPEG;
|
||||
src_format = ART_FMT_JPEG;
|
||||
else if (transcode_decode_query(xcode_decode, "is_png"))
|
||||
format_ok = ART_FMT_PNG;
|
||||
src_format = ART_FMT_PNG;
|
||||
else
|
||||
{
|
||||
if (is_embedded)
|
||||
@ -623,18 +630,18 @@ artwork_get(struct evbuffer *evbuf, char *path, struct evbuffer *inbuf, int max_
|
||||
goto fail_free_decode;
|
||||
}
|
||||
|
||||
width = transcode_decode_query(xcode_decode, "width");
|
||||
height = transcode_decode_query(xcode_decode, "height");
|
||||
src_width = transcode_decode_query(xcode_decode, "width");
|
||||
src_height = transcode_decode_query(xcode_decode, "height");
|
||||
|
||||
ret = rescale_calculate(&target_w, &target_h, width, height, max_w, max_h);
|
||||
ret = rescale_calculate(&dst_width, &dst_height, src_width, src_height, req_params.max_w, req_params.max_h);
|
||||
if (ret < 0)
|
||||
{
|
||||
if (is_embedded)
|
||||
{
|
||||
target_w = width;
|
||||
target_h = height;
|
||||
dst_width = src_width;
|
||||
dst_height = src_height;
|
||||
}
|
||||
else if (path)
|
||||
else if (path && src_format == req_params.format)
|
||||
{
|
||||
// No rescaling required, just read the raw file into the evbuf
|
||||
ret = artwork_read_bypath(evbuf, path);
|
||||
@ -642,7 +649,7 @@ artwork_get(struct evbuffer *evbuf, char *path, struct evbuffer *inbuf, int max_
|
||||
goto fail_free_decode;
|
||||
|
||||
transcode_decode_cleanup(&xcode_decode);
|
||||
return format_ok;
|
||||
return src_format;
|
||||
}
|
||||
else
|
||||
{
|
||||
@ -650,10 +657,15 @@ artwork_get(struct evbuffer *evbuf, char *path, struct evbuffer *inbuf, int max_
|
||||
}
|
||||
}
|
||||
|
||||
if (format_ok == ART_FMT_JPEG)
|
||||
xcode_encode = transcode_encode_setup(XCODE_JPEG, NULL, xcode_decode, NULL, target_w, target_h);
|
||||
dst_format = req_params.format ? req_params.format : src_format;
|
||||
if (dst_format == ART_FMT_JPEG)
|
||||
xcode_encode = transcode_encode_setup(XCODE_JPEG, NULL, xcode_decode, NULL, dst_width, dst_height);
|
||||
else if (dst_format == ART_FMT_PNG)
|
||||
xcode_encode = transcode_encode_setup(XCODE_PNG, NULL, xcode_decode, NULL, dst_width, dst_height);
|
||||
else if (dst_format == ART_FMT_VP8)
|
||||
xcode_encode = transcode_encode_setup(XCODE_VP8, NULL, xcode_decode, NULL, dst_width, dst_height);
|
||||
else
|
||||
xcode_encode = transcode_encode_setup(XCODE_PNG, NULL, xcode_decode, NULL, target_w, target_h);
|
||||
xcode_encode = transcode_encode_setup(XCODE_JPEG, NULL, xcode_decode, NULL, dst_width, dst_height);
|
||||
|
||||
if (!xcode_encode)
|
||||
{
|
||||
@ -680,7 +692,7 @@ artwork_get(struct evbuffer *evbuf, char *path, struct evbuffer *inbuf, int max_
|
||||
return ART_E_ERROR;
|
||||
}
|
||||
|
||||
return format_ok;
|
||||
return dst_format;
|
||||
|
||||
fail_free_encode:
|
||||
transcode_encode_cleanup(&xcode_encode);
|
||||
@ -689,16 +701,15 @@ artwork_get(struct evbuffer *evbuf, char *path, struct evbuffer *inbuf, int max_
|
||||
return ART_E_ERROR;
|
||||
}
|
||||
|
||||
/* Rescales an image in an evbuf (if required)
|
||||
/* Rescales and reformats an image in an evbuf (if required)
|
||||
*
|
||||
* @out artwork Rescaled image data (or original, if not rescaled)
|
||||
* @in raw Original image data
|
||||
* @in max_w Requested max width
|
||||
* @in max_h Requested max height
|
||||
* @in req_params Requested max size/format
|
||||
* @return 0 on success, -1 on error
|
||||
*/
|
||||
static int
|
||||
artwork_evbuf_rescale(struct evbuffer *artwork, struct evbuffer *raw, int max_w, int max_h)
|
||||
artwork_evbuf_rescale(struct evbuffer *artwork, struct evbuffer *raw, struct artwork_req_params req_params)
|
||||
{
|
||||
struct evbuffer *refbuf;
|
||||
int ret;
|
||||
@ -726,7 +737,7 @@ artwork_evbuf_rescale(struct evbuffer *artwork, struct evbuffer *raw, int max_w,
|
||||
}
|
||||
|
||||
// For non-file input, artwork_get() will also fail if no rescaling is required
|
||||
ret = artwork_get(artwork, NULL, refbuf, max_w, max_h, false, 0);
|
||||
ret = artwork_get(artwork, NULL, refbuf, false, 0, req_params);
|
||||
if (ret == ART_E_ERROR)
|
||||
{
|
||||
DPRINTF(E_DBG, L_ART, "No rescaling required\n");
|
||||
@ -878,28 +889,27 @@ parent_dir_image_find(char *out_path, size_t len, const char *dir)
|
||||
/* Looks for an artwork file in a directory. Will rescale if needed.
|
||||
*
|
||||
* @out evbuf Image data
|
||||
* @in dir Directory to search
|
||||
* @in max_w Requested width
|
||||
* @in max_h Requested height
|
||||
* @out out_path Path to the artwork file if found, must be a char[PATH_MAX] buffer
|
||||
* @in len Max size of "out_path"
|
||||
* @in dir Directory to search
|
||||
* @in req_params Requested max size/format
|
||||
* @return ART_FMT_* on success, ART_E_NONE on nothing found, ART_E_ERROR on error
|
||||
*/
|
||||
static int
|
||||
artwork_get_bydir(struct evbuffer *evbuf, char *dir, int max_w, int max_h, char *out_path, size_t len)
|
||||
artwork_get_bydir(struct evbuffer *evbuf, char *out_path, size_t len, char *dir, struct artwork_req_params req_params)
|
||||
{
|
||||
int ret;
|
||||
|
||||
ret = dir_image_find(out_path, len, dir);
|
||||
if (ret >= 0)
|
||||
{
|
||||
return artwork_get(evbuf, out_path, NULL, max_w, max_h, false, DATA_KIND_FILE);
|
||||
return artwork_get(evbuf, out_path, NULL, false, DATA_KIND_FILE, req_params);
|
||||
}
|
||||
|
||||
ret = parent_dir_image_find(out_path, len, dir);
|
||||
if (ret >= 0)
|
||||
{
|
||||
return artwork_get(evbuf, out_path, NULL, max_w, max_h, false, DATA_KIND_FILE);
|
||||
return artwork_get(evbuf, out_path, NULL, false, DATA_KIND_FILE, req_params);
|
||||
}
|
||||
|
||||
return ART_E_NONE;
|
||||
@ -910,12 +920,11 @@ artwork_get_bydir(struct evbuffer *evbuf, char *dir, int max_w, int max_h, char
|
||||
*
|
||||
* @out artwork Image data
|
||||
* @in url URL of the artwork
|
||||
* @in max_w Requested max width
|
||||
* @in max_h Requested max height
|
||||
* @in req_params Requested max size/format
|
||||
* @return ART_FMT_* on success, ART_E_NONE or ART_E_ERROR
|
||||
*/
|
||||
static int
|
||||
artwork_get_byurl(struct evbuffer *artwork, const char *url, int max_w, int max_h)
|
||||
artwork_get_byurl(struct evbuffer *artwork, const char *url, struct artwork_req_params req_params)
|
||||
{
|
||||
struct evbuffer *raw;
|
||||
int format;
|
||||
@ -935,7 +944,7 @@ artwork_get_byurl(struct evbuffer *artwork, const char *url, int max_w, int max_
|
||||
if (format <= 0)
|
||||
goto out;
|
||||
|
||||
ret = artwork_evbuf_rescale(artwork, raw, max_w, max_h);
|
||||
ret = artwork_evbuf_rescale(artwork, raw, req_params);
|
||||
if (ret < 0)
|
||||
format = ART_E_ERROR;
|
||||
|
||||
@ -1250,7 +1259,7 @@ online_source_search(struct online_source *src, struct artwork_ctx *ctx)
|
||||
|
||||
// Be nice to our peer + improve response times by not repeating search requests
|
||||
hash = djb_hash(url, strlen(url));
|
||||
ret = online_source_search_check_last(&artwork_url, src, hash, ctx->max_w, ctx->max_h);
|
||||
ret = online_source_search_check_last(&artwork_url, src, hash, ctx->req_params.max_w, ctx->req_params.max_h);
|
||||
if (ret == 0)
|
||||
{
|
||||
return artwork_url; // Will be NULL if we are repeating a search that failed
|
||||
@ -1288,7 +1297,7 @@ online_source_search(struct online_source *src, struct artwork_ctx *ctx)
|
||||
goto error;
|
||||
}
|
||||
|
||||
ret = online_source_response_parse(&artwork_url, src, client.input_body, ctx->max_w, ctx->max_h);
|
||||
ret = online_source_response_parse(&artwork_url, src, client.input_body, ctx->req_params.max_w, ctx->req_params.max_h);
|
||||
if (ret == ONLINE_SOURCE_PARSE_NOT_FOUND)
|
||||
DPRINTF(E_DBG, L_ART, "No image tag found in response from source '%s'\n", src->name);
|
||||
else if (ret == ONLINE_SOURCE_PARSE_INVALID)
|
||||
@ -1338,7 +1347,7 @@ source_group_cache_get(struct artwork_ctx *ctx)
|
||||
int cached;
|
||||
int ret;
|
||||
|
||||
ret = cache_artwork_get(CACHE_ARTWORK_GROUP, ctx->persistentid, ctx->max_w, ctx->max_h, &cached, &format, ctx->evbuf);
|
||||
ret = cache_artwork_get(CACHE_ARTWORK_GROUP, ctx->persistentid, ctx->req_params.max_w, ctx->req_params.max_h, &cached, &format, ctx->evbuf);
|
||||
if (ret < 0)
|
||||
return ART_E_ERROR;
|
||||
|
||||
@ -1381,7 +1390,7 @@ source_group_dir_get(struct artwork_ctx *ctx)
|
||||
if (access(dir, F_OK) < 0)
|
||||
continue;
|
||||
|
||||
ret = artwork_get_bydir(ctx->evbuf, dir, ctx->max_w, ctx->max_h, ctx->path, sizeof(ctx->path));
|
||||
ret = artwork_get_bydir(ctx->evbuf, ctx->path, sizeof(ctx->path), dir, ctx->req_params);
|
||||
if (ret > 0)
|
||||
{
|
||||
db_query_end(&qp);
|
||||
@ -1413,7 +1422,7 @@ source_item_cache_get(struct artwork_ctx *ctx)
|
||||
if (!ctx->individual)
|
||||
return ART_E_NONE;
|
||||
|
||||
ret = cache_artwork_get(CACHE_ARTWORK_INDIVIDUAL, ctx->id, ctx->max_w, ctx->max_h, &cached, &format, ctx->evbuf);
|
||||
ret = cache_artwork_get(CACHE_ARTWORK_INDIVIDUAL, ctx->id, ctx->req_params.max_w, ctx->req_params.max_h, &cached, &format, ctx->evbuf);
|
||||
if (ret < 0)
|
||||
return ART_E_ERROR;
|
||||
|
||||
@ -1446,7 +1455,7 @@ source_item_embedded_get(struct artwork_ctx *ctx)
|
||||
|
||||
snprintf(ctx->path, sizeof(ctx->path), "%s", ctx->dbmfi->path);
|
||||
|
||||
return artwork_get(ctx->evbuf, ctx->path, NULL, ctx->max_w, ctx->max_h, true, ctx->data_kind);
|
||||
return artwork_get(ctx->evbuf, ctx->path, NULL, true, ctx->data_kind, ctx->req_params);
|
||||
}
|
||||
|
||||
/* Looks for basename(in_path).{png,jpg}, so if in_path is /foo/bar.mp3 it
|
||||
@ -1500,7 +1509,7 @@ source_item_own_get(struct artwork_ctx *ctx)
|
||||
|
||||
snprintf(ctx->path, sizeof(ctx->path), "%s", path);
|
||||
|
||||
return artwork_get(ctx->evbuf, path, NULL, ctx->max_w, ctx->max_h, false, ctx->data_kind);
|
||||
return artwork_get(ctx->evbuf, path, NULL, false, ctx->data_kind, ctx->req_params);
|
||||
}
|
||||
|
||||
/*
|
||||
@ -1521,7 +1530,7 @@ source_item_artwork_url_get(struct artwork_ctx *ctx)
|
||||
return ART_E_NONE;
|
||||
}
|
||||
|
||||
ret = artwork_get_byurl(ctx->evbuf, queue_item->artwork_url, ctx->max_w, ctx->max_h);
|
||||
ret = artwork_get_byurl(ctx->evbuf, queue_item->artwork_url, ctx->req_params);
|
||||
|
||||
snprintf(ctx->path, sizeof(ctx->path), "%s", queue_item->artwork_url);
|
||||
|
||||
@ -1557,7 +1566,7 @@ source_item_pipe_get(struct artwork_ctx *ctx)
|
||||
|
||||
free_queue_item(queue_item, 0);
|
||||
|
||||
return artwork_get(ctx->evbuf, ctx->path, NULL, ctx->max_w, ctx->max_h, false, ctx->data_kind);
|
||||
return artwork_get(ctx->evbuf, ctx->path, NULL, false, ctx->data_kind, ctx->req_params);
|
||||
}
|
||||
|
||||
static int
|
||||
@ -1575,7 +1584,7 @@ source_item_discogs_get(struct artwork_ctx *ctx)
|
||||
|
||||
snprintf(ctx->path, sizeof(ctx->path), "%s", url);
|
||||
|
||||
ret = artwork_get_byurl(ctx->evbuf, url, ctx->max_w, ctx->max_h);
|
||||
ret = artwork_get_byurl(ctx->evbuf, url, ctx->req_params);
|
||||
|
||||
free(url);
|
||||
return ret;
|
||||
@ -1598,7 +1607,7 @@ source_item_coverartarchive_get(struct artwork_ctx *ctx)
|
||||
|
||||
snprintf(ctx->path, sizeof(ctx->path), "%s", url);
|
||||
|
||||
ret = artwork_get_byurl(ctx->evbuf, url, ctx->max_w, ctx->max_h);
|
||||
ret = artwork_get_byurl(ctx->evbuf, url, ctx->req_params);
|
||||
|
||||
free(url);
|
||||
return ret;
|
||||
@ -1611,14 +1620,14 @@ source_item_spotifywebapi_track_get(struct artwork_ctx *ctx)
|
||||
char *artwork_url;
|
||||
int ret;
|
||||
|
||||
artwork_url = spotifywebapi_artwork_url_get(ctx->dbmfi->path, ctx->max_w, ctx->max_h);
|
||||
artwork_url = spotifywebapi_artwork_url_get(ctx->dbmfi->path, ctx->req_params.max_w, ctx->req_params.max_h);
|
||||
if (!artwork_url)
|
||||
{
|
||||
DPRINTF(E_WARN, L_ART, "No artwork from Spotify for %s\n", ctx->dbmfi->path);
|
||||
return ART_E_NONE;
|
||||
}
|
||||
|
||||
ret = artwork_get_byurl(ctx->evbuf, artwork_url, ctx->max_w, ctx->max_h);
|
||||
ret = artwork_get_byurl(ctx->evbuf, artwork_url, ctx->req_params);
|
||||
|
||||
free(artwork_url);
|
||||
return ret;
|
||||
@ -1652,7 +1661,7 @@ source_item_spotifywebapi_search_get(struct artwork_ctx *ctx)
|
||||
|
||||
snprintf(ctx->path, sizeof(ctx->path), "%s", url);
|
||||
|
||||
ret = artwork_get_byurl(ctx->evbuf, url, ctx->max_w, ctx->max_h);
|
||||
ret = artwork_get_byurl(ctx->evbuf, url, ctx->req_params);
|
||||
|
||||
free(url);
|
||||
return ret;
|
||||
@ -1716,7 +1725,7 @@ source_item_ownpl_get(struct artwork_ctx *ctx)
|
||||
|
||||
if (dbpli.artwork_url)
|
||||
{
|
||||
format = artwork_get_byurl(ctx->evbuf, dbpli.artwork_url, ctx->max_w, ctx->max_h);
|
||||
format = artwork_get_byurl(ctx->evbuf, dbpli.artwork_url, ctx->req_params);
|
||||
if (format > 0)
|
||||
break;
|
||||
}
|
||||
@ -1894,7 +1903,7 @@ process_group(struct artwork_ctx *ctx)
|
||||
/* ------------------------------ ARTWORK API ------------------------------ */
|
||||
|
||||
int
|
||||
artwork_get_item(struct evbuffer *evbuf, int id, int max_w, int max_h)
|
||||
artwork_get_item(struct evbuffer *evbuf, int id, int max_w, int max_h, int format)
|
||||
{
|
||||
struct artwork_ctx ctx;
|
||||
char filter[32];
|
||||
@ -1910,8 +1919,9 @@ artwork_get_item(struct evbuffer *evbuf, int id, int max_w, int max_h)
|
||||
ctx.qp.type = Q_ITEMS;
|
||||
ctx.qp.filter = filter;
|
||||
ctx.evbuf = evbuf;
|
||||
ctx.max_w = max_w;
|
||||
ctx.max_h = max_h;
|
||||
ctx.req_params.max_w = max_w;
|
||||
ctx.req_params.max_h = max_h;
|
||||
ctx.req_params.format = format;
|
||||
ctx.cache = ON_FAILURE;
|
||||
ctx.individual = cfg_getbool(cfg_getsec(cfg, "library"), "artwork_individual");
|
||||
|
||||
@ -1954,7 +1964,7 @@ artwork_get_item(struct evbuffer *evbuf, int id, int max_w, int max_h)
|
||||
}
|
||||
|
||||
int
|
||||
artwork_get_group(struct evbuffer *evbuf, int id, int max_w, int max_h)
|
||||
artwork_get_group(struct evbuffer *evbuf, int id, int max_w, int max_h, int format)
|
||||
{
|
||||
struct artwork_ctx ctx;
|
||||
int ret;
|
||||
@ -1974,8 +1984,9 @@ artwork_get_group(struct evbuffer *evbuf, int id, int max_w, int max_h)
|
||||
ctx.qp.type = Q_GROUP_ITEMS;
|
||||
ctx.qp.persistentid = ctx.persistentid;
|
||||
ctx.evbuf = evbuf;
|
||||
ctx.max_w = max_w;
|
||||
ctx.max_h = max_h;
|
||||
ctx.req_params.max_w = max_w;
|
||||
ctx.req_params.max_h = max_h;
|
||||
ctx.req_params.format = format;
|
||||
ctx.cache = ON_FAILURE;
|
||||
ctx.individual = cfg_getbool(cfg_getsec(cfg, "library"), "artwork_individual");
|
||||
|
||||
|
@ -4,6 +4,7 @@
|
||||
|
||||
#define ART_FMT_PNG 1
|
||||
#define ART_FMT_JPEG 2
|
||||
#define ART_FMT_VP8 3
|
||||
|
||||
#define ART_DEFAULT_HEIGHT 600
|
||||
#define ART_DEFAULT_WIDTH 600
|
||||
@ -18,10 +19,11 @@
|
||||
* @in id The mfi item id
|
||||
* @in max_w Requested maximum image width (may not be obeyed)
|
||||
* @in max_h Requested maximum image height (may not be obeyed)
|
||||
* @in format Requested format (may not be obeyed), 0 for default
|
||||
* @return ART_FMT_* on success, -1 on error or no artwork found
|
||||
*/
|
||||
int
|
||||
artwork_get_item(struct evbuffer *evbuf, int id, int max_w, int max_h);
|
||||
artwork_get_item(struct evbuffer *evbuf, int id, int max_w, int max_h, int format);
|
||||
|
||||
/*
|
||||
* Get the artwork image for a group (an album or an artist)
|
||||
@ -30,10 +32,11 @@ artwork_get_item(struct evbuffer *evbuf, int id, int max_w, int max_h);
|
||||
* @in id The group id (not the persistentid)
|
||||
* @in max_w Requested maximum image width (may not be obeyed)
|
||||
* @in max_h Requested maximum image height (may not be obeyed)
|
||||
* @in format Requested format (may not be obeyed), 0 for default
|
||||
* @return ART_FMT_* on success, -1 on error or no artwork found
|
||||
*/
|
||||
int
|
||||
artwork_get_group(struct evbuffer *evbuf, int id, int max_w, int max_h);
|
||||
artwork_get_group(struct evbuffer *evbuf, int id, int max_w, int max_h, int format);
|
||||
|
||||
/*
|
||||
* Checks if the file is an artwork file (based on user config)
|
||||
|
@ -166,6 +166,7 @@ static cfg_opt_t sec_airplay[] =
|
||||
/* Chromecast device section structure */
|
||||
static cfg_opt_t sec_chromecast[] =
|
||||
{
|
||||
CFG_INT("max_volume", 11, CFGF_NONE),
|
||||
CFG_BOOL("exclude", cfg_false, CFGF_NONE),
|
||||
CFG_INT("offset_ms", 0, CFGF_NONE),
|
||||
CFG_END()
|
||||
|
@ -92,7 +92,7 @@ artworkapi_reply_nowplaying(struct httpd_request *hreq)
|
||||
if (ret != 0)
|
||||
return HTTP_NOTFOUND;
|
||||
|
||||
ret = artwork_get_item(hreq->reply, id, max_w, max_h);
|
||||
ret = artwork_get_item(hreq->reply, id, max_w, max_h, 0);
|
||||
|
||||
return response_process(hreq, ret);
|
||||
}
|
||||
@ -113,7 +113,7 @@ artworkapi_reply_item(struct httpd_request *hreq)
|
||||
if (ret != 0)
|
||||
return HTTP_BADREQUEST;
|
||||
|
||||
ret = artwork_get_item(hreq->reply, id, max_w, max_h);
|
||||
ret = artwork_get_item(hreq->reply, id, max_w, max_h, 0);
|
||||
|
||||
return response_process(hreq, ret);
|
||||
}
|
||||
@ -134,7 +134,7 @@ artworkapi_reply_group(struct httpd_request *hreq)
|
||||
if (ret != 0)
|
||||
return HTTP_BADREQUEST;
|
||||
|
||||
ret = artwork_get_group(hreq->reply, id, max_w, max_h);
|
||||
ret = artwork_get_group(hreq->reply, id, max_w, max_h, 0);
|
||||
|
||||
return response_process(hreq, ret);
|
||||
}
|
||||
|
@ -2020,9 +2020,9 @@ daap_reply_extra_data(struct httpd_request *hreq)
|
||||
}
|
||||
|
||||
if (strcmp(hreq->uri_parsed->path_parts[2], "groups") == 0)
|
||||
ret = artwork_get_group(hreq->reply, id, max_w, max_h);
|
||||
ret = artwork_get_group(hreq->reply, id, max_w, max_h, 0);
|
||||
else if (strcmp(hreq->uri_parsed->path_parts[2], "items") == 0)
|
||||
ret = artwork_get_item(hreq->reply, id, max_w, max_h);
|
||||
ret = artwork_get_item(hreq->reply, id, max_w, max_h, 0);
|
||||
|
||||
len = evbuffer_get_length(hreq->reply);
|
||||
|
||||
|
@ -2363,7 +2363,7 @@ dacp_reply_nowplayingartwork(struct httpd_request *hreq)
|
||||
if (ret < 0)
|
||||
goto no_artwork;
|
||||
|
||||
ret = artwork_get_item(hreq->reply, id, max_w, max_h);
|
||||
ret = artwork_get_item(hreq->reply, id, max_w, max_h, 0);
|
||||
len = evbuffer_get_length(hreq->reply);
|
||||
|
||||
switch (ret)
|
||||
|
@ -25,6 +25,11 @@
|
||||
# define MAX(a, b) (((a) > (b)) ? (a) : (b))
|
||||
#endif
|
||||
|
||||
// If you have something like "#define MYNUMBER 3" then NTOSTR(MYNUMBER) will be
|
||||
// the string value, so "3".
|
||||
#define NTOSTR_HELPER(x) #x
|
||||
#define NTOSTR(x) NTOSTR_HELPER(x)
|
||||
|
||||
|
||||
// Remember to adjust quality_is_equal() if adding elements
|
||||
struct media_quality {
|
||||
|
@ -4694,7 +4694,7 @@ artwork_cb(struct evhttp_request *req, void *arg)
|
||||
return;
|
||||
}
|
||||
|
||||
format = artwork_get_item(evbuffer, itemid, ART_DEFAULT_WIDTH, ART_DEFAULT_HEIGHT);
|
||||
format = artwork_get_item(evbuffer, itemid, ART_DEFAULT_WIDTH, ART_DEFAULT_HEIGHT, 0);
|
||||
if (format < 0)
|
||||
{
|
||||
httpd_send_error(req, HTTP_NOTFOUND, "Document was not found");
|
||||
|
1040
src/outputs/cast.c
1040
src/outputs/cast.c
File diff suppressed because it is too large
Load Diff
@ -45,17 +45,6 @@
|
||||
#include <fcntl.h>
|
||||
#include <time.h>
|
||||
|
||||
#ifdef HAVE_ENDIAN_H
|
||||
# include <endian.h>
|
||||
#elif defined(HAVE_SYS_ENDIAN_H)
|
||||
# include <sys/endian.h>
|
||||
#elif defined(HAVE_LIBKERN_OSBYTEORDER_H)
|
||||
#include <libkern/OSByteOrder.h>
|
||||
#define htobe16(x) OSSwapHostToBigInt16(x)
|
||||
#define be16toh(x) OSSwapBigToHostInt16(x)
|
||||
#define htobe32(x) OSSwapHostToBigInt32(x)
|
||||
#endif
|
||||
|
||||
#include <arpa/inet.h>
|
||||
#include <net/if.h>
|
||||
#include <netinet/in.h>
|
||||
@ -91,6 +80,8 @@
|
||||
// alignment, which improves performance of some encoders
|
||||
#define RAOP_SAMPLES_PER_PACKET 352
|
||||
|
||||
#define RAOP_RTP_PAYLOADTYPE 0x60
|
||||
|
||||
// How many RTP packets keep in a buffer for retransmission
|
||||
#define RAOP_PACKET_BUFFER_SIZE 1000
|
||||
|
||||
@ -2210,7 +2201,7 @@ raop_metadata_prepare(struct output_metadata *metadata)
|
||||
CHECK_NULL(L_RAOP, rmd->metadata = evbuffer_new());
|
||||
CHECK_NULL(L_RAOP, tmp = evbuffer_new());
|
||||
|
||||
ret = artwork_get_item(rmd->artwork, queue_item->file_id, ART_DEFAULT_WIDTH, ART_DEFAULT_HEIGHT);
|
||||
ret = artwork_get_item(rmd->artwork, queue_item->file_id, ART_DEFAULT_WIDTH, ART_DEFAULT_HEIGHT, 0);
|
||||
if (ret < 0)
|
||||
{
|
||||
DPRINTF(E_INFO, L_RAOP, "Failed to retrieve artwork for file '%s'; no artwork will be sent\n", queue_item->path);
|
||||
@ -2859,7 +2850,7 @@ packets_send(struct raop_master_session *rms)
|
||||
struct raop_session *rs;
|
||||
int ret;
|
||||
|
||||
pkt = rtp_packet_next(rms->rtp_session, ALAC_HEADER_LEN + rms->rawbuf_size, rms->samples_per_packet, 0x60);
|
||||
pkt = rtp_packet_next(rms->rtp_session, ALAC_HEADER_LEN + rms->rawbuf_size, rms->samples_per_packet, RAOP_RTP_PAYLOADTYPE, 0);
|
||||
|
||||
ret = packet_prepare(pkt, rms->rawbuf, rms->rawbuf_size, rms->encrypt);
|
||||
if (ret < 0)
|
||||
|
@ -31,17 +31,6 @@
|
||||
#include <limits.h>
|
||||
#include <sys/param.h>
|
||||
|
||||
#ifdef HAVE_ENDIAN_H
|
||||
# include <endian.h>
|
||||
#elif defined(HAVE_SYS_ENDIAN_H)
|
||||
# include <sys/endian.h>
|
||||
#elif defined(HAVE_LIBKERN_OSBYTEORDER_H)
|
||||
#include <libkern/OSByteOrder.h>
|
||||
#define htobe16(x) OSSwapHostToBigInt16(x)
|
||||
#define be16toh(x) OSSwapBigToHostInt16(x)
|
||||
#define htobe32(x) OSSwapHostToBigInt32(x)
|
||||
#endif
|
||||
|
||||
#include <gcrypt.h>
|
||||
|
||||
#include "logger.h"
|
||||
@ -55,12 +44,6 @@
|
||||
#define FRAC 4294967296. // 2^32 as a double
|
||||
#define NTP_EPOCH_DELTA 0x83aa7e80 // 2208988800 - that's 1970 - 1900 in seconds
|
||||
|
||||
struct ntp_timestamp
|
||||
{
|
||||
uint32_t sec;
|
||||
uint32_t frac;
|
||||
};
|
||||
|
||||
|
||||
static inline void
|
||||
timespec_to_ntp(struct timespec *ts, struct ntp_timestamp *ns)
|
||||
@ -92,6 +75,7 @@ rtp_session_new(struct media_quality *quality, int pktbuf_size, int sync_each_ns
|
||||
gcry_randomize(&session->pos, sizeof(session->pos), GCRY_STRONG_RANDOM);
|
||||
gcry_randomize(&session->seqnum, sizeof(session->seqnum), GCRY_STRONG_RANDOM);
|
||||
|
||||
if (quality)
|
||||
session->quality = *quality;
|
||||
|
||||
session->pktbuf_size = pktbuf_size;
|
||||
@ -99,7 +83,7 @@ rtp_session_new(struct media_quality *quality, int pktbuf_size, int sync_each_ns
|
||||
|
||||
if (sync_each_nsamples > 0)
|
||||
session->sync_each_nsamples = sync_each_nsamples;
|
||||
else if (sync_each_nsamples == 0)
|
||||
else if (sync_each_nsamples == 0 && quality)
|
||||
session->sync_each_nsamples = quality->sample_rate;
|
||||
|
||||
return session;
|
||||
@ -128,7 +112,7 @@ rtp_session_flush(struct rtp_session *session)
|
||||
// We don't want the caller to malloc payload for every packet, so instead we
|
||||
// will get him a packet from the ring buffer, thus in most cases reusing memory
|
||||
struct rtp_packet *
|
||||
rtp_packet_next(struct rtp_session *session, size_t payload_len, int samples, char type)
|
||||
rtp_packet_next(struct rtp_session *session, size_t payload_len, int samples, char payload_type, char marker_bit)
|
||||
{
|
||||
struct rtp_packet *pkt;
|
||||
uint16_t seq;
|
||||
@ -168,7 +152,7 @@ rtp_packet_next(struct rtp_session *session, size_t payload_len, int samples, ch
|
||||
// | synchronization source (SSRC) identifier |
|
||||
// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
||||
pkt->header[0] = 0x80; // Version = 2, P, X and CC are 0
|
||||
pkt->header[1] = type; // RTP payload type
|
||||
pkt->header[1] = (marker_bit << 7) | payload_type; // M and payload type
|
||||
|
||||
seq = htobe16(session->seqnum);
|
||||
memcpy(pkt->header + 2, &seq, 2);
|
||||
@ -288,3 +272,78 @@ rtp_sync_packet_next(struct rtp_session *session, struct rtcp_timestamp cur_stam
|
||||
return &session->sync_packet_next;
|
||||
}
|
||||
|
||||
int
|
||||
rtcp_packet_parse(struct rtcp_packet *pkt, uint8_t *data, size_t size)
|
||||
{
|
||||
if (size < 8) // Must be large enough for at least SSRC
|
||||
goto packet_malformed;
|
||||
|
||||
memset(pkt, 0, sizeof(struct rtcp_packet));
|
||||
|
||||
pkt->version = (data[0] & 0xc0) >> 6; // AND 11000000
|
||||
if (pkt->version != 2)
|
||||
goto packet_malformed;
|
||||
|
||||
pkt->padding = (data[0] & 0x20) >> 5; // AND 00100000
|
||||
memcpy(&pkt->len, data + 2, 2);
|
||||
pkt->len = 4 * (be16toh(pkt->len) + 1); // Input len is 32-bit words excl. the 32-bit header
|
||||
memcpy(&pkt->ssrc, data + 4, 4);
|
||||
pkt->ssrc = be32toh(pkt->ssrc);
|
||||
|
||||
if (size < pkt->len)
|
||||
goto packet_malformed; // Possibly a partial read?
|
||||
|
||||
pkt->payload = data + pkt->len;
|
||||
pkt->payload_len = size - pkt->len;
|
||||
|
||||
pkt->packet_type = data[1];
|
||||
// TODO use a switch()
|
||||
if (pkt->packet_type == RTCP_PACKET_RR) // 201, see RFC 1889
|
||||
{
|
||||
pkt->rr.report_count = data[0] & 0x1f; // AND 00011111
|
||||
// TODO check total size of reports is smaller than size?
|
||||
}
|
||||
else if (pkt->packet_type == RTCP_PACKET_APP && size >= 12) // 204, see RFC 1889
|
||||
{
|
||||
pkt->app.subtype = data[0] & 0x1f; // AND 00011111
|
||||
memcpy(pkt->app.name, data + 8, 4);
|
||||
}
|
||||
else if (pkt->packet_type == RTCP_PACKET_PSFB && size >= 12) // 206, see RFC 4585, payload specific feedback
|
||||
{
|
||||
pkt->psfb.message_type = data[0] & 0x1f; // AND 00011111
|
||||
memcpy(&pkt->psfb.media_src, data + 8, 4);
|
||||
pkt->psfb.media_src = be32toh(pkt->psfb.media_src);
|
||||
pkt->psfb.fci = data + 12;
|
||||
pkt->psfb.fci_len = size - 12;
|
||||
}
|
||||
else if (pkt->packet_type == RTCP_PACKET_XR && size >= 24) // 207, see RFC 3611, however we can handle only 1 block
|
||||
{
|
||||
pkt->xr.block_type = data[8];
|
||||
pkt->xr.block_specific = data[9];
|
||||
memcpy(&pkt->xr.block_len, data + 10, 2);
|
||||
pkt->xr.block_len = 4 * be16toh(pkt->xr.block_len);
|
||||
if (pkt->xr.block_type != 4 || pkt->xr.block_len != 8)
|
||||
return 0; // We can only parse handle Receiver Reference Time Report with 8 byte NTP timestamp
|
||||
|
||||
memcpy(&pkt->xr.ntp.sec, data + 12, 4);
|
||||
pkt->xr.ntp.sec = be32toh(pkt->xr.ntp.sec);
|
||||
memcpy(&pkt->xr.ntp.frac, data + 16, 4);
|
||||
pkt->xr.ntp.frac = be32toh(pkt->xr.ntp.frac);
|
||||
}
|
||||
else
|
||||
return -1; // Don't know how to parse
|
||||
|
||||
/*
|
||||
DPRINTF(E_DBG, L_PLAYER, "RTCP PACKET vers=%d, padding=%d, len=%" PRIu16 ", payload_len=%zu, ssrc=%" PRIu32 "\n", pkt->version, pkt->padding, pkt->len, pkt->payload_len, pkt->ssrc);
|
||||
if (pkt->packet_type == RTCP_PACKET_APP)
|
||||
DPRINTF(E_DBG, L_PLAYER, "RTCP APP PACKET subtype=%d, name=%s\n", pkt->app.subtype, pkt->app.name);
|
||||
else if (pkt->packet_type == RTCP_PACKET_XR)
|
||||
DPRINTF(E_DBG, L_PLAYER, "RTCP XR PACKET block_type=%d, block_len=%" PRIu16 "\n", pkt->xr.block_type, pkt->xr.block_len);
|
||||
*/
|
||||
|
||||
return 0;
|
||||
|
||||
packet_malformed:
|
||||
DPRINTF(E_SPAM, L_PLAYER, "Ignoring incoming packet, packet is non-RTCP, malformed or partial (size=%zu)\n", size);
|
||||
return -1;
|
||||
}
|
||||
|
@ -5,12 +5,30 @@
|
||||
#include <inttypes.h>
|
||||
#include <stdbool.h>
|
||||
|
||||
#ifdef HAVE_ENDIAN_H
|
||||
# include <endian.h>
|
||||
#elif defined(HAVE_SYS_ENDIAN_H)
|
||||
# include <sys/endian.h>
|
||||
#elif defined(HAVE_LIBKERN_OSBYTEORDER_H)
|
||||
#include <libkern/OSByteOrder.h>
|
||||
#define htobe16(x) OSSwapHostToBigInt16(x)
|
||||
#define be16toh(x) OSSwapBigToHostInt16(x)
|
||||
#define htobe32(x) OSSwapHostToBigInt32(x)
|
||||
#define be32toh(x) OSSwapBigToHostInt32(x)
|
||||
#endif
|
||||
|
||||
struct rtcp_timestamp
|
||||
{
|
||||
uint32_t pos;
|
||||
struct timespec ts;
|
||||
};
|
||||
|
||||
struct ntp_timestamp
|
||||
{
|
||||
uint32_t sec;
|
||||
uint32_t frac;
|
||||
};
|
||||
|
||||
struct rtp_packet
|
||||
{
|
||||
uint16_t seqnum; // Sequence number
|
||||
@ -27,6 +45,52 @@ struct rtp_packet
|
||||
size_t data_len; // Length of actual packet data
|
||||
};
|
||||
|
||||
struct rtcp_packet
|
||||
{
|
||||
uint8_t version; // Always 2
|
||||
bool padding;
|
||||
uint16_t len;
|
||||
uint32_t ssrc;
|
||||
|
||||
uint8_t *payload;
|
||||
size_t payload_len;
|
||||
|
||||
enum rtcp_packet_type
|
||||
{
|
||||
RTCP_PACKET_RR = 201, // RFC 3550
|
||||
RTCP_PACKET_APP = 204, // RFC 1889
|
||||
RTCP_PACKET_PSFB = 206, // RFC 4585
|
||||
RTCP_PACKET_XR = 207, // RFC 3611
|
||||
} packet_type;
|
||||
|
||||
union
|
||||
{
|
||||
struct rtcp_packet_rr
|
||||
{
|
||||
uint8_t report_count;
|
||||
} rr;
|
||||
struct rtcp_packet_app
|
||||
{
|
||||
uint8_t subtype;
|
||||
char name[5]; // Zero-terminated
|
||||
} app;
|
||||
struct rtcp_packet_psfb
|
||||
{
|
||||
uint8_t message_type;
|
||||
uint32_t media_src;
|
||||
uint8_t *fci;
|
||||
size_t fci_len;
|
||||
} psfb;
|
||||
struct rtcp_packet_xr
|
||||
{
|
||||
uint8_t block_type;
|
||||
uint8_t block_specific;
|
||||
uint16_t block_len;
|
||||
struct ntp_timestamp ntp;
|
||||
} xr;
|
||||
};
|
||||
};
|
||||
|
||||
// An RTP session is characterised by all the receivers belonging to the session
|
||||
// getting the same RTP and RTCP packets. So if you have clients that require
|
||||
// different sample rates or where only some can accept encrypted payloads then
|
||||
@ -62,12 +126,36 @@ rtp_session_free(struct rtp_session *session);
|
||||
void
|
||||
rtp_session_flush(struct rtp_session *session);
|
||||
|
||||
struct rtp_packet *
|
||||
rtp_packet_next(struct rtp_session *session, size_t payload_len, int samples, char type);
|
||||
|
||||
/* Gets the next packet from the packet buffer, pkt->payload will be allocated
|
||||
* to a size of payload_len (or larger).
|
||||
*
|
||||
* @in session RTP session
|
||||
* @in payload_len Length of payload the packet needs to hold
|
||||
* @in samples Number of samples in packet
|
||||
* @in payload_type RTP payload type
|
||||
* @in marker_bit Marker bit, see RFC3551
|
||||
* @return Pointer to the next packet in the packet buffer
|
||||
*/
|
||||
struct rtp_packet *
|
||||
rtp_packet_next(struct rtp_session *session, size_t payload_len, int samples, char payload_type, char marker_bit);
|
||||
|
||||
/* Call this after finalizing a packet, i.e. writing the payload and possibly
|
||||
* sending. Registers the packet as final, i.e. it can now be retrieved with
|
||||
* rtp_packet_get() for retransmission, if required. Also advances RTP position
|
||||
* (seqnum and RTP time).
|
||||
*
|
||||
* @in session RTP session
|
||||
* @in pkt RTP packet to commit
|
||||
*/
|
||||
void
|
||||
rtp_packet_commit(struct rtp_session *session, struct rtp_packet *pkt);
|
||||
|
||||
/* Get a previously committed packet from the packet buffer
|
||||
*
|
||||
* @in session RTP session
|
||||
* @in seqnum Packet sequence number
|
||||
*/
|
||||
struct rtp_packet *
|
||||
rtp_packet_get(struct rtp_session *session, uint16_t seqnum);
|
||||
|
||||
@ -77,4 +165,7 @@ rtp_sync_is_time(struct rtp_session *session);
|
||||
struct rtp_packet *
|
||||
rtp_sync_packet_next(struct rtp_session *session, struct rtcp_timestamp cur_stamp, char type);
|
||||
|
||||
int
|
||||
rtcp_packet_parse(struct rtcp_packet *pkt, uint8_t *data, size_t size);
|
||||
|
||||
#endif /* !__RTP_COMMON_H__ */
|
||||
|
@ -260,6 +260,19 @@ init_settings(struct settings_ctx *settings, enum transcode_profile profile, str
|
||||
settings->video_codec = AV_CODEC_ID_PNG;
|
||||
break;
|
||||
|
||||
case XCODE_VP8:
|
||||
settings->encode_video = 1;
|
||||
settings->silent = 1;
|
||||
// See explanation above
|
||||
#if (LIBAVFORMAT_VERSION_MAJOR > 58) || ((LIBAVFORMAT_VERSION_MAJOR == 58) && (LIBAVFORMAT_VERSION_MINOR > 29))
|
||||
settings->format = "image2pipe";
|
||||
#else
|
||||
settings->format = "image2";
|
||||
#endif
|
||||
settings->pix_fmt = AV_PIX_FMT_YUVJ420P;
|
||||
settings->video_codec = AV_CODEC_ID_VP8;
|
||||
break;
|
||||
|
||||
default:
|
||||
DPRINTF(E_LOG, L_XCODE, "Bug! Unknown transcoding profile\n");
|
||||
return -1;
|
||||
@ -283,7 +296,7 @@ init_settings(struct settings_ctx *settings, enum transcode_profile profile, str
|
||||
|
||||
if (quality && quality->bits_per_sample && (quality->bits_per_sample != 8 * av_get_bytes_per_sample(settings->sample_format)))
|
||||
{
|
||||
DPRINTF(E_LOG, L_XCODE, "Bug! Mismatch between profile and media quality\n");
|
||||
DPRINTF(E_LOG, L_XCODE, "Bug! Mismatch between profile (%d bps) and media quality (%d bps)\n", 8 * av_get_bytes_per_sample(settings->sample_format), quality->bits_per_sample);
|
||||
return -1;
|
||||
}
|
||||
|
||||
@ -449,6 +462,12 @@ stream_add(struct encode_ctx *ctx, struct stream_ctx *s, enum AVCodecID codec_id
|
||||
if (codec_id == AV_CODEC_ID_MJPEG)
|
||||
av_dict_set(&options, "huffman", "default", 0);
|
||||
|
||||
// 20 ms frames is the current ffmpeg default, but we set it anyway, so that
|
||||
// we don't risk issues if future versions change the default (it would become
|
||||
// an issue because outputs/cast.c relies on 20 ms frames)
|
||||
if (codec_id == AV_CODEC_ID_OPUS)
|
||||
av_dict_set(&options, "frame_duration", "20", 0);
|
||||
|
||||
ret = avcodec_open2(s->codec, NULL, &options);
|
||||
if (ret < 0)
|
||||
{
|
||||
|
@ -23,9 +23,10 @@ enum transcode_profile
|
||||
XCODE_MP3,
|
||||
// Transcodes the best audio stream into OPUS
|
||||
XCODE_OPUS,
|
||||
// Transcodes the best video stream into JPEG/PNG
|
||||
// Transcodes the best video stream into JPEG/PNG/VP8
|
||||
XCODE_JPEG,
|
||||
XCODE_PNG,
|
||||
XCODE_VP8,
|
||||
};
|
||||
|
||||
struct decode_ctx;
|
||||
|
Loading…
Reference in New Issue
Block a user