Merge branch 'rsp_daap_format3'
This commit is contained in:
commit
4cbce79a0f
|
@ -341,6 +341,8 @@ GET /api/outputs
|
|||
| requires_auth | boolean | `true` if output requires authentication |
|
||||
| needs_auth_key | boolean | `true` if output requires an authorization key (device verification) |
|
||||
| volume | integer | Volume in percent (0 - 100) |
|
||||
| format | string | Stream format |
|
||||
| supported_formats | array | Array of formats supported by output |
|
||||
|
||||
**Example**
|
||||
|
||||
|
@ -359,7 +361,9 @@ curl -X GET "http://localhost:3689/api/outputs"
|
|||
"has_password": false,
|
||||
"requires_auth": false,
|
||||
"needs_auth_key": false,
|
||||
"volume": 0
|
||||
"volume": 0,
|
||||
"format": "alac",
|
||||
"supported_formats": [ "alac" ]
|
||||
},
|
||||
{
|
||||
"id": "0",
|
||||
|
@ -369,7 +373,9 @@ curl -X GET "http://localhost:3689/api/outputs"
|
|||
"has_password": false,
|
||||
"requires_auth": false,
|
||||
"needs_auth_key": false,
|
||||
"volume": 19
|
||||
"volume": 19,
|
||||
"format": "pcm",
|
||||
"supported_formats": [ "pcm" ]
|
||||
},
|
||||
{
|
||||
"id": "100",
|
||||
|
@ -379,7 +385,9 @@ curl -X GET "http://localhost:3689/api/outputs"
|
|||
"has_password": false,
|
||||
"requires_auth": false,
|
||||
"needs_auth_key": false,
|
||||
"volume": 0
|
||||
"volume": 0,
|
||||
"format": "pcm",
|
||||
"supported_formats": [ "pcm" ]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
@ -448,6 +456,8 @@ curl -X GET "http://localhost:3689/api/outputs/0"
|
|||
"requires_auth": false,
|
||||
"needs_auth_key": false,
|
||||
"volume": 3
|
||||
"format": "pcm",
|
||||
"supported_formats": [ "pcm" ]
|
||||
}
|
||||
```
|
||||
|
||||
|
@ -474,6 +484,7 @@ PUT /api/outputs/{id}
|
|||
| selected | boolean | *(Optional)* `true` to enable and `false` to disable the output |
|
||||
| volume | integer | *(Optional)* Volume in percent (0 - 100) |
|
||||
| pin | string | *(Optional)* PIN for device verification |
|
||||
| format | string | *(Optional)* Stream format |
|
||||
|
||||
**Response**
|
||||
|
||||
|
|
|
@ -52,8 +52,8 @@ general {
|
|||
# IP addresses.
|
||||
# bind_address = "::"
|
||||
|
||||
# Location of cache database
|
||||
# cache_path = "@localstatedir@/cache/@PACKAGE@/cache.db"
|
||||
# Directory where the server keeps cached data
|
||||
# cache_dir = "@localstatedir@/cache/@PACKAGE@"
|
||||
|
||||
# DAAP requests that take longer than this threshold (in msec) get their
|
||||
# replies cached for next time. Set to 0 to disable caching.
|
||||
|
@ -187,19 +187,25 @@ library {
|
|||
# Should we import the content of iTunes smart playlists?
|
||||
# itunes_smartpl = false
|
||||
|
||||
# Decoding options for DAAP and RSP clients
|
||||
# Transcoding options for DAAP and RSP clients
|
||||
# Since iTunes has native support for mpeg, mp4a, mp4v, alac and wav,
|
||||
# such files will be sent as they are. Any other formats will be decoded
|
||||
# to raw wav. If OwnTone detects a non-iTunes DAAP client, it is
|
||||
# assumed to only support mpeg and wav, other formats will be decoded.
|
||||
# Here you can change when to decode. Note that these settings only
|
||||
# affect serving media to DAAP and RSP clients, they have no effect on
|
||||
# such files will be sent as they are. Any other formats will be
|
||||
# transcoded. Some other clients, including Roku/RSP, announce what
|
||||
# formats they support, and the server will transcode to one of those if
|
||||
# necessary. Clients that don't announce supported formats are assumed
|
||||
# to support mpeg (mp3), wav and alac.
|
||||
# Here you can change when and how to transcode. The settings *only*
|
||||
# affect serving audio to DAAP and RSP clients, they have no effect on
|
||||
# direct AirPlay, Chromecast and local audio playback.
|
||||
# Formats: mp4a, mp4v, mpeg, alac, flac, mpc, ogg, wma, wmal, wmav, aif, wav
|
||||
# Formats that should never be decoded
|
||||
# Formats that should never be transcoded
|
||||
# no_decode = { "format", "format" }
|
||||
# Formats that should always be decoded
|
||||
# Formats that should always be transcoded
|
||||
# force_decode = { "format", "format" }
|
||||
# Prefer transcode to wav (default), alac or mpeg (mp3 with the bit rate
|
||||
# configured below in the streaming section). Note that alac requires
|
||||
# precomputing and caching mp4 headers, which takes both cpu and disk.
|
||||
# prefer_format = "format"
|
||||
|
||||
# Set ffmpeg filters (similar to 'ffmpeg -af xxx') that you want the
|
||||
# server to use when decoding files from your library. Examples:
|
||||
|
|
|
@ -604,6 +604,8 @@ size_calculate(int *dst_w, int *dst_h, int src_w, int src_h, int max_w, int max_
|
|||
static int
|
||||
artwork_get(struct evbuffer *evbuf, char *path, struct evbuffer *in_buf, bool is_embedded, enum data_kind data_kind, struct artwork_req_params req_params)
|
||||
{
|
||||
struct transcode_decode_setup_args xcode_decode_args = { .profile = XCODE_JPEG }; // Covers XCODE_PNG too
|
||||
struct transcode_encode_setup_args xcode_encode_args = { 0 };
|
||||
struct decode_ctx *xcode_decode = NULL;
|
||||
struct encode_ctx *xcode_encode = NULL;
|
||||
struct transcode_evbuf_io xcode_evbuf_io = { 0 };
|
||||
|
@ -637,13 +639,16 @@ artwork_get(struct evbuffer *evbuf, char *path, struct evbuffer *in_buf, bool is
|
|||
}
|
||||
|
||||
xcode_evbuf_io.evbuf = xcode_buf;
|
||||
xcode_decode = transcode_decode_setup(XCODE_JPEG, NULL, data_kind, NULL, &xcode_evbuf_io, 0); // Covers XCODE_PNG too
|
||||
xcode_decode_args.evbuf_io = &xcode_evbuf_io;
|
||||
xcode_decode_args.is_http = (data_kind == DATA_KIND_HTTP);
|
||||
}
|
||||
else
|
||||
{
|
||||
xcode_decode = transcode_decode_setup(XCODE_JPEG, NULL, data_kind, path, NULL, 0); // Covers XCODE_PNG too
|
||||
xcode_decode_args.path = path;
|
||||
xcode_decode_args.is_http = (data_kind == DATA_KIND_HTTP);
|
||||
}
|
||||
|
||||
xcode_decode = transcode_decode_setup(xcode_decode_args);
|
||||
if (!xcode_decode)
|
||||
{
|
||||
if (path)
|
||||
|
@ -702,15 +707,19 @@ artwork_get(struct evbuffer *evbuf, char *path, struct evbuffer *in_buf, bool is
|
|||
goto out;
|
||||
}
|
||||
|
||||
xcode_encode_args.src_ctx = xcode_decode;
|
||||
xcode_encode_args.width = dst_width;
|
||||
xcode_encode_args.height = dst_height;
|
||||
if (dst_format == ART_FMT_JPEG)
|
||||
xcode_encode = transcode_encode_setup(XCODE_JPEG, NULL, xcode_decode, dst_width, dst_height);
|
||||
xcode_encode_args.profile = XCODE_JPEG;
|
||||
else if (dst_format == ART_FMT_PNG)
|
||||
xcode_encode = transcode_encode_setup(XCODE_PNG, NULL, xcode_decode, dst_width, dst_height);
|
||||
xcode_encode_args.profile = XCODE_PNG;
|
||||
else if (dst_format == ART_FMT_VP8)
|
||||
xcode_encode = transcode_encode_setup(XCODE_VP8, NULL, xcode_decode, dst_width, dst_height);
|
||||
xcode_encode_args.profile = XCODE_VP8;
|
||||
else
|
||||
xcode_encode = transcode_encode_setup(XCODE_JPEG, NULL, xcode_decode, dst_width, dst_height);
|
||||
xcode_encode_args.profile = XCODE_JPEG;
|
||||
|
||||
xcode_encode = transcode_encode_setup(xcode_encode_args);
|
||||
if (!xcode_encode)
|
||||
{
|
||||
if (path)
|
||||
|
|
1377
src/cache.c
1377
src/cache.c
File diff suppressed because it is too large
Load Diff
17
src/cache.h
17
src/cache.h
|
@ -4,7 +4,7 @@
|
|||
|
||||
#include <event2/buffer.h>
|
||||
|
||||
/* ---------------------------- DAAP cache API --------------------------- */
|
||||
/* ----------------------------- DAAP cache API ---------------------------- */
|
||||
|
||||
void
|
||||
cache_daap_suspend(void);
|
||||
|
@ -19,10 +19,19 @@ void
|
|||
cache_daap_add(const char *query, const char *ua, int is_remote, int msec);
|
||||
|
||||
int
|
||||
cache_daap_threshold(void);
|
||||
cache_daap_threshold_get(void);
|
||||
|
||||
|
||||
/* ---------------------------- Artwork cache API --------------------------- */
|
||||
/* --------------------------- Transcode cache API ------------------------- */
|
||||
|
||||
int
|
||||
cache_xcode_header_get(struct evbuffer *evbuf, int *cached, uint32_t id, const char *format);
|
||||
|
||||
int
|
||||
cache_xcode_toggle(bool enable);
|
||||
|
||||
|
||||
/* ---------------------------- Artwork cache API -------------------------- */
|
||||
|
||||
#define CACHE_ARTWORK_GROUP 0
|
||||
#define CACHE_ARTWORK_INDIVIDUAL 1
|
||||
|
@ -48,7 +57,7 @@ cache_artwork_stash(struct evbuffer *evbuf, const char *path, int format);
|
|||
int
|
||||
cache_artwork_read(struct evbuffer *evbuf, const char *path, int *format);
|
||||
|
||||
/* ---------------------------- Cache API --------------------------- */
|
||||
/* ------------------------------- Cache API ------------------------------- */
|
||||
|
||||
int
|
||||
cache_init(void);
|
||||
|
|
|
@ -55,7 +55,7 @@ static cfg_opt_t sec_general[] =
|
|||
CFG_STR_LIST("trusted_networks", "{lan}", CFGF_NONE),
|
||||
CFG_BOOL("ipv6", cfg_false, CFGF_NONE),
|
||||
CFG_STR("bind_address", NULL, CFGF_NONE),
|
||||
CFG_STR("cache_path", STATEDIR "/cache/" PACKAGE "/cache.db", CFGF_NONE),
|
||||
CFG_STR("cache_dir", STATEDIR "/cache/" PACKAGE, CFGF_NONE),
|
||||
CFG_INT("cache_daap_threshold", 1000, CFGF_NONE),
|
||||
CFG_BOOL("speaker_autoselect", cfg_false, CFGF_NONE),
|
||||
#if defined(__FreeBSD__) || defined(__FreeBSD_kernel__)
|
||||
|
@ -67,6 +67,9 @@ static cfg_opt_t sec_general[] =
|
|||
CFG_INT("db_pragma_cache_size", -1, CFGF_NONE),
|
||||
CFG_STR("db_pragma_journal_mode", NULL, CFGF_NONE),
|
||||
CFG_INT("db_pragma_synchronous", -1, CFGF_NONE),
|
||||
CFG_STR("cache_daap_filename", "daap.db", CFGF_NONE),
|
||||
CFG_STR("cache_artwork_filename", "artwork.db", CFGF_NONE),
|
||||
CFG_STR("cache_xcode_filename", "xcode.db", CFGF_NONE),
|
||||
CFG_STR("allow_origin", "*", CFGF_NONE),
|
||||
CFG_STR("user_agent", PACKAGE_NAME "/" PACKAGE_VERSION, CFGF_NONE),
|
||||
CFG_BOOL("ssl_verifypeer", cfg_true, CFGF_NONE),
|
||||
|
@ -312,6 +315,31 @@ cb_loglevel(cfg_t *config, cfg_opt_t *opt, const char *value, void *result)
|
|||
return 0;
|
||||
}
|
||||
|
||||
// Makes sure cache_dir ends with a slash
|
||||
static int
|
||||
sanitize_cache_dir(cfg_t *general)
|
||||
{
|
||||
char *dir;
|
||||
const char *s;
|
||||
char *appended;
|
||||
size_t len;
|
||||
|
||||
dir = cfg_getstr(general, "cache_dir");
|
||||
len = strlen(dir);
|
||||
|
||||
s = strrchr(dir, '/');
|
||||
if (s && (s + 1 == dir + len))
|
||||
return 0;
|
||||
|
||||
appended = safe_asprintf("%s/", dir);
|
||||
|
||||
cfg_setstr(general, "cache_dir", appended);
|
||||
|
||||
free(appended);
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int
|
||||
conffile_expand_libname(cfg_t *lib)
|
||||
{
|
||||
|
@ -425,7 +453,6 @@ conffile_expand_libname(cfg_t *lib)
|
|||
return 0;
|
||||
}
|
||||
|
||||
|
||||
int
|
||||
conffile_load(char *file)
|
||||
{
|
||||
|
@ -466,6 +493,14 @@ conffile_load(char *file)
|
|||
runas_uid = pw->pw_uid;
|
||||
runas_gid = pw->pw_gid;
|
||||
|
||||
ret = sanitize_cache_dir(cfg_getsec(cfg, "general"));
|
||||
if (ret != 0)
|
||||
{
|
||||
DPRINTF(E_FATAL, L_CONF, "Invalid configuration of cache_dir\n");
|
||||
|
||||
goto out_fail;
|
||||
}
|
||||
|
||||
lib = cfg_getsec(cfg, "library");
|
||||
|
||||
if (cfg_size(lib, "directories") == 0)
|
||||
|
|
28
src/db.c
28
src/db.c
|
@ -4787,10 +4787,10 @@ db_admin_delete(const char *key)
|
|||
int
|
||||
db_speaker_save(struct output_device *device)
|
||||
{
|
||||
#define Q_TMPL "INSERT OR REPLACE INTO speakers (id, selected, volume, name, auth_key) VALUES (%" PRIi64 ", %d, %d, %Q, %Q);"
|
||||
#define Q_TMPL "INSERT OR REPLACE INTO speakers (id, selected, volume, name, auth_key, format) VALUES (%" PRIi64 ", %d, %d, %Q, %Q, %d);"
|
||||
char *query;
|
||||
|
||||
query = sqlite3_mprintf(Q_TMPL, device->id, device->selected, device->volume, device->name, device->auth_key);
|
||||
query = sqlite3_mprintf(Q_TMPL, device->id, device->selected, device->volume, device->name, device->auth_key, device->selected_format);
|
||||
|
||||
return db_query_run(query, 1, 0);
|
||||
#undef Q_TMPL
|
||||
|
@ -4799,7 +4799,7 @@ db_speaker_save(struct output_device *device)
|
|||
int
|
||||
db_speaker_get(struct output_device *device, uint64_t id)
|
||||
{
|
||||
#define Q_TMPL "SELECT s.selected, s.volume, s.name, s.auth_key FROM speakers s WHERE s.id = %" PRIi64 ";"
|
||||
#define Q_TMPL "SELECT s.selected, s.volume, s.name, s.auth_key, s.format FROM speakers s WHERE s.id = %" PRIi64 ";"
|
||||
sqlite3_stmt *stmt;
|
||||
char *query;
|
||||
int ret;
|
||||
|
@ -4841,6 +4841,8 @@ db_speaker_get(struct output_device *device, uint64_t id)
|
|||
free(device->auth_key);
|
||||
device->auth_key = safe_strdup((char *)sqlite3_column_text(stmt, 3));
|
||||
|
||||
device->selected_format = sqlite3_column_int(stmt, 4);
|
||||
|
||||
#ifdef DB_PROFILE
|
||||
while (db_blocking_step(stmt) == SQLITE_ROW)
|
||||
; /* EMPTY */
|
||||
|
@ -6892,9 +6894,6 @@ db_open(void)
|
|||
int synchronous;
|
||||
int mmap_size;
|
||||
|
||||
if (!db_path)
|
||||
return -1;
|
||||
|
||||
ret = sqlite3_open(db_path, &hdl);
|
||||
if (ret != SQLITE_OK)
|
||||
{
|
||||
|
@ -7343,35 +7342,35 @@ db_init(void)
|
|||
db_path = cfg_getstr(cfg_getsec(cfg, "general"), "db_path");
|
||||
db_rating_updates = cfg_getbool(cfg_getsec(cfg, "library"), "rating_updates");
|
||||
|
||||
DPRINTF(E_LOG, L_DB, "Configured to use database file '%s'\n", db_path);
|
||||
DPRINTF(E_INFO, L_DB, "Configured to use database file '%s'\n", db_path);
|
||||
|
||||
ret = sqlite3_config(SQLITE_CONFIG_MULTITHREAD);
|
||||
if (ret != SQLITE_OK)
|
||||
{
|
||||
DPRINTF(E_FATAL, L_DB, "Could not switch SQLite3 to multithread mode\n");
|
||||
DPRINTF(E_FATAL, L_DB, "Check that SQLite3 has been configured for thread-safe operations\n");
|
||||
return -1;
|
||||
goto error;
|
||||
}
|
||||
|
||||
ret = sqlite3_enable_shared_cache(1);
|
||||
if (ret != SQLITE_OK)
|
||||
{
|
||||
DPRINTF(E_FATAL, L_DB, "Could not enable SQLite3 shared-cache mode\n");
|
||||
return -1;
|
||||
goto error;
|
||||
}
|
||||
|
||||
ret = sqlite3_initialize();
|
||||
if (ret != SQLITE_OK)
|
||||
{
|
||||
DPRINTF(E_FATAL, L_DB, "SQLite3 failed to initialize\n");
|
||||
return -1;
|
||||
goto error;
|
||||
}
|
||||
|
||||
ret = db_open();
|
||||
if (ret < 0)
|
||||
{
|
||||
DPRINTF(E_FATAL, L_DB, "Could not open database\n");
|
||||
return -1;
|
||||
goto error;
|
||||
}
|
||||
|
||||
ret = db_check_version();
|
||||
|
@ -7380,7 +7379,7 @@ db_init(void)
|
|||
DPRINTF(E_FATAL, L_DB, "Database version check errored out, incompatible database\n");
|
||||
|
||||
db_perthread_deinit();
|
||||
return -1;
|
||||
goto error;
|
||||
}
|
||||
else if (ret > 0)
|
||||
{
|
||||
|
@ -7391,7 +7390,7 @@ db_init(void)
|
|||
{
|
||||
DPRINTF(E_FATAL, L_DB, "Could not create tables\n");
|
||||
db_perthread_deinit();
|
||||
return -1;
|
||||
goto error;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -7409,6 +7408,9 @@ db_init(void)
|
|||
rng_init(&shuffle_rng);
|
||||
|
||||
return 0;
|
||||
|
||||
error:
|
||||
return -1;
|
||||
}
|
||||
|
||||
void
|
||||
|
|
|
@ -151,8 +151,9 @@
|
|||
" id INTEGER PRIMARY KEY NOT NULL," \
|
||||
" selected INTEGER NOT NULL," \
|
||||
" volume INTEGER NOT NULL," \
|
||||
" name VARCHAR(255) DEFAULT NULL," \
|
||||
" auth_key VARCHAR(2048) DEFAULT NULL" \
|
||||
" name VARCHAR(255) DEFAULT NULL," \
|
||||
" auth_key VARCHAR(2048) DEFAULT NULL," \
|
||||
" format INTEGER DEFAULT 0" \
|
||||
");"
|
||||
|
||||
#define T_INOTIFY \
|
||||
|
|
|
@ -26,7 +26,7 @@
|
|||
* is a major upgrade. In other words minor version upgrades permit downgrading
|
||||
* the server after the database was upgraded. */
|
||||
#define SCHEMA_VERSION_MAJOR 22
|
||||
#define SCHEMA_VERSION_MINOR 1
|
||||
#define SCHEMA_VERSION_MINOR 2
|
||||
|
||||
int
|
||||
db_init_indices(sqlite3 *hdl);
|
||||
|
|
|
@ -1246,6 +1246,24 @@ static const struct db_upgrade_query db_upgrade_v2201_queries[] =
|
|||
};
|
||||
|
||||
|
||||
/* ---------------------------- 22.01 -> 22.02 ------------------------------ */
|
||||
|
||||
#define U_v2202_ALTER_SPEAKERS_ADD_FORMAT \
|
||||
"ALTER TABLE speakers ADD COLUMN format INTEGER DEFAULT 0;"
|
||||
|
||||
#define U_v2202_SCVER_MAJOR \
|
||||
"UPDATE admin SET value = '22' WHERE key = 'schema_version_major';"
|
||||
#define U_v2202_SCVER_MINOR \
|
||||
"UPDATE admin SET value = '02' WHERE key = 'schema_version_minor';"
|
||||
|
||||
static const struct db_upgrade_query db_upgrade_v2202_queries[] =
|
||||
{
|
||||
{ U_v2202_ALTER_SPEAKERS_ADD_FORMAT, "alter table speakers add column format" },
|
||||
|
||||
{ U_v2202_SCVER_MAJOR, "set schema_version_major to 22" },
|
||||
{ U_v2202_SCVER_MINOR, "set schema_version_minor to 02" },
|
||||
};
|
||||
|
||||
|
||||
/* -------------------------- Main upgrade handler -------------------------- */
|
||||
|
||||
|
@ -1464,6 +1482,13 @@ db_upgrade(sqlite3 *hdl, int db_ver)
|
|||
if (ret < 0)
|
||||
return -1;
|
||||
|
||||
/* FALLTHROUGH */
|
||||
|
||||
case 2201:
|
||||
ret = db_generic_upgrade(hdl, db_upgrade_v2202_queries, ARRAY_SIZE(db_upgrade_v2202_queries));
|
||||
if (ret < 0)
|
||||
return -1;
|
||||
|
||||
/* Last case statement is the only one that ends with a break statement! */
|
||||
break;
|
||||
|
||||
|
|
135
src/httpd.c
135
src/httpd.c
|
@ -48,6 +48,9 @@
|
|||
#include "httpd.h"
|
||||
#include "httpd_internal.h"
|
||||
#include "transcode.h"
|
||||
#include "cache.h"
|
||||
#include "listener.h"
|
||||
#include "player.h"
|
||||
#ifdef LASTFM
|
||||
# include "lastfm.h"
|
||||
#endif
|
||||
|
@ -105,18 +108,19 @@ struct stream_ctx {
|
|||
|
||||
static const struct content_type_map ext2ctype[] =
|
||||
{
|
||||
{ ".html", XCODE_NONE, "text/html; charset=utf-8" },
|
||||
{ ".xml", XCODE_NONE, "text/xml; charset=utf-8" },
|
||||
{ ".css", XCODE_NONE, "text/css; charset=utf-8" },
|
||||
{ ".txt", XCODE_NONE, "text/plain; charset=utf-8" },
|
||||
{ ".js", XCODE_NONE, "application/javascript; charset=utf-8" },
|
||||
{ ".gif", XCODE_NONE, "image/gif" },
|
||||
{ ".ico", XCODE_NONE, "image/x-ico" },
|
||||
{ ".png", XCODE_PNG, "image/png" },
|
||||
{ ".jpg", XCODE_JPEG, "image/jpeg" },
|
||||
{ ".mp3", XCODE_MP3, "audio/mpeg" },
|
||||
{ ".wav", XCODE_WAV, "audio/wav" },
|
||||
{ NULL, XCODE_NONE, NULL }
|
||||
{ ".html", XCODE_NONE, "text/html; charset=utf-8" },
|
||||
{ ".xml", XCODE_NONE, "text/xml; charset=utf-8" },
|
||||
{ ".css", XCODE_NONE, "text/css; charset=utf-8" },
|
||||
{ ".txt", XCODE_NONE, "text/plain; charset=utf-8" },
|
||||
{ ".js", XCODE_NONE, "application/javascript; charset=utf-8" },
|
||||
{ ".gif", XCODE_NONE, "image/gif" },
|
||||
{ ".ico", XCODE_NONE, "image/x-ico" },
|
||||
{ ".png", XCODE_PNG, "image/png" },
|
||||
{ ".jpg", XCODE_JPEG, "image/jpeg" },
|
||||
{ ".mp3", XCODE_MP3, "audio/mpeg" },
|
||||
{ ".m4a", XCODE_MP4_ALAC, "audio/mp4" },
|
||||
{ ".wav", XCODE_WAV, "audio/wav" },
|
||||
{ NULL, XCODE_NONE, NULL }
|
||||
};
|
||||
|
||||
static char webroot_directory[PATH_MAX];
|
||||
|
@ -672,8 +676,16 @@ static struct stream_ctx *
|
|||
stream_new_transcode(struct media_file_info *mfi, enum transcode_profile profile, struct httpd_request *hreq,
|
||||
int64_t offset, int64_t end_offset, event_callback_fn stream_cb)
|
||||
{
|
||||
struct transcode_decode_setup_args decode_args = { 0 };
|
||||
struct transcode_encode_setup_args encode_args = { 0 };
|
||||
struct media_quality quality = { 0 };
|
||||
struct evbuffer *prepared_header = NULL;
|
||||
struct stream_ctx *st;
|
||||
struct media_quality quality = { HTTPD_STREAM_SAMPLE_RATE, HTTPD_STREAM_BPS, HTTPD_STREAM_CHANNELS, HTTPD_STREAM_BIT_RATE };
|
||||
int cached;
|
||||
int ret;
|
||||
|
||||
// We use source sample rate etc, but for MP3 we must set a bit rate
|
||||
quality.bit_rate = 1000 * cfg_getint(cfg_getsec(cfg, "streaming"), "bit_rate");
|
||||
|
||||
st = stream_new(mfi, hreq, stream_cb);
|
||||
if (!st)
|
||||
|
@ -681,7 +693,27 @@ stream_new_transcode(struct media_file_info *mfi, enum transcode_profile profile
|
|||
goto error;
|
||||
}
|
||||
|
||||
st->xcode = transcode_setup(profile, &quality, mfi->data_kind, mfi->path, mfi->song_length);
|
||||
if (profile == XCODE_MP4_ALAC)
|
||||
{
|
||||
CHECK_NULL(L_HTTPD, prepared_header = evbuffer_new());
|
||||
|
||||
ret = cache_xcode_header_get(prepared_header, &cached, mfi->id, "mp4");
|
||||
if (ret < 0 || !cached) // Error or not found
|
||||
{
|
||||
evbuffer_free(prepared_header);
|
||||
prepared_header = NULL;
|
||||
}
|
||||
}
|
||||
|
||||
decode_args.profile = profile;
|
||||
decode_args.is_http = (mfi->data_kind == DATA_KIND_HTTP);
|
||||
decode_args.path = mfi->path;
|
||||
decode_args.len_ms = mfi->song_length;
|
||||
encode_args.profile = profile;
|
||||
encode_args.quality = &quality;
|
||||
encode_args.prepared_header = prepared_header;
|
||||
|
||||
st->xcode = transcode_setup(decode_args, encode_args);
|
||||
if (!st->xcode)
|
||||
{
|
||||
DPRINTF(E_WARN, L_HTTPD, "Transcoding setup failed, aborting streaming\n");
|
||||
|
@ -705,9 +737,13 @@ stream_new_transcode(struct media_file_info *mfi, enum transcode_profile profile
|
|||
|
||||
st->start_offset = offset;
|
||||
|
||||
if (prepared_header)
|
||||
evbuffer_free(prepared_header);
|
||||
return st;
|
||||
|
||||
error:
|
||||
if (prepared_header)
|
||||
evbuffer_free(prepared_header);
|
||||
stream_free(st);
|
||||
return NULL;
|
||||
}
|
||||
|
@ -871,6 +907,39 @@ stream_fail_cb(void *arg)
|
|||
}
|
||||
|
||||
|
||||
/* -------------------------- SPEAKER/CACHE HANDLING ------------------------ */
|
||||
|
||||
// Thread: player (must not block)
|
||||
static void
|
||||
speaker_enum_cb(struct player_speaker_info *spk, void *arg)
|
||||
{
|
||||
bool *want_mp4 = arg;
|
||||
|
||||
*want_mp4 = *want_mp4 || (spk->format == MEDIA_FORMAT_ALAC && strcmp(spk->output_type, "RCP/SoundBridge") == 0);
|
||||
}
|
||||
|
||||
// Thread: worker
|
||||
static void
|
||||
speaker_update_handler_cb(void *arg)
|
||||
{
|
||||
const char *prefer_format = cfg_getstr(cfg_getsec(cfg, "library"), "prefer_format");
|
||||
bool want_mp4;
|
||||
|
||||
want_mp4 = (prefer_format && (strcmp(prefer_format, "alac") == 0));
|
||||
if (!want_mp4)
|
||||
player_speaker_enumerate(speaker_enum_cb, &want_mp4);
|
||||
|
||||
cache_xcode_toggle(want_mp4);
|
||||
}
|
||||
|
||||
// Thread: player (must not block)
|
||||
static void
|
||||
httpd_speaker_update_handler(short event_mask)
|
||||
{
|
||||
worker_execute(speaker_update_handler_cb, NULL, 0, 0);
|
||||
}
|
||||
|
||||
|
||||
/* ---------------------------- REQUEST CALLBACKS --------------------------- */
|
||||
|
||||
// Worker thread, invoked by request_cb() below
|
||||
|
@ -940,6 +1009,7 @@ httpd_stream_file(struct httpd_request *hreq, int id)
|
|||
struct media_file_info *mfi = NULL;
|
||||
struct stream_ctx *st = NULL;
|
||||
enum transcode_profile profile;
|
||||
enum transcode_profile spk_profile;
|
||||
const char *param;
|
||||
const char *param_end;
|
||||
const char *ctype;
|
||||
|
@ -1013,6 +1083,10 @@ httpd_stream_file(struct httpd_request *hreq, int id)
|
|||
{
|
||||
DPRINTF(E_INFO, L_HTTPD, "Preparing to transcode %s\n", mfi->path);
|
||||
|
||||
spk_profile = httpd_xcode_profile_get(hreq);
|
||||
if (spk_profile != XCODE_NONE)
|
||||
profile = spk_profile;
|
||||
|
||||
st = stream_new_transcode(mfi, profile, hreq, offset, end_offset, stream_chunk_xcode_cb);
|
||||
if (!st)
|
||||
goto error;
|
||||
|
@ -1119,6 +1193,32 @@ httpd_stream_file(struct httpd_request *hreq, int id)
|
|||
free_mfi(mfi, 0);
|
||||
}
|
||||
|
||||
// Returns enum transcode_profile, but is just declared with int so we don't
|
||||
// need to include transcode.h in httpd_internal.h
|
||||
int
|
||||
httpd_xcode_profile_get(struct httpd_request *hreq)
|
||||
{
|
||||
struct player_speaker_info spk;
|
||||
int ret;
|
||||
|
||||
DPRINTF(E_DBG, L_HTTPD, "Checking if client '%s' is a speaker\n", hreq->peer_address);
|
||||
|
||||
// A Roku Soundbridge may also be RCP device/speaker for which the user may
|
||||
// have set a prefered streaming format
|
||||
ret = player_speaker_get_byaddress(&spk, hreq->peer_address);
|
||||
if (ret < 0)
|
||||
return XCODE_NONE;
|
||||
|
||||
if (spk.format == MEDIA_FORMAT_WAV)
|
||||
return XCODE_WAV;
|
||||
if (spk.format == MEDIA_FORMAT_MP3)
|
||||
return XCODE_MP3;
|
||||
if (spk.format == MEDIA_FORMAT_ALAC)
|
||||
return XCODE_MP4_ALAC;
|
||||
|
||||
return XCODE_NONE;
|
||||
}
|
||||
|
||||
struct evbuffer *
|
||||
httpd_gzip_deflate(struct evbuffer *in)
|
||||
{
|
||||
|
@ -1186,7 +1286,6 @@ httpd_gzip_deflate(struct evbuffer *in)
|
|||
return NULL;
|
||||
}
|
||||
|
||||
|
||||
// The httpd_send functions below can be called from a worker thread (with
|
||||
// hreq->is_async) or directly from the httpd thread. In the former case, they
|
||||
// will command sending from the httpd thread, since it is not safe to access
|
||||
|
@ -1501,6 +1600,10 @@ httpd_init(const char *webroot)
|
|||
goto error;
|
||||
}
|
||||
|
||||
// We need to know about speaker format changes so we can ask the cache to
|
||||
// start preparing headers for mp4/alac if selected
|
||||
listener_add(httpd_speaker_update_handler, LISTENER_SPEAKER);
|
||||
|
||||
return 0;
|
||||
|
||||
error:
|
||||
|
@ -1512,6 +1615,8 @@ httpd_init(const char *webroot)
|
|||
void
|
||||
httpd_deinit(void)
|
||||
{
|
||||
listener_remove(httpd_speaker_update_handler);
|
||||
|
||||
// Give modules a chance to hang up connections nicely
|
||||
modules_deinit();
|
||||
|
||||
|
|
|
@ -1146,12 +1146,13 @@ daap_reply_songlist_generic(struct httpd_request *hreq, int playlist)
|
|||
const struct dmap_field **meta = NULL;
|
||||
struct sort_ctx *sctx;
|
||||
const char *param;
|
||||
const char *client_codecs;
|
||||
const char *accept_codecs;
|
||||
const char *tag;
|
||||
size_t len;
|
||||
enum transcode_profile spk_profile;
|
||||
enum transcode_profile profile;
|
||||
struct transcode_metadata_string xcode_metadata;
|
||||
struct media_quality quality = { HTTPD_STREAM_SAMPLE_RATE, HTTPD_STREAM_BPS, HTTPD_STREAM_CHANNELS, HTTPD_STREAM_BIT_RATE };
|
||||
struct media_quality quality = { 0 };
|
||||
uint32_t len_ms;
|
||||
int nmeta = 0;
|
||||
int sort_headers;
|
||||
|
@ -1216,12 +1217,14 @@ daap_reply_songlist_generic(struct httpd_request *hreq, int playlist)
|
|||
goto error;
|
||||
}
|
||||
|
||||
client_codecs = NULL;
|
||||
accept_codecs = NULL;
|
||||
if (!s->is_remote && hreq->in_headers)
|
||||
{
|
||||
client_codecs = httpd_header_find(hreq->in_headers, "Accept-Codecs");
|
||||
accept_codecs = httpd_header_find(hreq->in_headers, "Accept-Codecs");
|
||||
}
|
||||
|
||||
spk_profile = httpd_xcode_profile_get(hreq);
|
||||
|
||||
nsongs = 0;
|
||||
while ((ret = db_query_fetch_file(&dbmfi, &qp)) == 0)
|
||||
{
|
||||
|
@ -1229,16 +1232,24 @@ daap_reply_songlist_generic(struct httpd_request *hreq, int playlist)
|
|||
|
||||
// Not sure if the is_remote path is really needed. Note that if you
|
||||
// change the below you might need to do the same in rsp_reply_playlist()
|
||||
profile = s->is_remote ? XCODE_WAV : transcode_needed(hreq->user_agent, client_codecs, dbmfi.codectype);
|
||||
profile = s->is_remote ? XCODE_WAV : transcode_needed(hreq->user_agent, accept_codecs, dbmfi.codectype);
|
||||
if (profile == XCODE_UNKNOWN)
|
||||
{
|
||||
DPRINTF(E_LOG, L_DAAP, "Cannot transcode '%s', codec type is unknown\n", dbmfi.fname);
|
||||
}
|
||||
else if (profile != XCODE_NONE)
|
||||
{
|
||||
if (spk_profile != XCODE_NONE)
|
||||
profile = spk_profile;
|
||||
|
||||
if (safe_atou32(dbmfi.song_length, &len_ms) < 0)
|
||||
len_ms = 3 * 60 * 1000; // just a fallback default
|
||||
|
||||
safe_atoi32(dbmfi.samplerate, &quality.sample_rate);
|
||||
safe_atoi32(dbmfi.bits_per_sample, &quality.bits_per_sample);
|
||||
safe_atoi32(dbmfi.channels, &quality.channels);
|
||||
quality.bit_rate = cfg_getint(cfg_getsec(cfg, "streaming"), "bit_rate");
|
||||
|
||||
transcode_metadata_strings_set(&xcode_metadata, profile, &quality, len_ms);
|
||||
dbmfi.type = xcode_metadata.type;
|
||||
dbmfi.codectype = xcode_metadata.codectype;
|
||||
|
@ -2271,7 +2282,7 @@ daap_request(struct httpd_request *hreq)
|
|||
|
||||
DPRINTF(E_DBG, L_DAAP, "DAAP request handled in %d milliseconds\n", msec);
|
||||
|
||||
if (ret == DAAP_REPLY_OK && msec > cache_daap_threshold() && hreq->user_agent)
|
||||
if (ret == DAAP_REPLY_OK && msec > cache_daap_threshold_get() && hreq->user_agent)
|
||||
cache_daap_add(hreq->uri, hreq->user_agent, ((struct daap_session *)hreq->extra_data)->is_remote, msec);
|
||||
|
||||
daap_reply_send(hreq, ret); // hreq is deallocted
|
||||
|
|
|
@ -36,11 +36,6 @@
|
|||
#define HTTP_BADGATEWAY 502 /**< received an invalid response from the upstream */
|
||||
#define HTTP_SERVUNAVAIL 503 /**< the server is not available */
|
||||
|
||||
#define HTTPD_STREAM_SAMPLE_RATE 44100
|
||||
#define HTTPD_STREAM_BPS 16
|
||||
#define HTTPD_STREAM_CHANNELS 2
|
||||
#define HTTPD_STREAM_BIT_RATE 320000
|
||||
|
||||
|
||||
struct httpd_request;
|
||||
|
||||
|
@ -211,6 +206,9 @@ struct httpd_request {
|
|||
void
|
||||
httpd_stream_file(struct httpd_request *hreq, int id);
|
||||
|
||||
int
|
||||
httpd_xcode_profile_get(struct httpd_request *hreq);
|
||||
|
||||
void
|
||||
httpd_request_handler_set(struct httpd_request *hreq);
|
||||
|
||||
|
|
|
@ -1526,10 +1526,19 @@ static json_object *
|
|||
speaker_to_json(struct player_speaker_info *spk)
|
||||
{
|
||||
json_object *output;
|
||||
json_object *supported_formats;
|
||||
char output_id[21];
|
||||
enum media_format format;
|
||||
|
||||
output = json_object_new_object();
|
||||
|
||||
supported_formats = json_object_new_array();
|
||||
for (format = MEDIA_FORMAT_FIRST; format <= MEDIA_FORMAT_LAST; format = MEDIA_FORMAT_NEXT(format))
|
||||
{
|
||||
if (format & spk->supported_formats)
|
||||
json_object_array_add(supported_formats, json_object_new_string(media_format_to_string(format)));
|
||||
}
|
||||
|
||||
snprintf(output_id, sizeof(output_id), "%" PRIu64, spk->id);
|
||||
json_object_object_add(output, "id", json_object_new_string(output_id));
|
||||
json_object_object_add(output, "name", json_object_new_string(spk->name));
|
||||
|
@ -1539,6 +1548,8 @@ speaker_to_json(struct player_speaker_info *spk)
|
|||
json_object_object_add(output, "requires_auth", json_object_new_boolean(spk->requires_auth));
|
||||
json_object_object_add(output, "needs_auth_key", json_object_new_boolean(spk->needs_auth_key));
|
||||
json_object_object_add(output, "volume", json_object_new_int(spk->absvol));
|
||||
json_object_object_add(output, "format", json_object_new_string(media_format_to_string(spk->format)));
|
||||
json_object_object_add(output, "supported_formats", supported_formats);
|
||||
|
||||
return output;
|
||||
}
|
||||
|
@ -1602,6 +1613,7 @@ jsonapi_reply_outputs_put_byid(struct httpd_request *hreq)
|
|||
bool selected;
|
||||
int volume;
|
||||
const char *pin;
|
||||
const char *format;
|
||||
int ret;
|
||||
|
||||
ret = safe_atou64(hreq->path_parts[2], &output_id);
|
||||
|
@ -1644,10 +1656,17 @@ jsonapi_reply_outputs_put_byid(struct httpd_request *hreq)
|
|||
ret = player_speaker_authorize(output_id, pin);
|
||||
}
|
||||
|
||||
if (ret == 0 && jparse_contains_key(request, "format", json_type_string))
|
||||
{
|
||||
format = jparse_str_from_obj(request, "format");
|
||||
if (format)
|
||||
ret = player_speaker_format_set(output_id, media_format_from_string(format));
|
||||
}
|
||||
|
||||
jparse_free(request);
|
||||
|
||||
if (ret < 0)
|
||||
return HTTP_INTERNAL;
|
||||
return HTTP_BADREQUEST;
|
||||
|
||||
return HTTP_NOCONTENT;
|
||||
}
|
||||
|
|
|
@ -525,6 +525,7 @@ httpd_backend_output_buffer_get(httpd_backend *backend)
|
|||
int
|
||||
httpd_backend_peer_get(const char **addr, uint16_t *port, httpd_backend *backend, httpd_backend_data *backend_data)
|
||||
{
|
||||
#define IPV4_MAPPED_IPV6_PREFIX "::ffff:"
|
||||
httpd_connection *conn = evhttp_request_get_connection(backend);
|
||||
if (!conn)
|
||||
return -1;
|
||||
|
@ -534,6 +535,11 @@ httpd_backend_peer_get(const char **addr, uint16_t *port, httpd_backend *backend
|
|||
#else
|
||||
evhttp_connection_get_peer(conn, (char **)addr, port);
|
||||
#endif
|
||||
|
||||
// Just use the pure ipv4 address if it's mapped
|
||||
if (strncmp(*addr, IPV4_MAPPED_IPV6_PREFIX, strlen(IPV4_MAPPED_IPV6_PREFIX)) == 0)
|
||||
*addr += strlen(IPV4_MAPPED_IPV6_PREFIX);
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
|
|
@ -415,9 +415,9 @@ rsp_reply_db(struct httpd_request *hreq)
|
|||
}
|
||||
|
||||
static int
|
||||
item_add(xml_node *parent, struct query_params *qp, const char *user_agent, const char *client_codecs, int mode)
|
||||
item_add(xml_node *parent, struct query_params *qp, enum transcode_profile spk_profile, const char *user_agent, const char *accept_codecs, int mode)
|
||||
{
|
||||
struct media_quality quality = { HTTPD_STREAM_SAMPLE_RATE, HTTPD_STREAM_BPS, HTTPD_STREAM_CHANNELS, HTTPD_STREAM_BIT_RATE };
|
||||
struct media_quality quality = { 0 };
|
||||
struct db_media_file_info dbmfi;
|
||||
struct transcode_metadata_string xcode_metadata;
|
||||
enum transcode_profile profile;
|
||||
|
@ -432,18 +432,26 @@ item_add(xml_node *parent, struct query_params *qp, const char *user_agent, cons
|
|||
if (ret != 0)
|
||||
return ret;
|
||||
|
||||
profile = transcode_needed(user_agent, client_codecs, dbmfi.codectype);
|
||||
profile = transcode_needed(user_agent, accept_codecs, dbmfi.codectype);
|
||||
if (profile == XCODE_UNKNOWN)
|
||||
{
|
||||
DPRINTF(E_LOG, L_DAAP, "Cannot transcode '%s', codec type is unknown\n", dbmfi.fname);
|
||||
}
|
||||
else if (profile != XCODE_NONE)
|
||||
{
|
||||
if (spk_profile != XCODE_NONE)
|
||||
profile = spk_profile; // User has configured a specific transcode format for this speaker
|
||||
|
||||
orgcodec = dbmfi.codectype;
|
||||
|
||||
if (safe_atou32(dbmfi.song_length, &len_ms) < 0)
|
||||
len_ms = 3 * 60 * 1000; // just a fallback default
|
||||
|
||||
safe_atoi32(dbmfi.samplerate, &quality.sample_rate);
|
||||
safe_atoi32(dbmfi.bits_per_sample, &quality.bits_per_sample);
|
||||
safe_atoi32(dbmfi.channels, &quality.channels);
|
||||
quality.bit_rate = cfg_getint(cfg_getsec(cfg, "streaming"), "bit_rate");
|
||||
|
||||
transcode_metadata_strings_set(&xcode_metadata, profile, &quality, len_ms);
|
||||
dbmfi.type = xcode_metadata.type;
|
||||
dbmfi.codectype = xcode_metadata.codectype;
|
||||
|
@ -479,7 +487,8 @@ rsp_reply_playlist(struct httpd_request *hreq)
|
|||
{
|
||||
struct query_params qp;
|
||||
const char *param;
|
||||
const char *client_codecs;
|
||||
const char *accept_codecs;
|
||||
enum transcode_profile spk_profile;
|
||||
xml_node *response;
|
||||
xml_node *items;
|
||||
int mode;
|
||||
|
@ -488,7 +497,8 @@ rsp_reply_playlist(struct httpd_request *hreq)
|
|||
|
||||
memset(&qp, 0, sizeof(struct query_params));
|
||||
|
||||
client_codecs = httpd_header_find(hreq->in_headers, "Accept-Codecs");
|
||||
accept_codecs = httpd_header_find(hreq->in_headers, "Accept-Codecs");
|
||||
spk_profile = httpd_xcode_profile_get(hreq);
|
||||
|
||||
ret = safe_atoi32(hreq->path_parts[2], &qp.id);
|
||||
if (ret < 0)
|
||||
|
@ -550,7 +560,7 @@ rsp_reply_playlist(struct httpd_request *hreq)
|
|||
items = xml_new_node(response, "items", NULL);
|
||||
do
|
||||
{
|
||||
ret = item_add(items, &qp, hreq->user_agent, client_codecs, mode);
|
||||
ret = item_add(items, &qp, spk_profile, hreq->user_agent, accept_codecs, mode);
|
||||
}
|
||||
while (ret == 0);
|
||||
|
||||
|
@ -715,6 +725,16 @@ rsp_stream(struct httpd_request *hreq)
|
|||
// /rsp/stream/36364
|
||||
// /rsp/db/0?query=id%3D36365&type=full
|
||||
// /rsp/stream/36365
|
||||
//
|
||||
// Headers sent from Roku M2000 and M1001 in stream requests (and other?):
|
||||
//
|
||||
// 'User-Agent': 'Roku SoundBridge/3.0'
|
||||
// 'Host': '192.168.1.119:3689'
|
||||
// 'Accept': '*/*'
|
||||
// 'Pragma': 'no-cache'
|
||||
// 'accept-codecs': 'wma,mpeg,wav,mp4a,alac'
|
||||
// 'rsp-version': '0.1'
|
||||
// 'transcode-codecs': 'wav,mp3'
|
||||
static struct httpd_uri_map rsp_handlers[] =
|
||||
{
|
||||
{
|
||||
|
|
|
@ -228,7 +228,7 @@ session_free(struct streaming_session *session)
|
|||
}
|
||||
|
||||
static struct streaming_session *
|
||||
session_new(struct httpd_request *hreq, bool icy_is_requested, enum player_format format, struct media_quality quality)
|
||||
session_new(struct httpd_request *hreq, bool icy_is_requested, enum media_format format, struct media_quality quality)
|
||||
{
|
||||
struct streaming_session *session;
|
||||
int audio_fd;
|
||||
|
@ -279,7 +279,7 @@ streaming_mp3_handler(struct httpd_request *hreq)
|
|||
httpd_header_add(hreq->out_headers, "icy-metaint", buf);
|
||||
}
|
||||
|
||||
session = session_new(hreq, icy_is_requested, PLAYER_FORMAT_MP3, streaming_default_quality);
|
||||
session = session_new(hreq, icy_is_requested, MEDIA_FORMAT_MP3, streaming_default_quality);
|
||||
if (!session)
|
||||
return -1; // Error sent by caller
|
||||
|
||||
|
|
|
@ -36,9 +36,11 @@
|
|||
static int
|
||||
setup(struct input_source *source)
|
||||
{
|
||||
struct transcode_decode_setup_args decode_args = { .profile = XCODE_PCM_NATIVE, .path = source->path, .len_ms = source->len_ms };
|
||||
struct transcode_encode_setup_args encode_args = { .profile = XCODE_PCM_NATIVE, };
|
||||
struct transcode_ctx *ctx;
|
||||
|
||||
ctx = transcode_setup(XCODE_PCM_NATIVE, NULL, source->data_kind, source->path, source->len_ms);
|
||||
ctx = transcode_setup(decode_args, encode_args);
|
||||
if (!ctx)
|
||||
return -1;
|
||||
|
||||
|
|
|
@ -295,6 +295,8 @@ metadata_prepare(struct input_source *source)
|
|||
static int
|
||||
setup(struct input_source *source)
|
||||
{
|
||||
struct transcode_decode_setup_args decode_args = { .profile = XCODE_PCM_NATIVE, .is_http = true, .len_ms = source->len_ms };
|
||||
struct transcode_encode_setup_args encode_args = { .profile = XCODE_PCM_NATIVE, };
|
||||
struct transcode_ctx *ctx;
|
||||
char *url;
|
||||
|
||||
|
@ -303,8 +305,9 @@ setup(struct input_source *source)
|
|||
|
||||
free(source->path);
|
||||
source->path = url;
|
||||
decode_args.path = url;
|
||||
|
||||
ctx = transcode_setup(XCODE_PCM_NATIVE, NULL, source->data_kind, source->path, source->len_ms);
|
||||
ctx = transcode_setup(decode_args, encode_args);
|
||||
if (!ctx)
|
||||
return -1;
|
||||
|
||||
|
|
|
@ -391,21 +391,18 @@ download_seek(void *arg, int64_t offset, enum transcode_seek_type type)
|
|||
static int
|
||||
download_xcode_setup(struct download_ctx *download)
|
||||
{
|
||||
struct transcode_ctx *xcode;
|
||||
struct transcode_decode_setup_args decode_args = { .profile = XCODE_OGG, .len_ms = download->len_ms };
|
||||
struct transcode_encode_setup_args encode_args = { .profile = XCODE_PCM16, };
|
||||
struct transcode_evbuf_io xcode_evbuf_io = { 0 };
|
||||
|
||||
CHECK_NULL(L_SPOTIFY, xcode = malloc(sizeof(struct transcode_ctx)));
|
||||
struct transcode_ctx *xcode;
|
||||
|
||||
xcode_evbuf_io.evbuf = download->read_buf;
|
||||
xcode_evbuf_io.seekfn = download_seek;
|
||||
xcode_evbuf_io.seekfn_arg = download;
|
||||
decode_args.evbuf_io = &xcode_evbuf_io;
|
||||
|
||||
xcode->decode_ctx = transcode_decode_setup(XCODE_OGG, NULL, DATA_KIND_SPOTIFY, NULL, &xcode_evbuf_io, download->len_ms);
|
||||
if (!xcode->decode_ctx)
|
||||
goto error;
|
||||
|
||||
xcode->encode_ctx = transcode_encode_setup(XCODE_PCM16, NULL, xcode->decode_ctx, 0, 0);
|
||||
if (!xcode->encode_ctx)
|
||||
xcode = transcode_setup(decode_args, encode_args);
|
||||
if (!xcode)
|
||||
goto error;
|
||||
|
||||
download->xcode = xcode;
|
||||
|
|
34
src/misc.c
34
src/misc.c
|
@ -1731,6 +1731,40 @@ quality_is_equal(struct media_quality *a, struct media_quality *b)
|
|||
return (a->sample_rate == b->sample_rate && a->bits_per_sample == b->bits_per_sample && a->channels == b->channels && a->bit_rate == b->bit_rate);
|
||||
}
|
||||
|
||||
enum media_format
|
||||
media_format_from_string(const char *s)
|
||||
{
|
||||
if (strcmp(s, "pcm") == 0)
|
||||
return MEDIA_FORMAT_PCM;
|
||||
if (strcmp(s, "wav") == 0)
|
||||
return MEDIA_FORMAT_WAV;
|
||||
if (strcmp(s, "mp3") == 0)
|
||||
return MEDIA_FORMAT_MP3;
|
||||
if (strcmp(s, "alac") == 0)
|
||||
return MEDIA_FORMAT_ALAC;
|
||||
if (strcmp(s, "opus") == 0)
|
||||
return MEDIA_FORMAT_OPUS;
|
||||
|
||||
return MEDIA_FORMAT_UNKNOWN;
|
||||
}
|
||||
|
||||
const char *
|
||||
media_format_to_string(enum media_format format)
|
||||
{
|
||||
if (format == MEDIA_FORMAT_PCM)
|
||||
return "pcm";
|
||||
if (format == MEDIA_FORMAT_WAV)
|
||||
return "wav";
|
||||
if (format == MEDIA_FORMAT_MP3)
|
||||
return "mp3";
|
||||
if (format == MEDIA_FORMAT_ALAC)
|
||||
return "alac";
|
||||
if (format == MEDIA_FORMAT_OPUS)
|
||||
return "opus";
|
||||
|
||||
return "unknown";
|
||||
}
|
||||
|
||||
|
||||
/* -------------------------- Misc utility functions ------------------------ */
|
||||
|
||||
|
|
33
src/misc.h
33
src/misc.h
|
@ -59,6 +59,18 @@ net_is_http_or_https(const char *url);
|
|||
|
||||
/* ----------------------- Conversion/hashing/sanitizers -------------------- */
|
||||
|
||||
#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
|
||||
|
||||
// Samples to bytes, bytes to samples
|
||||
#define STOB(s, bits, c) ((s) * (c) * (bits) / 8)
|
||||
#define BTOS(b, bits, c) ((b) / ((c) * (bits) / 8))
|
||||
|
@ -260,6 +272,21 @@ timespec_reltoabs(struct timespec relative);
|
|||
|
||||
/* ------------------------------- Media quality ---------------------------- */
|
||||
|
||||
// Bit flags for the sake of outputs announcing what they support
|
||||
enum media_format {
|
||||
MEDIA_FORMAT_UNKNOWN = 0,
|
||||
MEDIA_FORMAT_PCM = (1 << 0),
|
||||
MEDIA_FORMAT_WAV = (1 << 1),
|
||||
MEDIA_FORMAT_MP3 = (1 << 2),
|
||||
MEDIA_FORMAT_ALAC = (1 << 3),
|
||||
MEDIA_FORMAT_OPUS = (1 << 4),
|
||||
};
|
||||
|
||||
// For iteration
|
||||
#define MEDIA_FORMAT_FIRST MEDIA_FORMAT_PCM
|
||||
#define MEDIA_FORMAT_LAST MEDIA_FORMAT_OPUS
|
||||
#define MEDIA_FORMAT_NEXT(f) (f << 1)
|
||||
|
||||
// Remember to adjust quality_is_equal() if adding elements
|
||||
struct media_quality {
|
||||
int sample_rate;
|
||||
|
@ -271,6 +298,12 @@ struct media_quality {
|
|||
bool
|
||||
quality_is_equal(struct media_quality *a, struct media_quality *b);
|
||||
|
||||
enum media_format
|
||||
media_format_from_string(const char *s);
|
||||
|
||||
const char *
|
||||
media_format_to_string(enum media_format format);
|
||||
|
||||
|
||||
/* -------------------------- Misc utility functions ------------------------ */
|
||||
|
||||
|
|
|
@ -280,8 +280,8 @@ quality_to_xcode(struct media_quality *quality)
|
|||
static int
|
||||
encoding_reset(struct media_quality *quality)
|
||||
{
|
||||
struct transcode_encode_setup_args encode_args = { 0 };
|
||||
struct output_quality_subscription *subscription;
|
||||
struct decode_ctx *decode_ctx;
|
||||
enum transcode_profile profile;
|
||||
int i;
|
||||
|
||||
|
@ -293,8 +293,8 @@ encoding_reset(struct media_quality *quality)
|
|||
return -1;
|
||||
}
|
||||
|
||||
decode_ctx = transcode_decode_setup_raw(profile, quality);
|
||||
if (!decode_ctx)
|
||||
encode_args.src_ctx = transcode_decode_setup_raw(profile, quality);
|
||||
if (!encode_args.src_ctx)
|
||||
{
|
||||
DPRINTF(E_LOG, L_PLAYER, "Could not create subscription decoding context (profile %d)\n", profile);
|
||||
return -1;
|
||||
|
@ -309,15 +309,16 @@ encoding_reset(struct media_quality *quality)
|
|||
if (quality_is_equal(quality, &subscription->quality))
|
||||
continue; // No resampling required
|
||||
|
||||
profile = quality_to_xcode(&subscription->quality);
|
||||
if (profile != XCODE_UNKNOWN)
|
||||
subscription->encode_ctx = transcode_encode_setup(profile, &subscription->quality, decode_ctx, 0, 0);
|
||||
encode_args.profile = quality_to_xcode(&subscription->quality);
|
||||
encode_args.quality = &subscription->quality;
|
||||
if (encode_args.profile != XCODE_UNKNOWN)
|
||||
subscription->encode_ctx = transcode_encode_setup(encode_args);
|
||||
else
|
||||
DPRINTF(E_LOG, L_PLAYER, "Could not setup resampling to %d/%d/%d for output\n",
|
||||
subscription->quality.sample_rate, subscription->quality.bits_per_sample, subscription->quality.channels);
|
||||
}
|
||||
|
||||
transcode_decode_cleanup(&decode_ctx);
|
||||
transcode_decode_cleanup(&encode_args.src_ctx);
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
|
|
@ -134,7 +134,11 @@ struct output_device
|
|||
|
||||
// Quality of audio output
|
||||
struct media_quality quality;
|
||||
int format;
|
||||
|
||||
// selected_format only set (not UNKNOWN) in case of active user selection
|
||||
enum media_format selected_format;
|
||||
enum media_format default_format;
|
||||
uint32_t supported_formats;
|
||||
|
||||
// Address
|
||||
char *v4_address;
|
||||
|
|
|
@ -1120,7 +1120,7 @@ static struct airplay_master_session *
|
|||
master_session_make(struct media_quality *quality)
|
||||
{
|
||||
struct airplay_master_session *rms;
|
||||
struct decode_ctx *decode_ctx;
|
||||
struct transcode_encode_setup_args encode_args = { .profile = XCODE_ALAC, .quality = quality };
|
||||
int ret;
|
||||
|
||||
// First check if we already have a suitable session
|
||||
|
@ -1146,15 +1146,15 @@ master_session_make(struct media_quality *quality)
|
|||
goto error;
|
||||
}
|
||||
|
||||
decode_ctx = transcode_decode_setup_raw(XCODE_PCM16, quality);
|
||||
if (!decode_ctx)
|
||||
encode_args.src_ctx = transcode_decode_setup_raw(XCODE_PCM16, quality);
|
||||
if (!encode_args.src_ctx)
|
||||
{
|
||||
DPRINTF(E_LOG, L_AIRPLAY, "Could not create decoding context\n");
|
||||
goto error;
|
||||
}
|
||||
|
||||
rms->encode_ctx = transcode_encode_setup(XCODE_ALAC, quality, decode_ctx, 0, 0);
|
||||
transcode_decode_cleanup(&decode_ctx);
|
||||
rms->encode_ctx = transcode_encode_setup(encode_args);
|
||||
transcode_decode_cleanup(&encode_args.src_ctx);
|
||||
if (!rms->encode_ctx)
|
||||
{
|
||||
DPRINTF(E_LOG, L_AIRPLAY, "Will not be able to stream AirPlay 2, ffmpeg has no ALAC encoder\n");
|
||||
|
@ -3737,6 +3737,7 @@ airplay_device_cb(const char *name, const char *type, const char *domain, const
|
|||
rd->type = OUTPUT_TYPE_AIRPLAY;
|
||||
rd->type_name = outputs_name(rd->type);
|
||||
rd->extra_device_info = re;
|
||||
rd->supported_formats = MEDIA_FORMAT_ALAC;
|
||||
|
||||
if (port < 0)
|
||||
{
|
||||
|
|
|
@ -1374,6 +1374,7 @@ alsa_device_add(cfg_t* cfg_audio, int id)
|
|||
device->type = OUTPUT_TYPE_ALSA;
|
||||
device->type_name = outputs_name(device->type);
|
||||
device->extra_device_info = ae;
|
||||
device->supported_formats = MEDIA_FORMAT_PCM;
|
||||
|
||||
// The audio section will have no title, so there we get the value from the
|
||||
// "card" option
|
||||
|
|
|
@ -32,15 +32,7 @@
|
|||
#include <ifaddrs.h>
|
||||
#include <unistd.h>
|
||||
#include <fcntl.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 htobe32(x) OSSwapHostToBigInt32(x)
|
||||
#define be32toh(x) OSSwapBigToHostInt32(x)
|
||||
#endif
|
||||
|
||||
#include <gnutls/gnutls.h>
|
||||
#include <event2/event.h>
|
||||
#include <json.h>
|
||||
|
@ -55,6 +47,7 @@
|
|||
#include "outputs.h"
|
||||
#include "db.h"
|
||||
#include "artwork.h"
|
||||
#include "misc.h"
|
||||
|
||||
#ifdef HAVE_PROTOBUF_OLD
|
||||
#include "cast_channel.v0.pb-c.h"
|
||||
|
@ -1785,6 +1778,7 @@ cast_device_cb(const char *name, const char *type, const char *domain, const cha
|
|||
device->name = strdup(name);
|
||||
device->type = OUTPUT_TYPE_CAST;
|
||||
device->type_name = outputs_name(device->type);
|
||||
device->supported_formats = MEDIA_FORMAT_OPUS;
|
||||
|
||||
if (port < 0)
|
||||
{
|
||||
|
@ -2369,7 +2363,7 @@ cast_metadata_send(struct output_metadata *metadata)
|
|||
static int
|
||||
cast_init(void)
|
||||
{
|
||||
struct decode_ctx *decode_ctx;
|
||||
struct transcode_encode_setup_args encode_args = { .profile = XCODE_OPUS, .quality = &cast_quality_default };
|
||||
int i;
|
||||
int ret;
|
||||
|
||||
|
@ -2393,15 +2387,15 @@ cast_init(void)
|
|||
return -1;
|
||||
}
|
||||
|
||||
decode_ctx = transcode_decode_setup_raw(XCODE_PCM16, &cast_quality_default);
|
||||
if (!decode_ctx)
|
||||
encode_args.src_ctx = transcode_decode_setup_raw(XCODE_PCM16, &cast_quality_default);
|
||||
if (!encode_args.src_ctx)
|
||||
{
|
||||
DPRINTF(E_LOG, L_CAST, "Could not create decoding context\n");
|
||||
goto out_tls_deinit;
|
||||
}
|
||||
|
||||
cast_encode_ctx = transcode_encode_setup(XCODE_OPUS, &cast_quality_default, decode_ctx, 0, 0);
|
||||
transcode_decode_cleanup(&decode_ctx);
|
||||
cast_encode_ctx = transcode_encode_setup(encode_args);
|
||||
transcode_decode_cleanup(&encode_args.src_ctx);
|
||||
if (!cast_encode_ctx)
|
||||
{
|
||||
DPRINTF(E_LOG, L_CAST, "Will not be able to stream Chromecast, libav does not support Opus encoding\n");
|
||||
|
|
|
@ -491,6 +491,7 @@ fifo_init(void)
|
|||
device->type_name = outputs_name(device->type);
|
||||
device->has_video = 0;
|
||||
device->extra_device_info = path;
|
||||
device->supported_formats = MEDIA_FORMAT_PCM;
|
||||
DPRINTF(E_INFO, L_FIFO, "Adding fifo output device '%s' with path '%s'\n", nickname, path);
|
||||
|
||||
player_device_add(device);
|
||||
|
|
|
@ -436,6 +436,7 @@ sinklist_cb(pa_context *ctx, const pa_sink_info *info, int eol, void *userdata)
|
|||
device->type = OUTPUT_TYPE_PULSE;
|
||||
device->type_name = outputs_name(device->type);
|
||||
device->extra_device_info = strdup(info->name);
|
||||
device->supported_formats = MEDIA_FORMAT_PCM;
|
||||
|
||||
player_device_add(device);
|
||||
}
|
||||
|
|
|
@ -1855,7 +1855,7 @@ static struct raop_master_session *
|
|||
master_session_make(struct media_quality *quality, bool encrypt)
|
||||
{
|
||||
struct raop_master_session *rms;
|
||||
struct decode_ctx *decode_ctx;
|
||||
struct transcode_encode_setup_args encode_args = { .profile = XCODE_ALAC, .quality = quality };
|
||||
int ret;
|
||||
|
||||
// First check if we already have a suitable session
|
||||
|
@ -1883,15 +1883,15 @@ master_session_make(struct media_quality *quality, bool encrypt)
|
|||
return NULL;
|
||||
}
|
||||
|
||||
decode_ctx = transcode_decode_setup_raw(XCODE_PCM16, quality);
|
||||
if (!decode_ctx)
|
||||
encode_args.src_ctx = transcode_decode_setup_raw(XCODE_PCM16, quality);
|
||||
if (!encode_args.src_ctx)
|
||||
{
|
||||
DPRINTF(E_LOG, L_RAOP, "Could not create decoding context\n");
|
||||
goto error;
|
||||
}
|
||||
|
||||
rms->encode_ctx = transcode_encode_setup(XCODE_ALAC, quality, decode_ctx, 0, 0);
|
||||
transcode_decode_cleanup(&decode_ctx);
|
||||
rms->encode_ctx = transcode_encode_setup(encode_args);
|
||||
transcode_decode_cleanup(&encode_args.src_ctx);
|
||||
if (!rms->encode_ctx)
|
||||
{
|
||||
DPRINTF(E_LOG, L_RAOP, "Will not be able to stream AirPlay 2, ffmpeg has no ALAC encoder\n");
|
||||
|
@ -4230,6 +4230,7 @@ raop_device_cb(const char *name, const char *type, const char *domain, const cha
|
|||
rd->type = OUTPUT_TYPE_RAOP;
|
||||
rd->type_name = outputs_name(rd->type);
|
||||
rd->extra_device_info = re;
|
||||
rd->supported_formats = MEDIA_FORMAT_ALAC;
|
||||
|
||||
if (port < 0)
|
||||
{
|
||||
|
|
|
@ -1295,6 +1295,8 @@ rcp_mdns_device_cb(const char *name, const char *type, const char *domain, const
|
|||
device->name = strdup(name);
|
||||
device->type = OUTPUT_TYPE_RCP;
|
||||
device->type_name = outputs_name(device->type);
|
||||
device->default_format = MEDIA_FORMAT_WAV;
|
||||
device->supported_formats = MEDIA_FORMAT_WAV | MEDIA_FORMAT_MP3 | MEDIA_FORMAT_ALAC;
|
||||
|
||||
if (port < 0 || !address)
|
||||
{
|
||||
|
|
|
@ -5,18 +5,6 @@
|
|||
#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;
|
||||
|
|
|
@ -65,7 +65,7 @@ struct streaming_wanted
|
|||
struct pipepair audio[WANTED_PIPES_MAX];
|
||||
struct pipepair metadata[WANTED_PIPES_MAX];
|
||||
|
||||
enum player_format format;
|
||||
enum media_format format;
|
||||
struct media_quality quality;
|
||||
|
||||
struct evbuffer *audio_in;
|
||||
|
@ -113,27 +113,27 @@ extern struct event_base *evbase_player;
|
|||
/* ------------------------------- Helpers ---------------------------------- */
|
||||
|
||||
static struct encode_ctx *
|
||||
encoder_setup(enum player_format format, struct media_quality *quality)
|
||||
encoder_setup(enum media_format format, struct media_quality *quality)
|
||||
{
|
||||
struct decode_ctx *decode_ctx = NULL;
|
||||
struct transcode_encode_setup_args encode_args = { .profile = XCODE_MP3, .quality = quality };
|
||||
struct encode_ctx *encode_ctx = NULL;
|
||||
|
||||
if (quality->bits_per_sample == 16)
|
||||
decode_ctx = transcode_decode_setup_raw(XCODE_PCM16, quality);
|
||||
encode_args.src_ctx = transcode_decode_setup_raw(XCODE_PCM16, quality);
|
||||
else if (quality->bits_per_sample == 24)
|
||||
decode_ctx = transcode_decode_setup_raw(XCODE_PCM24, quality);
|
||||
encode_args.src_ctx = transcode_decode_setup_raw(XCODE_PCM24, quality);
|
||||
else if (quality->bits_per_sample == 32)
|
||||
decode_ctx = transcode_decode_setup_raw(XCODE_PCM32, quality);
|
||||
encode_args.src_ctx = transcode_decode_setup_raw(XCODE_PCM32, quality);
|
||||
|
||||
if (!decode_ctx)
|
||||
if (!encode_args.src_ctx)
|
||||
{
|
||||
DPRINTF(E_LOG, L_STREAMING, "Error setting up decoder for quality sr %d, bps %d, ch %d, cannot encode\n",
|
||||
quality->sample_rate, quality->bits_per_sample, quality->channels);
|
||||
goto out;
|
||||
}
|
||||
|
||||
if (format == PLAYER_FORMAT_MP3)
|
||||
encode_ctx = transcode_encode_setup(XCODE_MP3, quality, decode_ctx, 0, 0);
|
||||
if (format == MEDIA_FORMAT_MP3)
|
||||
encode_ctx = transcode_encode_setup(encode_args);
|
||||
|
||||
if (!encode_ctx)
|
||||
{
|
||||
|
@ -143,7 +143,7 @@ encoder_setup(enum player_format format, struct media_quality *quality)
|
|||
}
|
||||
|
||||
out:
|
||||
transcode_decode_cleanup(&decode_ctx);
|
||||
transcode_decode_cleanup(&encode_args.src_ctx);
|
||||
return encode_ctx;
|
||||
}
|
||||
|
||||
|
@ -217,7 +217,7 @@ pipe_index_find_byreadfd(struct pipepair *p, int readfd)
|
|||
}
|
||||
|
||||
static struct streaming_wanted *
|
||||
wanted_new(enum player_format format, struct media_quality quality)
|
||||
wanted_new(enum media_format format, struct media_quality quality)
|
||||
{
|
||||
struct streaming_wanted *w;
|
||||
|
||||
|
@ -277,7 +277,7 @@ wanted_remove(struct streaming_wanted **wanted, struct streaming_wanted *remove)
|
|||
}
|
||||
|
||||
static struct streaming_wanted *
|
||||
wanted_add(struct streaming_wanted **wanted, enum player_format format, struct media_quality quality)
|
||||
wanted_add(struct streaming_wanted **wanted, enum media_format format, struct media_quality quality)
|
||||
{
|
||||
struct streaming_wanted *w;
|
||||
|
||||
|
@ -289,7 +289,7 @@ wanted_add(struct streaming_wanted **wanted, enum player_format format, struct m
|
|||
}
|
||||
|
||||
static struct streaming_wanted *
|
||||
wanted_find_byformat(struct streaming_wanted *wanted, enum player_format format, struct media_quality quality)
|
||||
wanted_find_byformat(struct streaming_wanted *wanted, enum media_format format, struct media_quality quality)
|
||||
{
|
||||
struct streaming_wanted *w;
|
||||
|
||||
|
@ -623,9 +623,9 @@ streaming_start(struct output_device *device, int callback_id)
|
|||
int ret;
|
||||
|
||||
pthread_mutex_lock(&streaming_wanted_lck);
|
||||
w = wanted_find_byformat(streaming.wanted, device->format, device->quality);
|
||||
w = wanted_find_byformat(streaming.wanted, device->selected_format, device->quality);
|
||||
if (!w)
|
||||
w = wanted_add(&streaming.wanted, device->format, device->quality);
|
||||
w = wanted_add(&streaming.wanted, device->selected_format, device->quality);
|
||||
ret = wanted_session_add(&device->audio_fd, &device->metadata_fd, w);
|
||||
if (ret < 0)
|
||||
goto error;
|
||||
|
|
101
src/player.c
101
src/player.c
|
@ -152,7 +152,7 @@ struct speaker_attr_param
|
|||
bool busy;
|
||||
|
||||
struct media_quality quality;
|
||||
enum player_format format;
|
||||
enum media_format format;
|
||||
|
||||
int audio_fd;
|
||||
int metadata_fd;
|
||||
|
@ -164,6 +164,7 @@ struct speaker_get_param
|
|||
{
|
||||
uint64_t spk_id;
|
||||
uint32_t active_remote;
|
||||
const char *address;
|
||||
struct player_speaker_info *spk_info;
|
||||
};
|
||||
|
||||
|
@ -2527,6 +2528,15 @@ device_to_speaker_info(struct player_speaker_info *spk, struct output_device *de
|
|||
spk->relvol = device->relvol;
|
||||
spk->absvol = device->volume;
|
||||
|
||||
spk->supported_formats = device->supported_formats;
|
||||
// Devices supporting more than one format should at least have default_format set
|
||||
if (device->selected_format != MEDIA_FORMAT_UNKNOWN)
|
||||
spk->format = device->selected_format;
|
||||
else if (device->default_format != MEDIA_FORMAT_UNKNOWN)
|
||||
spk->format = device->default_format;
|
||||
else
|
||||
spk->format = device->supported_formats;
|
||||
|
||||
spk->selected = OUTPUTS_DEVICE_DISPLAY_SELECTED(device);
|
||||
|
||||
spk->has_password = device->has_password;
|
||||
|
@ -2597,6 +2607,31 @@ speaker_get_byactiveremote(void *arg, int *retval)
|
|||
return COMMAND_END;
|
||||
}
|
||||
|
||||
static enum command_state
|
||||
speaker_get_byaddress(void *arg, int *retval)
|
||||
{
|
||||
struct speaker_get_param *spk_param = arg;
|
||||
struct output_device *device;
|
||||
bool match_v4;
|
||||
bool match_v6;
|
||||
|
||||
for (device = outputs_list(); device; device = device->next)
|
||||
{
|
||||
match_v4 = device->v4_address && (strcmp(spk_param->address, device->v4_address) == 0);
|
||||
match_v6 = device->v6_address && (strcmp(spk_param->address, device->v6_address) == 0);
|
||||
if (match_v4 || match_v6)
|
||||
{
|
||||
device_to_speaker_info(spk_param->spk_info, device);
|
||||
*retval = 0;
|
||||
return COMMAND_END;
|
||||
}
|
||||
}
|
||||
|
||||
// No output device found with matching id
|
||||
*retval = -1;
|
||||
return COMMAND_END;
|
||||
}
|
||||
|
||||
static enum command_state
|
||||
speaker_set(void *arg, int *retval)
|
||||
{
|
||||
|
@ -2730,6 +2765,8 @@ speaker_prevent_playback_set(void *arg, int *retval)
|
|||
struct speaker_attr_param *param = arg;
|
||||
struct output_device *device;
|
||||
|
||||
*retval = -1;
|
||||
|
||||
device = outputs_device_get(param->spk_id);
|
||||
if (!device)
|
||||
return COMMAND_END;
|
||||
|
@ -2778,6 +2815,8 @@ speaker_busy_set(void *arg, int *retval)
|
|||
struct speaker_attr_param *param = arg;
|
||||
struct output_device *device;
|
||||
|
||||
*retval = -1;
|
||||
|
||||
device = outputs_device_get(param->spk_id);
|
||||
if (!device)
|
||||
return COMMAND_END;
|
||||
|
@ -2803,6 +2842,33 @@ speaker_busy_set(void *arg, int *retval)
|
|||
return COMMAND_END;
|
||||
}
|
||||
|
||||
static enum command_state
|
||||
speaker_format_set(void *arg, int *retval)
|
||||
{
|
||||
struct speaker_attr_param *param = arg;
|
||||
struct output_device *device;
|
||||
|
||||
if (param->format == MEDIA_FORMAT_UNKNOWN)
|
||||
goto error;
|
||||
|
||||
device = outputs_device_get(param->spk_id);
|
||||
if (!device)
|
||||
goto error;
|
||||
|
||||
if (!(param->format & device->supported_formats))
|
||||
goto error;
|
||||
|
||||
device->selected_format = param->format;
|
||||
|
||||
*retval = 0;
|
||||
return COMMAND_END;
|
||||
|
||||
error:
|
||||
DPRINTF(E_LOG, L_PLAYER, "Error setting format '%s', device unknown or format unsupported\n", media_format_to_string(param->format));
|
||||
*retval = -1;
|
||||
return COMMAND_END;
|
||||
}
|
||||
|
||||
// Attempts to reactivate a speaker that has failed. That includes restarting
|
||||
// playback if it was stopped.
|
||||
static enum command_state
|
||||
|
@ -2907,7 +2973,7 @@ streaming_register(void *arg, int *retval)
|
|||
.type_name = "streaming",
|
||||
.name = "streaming",
|
||||
.quality = param->quality,
|
||||
.format = param->format,
|
||||
.selected_format = param->format,
|
||||
};
|
||||
|
||||
*retval = outputs_device_start(&device, NULL, false);
|
||||
|
@ -3404,6 +3470,19 @@ player_speaker_get_byactiveremote(struct player_speaker_info *spk, uint32_t acti
|
|||
return ret;
|
||||
}
|
||||
|
||||
int
|
||||
player_speaker_get_byaddress(struct player_speaker_info *spk, const char *address)
|
||||
{
|
||||
struct speaker_get_param param;
|
||||
int ret;
|
||||
|
||||
param.address = address;
|
||||
param.spk_info = spk;
|
||||
|
||||
ret = commands_exec_sync(cmdbase, speaker_get_byaddress, NULL, ¶m);
|
||||
return ret;
|
||||
}
|
||||
|
||||
int
|
||||
player_speaker_enable(uint64_t id)
|
||||
{
|
||||
|
@ -3466,18 +3545,26 @@ int
|
|||
player_speaker_authorize(uint64_t id, const char *pin)
|
||||
{
|
||||
struct speaker_attr_param param;
|
||||
int ret;
|
||||
|
||||
param.spk_id = id;
|
||||
param.pin = pin;
|
||||
|
||||
ret = commands_exec_sync(cmdbase, speaker_authorize, speaker_generic_bh, ¶m);
|
||||
|
||||
return ret;
|
||||
return commands_exec_sync(cmdbase, speaker_authorize, speaker_generic_bh, ¶m);
|
||||
}
|
||||
|
||||
int
|
||||
player_streaming_register(int *audio_fd, int *metadata_fd, enum player_format format, struct media_quality quality)
|
||||
player_speaker_format_set(uint64_t id, enum media_format format)
|
||||
{
|
||||
struct speaker_attr_param param;
|
||||
|
||||
param.spk_id = id;
|
||||
param.format = format;
|
||||
|
||||
return commands_exec_sync(cmdbase, speaker_format_set, speaker_generic_bh, ¶m);
|
||||
}
|
||||
|
||||
int
|
||||
player_streaming_register(int *audio_fd, int *metadata_fd, enum media_format format, struct media_quality quality)
|
||||
{
|
||||
struct speaker_attr_param param;
|
||||
int ret;
|
||||
|
|
15
src/player.h
15
src/player.h
|
@ -28,10 +28,6 @@ enum player_seek_mode {
|
|||
PLAYER_SEEK_RELATIVE = 2,
|
||||
};
|
||||
|
||||
enum player_format {
|
||||
PLAYER_FORMAT_MP3,
|
||||
};
|
||||
|
||||
struct player_speaker_info {
|
||||
uint64_t id;
|
||||
uint32_t active_remote;
|
||||
|
@ -40,6 +36,9 @@ struct player_speaker_info {
|
|||
int relvol;
|
||||
int absvol;
|
||||
|
||||
enum media_format format;
|
||||
uint32_t supported_formats;
|
||||
|
||||
bool selected;
|
||||
bool has_password;
|
||||
bool requires_auth;
|
||||
|
@ -104,6 +103,9 @@ player_speaker_get_byid(struct player_speaker_info *spk, uint64_t id);
|
|||
int
|
||||
player_speaker_get_byactiveremote(struct player_speaker_info *spk, uint32_t active_remote);
|
||||
|
||||
int
|
||||
player_speaker_get_byaddress(struct player_speaker_info *spk, const char *address);
|
||||
|
||||
int
|
||||
player_speaker_enable(uint64_t id);
|
||||
|
||||
|
@ -123,7 +125,10 @@ int
|
|||
player_speaker_authorize(uint64_t id, const char *pin);
|
||||
|
||||
int
|
||||
player_streaming_register(int *audio_fd, int *metadata_fd, enum player_format format, struct media_quality quality);
|
||||
player_speaker_format_set(uint64_t id, enum media_format format);
|
||||
|
||||
int
|
||||
player_streaming_register(int *audio_fd, int *metadata_fd, enum media_format format, struct media_quality quality);
|
||||
|
||||
int
|
||||
player_streaming_deregister(int id);
|
||||
|
|
710
src/transcode.c
710
src/transcode.c
File diff suppressed because it is too large
Load Diff
|
@ -3,7 +3,6 @@
|
|||
#define __TRANSCODE_H__
|
||||
|
||||
#include <event2/buffer.h>
|
||||
#include "db.h"
|
||||
#include "http.h"
|
||||
#include "misc.h"
|
||||
|
||||
|
@ -23,10 +22,14 @@ enum transcode_profile
|
|||
XCODE_PCM32,
|
||||
// Transcodes the best audio stream to MP3
|
||||
XCODE_MP3,
|
||||
// Transcodes the best audio stream to OPUS
|
||||
// Transcodes the best audio stream to raw OPUS (no container)
|
||||
XCODE_OPUS,
|
||||
// Transcodes the best audio stream to ALAC
|
||||
// Transcodes the best audio stream to raw ALAC (no container)
|
||||
XCODE_ALAC,
|
||||
// Transcodes the best audio stream to ALAC in a MP4 container
|
||||
XCODE_MP4_ALAC,
|
||||
// Produces just the header for a MP4 container with ALAC
|
||||
XCODE_MP4_ALAC_HEADER,
|
||||
// Transcodes the best audio stream from OGG
|
||||
XCODE_OGG,
|
||||
// Transcodes the best video stream to JPEG/PNG/VP8
|
||||
|
@ -62,6 +65,29 @@ struct transcode_evbuf_io
|
|||
void *seekfn_arg;
|
||||
};
|
||||
|
||||
struct transcode_decode_setup_args
|
||||
{
|
||||
enum transcode_profile profile;
|
||||
struct media_quality *quality;
|
||||
bool is_http;
|
||||
uint32_t len_ms;
|
||||
|
||||
// Source must be either of these
|
||||
const char *path;
|
||||
struct transcode_evbuf_io *evbuf_io;
|
||||
};
|
||||
|
||||
struct transcode_encode_setup_args
|
||||
{
|
||||
enum transcode_profile profile;
|
||||
struct media_quality *quality;
|
||||
struct decode_ctx *src_ctx;
|
||||
struct transcode_evbuf_io *evbuf_io;
|
||||
struct evbuffer *prepared_header;
|
||||
int width;
|
||||
int height;
|
||||
};
|
||||
|
||||
struct transcode_metadata_string
|
||||
{
|
||||
char *type;
|
||||
|
@ -74,19 +100,19 @@ struct transcode_metadata_string
|
|||
|
||||
// Setting up
|
||||
struct decode_ctx *
|
||||
transcode_decode_setup(enum transcode_profile profile, struct media_quality *quality, enum data_kind data_kind, const char *path, struct transcode_evbuf_io *evbuf_io, uint32_t len_ms);
|
||||
transcode_decode_setup(struct transcode_decode_setup_args args);
|
||||
|
||||
struct encode_ctx *
|
||||
transcode_encode_setup(enum transcode_profile profile, struct media_quality *quality, struct decode_ctx *src_ctx, int width, int height);
|
||||
transcode_encode_setup(struct transcode_encode_setup_args args);
|
||||
|
||||
struct transcode_ctx *
|
||||
transcode_setup(enum transcode_profile profile, struct media_quality *quality, enum data_kind data_kind, const char *path, uint32_t len_ms);
|
||||
transcode_setup(struct transcode_decode_setup_args decode_args, struct transcode_encode_setup_args encode_args);
|
||||
|
||||
struct decode_ctx *
|
||||
transcode_decode_setup_raw(enum transcode_profile profile, struct media_quality *quality);
|
||||
|
||||
enum transcode_profile
|
||||
transcode_needed(const char *user_agent, const char *client_codecs, char *file_codectype);
|
||||
transcode_needed(const char *user_agent, const char *client_codecs, const char *file_codectype);
|
||||
|
||||
// Cleaning up
|
||||
void
|
||||
|
@ -182,10 +208,27 @@ transcode_encode_query(struct encode_ctx *ctx, const char *query);
|
|||
struct http_icy_metadata *
|
||||
transcode_metadata(struct transcode_ctx *ctx, int *changed);
|
||||
|
||||
// When transcoding, we are in essence serving a different source file than the
|
||||
// original to the client. So we can't serve some of the file metadata from the
|
||||
// filescanner. This function creates strings to be used for override.
|
||||
/* When transcoding, we are in essence serving a different source file than the
|
||||
* original to the client. So we can't serve some of the file metadata from the
|
||||
* filescanner. This function creates strings to be used for override.
|
||||
*
|
||||
* @out s Structure with (non-allocated) strings
|
||||
* @in profile Transcoding profile
|
||||
* @in q Transcoding quality
|
||||
* @in len_ms Length of source track
|
||||
*/
|
||||
void
|
||||
transcode_metadata_strings_set(struct transcode_metadata_string *s, enum transcode_profile profile, struct media_quality *q, uint32_t len_ms);
|
||||
|
||||
/* Creates a header for later transcoding of a source file. This header can be
|
||||
* given to transcode_encode_setup which in some cases will make it faster (MP4)
|
||||
*
|
||||
* @out header An evbuffer with the header
|
||||
* @in profile Transcoding profile
|
||||
* @in path Path to the source file
|
||||
* @return Negative if error, otherwise zero
|
||||
*/
|
||||
int
|
||||
transcode_prepare_header(struct evbuffer **header, enum transcode_profile profile, const char *path);
|
||||
|
||||
#endif /* !__TRANSCODE_H__ */
|
||||
|
|
Loading…
Reference in New Issue