owntone-server/src/player.c
ejurgensen 0cb3881621 [player/outputs/raop] Get rid of outputs_playback_start() (still WIP)
outputs_playback_start() had the problem that was not consistently invoked: If
for instance local audio playback was running and a Airplay device was then
activated, the raop's playback_start would never be invoked (and vice versa,
of course).

Instead, the player now writes the presentation timestamp every time to the
output, so it doesn't need to keep track of it from the start.
2019-03-18 23:06:08 +01:00

3655 lines
80 KiB
C

/*
* Copyright (C) 2010-2011 Julien BLACHE <jb@jblache.org>
* Copyright (C) 2016-2017 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
* About player.c
* --------------
* The main tasks of the player are the following:
* - handle playback commands, status checks and events from other threads
* - receive audio from the input thread and to own the playback buffer
* - feed the outputs at the appropriate rate (controlled by the playback timer)
* - output device handling (partly outsourced to outputs.c)
* - notify about playback status changes
* - maintain the playback queue
*
* The player thread should never be making operations that may block, since
* that could block callers requesting status (effectively making forked-daapd
* unresponsive) and it could also starve the outputs. In practice this rule is
* not always obeyed, for instance some outputs do their setup in ways that
* could block.
*
*
* About metadata
* --------------
* The player gets metadata from library + inputs and passes it to the outputs
* and other clients (e.g. Remotes).
*
* 1. On playback start, metadata from the library is loaded into the queue
* items, and these items are then the source of metadata for clients.
* 2. During playback, the input may signal new metadata by making a
* input_write() with the INPUT_FLAG_METADATA flag. When the player read
* reaches that data, the player will request the metadata from the input
* with input_metadata_get(). This metadata is then saved to the currently
* playing queue item, and the clients are told to update metadata.
* 3. Artwork works differently than textual metadata. The artwork module will
* look for artwork in the library, and addition also check the artwork_url
* of the queue_item.
*/
#ifdef HAVE_CONFIG_H
# include <config.h>
#endif
#include <stdio.h>
#include <stdbool.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
#include <string.h>
#include <inttypes.h>
#include <stdint.h>
#include <errno.h>
#include <time.h>
#include <pthread.h>
#ifdef HAVE_PTHREAD_NP_H
# include <pthread_np.h>
#endif
#ifdef HAVE_TIMERFD
# include <sys/timerfd.h>
#elif defined(__FreeBSD__) || defined(__FreeBSD_kernel__)
# include <signal.h>
#endif
#include <event2/event.h>
#include <event2/buffer.h>
#include <gcrypt.h>
#include "db.h"
#include "logger.h"
#include "conffile.h"
#include "misc.h"
#include "player.h"
#include "worker.h"
#include "listener.h"
#include "commands.h"
// Audio and metadata outputs
#include "outputs.h"
// Audio and metadata input
#include "input.h"
// Scrobbling
#ifdef LASTFM
# include "lastfm.h"
#endif
#ifndef MIN
# define MIN(a, b) ((a < b) ? a : b)
#endif
#ifndef MAX
#define MAX(a, b) ((a > b) ? a : b)
#endif
// Default volume (must be from 0 - 100)
#define PLAYER_DEFAULT_VOLUME 50
// The interval between each tick of the playback clock in ms. This means that
// we read 10 ms frames from the input and pass to the output, so the clock
// ticks 100 times a second. We use this value because most common sample rates
// are divisible by 100, and because it keeps delay low.
// TODO sample rates of 22050 might cause underruns, since we would be reading
// only 100 x 220 = 22000 samples each second.
#define PLAYER_TICK_INTERVAL 10
// For every tick_interval, we will read a frame from the input buffer and
// write it to the outputs. If the input is empty, we will try to catch up next
// tick. However, at some point we will owe the outputs so much data that we
// have to suspend playback and wait for the input to get its act together.
// (value is in milliseconds and should be low enough to avoid output underrun)
#define PLAYER_READ_BEHIND_MAX 1500
// Generally, an output must not block (for long) when outputs_write() is
// called. If an output does that anyway, the next tick event will be late, and
// by extension playback_cb(). We will try to catch up, but if the delay
// gets above this value, we will suspend playback and reset the output.
// (value is in milliseconds)
#define PLAYER_WRITE_BEHIND_MAX 1500
// TODO get rid of me
#define TEMP_NEXT_RTPTIME (last_rtptime + pb_session.samples_written + pb_session.samples_per_read)
struct volume_param {
int volume;
uint64_t spk_id;
};
struct activeremote_param {
uint32_t activeremote;
const char *value;
};
struct spk_enum
{
spk_enum_cb cb;
void *arg;
};
struct speaker_set_param
{
uint64_t *device_ids;
int intval;
};
struct speaker_get_param
{
uint64_t spk_id;
struct player_speaker_info *spk_info;
};
struct metadata_param
{
struct input_metadata *input;
struct output_metadata *output;
};
struct speaker_auth_param
{
enum output_types type;
char pin[5];
};
union player_arg
{
struct output_device *device;
struct speaker_auth_param auth;
uint32_t id;
int intval;
};
struct player_session
{
uint8_t *buffer;
size_t bufsize;
uint32_t samples_written;
int samples_per_read;
struct media_quality quality;
// The time the playback session started
struct timespec start_ts;
// The time the first sample in the buffer should be played by the output.
// It will be equal to:
// pts = start_ts + OUTPUTS_BUFFER_DURATION + ticks_elapsed * player_tick_interval
struct timespec pts;
};
static struct player_session pb_session;
struct event_base *evbase_player;
static int player_exit;
static pthread_t tid_player;
static struct commands_base *cmdbase;
// Keep track of how many outputs need to call back when flushing internally
// from the player thread (where we can't use player_playback_pause)
static int player_flush_pending;
// Config values
static int speaker_autoselect;
static int clear_queue_on_stop_disabled;
// Player status
static enum play_status player_state;
static enum repeat_mode repeat;
static char shuffle;
static char consume;
// Playback timer
#ifdef HAVE_TIMERFD
static int pb_timer_fd;
#else
timer_t pb_timer;
#endif
static struct event *pb_timer_ev;
// Time between ticks, i.e. time between when playback_cb() is invoked
static struct timespec player_tick_interval;
// Timer resolution
static struct timespec player_timer_res;
// How many writes we owe the output (when the input is underrunning)
static int pb_read_deficit;
// PLAYER_READ_BEHIND_MAX and PLAYER_WRITE_BEHIND_MAX converted to clock ticks
static int pb_read_deficit_max;
static int pb_write_deficit_max;
// True if we are trying to recover from a major playback timer overrun (write problems)
static bool pb_write_recovery;
// Sync values
static struct timespec pb_pos_stamp;
static uint64_t pb_pos;
// Stream position (packets)
static uint64_t last_rtptime;
// Output devices
static struct output_device *dev_list;
// Output status
static int output_sessions;
// Last commanded volume
static int master_volume;
// Audio source
static struct player_source *cur_playing;
static struct player_source *cur_streaming;
static uint32_t cur_plid;
static uint32_t cur_plversion;
// Player buffer (holds one packet)
//static uint8_t pb_buffer[STOB(AIRTUNES_V2_PACKET_SAMPLES)];
//static size_t pb_buffer_offset;
// Play history
static struct player_history *history;
/* -------------------------------- Forwards -------------------------------- */
static void
playback_abort(void);
static void
playback_suspend(void);
/* ----------------------------- Volume helpers ----------------------------- */
static int
rel_to_vol(int relvol)
{
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)
{
float rel;
if (volume == master_volume)
return 100;
rel = ((float)volume / (float)master_volume) * 100.0;
return (int)rel;
}
// Master volume helpers
static void
volume_master_update(int newvol)
{
struct output_device *device;
master_volume = newvol;
for (device = dev_list; device; device = device->next)
{
if (device->selected)
device->relvol = vol_to_rel(device->volume);
}
}
static void
volume_master_find(void)
{
struct output_device *device;
int newmaster;
newmaster = -1;
for (device = dev_list; device; device = device->next)
{
if (device->selected && (device->volume > newmaster))
newmaster = device->volume;
}
volume_master_update(newmaster);
}
/* ---------------------- Device select/deselect hooks ---------------------- */
static void
speaker_select_output(struct output_device *device)
{
device->selected = 1;
if (device->volume > master_volume)
{
if (player_state == PLAY_STOPPED || master_volume == -1)
volume_master_update(device->volume);
else
device->volume = master_volume;
}
device->relvol = vol_to_rel(device->volume);
}
static void
speaker_deselect_output(struct output_device *device)
{
device->selected = 0;
if (device->volume == master_volume)
volume_master_find();
}
/* ----------------------- Misc helpers and callbacks ----------------------- */
// Callback from the worker thread (async operation as it may block)
static void
playcount_inc_cb(void *arg)
{
int *id = arg;
db_file_inc_playcount(*id);
}
static void
skipcount_inc_cb(void *arg)
{
int *id = arg;
db_file_inc_skipcount(*id);
}
#ifdef LASTFM
// Callback from the worker thread (async operation as it may block)
static void
scrobble_cb(void *arg)
{
int *id = arg;
lastfm_scrobble(*id);
}
#endif
// Callback from the worker thread. Here the heavy lifting is done: updating the
// db_queue_item, retrieving artwork (through outputs_metadata_prepare) and
// when done, telling the player to send the metadata to the clients
static void
metadata_update_cb(void *arg)
{
struct input_metadata *metadata = arg;
struct output_metadata *o_metadata;
struct db_queue_item *queue_item;
int ret;
queue_item = db_queue_fetch_byitemid(metadata->item_id);
if (!queue_item)
{
DPRINTF(E_LOG, L_PLAYER, "Bug! Input metadata item_id does not match anything in queue\n");
goto out_free_metadata;
}
// Update queue item if metadata changed
if (metadata->artist || metadata->title || metadata->album || metadata->genre || metadata->artwork_url || metadata->song_length)
{
// Since we won't be using the metadata struct values for anything else than
// this we just swap pointers
if (metadata->artist)
swap_pointers(&queue_item->artist, &metadata->artist);
if (metadata->title)
swap_pointers(&queue_item->title, &metadata->title);
if (metadata->album)
swap_pointers(&queue_item->album, &metadata->album);
if (metadata->genre)
swap_pointers(&queue_item->genre, &metadata->genre);
if (metadata->artwork_url)
swap_pointers(&queue_item->artwork_url, &metadata->artwork_url);
if (metadata->song_length)
queue_item->song_length = metadata->song_length;
ret = db_queue_update_item(queue_item);
if (ret < 0)
{
DPRINTF(E_LOG, L_PLAYER, "Database error while updating queue with new metadata\n");
goto out_free_queueitem;
}
}
o_metadata = outputs_metadata_prepare(metadata->item_id);
// Actual sending must be done by player, since the worker does not own the outputs
player_metadata_send(metadata, o_metadata);
outputs_metadata_free(o_metadata);
out_free_queueitem:
free_queue_item(queue_item, 0);
out_free_metadata:
input_metadata_free(metadata, 1);
}
// Gets the metadata, but since the actual update requires db writes and
// possibly retrieving artwork we let the worker do the next step
static void
metadata_trigger(int startup)
{
struct input_metadata metadata;
int ret;
ret = input_metadata_get(&metadata, cur_streaming, startup, TEMP_NEXT_RTPTIME);
if (ret < 0)
return;
worker_execute(metadata_update_cb, &metadata, sizeof(metadata), 0);
}
/*
* Add the song with the given id to the list of previously played songs
*/
static void
history_add(uint32_t id, uint32_t item_id)
{
unsigned int cur_index;
unsigned int next_index;
// Check if the current song is already the last in the history to avoid duplicates
cur_index = (history->start_index + history->count - 1) % MAX_HISTORY_COUNT;
if (id == history->id[cur_index])
{
DPRINTF(E_DBG, L_PLAYER, "Current playing/streaming song already in history\n");
return;
}
// Calculate the next index and update the start-index and count for the id-buffer
next_index = (history->start_index + history->count) % MAX_HISTORY_COUNT;
if (next_index == history->start_index && history->count > 0)
history->start_index = (history->start_index + 1) % MAX_HISTORY_COUNT;
history->id[next_index] = id;
history->item_id[next_index] = item_id;
if (history->count < MAX_HISTORY_COUNT)
history->count++;
}
static void
seek_save(void)
{
int seek;
if (!cur_streaming)
return;
if (cur_streaming->media_kind & (MEDIA_KIND_MOVIE | MEDIA_KIND_PODCAST | MEDIA_KIND_AUDIOBOOK | MEDIA_KIND_TVSHOW))
{
seek = (cur_streaming->output_start - cur_streaming->stream_start) / 44100 * 1000;
db_file_seek_update(cur_streaming->id, seek);
}
}
static void
status_update(enum play_status status)
{
player_state = status;
listener_notify(LISTENER_PLAYER);
}
/* ----------- Audio source handling (interfaces with input module) --------- */
static struct player_source *
source_now_playing()
{
if (cur_playing)
return cur_playing;
return cur_streaming;
}
/*
* Creates a new player source for the given queue item
*/
static struct player_source *
source_new(struct db_queue_item *queue_item)
{
struct player_source *ps;
ps = calloc(1, sizeof(struct player_source));
if (!ps)
{
DPRINTF(E_LOG, L_PLAYER, "Out of memory (ps)\n");
return NULL;
}
ps->id = queue_item->file_id;
ps->item_id = queue_item->id;
ps->data_kind = queue_item->data_kind;
ps->media_kind = queue_item->media_kind;
ps->len_ms = queue_item->song_length;
ps->play_next = NULL;
ps->path = strdup(queue_item->path);
return ps;
}
static void
source_free(struct player_source *ps)
{
if (ps->path)
free(ps->path);
free(ps);
}
/*
* Stops playback for the current streaming source and frees all
* player sources (starting from the playing source). Sets current streaming
* and playing sources to NULL.
*/
static void
source_stop()
{
struct player_source *ps_playing;
struct player_source *ps_temp;
if (cur_streaming)
input_stop(cur_streaming);
ps_playing = source_now_playing();
while (ps_playing)
{
ps_temp = ps_playing;
ps_playing = ps_playing->play_next;
ps_temp->play_next = NULL;
source_free(ps_temp);
}
cur_playing = NULL;
cur_streaming = NULL;
}
/*
* Pauses playback
*
* Resets the streaming source to the playing source and adjusts stream-start
* and output-start values to the playing time. Sets the current streaming
* source to NULL.
*/
static int
source_pause(uint64_t pos)
{
struct player_source *ps_playing;
struct player_source *ps_playnext;
struct player_source *ps_temp;
uint64_t seek_frames;
int seek_ms;
int ret;
ps_playing = source_now_playing();
if (!ps_playing)
return -1;
if (cur_streaming)
{
if (ps_playing != cur_streaming)
{
DPRINTF(E_DBG, L_PLAYER,
"Pause called on playing source (id=%d) and streaming source already "
"switched to the next item (id=%d)\n", ps_playing->id, cur_streaming->id);
ret = input_stop(cur_streaming);
if (ret < 0)
return -1;
}
else
{
ret = input_pause(cur_streaming);
if (ret < 0)
return -1;
}
}
ps_playnext = ps_playing->play_next;
while (ps_playnext)
{
ps_temp = ps_playnext;
ps_playnext = ps_playnext->play_next;
ps_temp->play_next = NULL;
source_free(ps_temp);
}
ps_playing->play_next = NULL;
cur_playing = NULL;
cur_streaming = ps_playing;
if (!cur_streaming->setup_done)
{
DPRINTF(E_INFO, L_PLAYER, "Opening '%s'\n", cur_streaming->path);
ret = input_setup(cur_streaming);
if (ret < 0)
{
DPRINTF(E_LOG, L_PLAYER, "Failed to open '%s'\n", cur_streaming->path);
return -1;
}
}
// Seek back to the pause position
seek_frames = (pos - cur_streaming->stream_start);
seek_ms = (int)((seek_frames * 1000) / 44100);
ret = input_seek(cur_streaming, seek_ms);
// TODO what if ret < 0?
// Adjust start_pos to take into account the pause and seek back
cur_streaming->stream_start = TEMP_NEXT_RTPTIME - ((uint64_t)ret * 44100) / 1000;
cur_streaming->output_start = TEMP_NEXT_RTPTIME;
cur_streaming->end = 0;
return 0;
}
/*
* Seeks the current streaming source to the given postion in milliseconds
* and adjusts stream-start and output-start values.
*
* @param seek_ms Position in milliseconds to seek
* @return The new position in milliseconds or -1 on error
*/
static int
source_seek(int seek_ms)
{
int ret;
ret = input_seek(cur_streaming, seek_ms);
if (ret < 0)
return -1;
// Adjust start_pos to take into account the pause and seek back
cur_streaming->stream_start = TEMP_NEXT_RTPTIME - ((uint64_t)ret * 44100) / 1000;
cur_streaming->output_start = TEMP_NEXT_RTPTIME;
return ret;
}
/*
* Starts or resumes playback
*/
static int
source_play()
{
int ret;
ret = input_start(cur_streaming);
return ret;
}
/*
* Opens the given player source for playback (but does not start playback)
*
* The given source is appended to the current streaming source (if one exists) and
* becomes the new current streaming source.
*
* Stream-start and output-start values are set to the given start position.
*/
static int
source_open(struct player_source *ps, uint64_t start_pos, int seek_ms)
{
int ret;
DPRINTF(E_INFO, L_PLAYER, "Opening '%s' (id=%d, item-id=%d)\n", ps->path, ps->id, ps->item_id);
if (cur_streaming && cur_streaming->end == 0)
{
DPRINTF(E_LOG, L_PLAYER, "Current streaming source not at eof '%s' (id=%d, item-id=%d)\n",
cur_streaming->path, cur_streaming->id, cur_streaming->item_id);
return -1;
}
ret = input_setup(ps);
if (ret < 0)
{
DPRINTF(E_LOG, L_PLAYER, "Failed to open '%s' (id=%d, item-id=%d)\n", ps->path, ps->id, ps->item_id);
return -1;
}
// If a streaming source exists, append the new source as play-next and set it
// as the new streaming source
if (cur_streaming)
cur_streaming->play_next = ps;
cur_streaming = ps;
cur_streaming->stream_start = start_pos;
cur_streaming->output_start = cur_streaming->stream_start;
cur_streaming->end = 0;
// Seek to the given seek position
if (seek_ms)
{
DPRINTF(E_INFO, L_PLAYER, "Seek to %d ms for '%s' (id=%d, item-id=%d)\n", seek_ms, ps->path, ps->id, ps->item_id);
source_seek(seek_ms);
}
return ret;
}
/*
* Closes the current streaming source and sets its end-time to the given
* position
*/
static int
source_close(uint64_t end_pos)
{
input_stop(cur_streaming);
cur_streaming->end = end_pos;
return 0;
}
/*
* Updates the now playing item (cur_playing) and notifies remotes and raop devices
* about changes. Also takes care of stopping playback after the last item.
*
* @return Returns the current playback position as rtp-time
*/
static uint64_t
source_check(void)
{
struct timespec ts;
struct player_source *ps;
uint64_t pos;
int i;
int id;
int ret;
ret = player_get_current_pos(&pos, &ts, 0);
if (ret < 0)
{
DPRINTF(E_LOG, L_PLAYER, "Couldn't get current playback position\n");
return 0;
}
if (player_state == PLAY_STOPPED)
{
DPRINTF(E_LOG, L_PLAYER, "Bug! source_check called but playback has already stopped\n");
return pos;
}
// If cur_playing is NULL, we are still in the first two seconds after starting the stream
if (!cur_playing)
{
if (pos >= cur_streaming->output_start)
{
cur_playing = cur_streaming;
status_update(PLAY_PLAYING);
// Start of streaming, no metadata to prune yet
}
return pos;
}
// Check if we are still in the middle of the current playing song
if ((cur_playing->end == 0) || (pos < cur_playing->end))
return pos;
// We have reached the end of the current playing song, update cur_playing to
// the next song in the queue and initialize stream_start and output_start values.
i = 0;
while (cur_playing && (cur_playing->end != 0) && (pos > cur_playing->end))
{
i++;
id = (int)cur_playing->id;
if (id != DB_MEDIA_FILE_NON_PERSISTENT_ID)
{
worker_execute(playcount_inc_cb, &id, sizeof(int), 5);
#ifdef LASTFM
worker_execute(scrobble_cb, &id, sizeof(int), 8);
#endif
history_add(cur_playing->id, cur_playing->item_id);
}
if (consume)
db_queue_delete_byitemid(cur_playing->item_id);
if (!cur_playing->play_next)
{
playback_abort();
return pos;
}
ps = cur_playing;
cur_playing = cur_playing->play_next;
source_free(ps);
}
if (i > 0)
{
DPRINTF(E_DBG, L_PLAYER, "Playback switched to next song\n");
status_update(PLAY_PLAYING);
outputs_metadata_prune(pos);
}
return pos;
}
/*
* Returns the next player source based on the current streaming source and repeat mode
*
* If repeat mode is repeat all, shuffle is active and the current streaming source is the
* last item in the queue, the queue is reshuffled prior to returning the first item of the
* queue.
*/
static struct player_source *
source_next()
{
struct player_source *ps = NULL;
struct db_queue_item *queue_item;
if (!cur_streaming)
{
DPRINTF(E_LOG, L_PLAYER, "source_next() called with no current streaming source available\n");
return NULL;
}
if (repeat == REPEAT_SONG)
{
queue_item = db_queue_fetch_byitemid(cur_streaming->item_id);
if (!queue_item)
{
DPRINTF(E_LOG, L_PLAYER, "Error fetching item from queue '%s' (id=%d, item-id=%d)\n", cur_streaming->path, cur_streaming->id, cur_streaming->item_id);
return NULL;
}
}
else
{
queue_item = db_queue_fetch_next(cur_streaming->item_id, shuffle);
if (!queue_item && repeat == REPEAT_ALL)
{
if (shuffle)
{
db_queue_reshuffle(0);
}
queue_item = db_queue_fetch_bypos(0, shuffle);
if (!queue_item)
{
DPRINTF(E_LOG, L_PLAYER, "Error fetching item from queue '%s' (id=%d, item-id=%d)\n", cur_streaming->path, cur_streaming->id, cur_streaming->item_id);
return NULL;
}
}
}
if (!queue_item)
{
DPRINTF(E_DBG, L_PLAYER, "Reached end of queue\n");
return NULL;
}
ps = source_new(queue_item);
free_queue_item(queue_item, 0);
return ps;
}
/*
* Returns the previous player source based on the current streaming source
*/
static struct player_source *
source_prev()
{
struct player_source *ps = NULL;
struct db_queue_item *queue_item;
if (!cur_streaming)
{
DPRINTF(E_LOG, L_PLAYER, "source_prev() called with no current streaming source available\n");
return NULL;
}
queue_item = db_queue_fetch_prev(cur_streaming->item_id, shuffle);
if (!queue_item)
return NULL;
ps = source_new(queue_item);
free_queue_item(queue_item, 0);
return ps;
}
static int
source_switch(int nbytes)
{
struct player_source *ps;
int ret;
DPRINTF(E_DBG, L_PLAYER, "Switching track\n");
source_close(TEMP_NEXT_RTPTIME + BTOS(nbytes, pb_session.quality.bits_per_sample, 2) - 1);
while ((ps = source_next()))
{
ret = source_open(ps, cur_streaming->end + 1, 0);
if (ret < 0)
{
db_queue_delete_byitemid(ps->item_id);
continue;
}
ret = source_play();
if (ret < 0)
{
db_queue_delete_byitemid(ps->item_id);
source_close(TEMP_NEXT_RTPTIME + BTOS(nbytes, pb_session.quality.bits_per_sample, 2) - 1);
continue;
}
break;
}
if (!ps) // End of queue
{
cur_streaming = NULL;
return 0;
}
metadata_trigger(0);
return 0;
}
static void
session_init(struct player_session *session, struct media_quality *quality)
{
session->samples_written = 0;
session->quality = *quality;
session->samples_per_read = (quality->sample_rate / 1000) * (player_tick_interval.tv_nsec / 1000000);
session->bufsize = STOB(session->samples_per_read, quality->bits_per_sample, quality->channels);
DPRINTF(E_DBG, L_PLAYER, "New session values (q=%d/%d/%d, spr=%d, bufsize=%zu)\n",
quality->sample_rate, quality->bits_per_sample, quality->channels,
session->samples_per_read, session->bufsize);
if (session->buffer)
session->buffer = realloc(session->buffer, session->bufsize);
else
session->buffer = malloc(session->bufsize);
CHECK_NULL(L_PLAYER, session->buffer);
clock_gettime_with_res(CLOCK_MONOTONIC, &session->start_ts, &player_timer_res);
session->pts.tv_sec = session->start_ts.tv_sec + OUTPUTS_BUFFER_DURATION;
session->pts.tv_nsec = session->start_ts.tv_nsec;
}
static void
session_deinit(struct player_session *session)
{
free(session->buffer);
memset(session, 0, sizeof(struct player_session));
}
/* ----------------- Main read, write and playback timer event -------------- */
// Returns -1 on error (caller should abort playback), or bytes read (possibly 0)
static int
source_read(uint8_t *buf, int len)
{
struct media_quality quality;
int nbytes;
uint32_t item_id;
int ret;
short flags;
// Nothing to read, stream silence until source_check() stops playback
if (!cur_streaming)
{
memset(buf, 0, len);
return len;
}
nbytes = input_read(buf, len, &flags);
if ((nbytes < 0) || (flags & INPUT_FLAG_ERROR))
{
DPRINTF(E_LOG, L_PLAYER, "Error reading source %d\n", cur_streaming->id);
nbytes = 0;
item_id = cur_streaming->item_id;
ret = source_switch(0);
db_queue_delete_byitemid(item_id);
if (ret < 0)
return -1;
}
else if (flags & INPUT_FLAG_EOF)
{
ret = source_switch(nbytes);
if (ret < 0)
return -1;
}
else if (flags & INPUT_FLAG_METADATA)
{
metadata_trigger(0);
}
else if (flags & INPUT_FLAG_QUALITY)
{
input_quality_get(&quality);
session_init(&pb_session, &quality);
}
// We pad the output buffer with silence if we don't have enough data for a
// full packet and there is no more data coming up (no more tracks in queue)
if ((nbytes < len) && (!cur_streaming))
{
memset(buf + nbytes, 0, len - nbytes);
nbytes = len;
}
return nbytes;
}
static void
playback_cb(int fd, short what, void *arg)
{
struct timespec ts;
uint64_t overrun;
int got;
int nsamples;
int i;
int ret;
// Check if we missed any timer expirations
overrun = 0;
#ifdef HAVE_TIMERFD
ret = read(fd, &overrun, sizeof(overrun));
if (ret <= 0)
DPRINTF(E_LOG, L_PLAYER, "Error reading timer\n");
else if (overrun > 0)
overrun--;
#else
ret = timer_getoverrun(pb_timer);
if (ret < 0)
DPRINTF(E_LOG, L_PLAYER, "Error getting timer overrun\n");
else
overrun = ret;
#endif /* HAVE_TIMERFD */
// We are too delayed, probably some output blocked: reset if first overrun or abort if second overrun
if (overrun > pb_write_deficit_max)
{
if (pb_write_recovery)
{
DPRINTF(E_LOG, L_PLAYER, "Permanent output delay detected (behind=%" PRIu64 ", max=%d), aborting\n", overrun, pb_write_deficit_max);
playback_abort();
return;
}
DPRINTF(E_LOG, L_PLAYER, "Output delay detected (behind=%" PRIu64 ", max=%d), resetting all outputs\n", overrun, pb_write_deficit_max);
pb_write_recovery = true;
playback_suspend();
return;
}
else
{
if (overrun > 1) // An overrun of 1 is no big deal
DPRINTF(E_WARN, L_PLAYER, "Output delay detected: player is %" PRIu64 " ticks behind, catching up\n", overrun);
pb_write_recovery = false;
}
// If there was an overrun, we will try to read/write a corresponding number
// of times so we catch up. The read from the input is non-blocking, so it
// should not bring us further behind, even if there is no data.
for (i = 1 + overrun + pb_read_deficit; i > 0; i--)
{
source_check();
// Make sure playback is still running after source_check()
if (player_state != PLAY_PLAYING)
return;
got = source_read(pb_session.buffer, pb_session.bufsize);
if (got < 0)
{
DPRINTF(E_LOG, L_PLAYER, "Error reading from source, aborting playback\n");
playback_abort();
break;
}
else if (got == 0)
{
pb_read_deficit++;
break;
}
nsamples = BTOS(got, pb_session.quality.bits_per_sample, pb_session.quality.channels);
outputs_write2(pb_session.buffer, pb_session.bufsize, &pb_session.quality, nsamples, &pb_session.pts);
pb_session.samples_written += nsamples;
if (got < pb_session.bufsize)
{
DPRINTF(E_DBG, L_PLAYER, "Incomplete read, wanted %zu, got %d\n", pb_session.bufsize, got);
// How much the number of samples we got corresponds to in time (nanoseconds)
ts.tv_sec = 0;
ts.tv_nsec = 1000000000L * nsamples / pb_session.quality.sample_rate;
pb_session.pts = timespec_add(pb_session.pts, ts);
pb_read_deficit++;
}
else
{
// We got a full frame, so that means we can also advance the presentation timestamp by a full tick
pb_session.pts = timespec_add(pb_session.pts, player_tick_interval);
if (pb_read_deficit > 0)
pb_read_deficit--;
}
}
if (pb_read_deficit > pb_read_deficit_max)
{
DPRINTF(E_LOG, L_PLAYER, "Source is not providing sufficient data, temporarily suspending playback (deficit=%d)\n", pb_read_deficit);
playback_suspend();
}
}
/* ----------------- Output device handling (add/remove etc) ---------------- */
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 = dev_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 == dev_list)
dev_list = next;
if (prev)
prev->next = next;
device->next = next->next;
next->next = device;
swaps++;
}
prev = device;
}
}
while (swaps > 0);
}
static void
device_remove(struct output_device *remove)
{
struct output_device *device;
struct output_device *prev;
int ret;
prev = NULL;
for (device = dev_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);
// Make sure device isn't selected anymore
if (remove->selected)
speaker_deselect_output(remove);
if (!prev)
dev_list = remove->next;
else
prev->next = remove->next;
outputs_device_free(remove);
}
static int
device_check(struct output_device *check)
{
struct output_device *device;
for (device = dev_list; device; device = device->next)
{
if (device == check)
break;
}
return (device) ? 0 : -1;
}
static enum command_state
device_add(void *arg, int *retval)
{
union player_arg *cmdarg;
struct output_device *add;
struct output_device *device;
char *keep_name;
int ret;
cmdarg = arg;
add = cmdarg->device;
for (device = dev_list; device; device = device->next)
{
if (device->id == add->id)
break;
}
// New device
if (!device)
{
device = add;
keep_name = strdup(device->name);
ret = db_speaker_get(device, device->id);
if (ret < 0)
{
device->selected = 0;
device->volume = (master_volume >= 0) ? master_volume : PLAYER_DEFAULT_VOLUME;
}
free(device->name);
device->name = keep_name;
if (device->selected && (player_state != PLAY_PLAYING))
speaker_select_output(device);
else
device->selected = 0;
device->next = dev_list;
dev_list = device;
}
// Update to a device already in the list
else
{
device->advertised = 1;
if (add->v4_address)
{
if (device->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)
{
if (device->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;
}
if (device->name)
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();
listener_notify(LISTENER_SPEAKER);
*retval = 0;
return COMMAND_END;
}
static enum command_state
device_remove_family(void *arg, int *retval)
{
union player_arg *cmdarg;
struct output_device *remove;
struct output_device *device;
cmdarg = arg;
remove = cmdarg->device;
for (device = dev_list; device; device = device->next)
{
if (device->id == remove->id)
break;
}
if (!device)
{
DPRINTF(E_WARN, L_PLAYER, "The %s device '%s' stopped advertising, but not in our list\n", remove->type_name, remove->name);
outputs_device_free(remove);
*retval = 0;
return COMMAND_END;
}
// v{4,6}_port non-zero indicates the address family stopped advertising
if (remove->v4_port && device->v4_address)
{
free(device->v4_address);
device->v4_address = NULL;
device->v4_port = 0;
}
if (remove->v6_port && device->v6_address)
{
free(device->v6_address);
device->v6_address = NULL;
device->v6_port = 0;
}
if (!device->v4_address && !device->v6_address)
{
device->advertised = 0;
if (!device->session)
device_remove(device);
}
outputs_device_free(remove);
listener_notify(LISTENER_SPEAKER);
*retval = 0;
return COMMAND_END;
}
static enum command_state
device_auth_kickoff(void *arg, int *retval)
{
union player_arg *cmdarg = arg;
outputs_authorize(cmdarg->auth.type, cmdarg->auth.pin);
*retval = 0;
return COMMAND_END;
}
static enum command_state
device_metadata_send(void *arg, int *retval)
{
struct metadata_param *metadata_param = arg;
struct input_metadata *imd;
struct output_metadata *omd;
imd = metadata_param->input;
omd = metadata_param->output;
outputs_metadata_send(omd, imd->rtptime, imd->offset, imd->startup);
status_update(player_state);
*retval = 0;
return COMMAND_END;
}
/* -------- Output device callbacks executed in the player thread ----------- */
static void
device_streaming_cb(struct output_device *device, struct output_session *session, enum output_device_state status)
{
int ret;
DPRINTF(E_DBG, L_PLAYER, "Callback from %s to device_streaming_cb\n", outputs_name(device->type));
ret = device_check(device);
if (ret < 0)
{
DPRINTF(E_LOG, L_PLAYER, "Output device disappeared during streaming!\n");
output_sessions--;
return;
}
if (status == OUTPUT_STATE_FAILED)
{
DPRINTF(E_LOG, L_PLAYER, "The %s device '%s' FAILED\n", device->type_name, device->name);
output_sessions--;
if (player_state == PLAY_PLAYING)
speaker_deselect_output(device);
device->session = NULL;
if (!device->advertised)
device_remove(device);
if (output_sessions == 0)
playback_abort();
}
else if (status == OUTPUT_STATE_STOPPED)
{
DPRINTF(E_INFO, L_PLAYER, "The %s device '%s' stopped\n", device->type_name, device->name);
output_sessions--;
device->session = NULL;
if (!device->advertised)
device_remove(device);
}
else
outputs_status_cb(session, device_streaming_cb);
}
static void
device_command_cb(struct output_device *device, struct output_session *session, enum output_device_state status)
{
DPRINTF(E_DBG, L_PLAYER, "Callback from %s to device_command_cb\n", outputs_name(device->type));
outputs_status_cb(session, device_streaming_cb);
if (status == OUTPUT_STATE_FAILED)
device_streaming_cb(device, session, status);
// Used by playback_suspend - is basically the bottom half
if (player_flush_pending > 0)
{
player_flush_pending--;
if (player_flush_pending == 0)
input_buffer_full_cb(player_playback_start);
}
commands_exec_end(cmdbase, 0);
}
static void
device_shutdown_cb(struct output_device *device, struct output_session *session, enum output_device_state status)
{
int retval;
int ret;
DPRINTF(E_DBG, L_PLAYER, "Callback from %s to device_shutdown_cb\n", outputs_name(device->type));
if (output_sessions)
output_sessions--;
retval = commands_exec_returnvalue(cmdbase);
ret = device_check(device);
if (ret < 0)
{
DPRINTF(E_WARN, L_PLAYER, "Output device disappeared before shutdown completion!\n");
if (retval != -2)
retval = -1;
goto out;
}
device->session = NULL;
if (!device->advertised)
device_remove(device);
out:
/* cur_cmd->ret already set
* - to 0 (or -2 if password issue) in speaker_set()
* - to -1 above on error
*/
commands_exec_end(cmdbase, retval);
}
static void
device_lost_cb(struct output_device *device, struct output_session *session, enum output_device_state status)
{
DPRINTF(E_DBG, L_PLAYER, "Callback from %s to device_lost_cb\n", outputs_name(device->type));
// We lost that device during startup for some reason, not much we can do here
if (status == OUTPUT_STATE_FAILED)
DPRINTF(E_WARN, L_PLAYER, "Failed to stop lost device\n");
else
DPRINTF(E_INFO, L_PLAYER, "Lost device stopped properly\n");
}
static void
device_activate_cb(struct output_device *device, struct output_session *session, enum output_device_state status)
{
int retval;
int ret;
DPRINTF(E_DBG, L_PLAYER, "Callback from %s to device_activate_cb\n", outputs_name(device->type));
retval = commands_exec_returnvalue(cmdbase);
ret = device_check(device);
if (ret < 0)
{
DPRINTF(E_WARN, L_PLAYER, "Output device disappeared during startup!\n");
outputs_status_cb(session, device_lost_cb);
outputs_device_stop(session);
if (retval != -2)
retval = -1;
goto out;
}
if (status == OUTPUT_STATE_PASSWORD)
{
status = OUTPUT_STATE_FAILED;
retval = -2;
}
if (status == OUTPUT_STATE_FAILED)
{
speaker_deselect_output(device);
if (!device->advertised)
device_remove(device);
if (retval != -2)
retval = -1;
goto out;
}
device->session = session;
output_sessions++;
outputs_status_cb(session, device_streaming_cb);
out:
/* cur_cmd->ret already set
* - to 0 in speaker_set() (default)
* - to -2 above if password issue
* - to -1 above on error
*/
commands_exec_end(cmdbase, retval);
}
static void
device_probe_cb(struct output_device *device, struct output_session *session, enum output_device_state status)
{
int retval;
int ret;
DPRINTF(E_DBG, L_PLAYER, "Callback from %s to device_probe_cb\n", outputs_name(device->type));
retval = commands_exec_returnvalue(cmdbase);
ret = device_check(device);
if (ret < 0)
{
DPRINTF(E_WARN, L_PLAYER, "Output device disappeared during probe!\n");
if (retval != -2)
retval = -1;
goto out;
}
if (status == OUTPUT_STATE_PASSWORD)
{
status = OUTPUT_STATE_FAILED;
retval = -2;
}
if (status == OUTPUT_STATE_FAILED)
{
speaker_deselect_output(device);
if (!device->advertised)
device_remove(device);
if (retval != -2)
retval = -1;
goto out;
}
out:
/* cur_cmd->ret already set
* - to 0 in speaker_set() (default)
* - to -2 above if password issue
* - to -1 above on error
*/
commands_exec_end(cmdbase, retval);
}
static void
device_restart_cb(struct output_device *device, struct output_session *session, enum output_device_state status)
{
int retval;
int ret;
DPRINTF(E_DBG, L_PLAYER, "Callback from %s to device_restart_cb\n", outputs_name(device->type));
retval = commands_exec_returnvalue(cmdbase);
ret = device_check(device);
if (ret < 0)
{
DPRINTF(E_WARN, L_PLAYER, "Output device disappeared during restart!\n");
outputs_status_cb(session, device_lost_cb);
outputs_device_stop(session);
if (retval != -2)
retval = -1;
goto out;
}
if (status == OUTPUT_STATE_PASSWORD)
{
status = OUTPUT_STATE_FAILED;
retval = -2;
}
if (status == OUTPUT_STATE_FAILED)
{
speaker_deselect_output(device);
if (!device->advertised)
device_remove(device);
if (retval != -2)
retval = -1;
goto out;
}
device->session = session;
output_sessions++;
outputs_status_cb(session, device_streaming_cb);
out:
commands_exec_end(cmdbase, retval);
}
/* ------------------------- Internal playback routines --------------------- */
static int
playback_timer_start(void)
{
struct itimerspec tick;
int ret;
ret = event_add(pb_timer_ev, NULL);
if (ret < 0)
{
DPRINTF(E_LOG, L_PLAYER, "Could not add playback timer\n");
return -1;
}
tick.it_interval = player_tick_interval;
tick.it_value = player_tick_interval;
#ifdef HAVE_TIMERFD
ret = timerfd_settime(pb_timer_fd, 0, &tick, NULL);
#else
ret = timer_settime(pb_timer, 0, &tick, NULL);
#endif
if (ret < 0)
{
DPRINTF(E_LOG, L_PLAYER, "Could not arm playback timer: %s\n", strerror(errno));
return -1;
}
return 0;
}
static int
playback_timer_stop(void)
{
struct itimerspec tick;
int ret;
event_del(pb_timer_ev);
memset(&tick, 0, sizeof(struct itimerspec));
#ifdef HAVE_TIMERFD
ret = timerfd_settime(pb_timer_fd, 0, &tick, NULL);
#else
ret = timer_settime(pb_timer, 0, &tick, NULL);
#endif
if (ret < 0)
{
DPRINTF(E_LOG, L_PLAYER, "Could not disarm playback timer: %s\n", strerror(errno));
return -1;
}
return 0;
}
static void
playback_abort(void)
{
outputs_playback_stop();
playback_timer_stop();
source_stop();
if (!clear_queue_on_stop_disabled)
db_queue_clear(0);
status_update(PLAY_STOPPED);
outputs_metadata_purge();
}
// Temporarily suspends/resets playback, used when input buffer underruns or in
// case of problems writing to the outputs
static void
playback_suspend(void)
{
player_flush_pending = outputs_flush2(device_command_cb);
playback_timer_stop();
status_update(PLAY_PAUSED);
seek_save();
// No devices to wait for, just set the restart cb right away
if (player_flush_pending == 0)
input_buffer_full_cb(player_playback_start);
}
/* --------------- Actual commands, executed in the player thread ----------- */
static enum command_state
get_status(void *arg, int *retval)
{
struct player_status *status = arg;
struct timespec ts;
struct player_source *ps;
uint64_t pos;
int ret;
memset(status, 0, sizeof(struct player_status));
status->shuffle = shuffle;
status->consume = consume;
status->repeat = repeat;
status->volume = master_volume;
status->plid = cur_plid;
switch (player_state)
{
case PLAY_STOPPED:
DPRINTF(E_DBG, L_PLAYER, "Player status: stopped\n");
status->status = PLAY_STOPPED;
break;
case PLAY_PAUSED:
DPRINTF(E_DBG, L_PLAYER, "Player status: paused\n");
status->status = PLAY_PAUSED;
status->id = cur_streaming->id;
status->item_id = cur_streaming->item_id;
pos = TEMP_NEXT_RTPTIME - cur_streaming->stream_start;
status->pos_ms = (pos * 1000) / 44100;
status->len_ms = cur_streaming->len_ms;
break;
case PLAY_PLAYING:
if (!cur_playing)
{
DPRINTF(E_DBG, L_PLAYER, "Player status: playing (buffering)\n");
status->status = PLAY_PAUSED;
ps = cur_streaming;
// Avoid a visible 2-second jump backward for the client
pos = ps->output_start - ps->stream_start;
}
else
{
DPRINTF(E_DBG, L_PLAYER, "Player status: playing\n");
status->status = PLAY_PLAYING;
ps = cur_playing;
ret = player_get_current_pos(&pos, &ts, 0);
if (ret < 0)
{
DPRINTF(E_LOG, L_PLAYER, "Could not get current stream position for playstatus\n");
pos = 0;
}
if (pos < ps->stream_start)
pos = 0;
else
pos -= ps->stream_start;
}
status->pos_ms = (pos * 1000) / 44100;
status->len_ms = ps->len_ms;
status->id = ps->id;
status->item_id = ps->item_id;
break;
}
*retval = 0;
return COMMAND_END;
}
static enum command_state
now_playing(void *arg, int *retval)
{
uint32_t *id = arg;
struct player_source *ps_playing;
ps_playing = source_now_playing();
if (ps_playing)
*id = ps_playing->id;
else
{
*retval = -1;
return COMMAND_END;
}
*retval = 0;
return COMMAND_END;
}
static enum command_state
playback_stop(void *arg, int *retval)
{
struct player_source *ps_playing;
if (player_state == PLAY_STOPPED)
{
*retval = 0;
return COMMAND_END;
}
// We may be restarting very soon, so we don't bring the devices to a full
// stop just yet; this saves time when restarting, which is nicer for the user
*retval = outputs_flush2(device_command_cb);
playback_timer_stop();
ps_playing = source_now_playing();
if (ps_playing)
{
history_add(ps_playing->id, ps_playing->item_id);
}
source_stop();
status_update(PLAY_STOPPED);
outputs_metadata_purge();
// We're async if we need to flush devices
if (*retval > 0)
return COMMAND_PENDING;
return COMMAND_END;
}
static enum command_state
playback_start_bh(void *arg, int *retval)
{
struct timespec ts;
int ret;
// initialize the packet timer to the same relative time that we have
// for the playback timer.
// packet_timer_last.tv_sec = pb_pos_stamp.tv_sec;
// packet_timer_last.tv_nsec = pb_pos_stamp.tv_nsec;
// pb_timer_last.tv_sec = pb_pos_stamp.tv_sec;
// pb_timer_last.tv_nsec = pb_pos_stamp.tv_nsec;
// pb_buffer_offset = 0;
pb_read_deficit = 0;
ret = clock_gettime_with_res(CLOCK_MONOTONIC, &ts, &player_timer_res);
if (ret < 0)
goto out_fail;
ret = playback_timer_start();
if (ret < 0)
goto out_fail;
status_update(PLAY_PLAYING);
*retval = 0;
return COMMAND_END;
out_fail:
playback_abort();
*retval = -1;
return COMMAND_END;
}
static enum command_state
playback_start_item(void *arg, int *retval)
{
struct db_queue_item *queue_item = arg;
struct media_file_info *mfi;
struct output_device *device;
struct player_source *ps;
int seek_ms;
int ret;
if (player_state == PLAY_PLAYING)
{
DPRINTF(E_DBG, L_PLAYER, "Player is already playing, ignoring call to playback start\n");
status_update(player_state);
*retval = 1; // Value greater 0 will prevent execution of the bottom half function
return COMMAND_END;
}
// Update global playback position
pb_pos = TEMP_NEXT_RTPTIME - 88200;
if (player_state == PLAY_STOPPED && !queue_item)
{
DPRINTF(E_LOG, L_PLAYER, "Failed to start/resume playback, no queue item given\n");
*retval = -1;
return COMMAND_END;
}
if (!queue_item)
{
// Resume playback of current source
ps = source_now_playing();
DPRINTF(E_DBG, L_PLAYER, "Resume playback of '%s' (id=%d, item-id=%d)\n", ps->path, ps->id, ps->item_id);
}
else
{
// Start playback for given queue item
DPRINTF(E_DBG, L_PLAYER, "Start playback of '%s' (id=%d, item-id=%d)\n", queue_item->path, queue_item->file_id, queue_item->id);
source_stop();
ps = source_new(queue_item);
if (!ps)
{
playback_abort();
*retval = -1;
return COMMAND_END;
}
seek_ms = 0;
if (queue_item->file_id > 0)
{
mfi = db_file_fetch_byid(queue_item->file_id);
if (mfi)
{
seek_ms = mfi->seek;
free_mfi(mfi, 0);
}
}
ret = source_open(ps, TEMP_NEXT_RTPTIME, seek_ms);
if (ret < 0)
{
playback_abort();
source_free(ps);
*retval = -1;
return COMMAND_END;
}
}
ret = source_play();
if (ret < 0)
{
playback_abort();
*retval = -1;
return COMMAND_END;
}
metadata_trigger(1);
// Start sessions on selected devices
*retval = 0;
for (device = dev_list; device; device = device->next)
{
if (device->selected && !device->session)
{
ret = outputs_device_start2(device, device_restart_cb);
if (ret < 0)
{
DPRINTF(E_LOG, L_PLAYER, "Could not start selected %s device '%s'\n", device->type_name, device->name);
continue;
}
DPRINTF(E_INFO, L_PLAYER, "Using selected %s device '%s'\n", device->type_name, device->name);
(*retval)++;
}
}
// If autoselecting is enabled, try to autoselect a non-selected device if the above failed
if (speaker_autoselect && (*retval == 0) && (output_sessions == 0))
for (device = dev_list; device; device = device->next)
{
if ((outputs_priority(device) == 0) || device->session)
continue;
speaker_select_output(device);
ret = outputs_device_start(device, device_restart_cb, TEMP_NEXT_RTPTIME);
if (ret < 0)
{
DPRINTF(E_DBG, L_PLAYER, "Could not autoselect %s device '%s'\n", device->type_name, device->name);
speaker_deselect_output(device);
continue;
}
DPRINTF(E_INFO, L_PLAYER, "Autoselecting %s device '%s'\n", device->type_name, device->name);
(*retval)++;
break;
}
// We're async if we need to start devices
if (*retval > 0)
return COMMAND_PENDING; // async
// Otherwise, just run the bottom half
*retval = 0;
return COMMAND_END;
}
static enum command_state
playback_start_id(void *arg, int *retval)
{
struct db_queue_item *queue_item = NULL;
union player_arg *cmdarg = arg;
enum command_state cmd_state;
int ret;
*retval = -1;
if (player_state == PLAY_STOPPED)
{
db_queue_clear(0);
ret = db_queue_add_by_fileid(cmdarg->id, 0, 0, -1, NULL, NULL);
if (ret < 0)
return COMMAND_END;
queue_item = db_queue_fetch_byfileid(cmdarg->id);
if (!queue_item)
return COMMAND_END;
}
cmd_state = playback_start_item(queue_item, retval);
free_queue_item(queue_item, 0);
return cmd_state;
}
static enum command_state
playback_start(void *arg, int *retval)
{
struct db_queue_item *queue_item = NULL;
enum command_state cmd_state;
*retval = -1;
if (player_state == PLAY_STOPPED)
{
// Start playback of first item in queue
queue_item = db_queue_fetch_bypos(0, shuffle);
if (!queue_item)
return COMMAND_END;
}
cmd_state = playback_start_item(queue_item, retval);
free_queue_item(queue_item, 0);
return cmd_state;
}
static enum command_state
playback_prev_bh(void *arg, int *retval)
{
int ret;
int pos_sec;
struct player_source *ps;
// The upper half is playback_pause, therefor the current playing item is
// already set as the cur_streaming (cur_playing is NULL).
if (!cur_streaming)
{
DPRINTF(E_LOG, L_PLAYER, "Could not get current stream source\n");
*retval = -1;
return COMMAND_END;
}
// Only add to history if playback started
if (cur_streaming->output_start > cur_streaming->stream_start)
history_add(cur_streaming->id, cur_streaming->item_id);
// Compute the playing time in seconds for the current song
if (cur_streaming->output_start > cur_streaming->stream_start)
pos_sec = (cur_streaming->output_start - cur_streaming->stream_start) / 44100;
else
pos_sec = 0;
// Only skip to the previous song if the playing time is less than 3 seconds,
// otherwise restart the current song.
DPRINTF(E_DBG, L_PLAYER, "Skipping song played %d sec\n", pos_sec);
if (pos_sec < 3)
{
ps = source_prev();
if (!ps)
{
playback_abort();
*retval = -1;
return COMMAND_END;
}
source_stop();
ret = source_open(ps, TEMP_NEXT_RTPTIME, 0);
if (ret < 0)
{
source_free(ps);
playback_abort();
*retval = -1;
return COMMAND_END;
}
}
else
{
ret = source_seek(0);
if (ret < 0)
{
playback_abort();
*retval = -1;
return COMMAND_END;
}
}
if (player_state == PLAY_STOPPED)
{
*retval = -1;
return COMMAND_END;
}
// Silent status change - playback_start() sends the real status update
player_state = PLAY_PAUSED;
*retval = 0;
return COMMAND_END;
}
static enum command_state
playback_next_bh(void *arg, int *retval)
{
struct player_source *ps;
int ret;
int id;
uint32_t item_id;
// The upper half is playback_pause, therefor the current playing item is
// already set as the cur_streaming (cur_playing is NULL).
if (!cur_streaming)
{
DPRINTF(E_LOG, L_PLAYER, "Could not get current stream source\n");
*retval = -1;
return COMMAND_END;
}
item_id = cur_streaming->item_id;
// Only add to history if playback started
if (cur_streaming->output_start > cur_streaming->stream_start)
{
history_add(cur_streaming->id, item_id);
id = (int)cur_streaming->id;
worker_execute(skipcount_inc_cb, &id, sizeof(int), 5);
}
ps = source_next();
if (!ps)
{
playback_abort();
*retval = -1;
return COMMAND_END;
}
source_stop();
ret = source_open(ps, TEMP_NEXT_RTPTIME, 0);
if (ret < 0)
{
source_free(ps);
playback_abort();
*retval = -1;
return COMMAND_END;
}
if (player_state == PLAY_STOPPED)
{
*retval = -1;
return COMMAND_END;
}
if (consume)
db_queue_delete_byitemid(item_id);
// Silent status change - playback_start() sends the real status update
player_state = PLAY_PAUSED;
*retval = 0;
return COMMAND_END;
}
static enum command_state
playback_seek_bh(void *arg, int *retval)
{
union player_arg *cmdarg = arg;
int ms;
int ret;
*retval = -1;
if (!cur_streaming)
return COMMAND_END;
ms = cmdarg->intval;
ret = source_seek(ms);
if (ret < 0)
{
playback_abort();
return COMMAND_END;
}
// Silent status change - playback_start() sends the real status update
player_state = PLAY_PAUSED;
*retval = 0;
return COMMAND_END;
}
static enum command_state
playback_pause_bh(void *arg, int *retval)
{
*retval = -1;
// outputs_flush() in playback_pause() may have a caused a failure callback
// from the output, which in streaming_cb() can cause playback_abort() ->
// cur_streaming is NULL
if (!cur_streaming)
return COMMAND_END;
if (cur_streaming->data_kind == DATA_KIND_HTTP || cur_streaming->data_kind == DATA_KIND_PIPE)
{
DPRINTF(E_DBG, L_PLAYER, "Source is not pausable, abort playback\n");
playback_abort();
return COMMAND_END;
}
status_update(PLAY_PAUSED);
seek_save();
*retval = 0;
return COMMAND_END;
}
static enum command_state
playback_pause(void *arg, int *retval)
{
uint64_t pos;
if (player_state == PLAY_STOPPED)
{
*retval = -1;
return COMMAND_END;
}
if (player_state == PLAY_PAUSED)
{
*retval = 0;
return COMMAND_END;
}
pos = source_check();
if (pos == 0)
{
DPRINTF(E_LOG, L_PLAYER, "Could not retrieve current position for pause\n");
playback_abort();
*retval = -1;
return COMMAND_END;
}
// Make sure playback is still running after source_check()
if (player_state == PLAY_STOPPED)
{
*retval = -1;
return COMMAND_END;
}
*retval = outputs_flush2(device_command_cb);
playback_timer_stop();
source_pause(pos);
outputs_metadata_purge();
// We're async if we need to flush devices
if (*retval > 0)
return COMMAND_PENDING; // async
// Otherwise, just run the bottom half
return COMMAND_END;
}
/*
* Notify of speaker/device changes
*/
void
player_speaker_status_trigger(void)
{
listener_notify(LISTENER_SPEAKER);
}
static void
device_to_speaker_info(struct player_speaker_info *spk, struct output_device *device)
{
memset(spk, 0, sizeof(struct player_speaker_info));
spk->id = device->id;
strncpy(spk->name, device->name, sizeof(spk->name));
spk->name[sizeof(spk->name) - 1] = '\0';
strncpy(spk->output_type, device->type_name, sizeof(spk->output_type));
spk->output_type[sizeof(spk->output_type) - 1] = '\0';
spk->relvol = device->relvol;
spk->absvol = device->volume;
spk->selected = device->selected;
spk->has_password = device->has_password;
spk->has_video = device->has_video;
spk->requires_auth = device->requires_auth;
spk->needs_auth_key = (device->requires_auth && device->auth_key == NULL);
}
static enum command_state
speaker_enumerate(void *arg, int *retval)
{
struct spk_enum *spk_enum = arg;
struct output_device *device;
struct player_speaker_info spk;
for (device = dev_list; device; device = device->next)
{
if (device->advertised || device->selected)
{
device_to_speaker_info(&spk, device);
spk_enum->cb(&spk, spk_enum->arg);
}
}
*retval = 0;
return COMMAND_END;
}
static enum command_state
speaker_get_byid(void *arg, int *retval)
{
struct speaker_get_param *spk_param = arg;
struct output_device *device;
for (device = dev_list; device; device = device->next)
{
if ((device->advertised || device->selected)
&& device->id == spk_param->spk_id)
{
device_to_speaker_info(spk_param->spk_info, device);
*retval = 0;
return COMMAND_END;
}
}
// No output device found with matching id
*retval = -1;
return COMMAND_END;
}
static int
speaker_activate(struct output_device *device)
{
int ret;
if (device->has_password && !device->password)
{
DPRINTF(E_INFO, L_PLAYER, "The %s device '%s' is password-protected, but we don't have it\n", device->type_name, device->name);
return -2;
}
DPRINTF(E_DBG, L_PLAYER, "The %s device '%s' is selected\n", device->type_name, device->name);
if (!device->selected)
speaker_select_output(device);
if (device->session)
return 0;
if (player_state == PLAY_PLAYING)
{
DPRINTF(E_DBG, L_PLAYER, "Activating %s device '%s'\n", device->type_name, device->name);
ret = outputs_device_start2(device, device_activate_cb);
if (ret < 0)
{
DPRINTF(E_LOG, L_PLAYER, "Could not start %s device '%s'\n", device->type_name, device->name);
goto error;
}
}
else
{
DPRINTF(E_DBG, L_PLAYER, "Probing %s device '%s'\n", device->type_name, device->name);
ret = outputs_device_probe(device, device_probe_cb);
if (ret < 0)
{
DPRINTF(E_LOG, L_PLAYER, "Could not probe %s device '%s'\n", device->type_name, device->name);
goto error;
}
}
return 0;
error:
DPRINTF(E_LOG, L_PLAYER, "Could not activate %s device '%s'\n", device->type_name, device->name);
speaker_deselect_output(device);
return -1;
}
static int
speaker_deactivate(struct output_device *device)
{
DPRINTF(E_DBG, L_PLAYER, "Deactivating %s device '%s'\n", device->type_name, device->name);
if (device->selected)
speaker_deselect_output(device);
if (!device->session)
return 0;
outputs_status_cb(device->session, device_shutdown_cb);
outputs_device_stop(device->session);
return 1;
}
static enum command_state
speaker_set(void *arg, int *retval)
{
struct speaker_set_param *speaker_set_param = arg;
struct output_device *device;
uint64_t *ids;
int nspk;
int i;
int ret;
*retval = 0;
ids = speaker_set_param->device_ids;
if (ids)
nspk = ids[0];
else
nspk = 0;
DPRINTF(E_DBG, L_PLAYER, "Speaker set: %d speakers\n", nspk);
*retval = 0;
for (device = dev_list; device; device = device->next)
{
for (i = 1; i <= nspk; i++)
{
DPRINTF(E_DBG, L_PLAYER, "Set %" PRIu64 " device %" PRIu64 "\n", ids[i], device->id);
if (ids[i] == device->id)
break;
}
if (i <= nspk)
{
ret = speaker_activate(device);
if (ret > 0)
(*retval)++;
else if (ret < 0 && speaker_set_param->intval != -2)
speaker_set_param->intval = ret;
}
else
{
ret = speaker_deactivate(device);
if (ret > 0)
(*retval)++;
}
}
if (*retval > 0)
return COMMAND_PENDING; // async
*retval = speaker_set_param->intval;
return COMMAND_END;
}
static enum command_state
speaker_enable(void *arg, int *retval)
{
uint64_t *id = arg;
struct output_device *device;
*retval = 0;
DPRINTF(E_DBG, L_PLAYER, "Speaker enable: %" PRIu64 "\n", *id);
*retval = 0;
for (device = dev_list; device; device = device->next)
{
if (*id == device->id)
{
*retval = speaker_activate(device);
break;
}
}
if (*retval > 0)
return COMMAND_PENDING; // async
return COMMAND_END;
}
static enum command_state
speaker_disable(void *arg, int *retval)
{
uint64_t *id = arg;
struct output_device *device;
*retval = 0;
DPRINTF(E_DBG, L_PLAYER, "Speaker disable: %" PRIu64 "\n", *id);
*retval = 0;
for (device = dev_list; device; device = device->next)
{
if (*id == device->id)
{
*retval = speaker_deactivate(device);
break;
}
}
if (*retval > 0)
return COMMAND_PENDING; // async
return COMMAND_END;
}
static enum command_state
volume_set(void *arg, int *retval)
{
union player_arg *cmdarg = arg;
struct output_device *device;
int volume;
*retval = 0;
volume = cmdarg->intval;
if (master_volume == volume)
return COMMAND_END;
master_volume = volume;
for (device = dev_list; device; device = device->next)
{
if (!device->selected)
continue;
device->volume = rel_to_vol(device->relvol);
#ifdef DEBUG_RELVOL
DPRINTF(E_DBG, L_PLAYER, "*** %s: abs %d rel %d\n", device->name, device->volume, device->relvol);
#endif
if (device->session)
*retval += outputs_device_volume_set(device, device_command_cb);
}
listener_notify(LISTENER_VOLUME);
if (*retval > 0)
return COMMAND_PENDING; // async
return COMMAND_END;
}
#ifdef DEBUG_RELVOL
static void debug_print_speaker()
{
struct output_device *device;
DPRINTF(E_DBG, L_PLAYER, "*** Master: %d\n", master_volume);
for (device = dev_list; device; device = device->next)
{
if (!device->selected)
continue;
DPRINTF(E_DBG, L_PLAYER, "*** %s: abs %d rel %d\n", device->name, device->volume, device->relvol);
}
}
#endif
static enum command_state
volume_setrel_speaker(void *arg, int *retval)
{
struct volume_param *vol_param = arg;
struct output_device *device;
uint64_t id;
int relvol;
*retval = 0;
id = vol_param->spk_id;
relvol = vol_param->volume;
for (device = dev_list; device; device = device->next)
{
if (device->id != id)
continue;
if (!device->selected)
{
*retval = 0;
return COMMAND_END;
}
device->relvol = relvol;
device->volume = rel_to_vol(relvol);
#ifdef DEBUG_RELVOL
DPRINTF(E_DBG, L_PLAYER, "*** %s: abs %d rel %d\n", device->name, device->volume, device->relvol);
#endif
if (device->session)
*retval = outputs_device_volume_set(device, device_command_cb);
break;
}
volume_master_find();
#ifdef DEBUG_RELVOL
debug_print_speaker();
#endif
listener_notify(LISTENER_VOLUME);
if (*retval > 0)
return COMMAND_PENDING; // async
return COMMAND_END;
}
static enum command_state
volume_setabs_speaker(void *arg, int *retval)
{
struct volume_param *vol_param = arg;
struct output_device *device;
uint64_t id;
int volume;
*retval = 0;
id = vol_param->spk_id;
volume = vol_param->volume;
master_volume = volume;
for (device = dev_list; device; device = device->next)
{
if (!device->selected)
continue;
if (device->id != id)
{
device->relvol = vol_to_rel(device->volume);
#ifdef DEBUG_RELVOL
DPRINTF(E_DBG, L_PLAYER, "*** %s: abs %d rel %d\n", device->name, device->volume, device->relvol);
#endif
continue;
}
else
{
device->relvol = 100;
device->volume = master_volume;
#ifdef DEBUG_RELVOL
DPRINTF(E_DBG, L_PLAYER, "*** %s: abs %d rel %d\n", device->name, device->volume, device->relvol);
#endif
if (device->session)
*retval = outputs_device_volume_set(device, device_command_cb);//FIXME Does this need to be += ?
}
}
volume_master_find();
#ifdef DEBUG_RELVOL
debug_print_speaker();
#endif
listener_notify(LISTENER_VOLUME);
if (*retval > 0)
return COMMAND_PENDING; // async
return COMMAND_END;
}
// Just updates internal volume params (does not make actual requests to the speaker)
static enum command_state
volume_byactiveremote(void *arg, int *retval)
{
struct activeremote_param *ar_param = arg;
struct output_device *device;
uint32_t activeremote;
int volume;
*retval = 0;
activeremote = ar_param->activeremote;
for (device = dev_list; device; device = device->next)
{
if ((uint32_t)device->id == activeremote)
break;
}
if (!device)
{
DPRINTF(E_LOG, L_DACP, "Could not find speaker with Active-Remote id %d\n", activeremote);
*retval = -1;
return COMMAND_END;
}
volume = outputs_device_volume_to_pct(device, ar_param->value); // Only converts
if (volume < 0)
{
DPRINTF(E_LOG, L_DACP, "Could not parse volume given by Active-Remote id %d\n", activeremote);
*retval = -1;
return COMMAND_END;
}
device->volume = volume;
volume_master_find();
#ifdef DEBUG_RELVOL
DPRINTF(E_DBG, L_PLAYER, "*** %s: abs %d rel %d\n", device->name, device->volume, device->relvol);
#endif
listener_notify(LISTENER_VOLUME);
return COMMAND_END;
}
static enum command_state
repeat_set(void *arg, int *retval)
{
enum repeat_mode *mode = arg;
if (*mode == repeat)
{
*retval = 0;
return COMMAND_END;
}
switch (*mode)
{
case REPEAT_OFF:
case REPEAT_SONG:
case REPEAT_ALL:
repeat = *mode;
break;
default:
DPRINTF(E_LOG, L_PLAYER, "Invalid repeat mode: %d\n", *mode);
*retval = -1;
return COMMAND_END;
}
listener_notify(LISTENER_OPTIONS);
*retval = 0;
return COMMAND_END;
}
static enum command_state
shuffle_set(void *arg, int *retval)
{
union player_arg *cmdarg = arg;
char new_shuffle;
uint32_t cur_id;
new_shuffle = (cmdarg->intval == 0) ? 0 : 1;
// Ignore unchanged shuffle mode requests
if (new_shuffle == shuffle)
goto out;
// Update queue and notify listeners
if (new_shuffle)
{
cur_id = cur_streaming ? cur_streaming->item_id : 0;
db_queue_reshuffle(cur_id);
}
else
{
db_queue_inc_version();
}
// Update shuffle mode and notify listeners
shuffle = new_shuffle;
listener_notify(LISTENER_OPTIONS);
out:
*retval = 0;
return COMMAND_END;
}
static enum command_state
consume_set(void *arg, int *retval)
{
union player_arg *cmdarg = arg;
consume = cmdarg->intval;
listener_notify(LISTENER_OPTIONS);
*retval = 0;
return COMMAND_END;
}
/*
* Removes all items from the history
*/
static enum command_state
playerqueue_clear_history(void *arg, int *retval)
{
memset(history, 0, sizeof(struct player_history));
cur_plversion++; // TODO [db_queue] need to update db queue version
listener_notify(LISTENER_QUEUE);
*retval = 0;
return COMMAND_END;
}
static enum command_state
playerqueue_plid(void *arg, int *retval)
{
union player_arg *cmdarg = arg;
cur_plid = cmdarg->id;
*retval = 0;
return COMMAND_END;
}
/* ------------------------------- Player API ------------------------------- */
int
player_get_current_pos(uint64_t *pos, struct timespec *ts, int commit)
{
uint64_t delta;
int ret;
ret = clock_gettime_with_res(CLOCK_MONOTONIC, ts, &player_timer_res);
if (ret < 0)
{
DPRINTF(E_LOG, L_PLAYER, "Couldn't get clock: %s\n", strerror(errno));
return -1;
}
delta = (ts->tv_sec - pb_pos_stamp.tv_sec) * 1000000 + (ts->tv_nsec - pb_pos_stamp.tv_nsec) / 1000;
#ifdef DEBUG_SYNC
DPRINTF(E_DBG, L_PLAYER, "Delta is %" PRIu64 " usec\n", delta);
#endif
delta = (delta * 44100) / 1000000;
#ifdef DEBUG_SYNC
DPRINTF(E_DBG, L_PLAYER, "Delta is %" PRIu64 " samples\n", delta);
#endif
*pos = pb_pos + delta;
if (commit)
{
pb_pos = *pos;
pb_pos_stamp.tv_sec = ts->tv_sec;
pb_pos_stamp.tv_nsec = ts->tv_nsec;
#ifdef DEBUG_SYNC
DPRINTF(E_DBG, L_PLAYER, "Pos: %" PRIu64 " (clock)\n", *pos);
#endif
}
return 0;
}
int
player_get_time(struct timespec *ts)
{
int ret;
ret = clock_gettime_with_res(CLOCK_MONOTONIC, ts, &player_timer_res);
if (ret < 0)
{
DPRINTF(E_LOG, L_PLAYER, "Couldn't get clock: %s\n", strerror(errno));
return -1;
}
return 0;
}
int
player_get_status(struct player_status *status)
{
int ret;
ret = commands_exec_sync(cmdbase, get_status, NULL, status);
return ret;
}
/* --------------------------- Thread: httpd (DACP) ------------------------- */
/*
* Stores the now playing media item dbmfi-id in the given id pointer.
*
* @param id Pointer will hold the playing item (dbmfi) id if the function returns 0
* @return 0 on success, -1 on failure (e. g. no playing item found)
*/
int
player_now_playing(uint32_t *id)
{
int ret;
ret = commands_exec_sync(cmdbase, now_playing, NULL, id);
return ret;
}
/*
* Starts/resumes playback
*
* Depending on the player state, this will either resume playing the current
* item (player is paused) or begin playing the queue from the beginning.
*
* If shuffle is set, the queue is reshuffled prior to starting playback.
*
* @return 0 if successful, -1 if an error occurred
*/
int
player_playback_start(void)
{
int ret;
ret = commands_exec_sync(cmdbase, playback_start, playback_start_bh, NULL);
return ret;
}
/*
* Starts/resumes playback of the given queue_item
*
* If shuffle is set, the queue is reshuffled prior to starting playback.
*
* If a pointer is given as argument "itemid", its value will be set to the playing item id.
*
* @param queue_item to start playing
* @return 0 if successful, -1 if an error occurred
*/
int
player_playback_start_byitem(struct db_queue_item *queue_item)
{
int ret;
ret = commands_exec_sync(cmdbase, playback_start_item, playback_start_bh, queue_item);
return ret;
}
int
player_playback_start_byid(uint32_t id)
{
union player_arg cmdarg;
int ret;
cmdarg.id = id;
ret = commands_exec_sync(cmdbase, playback_start_id, playback_start_bh, &cmdarg);
return ret;
}
int
player_playback_stop(void)
{
int ret;
ret = commands_exec_sync(cmdbase, playback_stop, NULL, NULL);
return ret;
}
int
player_playback_pause(void)
{
int ret;
ret = commands_exec_sync(cmdbase, playback_pause, playback_pause_bh, NULL);
return ret;
}
int
player_playback_seek(int ms)
{
union player_arg cmdarg;
int ret;
cmdarg.intval = ms;
ret = commands_exec_sync(cmdbase, playback_pause, playback_seek_bh, &cmdarg);
return ret;
}
int
player_playback_next(void)
{
int ret;
ret = commands_exec_sync(cmdbase, playback_pause, playback_next_bh, NULL);
return ret;
}
int
player_playback_prev(void)
{
int ret;
ret = commands_exec_sync(cmdbase, playback_pause, playback_prev_bh, NULL);
return ret;
}
void
player_speaker_enumerate(spk_enum_cb cb, void *arg)
{
struct spk_enum spk_enum;
spk_enum.cb = cb;
spk_enum.arg = arg;
commands_exec_sync(cmdbase, speaker_enumerate, NULL, &spk_enum);
}
int
player_speaker_set(uint64_t *ids)
{
struct speaker_set_param speaker_set_param;
int ret;
speaker_set_param.device_ids = ids;
speaker_set_param.intval = 0;
ret = commands_exec_sync(cmdbase, speaker_set, NULL, &speaker_set_param);
listener_notify(LISTENER_SPEAKER);
return ret;
}
int
player_speaker_get_byid(uint64_t id, struct player_speaker_info *spk)
{
struct speaker_get_param param;
int ret;
param.spk_id = id;
param.spk_info = spk;
ret = commands_exec_sync(cmdbase, speaker_get_byid, NULL, &param);
return ret;
}
int
player_speaker_enable(uint64_t id)
{
int ret;
ret = commands_exec_sync(cmdbase, speaker_enable, NULL, &id);
listener_notify(LISTENER_SPEAKER);
return ret;
}
int
player_speaker_disable(uint64_t id)
{
int ret;
ret = commands_exec_sync(cmdbase, speaker_disable, NULL, &id);
listener_notify(LISTENER_SPEAKER);
return ret;
}
int
player_volume_set(int vol)
{
union player_arg cmdarg;
int ret;
if (vol < 0 || vol > 100)
{
DPRINTF(E_LOG, L_PLAYER, "Volume (%d) for player_volume_set is out of range\n", vol);
return -1;
}
cmdarg.intval = vol;
ret = commands_exec_sync(cmdbase, volume_set, NULL, &cmdarg);
return ret;
}
int
player_volume_setrel_speaker(uint64_t id, int relvol)
{
struct volume_param vol_param;
int ret;
if (relvol < 0 || relvol > 100)
{
DPRINTF(E_LOG, L_PLAYER, "Volume (%d) for player_volume_setrel_speaker is out of range\n", relvol);
return -1;
}
vol_param.spk_id = id;
vol_param.volume = relvol;
ret = commands_exec_sync(cmdbase, volume_setrel_speaker, NULL, &vol_param);
return ret;
}
int
player_volume_setabs_speaker(uint64_t id, int vol)
{
struct volume_param vol_param;
int ret;
if (vol < 0 || vol > 100)
{
DPRINTF(E_LOG, L_PLAYER, "Volume (%d) for player_volume_setabs_speaker is out of range\n", vol);
return -1;
}
vol_param.spk_id = id;
vol_param.volume = vol;
ret = commands_exec_sync(cmdbase, volume_setabs_speaker, NULL, &vol_param);
return ret;
}
int
player_volume_byactiveremote(uint32_t activeremote, const char *value)
{
struct activeremote_param ar_param;
int ret;
ar_param.activeremote = activeremote;
ar_param.value = value;
ret = commands_exec_sync(cmdbase, volume_byactiveremote, NULL, &ar_param);
return ret;
}
int
player_repeat_set(enum repeat_mode mode)
{
int ret;
ret = commands_exec_sync(cmdbase, repeat_set, NULL, &mode);
return ret;
}
int
player_shuffle_set(int enable)
{
union player_arg cmdarg;
int ret;
cmdarg.intval = enable;
ret = commands_exec_sync(cmdbase, shuffle_set, NULL, &cmdarg);
return ret;
}
int
player_consume_set(int enable)
{
union player_arg cmdarg;
int ret;
cmdarg.intval = enable;
ret = commands_exec_sync(cmdbase, consume_set, NULL, &cmdarg);
return ret;
}
void
player_queue_clear_history()
{
commands_exec_sync(cmdbase, playerqueue_clear_history, NULL, NULL);
}
void
player_queue_plid(uint32_t plid)
{
union player_arg cmdarg;
cmdarg.id = plid;
commands_exec_sync(cmdbase, playerqueue_plid, NULL, &cmdarg);
}
struct player_history *
player_history_get(void)
{
return history;
}
/* ------------------- Non-blocking commands used by mDNS ------------------- */
int
player_device_add(void *device)
{
union player_arg *cmdarg;
int ret;
cmdarg = calloc(1, sizeof(union player_arg));
if (!cmdarg)
{
DPRINTF(E_LOG, L_PLAYER, "Could not allocate player_command\n");
return -1;
}
cmdarg->device = device;
ret = commands_exec_async(cmdbase, device_add, cmdarg);
return ret;
}
int
player_device_remove(void *device)
{
union player_arg *cmdarg;
int ret;
cmdarg = calloc(1, sizeof(union player_arg));
if (!cmdarg)
{
DPRINTF(E_LOG, L_PLAYER, "Could not allocate player_command\n");
return -1;
}
cmdarg->device = device;
ret = commands_exec_async(cmdbase, device_remove_family, cmdarg);
return ret;
}
static void
player_device_auth_kickoff(enum output_types type, char **arglist)
{
union player_arg *cmdarg;
cmdarg = calloc(1, sizeof(union player_arg));
if (!cmdarg)
{
DPRINTF(E_LOG, L_PLAYER, "Could not allocate player_command\n");
return;
}
cmdarg->auth.type = type;
memcpy(cmdarg->auth.pin, arglist[0], 4);
commands_exec_async(cmdbase, device_auth_kickoff, cmdarg);
}
/* --------------------------- Thread: filescanner -------------------------- */
void
player_raop_verification_kickoff(char **arglist)
{
player_device_auth_kickoff(OUTPUT_TYPE_RAOP, arglist);
}
/* ---------------------------- Thread: worker ------------------------------ */
void
player_metadata_send(void *imd, void *omd)
{
struct metadata_param metadata_param;
metadata_param.input = imd;
metadata_param.output = omd;
commands_exec_sync(cmdbase, device_metadata_send, NULL, &metadata_param);
}
/* ---------------------------- Thread: player ------------------------------ */
static void *
player(void *arg)
{
struct output_device *device;
int ret;
ret = db_perthread_init();
if (ret < 0)
{
DPRINTF(E_LOG, L_PLAYER, "Error: DB init failed\n");
pthread_exit(NULL);
}
event_base_dispatch(evbase_player);
if (!player_exit)
DPRINTF(E_LOG, L_PLAYER, "Player event loop terminated ahead of time!\n");
db_speaker_clear_all();
for (device = dev_list; device; device = device->next)
{
ret = db_speaker_save(device);
if (ret < 0)
DPRINTF(E_LOG, L_PLAYER, "Could not save state for %s device '%s'\n", device->type_name, device->name);
}
db_perthread_deinit();
pthread_exit(NULL);
}
/* ----------------------------- Thread: main ------------------------------- */
int
player_init(void)
{
struct media_quality default_quality = { 44100, 16, 2 };
uint64_t interval;
uint32_t rnd;
int ret;
speaker_autoselect = cfg_getbool(cfg_getsec(cfg, "general"), "speaker_autoselect");
clear_queue_on_stop_disabled = cfg_getbool(cfg_getsec(cfg, "mpd"), "clear_queue_on_stop_disable");
master_volume = -1;
player_state = PLAY_STOPPED;
repeat = REPEAT_OFF;
history = calloc(1, sizeof(struct player_history));
// Determine if the resolution of the system timer is > or < the size
// of an audio packet. NOTE: this assumes the system clock resolution
// is less than one second.
if (clock_getres(CLOCK_MONOTONIC, &player_timer_res) < 0)
{
DPRINTF(E_LOG, L_PLAYER, "Could not get the system timer resolution.\n");
return -1;
}
if (!cfg_getbool(cfg_getsec(cfg, "general"), "high_resolution_clock"))
{
DPRINTF(E_INFO, L_PLAYER, "High resolution clock not enabled on this system (res is %ld)\n", player_timer_res.tv_nsec);
player_timer_res.tv_nsec = 10 * PLAYER_TICK_INTERVAL * 1000000;
}
// Set the tick interval for the playback timer
interval = MAX(player_timer_res.tv_nsec, PLAYER_TICK_INTERVAL * 1000000);
player_tick_interval.tv_nsec = interval;
pb_write_deficit_max = (PLAYER_WRITE_BEHIND_MAX * 1000000 / interval);
pb_read_deficit_max = (PLAYER_READ_BEHIND_MAX * 1000000 / interval);
// Create the playback timer
#ifdef HAVE_TIMERFD
pb_timer_fd = timerfd_create(CLOCK_MONOTONIC, TFD_CLOEXEC | TFD_NONBLOCK);
ret = pb_timer_fd;
#else
ret = timer_create(CLOCK_MONOTONIC, NULL, &pb_timer);
#endif
if (ret < 0)
{
DPRINTF(E_LOG, L_PLAYER, "Could not create playback timer: %s\n", strerror(errno));
return -1;
}
// Random RTP time start
gcry_randomize(&rnd, sizeof(rnd), GCRY_STRONG_RANDOM);
last_rtptime = ((uint64_t)1 << 32) | rnd;
evbase_player = event_base_new();
if (!evbase_player)
{
DPRINTF(E_LOG, L_PLAYER, "Could not create an event base\n");
goto evbase_fail;
}
#ifdef HAVE_TIMERFD
pb_timer_ev = event_new(evbase_player, pb_timer_fd, EV_READ | EV_PERSIST, playback_cb, NULL);
#else
pb_timer_ev = event_new(evbase_player, SIGALRM, EV_SIGNAL | EV_PERSIST, playback_cb, NULL);
#endif
if (!pb_timer_ev)
{
DPRINTF(E_LOG, L_PLAYER, "Could not create playback timer event\n");
goto evnew_fail;
}
session_init(&pb_session, &default_quality);
cmdbase = commands_base_new(evbase_player, NULL);
ret = outputs_init();
if (ret < 0)
{
DPRINTF(E_FATAL, L_PLAYER, "Output initiation failed\n");
goto outputs_fail;
}
ret = input_init();
if (ret < 0)
{
DPRINTF(E_FATAL, L_PLAYER, "Input initiation failed\n");
goto input_fail;
}
ret = pthread_create(&tid_player, NULL, player, NULL);
if (ret < 0)
{
DPRINTF(E_FATAL, L_PLAYER, "Could not spawn player thread: %s\n", strerror(errno));
goto thread_fail;
}
#if defined(HAVE_PTHREAD_SETNAME_NP)
pthread_setname_np(tid_player, "player");
#elif defined(HAVE_PTHREAD_SET_NAME_NP)
pthread_set_name_np(tid_player, "player");
#endif
return 0;
thread_fail:
input_deinit();
input_fail:
outputs_deinit();
outputs_fail:
commands_base_free(cmdbase);
session_deinit(&pb_session);
evnew_fail:
event_base_free(evbase_player);
evbase_fail:
#ifdef HAVE_TIMERFD
close(pb_timer_fd);
#else
timer_delete(pb_timer);
#endif
return -1;
}
void
player_deinit(void)
{
int ret;
player_playback_stop();
#ifdef HAVE_TIMERFD
close(pb_timer_fd);
#else
timer_delete(pb_timer);
#endif
input_deinit();
outputs_deinit();
player_exit = 1;
commands_base_destroy(cmdbase);
session_deinit(&pb_session);
ret = pthread_join(tid_player, NULL);
if (ret != 0)
{
DPRINTF(E_LOG, L_PLAYER, "Could not join player thread: %s\n", strerror(errno));
return;
}
free(history);
event_base_free(evbase_player);
}