mirror of
https://github.com/owntone/owntone-server.git
synced 2025-02-24 20:09:15 -05:00
[input] Add input interface to player - WIP
This commit is contained in:
parent
c50b038397
commit
3e24f857fa
@ -108,6 +108,8 @@ forked_daapd_SOURCES = main.c \
|
||||
daap_query.c daap_query.h \
|
||||
player.c player.h \
|
||||
worker.c worker.h \
|
||||
input.h input.c \
|
||||
inputs/file_http.c \
|
||||
outputs.h outputs.c \
|
||||
outputs/raop.c outputs/streaming.c outputs/dummy.c outputs/fifo.c \
|
||||
$(ALSA_SRC) $(PULSEAUDIO_SRC) $(CHROMECAST_SRC) \
|
||||
|
470
src/input.c
Normal file
470
src/input.c
Normal file
@ -0,0 +1,470 @@
|
||||
/*
|
||||
* Copyright (C) 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
|
||||
*/
|
||||
|
||||
#ifdef HAVE_CONFIG_H
|
||||
# include <config.h>
|
||||
#endif
|
||||
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <unistd.h>
|
||||
#include <string.h>
|
||||
#include <errno.h>
|
||||
#include <stdint.h>
|
||||
#include <inttypes.h>
|
||||
|
||||
#include <event2/event.h>
|
||||
#include <event2/buffer.h>
|
||||
#include <pthread.h>
|
||||
#ifdef HAVE_PTHREAD_NP_H
|
||||
# include <pthread_np.h>
|
||||
#endif
|
||||
|
||||
#include "misc.h"
|
||||
#include "logger.h"
|
||||
#include "input.h"
|
||||
|
||||
// Disallow further writes to the buffer when its size is larger than this threshold
|
||||
#define INPUT_BUFFER_THRESHOLD STOB(44100)
|
||||
|
||||
#define DEBUG 1 //TODO disable
|
||||
|
||||
extern struct input_definition input_file;
|
||||
extern struct input_definition input_http;
|
||||
|
||||
// Must be in sync with enum input_types
|
||||
static struct input_definition *inputs[] = {
|
||||
&input_file,
|
||||
&input_http,
|
||||
NULL
|
||||
};
|
||||
|
||||
struct input_buffer
|
||||
{
|
||||
// Raw pcm stream data
|
||||
struct evbuffer *evbuf;
|
||||
|
||||
// If non-zero, remaining length of buffer until EOF
|
||||
size_t eof;
|
||||
// If non-zero, remaining length of buffer until (possible) new metadata
|
||||
size_t metadata;
|
||||
|
||||
// Locks for sharing the buffer between input and player thread
|
||||
pthread_mutex_t mutex;
|
||||
pthread_cond_t cond;
|
||||
};
|
||||
|
||||
/* --- Globals --- */
|
||||
// Input thread
|
||||
static pthread_t tid_input;
|
||||
|
||||
// Input buffer
|
||||
static struct input_buffer input_buffer;
|
||||
|
||||
#ifdef DEBUG
|
||||
static size_t debug_elapsed;
|
||||
#endif
|
||||
|
||||
|
||||
/* ------------------------------ MISC HELPERS ---------------------------- */
|
||||
|
||||
static short
|
||||
flags_set(size_t len)
|
||||
{
|
||||
short flags = 0;
|
||||
|
||||
if (input_buffer.eof)
|
||||
{
|
||||
if (len >= input_buffer.eof)
|
||||
{
|
||||
flags |= INPUT_FLAG_EOF;
|
||||
input_buffer.eof = 0;
|
||||
}
|
||||
else
|
||||
input_buffer.eof -= len;
|
||||
}
|
||||
|
||||
if (input_buffer.metadata)
|
||||
{
|
||||
if (len >= input_buffer.metadata)
|
||||
{
|
||||
flags |= INPUT_FLAG_METADATA;
|
||||
input_buffer.metadata = 0;
|
||||
}
|
||||
else
|
||||
input_buffer.metadata -= len;
|
||||
}
|
||||
|
||||
return flags;
|
||||
}
|
||||
|
||||
static int
|
||||
map_data_kind(int data_kind)
|
||||
{
|
||||
switch (data_kind)
|
||||
{
|
||||
case DATA_KIND_FILE:
|
||||
return INPUT_TYPE_FILE;
|
||||
|
||||
case DATA_KIND_HTTP:
|
||||
return INPUT_TYPE_HTTP;
|
||||
|
||||
default:
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
static int
|
||||
source_check_and_map(struct player_source *ps, const char *action, char check_setup)
|
||||
{
|
||||
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;
|
||||
}
|
||||
|
||||
/* ----------------------------- PLAYBACK LOOP ---------------------------- */
|
||||
/* Thread: input */
|
||||
|
||||
// TODO Thread safety of ps? Do we need the return of the loop?
|
||||
static void *
|
||||
playback(void *arg)
|
||||
{
|
||||
struct player_source *ps = arg;
|
||||
int type;
|
||||
|
||||
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
|
||||
inputs[type]->start(ps);
|
||||
|
||||
#ifdef DEBUG
|
||||
DPRINTF(E_DBG, L_PLAYER, "Playback loop stopped (break is %d)\n", input_loop_break);
|
||||
#endif
|
||||
|
||||
thread_exit:
|
||||
pthread_exit(NULL);
|
||||
}
|
||||
|
||||
// Called by input modules from within the playback loop
|
||||
int
|
||||
input_write(struct evbuffer *evbuf, short flags)
|
||||
{
|
||||
int ret;
|
||||
|
||||
pthread_mutex_lock(&input_buffer.mutex);
|
||||
|
||||
while ( (!input_loop_break) && (evbuffer_get_length(input_buffer.evbuf) > INPUT_BUFFER_THRESHOLD) )
|
||||
{
|
||||
if (flags & INPUT_FLAG_NONBLOCK)
|
||||
{
|
||||
pthread_mutex_unlock(&input_buffer.mutex);
|
||||
return EAGAIN;
|
||||
}
|
||||
|
||||
pthread_cond_wait(&input_buffer.cond, &input_buffer.mutex);
|
||||
|
||||
// TODO protect against infinite looping and waiting?
|
||||
}
|
||||
|
||||
if (!input_loop_break)
|
||||
{
|
||||
ret = evbuffer_add_buffer(input_buffer.evbuf, evbuf);
|
||||
if (ret < 0)
|
||||
DPRINTF(E_LOG, L_PLAYER, "Error adding stream data to input buffer\n");
|
||||
|
||||
if (!input_buffer.eof && (flags & INPUT_FLAG_EOF))
|
||||
input_buffer.eof = evbuffer_get_length(input_buffer.evbuf);
|
||||
if (!input_buffer.metadata && (flags & INPUT_FLAG_METADATA))
|
||||
input_buffer.metadata = evbuffer_get_length(input_buffer.evbuf);
|
||||
}
|
||||
else
|
||||
ret = 0;
|
||||
|
||||
pthread_mutex_unlock(&input_buffer.mutex);
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
|
||||
/* -------------------- Interface towards player thread ------------------- */
|
||||
/* Thread: player */
|
||||
|
||||
int
|
||||
input_read(struct evbuffer *evbuf, size_t want, short *flags)
|
||||
{
|
||||
int len;
|
||||
|
||||
*flags = 0;
|
||||
|
||||
pthread_mutex_lock(&input_buffer.mutex);
|
||||
|
||||
#ifdef DEBUG
|
||||
debug_elapsed += want;
|
||||
if (debug_elapsed > STOB(441000)) // 10 sec
|
||||
{
|
||||
DPRINTF(E_DBG, L_PLAYER, "Input buffer has %zu bytes\n", evbuffer_get_length(input_buffer.evbuf));
|
||||
debug_elapsed = 0;
|
||||
}
|
||||
#endif
|
||||
|
||||
len = evbuffer_remove_buffer(input_buffer.evbuf, evbuf, want);
|
||||
if (len < 0)
|
||||
{
|
||||
DPRINTF(E_LOG, L_PLAYER, "Error reading stream data from input buffer\n");
|
||||
goto out_unlock;
|
||||
}
|
||||
|
||||
*flags = flags_set(len);
|
||||
|
||||
out_unlock:
|
||||
pthread_cond_signal(&input_buffer.cond);
|
||||
pthread_mutex_unlock(&input_buffer.mutex);
|
||||
|
||||
return len;
|
||||
}
|
||||
|
||||
int
|
||||
input_setup(struct player_source *ps)
|
||||
{
|
||||
int type;
|
||||
|
||||
type = source_check_and_map(ps, "setup", 0);
|
||||
if ((type < 0) || (inputs[type]->disabled))
|
||||
return -1;
|
||||
|
||||
if (!inputs[type]->setup)
|
||||
return 0;
|
||||
|
||||
return inputs[type]->setup(ps);
|
||||
}
|
||||
|
||||
int
|
||||
input_start(struct player_source *ps)
|
||||
{
|
||||
int ret;
|
||||
|
||||
if (tid_input)
|
||||
{
|
||||
DPRINTF(E_LOG, L_PLAYER, "Bug! Input start called, but playback already running\n");
|
||||
input_pause(ps);
|
||||
}
|
||||
|
||||
input_loop_break = 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;
|
||||
}
|
||||
|
||||
int
|
||||
input_pause(struct player_source *ps)
|
||||
{
|
||||
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);
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
void
|
||||
input_flush(short *flags)
|
||||
{
|
||||
size_t len;
|
||||
|
||||
pthread_mutex_lock(&input_buffer.mutex);
|
||||
|
||||
len = evbuffer_get_length(input_buffer.evbuf);
|
||||
|
||||
evbuffer_drain(input_buffer.evbuf, len);
|
||||
|
||||
*flags = flags_set(len);
|
||||
|
||||
input_buffer.eof = 0;
|
||||
input_buffer.metadata = 0;
|
||||
|
||||
pthread_mutex_unlock(&input_buffer.mutex);
|
||||
|
||||
#ifdef DEBUG
|
||||
DPRINTF(E_DBG, L_PLAYER, "Flush with flags %d\n", *flags);
|
||||
#endif
|
||||
}
|
||||
|
||||
int
|
||||
input_init(void)
|
||||
{
|
||||
int no_input;
|
||||
int ret;
|
||||
int i;
|
||||
|
||||
// Prepare input buffer
|
||||
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;
|
||||
}
|
||||
|
||||
no_input = 1;
|
||||
for (i = 0; inputs[i]; i++)
|
||||
{
|
||||
if (inputs[i]->type != i)
|
||||
{
|
||||
DPRINTF(E_FATAL, L_PLAYER, "BUG! Input definitions are misaligned with input enum\n");
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (!inputs[i]->init)
|
||||
{
|
||||
no_input = 0;
|
||||
continue;
|
||||
}
|
||||
|
||||
ret = inputs[i]->init();
|
||||
if (ret < 0)
|
||||
inputs[i]->disabled = 1;
|
||||
else
|
||||
no_input = 0;
|
||||
}
|
||||
|
||||
if (no_input)
|
||||
return -1;
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
void
|
||||
input_deinit(void)
|
||||
{
|
||||
int i;
|
||||
|
||||
input_stop(NULL);
|
||||
|
||||
for (i = 0; inputs[i]; i++)
|
||||
{
|
||||
if (inputs[i]->disabled)
|
||||
continue;
|
||||
|
||||
if (inputs[i]->deinit)
|
||||
inputs[i]->deinit();
|
||||
}
|
||||
|
||||
evbuffer_free(input_buffer.evbuf);
|
||||
}
|
||||
|
176
src/input.h
Normal file
176
src/input.h
Normal file
@ -0,0 +1,176 @@
|
||||
|
||||
#ifndef __INPUT_H__
|
||||
#define __INPUT_H__
|
||||
|
||||
#include <event2/buffer.h>
|
||||
#include "transcode.h"
|
||||
|
||||
// Must be in sync with inputs[] in input.c
|
||||
enum input_types
|
||||
{
|
||||
INPUT_TYPE_FILE,
|
||||
INPUT_TYPE_HTTP,
|
||||
};
|
||||
|
||||
enum input_flags
|
||||
{
|
||||
// Write to input buffer must not block
|
||||
INPUT_FLAG_NONBLOCK = (1 << 0),
|
||||
// Flags end of file
|
||||
INPUT_FLAG_EOF = (1 << 1),
|
||||
// Flags possible new stream metadata
|
||||
INPUT_FLAG_METADATA = (1 << 2),
|
||||
};
|
||||
|
||||
struct player_source
|
||||
{
|
||||
/* Id of the file/item in the files database */
|
||||
uint32_t id;
|
||||
|
||||
/* Item-Id of the file/item in the queue */
|
||||
uint32_t item_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;
|
||||
|
||||
/* 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;
|
||||
|
||||
struct transcode_ctx *xcode;
|
||||
int setup_done;
|
||||
|
||||
struct player_source *play_next;
|
||||
};
|
||||
|
||||
struct input_definition
|
||||
{
|
||||
// Name of the input
|
||||
const char *name;
|
||||
|
||||
// Type of input
|
||||
enum input_types type;
|
||||
|
||||
// Set to 1 if the input initialization failed
|
||||
char disabled;
|
||||
|
||||
// Prepare a playback session
|
||||
int (*setup)(struct player_source *ps);
|
||||
|
||||
// Starts playback loop (must be defined)
|
||||
int (*start)(struct player_source *ps);
|
||||
|
||||
// Cleans up when playback loop has ended
|
||||
int (*stop)(struct player_source *ps);
|
||||
|
||||
// Changes the playback position
|
||||
int (*seek)(struct player_source *ps, int seek_ms);
|
||||
|
||||
// Initialization function called during startup
|
||||
int (*init)(void);
|
||||
|
||||
// Deinitialization function called at shutdown
|
||||
void (*deinit)(void);
|
||||
|
||||
};
|
||||
|
||||
/*
|
||||
* Input modules use this to test if playback should be stopped or seeked
|
||||
*/
|
||||
int input_loop_break;
|
||||
|
||||
/*
|
||||
* Transfer stream data to the player's input buffer. 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).
|
||||
*
|
||||
* @in evbuf Raw audio data to write
|
||||
* @in flags One or more INPUT_FLAG_*
|
||||
* @return 0 on success, EAGAIN if buffer was full (and _NONBLOCK is set),
|
||||
* -1 on error
|
||||
*/
|
||||
int
|
||||
input_write(struct evbuffer *evbuf, short flags);
|
||||
|
||||
/*
|
||||
* 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.
|
||||
*
|
||||
* @in evbuf Output buffer
|
||||
* @in want How much data to move to the output buffer
|
||||
* @out flags Flags INPUT_FLAG_EOF or INPUT_FLAG_METADATA
|
||||
* @return Number of bytes moved
|
||||
*/
|
||||
int
|
||||
input_read(struct evbuffer *evbuf, size_t want, short *flags);
|
||||
|
||||
/*
|
||||
* Initializes the given player source for playback
|
||||
*/
|
||||
int
|
||||
input_setup(struct player_source *ps);
|
||||
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
int
|
||||
input_start(struct player_source *ps);
|
||||
|
||||
/*
|
||||
* Pauses playback of the given player source (stops playback loop) and 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);
|
||||
|
||||
/*
|
||||
* Flush input buffer. Output flags will be the same as input_read().
|
||||
*/
|
||||
void
|
||||
input_flush(short *flags);
|
||||
|
||||
int
|
||||
input_init(void);
|
||||
|
||||
void
|
||||
input_deinit(void);
|
||||
|
||||
#endif /* !__INPUT_H__ */
|
125
src/inputs/file_http.c
Normal file
125
src/inputs/file_http.c
Normal file
@ -0,0 +1,125 @@
|
||||
/*
|
||||
* Copyright (C) 2017 Espen Jurgensen
|
||||
*
|
||||
* 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
|
||||
*/
|
||||
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <unistd.h>
|
||||
#include <stdint.h>
|
||||
|
||||
#include <event2/buffer.h>
|
||||
|
||||
#include "transcode.h"
|
||||
#include "http.h"
|
||||
#include "input.h"
|
||||
|
||||
static int
|
||||
setup(struct player_source *ps)
|
||||
{
|
||||
ps->xcode = transcode_setup(ps->data_kind, ps->path, ps->len_ms, XCODE_PCM16_NOHEADER, NULL);
|
||||
if (!ps->xcode)
|
||||
return -1;
|
||||
|
||||
ps->setup_done = 1;
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int
|
||||
http_setup(struct player_source *ps)
|
||||
{
|
||||
char *url;
|
||||
|
||||
if (http_stream_setup(&url, ps->path) < 0)
|
||||
return -1;
|
||||
|
||||
free(ps->path);
|
||||
ps->path = url;
|
||||
|
||||
return setup(ps);
|
||||
}
|
||||
|
||||
static int
|
||||
start(struct player_source *ps)
|
||||
{
|
||||
struct evbuffer *evbuf;
|
||||
short flags;
|
||||
int ret;
|
||||
int icy_timer;
|
||||
|
||||
evbuf = evbuffer_new();
|
||||
|
||||
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, 1, ps->xcode, &icy_timer);
|
||||
if (ret < 0)
|
||||
break;
|
||||
|
||||
flags = ((ret == 0) ? INPUT_FLAG_EOF : 0) |
|
||||
(icy_timer ? INPUT_FLAG_METADATA : 0);
|
||||
|
||||
ret = input_write(evbuf, flags);
|
||||
if (ret < 0)
|
||||
break;
|
||||
}
|
||||
|
||||
evbuffer_free(evbuf);
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
static int
|
||||
stop(struct player_source *ps)
|
||||
{
|
||||
transcode_cleanup(ps->xcode);
|
||||
|
||||
ps->xcode = NULL;
|
||||
ps->setup_done = 0;
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int
|
||||
seek(struct player_source *ps, int seek_ms)
|
||||
{
|
||||
return transcode_seek(ps->xcode, seek_ms);
|
||||
}
|
||||
|
||||
struct input_definition input_file =
|
||||
{
|
||||
.name = "file",
|
||||
.type = INPUT_TYPE_FILE,
|
||||
.disabled = 0,
|
||||
.setup = setup,
|
||||
.start = start,
|
||||
.stop = stop,
|
||||
.seek = seek,
|
||||
};
|
||||
|
||||
struct input_definition input_http =
|
||||
{
|
||||
.name = "http",
|
||||
.type = INPUT_TYPE_HTTP,
|
||||
.disabled = 0,
|
||||
.setup = http_setup,
|
||||
.start = start,
|
||||
.stop = stop,
|
||||
};
|
375
src/player.c
375
src/player.c
@ -1,6 +1,6 @@
|
||||
/*
|
||||
* Copyright (C) 2010-2011 Julien BLACHE <jb@jblache.org>
|
||||
* Copyright (C) 2016 Espen Jürgensen <espenjurgensen@gmail.com>
|
||||
* 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
|
||||
@ -15,6 +15,22 @@
|
||||
* 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.
|
||||
*
|
||||
*/
|
||||
|
||||
#ifdef HAVE_CONFIG_H
|
||||
@ -55,18 +71,13 @@
|
||||
#include "listener.h"
|
||||
#include "commands.h"
|
||||
|
||||
/* Audio outputs */
|
||||
/* Audio and metadata outputs */
|
||||
#include "outputs.h"
|
||||
|
||||
/* Audio inputs */
|
||||
#include "transcode.h"
|
||||
#include "pipe.h"
|
||||
#ifdef HAVE_SPOTIFY_H
|
||||
# include "spotify.h"
|
||||
#endif
|
||||
/* Audio and metadata input */
|
||||
#include "input.h"
|
||||
|
||||
/* Metadata input/output */
|
||||
#include "http.h"
|
||||
/* Scrobbling */
|
||||
#ifdef LASTFM
|
||||
# include "lastfm.h"
|
||||
#endif
|
||||
@ -84,48 +95,6 @@
|
||||
// Used to keep the player from getting ahead of a rate limited source (see below)
|
||||
#define PLAYER_TICKS_MAX_OVERRUN 2
|
||||
|
||||
struct player_source
|
||||
{
|
||||
/* Id of the file/item in the files database */
|
||||
uint32_t id;
|
||||
|
||||
/* Item-Id of the file/item in the queue */
|
||||
uint32_t item_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;
|
||||
|
||||
/* 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;
|
||||
|
||||
struct transcode_ctx *xcode;
|
||||
int setup_done;
|
||||
|
||||
struct player_source *play_next;
|
||||
};
|
||||
|
||||
struct volume_param {
|
||||
int volume;
|
||||
uint64_t spk_id;
|
||||
@ -594,125 +563,11 @@ history_add(uint32_t id, uint32_t item_id)
|
||||
|
||||
/* Audio sources */
|
||||
|
||||
/*
|
||||
* Initializes the given player source for playback
|
||||
*/
|
||||
static int
|
||||
stream_setup(struct player_source *ps)
|
||||
{
|
||||
char *url;
|
||||
int ret;
|
||||
|
||||
if (!ps)
|
||||
{
|
||||
DPRINTF(E_LOG, L_PLAYER, "No player source given to stream_setup\n");
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (ps->setup_done)
|
||||
{
|
||||
DPRINTF(E_LOG, L_PLAYER, "Given player source already setup (id = %d)\n", ps->id);
|
||||
return -1;
|
||||
}
|
||||
|
||||
// Setup depending on data kind
|
||||
switch (ps->data_kind)
|
||||
{
|
||||
case DATA_KIND_FILE:
|
||||
ps->xcode = transcode_setup(ps->data_kind, ps->path, ps->len_ms, XCODE_PCM16_NOHEADER, NULL);
|
||||
ret = ps->xcode ? 0 : -1;
|
||||
break;
|
||||
|
||||
case DATA_KIND_HTTP:
|
||||
ret = http_stream_setup(&url, ps->path);
|
||||
if (ret < 0)
|
||||
break;
|
||||
|
||||
free(ps->path);
|
||||
ps->path = url;
|
||||
|
||||
ps->xcode = transcode_setup(ps->data_kind, ps->path, ps->len_ms, XCODE_PCM16_NOHEADER, NULL);
|
||||
ret = ps->xcode ? 0 : -1;
|
||||
break;
|
||||
|
||||
case DATA_KIND_SPOTIFY:
|
||||
#ifdef HAVE_SPOTIFY_H
|
||||
ret = spotify_playback_setup(ps->path);
|
||||
#else
|
||||
DPRINTF(E_LOG, L_PLAYER, "Player source has data kind 'spotify' (%d), but forked-daapd is compiled without spotify support - cannot setup source '%s'\n",
|
||||
ps->data_kind, ps->path);
|
||||
ret = -1;
|
||||
#endif
|
||||
break;
|
||||
|
||||
case DATA_KIND_PIPE:
|
||||
ret = pipe_setup(ps->path);
|
||||
break;
|
||||
|
||||
default:
|
||||
DPRINTF(E_LOG, L_PLAYER, "Unknown data kind (%d) for player source - cannot setup source '%s'\n",
|
||||
ps->data_kind, ps->path);
|
||||
ret = -1;
|
||||
}
|
||||
|
||||
if (ret == 0)
|
||||
ps->setup_done = 1;
|
||||
else
|
||||
DPRINTF(E_LOG, L_PLAYER, "Failed to setup player source (id = %d)\n", ps->id);
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
/*
|
||||
* Starts or resumes plaback for the given player source
|
||||
*/
|
||||
static int
|
||||
stream_play(struct player_source *ps)
|
||||
{
|
||||
int ret;
|
||||
|
||||
if (!ps)
|
||||
{
|
||||
DPRINTF(E_LOG, L_PLAYER, "Stream play called with no active streaming player source\n");
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (!ps->setup_done)
|
||||
{
|
||||
DPRINTF(E_LOG, L_PLAYER, "Given player source not setup, play not possible\n");
|
||||
return -1;
|
||||
}
|
||||
|
||||
// Start/resume playback depending on data kind
|
||||
switch (ps->data_kind)
|
||||
{
|
||||
case DATA_KIND_HTTP:
|
||||
case DATA_KIND_FILE:
|
||||
ret = 0;
|
||||
break;
|
||||
|
||||
#ifdef HAVE_SPOTIFY_H
|
||||
case DATA_KIND_SPOTIFY:
|
||||
ret = spotify_playback_play();
|
||||
break;
|
||||
#endif
|
||||
|
||||
case DATA_KIND_PIPE:
|
||||
ret = 0;
|
||||
break;
|
||||
|
||||
default:
|
||||
ret = -1;
|
||||
}
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
/*
|
||||
* Read up to "len" data from the given player source and returns
|
||||
* the actual amount of data read.
|
||||
*/
|
||||
static int
|
||||
/*static int
|
||||
stream_read(struct player_source *ps, int len)
|
||||
{
|
||||
int icy_timer;
|
||||
@ -759,149 +614,7 @@ stream_read(struct player_source *ps, int len)
|
||||
}
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
/*
|
||||
* Pauses playback of the given player source
|
||||
*/
|
||||
static int
|
||||
stream_pause(struct player_source *ps)
|
||||
{
|
||||
int ret;
|
||||
|
||||
if (!ps)
|
||||
{
|
||||
DPRINTF(E_LOG, L_PLAYER, "Stream pause called with no active streaming player source\n");
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (!ps->setup_done)
|
||||
{
|
||||
DPRINTF(E_LOG, L_PLAYER, "Given player source not setup, pause not possible\n");
|
||||
return -1;
|
||||
}
|
||||
|
||||
// Pause playback depending on data kind
|
||||
switch (ps->data_kind)
|
||||
{
|
||||
case DATA_KIND_HTTP:
|
||||
ret = 0;
|
||||
break;
|
||||
|
||||
case DATA_KIND_FILE:
|
||||
ret = 0;
|
||||
break;
|
||||
|
||||
#ifdef HAVE_SPOTIFY_H
|
||||
case DATA_KIND_SPOTIFY:
|
||||
ret = spotify_playback_pause();
|
||||
break;
|
||||
#endif
|
||||
|
||||
case DATA_KIND_PIPE:
|
||||
ret = 0;
|
||||
break;
|
||||
|
||||
default:
|
||||
ret = -1;
|
||||
}
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
/*
|
||||
* Seeks to the given position in milliseconds of the given player source
|
||||
*/
|
||||
static int
|
||||
stream_seek(struct player_source *ps, int seek_ms)
|
||||
{
|
||||
int ret;
|
||||
|
||||
if (!ps)
|
||||
{
|
||||
DPRINTF(E_LOG, L_PLAYER, "Stream seek called with no active streaming player source\n");
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (!ps->setup_done)
|
||||
{
|
||||
DPRINTF(E_LOG, L_PLAYER, "Given player source not setup, seek not possible\n");
|
||||
return -1;
|
||||
}
|
||||
|
||||
// Seek depending on data kind
|
||||
switch (ps->data_kind)
|
||||
{
|
||||
case DATA_KIND_HTTP:
|
||||
ret = 0;
|
||||
break;
|
||||
|
||||
case DATA_KIND_FILE:
|
||||
ret = transcode_seek(ps->xcode, seek_ms);
|
||||
break;
|
||||
|
||||
#ifdef HAVE_SPOTIFY_H
|
||||
case DATA_KIND_SPOTIFY:
|
||||
ret = spotify_playback_seek(seek_ms);
|
||||
break;
|
||||
#endif
|
||||
|
||||
case DATA_KIND_PIPE:
|
||||
ret = 0;
|
||||
break;
|
||||
|
||||
default:
|
||||
ret = -1;
|
||||
}
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
/*
|
||||
* Stops playback and cleanup for the given player source
|
||||
*/
|
||||
static int
|
||||
stream_stop(struct player_source *ps)
|
||||
{
|
||||
if (!ps)
|
||||
{
|
||||
DPRINTF(E_LOG, L_PLAYER, "Stream cleanup called with no active streaming player source\n");
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (!ps->setup_done)
|
||||
{
|
||||
DPRINTF(E_LOG, L_PLAYER, "Given player source not setup, cleanup not possible\n");
|
||||
return -1;
|
||||
}
|
||||
|
||||
switch (ps->data_kind)
|
||||
{
|
||||
case DATA_KIND_FILE:
|
||||
case DATA_KIND_HTTP:
|
||||
if (ps->xcode)
|
||||
{
|
||||
transcode_cleanup(ps->xcode);
|
||||
ps->xcode = NULL;
|
||||
}
|
||||
break;
|
||||
|
||||
case DATA_KIND_SPOTIFY:
|
||||
#ifdef HAVE_SPOTIFY_H
|
||||
spotify_playback_stop();
|
||||
#endif
|
||||
break;
|
||||
|
||||
case DATA_KIND_PIPE:
|
||||
pipe_cleanup();
|
||||
break;
|
||||
}
|
||||
|
||||
ps->setup_done = 0;
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
}*/
|
||||
|
||||
static struct player_source *
|
||||
source_now_playing()
|
||||
@ -959,7 +672,7 @@ source_stop()
|
||||
struct player_source *ps_temp;
|
||||
|
||||
if (cur_streaming)
|
||||
stream_stop(cur_streaming);
|
||||
input_stop(cur_streaming);
|
||||
|
||||
ps_playing = source_now_playing();
|
||||
|
||||
@ -997,20 +710,20 @@ source_pause(uint64_t pos)
|
||||
if (!ps_playing)
|
||||
return -1;
|
||||
|
||||
if (cur_streaming)
|
||||
if (cur_streaming && (cur_streaming == ps_playing))
|
||||
{
|
||||
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 = stream_stop(cur_streaming);
|
||||
ret = input_stop(cur_streaming);
|
||||
if (ret < 0)
|
||||
return -1;
|
||||
}
|
||||
else
|
||||
{
|
||||
ret = stream_pause(cur_streaming);
|
||||
ret = input_pause(cur_streaming);
|
||||
if (ret < 0)
|
||||
return -1;
|
||||
}
|
||||
@ -1034,7 +747,7 @@ source_pause(uint64_t pos)
|
||||
{
|
||||
DPRINTF(E_INFO, L_PLAYER, "Opening '%s'\n", cur_streaming->path);
|
||||
|
||||
ret = stream_setup(cur_streaming);
|
||||
ret = input_setup(cur_streaming);
|
||||
if (ret < 0)
|
||||
{
|
||||
DPRINTF(E_LOG, L_PLAYER, "Failed to open '%s'\n", cur_streaming->path);
|
||||
@ -1045,7 +758,9 @@ source_pause(uint64_t pos)
|
||||
/* Seek back to the pause position */
|
||||
seek_frames = (pos - cur_streaming->stream_start);
|
||||
seek_ms = (int)((seek_frames * 1000) / 44100);
|
||||
ret = stream_seek(cur_streaming, seek_ms);
|
||||
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 = last_rtptime + AIRTUNES_V2_PACKET_SAMPLES - ((uint64_t)ret * 44100) / 1000;
|
||||
@ -1067,7 +782,7 @@ source_seek(int seek_ms)
|
||||
{
|
||||
int ret;
|
||||
|
||||
ret = stream_seek(cur_streaming, seek_ms);
|
||||
ret = input_seek(cur_streaming, seek_ms);
|
||||
if (ret < 0)
|
||||
return -1;
|
||||
|
||||
@ -1086,7 +801,7 @@ source_play()
|
||||
{
|
||||
int ret;
|
||||
|
||||
ret = stream_play(cur_streaming);
|
||||
ret = input_start(cur_streaming);
|
||||
|
||||
ticks_skip = 0;
|
||||
memset(rawbuf, 0, sizeof(rawbuf));
|
||||
@ -1116,7 +831,7 @@ source_open(struct player_source *ps, uint64_t start_pos, int seek_ms)
|
||||
return -1;
|
||||
}
|
||||
|
||||
ret = stream_setup(ps);
|
||||
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);
|
||||
@ -1151,7 +866,7 @@ source_open(struct player_source *ps, uint64_t start_pos, int seek_ms)
|
||||
static int
|
||||
source_close(uint64_t end_pos)
|
||||
{
|
||||
stream_stop(cur_streaming);
|
||||
input_stop(cur_streaming);
|
||||
|
||||
cur_streaming->end = end_pos;
|
||||
|
||||
@ -1339,6 +1054,7 @@ source_read(uint8_t *buf, int len, uint64_t rtptime)
|
||||
{
|
||||
int ret;
|
||||
int nbytes;
|
||||
short flags;
|
||||
char *silence_buf;
|
||||
struct player_source *ps;
|
||||
|
||||
@ -1350,9 +1066,10 @@ source_read(uint8_t *buf, int len, uint64_t rtptime)
|
||||
{
|
||||
if (evbuffer_get_length(audio_buf) == 0)
|
||||
{
|
||||
flags = 0;
|
||||
if (cur_streaming)
|
||||
{
|
||||
ret = stream_read(cur_streaming, len - nbytes);
|
||||
ret = input_read(audio_buf, len - nbytes, &flags);
|
||||
}
|
||||
else if (cur_playing)
|
||||
{
|
||||
@ -1369,9 +1086,10 @@ source_read(uint8_t *buf, int len, uint64_t rtptime)
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (ret <= 0)
|
||||
// if (ret == 0)
|
||||
//TODO Underrun -> pause
|
||||
if ((ret < 0) || (flags & INPUT_FLAG_EOF))
|
||||
{
|
||||
/* EOF or error */
|
||||
source_close(rtptime + BTOS(nbytes) - 1);
|
||||
|
||||
DPRINTF(E_DBG, L_PLAYER, "New file\n");
|
||||
@ -3224,7 +2942,6 @@ player_playback_prev(void)
|
||||
return ret;
|
||||
}
|
||||
|
||||
|
||||
void
|
||||
player_speaker_enumerate(spk_enum_cb cb, void *arg)
|
||||
{
|
||||
@ -3539,6 +3256,13 @@ player_init(void)
|
||||
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)
|
||||
{
|
||||
@ -3554,6 +3278,8 @@ player_init(void)
|
||||
return 0;
|
||||
|
||||
thread_fail:
|
||||
input_deinit();
|
||||
input_fail:
|
||||
outputs_deinit();
|
||||
outputs_fail:
|
||||
commands_base_free(cmdbase);
|
||||
@ -3599,6 +3325,7 @@ player_deinit(void)
|
||||
|
||||
evbuffer_free(audio_buf);
|
||||
|
||||
input_deinit();
|
||||
outputs_deinit();
|
||||
|
||||
event_base_free(evbase_player);
|
||||
|
@ -13,11 +13,6 @@
|
||||
/* AirTunes v2 number of samples per packet */
|
||||
#define AIRTUNES_V2_PACKET_SAMPLES 352
|
||||
|
||||
|
||||
/* Samples to bytes, bytes to samples */
|
||||
#define STOB(s) ((s) * 4)
|
||||
#define BTOS(b) ((b) / 4)
|
||||
|
||||
/* Maximum number of previously played songs that are remembered */
|
||||
#define MAX_HISTORY_COUNT 20
|
||||
|
||||
@ -115,7 +110,6 @@ player_playback_next(void);
|
||||
int
|
||||
player_playback_prev(void);
|
||||
|
||||
|
||||
int
|
||||
player_volume_set(int vol);
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user