mirror of
https://github.com/owntone/owntone-server.git
synced 2024-12-26 23:25:56 -05:00
[input] Add Spotify input module
This commit is contained in:
parent
c92ebf9dfb
commit
79639c73ed
@ -6,7 +6,7 @@ ITUNES_SRC=library/filescanner_itunes.c
|
||||
endif
|
||||
|
||||
if COND_SPOTIFY
|
||||
SPOTIFY_SRC=spotify.c spotify.h spotify_webapi.c spotify_webapi.h
|
||||
SPOTIFY_SRC=spotify.c spotify.h spotify_webapi.c spotify_webapi.h inputs/spotify.c
|
||||
endif
|
||||
|
||||
if COND_LASTFM
|
||||
|
21
src/input.c
21
src/input.c
@ -46,11 +46,17 @@
|
||||
|
||||
extern struct input_definition input_file;
|
||||
extern struct input_definition input_http;
|
||||
#ifdef HAVE_SPOTIFY_H
|
||||
extern struct input_definition input_spotify;
|
||||
#endif
|
||||
|
||||
// Must be in sync with enum input_types
|
||||
static struct input_definition *inputs[] = {
|
||||
&input_file,
|
||||
&input_http,
|
||||
#ifdef HAVE_SPOTIFY_H
|
||||
&input_spotify,
|
||||
#endif
|
||||
NULL
|
||||
};
|
||||
|
||||
@ -124,6 +130,11 @@ map_data_kind(int data_kind)
|
||||
case DATA_KIND_HTTP:
|
||||
return INPUT_TYPE_HTTP;
|
||||
|
||||
#ifdef HAVE_SPOTIFY_H
|
||||
case DATA_KIND_SPOTIFY:
|
||||
return INPUT_TYPE_SPOTIFY;
|
||||
#endif
|
||||
|
||||
default:
|
||||
return -1;
|
||||
}
|
||||
@ -185,6 +196,15 @@ playback(void *arg)
|
||||
pthread_exit(NULL);
|
||||
}
|
||||
|
||||
void
|
||||
input_wait(void)
|
||||
{
|
||||
pthread_mutex_lock(&input_buffer.mutex);
|
||||
pthread_cond_wait(&input_buffer.cond, &input_buffer.mutex);
|
||||
|
||||
pthread_mutex_unlock(&input_buffer.mutex);
|
||||
}
|
||||
|
||||
// Called by input modules from within the playback loop
|
||||
int
|
||||
input_write(struct evbuffer *evbuf, short flags)
|
||||
@ -333,6 +353,7 @@ input_pause(struct player_source *ps)
|
||||
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)
|
||||
{
|
||||
|
15
src/input.h
15
src/input.h
@ -2,6 +2,9 @@
|
||||
#ifndef __INPUT_H__
|
||||
#define __INPUT_H__
|
||||
|
||||
#ifdef HAVE_CONFIG_H
|
||||
# include <config.h>
|
||||
#endif
|
||||
#include <event2/buffer.h>
|
||||
#include "transcode.h"
|
||||
|
||||
@ -10,6 +13,9 @@ enum input_types
|
||||
{
|
||||
INPUT_TYPE_FILE,
|
||||
INPUT_TYPE_HTTP,
|
||||
#ifdef HAVE_SPOTIFY_H
|
||||
INPUT_TYPE_SPOTIFY,
|
||||
#endif
|
||||
};
|
||||
|
||||
enum input_flags
|
||||
@ -96,7 +102,7 @@ struct input_definition
|
||||
};
|
||||
|
||||
/*
|
||||
* Input modules use this to test if playback should be stopped or seeked
|
||||
* Input modules should use this to test if playback should end
|
||||
*/
|
||||
int input_loop_break;
|
||||
|
||||
@ -114,6 +120,13 @@ int input_loop_break;
|
||||
int
|
||||
input_write(struct evbuffer *evbuf, short flags);
|
||||
|
||||
/*
|
||||
* Input modules can use this to wait in the playback loop (like input_write()
|
||||
* would have done)
|
||||
*/
|
||||
void
|
||||
input_wait(void);
|
||||
|
||||
/*
|
||||
* 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.
|
||||
|
96
src/inputs/spotify.c
Normal file
96
src/inputs/spotify.c
Normal file
@ -0,0 +1,96 @@
|
||||
/*
|
||||
* 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 "input.h"
|
||||
#include "spotify.h"
|
||||
|
||||
static int
|
||||
setup(struct player_source *ps)
|
||||
{
|
||||
int ret;
|
||||
|
||||
ret = spotify_playback_setup(ps->path);
|
||||
if (ret < 0)
|
||||
return -1;
|
||||
|
||||
ps->setup_done = 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)
|
||||
{
|
||||
int ret;
|
||||
|
||||
ret = spotify_playback_stop();
|
||||
if (ret < 0)
|
||||
return -1;
|
||||
|
||||
ps->setup_done = 0;
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int
|
||||
seek(struct player_source *ps, int seek_ms)
|
||||
{
|
||||
int ret;
|
||||
|
||||
ret = spotify_playback_seek(seek_ms);
|
||||
if (ret < 0)
|
||||
return -1;
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
struct input_definition input_spotify =
|
||||
{
|
||||
.name = "Spotify",
|
||||
.type = INPUT_TYPE_SPOTIFY,
|
||||
.disabled = 0,
|
||||
.setup = setup,
|
||||
.start = start,
|
||||
.stop = stop,
|
||||
.seek = seek,
|
||||
};
|
||||
|
@ -1097,9 +1097,9 @@ source_read(uint8_t *buf, int len, uint64_t rtptime)
|
||||
return -1;
|
||||
}
|
||||
|
||||
// if (ret == 0)
|
||||
//TODO Underrun -> pause
|
||||
if ((ret < 0) || (flags & INPUT_FLAG_EOF))
|
||||
if (ret == 0)
|
||||
sleep(1); // TODO Underrun -> proper pause
|
||||
else if ((ret < 0) || (flags & INPUT_FLAG_EOF))
|
||||
{
|
||||
source_close(rtptime + BTOS(nbytes) - 1);
|
||||
|
||||
|
220
src/spotify.c
220
src/spotify.c
@ -55,6 +55,7 @@
|
||||
#include "cache.h"
|
||||
#include "commands.h"
|
||||
#include "library.h"
|
||||
#include "input.h"
|
||||
|
||||
/* TODO for the web api:
|
||||
* - UI should be prettier
|
||||
@ -88,22 +89,6 @@
|
||||
#define SPOTIFY_WEB_REQUESTS_MAX 20
|
||||
|
||||
/* --- Types --- */
|
||||
typedef struct audio_fifo_data
|
||||
{
|
||||
TAILQ_ENTRY(audio_fifo_data) link;
|
||||
int nsamples;
|
||||
int16_t samples[0];
|
||||
} audio_fifo_data_t;
|
||||
|
||||
typedef struct audio_fifo
|
||||
{
|
||||
TAILQ_HEAD(, audio_fifo_data) q;
|
||||
int qlen;
|
||||
int fullcount;
|
||||
pthread_mutex_t mutex;
|
||||
pthread_cond_t cond;
|
||||
} audio_fifo_t;
|
||||
|
||||
enum spotify_state
|
||||
{
|
||||
SPOTIFY_STATE_INACTIVE,
|
||||
@ -114,12 +99,6 @@ enum spotify_state
|
||||
SPOTIFY_STATE_STOPPED,
|
||||
};
|
||||
|
||||
struct audio_get_param
|
||||
{
|
||||
struct evbuffer *evbuf;
|
||||
int wanted;
|
||||
};
|
||||
|
||||
struct artwork_get_param
|
||||
{
|
||||
struct evbuffer *evbuf;
|
||||
@ -164,8 +143,8 @@ static int spotify_saved_plid;
|
||||
// Flag to avoid triggering playlist change events while the (re)scan is running
|
||||
static bool scanning;
|
||||
|
||||
// Audio fifo
|
||||
static audio_fifo_t *g_audio_fifo;
|
||||
// Audio buffer
|
||||
static struct evbuffer *spotify_audio_buffer;
|
||||
|
||||
/**
|
||||
* The application key is specific to forked-daapd, and allows Spotify
|
||||
@ -1180,25 +1159,6 @@ mk_reltime(struct timespec *ts, time_t sec)
|
||||
ts->tv_sec += sec;
|
||||
}
|
||||
|
||||
static void
|
||||
audio_fifo_flush(void)
|
||||
{
|
||||
audio_fifo_data_t *afd;
|
||||
|
||||
DPRINTF(E_DBG, L_SPOTIFY, "Flushing audio fifo\n");
|
||||
|
||||
CHECK_ERR(L_SPOTIFY, pthread_mutex_lock(&g_audio_fifo->mutex));
|
||||
|
||||
while((afd = TAILQ_FIRST(&g_audio_fifo->q))) {
|
||||
TAILQ_REMOVE(&g_audio_fifo->q, afd, link);
|
||||
free(afd);
|
||||
}
|
||||
|
||||
g_audio_fifo->qlen = 0;
|
||||
g_audio_fifo->fullcount = 0;
|
||||
CHECK_ERR(L_SPOTIFY, pthread_mutex_unlock(&g_audio_fifo->mutex));
|
||||
}
|
||||
|
||||
static enum command_state
|
||||
playback_setup(void *arg, int *retval)
|
||||
{
|
||||
@ -1240,8 +1200,6 @@ playback_setup(void *arg, int *retval)
|
||||
return COMMAND_END;
|
||||
}
|
||||
|
||||
audio_fifo_flush();
|
||||
|
||||
*retval = 0;
|
||||
return COMMAND_END;
|
||||
}
|
||||
@ -1330,8 +1288,6 @@ playback_seek(void *arg, int *retval)
|
||||
return COMMAND_END;
|
||||
}
|
||||
|
||||
audio_fifo_flush();
|
||||
|
||||
*retval = 0;
|
||||
return COMMAND_END;
|
||||
}
|
||||
@ -1353,92 +1309,14 @@ playback_eot(void *arg, int *retval)
|
||||
|
||||
g_state = SPOTIFY_STATE_STOPPING;
|
||||
|
||||
// TODO 1) This will block for a while, but perhaps ok?
|
||||
// 2) spotify_audio_buffer not entirely thread safe here (but unlikely risk...)
|
||||
input_write(spotify_audio_buffer, INPUT_FLAG_EOF);
|
||||
|
||||
*retval = 0;
|
||||
return COMMAND_END;
|
||||
}
|
||||
|
||||
static enum command_state
|
||||
audio_get(void *arg, int *retval)
|
||||
{
|
||||
struct audio_get_param *audio;
|
||||
struct timespec ts;
|
||||
audio_fifo_data_t *afd;
|
||||
int processed;
|
||||
int timeout;
|
||||
int ret;
|
||||
int err;
|
||||
int s;
|
||||
|
||||
audio = (struct audio_get_param *) arg;
|
||||
afd = NULL;
|
||||
processed = 0;
|
||||
|
||||
// If spotify was paused begin by resuming playback
|
||||
if (g_state == SPOTIFY_STATE_PAUSED)
|
||||
playback_play(NULL, retval);
|
||||
|
||||
CHECK_ERR(L_SPOTIFY, pthread_mutex_lock(&g_audio_fifo->mutex));
|
||||
|
||||
while ((processed < audio->wanted) && (g_state != SPOTIFY_STATE_STOPPED))
|
||||
{
|
||||
// If track has ended and buffer is empty
|
||||
if ((g_state == SPOTIFY_STATE_STOPPING) && (g_audio_fifo->qlen <= 0))
|
||||
{
|
||||
DPRINTF(E_DBG, L_SPOTIFY, "Track finished\n");
|
||||
g_state = SPOTIFY_STATE_STOPPED;
|
||||
break;
|
||||
}
|
||||
|
||||
// If buffer is empty, wait for audio, but use timed wait so we don't
|
||||
// risk waiting forever (maybe the player stopped while we were waiting)
|
||||
timeout = 0;
|
||||
while ( !(afd = TAILQ_FIRST(&g_audio_fifo->q)) &&
|
||||
(g_state != SPOTIFY_STATE_STOPPED) &&
|
||||
(g_state != SPOTIFY_STATE_STOPPING) &&
|
||||
(timeout < SPOTIFY_TIMEOUT) )
|
||||
{
|
||||
DPRINTF(E_DBG, L_SPOTIFY, "Waiting for audio\n");
|
||||
timeout += 5;
|
||||
mk_reltime(&ts, 5);
|
||||
CHECK_ERR_EXCEPT(L_SPOTIFY, pthread_cond_timedwait(&g_audio_fifo->cond, &g_audio_fifo->mutex, &ts), err, ETIMEDOUT);
|
||||
}
|
||||
|
||||
if ((!afd) && (timeout >= SPOTIFY_TIMEOUT))
|
||||
{
|
||||
DPRINTF(E_LOG, L_SPOTIFY, "Timeout waiting for audio (waited %d sec)\n", timeout);
|
||||
|
||||
spotify_playback_stop_nonblock();
|
||||
}
|
||||
|
||||
if (!afd)
|
||||
break;
|
||||
|
||||
TAILQ_REMOVE(&g_audio_fifo->q, afd, link);
|
||||
g_audio_fifo->qlen -= afd->nsamples;
|
||||
|
||||
s = afd->nsamples * sizeof(int16_t) * 2;
|
||||
|
||||
ret = evbuffer_add(audio->evbuf, afd->samples, s);
|
||||
free(afd);
|
||||
afd = NULL;
|
||||
if (ret < 0)
|
||||
{
|
||||
DPRINTF(E_LOG, L_SPOTIFY, "Out of memory for evbuffer (tried to add %d bytes)\n", s);
|
||||
CHECK_ERR(L_SPOTIFY, pthread_mutex_unlock(&g_audio_fifo->mutex));
|
||||
*retval = -1;
|
||||
return COMMAND_END;
|
||||
}
|
||||
|
||||
processed += s;
|
||||
}
|
||||
|
||||
CHECK_ERR(L_SPOTIFY, pthread_mutex_unlock(&g_audio_fifo->mutex));
|
||||
|
||||
|
||||
*retval = processed;
|
||||
return COMMAND_END;
|
||||
}
|
||||
|
||||
static void
|
||||
artwork_loaded_cb(sp_image *image, void *userdata)
|
||||
{
|
||||
@ -1681,8 +1559,8 @@ logged_out(sp_session *sess)
|
||||
static int music_delivery(sp_session *sess, const sp_audioformat *format,
|
||||
const void *frames, int num_frames)
|
||||
{
|
||||
audio_fifo_data_t *afd;
|
||||
size_t s;
|
||||
size_t size;
|
||||
int ret;
|
||||
|
||||
/* No support for resampling right now */
|
||||
if ((format->sample_rate != 44100) || (format->channels != 2))
|
||||
@ -1692,44 +1570,26 @@ static int music_delivery(sp_session *sess, const sp_audioformat *format,
|
||||
return num_frames;
|
||||
}
|
||||
|
||||
// Audio discontinuity, e.g. seek
|
||||
if (num_frames == 0)
|
||||
return 0; // Audio discontinuity, do nothing
|
||||
|
||||
CHECK_ERR(L_SPOTIFY, pthread_mutex_lock(&g_audio_fifo->mutex));
|
||||
|
||||
/* Buffer three seconds of audio */
|
||||
if (g_audio_fifo->qlen > (3 * format->sample_rate))
|
||||
{
|
||||
// If the buffer has been full the last 300 times (~about a minute) we
|
||||
// assume the player thread paused/died without telling us, so we signal pause
|
||||
if (g_audio_fifo->fullcount < 300)
|
||||
g_audio_fifo->fullcount++;
|
||||
else
|
||||
{
|
||||
DPRINTF(E_WARN, L_SPOTIFY, "Buffer full more than 300 times, pausing\n");
|
||||
spotify_playback_pause_nonblock();
|
||||
g_audio_fifo->fullcount = 0;
|
||||
}
|
||||
|
||||
CHECK_ERR(L_SPOTIFY, pthread_mutex_unlock(&g_audio_fifo->mutex));
|
||||
|
||||
evbuffer_drain(spotify_audio_buffer, evbuffer_get_length(spotify_audio_buffer));
|
||||
return 0;
|
||||
}
|
||||
else
|
||||
g_audio_fifo->fullcount = 0;
|
||||
|
||||
s = num_frames * sizeof(int16_t) * format->channels;
|
||||
size = num_frames * sizeof(int16_t) * format->channels;
|
||||
|
||||
afd = malloc(sizeof(*afd) + s);
|
||||
memcpy(afd->samples, frames, s);
|
||||
ret = evbuffer_add(spotify_audio_buffer, frames, size);
|
||||
if (ret < 0)
|
||||
{
|
||||
DPRINTF(E_LOG, L_SPOTIFY, "Out of memory adding audio to buffer\n");
|
||||
return num_frames;
|
||||
}
|
||||
|
||||
afd->nsamples = num_frames;
|
||||
|
||||
TAILQ_INSERT_TAIL(&g_audio_fifo->q, afd, link);
|
||||
g_audio_fifo->qlen += num_frames;
|
||||
|
||||
CHECK_ERR(L_SPOTIFY, pthread_cond_signal(&g_audio_fifo->cond));
|
||||
CHECK_ERR(L_SPOTIFY, pthread_mutex_unlock(&g_audio_fifo->mutex));
|
||||
// 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, INPUT_FLAG_NONBLOCK);
|
||||
|
||||
return num_frames;
|
||||
}
|
||||
@ -1975,18 +1835,6 @@ spotify_playback_seek(int ms)
|
||||
return -1;
|
||||
}
|
||||
|
||||
/* Thread: player */
|
||||
int
|
||||
spotify_audio_get(struct evbuffer *evbuf, int wanted)
|
||||
{
|
||||
struct audio_get_param audio;
|
||||
|
||||
audio.evbuf = evbuf;
|
||||
audio.wanted = wanted;
|
||||
|
||||
return commands_exec_sync(cmdbase, audio_get, NULL, &audio);
|
||||
}
|
||||
|
||||
/* Thread: httpd (artwork) and worker */
|
||||
int
|
||||
spotify_artwork_get(struct evbuffer *evbuf, char *path, int max_w, int max_h)
|
||||
@ -2613,7 +2461,6 @@ spotify_init(void)
|
||||
|
||||
event_add(g_notifyev, NULL);
|
||||
|
||||
|
||||
cmdbase = commands_base_new(evbase_spotify, exit_cb);
|
||||
if (!cmdbase)
|
||||
{
|
||||
@ -2651,17 +2498,7 @@ spotify_init(void)
|
||||
break;
|
||||
}
|
||||
|
||||
/* Prepare audio buffer */
|
||||
g_audio_fifo = (audio_fifo_t *)malloc(sizeof(audio_fifo_t));
|
||||
if (!g_audio_fifo)
|
||||
{
|
||||
DPRINTF(E_LOG, L_SPOTIFY, "Out of memory for audio buffer\n");
|
||||
goto audio_fifo_fail;
|
||||
}
|
||||
TAILQ_INIT(&g_audio_fifo->q);
|
||||
g_audio_fifo->qlen = 0;
|
||||
CHECK_ERR(L_SPOTIFY, mutex_init(&g_audio_fifo->mutex));
|
||||
CHECK_ERR(L_SPOTIFY, pthread_cond_init(&g_audio_fifo->cond, NULL));
|
||||
spotify_audio_buffer = evbuffer_new();
|
||||
|
||||
CHECK_ERR(L_SPOTIFY, mutex_init(&login_lck));
|
||||
CHECK_ERR(L_SPOTIFY, pthread_cond_init(&login_cond, NULL));
|
||||
@ -2687,11 +2524,8 @@ spotify_init(void)
|
||||
CHECK_ERR(L_SPOTIFY, pthread_cond_destroy(&login_cond));
|
||||
CHECK_ERR(L_SPOTIFY, pthread_mutex_destroy(&login_lck));
|
||||
|
||||
CHECK_ERR(L_SPOTIFY, pthread_cond_destroy(&g_audio_fifo->cond));
|
||||
CHECK_ERR(L_SPOTIFY, pthread_mutex_destroy(&g_audio_fifo->mutex));
|
||||
free(g_audio_fifo);
|
||||
evbuffer_free(spotify_audio_buffer);
|
||||
|
||||
audio_fifo_fail:
|
||||
fptr_sp_session_release(g_sess);
|
||||
g_sess = NULL;
|
||||
|
||||
@ -2751,10 +2585,8 @@ spotify_deinit(void)
|
||||
CHECK_ERR(L_SPOTIFY, pthread_cond_destroy(&login_cond));
|
||||
CHECK_ERR(L_SPOTIFY, pthread_mutex_destroy(&login_lck));
|
||||
|
||||
/* Clear audio fifo */
|
||||
CHECK_ERR(L_SPOTIFY, pthread_cond_destroy(&g_audio_fifo->cond));
|
||||
CHECK_ERR(L_SPOTIFY, pthread_mutex_destroy(&g_audio_fifo->mutex));
|
||||
free(g_audio_fifo);
|
||||
/* Free audio buffer */
|
||||
evbuffer_free(spotify_audio_buffer);
|
||||
|
||||
/* Release libspotify handle */
|
||||
dlclose(g_libhandle);
|
||||
|
@ -27,9 +27,6 @@ spotify_playback_stop_nonblock(void);
|
||||
int
|
||||
spotify_playback_seek(int ms);
|
||||
|
||||
int
|
||||
spotify_audio_get(struct evbuffer *evbuf, int wanted);
|
||||
|
||||
int
|
||||
spotify_artwork_get(struct evbuffer *evbuf, char *path, int max_w, int max_h);
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user