mirror of
https://github.com/owntone/owntone-server.git
synced 2025-01-05 20:13:23 -05:00
0cb3881621
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.
3655 lines
80 KiB
C
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, ¶m);
|
|
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);
|
|
}
|