mirror of
https://github.com/owntone/owntone-server.git
synced 2025-01-13 16:03:23 -05:00
[player/input] Refactor - WIP
* Open input sources earlier * Gapless playback * Remove fixed 44100/16 from player * Complete restructure player internals
This commit is contained in:
parent
e97ad7d970
commit
87ca6363ae
@ -88,7 +88,7 @@ artworkapi_reply_nowplaying(struct httpd_request *hreq)
|
||||
if (ret != 0)
|
||||
return ret;
|
||||
|
||||
ret = player_now_playing(&id);
|
||||
ret = player_playing_now(&id);
|
||||
if (ret != 0)
|
||||
return HTTP_NOTFOUND;
|
||||
|
||||
|
@ -1083,7 +1083,7 @@ dacp_propset_userrating(const char *value, struct httpd_request *hreq)
|
||||
{
|
||||
DPRINTF(E_WARN, L_DACP, "Invalid id %d for rating, defaulting to player id\n", itemid);
|
||||
|
||||
ret = player_now_playing(&itemid);
|
||||
ret = player_playing_now(&itemid);
|
||||
if (ret < 0)
|
||||
{
|
||||
DPRINTF(E_WARN, L_DACP, "Could not find an id for rating\n");
|
||||
@ -2277,7 +2277,7 @@ dacp_reply_nowplayingartwork(struct httpd_request *hreq)
|
||||
goto error;
|
||||
}
|
||||
|
||||
ret = player_now_playing(&id);
|
||||
ret = player_playing_now(&id);
|
||||
if (ret < 0)
|
||||
goto no_artwork;
|
||||
|
||||
|
693
src/input.c
693
src/input.c
@ -22,6 +22,7 @@
|
||||
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <stdbool.h>
|
||||
#include <unistd.h>
|
||||
#include <string.h>
|
||||
#include <errno.h>
|
||||
@ -37,6 +38,7 @@
|
||||
|
||||
#include "misc.h"
|
||||
#include "logger.h"
|
||||
#include "commands.h"
|
||||
#include "input.h"
|
||||
|
||||
// Disallow further writes to the buffer when its size is larger than this threshold
|
||||
@ -100,10 +102,26 @@ struct input_buffer
|
||||
pthread_cond_t cond;
|
||||
};
|
||||
|
||||
struct input_arg
|
||||
{
|
||||
uint32_t item_id;
|
||||
int seek_ms;
|
||||
struct input_metadata *metadata;
|
||||
};
|
||||
|
||||
/* --- Globals --- */
|
||||
// Input thread
|
||||
static pthread_t tid_input;
|
||||
|
||||
// Event base, cmdbase and event we use to iterate in the playback loop
|
||||
static struct event_base *evbase_input;
|
||||
static struct commands_base *cmdbase;
|
||||
static struct event *inputev;
|
||||
static bool input_initialized;
|
||||
|
||||
// The source we are reading now
|
||||
static struct input_source input_now_reading;
|
||||
|
||||
// Input buffer
|
||||
static struct input_buffer input_buffer;
|
||||
|
||||
@ -115,7 +133,7 @@ static size_t debug_elapsed;
|
||||
#endif
|
||||
|
||||
|
||||
/* ------------------------------ MISC HELPERS ---------------------------- */
|
||||
/* ------------------------------- MISC HELPERS ----------------------------- */
|
||||
|
||||
static int
|
||||
map_data_kind(int data_kind)
|
||||
@ -142,14 +160,14 @@ map_data_kind(int data_kind)
|
||||
}
|
||||
|
||||
static void
|
||||
marker_add(short flags)
|
||||
marker_add(size_t pos, short flags)
|
||||
{
|
||||
struct marker *head;
|
||||
struct marker *marker;
|
||||
|
||||
CHECK_NULL(L_PLAYER, marker = calloc(1, sizeof(struct marker)));
|
||||
|
||||
marker->pos = input_buffer.bytes_written;
|
||||
marker->pos = pos;
|
||||
marker->quality = input_buffer.cur_write_quality;
|
||||
marker->flags = flags;
|
||||
|
||||
@ -162,88 +180,280 @@ marker_add(short flags)
|
||||
head->prev = marker;
|
||||
}
|
||||
|
||||
static int
|
||||
source_check_and_map(struct player_source *ps, const char *action, char check_setup)
|
||||
|
||||
/* ------------------------- INPUT SOURCE HANDLING -------------------------- */
|
||||
|
||||
static void
|
||||
clear(struct input_source *source)
|
||||
{
|
||||
int type;
|
||||
|
||||
#ifdef DEBUG
|
||||
DPRINTF(E_DBG, L_PLAYER, "Action is %s\n", action);
|
||||
#endif
|
||||
|
||||
if (!ps)
|
||||
{
|
||||
DPRINTF(E_LOG, L_PLAYER, "Stream %s called with invalid player source\n", action);
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (check_setup && !ps->setup_done)
|
||||
{
|
||||
DPRINTF(E_LOG, L_PLAYER, "Given player source not setup, %s not possible\n", action);
|
||||
return -1;
|
||||
}
|
||||
|
||||
type = map_data_kind(ps->data_kind);
|
||||
if (type < 0)
|
||||
{
|
||||
DPRINTF(E_LOG, L_PLAYER, "Unsupported input type, %s not possible\n", action);
|
||||
return -1;
|
||||
}
|
||||
|
||||
return type;
|
||||
free(source->path);
|
||||
memset(source, 0, sizeof(struct input_source));
|
||||
}
|
||||
|
||||
/* ----------------------------- PLAYBACK LOOP ---------------------------- */
|
||||
/* Thread: input */
|
||||
|
||||
// TODO Thread safety of ps?
|
||||
static void *
|
||||
playback(void *arg)
|
||||
static void
|
||||
flush(short *flags)
|
||||
{
|
||||
struct player_source *ps = arg;
|
||||
int type;
|
||||
int ret;
|
||||
|
||||
type = source_check_and_map(ps, "start", 1);
|
||||
if ((type < 0) || (inputs[type]->disabled))
|
||||
goto thread_exit;
|
||||
|
||||
// Loops until input_loop_break is set or no more input, e.g. EOF
|
||||
ret = inputs[type]->start(ps);
|
||||
if (ret < 0)
|
||||
input_write(NULL, NULL, INPUT_FLAG_ERROR);
|
||||
|
||||
#ifdef DEBUG
|
||||
DPRINTF(E_DBG, L_PLAYER, "Playback loop stopped (break is %d, ret %d)\n", input_loop_break, ret);
|
||||
#endif
|
||||
|
||||
thread_exit:
|
||||
pthread_exit(NULL);
|
||||
}
|
||||
|
||||
void
|
||||
input_wait(void)
|
||||
{
|
||||
struct timespec ts;
|
||||
struct marker *marker;
|
||||
size_t len;
|
||||
|
||||
pthread_mutex_lock(&input_buffer.mutex);
|
||||
|
||||
ts = timespec_reltoabs(input_loop_timeout);
|
||||
pthread_cond_timedwait(&input_buffer.cond, &input_buffer.mutex, &ts);
|
||||
// We will return an OR of all the unread marker flags
|
||||
*flags = 0;
|
||||
for (marker = input_buffer.marker_tail; marker; marker = input_buffer.marker_tail)
|
||||
{
|
||||
*flags |= marker->flags;
|
||||
input_buffer.marker_tail = marker->prev;
|
||||
free(marker);
|
||||
}
|
||||
|
||||
len = evbuffer_get_length(input_buffer.evbuf);
|
||||
|
||||
evbuffer_drain(input_buffer.evbuf, len);
|
||||
|
||||
memset(&input_buffer.cur_read_quality, 0, sizeof(struct media_quality));
|
||||
memset(&input_buffer.cur_write_quality, 0, sizeof(struct media_quality));
|
||||
|
||||
input_buffer.bytes_read = 0;
|
||||
input_buffer.bytes_written = 0;
|
||||
|
||||
input_buffer.full_cb = NULL;
|
||||
|
||||
pthread_mutex_unlock(&input_buffer.mutex);
|
||||
|
||||
#ifdef DEBUG
|
||||
DPRINTF(E_DBG, L_PLAYER, "Flushing %zu bytes with flags %d\n", len, *flags);
|
||||
#endif
|
||||
}
|
||||
|
||||
static enum command_state
|
||||
stop(void *arg, int *retval)
|
||||
{
|
||||
short flags;
|
||||
int type;
|
||||
|
||||
event_del(inputev);
|
||||
|
||||
type = input_now_reading.type;
|
||||
|
||||
if (inputs[type]->stop)
|
||||
inputs[type]->stop(&input_now_reading);
|
||||
|
||||
flush(&flags);
|
||||
|
||||
clear(&input_now_reading);
|
||||
|
||||
*retval = 0;
|
||||
return COMMAND_END;
|
||||
}
|
||||
|
||||
static int
|
||||
seek(struct input_source *source, int seek_ms)
|
||||
{
|
||||
if (seek_ms > 0 && inputs[source->type]->seek)
|
||||
return inputs[source->type]->seek(source, seek_ms);
|
||||
else
|
||||
return 0;
|
||||
}
|
||||
|
||||
// On error returns -1, on success + seek given + seekable returns the position
|
||||
// that the seek gave us, otherwise returns 0.
|
||||
static int
|
||||
setup(struct input_source *source, struct db_queue_item *queue_item, int seek_ms)
|
||||
{
|
||||
int type;
|
||||
int ret;
|
||||
|
||||
type = map_data_kind(queue_item->data_kind);
|
||||
if ((type < 0) || (inputs[type]->disabled))
|
||||
goto setup_error;
|
||||
|
||||
source->type = type;
|
||||
source->data_kind = queue_item->data_kind;
|
||||
source->media_kind = queue_item->media_kind;
|
||||
source->item_id = queue_item->id;
|
||||
source->id = queue_item->file_id;
|
||||
source->len_ms = queue_item->song_length;
|
||||
source->path = safe_strdup(queue_item->path);
|
||||
|
||||
DPRINTF(E_DBG, L_PLAYER, "Setting up input item '%s' (item id %" PRIu32 ")\n", source->path, source->item_id);
|
||||
|
||||
if (inputs[type]->setup)
|
||||
{
|
||||
ret = inputs[type]->setup(source);
|
||||
if (ret < 0)
|
||||
goto setup_error;
|
||||
}
|
||||
|
||||
source->open = true;
|
||||
|
||||
ret = seek(source, seek_ms);
|
||||
if (ret < 0)
|
||||
goto seek_error;
|
||||
|
||||
return ret;
|
||||
|
||||
seek_error:
|
||||
stop(NULL, NULL);
|
||||
setup_error:
|
||||
clear(source);
|
||||
return -1;
|
||||
}
|
||||
|
||||
static enum command_state
|
||||
start(void *arg, int *retval)
|
||||
{
|
||||
struct input_arg *cmdarg = arg;
|
||||
struct db_queue_item *queue_item;
|
||||
short flags;
|
||||
int ret;
|
||||
|
||||
// If we are asked to start the item that is currently open we can just seek
|
||||
if (input_now_reading.open && cmdarg->item_id == input_now_reading.item_id)
|
||||
{
|
||||
flush(&flags);
|
||||
|
||||
ret = seek(&input_now_reading, cmdarg->seek_ms);
|
||||
if (ret < 0)
|
||||
DPRINTF(E_WARN, L_PLAYER, "Ignoring failed seek to %d ms in '%s'\n", cmdarg->seek_ms, input_now_reading.path);
|
||||
}
|
||||
else
|
||||
{
|
||||
if (input_now_reading.open)
|
||||
stop(NULL, NULL);
|
||||
|
||||
// Get the queue_item from the db
|
||||
queue_item = db_queue_fetch_byitemid(cmdarg->item_id);
|
||||
if (!queue_item)
|
||||
{
|
||||
DPRINTF(E_LOG, L_PLAYER, "Input start was called with an item id that has disappeared (id=%d)\n", cmdarg->item_id);
|
||||
goto error;
|
||||
}
|
||||
|
||||
ret = setup(&input_now_reading, queue_item, cmdarg->seek_ms);
|
||||
free_queue_item(queue_item, 0);
|
||||
if (ret < 0)
|
||||
goto error;
|
||||
}
|
||||
|
||||
DPRINTF(E_DBG, L_PLAYER, "Starting input read loop for item '%s' (item id %" PRIu32 "), seek %d\n",
|
||||
input_now_reading.path, input_now_reading.item_id, cmdarg->seek_ms);
|
||||
|
||||
event_active(inputev, 0, 0);
|
||||
|
||||
*retval = ret; // Return is the seek result
|
||||
return COMMAND_END;
|
||||
|
||||
error:
|
||||
input_write(NULL, NULL, INPUT_FLAG_ERROR);
|
||||
clear(&input_now_reading);
|
||||
*retval = -1;
|
||||
return COMMAND_END;
|
||||
}
|
||||
|
||||
/*
|
||||
static enum command_state
|
||||
next(void *arg, int *retval)
|
||||
{
|
||||
struct player_status status;
|
||||
struct db_queue_item *queue_item;
|
||||
uint32_t item_id;
|
||||
int type;
|
||||
int ret;
|
||||
|
||||
// We may have finished reading source way before end of playback, and we
|
||||
// don't want to proceed prematurely. So we wait until the input buffer is
|
||||
// below the write threshold.
|
||||
ret = input_wait();
|
||||
if (ret < 0)
|
||||
{
|
||||
input_next(); // Async call to ourselves
|
||||
return;
|
||||
}
|
||||
|
||||
item_id = input_now_reading.item_id;
|
||||
|
||||
// Cleans up the source that has ended/failed and clears input_now_reading
|
||||
stop(NULL, NULL);
|
||||
|
||||
player_get_status(&status);
|
||||
|
||||
// TODO what about repeat/repeat_all? Maybe move next() to player that can
|
||||
// just call input_start()
|
||||
|
||||
// Get the next queue_item from the db
|
||||
queue_item = db_queue_fetch_next(item_id, status.shuffle);
|
||||
if (!queue_item)
|
||||
{
|
||||
DPRINTF(E_DBG, L_PLAYER, "Reached end of playback queue\n");
|
||||
*retval = 0;
|
||||
return COMMAND_END;
|
||||
}
|
||||
|
||||
ret = setup(&input_now_reading, queue_item, 0);
|
||||
free_queue_item(queue_item, 0);
|
||||
if (ret < 0)
|
||||
goto error;
|
||||
|
||||
DPRINTF(E_DBG, L_PLAYER, "Continuing input read loop for item '%s' (item id %" PRIu32 ")\n", input_now_reading.path, input_now_reading.item_id);
|
||||
|
||||
event_active(inputev, 0, 0);
|
||||
|
||||
*retval = 0;
|
||||
return COMMAND_END;
|
||||
|
||||
error:
|
||||
input_write(NULL, NULL, INPUT_FLAG_ERROR);
|
||||
clear(&input_now_reading);
|
||||
*retval = -1;
|
||||
return COMMAND_END;
|
||||
}
|
||||
*/
|
||||
|
||||
static enum command_state
|
||||
metadata_get(void *arg, int *retval)
|
||||
{
|
||||
struct input_arg *cmdarg = arg;
|
||||
int type;
|
||||
|
||||
if (!input_now_reading.open)
|
||||
{
|
||||
DPRINTF(E_WARN, L_PLAYER, "Source is no longer available for input_metadata_get()\n");
|
||||
goto error;
|
||||
}
|
||||
|
||||
type = input_now_reading.type;
|
||||
if ((type < 0) || (inputs[type]->disabled))
|
||||
goto error;
|
||||
|
||||
if (inputs[type]->metadata_get)
|
||||
*retval = inputs[type]->metadata_get(cmdarg->metadata, &input_now_reading);
|
||||
else
|
||||
*retval = 0;
|
||||
|
||||
return COMMAND_END;
|
||||
|
||||
error:
|
||||
*retval = -1;
|
||||
return COMMAND_END;
|
||||
}
|
||||
|
||||
|
||||
/* ---------------------- Interface towards input backends ------------------ */
|
||||
/* Thread: input and spotify */
|
||||
|
||||
// Called by input modules from within the playback loop
|
||||
int
|
||||
input_write(struct evbuffer *evbuf, struct media_quality *quality, short flags)
|
||||
{
|
||||
struct timespec ts;
|
||||
bool read_end;
|
||||
int ret;
|
||||
|
||||
pthread_mutex_lock(&input_buffer.mutex);
|
||||
|
||||
while ( (!input_loop_break) && (evbuffer_get_length(input_buffer.evbuf) > INPUT_BUFFER_THRESHOLD) && evbuf )
|
||||
read_end = (flags & (INPUT_FLAG_EOF | INPUT_FLAG_ERROR));
|
||||
|
||||
if ((evbuffer_get_length(input_buffer.evbuf) > INPUT_BUFFER_THRESHOLD) && evbuf)
|
||||
{
|
||||
if (input_buffer.full_cb)
|
||||
{
|
||||
@ -251,26 +461,19 @@ input_write(struct evbuffer *evbuf, struct media_quality *quality, short flags)
|
||||
input_buffer.full_cb = NULL;
|
||||
}
|
||||
|
||||
if (flags & INPUT_FLAG_NONBLOCK)
|
||||
// In case of EOF or error the input is always allowed to write, even if the
|
||||
// buffer is full. There is no point in holding back the input in that case.
|
||||
if (!read_end)
|
||||
{
|
||||
pthread_mutex_unlock(&input_buffer.mutex);
|
||||
return EAGAIN;
|
||||
}
|
||||
|
||||
ts = timespec_reltoabs(input_loop_timeout);
|
||||
pthread_cond_timedwait(&input_buffer.cond, &input_buffer.mutex, &ts);
|
||||
}
|
||||
|
||||
if (input_loop_break)
|
||||
{
|
||||
pthread_mutex_unlock(&input_buffer.mutex);
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (quality && !quality_is_equal(quality, &input_buffer.cur_write_quality))
|
||||
{
|
||||
input_buffer.cur_write_quality = *quality;
|
||||
marker_add(INPUT_FLAG_QUALITY);
|
||||
marker_add(input_buffer.bytes_written, INPUT_FLAG_QUALITY);
|
||||
}
|
||||
|
||||
ret = 0;
|
||||
@ -280,24 +483,109 @@ input_write(struct evbuffer *evbuf, struct media_quality *quality, short flags)
|
||||
ret = evbuffer_add_buffer(input_buffer.evbuf, evbuf);
|
||||
if (ret < 0)
|
||||
{
|
||||
DPRINTF(E_LOG, L_PLAYER, "Error adding stream data to input buffer\n");
|
||||
DPRINTF(E_LOG, L_PLAYER, "Error adding stream data to input buffer, stopping\n");
|
||||
input_stop();
|
||||
flags |= INPUT_FLAG_ERROR;
|
||||
}
|
||||
}
|
||||
|
||||
// Note this marker is added at the post-write position, since EOF and ERROR
|
||||
// belong there. We never want to add a marker for the NONBLOCK flag.
|
||||
if (flags & ~INPUT_FLAG_NONBLOCK)
|
||||
marker_add(flags);
|
||||
if (flags)
|
||||
{
|
||||
if (input_buffer.bytes_written > INPUT_BUFFER_THRESHOLD)
|
||||
marker_add(input_buffer.bytes_written - INPUT_BUFFER_THRESHOLD, INPUT_FLAG_START_NEXT);
|
||||
else
|
||||
marker_add(input_buffer.bytes_written, INPUT_FLAG_START_NEXT);
|
||||
|
||||
// Note this marker is added at the post-write position, since EOF, error
|
||||
// and metadata belong there.
|
||||
marker_add(input_buffer.bytes_written, flags);
|
||||
}
|
||||
|
||||
pthread_mutex_unlock(&input_buffer.mutex);
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
int
|
||||
input_wait(void)
|
||||
{
|
||||
struct timespec ts;
|
||||
|
||||
/* -------------------- Interface towards player thread ------------------- */
|
||||
/* Thread: player */
|
||||
pthread_mutex_lock(&input_buffer.mutex);
|
||||
|
||||
ts = timespec_reltoabs(input_loop_timeout);
|
||||
pthread_cond_timedwait(&input_buffer.cond, &input_buffer.mutex, &ts);
|
||||
|
||||
// Is the buffer full?
|
||||
if (evbuffer_get_length(input_buffer.evbuf) > INPUT_BUFFER_THRESHOLD)
|
||||
{
|
||||
pthread_mutex_unlock(&input_buffer.mutex);
|
||||
return -1;
|
||||
}
|
||||
|
||||
pthread_mutex_unlock(&input_buffer.mutex);
|
||||
return 0;
|
||||
}
|
||||
|
||||
/*void
|
||||
input_next(void)
|
||||
{
|
||||
commands_exec_async(cmdbase, next, NULL);
|
||||
}*/
|
||||
|
||||
/* ---------------------------------- MAIN ---------------------------------- */
|
||||
/* Thread: input */
|
||||
|
||||
static void *
|
||||
input(void *arg)
|
||||
{
|
||||
int ret;
|
||||
|
||||
ret = db_perthread_init();
|
||||
if (ret < 0)
|
||||
{
|
||||
DPRINTF(E_LOG, L_MAIN, "Error: DB init failed (input thread)\n");
|
||||
pthread_exit(NULL);
|
||||
}
|
||||
|
||||
input_initialized = true;
|
||||
|
||||
event_base_dispatch(evbase_input);
|
||||
|
||||
if (input_initialized)
|
||||
{
|
||||
DPRINTF(E_LOG, L_MAIN, "Input event loop terminated ahead of time!\n");
|
||||
input_initialized = false;
|
||||
}
|
||||
|
||||
db_perthread_deinit();
|
||||
|
||||
pthread_exit(NULL);
|
||||
}
|
||||
|
||||
static void
|
||||
play(evutil_socket_t fd, short flags, void *arg)
|
||||
{
|
||||
struct timeval tv = { 0, 0 };
|
||||
int ret;
|
||||
|
||||
// Spotify runs in its own thread, so no reading is done by the input thread,
|
||||
// thus there is no reason to activate inputev
|
||||
if (!inputs[input_now_reading.type]->play)
|
||||
return;
|
||||
|
||||
// Return will be negative if there is an error or EOF. Here, we just don't
|
||||
// loop any more. input_write() will pass the message to the player.
|
||||
ret = inputs[input_now_reading.type]->play(&input_now_reading);
|
||||
if (ret < 0)
|
||||
return; // Error or EOF, so don't come back
|
||||
|
||||
event_add(inputev, &tv);
|
||||
}
|
||||
|
||||
|
||||
/* ---------------------- Interface towards player thread ------------------- */
|
||||
/* Thread: player */
|
||||
|
||||
int
|
||||
input_read(void *data, size_t size, short *flags)
|
||||
@ -307,12 +595,6 @@ input_read(void *data, size_t size, short *flags)
|
||||
|
||||
*flags = 0;
|
||||
|
||||
if (!tid_input)
|
||||
{
|
||||
DPRINTF(E_LOG, L_PLAYER, "Bug! Read called, but playback not running\n");
|
||||
return -1;
|
||||
}
|
||||
|
||||
pthread_mutex_lock(&input_buffer.mutex);
|
||||
|
||||
// First we check if there is a marker in the requested samples. If there is,
|
||||
@ -380,157 +662,40 @@ input_buffer_full_cb(input_cb cb)
|
||||
}
|
||||
|
||||
int
|
||||
input_setup(struct player_source *ps)
|
||||
input_seek(uint32_t item_id, int seek_ms)
|
||||
{
|
||||
int type;
|
||||
struct input_arg cmdarg;
|
||||
|
||||
type = source_check_and_map(ps, "setup", 0);
|
||||
if ((type < 0) || (inputs[type]->disabled))
|
||||
return -1;
|
||||
cmdarg.item_id = item_id;
|
||||
cmdarg.seek_ms = seek_ms;
|
||||
|
||||
if (!inputs[type]->setup)
|
||||
return 0;
|
||||
|
||||
return inputs[type]->setup(ps);
|
||||
return commands_exec_sync(cmdbase, start, NULL, &cmdarg);
|
||||
}
|
||||
|
||||
int
|
||||
input_start(struct player_source *ps)
|
||||
void
|
||||
input_start(uint32_t item_id)
|
||||
{
|
||||
int ret;
|
||||
struct input_arg *cmdarg;
|
||||
|
||||
if (tid_input)
|
||||
{
|
||||
DPRINTF(E_WARN, L_PLAYER, "Input start called, but playback already running\n");
|
||||
return 0;
|
||||
}
|
||||
CHECK_NULL(L_PLAYER, cmdarg = malloc(sizeof(struct input_arg)));
|
||||
|
||||
input_loop_break = 0;
|
||||
cmdarg->item_id = item_id;
|
||||
cmdarg->seek_ms = 0;
|
||||
|
||||
ret = pthread_create(&tid_input, NULL, playback, ps);
|
||||
if (ret < 0)
|
||||
{
|
||||
DPRINTF(E_LOG, L_PLAYER, "Could not spawn input thread: %s\n", strerror(errno));
|
||||
return -1;
|
||||
}
|
||||
|
||||
#if defined(HAVE_PTHREAD_SETNAME_NP)
|
||||
pthread_setname_np(tid_input, "input");
|
||||
#elif defined(HAVE_PTHREAD_SET_NAME_NP)
|
||||
pthread_set_name_np(tid_input, "input");
|
||||
#endif
|
||||
|
||||
return 0;
|
||||
commands_exec_async(cmdbase, start, cmdarg);
|
||||
}
|
||||
|
||||
int
|
||||
input_pause(struct player_source *ps)
|
||||
void
|
||||
input_stop(void)
|
||||
{
|
||||
short flags;
|
||||
int ret;
|
||||
|
||||
#ifdef DEBUG
|
||||
DPRINTF(E_DBG, L_PLAYER, "Pause called, stopping playback loop\n");
|
||||
#endif
|
||||
|
||||
if (!tid_input)
|
||||
return -1;
|
||||
|
||||
pthread_mutex_lock(&input_buffer.mutex);
|
||||
|
||||
input_loop_break = 1;
|
||||
|
||||
pthread_cond_signal(&input_buffer.cond);
|
||||
pthread_mutex_unlock(&input_buffer.mutex);
|
||||
|
||||
// TODO What if input thread is hanging waiting for source? Kill thread?
|
||||
ret = pthread_join(tid_input, NULL);
|
||||
if (ret != 0)
|
||||
{
|
||||
DPRINTF(E_LOG, L_PLAYER, "Could not join input thread: %s\n", strerror(errno));
|
||||
return -1;
|
||||
}
|
||||
|
||||
tid_input = 0;
|
||||
|
||||
input_flush(&flags);
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
int
|
||||
input_stop(struct player_source *ps)
|
||||
{
|
||||
int type;
|
||||
|
||||
if (tid_input)
|
||||
input_pause(ps);
|
||||
|
||||
if (!ps)
|
||||
return 0;
|
||||
|
||||
type = source_check_and_map(ps, "stop", 1);
|
||||
if ((type < 0) || (inputs[type]->disabled))
|
||||
return -1;
|
||||
|
||||
if (!inputs[type]->stop)
|
||||
return 0;
|
||||
|
||||
return inputs[type]->stop(ps);
|
||||
}
|
||||
|
||||
int
|
||||
input_seek(struct player_source *ps, int seek_ms)
|
||||
{
|
||||
int type;
|
||||
|
||||
type = source_check_and_map(ps, "seek", 1);
|
||||
if ((type < 0) || (inputs[type]->disabled))
|
||||
return -1;
|
||||
|
||||
if (!inputs[type]->seek)
|
||||
return 0;
|
||||
|
||||
if (tid_input)
|
||||
input_pause(ps);
|
||||
|
||||
return inputs[type]->seek(ps, seek_ms);
|
||||
commands_exec_async(cmdbase, stop, NULL);
|
||||
}
|
||||
|
||||
void
|
||||
input_flush(short *flags)
|
||||
{
|
||||
struct marker *marker;
|
||||
size_t len;
|
||||
|
||||
pthread_mutex_lock(&input_buffer.mutex);
|
||||
|
||||
// We will return an OR of all the unread marker flags
|
||||
*flags = 0;
|
||||
for (marker = input_buffer.marker_tail; marker; marker = input_buffer.marker_tail)
|
||||
{
|
||||
*flags |= marker->flags;
|
||||
input_buffer.marker_tail = marker->prev;
|
||||
free(marker);
|
||||
}
|
||||
|
||||
len = evbuffer_get_length(input_buffer.evbuf);
|
||||
|
||||
evbuffer_drain(input_buffer.evbuf, len);
|
||||
|
||||
memset(&input_buffer.cur_read_quality, 0, sizeof(struct media_quality));
|
||||
memset(&input_buffer.cur_write_quality, 0, sizeof(struct media_quality));
|
||||
|
||||
input_buffer.bytes_read = 0;
|
||||
input_buffer.bytes_written = 0;
|
||||
|
||||
input_buffer.full_cb = NULL;
|
||||
|
||||
pthread_mutex_unlock(&input_buffer.mutex);
|
||||
|
||||
#ifdef DEBUG
|
||||
DPRINTF(E_DBG, L_PLAYER, "Flushing %zu bytes with flags %d\n", len, *flags);
|
||||
#endif
|
||||
// Flush should be thread safe
|
||||
flush(flags);
|
||||
}
|
||||
|
||||
int
|
||||
@ -542,33 +707,13 @@ input_quality_get(struct media_quality *quality)
|
||||
}
|
||||
|
||||
int
|
||||
input_metadata_get(struct input_metadata *metadata, struct player_source *ps, int startup, uint64_t rtptime)
|
||||
input_metadata_get(struct input_metadata *metadata)
|
||||
{
|
||||
int type;
|
||||
struct input_arg cmdarg;
|
||||
|
||||
if (!metadata || !ps || !ps->stream_start || !ps->output_start)
|
||||
{
|
||||
DPRINTF(E_LOG, L_PLAYER, "Bug! Unhandled case in input_metadata_get()\n");
|
||||
return -1;
|
||||
}
|
||||
cmdarg.metadata = metadata;
|
||||
|
||||
memset(metadata, 0, sizeof(struct input_metadata));
|
||||
|
||||
metadata->item_id = ps->item_id;
|
||||
|
||||
metadata->startup = startup;
|
||||
metadata->offset = ps->output_start - ps->stream_start;
|
||||
metadata->rtptime = ps->stream_start;
|
||||
|
||||
// Note that the source may overwrite the above progress metadata
|
||||
type = source_check_and_map(ps, "metadata_get", 1);
|
||||
if ((type < 0) || (inputs[type]->disabled))
|
||||
return -1;
|
||||
|
||||
if (!inputs[type]->metadata_get)
|
||||
return 0;
|
||||
|
||||
return inputs[type]->metadata_get(metadata, ps, rtptime);
|
||||
return commands_exec_sync(cmdbase, metadata_get, NULL, &cmdarg);
|
||||
}
|
||||
|
||||
void
|
||||
@ -597,12 +742,9 @@ input_init(void)
|
||||
pthread_mutex_init(&input_buffer.mutex, NULL);
|
||||
pthread_cond_init(&input_buffer.cond, NULL);
|
||||
|
||||
input_buffer.evbuf = evbuffer_new();
|
||||
if (!input_buffer.evbuf)
|
||||
{
|
||||
DPRINTF(E_LOG, L_PLAYER, "Out of memory for input buffer\n");
|
||||
return -1;
|
||||
}
|
||||
CHECK_NULL(L_PLAYER, evbase_input = event_base_new());
|
||||
CHECK_NULL(L_PLAYER, input_buffer.evbuf = evbuffer_new());
|
||||
CHECK_NULL(L_PLAYER, inputev = event_new(evbase_input, -1, EV_PERSIST, play, NULL));
|
||||
|
||||
no_input = 1;
|
||||
for (i = 0; inputs[i]; i++)
|
||||
@ -610,7 +752,7 @@ input_init(void)
|
||||
if (inputs[i]->type != i)
|
||||
{
|
||||
DPRINTF(E_FATAL, L_PLAYER, "BUG! Input definitions are misaligned with input enum\n");
|
||||
return -1;
|
||||
goto input_fail;
|
||||
}
|
||||
|
||||
if (!inputs[i]->init)
|
||||
@ -627,17 +769,42 @@ input_init(void)
|
||||
}
|
||||
|
||||
if (no_input)
|
||||
return -1;
|
||||
goto input_fail;
|
||||
|
||||
cmdbase = commands_base_new(evbase_input, NULL);
|
||||
|
||||
ret = pthread_create(&tid_input, NULL, input, NULL);
|
||||
if (ret < 0)
|
||||
{
|
||||
DPRINTF(E_LOG, L_MAIN, "Could not spawn input thread: %s\n", strerror(errno));
|
||||
goto thread_fail;
|
||||
}
|
||||
|
||||
#if defined(HAVE_PTHREAD_SETNAME_NP)
|
||||
pthread_setname_np(tid_input, "input");
|
||||
#elif defined(HAVE_PTHREAD_SET_NAME_NP)
|
||||
pthread_set_name_np(tid_input, "input");
|
||||
#endif
|
||||
|
||||
return 0;
|
||||
|
||||
thread_fail:
|
||||
commands_base_free(cmdbase);
|
||||
input_fail:
|
||||
event_free(inputev);
|
||||
evbuffer_free(input_buffer.evbuf);
|
||||
event_base_free(evbase_input);
|
||||
return -1;
|
||||
}
|
||||
|
||||
void
|
||||
input_deinit(void)
|
||||
{
|
||||
int i;
|
||||
int ret;
|
||||
|
||||
input_stop(NULL);
|
||||
// TODO ok to do from here?
|
||||
input_stop();
|
||||
|
||||
for (i = 0; inputs[i]; i++)
|
||||
{
|
||||
@ -648,9 +815,21 @@ input_deinit(void)
|
||||
inputs[i]->deinit();
|
||||
}
|
||||
|
||||
input_initialized = false;
|
||||
commands_base_destroy(cmdbase);
|
||||
|
||||
ret = pthread_join(tid_input, NULL);
|
||||
if (ret != 0)
|
||||
{
|
||||
DPRINTF(E_FATAL, L_MAIN, "Could not join input thread: %s\n", strerror(errno));
|
||||
return;
|
||||
}
|
||||
|
||||
pthread_cond_destroy(&input_buffer.cond);
|
||||
pthread_mutex_destroy(&input_buffer.mutex);
|
||||
|
||||
event_free(inputev);
|
||||
evbuffer_free(input_buffer.evbuf);
|
||||
event_base_free(evbase_input);
|
||||
}
|
||||
|
||||
|
158
src/input.h
158
src/input.h
@ -6,8 +6,8 @@
|
||||
# include <config.h>
|
||||
#endif
|
||||
#include <event2/buffer.h>
|
||||
#include "db.h"
|
||||
#include "misc.h"
|
||||
#include "transcode.h"
|
||||
|
||||
// Must be in sync with inputs[] in input.c
|
||||
enum input_types
|
||||
@ -22,64 +22,49 @@ enum input_types
|
||||
|
||||
enum input_flags
|
||||
{
|
||||
// Write to input buffer must not block
|
||||
INPUT_FLAG_NONBLOCK = (1 << 0),
|
||||
// Flags that input is closing current source
|
||||
INPUT_FLAG_START_NEXT = (1 << 0),
|
||||
// Flags end of file
|
||||
INPUT_FLAG_EOF = (1 << 1),
|
||||
INPUT_FLAG_EOF = (1 << 1),
|
||||
// Flags error reading file
|
||||
INPUT_FLAG_ERROR = (1 << 2),
|
||||
INPUT_FLAG_ERROR = (1 << 2),
|
||||
// Flags possible new stream metadata
|
||||
INPUT_FLAG_METADATA = (1 << 3),
|
||||
INPUT_FLAG_METADATA = (1 << 3),
|
||||
// Flags new stream quality
|
||||
INPUT_FLAG_QUALITY = (1 << 4),
|
||||
INPUT_FLAG_QUALITY = (1 << 4),
|
||||
};
|
||||
|
||||
struct player_source
|
||||
struct input_source
|
||||
{
|
||||
/* Id of the file/item in the files database */
|
||||
uint32_t id;
|
||||
// Type of input
|
||||
enum input_types type;
|
||||
|
||||
/* Item-Id of the file/item in the queue */
|
||||
// Item-Id of the file/item in the queue
|
||||
uint32_t item_id;
|
||||
|
||||
/* Length of the file/item in milliseconds */
|
||||
// Id of the file/item in the files database
|
||||
uint32_t id;
|
||||
|
||||
// Length of the file/item in milliseconds
|
||||
uint32_t len_ms;
|
||||
|
||||
enum data_kind data_kind;
|
||||
enum media_kind media_kind;
|
||||
char *path;
|
||||
|
||||
/* Start time of the media item as rtp-time
|
||||
The stream-start is the rtp-time the media item did or would have
|
||||
started playing (after seek or pause), therefor the elapsed time of the
|
||||
media item is always:
|
||||
elapsed time = current rtptime - stream-start */
|
||||
uint64_t stream_start;
|
||||
// Flags that the input has been opened (i.e. needs to be closed)
|
||||
bool open;
|
||||
|
||||
/* Output start time of the media item as rtp-time
|
||||
The output start time is the rtp-time of the first audio packet send
|
||||
to the audio outputs.
|
||||
It differs from stream-start especially after a seek, where the first audio
|
||||
packet has the next rtp-time as output start and stream start becomes the
|
||||
rtp-time the media item would have been started playing if the seek did
|
||||
not happen. */
|
||||
uint64_t output_start;
|
||||
|
||||
/* End time of media item as rtp-time
|
||||
The end time is set if the reading (source_read) of the media item reached
|
||||
end of file, until then it is 0. */
|
||||
uint64_t end;
|
||||
|
||||
/* Opaque pointer to data that the input sets up when called with setup(), and
|
||||
* which is cleaned up by the input with stop()
|
||||
*/
|
||||
// The below is private data for the input backend. It is optional for the
|
||||
// backend to use, so nothing in the input or player should depend on it!
|
||||
//
|
||||
// Opaque pointer to data that the input backend sets up when start() is
|
||||
// called, and that is cleaned up by the backend when stop() is called
|
||||
void *input_ctx;
|
||||
|
||||
/* Input has completed setup of the source
|
||||
*/
|
||||
int setup_done;
|
||||
|
||||
struct player_source *play_next;
|
||||
// Private evbuf. Alloc'ed by backend at start() and free'd at stop()
|
||||
struct evbuffer *evbuf;
|
||||
// Private source quality storage
|
||||
struct media_quality quality;
|
||||
};
|
||||
|
||||
typedef int (*input_cb)(void);
|
||||
@ -90,6 +75,7 @@ struct input_metadata
|
||||
|
||||
int startup;
|
||||
|
||||
uint64_t start;
|
||||
uint64_t rtptime;
|
||||
uint64_t offset;
|
||||
|
||||
@ -115,55 +101,63 @@ struct input_definition
|
||||
char disabled;
|
||||
|
||||
// Prepare a playback session
|
||||
int (*setup)(struct player_source *ps);
|
||||
int (*setup)(struct input_source *source);
|
||||
|
||||
// Starts playback loop (must be defined)
|
||||
int (*start)(struct player_source *ps);
|
||||
// One iteration of the playback loop (= a read operation from source)
|
||||
int (*play)(struct input_source *source);
|
||||
|
||||
// Cleans up when playback loop has ended
|
||||
int (*stop)(struct player_source *ps);
|
||||
// Cleans up (only required when stopping source before it ends itself)
|
||||
int (*stop)(struct input_source *source);
|
||||
|
||||
// Changes the playback position
|
||||
int (*seek)(struct player_source *ps, int seek_ms);
|
||||
int (*seek)(struct input_source *source, int seek_ms);
|
||||
|
||||
// Return metadata
|
||||
int (*metadata_get)(struct input_metadata *metadata, struct player_source *ps, uint64_t rtptime);
|
||||
int (*metadata_get)(struct input_metadata *metadata, struct input_source *source);
|
||||
|
||||
// Initialization function called during startup
|
||||
int (*init)(void);
|
||||
|
||||
// Deinitialization function called at shutdown
|
||||
void (*deinit)(void);
|
||||
|
||||
};
|
||||
|
||||
/*
|
||||
* Input modules should use this to test if playback should end
|
||||
*/
|
||||
int input_loop_break;
|
||||
|
||||
/* ---------------------- Interface towards input backends ------------------ */
|
||||
/* Thread: input and spotify */
|
||||
|
||||
/*
|
||||
* Transfer stream data to the player's input buffer. Data must be PCM-LE
|
||||
* samples. The input evbuf will be drained on succesful write. This is to avoid
|
||||
* copying memory. If the player's input buffer is full the function will block
|
||||
* until the write can be made (unless INPUT_FILE_NONBLOCK is set).
|
||||
* copying memory.
|
||||
*
|
||||
* @in evbuf Raw PCM_LE audio data to write
|
||||
* @in evbuf Quality of the PCM (sample rate etc.)
|
||||
* @in flags One or more INPUT_FLAG_*
|
||||
* @return 0 on success, EAGAIN if buffer was full (and _NONBLOCK is set),
|
||||
* -1 on error
|
||||
* @return 0 on success, EAGAIN if buffer was full, -1 on error
|
||||
*/
|
||||
int
|
||||
input_write(struct evbuffer *evbuf, struct media_quality *quality, short flags);
|
||||
|
||||
/*
|
||||
* Input modules can use this to wait in the playback loop (like input_write()
|
||||
* would have done)
|
||||
* Input modules can use this to wait for the input_buffer to be ready for
|
||||
* writing. The wait is max INPUT_LOOP_TIMEOUT, which allows the event base to
|
||||
* loop and process pending commands once in a while.
|
||||
*/
|
||||
void
|
||||
int
|
||||
input_wait(void);
|
||||
|
||||
/*
|
||||
* Async switch to the next song in the queue. Mostly for internal use, but
|
||||
* might be relevant some day externally?
|
||||
*/
|
||||
//void
|
||||
//input_next(void);
|
||||
|
||||
|
||||
/* ---------------------- Interface towards player thread ------------------- */
|
||||
/* Thread: player */
|
||||
|
||||
/*
|
||||
* Move a chunk of stream data from the player's input buffer to an output
|
||||
* buffer. Should only be called by the player thread. Will not block.
|
||||
@ -186,39 +180,31 @@ void
|
||||
input_buffer_full_cb(input_cb cb);
|
||||
|
||||
/*
|
||||
* Initializes the given player source for playback
|
||||
* Tells the input to start, i.e. after calling this function the input buffer
|
||||
* will begin to fill up, and should be read periodically with input_read(). If
|
||||
* called while another item is still open, it will be closed and the input
|
||||
* buffer will be flushed. This operation blocks.
|
||||
*
|
||||
* @in item_id Queue item id to start playing
|
||||
* @in seek_ms Position to start playing
|
||||
* @return Actual seek position if seekable, 0 otherwise, -1 on error
|
||||
*/
|
||||
int
|
||||
input_setup(struct player_source *ps);
|
||||
input_seek(uint32_t item_id, int seek_ms);
|
||||
|
||||
/*
|
||||
* Tells the input to start or resume playback, i.e. after calling this function
|
||||
* the input buffer will begin to fill up, and should be read periodically with
|
||||
* input_read(). Before calling this input_setup() must have been called.
|
||||
* Same as input_seek(), just non-blocking and does not offer seek.
|
||||
*
|
||||
* @in item_id Queue item id to start playing
|
||||
*/
|
||||
int
|
||||
input_start(struct player_source *ps);
|
||||
void
|
||||
input_start(uint32_t item_id);
|
||||
|
||||
/*
|
||||
* Pauses playback of the given player source (stops playback loop) and flushes
|
||||
* the input buffer
|
||||
* Stops the input and clears everything. Flushes the input buffer.
|
||||
*/
|
||||
int
|
||||
input_pause(struct player_source *ps);
|
||||
|
||||
/*
|
||||
* Stops playback loop (if running), flushes input buffer and cleans up the
|
||||
* player source
|
||||
*/
|
||||
int
|
||||
input_stop(struct player_source *ps);
|
||||
|
||||
/*
|
||||
* Seeks playback position to seek_ms. Returns actual seek position, 0 on
|
||||
* unseekable, -1 on error. May block.
|
||||
*/
|
||||
int
|
||||
input_seek(struct player_source *ps, int seek_ms);
|
||||
void
|
||||
input_stop(void);
|
||||
|
||||
/*
|
||||
* Flush input buffer. Output flags will be the same as input_read().
|
||||
@ -236,7 +222,7 @@ input_quality_get(struct media_quality *quality);
|
||||
* Gets metadata from the input, returns 0 if metadata is set, otherwise -1
|
||||
*/
|
||||
int
|
||||
input_metadata_get(struct input_metadata *metadata, struct player_source *ps, int startup, uint64_t rtptime);
|
||||
input_metadata_get(struct input_metadata *metadata);
|
||||
|
||||
/*
|
||||
* Free the entire struct
|
||||
|
@ -30,96 +30,100 @@
|
||||
#include "input.h"
|
||||
|
||||
static int
|
||||
setup(struct player_source *ps)
|
||||
setup(struct input_source *source)
|
||||
{
|
||||
ps->input_ctx = transcode_setup(XCODE_PCM_NATIVE, ps->data_kind, ps->path, ps->len_ms, NULL);
|
||||
if (!ps->input_ctx)
|
||||
struct transcode_ctx *ctx;
|
||||
|
||||
ctx = transcode_setup(XCODE_PCM_NATIVE, source->data_kind, source->path, source->len_ms, NULL);
|
||||
if (!ctx)
|
||||
return -1;
|
||||
|
||||
ps->setup_done = 1;
|
||||
CHECK_NULL(L_PLAYER, source->evbuf = evbuffer_new());
|
||||
|
||||
source->quality.sample_rate = transcode_encode_query(ctx->encode_ctx, "sample_rate");
|
||||
source->quality.bits_per_sample = transcode_encode_query(ctx->encode_ctx, "bits_per_sample");
|
||||
source->quality.channels = transcode_encode_query(ctx->encode_ctx, "channels");
|
||||
|
||||
source->input_ctx = ctx;
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int
|
||||
setup_http(struct player_source *ps)
|
||||
setup_http(struct input_source *source)
|
||||
{
|
||||
char *url;
|
||||
|
||||
if (http_stream_setup(&url, ps->path) < 0)
|
||||
if (http_stream_setup(&url, source->path) < 0)
|
||||
return -1;
|
||||
|
||||
free(ps->path);
|
||||
ps->path = url;
|
||||
free(source->path);
|
||||
source->path = url;
|
||||
|
||||
return setup(ps);
|
||||
return setup(source);
|
||||
}
|
||||
|
||||
static int
|
||||
start(struct player_source *ps)
|
||||
stop(struct input_source *source)
|
||||
{
|
||||
struct transcode_ctx *ctx = ps->input_ctx;
|
||||
struct media_quality quality = { 0 };
|
||||
struct evbuffer *evbuf;
|
||||
short flags;
|
||||
int ret;
|
||||
int icy_timer;
|
||||
|
||||
evbuf = evbuffer_new();
|
||||
|
||||
quality.sample_rate = transcode_encode_query(ctx->encode_ctx, "sample_rate");
|
||||
quality.bits_per_sample = transcode_encode_query(ctx->encode_ctx, "bits_per_sample");
|
||||
quality.channels = transcode_encode_query(ctx->encode_ctx, "channels");
|
||||
|
||||
ret = -1;
|
||||
flags = 0;
|
||||
while (!input_loop_break && !(flags & INPUT_FLAG_EOF))
|
||||
{
|
||||
// We set "wanted" to 1 because the read size doesn't matter to us
|
||||
// TODO optimize?
|
||||
ret = transcode(evbuf, &icy_timer, ctx, 1);
|
||||
if (ret < 0)
|
||||
break;
|
||||
|
||||
flags = ((ret == 0) ? INPUT_FLAG_EOF : 0) |
|
||||
(icy_timer ? INPUT_FLAG_METADATA : 0);
|
||||
|
||||
ret = input_write(evbuf, &quality, flags);
|
||||
if (ret < 0)
|
||||
break;
|
||||
}
|
||||
|
||||
evbuffer_free(evbuf);
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
static int
|
||||
stop(struct player_source *ps)
|
||||
{
|
||||
struct transcode_ctx *ctx = ps->input_ctx;
|
||||
struct transcode_ctx *ctx = source->input_ctx;
|
||||
|
||||
transcode_cleanup(&ctx);
|
||||
evbuffer_free(source->evbuf);
|
||||
|
||||
ps->input_ctx = NULL;
|
||||
ps->setup_done = 0;
|
||||
source->input_ctx = NULL;
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int
|
||||
seek(struct player_source *ps, int seek_ms)
|
||||
play(struct input_source *source)
|
||||
{
|
||||
return transcode_seek(ps->input_ctx, seek_ms);
|
||||
struct transcode_ctx *ctx = source->input_ctx;
|
||||
int icy_timer;
|
||||
int ret;
|
||||
short flags;
|
||||
|
||||
ret = input_wait();
|
||||
if (ret < 0)
|
||||
return 0; // Loop, input_buffer is not ready for writing
|
||||
|
||||
// We set "wanted" to 1 because the read size doesn't matter to us
|
||||
// TODO optimize?
|
||||
ret = transcode(source->evbuf, &icy_timer, ctx, 1);
|
||||
if (ret == 0)
|
||||
{
|
||||
input_write(source->evbuf, &source->quality, INPUT_FLAG_EOF);
|
||||
stop(source);
|
||||
return -1;
|
||||
}
|
||||
else if (ret < 0)
|
||||
{
|
||||
input_write(NULL, NULL, INPUT_FLAG_ERROR);
|
||||
stop(source);
|
||||
return -1;
|
||||
}
|
||||
|
||||
flags = (icy_timer ? INPUT_FLAG_METADATA : 0);
|
||||
|
||||
input_write(source->evbuf, &source->quality, flags);
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int
|
||||
metadata_get_http(struct input_metadata *metadata, struct player_source *ps, uint64_t rtptime)
|
||||
seek(struct input_source *source, int seek_ms)
|
||||
{
|
||||
return transcode_seek(source->input_ctx, seek_ms);
|
||||
}
|
||||
|
||||
static int
|
||||
metadata_get_http(struct input_metadata *metadata, struct input_source *source)
|
||||
{
|
||||
struct http_icy_metadata *m;
|
||||
int changed;
|
||||
|
||||
m = transcode_metadata(ps->input_ctx, &changed);
|
||||
m = transcode_metadata(source->input_ctx, &changed);
|
||||
if (!m)
|
||||
return -1;
|
||||
|
||||
@ -147,7 +151,7 @@ struct input_definition input_file =
|
||||
.type = INPUT_TYPE_FILE,
|
||||
.disabled = 0,
|
||||
.setup = setup,
|
||||
.start = start,
|
||||
.play = play,
|
||||
.stop = stop,
|
||||
.seek = seek,
|
||||
};
|
||||
@ -158,7 +162,7 @@ struct input_definition input_http =
|
||||
.type = INPUT_TYPE_HTTP,
|
||||
.disabled = 0,
|
||||
.setup = setup_http,
|
||||
.start = start,
|
||||
.play = play,
|
||||
.stop = stop,
|
||||
.metadata_get = metadata_get_http,
|
||||
};
|
||||
|
@ -125,7 +125,7 @@ static pthread_mutex_t pipe_metadata_lock;
|
||||
// True if there is new metadata to push to the player
|
||||
static bool pipe_metadata_is_new;
|
||||
|
||||
/* -------------------------------- HELPERS ------------------------------- */
|
||||
/* -------------------------------- HELPERS --------------------------------- */
|
||||
|
||||
static struct pipe *
|
||||
pipe_create(const char *path, int id, enum pipetype type, event_callback_fn cb)
|
||||
@ -500,8 +500,8 @@ pipe_metadata_parse(struct input_metadata *m, struct evbuffer *evbuf)
|
||||
}
|
||||
|
||||
|
||||
/* ----------------------------- PIPE WATCHING ---------------------------- */
|
||||
/* Thread: pipe */
|
||||
/* ------------------------------ PIPE WATCHING ----------------------------- */
|
||||
/* Thread: pipe */
|
||||
|
||||
// Some data arrived on a pipe we watch - let's autostart playback
|
||||
static void
|
||||
@ -617,8 +617,8 @@ pipe_thread_run(void *arg)
|
||||
}
|
||||
|
||||
|
||||
/* -------------------------- METADATA PIPE HANDLING ---------------------- */
|
||||
/* Thread: worker */
|
||||
/* --------------------------- METADATA PIPE HANDLING ----------------------- */
|
||||
/* Thread: worker */
|
||||
|
||||
static void
|
||||
pipe_metadata_watch_del(void *arg)
|
||||
@ -706,8 +706,8 @@ pipe_metadata_watch_add(void *arg)
|
||||
}
|
||||
|
||||
|
||||
/* ---------------------- PIPE WATCH THREAD START/STOP -------------------- */
|
||||
/* Thread: filescanner */
|
||||
/* ----------------------- PIPE WATCH THREAD START/STOP --------------------- */
|
||||
/* Thread: filescanner */
|
||||
|
||||
static void
|
||||
pipe_thread_start(void)
|
||||
@ -802,92 +802,46 @@ pipe_listener_cb(short event_mask)
|
||||
}
|
||||
|
||||
|
||||
/* -------------------------- PIPE INPUT INTERFACE ------------------------ */
|
||||
/* Thread: player/input */
|
||||
/* --------------------------- PIPE INPUT INTERFACE ------------------------- */
|
||||
/* Thread: input */
|
||||
|
||||
static int
|
||||
setup(struct player_source *ps)
|
||||
setup(struct input_source *source)
|
||||
{
|
||||
struct pipe *pipe;
|
||||
int fd;
|
||||
|
||||
fd = pipe_open(ps->path, 0);
|
||||
fd = pipe_open(source->path, 0);
|
||||
if (fd < 0)
|
||||
return -1;
|
||||
|
||||
CHECK_NULL(L_PLAYER, pipe = pipe_create(ps->path, ps->id, PIPE_PCM, NULL));
|
||||
CHECK_NULL(L_PLAYER, pipe = pipe_create(source->path, source->id, PIPE_PCM, NULL));
|
||||
CHECK_NULL(L_PLAYER, source->evbuf = evbuffer_new());
|
||||
|
||||
pipe->fd = fd;
|
||||
pipe->is_autostarted = (ps->id == pipe_autostart_id);
|
||||
pipe->is_autostarted = (source->id == pipe_autostart_id);
|
||||
|
||||
worker_execute(pipe_metadata_watch_add, ps->path, strlen(ps->path) + 1, 0);
|
||||
worker_execute(pipe_metadata_watch_add, source->path, strlen(source->path) + 1, 0);
|
||||
|
||||
ps->input_ctx = pipe;
|
||||
ps->setup_done = 1;
|
||||
source->input_ctx = pipe;
|
||||
|
||||
source->quality.sample_rate = pipe_sample_rate;
|
||||
source->quality.bits_per_sample = pipe_bits_per_sample;
|
||||
source->quality.channels = 2;
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int
|
||||
start(struct player_source *ps)
|
||||
stop(struct input_source *source)
|
||||
{
|
||||
struct pipe *pipe = ps->input_ctx;
|
||||
struct media_quality quality = { 0 };
|
||||
struct evbuffer *evbuf;
|
||||
short flags;
|
||||
int ret;
|
||||
|
||||
evbuf = evbuffer_new();
|
||||
if (!evbuf)
|
||||
{
|
||||
DPRINTF(E_LOG, L_PLAYER, "Out of memory for pipe evbuf\n");
|
||||
return -1;
|
||||
}
|
||||
|
||||
quality.sample_rate = pipe_sample_rate;
|
||||
quality.bits_per_sample = pipe_bits_per_sample;
|
||||
quality.channels = 2;
|
||||
|
||||
ret = -1;
|
||||
while (!input_loop_break)
|
||||
{
|
||||
ret = evbuffer_read(evbuf, pipe->fd, PIPE_READ_MAX);
|
||||
if ((ret == 0) && (pipe->is_autostarted))
|
||||
{
|
||||
input_write(evbuf, NULL, INPUT_FLAG_EOF); // Autostop
|
||||
break;
|
||||
}
|
||||
else if ((ret == 0) || ((ret < 0) && (errno == EAGAIN)))
|
||||
{
|
||||
input_wait();
|
||||
continue;
|
||||
}
|
||||
else if (ret < 0)
|
||||
{
|
||||
DPRINTF(E_LOG, L_PLAYER, "Could not read from pipe '%s': %s\n", ps->path, strerror(errno));
|
||||
break;
|
||||
}
|
||||
|
||||
flags = (pipe_metadata_is_new ? INPUT_FLAG_METADATA : 0);
|
||||
pipe_metadata_is_new = 0;
|
||||
|
||||
ret = input_write(evbuf, &quality, flags);
|
||||
if (ret < 0)
|
||||
break;
|
||||
}
|
||||
|
||||
evbuffer_free(evbuf);
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
static int
|
||||
stop(struct player_source *ps)
|
||||
{
|
||||
struct pipe *pipe = ps->input_ctx;
|
||||
struct pipe *pipe = source->input_ctx;
|
||||
union pipe_arg *cmdarg;
|
||||
|
||||
DPRINTF(E_DBG, L_PLAYER, "Stopping pipe\n");
|
||||
|
||||
evbuffer_free(source->evbuf);
|
||||
|
||||
pipe_close(pipe->fd);
|
||||
|
||||
// Reset the pipe and start watching it again for new data. Must be async or
|
||||
@ -903,14 +857,51 @@ stop(struct player_source *ps)
|
||||
|
||||
pipe_free(pipe);
|
||||
|
||||
ps->input_ctx = NULL;
|
||||
ps->setup_done = 0;
|
||||
source->input_ctx = NULL;
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int
|
||||
metadata_get(struct input_metadata *metadata, struct player_source *ps, uint64_t rtptime)
|
||||
play(struct input_source *source)
|
||||
{
|
||||
struct pipe *pipe = source->input_ctx;
|
||||
short flags;
|
||||
int ret;
|
||||
|
||||
ret = input_wait();
|
||||
if (ret < 0)
|
||||
return 0; // Loop, input_buffer is not ready for writing
|
||||
|
||||
ret = evbuffer_read(source->evbuf, pipe->fd, PIPE_READ_MAX);
|
||||
if ((ret == 0) && (pipe->is_autostarted))
|
||||
{
|
||||
input_write(source->evbuf, NULL, INPUT_FLAG_EOF); // Autostop
|
||||
stop(source);
|
||||
return -1;
|
||||
}
|
||||
else if ((ret == 0) || ((ret < 0) && (errno == EAGAIN)))
|
||||
{
|
||||
return 0; // Loop
|
||||
}
|
||||
else if (ret < 0)
|
||||
{
|
||||
DPRINTF(E_LOG, L_PLAYER, "Could not read from pipe '%s': %s\n", source->path, strerror(errno));
|
||||
input_write(NULL, NULL, INPUT_FLAG_ERROR);
|
||||
stop(source);
|
||||
return -1;
|
||||
}
|
||||
|
||||
flags = (pipe_metadata_is_new ? INPUT_FLAG_METADATA : 0);
|
||||
pipe_metadata_is_new = 0;
|
||||
|
||||
input_write(source->evbuf, &source->quality, flags);
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int
|
||||
metadata_get(struct input_metadata *metadata, struct input_source *source)
|
||||
{
|
||||
pthread_mutex_lock(&pipe_metadata_lock);
|
||||
|
||||
@ -927,8 +918,9 @@ metadata_get(struct input_metadata *metadata, struct player_source *ps, uint64_t
|
||||
|
||||
if (pipe_metadata_parsed.song_length)
|
||||
{
|
||||
if (rtptime > ps->stream_start)
|
||||
metadata->rtptime = rtptime - pipe_metadata_parsed.offset;
|
||||
// TODO this is probably broken
|
||||
if (metadata->rtptime > metadata->start)
|
||||
metadata->rtptime -= pipe_metadata_parsed.offset;
|
||||
metadata->offset = pipe_metadata_parsed.offset;
|
||||
metadata->song_length = pipe_metadata_parsed.song_length;
|
||||
}
|
||||
@ -988,7 +980,7 @@ struct input_definition input_pipe =
|
||||
.type = INPUT_TYPE_PIPE,
|
||||
.disabled = 0,
|
||||
.setup = setup,
|
||||
.start = start,
|
||||
.play = play,
|
||||
.stop = stop,
|
||||
.metadata_get = metadata_get,
|
||||
.init = init,
|
||||
|
@ -31,12 +31,12 @@
|
||||
#define SPOTIFY_SETUP_RETRY_WAIT 500000
|
||||
|
||||
static int
|
||||
setup(struct player_source *ps)
|
||||
setup(struct input_source *source)
|
||||
{
|
||||
int i = 0;
|
||||
int ret;
|
||||
|
||||
while((ret = spotify_playback_setup(ps->path)) == SPOTIFY_SETUP_ERROR_IS_LOADING)
|
||||
while((ret = spotify_playback_setup(source->path)) == SPOTIFY_SETUP_ERROR_IS_LOADING)
|
||||
{
|
||||
if (i >= SPOTIFY_SETUP_RETRIES)
|
||||
break;
|
||||
@ -49,32 +49,15 @@ setup(struct player_source *ps)
|
||||
if (ret < 0)
|
||||
return -1;
|
||||
|
||||
ps->setup_done = 1;
|
||||
ret = spotify_playback_play();
|
||||
if (ret < 0)
|
||||
return -1;
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int
|
||||
start(struct player_source *ps)
|
||||
{
|
||||
int ret;
|
||||
|
||||
ret = spotify_playback_play();
|
||||
if (ret < 0)
|
||||
return -1;
|
||||
|
||||
while (!input_loop_break)
|
||||
{
|
||||
input_wait();
|
||||
}
|
||||
|
||||
ret = spotify_playback_pause();
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
static int
|
||||
stop(struct player_source *ps)
|
||||
stop(struct input_source *source)
|
||||
{
|
||||
int ret;
|
||||
|
||||
@ -82,13 +65,11 @@ stop(struct player_source *ps)
|
||||
if (ret < 0)
|
||||
return -1;
|
||||
|
||||
ps->setup_done = 0;
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int
|
||||
seek(struct player_source *ps, int seek_ms)
|
||||
seek(struct input_source *source, int seek_ms)
|
||||
{
|
||||
int ret;
|
||||
|
||||
|
@ -21,6 +21,11 @@
|
||||
# define MIN(a, b) ((a < b) ? a : b)
|
||||
#endif
|
||||
|
||||
#ifndef MAX
|
||||
#define MAX(a, b) ((a > b) ? a : b)
|
||||
#endif
|
||||
|
||||
|
||||
// Remember to adjust quality_is_equal() if adding elements
|
||||
struct media_quality {
|
||||
int sample_rate;
|
||||
|
@ -2358,6 +2358,9 @@ raop_metadata_send_progress(struct raop_session *rs, struct evbuffer *evbuf, str
|
||||
return -1;
|
||||
}
|
||||
|
||||
DPRINTF(E_DBG, L_PLAYER, "Metadata send is start_time=%zu, start=%zu, display=%zu, current=%zu, end=%zu\n",
|
||||
rs->start_rtptime, rmd->start, rmd->start - delay, rmd->start + offset, rmd->end);
|
||||
|
||||
ret = raop_send_req_set_parameter(rs, evbuf, "text/parameters", NULL, raop_cb_metadata, "send_progress");
|
||||
if (ret < 0)
|
||||
DPRINTF(E_LOG, L_RAOP, "Could not send SET_PARAMETER progress request to '%s'\n", rs->devname);
|
||||
@ -3014,7 +3017,8 @@ packets_sync_send(struct raop_master_session *rms, struct timespec pts)
|
||||
// OUTPUTS_BUFFER_DURATION secs into the future. However, in the sync packet
|
||||
// we want to tell the device what it should be playing right now. So we give
|
||||
// it a cur_time where we subtract this duration.
|
||||
cur_stamp.ts.tv_sec = pts.tv_sec - OUTPUTS_BUFFER_DURATION;
|
||||
// TODO update comment to match reality
|
||||
cur_stamp.ts.tv_sec = pts.tv_sec;
|
||||
cur_stamp.ts.tv_nsec = pts.tv_nsec;
|
||||
|
||||
// The cur_pos will be the rtptime of the coming packet, minus
|
||||
|
1458
src/player.c
1458
src/player.c
File diff suppressed because it is too large
Load Diff
@ -76,7 +76,7 @@ int
|
||||
player_get_status(struct player_status *status);
|
||||
|
||||
int
|
||||
player_now_playing(uint32_t *id);
|
||||
player_playing_now(uint32_t *id);
|
||||
|
||||
void
|
||||
player_speaker_enumerate(spk_enum_cb cb, void *arg);
|
||||
|
@ -718,7 +718,6 @@ playback_eot(void *arg, int *retval)
|
||||
|
||||
g_state = SPOTIFY_STATE_STOPPING;
|
||||
|
||||
// TODO 1) This will block for a while, but perhaps ok?
|
||||
input_write(spotify_audio_buffer, NULL, INPUT_FLAG_EOF);
|
||||
|
||||
*retval = 0;
|
||||
@ -1042,7 +1041,7 @@ static int music_delivery(sp_session *sess, const sp_audioformat *format,
|
||||
// The input buffer only accepts writing when it is approaching depletion, and
|
||||
// because we use NONBLOCK it will just return if this is not the case. So in
|
||||
// most cases no actual write is made and spotify_audio_buffer will just grow.
|
||||
input_write(spotify_audio_buffer, &quality, INPUT_FLAG_NONBLOCK);
|
||||
input_write(spotify_audio_buffer, &quality, 0);
|
||||
|
||||
return num_frames;
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user