/* * Copyright (C) 2016 Espen Jürgensen * * 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 #endif #include #include #include #include #include #include #include #include #include #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); }