owntone-server/src/outputs.c

1367 lines
34 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 "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_airplay;
extern struct output_definition output_streaming;
extern struct output_definition output_dummy;
extern struct output_definition output_fifo;
extern struct output_definition output_rcp;
#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_airplay,
&output_streaming,
&output_dummy,
&output_fifo,
&output_rcp,
#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;
};
// Buffer used to pass data to the backends
static struct output_buffer output_buffer;
static struct output_device *outputs_device_list;
static int outputs_master_volume;
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, 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 struct output_buffer *
buffer_copy(struct output_buffer *obuf)
{
struct output_buffer *copy;
int i;
if (!obuf)
return NULL;
CHECK_NULL(L_PLAYER, copy = malloc(sizeof(struct output_buffer)));
memcpy(copy, obuf, sizeof(struct output_buffer));
for (i = 0; obuf->data[i].buffer; i++)
{
CHECK_NULL(L_PLAYER, copy->data[i].evbuf = evbuffer_new());
evbuffer_add(copy->data[i].evbuf, obuf->data[i].buffer, obuf->data[i].bufsize);
copy->data[i].buffer = evbuffer_pullup(copy->data[i].evbuf, -1);
}
return copy;
}
static void
buffer_free(struct output_buffer *obuf)
{
int i;
if (!obuf)
return;
for (i = 0; obuf->data[i].buffer; i++)
evbuffer_free(obuf->data[i].evbuf);
free(obuf);
}
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 = outputs_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 == outputs_device_list)
outputs_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_free(struct output_metadata *metadata)
{
if (!metadata)
return;
if (metadata->ev)
event_free(metadata->ev);
free(metadata);
}
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 = outputs_device_list; device; device = device->next)
{
if (OUTPUTS_DEVICE_DISPLAY_SELECTED(device) && (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 = outputs_device_list; device; device = device->next)
{
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 = outputs_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, OUTPUTS_DEVICE_DISPLAY_SELECTED(device));
}
#endif
}
/* ----------------------------------- API ---------------------------------- */
struct output_device *
outputs_device_get(uint64_t device_id)
{
struct output_device *device;
for (device = outputs_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);
}
void
outputs_metadata_free(struct output_metadata *metadata)
{
metadata_free(metadata);
}
struct output_buffer *
outputs_buffer_copy(struct output_buffer *buffer)
{
return buffer_copy(buffer);
}
void
outputs_buffer_free(struct output_buffer *buffer)
{
buffer_free(buffer);
}
/* ---------------------------- 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 = outputs_device_list; device; device = device->next)
{
if (device->id == add->id)
break;
}
// This is relevant for Airplay 1 and 2 where the same device can support both
if (device && device->type != add->type)
{
if (outputs_priority(device) < outputs_priority(add))
{
DPRINTF(E_DBG, L_PLAYER, "Ignoring type %s for device '%s', will use type %s\n", add->type_name, add->name, device->type_name);
outputs_device_free(add);
return NULL;
}
// Remove existing device, higher priority device will be added below
outputs_device_remove(device);
device = NULL;
}
// 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 = outputs_device_list;
outputs_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;
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 = outputs_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'\n", remove->type_name, remove->name);
if (!prev)
outputs_device_list = remove->next;
else
prev->next = remove->next;
outputs_device_free(remove);
vol_adjust();
}
void
outputs_device_select(struct output_device *device, int max_volume)
{
device->selected = 1;
device->prevent_playback = 0;
device->busy = 0;
// The purpose of this is to cap the volume for a newly selected device. It is
// used by the player to avoid this scenario:
// 1 Play on two speakers, say Kitchen (100) and Office (75), master is 100
// 2 Disable Office, reduce master to 25, Kitchen is now 25, Office is still 75
// 3 Turn on Office, it now blasts at 75
// We could avoid this by reducing the unselected Office in step 2, but that
// leads to issue #1077, where volumes of unselected devices go to 0 (e.g. by
// reducing master to 0 and then increasing again -> unselected stays at 0).
if (max_volume >= 0 && device->volume > max_volume)
device->volume = max_volume;
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 ret; // We don't change device state just because of a failed flush
}
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();
}
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 ret; // We don't change device state just because of a failed volume change
}
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);
}
int
outputs_device_authorize(struct output_device *device, const char *pin, output_status_cb cb)
{
int ret;
if (outputs[device->type]->disabled || !outputs[device->type]->device_authorize)
return -1;
if (device->session)
return 0; // We are already connected to the device - no auth required
ret = outputs[device->type]->device_authorize(device, pin, callback_add(device, cb));
return device_state_update(device, ret); // If ret < 0 then we couldn't reach the speaker
}
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;
for (device = outputs_device_list; device; device = device->next)
{
if (device->selected)
continue;
ret = outputs_device_stop(device, stopped_cb);
if (ret > 0)
pending += ret;
}
ret = 0; // We don't care about devices that returned an error on stop
for (device = outputs_device_list; device; device = device->next)
{
if (!device->selected)
continue;
ret = outputs_device_start(device, started_cb, only_probe);
if (ret > 0)
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 = outputs_device_list; device; device = device->next)
{
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 = outputs_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 = outputs_device_list; device; device = device->next)
{
ret = outputs_device_flush(device, cb);
if (ret < 0)
continue;
pending += ret;
}
return pending;
}
int
outputs_volume_get(void)
{
return outputs_master_volume;
}
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 = outputs_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;
}
return pending;
}
int
outputs_sessions_count(void)
{
struct output_device *device;
int count = 0;
for (device = outputs_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();
}
}
int
outputs_priority(struct output_device *device)
{
return outputs[device->type]->priority;
}
const char *
outputs_name(enum output_types type)
{
return outputs[type]->name;
}
struct output_device *
outputs_list(void)
{
return outputs_device_list;
}
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]->disabled)
{
continue;
}
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);
}