mirror of
https://github.com/owntone/owntone-server.git
synced 2025-01-02 10:33:23 -05:00
661289990c
Moves speaker selection, volume handling and startup to outputs.c, plus adds the ability to "resurrect" a speaker that disconnects. The purpose of moving the code is to concentrate device handling in one place. Also changes how we deal with speaker selection. The player will now generally not alter a user selection, even if the device fails. The purpose of this is to maintain selection both if the device briefly fails, and if the user switches off the device (we stop playback) and later turns it on + starts new playback.
1259 lines
31 KiB
C
1259 lines
31 KiB
C
/*
|
|
* Copyright (C) 2016 Espen Jürgensen <espenjurgensen@gmail.com>
|
|
*
|
|
* This program is free software; you can redistribute it and/or modify
|
|
* it under the terms of the GNU General Public License as published by
|
|
* the Free Software Foundation; either version 2 of the License, or
|
|
* (at your option) any later version.
|
|
*
|
|
* This program is distributed in the hope that it will be useful,
|
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
* GNU General Public License for more details.
|
|
*
|
|
* You should have received a copy of the GNU General Public License
|
|
* along with this program; if not, write to the Free Software
|
|
* Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
|
|
*/
|
|
|
|
#ifdef HAVE_CONFIG_H
|
|
# include <config.h>
|
|
#endif
|
|
|
|
#include <stdio.h>
|
|
#include <stdlib.h>
|
|
#include <stdbool.h>
|
|
#include <unistd.h>
|
|
#include <string.h>
|
|
#include <errno.h>
|
|
#include <stdint.h>
|
|
#include <inttypes.h>
|
|
|
|
#include <event2/event.h>
|
|
|
|
#include "logger.h"
|
|
#include "misc.h"
|
|
#include "transcode.h"
|
|
#include "listener.h"
|
|
#include "db.h"
|
|
#include "player.h" //TODO remove me when player_pmap is removed again
|
|
#include "worker.h"
|
|
#include "outputs.h"
|
|
|
|
extern struct output_definition output_raop;
|
|
extern struct output_definition output_streaming;
|
|
extern struct output_definition output_dummy;
|
|
extern struct output_definition output_fifo;
|
|
#ifdef HAVE_ALSA
|
|
extern struct output_definition output_alsa;
|
|
#endif
|
|
#ifdef HAVE_LIBPULSE
|
|
extern struct output_definition output_pulse;
|
|
#endif
|
|
#ifdef CHROMECAST
|
|
extern struct output_definition output_cast;
|
|
#endif
|
|
|
|
/* From player.c */
|
|
extern struct event_base *evbase_player;
|
|
|
|
// Must be in sync with enum output_types
|
|
static struct output_definition *outputs[] = {
|
|
&output_raop,
|
|
&output_streaming,
|
|
&output_dummy,
|
|
&output_fifo,
|
|
#ifdef HAVE_ALSA
|
|
&output_alsa,
|
|
#endif
|
|
#ifdef HAVE_LIBPULSE
|
|
&output_pulse,
|
|
#endif
|
|
#ifdef CHROMECAST
|
|
&output_cast,
|
|
#endif
|
|
NULL
|
|
};
|
|
|
|
// Default volume (must be from 0 - 100)
|
|
#define OUTPUTS_DEFAULT_VOLUME 50
|
|
|
|
// When we stop, we keep the outputs open for a while, just in case we are
|
|
// actually just restarting. This timeout determines how long we wait before
|
|
// full stop.
|
|
// (value is in seconds)
|
|
#define OUTPUTS_STOP_TIMEOUT 10
|
|
|
|
#define OUTPUTS_MAX_CALLBACKS 64
|
|
|
|
struct outputs_callback_register
|
|
{
|
|
output_status_cb cb;
|
|
struct output_device *device;
|
|
|
|
// We have received the callback with the result from the backend
|
|
bool ready;
|
|
|
|
// We store a device_id to avoid the risk of dangling device pointer
|
|
uint64_t device_id;
|
|
enum output_device_state state;
|
|
};
|
|
|
|
struct output_quality_subscription
|
|
{
|
|
int count;
|
|
struct media_quality quality;
|
|
struct encode_ctx *encode_ctx;
|
|
};
|
|
|
|
static struct outputs_callback_register outputs_cb_register[OUTPUTS_MAX_CALLBACKS];
|
|
static struct event *outputs_deferredev;
|
|
static struct timeval outputs_stop_timeout = { OUTPUTS_STOP_TIMEOUT, 0 };
|
|
|
|
// Last element is a zero terminator
|
|
static struct output_quality_subscription output_quality_subscriptions[OUTPUTS_MAX_QUALITY_SUBSCRIPTIONS + 1];
|
|
static bool outputs_got_new_subscription;
|
|
|
|
|
|
/* ------------------------------- MISC HELPERS ----------------------------- */
|
|
|
|
static output_status_cb
|
|
callback_get(struct output_device *device)
|
|
{
|
|
int callback_id;
|
|
|
|
for (callback_id = 0; callback_id < ARRAY_SIZE(outputs_cb_register); callback_id++)
|
|
{
|
|
if (outputs_cb_register[callback_id].device == device)
|
|
return outputs_cb_register[callback_id].cb;
|
|
}
|
|
|
|
return NULL;
|
|
}
|
|
|
|
static void
|
|
callback_remove(struct output_device *device)
|
|
{
|
|
int callback_id;
|
|
|
|
if (!device)
|
|
return;
|
|
|
|
for (callback_id = 0; callback_id < ARRAY_SIZE(outputs_cb_register); callback_id++)
|
|
{
|
|
if (outputs_cb_register[callback_id].device == device)
|
|
{
|
|
DPRINTF(E_DBG, L_PLAYER, "Removing callback to %s, id %d\n", player_pmap(outputs_cb_register[callback_id].cb), callback_id);
|
|
memset(&outputs_cb_register[callback_id], 0, sizeof(struct outputs_callback_register));
|
|
}
|
|
}
|
|
}
|
|
|
|
static int
|
|
callback_add(struct output_device *device, output_status_cb cb)
|
|
{
|
|
int callback_id;
|
|
|
|
if (!cb)
|
|
return -1;
|
|
|
|
// We will replace any previously registered callbacks, since that's what the
|
|
// player expects
|
|
callback_remove(device);
|
|
|
|
// Find a free slot in the queue
|
|
for (callback_id = 0; callback_id < ARRAY_SIZE(outputs_cb_register); callback_id++)
|
|
{
|
|
if (outputs_cb_register[callback_id].cb == NULL)
|
|
break;
|
|
}
|
|
|
|
if (callback_id == ARRAY_SIZE(outputs_cb_register))
|
|
{
|
|
DPRINTF(E_LOG, L_PLAYER, "Output callback queue is full! (size is %d)\n", OUTPUTS_MAX_CALLBACKS);
|
|
return -1;
|
|
}
|
|
|
|
outputs_cb_register[callback_id].cb = cb;
|
|
outputs_cb_register[callback_id].device = device; // Don't dereference this later, it might become invalid!
|
|
|
|
DPRINTF(E_DBG, L_PLAYER, "Registered callback to %s with id %d (device %p, %s)\n", player_pmap(cb), callback_id, device, device->name);
|
|
|
|
int active = 0;
|
|
for (int i = 0; i < ARRAY_SIZE(outputs_cb_register); i++)
|
|
if (outputs_cb_register[i].cb)
|
|
active++;
|
|
|
|
DPRINTF(E_DBG, L_PLAYER, "Number of active callbacks: %d\n", active);
|
|
|
|
return callback_id;
|
|
};
|
|
|
|
static void
|
|
deferred_cb(int fd, short what, void *arg)
|
|
{
|
|
struct output_device *device;
|
|
output_status_cb cb;
|
|
enum output_device_state state;
|
|
int callback_id;
|
|
|
|
for (callback_id = 0; callback_id < ARRAY_SIZE(outputs_cb_register); callback_id++)
|
|
{
|
|
if (outputs_cb_register[callback_id].ready)
|
|
{
|
|
// Must copy before making callback, since you never know what the
|
|
// callback might result in (could call back in)
|
|
cb = outputs_cb_register[callback_id].cb;
|
|
state = outputs_cb_register[callback_id].state;
|
|
|
|
// Will be NULL if the device has disappeared
|
|
device = outputs_device_get(outputs_cb_register[callback_id].device_id);
|
|
|
|
memset(&outputs_cb_register[callback_id], 0, sizeof(struct outputs_callback_register));
|
|
|
|
// The device has left the building (stopped/failed), and the backend
|
|
// is not using it any more
|
|
if (device && !device->advertised && !device->session)
|
|
{
|
|
outputs_device_remove(device);
|
|
device = NULL;
|
|
}
|
|
else if (device)
|
|
device->state = state;
|
|
|
|
DPRINTF(E_DBG, L_PLAYER, "Making deferred callback to %s, id was %d\n", player_pmap(cb), callback_id);
|
|
|
|
cb(device, state);
|
|
}
|
|
}
|
|
|
|
for (int i = 0; i < ARRAY_SIZE(outputs_cb_register); i++)
|
|
{
|
|
if (outputs_cb_register[i].cb)
|
|
DPRINTF(E_DBG, L_PLAYER, "%d. Active callback: %s\n", i, player_pmap(outputs_cb_register[i].cb));
|
|
}
|
|
}
|
|
|
|
static void
|
|
stop_timer_cb(int fd, short what, void *arg)
|
|
{
|
|
struct output_device *device = arg;
|
|
output_status_cb cb = callback_get(device);
|
|
|
|
outputs_device_stop(device, cb);
|
|
}
|
|
|
|
static void
|
|
device_stop_cb(struct output_device *device, enum output_device_state state)
|
|
{
|
|
if (device)
|
|
device->state = state;
|
|
|
|
if (state == OUTPUT_STATE_FAILED)
|
|
DPRINTF(E_WARN, L_PLAYER, "Failed to stop device\n");
|
|
else
|
|
DPRINTF(E_INFO, L_PLAYER, "Device stopped properly\n");
|
|
}
|
|
|
|
static enum transcode_profile
|
|
quality_to_xcode(struct media_quality *quality)
|
|
{
|
|
if (quality->bits_per_sample == 16)
|
|
return XCODE_PCM16;
|
|
if (quality->bits_per_sample == 24)
|
|
return XCODE_PCM24;
|
|
if (quality->bits_per_sample == 32)
|
|
return XCODE_PCM32;
|
|
|
|
return XCODE_UNKNOWN;
|
|
}
|
|
|
|
static int
|
|
encoding_reset(struct media_quality *quality)
|
|
{
|
|
struct output_quality_subscription *subscription;
|
|
struct decode_ctx *decode_ctx;
|
|
enum transcode_profile profile;
|
|
int i;
|
|
|
|
profile = quality_to_xcode(quality);
|
|
if (profile == XCODE_UNKNOWN)
|
|
{
|
|
DPRINTF(E_LOG, L_PLAYER, "Could not create subscription decoding context, invalid quality (%d/%d/%d)\n",
|
|
quality->sample_rate, quality->bits_per_sample, quality->channels);
|
|
return -1;
|
|
}
|
|
|
|
decode_ctx = transcode_decode_setup_raw(profile, quality);
|
|
if (!decode_ctx)
|
|
{
|
|
DPRINTF(E_LOG, L_PLAYER, "Could not create subscription decoding context (profile %d)\n", profile);
|
|
return -1;
|
|
}
|
|
|
|
for (i = 0; output_quality_subscriptions[i].count > 0; i++)
|
|
{
|
|
subscription = &output_quality_subscriptions[i]; // Just for short-hand
|
|
|
|
transcode_encode_cleanup(&subscription->encode_ctx); // Will also point the ctx to NULL
|
|
|
|
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, NULL, 0, 0);
|
|
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);
|
|
|
|
return 0;
|
|
}
|
|
|
|
static void
|
|
buffer_fill(struct output_buffer *obuf, void *buf, size_t bufsize, struct media_quality *quality, int nsamples, struct timespec *pts)
|
|
{
|
|
transcode_frame *frame;
|
|
int ret;
|
|
int i;
|
|
int n;
|
|
|
|
obuf->pts = *pts;
|
|
|
|
// The resampling/encoding (transcode) contexts work for a given input quality,
|
|
// so if the quality changes we need to reset the contexts. We also do that if
|
|
// we have received a subscription for a new quality.
|
|
if (!quality_is_equal(quality, &obuf->data[0].quality) || outputs_got_new_subscription)
|
|
{
|
|
encoding_reset(quality);
|
|
outputs_got_new_subscription = false;
|
|
}
|
|
|
|
// The first element of the output_buffer is always just the raw input data
|
|
// TODO can we avoid the copy below? we can't use evbuffer_add_buffer_reference,
|
|
// because then the outputs can't use it and we would need to copy there instead
|
|
evbuffer_add(obuf->data[0].evbuf, buf, bufsize);
|
|
obuf->data[0].buffer = buf;
|
|
obuf->data[0].bufsize = bufsize;
|
|
obuf->data[0].quality = *quality;
|
|
obuf->data[0].samples = nsamples;
|
|
|
|
for (i = 0, n = 1; output_quality_subscriptions[i].count > 0; i++)
|
|
{
|
|
if (quality_is_equal(&output_quality_subscriptions[i].quality, quality))
|
|
continue; // Skip, no resampling required and we have the data in element 0
|
|
|
|
if (!output_quality_subscriptions[i].encode_ctx)
|
|
continue;
|
|
|
|
frame = transcode_frame_new(buf, bufsize, nsamples, quality);
|
|
if (!frame)
|
|
continue;
|
|
|
|
ret = transcode_encode(obuf->data[n].evbuf, output_quality_subscriptions[i].encode_ctx, frame, 0);
|
|
transcode_frame_free(frame);
|
|
if (ret < 0)
|
|
continue;
|
|
|
|
obuf->data[n].buffer = evbuffer_pullup(obuf->data[n].evbuf, -1);
|
|
obuf->data[n].bufsize = evbuffer_get_length(obuf->data[n].evbuf);
|
|
obuf->data[n].quality = output_quality_subscriptions[i].quality;
|
|
obuf->data[n].samples = BTOS(obuf->data[n].bufsize, obuf->data[n].quality.bits_per_sample, obuf->data[n].quality.channels);
|
|
n++;
|
|
}
|
|
}
|
|
|
|
static void
|
|
buffer_drain(struct output_buffer *obuf)
|
|
{
|
|
int i;
|
|
|
|
for (i = 0; obuf->data[i].buffer; i++)
|
|
{
|
|
evbuffer_drain(obuf->data[i].evbuf, obuf->data[i].bufsize);
|
|
obuf->data[i].buffer = NULL;
|
|
obuf->data[i].bufsize = 0;
|
|
// We don't reset quality and samples, would be a waste of time
|
|
}
|
|
}
|
|
|
|
static void
|
|
device_list_sort(void)
|
|
{
|
|
struct output_device *device;
|
|
struct output_device *next;
|
|
struct output_device *prev;
|
|
int swaps;
|
|
|
|
// Swap sorting since even the most inefficient sorting should do fine here
|
|
do
|
|
{
|
|
swaps = 0;
|
|
prev = NULL;
|
|
for (device = output_device_list; device && device->next; device = device->next)
|
|
{
|
|
next = device->next;
|
|
if ( (outputs_priority(device) > outputs_priority(next)) ||
|
|
(outputs_priority(device) == outputs_priority(next) && strcasecmp(device->name, next->name) > 0) )
|
|
{
|
|
if (device == output_device_list)
|
|
output_device_list = next;
|
|
if (prev)
|
|
prev->next = next;
|
|
|
|
device->next = next->next;
|
|
next->next = device;
|
|
swaps++;
|
|
}
|
|
prev = device;
|
|
}
|
|
}
|
|
while (swaps > 0);
|
|
}
|
|
|
|
// Convenience function
|
|
static inline int
|
|
device_state_update(struct output_device *device, int ret)
|
|
{
|
|
if (ret < 0)
|
|
device->state = OUTPUT_STATE_FAILED;
|
|
|
|
return ret;
|
|
}
|
|
|
|
static void
|
|
metadata_cb_send(int fd, short what, void *arg)
|
|
{
|
|
struct output_metadata *metadata = arg;
|
|
int ret;
|
|
|
|
event_free(metadata->ev);
|
|
metadata->ev = NULL;
|
|
|
|
ret = metadata->finalize_cb(metadata);
|
|
if (ret < 0)
|
|
return;
|
|
|
|
outputs[metadata->type]->metadata_send(metadata);
|
|
}
|
|
|
|
// *** Worker thread ***
|
|
static void
|
|
metadata_cb_prepare(void *arg)
|
|
{
|
|
struct output_metadata *metadata = *((struct output_metadata **)arg);
|
|
|
|
metadata->priv = outputs[metadata->type]->metadata_prepare(metadata);
|
|
if (!metadata->priv)
|
|
{
|
|
event_free(metadata->ev);
|
|
free(metadata);
|
|
return;
|
|
}
|
|
|
|
// Metadata is prepared, let the player thread do the actual sending
|
|
event_active(metadata->ev, 0, 0);
|
|
}
|
|
|
|
static void
|
|
metadata_send(enum output_types type, uint32_t item_id, bool startup, output_metadata_finalize_cb cb)
|
|
{
|
|
struct output_metadata *metadata;
|
|
|
|
CHECK_NULL(L_PLAYER, metadata = calloc(1, sizeof(struct output_metadata)));
|
|
|
|
metadata->type = type;
|
|
metadata->item_id = item_id;
|
|
metadata->startup = startup;
|
|
metadata->finalize_cb = cb;
|
|
|
|
metadata->ev = event_new(evbase_player, -1, 0, metadata_cb_send, metadata);
|
|
|
|
if (outputs[type]->metadata_prepare)
|
|
worker_execute(metadata_cb_prepare, &metadata, sizeof(struct output_metadata *), 0);
|
|
else
|
|
outputs[type]->metadata_send(metadata);
|
|
}
|
|
|
|
|
|
/* ----------------------------- Volume helpers ----------------------------- */
|
|
|
|
static int
|
|
rel_to_vol(int relvol, int master_volume)
|
|
{
|
|
float vol;
|
|
|
|
if (relvol == 100)
|
|
return master_volume;
|
|
|
|
vol = ((float)relvol * (float)master_volume) / 100.0;
|
|
|
|
return (int)vol;
|
|
}
|
|
|
|
static int
|
|
vol_to_rel(int volume, int master_volume)
|
|
{
|
|
float rel;
|
|
|
|
if (volume == master_volume)
|
|
return 100;
|
|
|
|
rel = ((float)volume / (float)master_volume) * 100.0;
|
|
|
|
return (int)rel;
|
|
}
|
|
|
|
static void
|
|
vol_adjust(void)
|
|
{
|
|
struct output_device *device;
|
|
int selected_highest = -1;
|
|
int all_highest = -1;
|
|
|
|
for (device = output_device_list; device; device = device->next)
|
|
{
|
|
if (device->selected && (device->volume > selected_highest))
|
|
selected_highest = device->volume;
|
|
|
|
if (device->volume > all_highest)
|
|
all_highest = device->volume;
|
|
}
|
|
|
|
outputs_master_volume = (selected_highest >= 0) ? selected_highest : all_highest;
|
|
|
|
for (device = output_device_list; device; device = device->next)
|
|
{
|
|
if (!device->selected && (device->volume > outputs_master_volume))
|
|
device->volume = outputs_master_volume;
|
|
|
|
device->relvol = vol_to_rel(device->volume, outputs_master_volume);
|
|
}
|
|
|
|
#ifdef DEBUG_VOLUME
|
|
DPRINTF(E_DBG, L_PLAYER, "*** Master: %d\n", outputs_master_volume);
|
|
|
|
for (device = output_device_list; device; device = device->next)
|
|
{
|
|
DPRINTF(E_DBG, L_PLAYER, "*** %s: abs %d rel %d selected %d\n", device->name, device->volume, device->relvol, device->selected);
|
|
}
|
|
#endif
|
|
}
|
|
|
|
/* ----------------------------------- API ---------------------------------- */
|
|
|
|
struct output_device *
|
|
outputs_device_get(uint64_t device_id)
|
|
{
|
|
struct output_device *device;
|
|
|
|
for (device = output_device_list; device; device = device->next)
|
|
{
|
|
if (device_id == device->id)
|
|
return device;
|
|
}
|
|
|
|
DPRINTF(E_WARN, L_PLAYER, "Output device with id %" PRIu64 " has disappeared from our list\n", device_id);
|
|
return NULL;
|
|
}
|
|
|
|
/* ----------------------- Called by backend modules ------------------------ */
|
|
|
|
// Sessions free their sessions themselves, but should not touch the device,
|
|
// since they can't know for sure that it is still valid in memory
|
|
int
|
|
outputs_device_session_add(uint64_t device_id, void *session)
|
|
{
|
|
struct output_device *device;
|
|
|
|
device = outputs_device_get(device_id);
|
|
if (!device)
|
|
return -1;
|
|
|
|
device->session = session;
|
|
return 0;
|
|
}
|
|
|
|
void
|
|
outputs_device_session_remove(uint64_t device_id)
|
|
{
|
|
struct output_device *device;
|
|
|
|
device = outputs_device_get(device_id);
|
|
if (device)
|
|
device->session = NULL;
|
|
|
|
return;
|
|
}
|
|
|
|
int
|
|
outputs_quality_subscribe(struct media_quality *quality)
|
|
{
|
|
int i;
|
|
|
|
// If someone else is already subscribing to this quality we just increase the
|
|
// reference count.
|
|
for (i = 0; output_quality_subscriptions[i].count > 0; i++)
|
|
{
|
|
if (!quality_is_equal(quality, &output_quality_subscriptions[i].quality))
|
|
continue;
|
|
|
|
output_quality_subscriptions[i].count++;
|
|
|
|
DPRINTF(E_DBG, L_PLAYER, "Subscription request for quality %d/%d/%d (now %d subscribers)\n",
|
|
quality->sample_rate, quality->bits_per_sample, quality->channels, output_quality_subscriptions[i].count);
|
|
|
|
return 0;
|
|
}
|
|
|
|
if (i >= (ARRAY_SIZE(output_quality_subscriptions) - 1))
|
|
{
|
|
DPRINTF(E_LOG, L_PLAYER, "Bug! The number of different quality levels requested by outputs is too high\n");
|
|
return -1;
|
|
}
|
|
|
|
output_quality_subscriptions[i].quality = *quality;
|
|
output_quality_subscriptions[i].count++;
|
|
|
|
DPRINTF(E_DBG, L_PLAYER, "Subscription request for quality %d/%d/%d (now %d subscribers)\n",
|
|
quality->sample_rate, quality->bits_per_sample, quality->channels, output_quality_subscriptions[i].count);
|
|
|
|
// Better way of signaling this?
|
|
outputs_got_new_subscription = true;
|
|
|
|
return 0;
|
|
}
|
|
|
|
void
|
|
outputs_quality_unsubscribe(struct media_quality *quality)
|
|
{
|
|
int i;
|
|
|
|
// Find subscription
|
|
for (i = 0; output_quality_subscriptions[i].count > 0; i++)
|
|
{
|
|
if (quality_is_equal(quality, &output_quality_subscriptions[i].quality))
|
|
break;
|
|
}
|
|
|
|
if (output_quality_subscriptions[i].count == 0)
|
|
{
|
|
DPRINTF(E_LOG, L_PLAYER, "Bug! Unsubscription request for a quality level that there is no subscription for\n");
|
|
return;
|
|
}
|
|
|
|
output_quality_subscriptions[i].count--;
|
|
|
|
DPRINTF(E_DBG, L_PLAYER, "Unsubscription request for quality %d/%d/%d (now %d subscribers)\n",
|
|
quality->sample_rate, quality->bits_per_sample, quality->channels, output_quality_subscriptions[i].count);
|
|
|
|
if (output_quality_subscriptions[i].count > 0)
|
|
return;
|
|
|
|
transcode_encode_cleanup(&output_quality_subscriptions[i].encode_ctx);
|
|
|
|
// Shift elements
|
|
for (; i < ARRAY_SIZE(output_quality_subscriptions) - 1; i++)
|
|
output_quality_subscriptions[i] = output_quality_subscriptions[i + 1];
|
|
}
|
|
|
|
// Output backends call back through the below wrapper to make sure that:
|
|
// 1. Callbacks are always deferred
|
|
// 2. The callback never has a dangling pointer to a device (a device that has been removed from our list)
|
|
void
|
|
outputs_cb(int callback_id, uint64_t device_id, enum output_device_state state)
|
|
{
|
|
if (callback_id < 0)
|
|
return;
|
|
|
|
if (!(callback_id < ARRAY_SIZE(outputs_cb_register)) || !outputs_cb_register[callback_id].cb)
|
|
{
|
|
DPRINTF(E_LOG, L_PLAYER, "Bug! Output backend called us with an illegal callback id (%d)\n", callback_id);
|
|
return;
|
|
}
|
|
|
|
DPRINTF(E_DBG, L_PLAYER, "Callback request received, id is %i\n", callback_id);
|
|
|
|
outputs_cb_register[callback_id].ready = true;
|
|
outputs_cb_register[callback_id].device_id = device_id;
|
|
outputs_cb_register[callback_id].state = state;
|
|
event_active(outputs_deferredev, 0, 0);
|
|
}
|
|
|
|
// Maybe not so great, seems it would be better if integrated into the callback
|
|
// mechanism so that the notifications where at least deferred
|
|
void
|
|
outputs_listener_notify(void)
|
|
{
|
|
listener_notify(LISTENER_SPEAKER);
|
|
}
|
|
|
|
|
|
/* ---------------------------- Called by player ---------------------------- */
|
|
|
|
struct output_device *
|
|
outputs_device_add(struct output_device *add, bool new_deselect)
|
|
{
|
|
struct output_device *device;
|
|
char *keep_name;
|
|
int ret;
|
|
|
|
for (device = output_device_list; device; device = device->next)
|
|
{
|
|
if (device->id == add->id)
|
|
break;
|
|
}
|
|
|
|
// New device
|
|
if (!device)
|
|
{
|
|
device = add;
|
|
|
|
device->stop_timer = evtimer_new(evbase_player, stop_timer_cb, device);
|
|
|
|
keep_name = strdup(device->name);
|
|
ret = db_speaker_get(device, device->id);
|
|
if (ret < 0)
|
|
{
|
|
device->selected = 0;
|
|
device->volume = (outputs_master_volume >= 0) ? outputs_master_volume : OUTPUTS_DEFAULT_VOLUME;;
|
|
}
|
|
|
|
free(device->name);
|
|
device->name = keep_name;
|
|
|
|
if (new_deselect)
|
|
device->selected = 0;
|
|
|
|
device->next = output_device_list;
|
|
output_device_list = device;
|
|
}
|
|
// Update to a device already in the list
|
|
else
|
|
{
|
|
if (add->v4_address)
|
|
{
|
|
free(device->v4_address);
|
|
|
|
device->v4_address = add->v4_address;
|
|
device->v4_port = add->v4_port;
|
|
|
|
// Address is ours now
|
|
add->v4_address = NULL;
|
|
}
|
|
|
|
if (add->v6_address)
|
|
{
|
|
free(device->v6_address);
|
|
|
|
device->v6_address = add->v6_address;
|
|
device->v6_port = add->v6_port;
|
|
|
|
// Address is ours now
|
|
add->v6_address = NULL;
|
|
}
|
|
|
|
free(device->name);
|
|
device->name = add->name;
|
|
add->name = NULL;
|
|
|
|
device->has_password = add->has_password;
|
|
device->password = add->password;
|
|
|
|
outputs_device_free(add);
|
|
}
|
|
|
|
device_list_sort();
|
|
|
|
vol_adjust();
|
|
|
|
device->advertised = 1;
|
|
|
|
listener_notify(LISTENER_SPEAKER | LISTENER_VOLUME);
|
|
|
|
return device;
|
|
}
|
|
|
|
void
|
|
outputs_device_remove(struct output_device *remove)
|
|
{
|
|
struct output_device *device;
|
|
struct output_device *prev;
|
|
int ret;
|
|
|
|
// Device stop should be able to handle that we invalidate the device, even
|
|
// if it is an async stop. It might call outputs_device_session_remove(), but
|
|
// that just won't do anything since the id will be unknown.
|
|
if (remove->session)
|
|
outputs_device_stop(remove, device_stop_cb);
|
|
|
|
prev = NULL;
|
|
for (device = output_device_list; device; device = device->next)
|
|
{
|
|
if (device == remove)
|
|
break;
|
|
|
|
prev = device;
|
|
}
|
|
|
|
if (!device)
|
|
return;
|
|
|
|
// Save device volume
|
|
ret = db_speaker_save(remove);
|
|
if (ret < 0)
|
|
DPRINTF(E_LOG, L_PLAYER, "Could not save state for %s device '%s'\n", remove->type_name, remove->name);
|
|
|
|
DPRINTF(E_INFO, L_PLAYER, "Removing %s device '%s'; stopped advertising\n", remove->type_name, remove->name);
|
|
|
|
if (!prev)
|
|
output_device_list = remove->next;
|
|
else
|
|
prev->next = remove->next;
|
|
|
|
outputs_device_free(remove);
|
|
|
|
vol_adjust();
|
|
|
|
listener_notify(LISTENER_SPEAKER | LISTENER_VOLUME);
|
|
}
|
|
|
|
void
|
|
outputs_device_select(struct output_device *device)
|
|
{
|
|
device->selected = 1;
|
|
device->prevent_playback = 0;
|
|
device->busy = 0;
|
|
|
|
vol_adjust();
|
|
}
|
|
|
|
void
|
|
outputs_device_deselect(struct output_device *device)
|
|
{
|
|
device->selected = 0;
|
|
|
|
vol_adjust();
|
|
}
|
|
|
|
int
|
|
outputs_device_start(struct output_device *device, output_status_cb cb, bool only_probe)
|
|
{
|
|
int ret;
|
|
|
|
if (outputs[device->type]->disabled || !outputs[device->type]->device_start || !outputs[device->type]->device_probe)
|
|
return -1;
|
|
|
|
if (device->session)
|
|
return 0; // Device is already running, nothing to do
|
|
|
|
if (only_probe)
|
|
ret = outputs[device->type]->device_probe(device, callback_add(device, cb));
|
|
else
|
|
ret = outputs[device->type]->device_start(device, callback_add(device, cb));
|
|
|
|
return device_state_update(device, ret);;
|
|
}
|
|
|
|
int
|
|
outputs_device_stop(struct output_device *device, output_status_cb cb)
|
|
{
|
|
int ret;
|
|
|
|
if (outputs[device->type]->disabled || !outputs[device->type]->device_stop)
|
|
return -1;
|
|
|
|
if (!device->session)
|
|
return 0; // Device is already stopped, nothing to do
|
|
|
|
ret = outputs[device->type]->device_stop(device, callback_add(device, cb));
|
|
|
|
return device_state_update(device, ret);
|
|
}
|
|
|
|
int
|
|
outputs_device_stop_delayed(struct output_device *device, output_status_cb cb)
|
|
{
|
|
if (outputs[device->type]->disabled || !outputs[device->type]->device_stop)
|
|
return -1;
|
|
|
|
if (!device->session)
|
|
return 0; // Device is already stopped, nothing to do
|
|
|
|
outputs[device->type]->device_cb_set(device, callback_add(device, cb));
|
|
|
|
event_add(device->stop_timer, &outputs_stop_timeout);
|
|
|
|
return 1;
|
|
}
|
|
|
|
int
|
|
outputs_device_flush(struct output_device *device, output_status_cb cb)
|
|
{
|
|
int ret;
|
|
|
|
if (outputs[device->type]->disabled || !outputs[device->type]->device_flush)
|
|
return -1;
|
|
|
|
if (!device->session)
|
|
return 0; // Nothing to flush
|
|
|
|
ret = outputs[device->type]->device_flush(device, callback_add(device, cb));
|
|
|
|
return device_state_update(device, ret);
|
|
}
|
|
|
|
void
|
|
outputs_device_volume_register(struct output_device *device, int absvol, int relvol)
|
|
{
|
|
if (absvol > -1)
|
|
device->volume = absvol;
|
|
else if (relvol > -1)
|
|
device->volume = rel_to_vol(relvol, outputs_master_volume);
|
|
|
|
vol_adjust();
|
|
|
|
listener_notify(LISTENER_VOLUME);
|
|
}
|
|
|
|
int
|
|
outputs_device_volume_set(struct output_device *device, output_status_cb cb)
|
|
{
|
|
int ret;
|
|
|
|
if (outputs[device->type]->disabled || !outputs[device->type]->device_volume_set)
|
|
return -1;
|
|
|
|
if (!device->session)
|
|
return 0; // Device isn't active
|
|
|
|
ret = outputs[device->type]->device_volume_set(device, callback_add(device, cb));
|
|
|
|
return device_state_update(device, ret);
|
|
}
|
|
|
|
int
|
|
outputs_device_volume_to_pct(struct output_device *device, const char *volume)
|
|
{
|
|
if (outputs[device->type]->disabled || !outputs[device->type]->device_volume_to_pct)
|
|
return -1;
|
|
|
|
return outputs[device->type]->device_volume_to_pct(device, volume);
|
|
}
|
|
|
|
int
|
|
outputs_device_quality_set(struct output_device *device, struct media_quality *quality, output_status_cb cb)
|
|
{
|
|
int ret;
|
|
|
|
if (outputs[device->type]->disabled || !outputs[device->type]->device_quality_set)
|
|
return -1;
|
|
|
|
ret = outputs[device->type]->device_quality_set(device, quality, callback_add(device, cb));
|
|
|
|
return device_state_update(device, ret);
|
|
}
|
|
|
|
void
|
|
outputs_device_cb_set(struct output_device *device, output_status_cb cb)
|
|
{
|
|
if (outputs[device->type]->disabled || !outputs[device->type]->device_cb_set)
|
|
return;
|
|
|
|
if (!device->session)
|
|
return;
|
|
|
|
outputs[device->type]->device_cb_set(device, callback_add(device, cb));
|
|
}
|
|
|
|
void
|
|
outputs_device_free(struct output_device *device)
|
|
{
|
|
if (!device)
|
|
return;
|
|
|
|
if (outputs[device->type]->disabled)
|
|
DPRINTF(E_LOG, L_PLAYER, "BUG! Freeing device from a disabled output?\n");
|
|
|
|
if (device->session)
|
|
DPRINTF(E_LOG, L_PLAYER, "BUG! Freeing device with active session?\n");
|
|
|
|
if (outputs[device->type]->device_free_extra)
|
|
outputs[device->type]->device_free_extra(device);
|
|
|
|
if (device->stop_timer)
|
|
event_free(device->stop_timer);
|
|
|
|
free(device->name);
|
|
free(device->auth_key);
|
|
free(device->v4_address);
|
|
free(device->v6_address);
|
|
|
|
free(device);
|
|
}
|
|
|
|
// The return value will be the number of devices we need to wait for, either
|
|
// because they are starting or shutting down. The return value is only negative
|
|
// if we don't have to wait, i.e. all the selected devices failed immediately.
|
|
int
|
|
outputs_start(output_status_cb started_cb, output_status_cb stopped_cb, bool only_probe)
|
|
{
|
|
struct output_device *device;
|
|
int pending = 0;
|
|
int ret = -1;
|
|
|
|
for (device = output_device_list; device; device = device->next)
|
|
{
|
|
if (device->selected)
|
|
ret = outputs_device_start(device, started_cb, only_probe);
|
|
else
|
|
ret = outputs_device_stop(device, stopped_cb);
|
|
|
|
if (ret < 0)
|
|
continue;
|
|
|
|
pending += ret;
|
|
}
|
|
|
|
return (pending > 0) ? pending : ret;
|
|
}
|
|
|
|
int
|
|
outputs_stop(output_status_cb cb)
|
|
{
|
|
struct output_device *device;
|
|
int pending = 0;
|
|
int ret;
|
|
|
|
for (device = output_device_list; device; device = device->next)
|
|
{
|
|
if (!device->session)
|
|
continue;
|
|
|
|
ret = outputs_device_stop(device, cb);
|
|
if (ret < 0)
|
|
continue;
|
|
|
|
pending += ret;
|
|
}
|
|
|
|
return pending;
|
|
}
|
|
|
|
int
|
|
outputs_stop_delayed_cancel(void)
|
|
{
|
|
struct output_device *device;
|
|
|
|
for (device = output_device_list; device; device = device->next)
|
|
event_del(device->stop_timer);
|
|
|
|
return 0;
|
|
}
|
|
|
|
int
|
|
outputs_flush(output_status_cb cb)
|
|
{
|
|
struct output_device *device;
|
|
int pending = 0;
|
|
int ret;
|
|
|
|
for (device = output_device_list; device; device = device->next)
|
|
{
|
|
ret = outputs_device_flush(device, cb);
|
|
if (ret < 0)
|
|
continue;
|
|
|
|
pending += ret;
|
|
}
|
|
|
|
return pending;
|
|
}
|
|
|
|
int
|
|
outputs_volume_set(int volume, output_status_cb cb)
|
|
{
|
|
struct output_device *device;
|
|
int pending = 0;
|
|
int ret;
|
|
|
|
if (outputs_master_volume == volume)
|
|
return 0;
|
|
|
|
outputs_master_volume = volume;
|
|
|
|
for (device = output_device_list; device; device = device->next)
|
|
{
|
|
if (!device->selected)
|
|
continue;
|
|
|
|
device->volume = rel_to_vol(device->relvol, outputs_master_volume);
|
|
|
|
ret = outputs_device_volume_set(device, cb);
|
|
if (ret < 0)
|
|
continue;
|
|
|
|
pending += ret;
|
|
}
|
|
|
|
listener_notify(LISTENER_VOLUME);
|
|
|
|
return pending;
|
|
}
|
|
|
|
int
|
|
outputs_sessions_count(void)
|
|
{
|
|
struct output_device *device;
|
|
int count = 0;
|
|
|
|
for (device = output_device_list; device; device = device->next)
|
|
if (device->session)
|
|
count++;
|
|
|
|
return count;
|
|
}
|
|
|
|
void
|
|
outputs_write(void *buf, size_t bufsize, int nsamples, struct media_quality *quality, struct timespec *pts)
|
|
{
|
|
int i;
|
|
|
|
buffer_fill(&output_buffer, buf, bufsize, quality, nsamples, pts);
|
|
|
|
for (i = 0; outputs[i]; i++)
|
|
{
|
|
if (outputs[i]->disabled)
|
|
continue;
|
|
|
|
if (outputs[i]->write)
|
|
outputs[i]->write(&output_buffer);
|
|
}
|
|
|
|
buffer_drain(&output_buffer);
|
|
}
|
|
|
|
void
|
|
outputs_metadata_send(uint32_t item_id, bool startup, output_metadata_finalize_cb cb)
|
|
{
|
|
int i;
|
|
|
|
for (i = 0; outputs[i]; i++)
|
|
{
|
|
if (outputs[i]->disabled || !outputs[i]->metadata_send)
|
|
continue;
|
|
|
|
metadata_send(i, item_id, startup, cb);
|
|
}
|
|
}
|
|
|
|
void
|
|
outputs_metadata_purge(void)
|
|
{
|
|
int i;
|
|
|
|
for (i = 0; outputs[i]; i++)
|
|
{
|
|
if (outputs[i]->disabled || !outputs[i]->metadata_purge)
|
|
continue;
|
|
|
|
outputs[i]->metadata_purge();
|
|
}
|
|
}
|
|
|
|
void
|
|
outputs_authorize(enum output_types type, const char *pin)
|
|
{
|
|
if (outputs[type]->disabled)
|
|
return;
|
|
|
|
if (outputs[type]->authorize)
|
|
outputs[type]->authorize(pin);
|
|
}
|
|
|
|
int
|
|
outputs_priority(struct output_device *device)
|
|
{
|
|
return outputs[device->type]->priority;
|
|
}
|
|
|
|
const char *
|
|
outputs_name(enum output_types type)
|
|
{
|
|
return outputs[type]->name;
|
|
}
|
|
|
|
int
|
|
outputs_init(void)
|
|
{
|
|
int no_output;
|
|
int ret;
|
|
int i;
|
|
|
|
outputs_master_volume = -1;
|
|
|
|
CHECK_NULL(L_PLAYER, outputs_deferredev = evtimer_new(evbase_player, deferred_cb, NULL));
|
|
|
|
no_output = 1;
|
|
for (i = 0; outputs[i]; i++)
|
|
{
|
|
if (outputs[i]->type != i)
|
|
{
|
|
DPRINTF(E_FATAL, L_PLAYER, "BUG! Output definitions are misaligned with output enum\n");
|
|
return -1;
|
|
}
|
|
|
|
if (!outputs[i]->init)
|
|
{
|
|
no_output = 0;
|
|
continue;
|
|
}
|
|
|
|
ret = outputs[i]->init();
|
|
if (ret < 0)
|
|
outputs[i]->disabled = 1;
|
|
else
|
|
no_output = 0;
|
|
}
|
|
|
|
if (no_output)
|
|
return -1;
|
|
|
|
for (i = 0; i < ARRAY_SIZE(output_buffer.data); i++)
|
|
output_buffer.data[i].evbuf = evbuffer_new();
|
|
|
|
return 0;
|
|
}
|
|
|
|
void
|
|
outputs_deinit(void)
|
|
{
|
|
int i;
|
|
|
|
event_free(outputs_deferredev);
|
|
|
|
for (i = 0; outputs[i]; i++)
|
|
{
|
|
if (outputs[i]->disabled)
|
|
continue;
|
|
|
|
if (outputs[i]->deinit)
|
|
outputs[i]->deinit();
|
|
}
|
|
|
|
// In case some outputs forgot to unsubscribe
|
|
for (i = 0; i < ARRAY_SIZE(output_quality_subscriptions); i++)
|
|
if (output_quality_subscriptions[i].count > 0)
|
|
{
|
|
transcode_encode_cleanup(&output_quality_subscriptions[i].encode_ctx);
|
|
memset(&output_quality_subscriptions[i], 0, sizeof(struct output_quality_subscription));
|
|
}
|
|
|
|
for (i = 0; i < ARRAY_SIZE(output_buffer.data); i++)
|
|
evbuffer_free(output_buffer.data[i].evbuf);
|
|
}
|
|
|