[misc/player] Introduce output ability to announce supported formats

Also introduce default output format and selected device format, should the
user want another format.

As part of this, change enum player_format in player.h to enum media_format in
misc.h so that it is akin to struct media_quality.

Modify json API to support this.
This commit is contained in:
ejurgensen 2024-01-07 23:12:03 +01:00
parent 9f719ca155
commit 62b42ce354
21 changed files with 209 additions and 104 deletions

View File

@ -342,6 +342,7 @@ GET /api/outputs
| 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**
@ -361,7 +362,8 @@ curl -X GET "http://localhost:3689/api/outputs"
"requires_auth": false,
"needs_auth_key": false,
"volume": 0,
"format": "alac"
"format": "alac",
"supported_formats": [ "alac" ]
},
{
"id": "0",
@ -372,7 +374,8 @@ curl -X GET "http://localhost:3689/api/outputs"
"requires_auth": false,
"needs_auth_key": false,
"volume": 19,
"format": "pcm"
"format": "pcm",
"supported_formats": [ "pcm" ]
},
{
"id": "100",
@ -383,7 +386,8 @@ curl -X GET "http://localhost:3689/api/outputs"
"requires_auth": false,
"needs_auth_key": false,
"volume": 0,
"format": "pcm"
"format": "pcm",
"supported_formats": [ "pcm" ]
}
]
}
@ -453,6 +457,7 @@ curl -X GET "http://localhost:3689/api/outputs/0"
"needs_auth_key": false,
"volume": 3
"format": "pcm",
"supported_formats": [ "pcm" ]
}
```

View File

@ -37,8 +37,9 @@
#include "conffile.h"
#include "logger.h"
#include "httpd.h"
#include "httpd.h" // TODO get rid of this, only used for httpd_gzip_deflate
#include "httpd_daap.h"
#include "transcode.h"
#include "db.h"
#include "cache.h"
#include "listener.h"
@ -189,6 +190,7 @@ static struct cache_db_def cache_artwork_db_def[] = {
static sqlite3 *cache_xcode_hdl;
static struct event *cache_xcode_updateev;
static struct event *cache_xcode_prepareev;
static bool cache_xcode_is_enabled;
static int cache_xcode_last_file;
static struct cache_db_def cache_xcode_db_def[] = {
DB_DEF_ADMIN,
@ -897,6 +899,20 @@ xcode_header_get(void *arg, int *retval)
#undef Q_TMPL
}
static enum command_state
xcode_toggle(void *arg, int *retval)
{
bool *enable = arg;
cache_xcode_is_enabled = *enable;
if (cache_xcode_is_enabled)
event_active(cache_xcode_updateev, 0, 0);
*retval = 0;
return COMMAND_END;
}
static int
xcode_add_entry(sqlite3 *hdl, uint32_t id, uint32_t ts, const char *path)
{
@ -982,8 +998,6 @@ xcode_sync_with_files(sqlite3 *hdl)
int i;
int ret;
DPRINTF(E_INFO, L_CACHE, "Beginning transcode sync\n");
// Both lists must be sorted by id, otherwise the compare below won't work
ret = sqlite3_prepare_v2(hdl, "SELECT id, time_modified FROM files ORDER BY id;", -1, &stmt, 0);
if (ret != SQLITE_OK)
@ -1044,8 +1058,6 @@ xcode_sync_with_files(sqlite3 *hdl)
}
db_query_end(&qp);
DPRINTF(E_INFO, L_CACHE, "Transcode sync completed\n");
free(cachelist);
return 0;
@ -1068,7 +1080,12 @@ xcode_prepare_header(sqlite3 *hdl, const char *format, int id, const char *path)
DPRINTF(E_DBG, L_CACHE, "Preparing %s header for '%s' (file id %d)\n", format, path, id);
#if 1
ret = httpd_prepare_header(&header, format, path); // Proceed even if error, we also cache that
if (strcmp(format, "mp4") == 0)
ret = transcode_prepare_header(&header, XCODE_MP4_ALAC, path);
else
ret = -1;
// Proceed even if error, we also cache that
if (ret == 0)
{
datalen = evbuffer_get_length(header);
@ -1202,7 +1219,7 @@ cache_database_update(void *arg, int *retval)
// TODO unlink or rename cache.db
// if (prefer_format && strcmp(prefer_format, "alac")) // TODO Ugly
if (cache_xcode_is_enabled)
event_add(cache_xcode_updateev, &delay_xcode);
*retval = 0;
@ -1731,6 +1748,15 @@ cache_xcode_header_get(struct evbuffer *evbuf, int *cached, uint32_t id, const c
return ret;
}
int
cache_xcode_toggle(bool enable)
{
if (!cache_is_initialized)
return -1;
return commands_exec_sync(cmdbase, xcode_toggle, NULL, &enable);
}
/* ---------------------------- Artwork cache API -------------------------- */

View File

@ -27,6 +27,10 @@ cache_daap_threshold_get(void);
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

View File

@ -4790,7 +4790,7 @@ db_speaker_save(struct output_device *device)
#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, device->format);
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
@ -4841,7 +4841,7 @@ 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->format = sqlite3_column_int(stmt, 4);
device->selected_format = sqlite3_column_int(stmt, 4);
#ifdef DB_PROFILE
while (db_blocking_step(stmt) == SQLITE_ROW)

View File

@ -49,6 +49,8 @@
#include "httpd_internal.h"
#include "transcode.h"
#include "cache.h"
#include "listener.h"
#include "player.h"
#ifdef LASTFM
# include "lastfm.h"
#endif
@ -905,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"));
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
@ -1220,15 +1255,6 @@ httpd_gzip_deflate(struct evbuffer *in)
return NULL;
}
int
httpd_prepare_header(struct evbuffer **header, const char *format, const char *path)
{
if (strcmp(format, "mp4") == 0)
return transcode_prepare_header(header, XCODE_MP4_ALAC, path);
else
return -1;
}
// 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
@ -1543,6 +1569,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:
@ -1554,6 +1584,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();

View File

@ -13,17 +13,6 @@
struct evbuffer *
httpd_gzip_deflate(struct evbuffer *in);
/*
* Passthrough to transcode, which will create a transcoded file header for path
*
* @out header Newly created evbuffer with the header
* @in format Which format caller wants a header for
* @in path Path to the file
* @return 0 if ok, otherwise -1
*/
int
httpd_prepare_header(struct evbuffer **header, const char *format, const char *path);
int
httpd_init(const char *webroot);

View File

@ -1522,48 +1522,23 @@ struct outputs_param
int output_volume;
};
static enum player_format
plformat_from_string(const char *format)
{
if (strcmp(format, "pcm") == 0)
return PLAYER_FORMAT_PCM;
if (strcmp(format, "wav") == 0)
return PLAYER_FORMAT_WAV;
if (strcmp(format, "mp3") == 0)
return PLAYER_FORMAT_MP3;
if (strcmp(format, "alac") == 0)
return PLAYER_FORMAT_ALAC;
if (strcmp(format, "opus") == 0)
return PLAYER_FORMAT_OPUS;
return PLAYER_FORMAT_UNKNOWN;
}
static const char *
plformat_to_string(enum player_format format)
{
if (format == PLAYER_FORMAT_PCM)
return "pcm";
if (format == PLAYER_FORMAT_WAV)
return "wav";
if (format == PLAYER_FORMAT_MP3)
return "mp3";
if (format == PLAYER_FORMAT_ALAC)
return "alac";
if (format == PLAYER_FORMAT_OPUS)
return "opus";
return "unknown";
}
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));
@ -1573,7 +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(plformat_to_string(spk->format)));
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;
}
@ -1684,13 +1660,13 @@ jsonapi_reply_outputs_put_byid(struct httpd_request *hreq)
{
format = jparse_str_from_obj(request, "format");
if (format)
ret = player_speaker_format_set(output_id, plformat_from_string(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;
}

View File

@ -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

View File

@ -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 ------------------------ */

View File

@ -272,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;
@ -283,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 ------------------------ */

View File

@ -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;

View File

@ -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)
{

View File

@ -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

View File

@ -1778,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)
{

View File

@ -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);

View File

@ -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);
}

View File

@ -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)
{

View File

@ -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)
{

View File

@ -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,7 +113,7 @@ 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 transcode_encode_setup_args encode_args = { .profile = XCODE_MP3, .quality = quality };
struct encode_ctx *encode_ctx = NULL;
@ -132,7 +132,7 @@ encoder_setup(enum player_format format, struct media_quality *quality)
goto out;
}
if (format == PLAYER_FORMAT_MP3)
if (format == MEDIA_FORMAT_MP3)
encode_ctx = transcode_encode_setup(encode_args);
if (!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;

View File

@ -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;
@ -2526,7 +2526,15 @@ device_to_speaker_info(struct player_speaker_info *spk, struct output_device *de
spk->output_type[sizeof(spk->output_type) - 1] = '\0';
spk->relvol = device->relvol;
spk->absvol = device->volume;
spk->format = device->format;
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);
@ -2814,19 +2822,25 @@ speaker_format_set(void *arg, int *retval)
struct speaker_attr_param *param = arg;
struct output_device *device;
*retval = -1;
if (param->format == PLAYER_FORMAT_UNKNOWN)
return COMMAND_END;
if (param->format == MEDIA_FORMAT_UNKNOWN)
goto error;
device = outputs_device_get(param->spk_id);
if (!device)
return COMMAND_END;
goto error;
device->format = param->format;
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
@ -2933,7 +2947,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);
@ -3500,18 +3514,18 @@ player_speaker_authorize(uint64_t id, const char *pin)
}
int
player_speaker_format_set(uint64_t id, enum player_format format)
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, NULL, &param);
return commands_exec_sync(cmdbase, speaker_format_set, speaker_generic_bh, &param);
}
int
player_streaming_register(int *audio_fd, int *metadata_fd, enum player_format format, struct media_quality quality)
player_streaming_register(int *audio_fd, int *metadata_fd, enum media_format format, struct media_quality quality)
{
struct speaker_attr_param param;
int ret;

View File

@ -28,15 +28,6 @@ enum player_seek_mode {
PLAYER_SEEK_RELATIVE = 2,
};
enum player_format {
PLAYER_FORMAT_UNKNOWN = -1,
PLAYER_FORMAT_PCM = 0,
PLAYER_FORMAT_WAV = 1,
PLAYER_FORMAT_MP3 = 2,
PLAYER_FORMAT_ALAC = 3,
PLAYER_FORMAT_OPUS = 4,
};
struct player_speaker_info {
uint64_t id;
uint32_t active_remote;
@ -45,7 +36,8 @@ struct player_speaker_info {
int relvol;
int absvol;
enum player_format format;
enum media_format format;
uint32_t supported_formats;
bool selected;
bool has_password;
@ -130,10 +122,10 @@ int
player_speaker_authorize(uint64_t id, const char *pin);
int
player_speaker_format_set(uint64_t id, enum player_format format);
player_speaker_format_set(uint64_t id, enum media_format format);
int
player_streaming_register(int *audio_fd, int *metadata_fd, enum player_format format, struct media_quality quality);
player_streaming_register(int *audio_fd, int *metadata_fd, enum media_format format, struct media_quality quality);
int
player_streaming_deregister(int id);