mirror of
https://github.com/owntone/owntone-server.git
synced 2025-01-27 06:33:21 -05:00
Merge branch 'player_refactor2'
Major refactor of player, input and output
This commit is contained in:
commit
4dcbb2f24f
32
configure.ac
32
configure.ac
@ -210,7 +210,7 @@ AC_ARG_WITH([libav], [AS_HELP_STRING([--with-libav],
|
||||
[[LIBAV=-libav]], [[LIBAV=]])
|
||||
dnl libav/ffmpeg requires many feature checks
|
||||
FORK_MODULES_CHECK([FORKED], [LIBAV],
|
||||
[libavformat$LIBAV libavcodec$LIBAV libswscale$LIBAV libavutil$LIBAV libavfilter$LIBAV],
|
||||
[libavformat$LIBAV libavcodec$LIBAV libavutil$LIBAV libavfilter$LIBAV],
|
||||
[av_init_packet], [libavcodec/avcodec.h],
|
||||
[dnl Checks for misc libav and ffmpeg API differences
|
||||
AC_MSG_CHECKING([whether libav libraries are ffmpeg])
|
||||
@ -231,37 +231,7 @@ FORK_MODULES_CHECK([FORKED], [LIBAV],
|
||||
[libavutil/avutil.h])
|
||||
FORK_CHECK_DECLS([avformat_network_init],
|
||||
[libavformat/avformat.h])
|
||||
dnl Check if we have modern or legacy AV api
|
||||
FORK_CHECK_DECLS([avcodec_send_packet, avcodec_parameters_from_context],
|
||||
[libavcodec/avcodec.h], [[libav_modern_api=yes]], [[libav_modern_api=no]])
|
||||
dnl The below we need to know only if we are going to use the legacy AV api
|
||||
FORK_CHECK_DECLS([av_buffersrc_add_frame_flags],
|
||||
[libavfilter/buffersrc.h])
|
||||
FORK_CHECK_DECLS([av_buffersink_get_frame],
|
||||
[libavfilter/buffersink.h])
|
||||
FORK_CHECK_DECLS([avfilter_graph_parse_ptr],
|
||||
[libavfilter/avfilter.h])
|
||||
FORK_CHECK_DECLS([av_packet_unref],
|
||||
[libavcodec/avcodec.h])
|
||||
FORK_CHECK_DECLS([av_packet_rescale_ts],
|
||||
[libavcodec/avcodec.h])
|
||||
FORK_CHECK_DECLS([avformat_alloc_output_context2],
|
||||
[libavformat/avformat.h])
|
||||
FORK_CHECK_DECLS([av_frame_alloc],
|
||||
[libavutil/frame.h])
|
||||
FORK_CHECK_DECLS([av_frame_get_best_effort_timestamp],
|
||||
[libavutil/frame.h])
|
||||
FORK_CHECK_DECLS([av_image_fill_arrays],
|
||||
[libavutil/imgutils.h])
|
||||
FORK_CHECK_DECLS([av_image_get_buffer_size],
|
||||
[libavutil/imgutils.h])
|
||||
AC_CHECK_HEADERS([libavutil/channel_layout.h libavutil/mathematics.h])
|
||||
])
|
||||
dnl Option to choose old ffmpeg/libav API even if modern api was found
|
||||
FORK_ARG_DISABLE([use of ffmpeg/libav API with avcodec_send_packet() and family],
|
||||
[avcodecsend], [USE_AVCODEC_SEND])
|
||||
AM_CONDITIONAL([COND_FFMPEG_LEGACY],
|
||||
[[test "x$libav_modern_api" = "xno" || test "x$enable_avcodecsend" = "xno" ]])
|
||||
|
||||
AC_CHECK_SIZEOF([void *])
|
||||
|
||||
|
@ -217,17 +217,23 @@ audio {
|
||||
# If not set, the value for "card" will be used.
|
||||
# mixer_device = ""
|
||||
|
||||
# Synchronization
|
||||
# If your local audio is out of sync with AirPlay, you can adjust this
|
||||
# value. Positive values correspond to moving local audio ahead,
|
||||
# negative correspond to delaying it. The unit is samples, where is
|
||||
# 44100 = 1 second. The offset must be between -44100 and 44100.
|
||||
# offset = 0
|
||||
# Enable or disable audio resampling to keep local audio in sync with
|
||||
# e.g. Airplay. This feature relies on accurate ALSA measurements of
|
||||
# delay, and some devices don't provide that. If that is the case you
|
||||
# are better off disabling the feature.
|
||||
# sync_disable = false
|
||||
|
||||
# How often to check and correct for drift between ALSA and AirPlay.
|
||||
# The value is an integer expressed in seconds.
|
||||
# Clamped to the range 1..20.
|
||||
# adjust_period_seconds = 10
|
||||
# Here you can adjust when local audio is started relative to other
|
||||
# speakers, e.g. Airplay. Negative values correspond to moving local
|
||||
# audio ahead, positive correspond to delaying it. The unit is
|
||||
# milliseconds. The offset must be between -1000 and 1000 (+/- 1 sec).
|
||||
# offset_ms = 0
|
||||
|
||||
# To calculate what and if resampling is required, local audio delay is
|
||||
# measured each second. After a period the collected measurements are
|
||||
# used to estimate drift and latency, which determines if corrections
|
||||
# are required. This setting sets the length of that period in seconds.
|
||||
# adjust_period_seconds = 100
|
||||
}
|
||||
|
||||
# Pipe output
|
||||
|
@ -47,12 +47,6 @@ if COND_LIBWEBSOCKETS
|
||||
LIBWEBSOCKETS_SRC=websocket.c websocket.h
|
||||
endif
|
||||
|
||||
if COND_FFMPEG_LEGACY
|
||||
FFMPEG_SRC=transcode_legacy.c artwork_legacy.c ffmpeg-compat.h
|
||||
else
|
||||
FFMPEG_SRC=transcode.c artwork.c
|
||||
endif
|
||||
|
||||
GPERF_FILES = \
|
||||
daap_query.gperf \
|
||||
rsp_query.gperf \
|
||||
@ -118,8 +112,8 @@ forked_daapd_SOURCES = main.c \
|
||||
httpd_artworkapi.c httpd_artworkapi.h \
|
||||
http.c http.h \
|
||||
dmap_common.c dmap_common.h \
|
||||
transcode.h artwork.h \
|
||||
$(FFMPEG_SRC) \
|
||||
transcode.c transcode.h \
|
||||
artwork.c artwork.h \
|
||||
misc.c misc.h \
|
||||
misc_json.c misc_json.h \
|
||||
rng.c rng.h \
|
||||
@ -131,6 +125,7 @@ forked_daapd_SOURCES = main.c \
|
||||
input.h input.c \
|
||||
inputs/file_http.c inputs/pipe.c \
|
||||
outputs.h outputs.c \
|
||||
outputs/rtp_common.h outputs/rtp_common.c \
|
||||
outputs/raop.c $(RAOP_VERIFICATION_SRC) \
|
||||
outputs/streaming.c outputs/dummy.c outputs/fifo.c \
|
||||
$(ALSA_SRC) $(PULSEAUDIO_SRC) $(CHROMECAST_SRC) \
|
||||
|
@ -408,7 +408,7 @@ artwork_get(struct evbuffer *evbuf, char *path, struct evbuffer *inbuf, int max_
|
||||
|
||||
DPRINTF(E_SPAM, L_ART, "Getting artwork (max destination width %d height %d)\n", max_w, max_h);
|
||||
|
||||
xcode_decode = transcode_decode_setup(XCODE_JPEG, DATA_KIND_FILE, path, inbuf, 0); // Covers XCODE_PNG too
|
||||
xcode_decode = transcode_decode_setup(XCODE_JPEG, NULL, DATA_KIND_FILE, path, inbuf, 0); // Covers XCODE_PNG too
|
||||
if (!xcode_decode)
|
||||
{
|
||||
if (path)
|
||||
@ -462,9 +462,9 @@ artwork_get(struct evbuffer *evbuf, char *path, struct evbuffer *inbuf, int max_
|
||||
}
|
||||
|
||||
if (format_ok == ART_FMT_JPEG)
|
||||
xcode_encode = transcode_encode_setup(XCODE_JPEG, xcode_decode, NULL, target_w, target_h);
|
||||
xcode_encode = transcode_encode_setup(XCODE_JPEG, NULL, xcode_decode, NULL, target_w, target_h);
|
||||
else
|
||||
xcode_encode = transcode_encode_setup(XCODE_PNG, xcode_decode, NULL, target_w, target_h);
|
||||
xcode_encode = transcode_encode_setup(XCODE_PNG, NULL, xcode_decode, NULL, target_w, target_h);
|
||||
|
||||
if (!xcode_encode)
|
||||
{
|
||||
|
1630
src/artwork_legacy.c
1630
src/artwork_legacy.c
File diff suppressed because it is too large
Load Diff
@ -100,6 +100,8 @@ static cfg_opt_t sec_library[] =
|
||||
CFG_STR_LIST("no_decode", NULL, CFGF_NONE),
|
||||
CFG_STR_LIST("force_decode", NULL, CFGF_NONE),
|
||||
CFG_BOOL("pipe_autostart", cfg_true, CFGF_NONE),
|
||||
CFG_INT("pipe_sample_rate", 44100, CFGF_NONE),
|
||||
CFG_INT("pipe_bits_per_sample", 16, CFGF_NONE),
|
||||
CFG_BOOL("rating_updates", cfg_false, CFGF_NONE),
|
||||
CFG_END()
|
||||
};
|
||||
@ -113,8 +115,10 @@ static cfg_opt_t sec_audio[] =
|
||||
CFG_STR("card", "default", CFGF_NONE),
|
||||
CFG_STR("mixer", NULL, CFGF_NONE),
|
||||
CFG_STR("mixer_device", NULL, CFGF_NONE),
|
||||
CFG_INT("offset", 0, CFGF_NONE),
|
||||
CFG_INT("adjust_period_seconds", 10, CFGF_NONE),
|
||||
CFG_BOOL("sync_disable", cfg_false, CFGF_NONE),
|
||||
CFG_INT("offset", 0, CFGF_NONE), // deprecated
|
||||
CFG_INT("offset_ms", 0, CFGF_NONE),
|
||||
CFG_INT("adjust_period_seconds", 100, CFGF_NONE),
|
||||
CFG_END()
|
||||
};
|
||||
|
||||
|
@ -1,105 +0,0 @@
|
||||
#ifndef __FFMPEG_COMPAT_H__
|
||||
#define __FFMPEG_COMPAT_H__
|
||||
|
||||
#ifdef HAVE_LIBAVUTIL_CHANNEL_LAYOUT_H
|
||||
# include <libavutil/channel_layout.h>
|
||||
#endif
|
||||
|
||||
#ifdef HAVE_LIBAVUTIL_MATHEMATICS_H
|
||||
# include <libavutil/mathematics.h>
|
||||
#endif
|
||||
|
||||
#ifndef HAVE_FFMPEG
|
||||
# define avcodec_find_best_pix_fmt_of_list(a, b, c, d) avcodec_find_best_pix_fmt2((enum AVPixelFormat *)(a), (b), (c), (d))
|
||||
#endif
|
||||
|
||||
#if !HAVE_DECL_AV_FRAME_ALLOC
|
||||
# define av_frame_alloc() avcodec_alloc_frame()
|
||||
# define av_frame_free(x) avcodec_free_frame((x))
|
||||
#endif
|
||||
|
||||
#if !HAVE_DECL_AV_FRAME_GET_BEST_EFFORT_TIMESTAMP
|
||||
# define av_frame_get_best_effort_timestamp(x) (x)->pts
|
||||
#endif
|
||||
|
||||
#if !HAVE_DECL_AV_IMAGE_GET_BUFFER_SIZE
|
||||
# define av_image_get_buffer_size(a, b, c, d) avpicture_get_size((a), (b), (c))
|
||||
#endif
|
||||
|
||||
#if !HAVE_DECL_AV_PACKET_UNREF
|
||||
# define av_packet_unref(a) av_free_packet((a))
|
||||
#endif
|
||||
|
||||
#if !HAVE_DECL_AV_PACKET_RESCALE_TS
|
||||
__attribute__((unused)) static void
|
||||
av_packet_rescale_ts(AVPacket *pkt, AVRational src_tb, AVRational dst_tb)
|
||||
{
|
||||
if (pkt->pts != AV_NOPTS_VALUE)
|
||||
pkt->pts = av_rescale_q(pkt->pts, src_tb, dst_tb);
|
||||
if (pkt->dts != AV_NOPTS_VALUE)
|
||||
pkt->dts = av_rescale_q(pkt->dts, src_tb, dst_tb);
|
||||
if (pkt->duration > 0)
|
||||
pkt->duration = av_rescale_q(pkt->duration, src_tb, dst_tb);
|
||||
if (pkt->convergence_duration > 0)
|
||||
pkt->convergence_duration = av_rescale_q(pkt->convergence_duration, src_tb, dst_tb);
|
||||
}
|
||||
#endif
|
||||
|
||||
#if !HAVE_DECL_AVFORMAT_ALLOC_OUTPUT_CONTEXT2
|
||||
# include <libavutil/opt.h>
|
||||
|
||||
__attribute__((unused)) static int
|
||||
avformat_alloc_output_context2(AVFormatContext **avctx, AVOutputFormat *oformat, const char *format, const char *filename)
|
||||
{
|
||||
AVFormatContext *s = avformat_alloc_context();
|
||||
int ret = 0;
|
||||
|
||||
*avctx = NULL;
|
||||
if (!s)
|
||||
goto nomem;
|
||||
|
||||
if (!oformat) {
|
||||
if (format) {
|
||||
oformat = av_guess_format(format, NULL, NULL);
|
||||
if (!oformat) {
|
||||
av_log(s, AV_LOG_ERROR, "Requested output format '%s' is not a suitable output format\n", format);
|
||||
ret = AVERROR(EINVAL);
|
||||
goto error;
|
||||
}
|
||||
} else {
|
||||
oformat = av_guess_format(NULL, filename, NULL);
|
||||
if (!oformat) {
|
||||
ret = AVERROR(EINVAL);
|
||||
av_log(s, AV_LOG_ERROR, "Unable to find a suitable output format for '%s'\n",
|
||||
filename);
|
||||
goto error;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
s->oformat = oformat;
|
||||
if (s->oformat->priv_data_size > 0) {
|
||||
s->priv_data = av_mallocz(s->oformat->priv_data_size);
|
||||
if (!s->priv_data)
|
||||
goto nomem;
|
||||
if (s->oformat->priv_class) {
|
||||
*(const AVClass**)s->priv_data= s->oformat->priv_class;
|
||||
av_opt_set_defaults(s->priv_data);
|
||||
}
|
||||
} else
|
||||
s->priv_data = NULL;
|
||||
|
||||
if (filename)
|
||||
snprintf(s->filename, sizeof(s->filename), "%s", filename);
|
||||
*avctx = s;
|
||||
return 0;
|
||||
nomem:
|
||||
av_log(s, AV_LOG_ERROR, "Out of memory\n");
|
||||
ret = AVERROR(ENOMEM);
|
||||
error:
|
||||
avformat_free_context(s);
|
||||
return ret;
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif /* !__FFMPEG_COMPAT_H__ */
|
@ -80,6 +80,11 @@
|
||||
"<h1>%s</h1>\n" \
|
||||
"</body>\n</html>\n"
|
||||
|
||||
#define HTTPD_STREAM_SAMPLE_RATE 44100
|
||||
#define HTTPD_STREAM_BPS 16
|
||||
#define HTTPD_STREAM_CHANNELS 2
|
||||
|
||||
|
||||
struct content_type_map {
|
||||
char *ext;
|
||||
char *ctype;
|
||||
@ -1029,6 +1034,7 @@ httpd_request_parse(struct evhttp_request *req, struct httpd_uri_parsed *uri_par
|
||||
void
|
||||
httpd_stream_file(struct evhttp_request *req, int id)
|
||||
{
|
||||
struct media_quality quality = { HTTPD_STREAM_SAMPLE_RATE, HTTPD_STREAM_BPS, HTTPD_STREAM_CHANNELS };
|
||||
struct media_file_info *mfi;
|
||||
struct stream_ctx *st;
|
||||
void (*stream_cb)(int fd, short event, void *arg);
|
||||
@ -1128,7 +1134,7 @@ httpd_stream_file(struct evhttp_request *req, int id)
|
||||
|
||||
stream_cb = stream_chunk_xcode_cb;
|
||||
|
||||
st->xcode = transcode_setup(XCODE_PCM16_HEADER, mfi->data_kind, mfi->path, mfi->song_length, &st->size);
|
||||
st->xcode = transcode_setup(XCODE_PCM16_HEADER, &quality, mfi->data_kind, mfi->path, mfi->song_length, &st->size);
|
||||
if (!st->xcode)
|
||||
{
|
||||
DPRINTF(E_WARN, L_HTTPD, "Transcoding setup failed, aborting streaming\n");
|
||||
|
@ -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;
|
||||
|
||||
|
@ -44,8 +44,13 @@ extern struct event_base *evbase_httpd;
|
||||
// Seconds between sending silence when player is idle
|
||||
// (to prevent client from hanging up)
|
||||
#define STREAMING_SILENCE_INTERVAL 1
|
||||
// Buffer size for transmitting from player to httpd thread
|
||||
#define STREAMING_RAWBUF_SIZE (STOB(AIRTUNES_V2_PACKET_SAMPLES))
|
||||
// How many bytes we try to read at a time from the httpd pipe
|
||||
#define STREAMING_READ_SIZE STOB(352, 16, 2)
|
||||
|
||||
#define STREAMING_MP3_SAMPLE_RATE 44100
|
||||
#define STREAMING_MP3_BPS 16
|
||||
#define STREAMING_MP3_CHANNELS 2
|
||||
|
||||
|
||||
// Linked list of mp3 streaming requests
|
||||
struct streaming_session {
|
||||
@ -54,23 +59,24 @@ struct streaming_session {
|
||||
};
|
||||
static struct streaming_session *streaming_sessions;
|
||||
|
||||
static int streaming_initialized;
|
||||
// Means we're not able to encode to mp3
|
||||
static bool streaming_not_supported;
|
||||
|
||||
// Buffers and interval for sending silence when playback is paused
|
||||
static uint8_t *streaming_silence_data;
|
||||
static size_t streaming_silence_size;
|
||||
// Interval for sending silence when playback is paused
|
||||
static struct timeval streaming_silence_tv = { STREAMING_SILENCE_INTERVAL, 0 };
|
||||
|
||||
// Input buffer, output buffer and encoding ctx for transcode
|
||||
static uint8_t streaming_rawbuf[STREAMING_RAWBUF_SIZE];
|
||||
static struct encode_ctx *streaming_encode_ctx;
|
||||
static struct evbuffer *streaming_encoded_data;
|
||||
static struct media_quality streaming_quality;
|
||||
|
||||
// Used for pushing events and data from the player
|
||||
static struct event *streamingev;
|
||||
static struct event *metaev;
|
||||
static struct player_status streaming_player_status;
|
||||
static int streaming_player_changed;
|
||||
static int streaming_pipe[2];
|
||||
static int streaming_meta[2];
|
||||
|
||||
|
||||
static void
|
||||
@ -111,15 +117,110 @@ streaming_fail_cb(struct evhttp_connection *evcon, void *arg)
|
||||
{
|
||||
DPRINTF(E_INFO, L_STREAMING, "No more clients, will stop streaming\n");
|
||||
event_del(streamingev);
|
||||
event_del(metaev);
|
||||
}
|
||||
}
|
||||
|
||||
static void
|
||||
streaming_end(void)
|
||||
{
|
||||
struct streaming_session *session;
|
||||
struct evhttp_connection *evcon;
|
||||
|
||||
for (session = streaming_sessions; streaming_sessions; session = streaming_sessions)
|
||||
{
|
||||
evcon = evhttp_request_get_connection(session->req);
|
||||
if (evcon)
|
||||
evhttp_connection_set_closecb(evcon, NULL, NULL);
|
||||
evhttp_send_reply_end(session->req);
|
||||
|
||||
streaming_sessions = session->next;
|
||||
free(session);
|
||||
}
|
||||
|
||||
event_del(streamingev);
|
||||
event_del(metaev);
|
||||
}
|
||||
|
||||
static void
|
||||
streaming_meta_cb(evutil_socket_t fd, short event, void *arg)
|
||||
{
|
||||
struct media_quality mp3_quality = { STREAMING_MP3_SAMPLE_RATE, STREAMING_MP3_BPS, STREAMING_MP3_CHANNELS };
|
||||
struct media_quality quality;
|
||||
struct decode_ctx *decode_ctx;
|
||||
int ret;
|
||||
|
||||
transcode_encode_cleanup(&streaming_encode_ctx);
|
||||
|
||||
ret = read(fd, &quality, sizeof(struct media_quality));
|
||||
if (ret != sizeof(struct media_quality))
|
||||
goto error;
|
||||
|
||||
decode_ctx = NULL;
|
||||
if (quality.bits_per_sample == 16)
|
||||
decode_ctx = transcode_decode_setup_raw(XCODE_PCM16, &quality);
|
||||
else if (quality.bits_per_sample == 24)
|
||||
decode_ctx = transcode_decode_setup_raw(XCODE_PCM24, &quality);
|
||||
else if (quality.bits_per_sample == 32)
|
||||
decode_ctx = transcode_decode_setup_raw(XCODE_PCM32, &quality);
|
||||
|
||||
if (!decode_ctx)
|
||||
goto error;
|
||||
|
||||
streaming_encode_ctx = transcode_encode_setup(XCODE_MP3, &mp3_quality, decode_ctx, NULL, 0, 0);
|
||||
transcode_decode_cleanup(&decode_ctx);
|
||||
if (!streaming_encode_ctx)
|
||||
{
|
||||
DPRINTF(E_LOG, L_STREAMING, "Will not be able to stream MP3, libav does not support MP3 encoding\n");
|
||||
streaming_not_supported = 1;
|
||||
return;
|
||||
}
|
||||
|
||||
streaming_quality = quality;
|
||||
streaming_not_supported = 0;
|
||||
|
||||
return;
|
||||
|
||||
error:
|
||||
DPRINTF(E_LOG, L_STREAMING, "Unknown or unsupported quality of input data (%d/%d/%d), cannot MP3 encode\n", quality.sample_rate, quality.bits_per_sample, quality.channels);
|
||||
streaming_not_supported = 1;
|
||||
streaming_end();
|
||||
}
|
||||
|
||||
static int
|
||||
encode_buffer(uint8_t *buffer, size_t size)
|
||||
{
|
||||
transcode_frame *frame;
|
||||
int samples;
|
||||
int ret;
|
||||
|
||||
if (streaming_not_supported || streaming_quality.channels == 0)
|
||||
{
|
||||
DPRINTF(E_LOG, L_STREAMING, "Streaming unsuppored or quality is zero\n");
|
||||
return -1;
|
||||
}
|
||||
|
||||
samples = BTOS(size, streaming_quality.bits_per_sample, streaming_quality.channels);
|
||||
|
||||
frame = transcode_frame_new(buffer, size, samples, &streaming_quality);
|
||||
if (!frame)
|
||||
{
|
||||
DPRINTF(E_LOG, L_STREAMING, "Could not convert raw PCM to frame\n");
|
||||
return -1;
|
||||
}
|
||||
|
||||
ret = transcode_encode(streaming_encoded_data, streaming_encode_ctx, frame, 0);
|
||||
transcode_frame_free(frame);
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
static void
|
||||
streaming_send_cb(evutil_socket_t fd, short event, void *arg)
|
||||
{
|
||||
struct streaming_session *session;
|
||||
struct evbuffer *evbuf;
|
||||
void *frame;
|
||||
uint8_t rawbuf[STREAMING_READ_SIZE];
|
||||
uint8_t *buf;
|
||||
int len;
|
||||
int ret;
|
||||
@ -127,24 +228,16 @@ streaming_send_cb(evutil_socket_t fd, short event, void *arg)
|
||||
// Player wrote data to the pipe (EV_READ)
|
||||
if (event & EV_READ)
|
||||
{
|
||||
ret = read(streaming_pipe[0], &streaming_rawbuf, STREAMING_RAWBUF_SIZE);
|
||||
if (ret < 0)
|
||||
return;
|
||||
|
||||
if (!streaming_sessions)
|
||||
return;
|
||||
|
||||
frame = transcode_frame_new(XCODE_MP3, streaming_rawbuf, STREAMING_RAWBUF_SIZE);
|
||||
if (!frame)
|
||||
while (1)
|
||||
{
|
||||
DPRINTF(E_LOG, L_STREAMING, "Could not convert raw PCM to frame\n");
|
||||
return;
|
||||
}
|
||||
ret = read(fd, &rawbuf, sizeof(rawbuf));
|
||||
if (ret <= 0)
|
||||
break;
|
||||
|
||||
ret = transcode_encode(streaming_encoded_data, streaming_encode_ctx, frame, 0);
|
||||
transcode_frame_free(frame);
|
||||
if (ret < 0)
|
||||
return;
|
||||
ret = encode_buffer(rawbuf, ret);
|
||||
if (ret < 0)
|
||||
return;
|
||||
}
|
||||
}
|
||||
// Event timed out, let's see what the player is doing and send silence if it is paused
|
||||
else
|
||||
@ -155,16 +248,18 @@ streaming_send_cb(evutil_socket_t fd, short event, void *arg)
|
||||
player_get_status(&streaming_player_status);
|
||||
}
|
||||
|
||||
if (!streaming_sessions)
|
||||
return;
|
||||
|
||||
if (streaming_player_status.status != PLAY_PAUSED)
|
||||
return;
|
||||
|
||||
evbuffer_add(streaming_encoded_data, streaming_silence_data, streaming_silence_size);
|
||||
memset(&rawbuf, 0, sizeof(rawbuf));
|
||||
ret = encode_buffer(rawbuf, sizeof(rawbuf));
|
||||
if (ret < 0)
|
||||
return;
|
||||
}
|
||||
|
||||
len = evbuffer_get_length(streaming_encoded_data);
|
||||
if (len == 0)
|
||||
return;
|
||||
|
||||
// Send data
|
||||
evbuf = evbuffer_new();
|
||||
@ -179,6 +274,7 @@ streaming_send_cb(evutil_socket_t fd, short event, void *arg)
|
||||
else
|
||||
evhttp_send_reply_chunk(session->req, streaming_encoded_data);
|
||||
}
|
||||
|
||||
evbuffer_free(evbuf);
|
||||
}
|
||||
|
||||
@ -191,14 +287,24 @@ player_change_cb(short event_mask)
|
||||
|
||||
// Thread: player (also prone to race conditions, mostly during deinit)
|
||||
void
|
||||
streaming_write(uint8_t *buf, uint64_t rtptime)
|
||||
streaming_write(struct output_buffer *obuf)
|
||||
{
|
||||
int ret;
|
||||
|
||||
if (!streaming_sessions)
|
||||
return;
|
||||
|
||||
ret = write(streaming_pipe[1], buf, STREAMING_RAWBUF_SIZE);
|
||||
if (!quality_is_equal(&obuf->data[0].quality, &streaming_quality))
|
||||
{
|
||||
ret = write(streaming_meta[1], &obuf->data[0].quality, sizeof(struct media_quality));
|
||||
if (ret < 0)
|
||||
{
|
||||
DPRINTF(E_LOG, L_STREAMING, "Error writing to streaming pipe: %s\n", strerror(errno));
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
ret = write(streaming_pipe[1], obuf->data[0].buffer, obuf->data[0].bufsize);
|
||||
if (ret < 0)
|
||||
{
|
||||
if (errno == EAGAIN)
|
||||
@ -219,9 +325,9 @@ streaming_request(struct evhttp_request *req, struct httpd_uri_parsed *uri_parse
|
||||
char *address;
|
||||
ev_uint16_t port;
|
||||
|
||||
if (!streaming_initialized)
|
||||
if (streaming_not_supported)
|
||||
{
|
||||
DPRINTF(E_LOG, L_STREAMING, "Got mp3 streaming request, but cannot encode to mp3\n");
|
||||
DPRINTF(E_LOG, L_STREAMING, "Got MP3 streaming request, but cannot encode to MP3\n");
|
||||
|
||||
evhttp_send_error(req, HTTP_NOTFOUND, "Not Found");
|
||||
return -1;
|
||||
@ -258,7 +364,10 @@ streaming_request(struct evhttp_request *req, struct httpd_uri_parsed *uri_parse
|
||||
}
|
||||
|
||||
if (!streaming_sessions)
|
||||
event_add(streamingev, &streaming_silence_tv);
|
||||
{
|
||||
event_add(streamingev, &streaming_silence_tv);
|
||||
event_add(metaev, NULL);
|
||||
}
|
||||
|
||||
session->req = req;
|
||||
session->next = streaming_sessions;
|
||||
@ -284,26 +393,8 @@ streaming_is_request(const char *path)
|
||||
int
|
||||
streaming_init(void)
|
||||
{
|
||||
struct decode_ctx *decode_ctx;
|
||||
void *frame;
|
||||
int remaining;
|
||||
int ret;
|
||||
|
||||
decode_ctx = transcode_decode_setup_raw();
|
||||
if (!decode_ctx)
|
||||
{
|
||||
DPRINTF(E_LOG, L_STREAMING, "Could not create decoding context\n");
|
||||
return -1;
|
||||
}
|
||||
|
||||
streaming_encode_ctx = transcode_encode_setup(XCODE_MP3, decode_ctx, NULL, 0, 0);
|
||||
transcode_decode_cleanup(&decode_ctx);
|
||||
if (!streaming_encode_ctx)
|
||||
{
|
||||
DPRINTF(E_LOG, L_STREAMING, "Will not be able to stream mp3, libav does not support mp3 encoding\n");
|
||||
return -1;
|
||||
}
|
||||
|
||||
// Non-blocking because otherwise httpd and player thread may deadlock
|
||||
#ifdef HAVE_PIPE2
|
||||
ret = pipe2(streaming_pipe, O_CLOEXEC | O_NONBLOCK);
|
||||
@ -318,7 +409,23 @@ streaming_init(void)
|
||||
if (ret < 0)
|
||||
{
|
||||
DPRINTF(E_FATAL, L_STREAMING, "Could not create pipe: %s\n", strerror(errno));
|
||||
goto pipe_fail;
|
||||
goto error;
|
||||
}
|
||||
|
||||
#ifdef HAVE_PIPE2
|
||||
ret = pipe2(streaming_meta, O_CLOEXEC | O_NONBLOCK);
|
||||
#else
|
||||
if ( pipe(streaming_meta) < 0 ||
|
||||
fcntl(streaming_meta[0], F_SETFL, O_CLOEXEC | O_NONBLOCK) < 0 ||
|
||||
fcntl(streaming_meta[1], F_SETFL, O_CLOEXEC | O_NONBLOCK) < 0 )
|
||||
ret = -1;
|
||||
else
|
||||
ret = 0;
|
||||
#endif
|
||||
if (ret < 0)
|
||||
{
|
||||
DPRINTF(E_FATAL, L_STREAMING, "Could not create pipe: %s\n", strerror(errno));
|
||||
goto error;
|
||||
}
|
||||
|
||||
// Listen to playback changes so we don't have to poll to check for pausing
|
||||
@ -326,77 +433,22 @@ streaming_init(void)
|
||||
if (ret < 0)
|
||||
{
|
||||
DPRINTF(E_FATAL, L_STREAMING, "Could not add listener\n");
|
||||
goto listener_fail;
|
||||
goto error;
|
||||
}
|
||||
|
||||
// Initialize buffer for encoded mp3 audio and event for pipe reading
|
||||
streaming_encoded_data = evbuffer_new();
|
||||
streamingev = event_new(evbase_httpd, streaming_pipe[0], EV_TIMEOUT | EV_READ | EV_PERSIST, streaming_send_cb, NULL);
|
||||
if (!streaming_encoded_data || !streamingev)
|
||||
{
|
||||
DPRINTF(E_LOG, L_STREAMING, "Out of memory for encoded_data or event\n");
|
||||
goto event_fail;
|
||||
}
|
||||
CHECK_NULL(L_STREAMING, streaming_encoded_data = evbuffer_new());
|
||||
|
||||
// Encode some silence which will be used for playback pause and put in a permanent buffer
|
||||
remaining = STREAMING_SILENCE_INTERVAL * STOB(44100);
|
||||
while (remaining > STREAMING_RAWBUF_SIZE)
|
||||
{
|
||||
frame = transcode_frame_new(XCODE_MP3, streaming_rawbuf, STREAMING_RAWBUF_SIZE);
|
||||
if (!frame)
|
||||
{
|
||||
DPRINTF(E_LOG, L_STREAMING, "Could not convert raw PCM to frame\n");
|
||||
goto silence_fail;
|
||||
}
|
||||
|
||||
ret = transcode_encode(streaming_encoded_data, streaming_encode_ctx, frame, 0);
|
||||
transcode_frame_free(frame);
|
||||
if (ret < 0)
|
||||
{
|
||||
DPRINTF(E_LOG, L_STREAMING, "Could not encode silence buffer\n");
|
||||
goto silence_fail;
|
||||
}
|
||||
|
||||
remaining -= STREAMING_RAWBUF_SIZE;
|
||||
}
|
||||
|
||||
streaming_silence_size = evbuffer_get_length(streaming_encoded_data);
|
||||
if (streaming_silence_size == 0)
|
||||
{
|
||||
DPRINTF(E_LOG, L_STREAMING, "The encoder didn't encode any silence\n");
|
||||
goto silence_fail;
|
||||
}
|
||||
|
||||
streaming_silence_data = malloc(streaming_silence_size);
|
||||
if (!streaming_silence_data)
|
||||
{
|
||||
DPRINTF(E_LOG, L_STREAMING, "Out of memory for streaming_silence_data\n");
|
||||
goto silence_fail;
|
||||
}
|
||||
|
||||
ret = evbuffer_remove(streaming_encoded_data, streaming_silence_data, streaming_silence_size);
|
||||
if (ret != streaming_silence_size)
|
||||
{
|
||||
DPRINTF(E_LOG, L_STREAMING, "Unknown error while copying silence buffer\n");
|
||||
free(streaming_silence_data);
|
||||
goto silence_fail;
|
||||
}
|
||||
|
||||
// All done
|
||||
streaming_initialized = 1;
|
||||
CHECK_NULL(L_STREAMING, streamingev = event_new(evbase_httpd, streaming_pipe[0], EV_TIMEOUT | EV_READ | EV_PERSIST, streaming_send_cb, NULL));
|
||||
CHECK_NULL(L_STREAMING, metaev = event_new(evbase_httpd, streaming_meta[0], EV_READ | EV_PERSIST, streaming_meta_cb, NULL));
|
||||
|
||||
return 0;
|
||||
|
||||
silence_fail:
|
||||
event_free(streamingev);
|
||||
evbuffer_free(streaming_encoded_data);
|
||||
event_fail:
|
||||
listener_remove(player_change_cb);
|
||||
listener_fail:
|
||||
error:
|
||||
close(streaming_pipe[0]);
|
||||
close(streaming_pipe[1]);
|
||||
pipe_fail:
|
||||
transcode_encode_cleanup(&streaming_encode_ctx);
|
||||
close(streaming_meta[0]);
|
||||
close(streaming_meta[1]);
|
||||
|
||||
return -1;
|
||||
}
|
||||
@ -407,9 +459,6 @@ streaming_deinit(void)
|
||||
struct streaming_session *session;
|
||||
struct streaming_session *next;
|
||||
|
||||
if (!streaming_initialized)
|
||||
return;
|
||||
|
||||
session = streaming_sessions;
|
||||
streaming_sessions = NULL; // Stops writing and sending
|
||||
|
||||
@ -428,8 +477,9 @@ streaming_deinit(void)
|
||||
|
||||
close(streaming_pipe[0]);
|
||||
close(streaming_pipe[1]);
|
||||
close(streaming_meta[0]);
|
||||
close(streaming_meta[1]);
|
||||
|
||||
transcode_encode_cleanup(&streaming_encode_ctx);
|
||||
evbuffer_free(streaming_encoded_data);
|
||||
free(streaming_silence_data);
|
||||
}
|
||||
|
@ -3,6 +3,7 @@
|
||||
#define __HTTPD_STREAMING_H__
|
||||
|
||||
#include "httpd.h"
|
||||
#include "outputs.h"
|
||||
|
||||
/* httpd_streaming takes care of incoming requests to /stream.mp3
|
||||
* It will receive decoded audio from the player, and encode it, and
|
||||
@ -11,7 +12,7 @@
|
||||
*/
|
||||
|
||||
void
|
||||
streaming_write(uint8_t *buf, uint64_t rtptime);
|
||||
streaming_write(struct output_buffer *obuf);
|
||||
|
||||
int
|
||||
streaming_request(struct evhttp_request *req, struct httpd_uri_parsed *uri_parsed);
|
||||
|
922
src/input.c
922
src/input.c
File diff suppressed because it is too large
Load Diff
192
src/input.h
192
src/input.h
@ -6,7 +6,8 @@
|
||||
# include <config.h>
|
||||
#endif
|
||||
#include <event2/buffer.h>
|
||||
#include "transcode.h"
|
||||
#include "db.h"
|
||||
#include "misc.h"
|
||||
|
||||
// Must be in sync with inputs[] in input.c
|
||||
enum input_types
|
||||
@ -21,83 +22,74 @@ 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),
|
||||
};
|
||||
|
||||
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);
|
||||
|
||||
struct input_metadata
|
||||
{
|
||||
// queue_item id
|
||||
uint32_t item_id;
|
||||
|
||||
int startup;
|
||||
// Input can override the default player progress by setting this
|
||||
// FIXME only implemented for Airplay speakers currently
|
||||
uint32_t pos_ms;
|
||||
|
||||
uint64_t rtptime;
|
||||
uint64_t offset;
|
||||
|
||||
// The player will update queue_item with the below
|
||||
uint32_t song_length;
|
||||
// Sets new song length (input will also update queue_item)
|
||||
uint32_t len_ms;
|
||||
|
||||
// Input can update queue_item with the below
|
||||
char *artist;
|
||||
char *title;
|
||||
char *album;
|
||||
char *genre;
|
||||
char *artwork_url;
|
||||
|
||||
// Indicates whether we are starting playback. Just passed on to output.
|
||||
int startup;
|
||||
};
|
||||
|
||||
struct input_definition
|
||||
@ -112,65 +104,75 @@ 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. 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).
|
||||
* 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.
|
||||
*
|
||||
* @in evbuf Raw audio data to write
|
||||
* @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, short flags);
|
||||
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.
|
||||
*
|
||||
* @in data Output buffer
|
||||
* @in size How much data to move to the output buffer
|
||||
* @out flags Flags INPUT_FLAG_*
|
||||
* @out flag Flag INPUT_FLAG_*
|
||||
* @out flagdata Data associated with the flag, e.g. quality or metadata struct
|
||||
* @return Number of bytes moved, -1 on error
|
||||
*/
|
||||
int
|
||||
input_read(void *data, size_t size, short *flags);
|
||||
input_read(void *data, size_t size, short *flag, void **flagdata);
|
||||
|
||||
/*
|
||||
* Player can set this to get a callback from the input when the input buffer
|
||||
@ -182,39 +184,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().
|
||||
@ -222,12 +216,6 @@ input_seek(struct player_source *ps, int seek_ms);
|
||||
void
|
||||
input_flush(short *flags);
|
||||
|
||||
/*
|
||||
* 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);
|
||||
|
||||
/*
|
||||
* Free the entire struct
|
||||
*/
|
||||
|
@ -26,93 +26,107 @@
|
||||
#include "transcode.h"
|
||||
#include "http.h"
|
||||
#include "misc.h"
|
||||
#include "logger.h"
|
||||
#include "input.h"
|
||||
|
||||
static int
|
||||
setup(struct player_source *ps)
|
||||
setup(struct input_source *source)
|
||||
{
|
||||
ps->input_ctx = transcode_setup(XCODE_PCM16_NOHEADER, ps->data_kind, ps->path, ps->len_ms, NULL);
|
||||
if (!ps->input_ctx)
|
||||
struct transcode_ctx *ctx;
|
||||
|
||||
ctx = transcode_setup(XCODE_PCM_NATIVE, NULL, 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 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, &icy_timer, ps->input_ctx, 1);
|
||||
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)
|
||||
{
|
||||
struct transcode_ctx *ctx = ps->input_ctx;
|
||||
struct transcode_ctx *ctx = source->input_ctx;
|
||||
|
||||
transcode_cleanup(&ctx);
|
||||
|
||||
ps->input_ctx = NULL;
|
||||
ps->setup_done = 0;
|
||||
if (source->evbuf)
|
||||
evbuffer_free(source->evbuf);
|
||||
|
||||
source->input_ctx = NULL;
|
||||
source->evbuf = 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;
|
||||
|
||||
@ -140,7 +154,7 @@ struct input_definition input_file =
|
||||
.type = INPUT_TYPE_FILE,
|
||||
.disabled = 0,
|
||||
.setup = setup,
|
||||
.start = start,
|
||||
.play = play,
|
||||
.stop = stop,
|
||||
.seek = seek,
|
||||
};
|
||||
@ -151,7 +165,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,
|
||||
};
|
||||
|
@ -103,6 +103,9 @@ static pthread_t tid_pipe;
|
||||
static struct event_base *evbase_pipe;
|
||||
static struct commands_base *cmdbase;
|
||||
|
||||
// From config - the sample rate and bps of the pipe input
|
||||
static int pipe_sample_rate;
|
||||
static int pipe_bits_per_sample;
|
||||
// From config - should we watch library pipes for data or only start on request
|
||||
static int pipe_autostart;
|
||||
// The mfi id of the pipe autostarted by the pipe thread
|
||||
@ -122,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)
|
||||
@ -305,9 +308,10 @@ parse_progress(struct input_metadata *m, char *progress)
|
||||
if (!start || !pos || !end)
|
||||
return;
|
||||
|
||||
m->rtptime = start; // Not actually used - we have our own rtptime
|
||||
m->offset = (pos > start) ? (pos - start) : 0;
|
||||
m->song_length = (end - start) * 10 / 441; // Convert to ms based on 44100
|
||||
if (pos > start)
|
||||
m->pos_ms = (pos - start) * 1000 / pipe_sample_rate;
|
||||
if (end > start)
|
||||
m->len_ms = (end - start) * 1000 / pipe_sample_rate;
|
||||
}
|
||||
|
||||
static void
|
||||
@ -497,8 +501,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
|
||||
@ -614,8 +618,8 @@ pipe_thread_run(void *arg)
|
||||
}
|
||||
|
||||
|
||||
/* -------------------------- METADATA PIPE HANDLING ---------------------- */
|
||||
/* Thread: worker */
|
||||
/* --------------------------- METADATA PIPE HANDLING ----------------------- */
|
||||
/* Thread: worker */
|
||||
|
||||
static void
|
||||
pipe_metadata_watch_del(void *arg)
|
||||
@ -703,8 +707,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)
|
||||
@ -799,87 +803,47 @@ 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 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;
|
||||
}
|
||||
|
||||
ret = -1;
|
||||
while (!input_loop_break)
|
||||
{
|
||||
ret = evbuffer_read(evbuf, pipe->fd, PIPE_READ_MAX);
|
||||
if ((ret == 0) && (pipe->is_autostarted))
|
||||
{
|
||||
input_write(evbuf, 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, 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");
|
||||
|
||||
if (source->evbuf)
|
||||
evbuffer_free(source->evbuf);
|
||||
|
||||
pipe_close(pipe->fd);
|
||||
|
||||
// Reset the pipe and start watching it again for new data. Must be async or
|
||||
@ -895,37 +859,56 @@ stop(struct player_source *ps)
|
||||
|
||||
pipe_free(pipe);
|
||||
|
||||
ps->input_ctx = NULL;
|
||||
ps->setup_done = 0;
|
||||
source->input_ctx = NULL;
|
||||
source->evbuf = 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);
|
||||
|
||||
if (pipe_metadata_parsed.artist)
|
||||
swap_pointers(&metadata->artist, &pipe_metadata_parsed.artist);
|
||||
if (pipe_metadata_parsed.title)
|
||||
swap_pointers(&metadata->title, &pipe_metadata_parsed.title);
|
||||
if (pipe_metadata_parsed.album)
|
||||
swap_pointers(&metadata->album, &pipe_metadata_parsed.album);
|
||||
if (pipe_metadata_parsed.genre)
|
||||
swap_pointers(&metadata->genre, &pipe_metadata_parsed.genre);
|
||||
if (pipe_metadata_parsed.artwork_url)
|
||||
swap_pointers(&metadata->artwork_url, &pipe_metadata_parsed.artwork_url);
|
||||
|
||||
if (pipe_metadata_parsed.song_length)
|
||||
{
|
||||
if (rtptime > ps->stream_start)
|
||||
metadata->rtptime = rtptime - pipe_metadata_parsed.offset;
|
||||
metadata->offset = pipe_metadata_parsed.offset;
|
||||
metadata->song_length = pipe_metadata_parsed.song_length;
|
||||
}
|
||||
|
||||
input_metadata_free(&pipe_metadata_parsed, 1);
|
||||
*metadata = pipe_metadata_parsed;
|
||||
|
||||
pthread_mutex_unlock(&pipe_metadata_lock);
|
||||
|
||||
@ -945,6 +928,20 @@ init(void)
|
||||
CHECK_ERR(L_PLAYER, listener_add(pipe_listener_cb, LISTENER_DATABASE));
|
||||
}
|
||||
|
||||
pipe_sample_rate = cfg_getint(cfg_getsec(cfg, "library"), "pipe_sample_rate");
|
||||
if (pipe_sample_rate != 44100 && pipe_sample_rate != 48000 && pipe_sample_rate != 96000)
|
||||
{
|
||||
DPRINTF(E_FATAL, L_PLAYER, "The configuration of pipe_sample_rate is invalid: %d\n", pipe_sample_rate);
|
||||
return -1;
|
||||
}
|
||||
|
||||
pipe_bits_per_sample = cfg_getint(cfg_getsec(cfg, "library"), "pipe_bits_per_sample");
|
||||
if (pipe_bits_per_sample != 16 && pipe_bits_per_sample != 24)
|
||||
{
|
||||
DPRINTF(E_FATAL, L_PLAYER, "The configuration of pipe_bits_per_sample is invalid: %d\n", pipe_bits_per_sample);
|
||||
return -1;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
@ -966,7 +963,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;
|
||||
|
||||
@ -105,7 +86,6 @@ struct input_definition input_spotify =
|
||||
.type = INPUT_TYPE_SPOTIFY,
|
||||
.disabled = 0,
|
||||
.setup = setup,
|
||||
.start = start,
|
||||
.stop = stop,
|
||||
.seek = seek,
|
||||
};
|
||||
|
@ -416,21 +416,13 @@ scan_metadata_ffmpeg(const char *file, struct media_file_info *mfi)
|
||||
|
||||
for (i = 0; i < ctx->nb_streams; i++)
|
||||
{
|
||||
#if HAVE_DECL_AVCODEC_PARAMETERS_FROM_CONTEXT
|
||||
codec_type = ctx->streams[i]->codecpar->codec_type;
|
||||
codec_id = ctx->streams[i]->codecpar->codec_id;
|
||||
sample_rate = ctx->streams[i]->codecpar->sample_rate;
|
||||
sample_fmt = ctx->streams[i]->codecpar->format;
|
||||
#else
|
||||
codec_type = ctx->streams[i]->codec->codec_type;
|
||||
codec_id = ctx->streams[i]->codec->codec_id;
|
||||
sample_rate = ctx->streams[i]->codec->sample_rate;
|
||||
sample_fmt = ctx->streams[i]->codec->sample_fmt;
|
||||
#endif
|
||||
switch (codec_type)
|
||||
{
|
||||
case AVMEDIA_TYPE_VIDEO:
|
||||
#if LIBAVFORMAT_VERSION_MAJOR >= 55 || (LIBAVFORMAT_VERSION_MAJOR == 54 && LIBAVFORMAT_VERSION_MINOR >= 6)
|
||||
if (ctx->streams[i]->disposition & AV_DISPOSITION_ATTACHED_PIC)
|
||||
{
|
||||
DPRINTF(E_DBG, L_SCAN, "Found embedded artwork (stream %d)\n", i);
|
||||
@ -438,7 +430,7 @@ scan_metadata_ffmpeg(const char *file, struct media_file_info *mfi)
|
||||
|
||||
break;
|
||||
}
|
||||
#endif
|
||||
|
||||
// We treat these as audio no matter what
|
||||
if (mfi->compilation || (mfi->media_kind & (MEDIA_KIND_PODCAST | MEDIA_KIND_AUDIOBOOK)))
|
||||
break;
|
||||
|
120
src/misc.c
120
src/misc.c
@ -1040,6 +1040,46 @@ murmur_hash64(const void *key, int len, uint32_t seed)
|
||||
#endif
|
||||
|
||||
|
||||
int
|
||||
linear_regression(double *m, double *b, double *r2, const double *x, const double *y, int n)
|
||||
{
|
||||
double x_val;
|
||||
double sum_x = 0;
|
||||
double sum_x2 = 0;
|
||||
double sum_y = 0;
|
||||
double sum_y2 = 0;
|
||||
double sum_xy = 0;
|
||||
double denom;
|
||||
int i;
|
||||
|
||||
for (i = 0; i < n; i++)
|
||||
{
|
||||
x_val = x ? x[i] : (double)i;
|
||||
sum_x += x_val;
|
||||
sum_x2 += x_val * x_val;
|
||||
sum_y += y[i];
|
||||
sum_y2 += y[i] * y[i];
|
||||
sum_xy += x_val * y[i];
|
||||
}
|
||||
|
||||
denom = (n * sum_x2 - sum_x * sum_x);
|
||||
if (denom == 0)
|
||||
return -1;
|
||||
|
||||
*m = (n * sum_xy - sum_x * sum_y) / denom;
|
||||
*b = (sum_y * sum_x2 - sum_x * sum_xy) / denom;
|
||||
if (r2)
|
||||
*r2 = (sum_xy - (sum_x * sum_y)/n) * (sum_xy - (sum_x * sum_y)/n) / ((sum_x2 - (sum_x * sum_x)/n) * (sum_y2 - (sum_y * sum_y)/n));
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
bool
|
||||
quality_is_equal(struct media_quality *a, struct media_quality *b)
|
||||
{
|
||||
return (a->sample_rate == b->sample_rate && a->bits_per_sample == b->bits_per_sample && a->channels == b->channels);
|
||||
}
|
||||
|
||||
bool
|
||||
peer_address_is_trusted(const char *addr)
|
||||
{
|
||||
@ -1077,6 +1117,86 @@ peer_address_is_trusted(const char *addr)
|
||||
return false;
|
||||
}
|
||||
|
||||
int
|
||||
ringbuffer_init(struct ringbuffer *buf, size_t size)
|
||||
{
|
||||
memset(buf, 0, sizeof(struct ringbuffer));
|
||||
|
||||
CHECK_NULL(L_MISC, buf->buffer = malloc(size));
|
||||
buf->size = size;
|
||||
buf->write_avail = size;
|
||||
return 0;
|
||||
}
|
||||
|
||||
void
|
||||
ringbuffer_free(struct ringbuffer *buf, bool content_only)
|
||||
{
|
||||
if (!buf)
|
||||
return;
|
||||
|
||||
free(buf->buffer);
|
||||
|
||||
if (content_only)
|
||||
memset(buf, 0, sizeof(struct ringbuffer));
|
||||
else
|
||||
free(buf);
|
||||
}
|
||||
|
||||
size_t
|
||||
ringbuffer_write(struct ringbuffer *buf, const void* src, size_t srclen)
|
||||
{
|
||||
int remaining;
|
||||
|
||||
if (buf->write_avail == 0 || srclen == 0)
|
||||
return 0;
|
||||
|
||||
if (srclen > buf->write_avail)
|
||||
srclen = buf->write_avail;
|
||||
|
||||
remaining = buf->size - buf->write_pos;
|
||||
if (srclen > remaining)
|
||||
{
|
||||
memcpy(buf->buffer + buf->write_pos, src, remaining);
|
||||
memcpy(buf->buffer, src + remaining, srclen - remaining);
|
||||
}
|
||||
else
|
||||
{
|
||||
memcpy(buf->buffer + buf->write_pos, src, srclen);
|
||||
}
|
||||
|
||||
buf->write_pos = (buf->write_pos + srclen) % buf->size;
|
||||
|
||||
buf->write_avail -= srclen;
|
||||
buf->read_avail += srclen;
|
||||
|
||||
return srclen;
|
||||
}
|
||||
|
||||
size_t
|
||||
ringbuffer_read(uint8_t **dst, size_t dstlen, struct ringbuffer *buf)
|
||||
{
|
||||
int remaining;
|
||||
|
||||
*dst = buf->buffer + buf->read_pos;
|
||||
|
||||
if (buf->read_avail == 0 || dstlen == 0)
|
||||
return 0;
|
||||
|
||||
remaining = buf->size - buf->read_pos;
|
||||
|
||||
// The number of bytes we will return will be MIN(dstlen, remaining, read_avail)
|
||||
if (dstlen > remaining)
|
||||
dstlen = remaining;
|
||||
if (dstlen > buf->read_avail)
|
||||
dstlen = buf->read_avail;
|
||||
|
||||
buf->read_pos = (buf->read_pos + dstlen) % buf->size;
|
||||
|
||||
buf->write_avail += dstlen;
|
||||
buf->read_avail -= dstlen;
|
||||
|
||||
return dstlen;
|
||||
}
|
||||
|
||||
int
|
||||
clock_gettime_with_res(clockid_t clock_id, struct timespec *tp, struct timespec *res)
|
||||
|
47
src/misc.h
47
src/misc.h
@ -12,11 +12,27 @@
|
||||
#include <pthread.h>
|
||||
|
||||
/* Samples to bytes, bytes to samples */
|
||||
#define STOB(s) ((s) * 4)
|
||||
#define BTOS(b) ((b) / 4)
|
||||
#define STOB(s, bits, c) ((s) * (c) * (bits) / 8)
|
||||
#define BTOS(b, bits, c) ((b) / ((c) * (bits) / 8))
|
||||
|
||||
#define ARRAY_SIZE(x) ((unsigned int)(sizeof(x) / sizeof((x)[0])))
|
||||
|
||||
#ifndef MIN
|
||||
# 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;
|
||||
int bits_per_sample;
|
||||
int channels;
|
||||
};
|
||||
|
||||
struct onekeyval {
|
||||
char *name;
|
||||
char *value;
|
||||
@ -30,6 +46,15 @@ struct keyval {
|
||||
struct onekeyval *tail;
|
||||
};
|
||||
|
||||
struct ringbuffer {
|
||||
uint8_t *buffer;
|
||||
size_t size;
|
||||
size_t write_avail;
|
||||
size_t read_avail;
|
||||
size_t write_pos;
|
||||
size_t read_pos;
|
||||
};
|
||||
|
||||
|
||||
char **
|
||||
buildopts_get(void);
|
||||
@ -114,10 +139,28 @@ b64_encode(const uint8_t *in, size_t len);
|
||||
uint64_t
|
||||
murmur_hash64(const void *key, int len, uint32_t seed);
|
||||
|
||||
int
|
||||
linear_regression(double *m, double *b, double *r, const double *x, const double *y, int n);
|
||||
|
||||
bool
|
||||
quality_is_equal(struct media_quality *a, struct media_quality *b);
|
||||
|
||||
// Checks if the address is in a network that is configured as trusted
|
||||
bool
|
||||
peer_address_is_trusted(const char *addr);
|
||||
|
||||
int
|
||||
ringbuffer_init(struct ringbuffer *buf, size_t size);
|
||||
|
||||
void
|
||||
ringbuffer_free(struct ringbuffer *buf, bool content_only);
|
||||
|
||||
size_t
|
||||
ringbuffer_write(struct ringbuffer *buf, const void* src, size_t srclen);
|
||||
|
||||
size_t
|
||||
ringbuffer_read(uint8_t **dst, size_t dstlen, struct ringbuffer *buf);
|
||||
|
||||
|
||||
#ifndef HAVE_CLOCK_GETTIME
|
||||
|
||||
|
998
src/outputs.c
998
src/outputs.c
File diff suppressed because it is too large
Load Diff
202
src/outputs.h
202
src/outputs.h
@ -2,7 +2,11 @@
|
||||
#ifndef __OUTPUTS_H__
|
||||
#define __OUTPUTS_H__
|
||||
|
||||
#include <stdbool.h>
|
||||
#include <time.h>
|
||||
#include <event2/event.h>
|
||||
#include <event2/buffer.h>
|
||||
#include "misc.h"
|
||||
|
||||
/* Outputs is a generic interface between the player and a media output method,
|
||||
* like for instance AirPlay (raop) or ALSA. The purpose of the interface is to
|
||||
@ -19,7 +23,6 @@
|
||||
* Here is the sequence of commands from the player to the outputs, and the
|
||||
* callback from the output once the command has been executed. Commands marked
|
||||
* with * may make multiple callbacks if multiple sessions are affected.
|
||||
* (TODO should callbacks always be deferred?)
|
||||
*
|
||||
* PLAYER OUTPUT PLAYER CB
|
||||
* speaker_activate -> device_start -> device_activate_cb
|
||||
@ -46,6 +49,28 @@
|
||||
*
|
||||
*/
|
||||
|
||||
// If an output requires a specific quality (like Airplay 1 devices often
|
||||
// require 44100/16) then it should make a subscription request to the output
|
||||
// module, which will then make sure to include this quality when it writes the
|
||||
// audio. The below sets the maximum number of *different* subscriptions
|
||||
// allowed. Note that multiple outputs requesting the *same* quality only counts
|
||||
// as one.
|
||||
#define OUTPUTS_MAX_QUALITY_SUBSCRIPTIONS 5
|
||||
|
||||
// Number of seconds the outputs should buffer before starting playback. Note
|
||||
// this value cannot freely be changed because 1) some Airplay devices ignore
|
||||
// the values we give and stick to 2 seconds, 2) those devices that can handle
|
||||
// different values can only do so within a limited range (maybe max 3 secs)
|
||||
#define OUTPUTS_BUFFER_DURATION 2
|
||||
|
||||
// Forward declarations
|
||||
struct output_device;
|
||||
struct output_metadata;
|
||||
enum output_device_state;
|
||||
|
||||
typedef void (*output_status_cb)(struct output_device *device, enum output_device_state status);
|
||||
typedef int (*output_metadata_finalize_cb)(struct output_metadata *metadata);
|
||||
|
||||
// Must be in sync with outputs[] in outputs.c
|
||||
enum output_types
|
||||
{
|
||||
@ -113,35 +138,59 @@ struct output_device
|
||||
int volume;
|
||||
int relvol;
|
||||
|
||||
// Quality of audio output
|
||||
struct media_quality quality;
|
||||
|
||||
// Address
|
||||
char *v4_address;
|
||||
char *v6_address;
|
||||
short v4_port;
|
||||
short v6_port;
|
||||
|
||||
struct event *stop_timer;
|
||||
|
||||
// Opaque pointers to device and session data
|
||||
void *extra_device_info;
|
||||
struct output_session *session;
|
||||
void *session;
|
||||
|
||||
struct output_device *next;
|
||||
};
|
||||
|
||||
// Except for the type, sessions are opaque outside of the output backend
|
||||
struct output_session
|
||||
{
|
||||
enum output_types type;
|
||||
void *session;
|
||||
};
|
||||
|
||||
// Linked list of metadata prepared by each output backend
|
||||
struct output_metadata
|
||||
{
|
||||
enum output_types type;
|
||||
void *metadata;
|
||||
struct output_metadata *next;
|
||||
uint32_t item_id;
|
||||
|
||||
// Progress data, filled out by finalize_cb()
|
||||
uint32_t pos_ms;
|
||||
uint32_t len_ms;
|
||||
struct timespec pts;
|
||||
bool startup;
|
||||
|
||||
// Private output data made by the metadata_prepare()
|
||||
void *priv;
|
||||
|
||||
struct event *ev;
|
||||
|
||||
// Finalize before right before sending, e.g. set playback position
|
||||
output_metadata_finalize_cb finalize_cb;
|
||||
};
|
||||
|
||||
typedef void (*output_status_cb)(struct output_device *device, struct output_session *session, enum output_device_state status);
|
||||
struct output_data
|
||||
{
|
||||
struct media_quality quality;
|
||||
struct evbuffer *evbuf;
|
||||
uint8_t *buffer;
|
||||
size_t bufsize;
|
||||
int samples;
|
||||
};
|
||||
|
||||
struct output_buffer
|
||||
{
|
||||
uint32_t write_counter; // REMOVE ME? not used for anything
|
||||
struct timespec pts;
|
||||
struct output_data data[OUTPUTS_MAX_QUALITY_SUBSCRIPTIONS + 1];
|
||||
} output_buffer;
|
||||
|
||||
struct output_definition
|
||||
{
|
||||
@ -166,94 +215,137 @@ struct output_definition
|
||||
void (*deinit)(void);
|
||||
|
||||
// Prepare a playback session on device and call back
|
||||
int (*device_start)(struct output_device *device, output_status_cb cb, uint64_t rtptime);
|
||||
int (*device_start)(struct output_device *device, int callback_id);
|
||||
|
||||
// Close a session prepared by device_start
|
||||
void (*device_stop)(struct output_session *session);
|
||||
// Close a session prepared by device_start and call back
|
||||
int (*device_stop)(struct output_device *device, int callback_id);
|
||||
|
||||
// Flush device session and call back
|
||||
int (*device_flush)(struct output_device *device, int callback_id);
|
||||
|
||||
// Test the connection to a device and call back
|
||||
int (*device_probe)(struct output_device *device, output_status_cb cb);
|
||||
|
||||
// Free the private device data
|
||||
void (*device_free_extra)(struct output_device *device);
|
||||
int (*device_probe)(struct output_device *device, int callback_id);
|
||||
|
||||
// Set the volume and call back
|
||||
int (*device_volume_set)(struct output_device *device, output_status_cb cb);
|
||||
int (*device_volume_set)(struct output_device *device, int callback_id);
|
||||
|
||||
// Convert device internal representation of volume to our pct scale
|
||||
int (*device_volume_to_pct)(struct output_device *device, const char *volume);
|
||||
|
||||
// Start/stop playback on devices that were started
|
||||
void (*playback_start)(uint64_t next_pkt, struct timespec *ts);
|
||||
void (*playback_stop)(void);
|
||||
// Request a change of quality from the device
|
||||
int (*device_quality_set)(struct output_device *device, struct media_quality *quality, int callback_id);
|
||||
|
||||
// Change the call back associated with a device
|
||||
void (*device_cb_set)(struct output_device *device, int callback_id);
|
||||
|
||||
// Free the private device data
|
||||
void (*device_free_extra)(struct output_device *device);
|
||||
|
||||
// Write stream data to the output devices
|
||||
void (*write)(uint8_t *buf, uint64_t rtptime);
|
||||
|
||||
// Flush all sessions, the return must be number of sessions pending the flush
|
||||
int (*flush)(output_status_cb cb, uint64_t rtptime);
|
||||
void (*write)(struct output_buffer *buffer);
|
||||
|
||||
// Authorize an output with a pin-code (probably coming from the filescanner)
|
||||
void (*authorize)(const char *pin);
|
||||
|
||||
// Change the call back associated with a session
|
||||
void (*status_cb)(struct output_session *session, output_status_cb cb);
|
||||
// Called from worker thread for async preparation of metadata (e.g. getting
|
||||
// artwork, which might involce downloading image data). The prepared data is
|
||||
// saved to metadata->data, which metadata_send() can use.
|
||||
void *(*metadata_prepare)(struct output_metadata *metadata);
|
||||
|
||||
// Metadata
|
||||
void *(*metadata_prepare)(int id);
|
||||
void (*metadata_send)(void *metadata, uint64_t rtptime, uint64_t offset, int startup);
|
||||
// Send metadata to outputs. Ownership of *metadata is transferred.
|
||||
void (*metadata_send)(struct output_metadata *metadata);
|
||||
|
||||
// Output will cleanup all metadata (so basically like flush but for metadata)
|
||||
void (*metadata_purge)(void);
|
||||
void (*metadata_prune)(uint64_t rtptime);
|
||||
};
|
||||
|
||||
// Our main list of devices, not for use by backend modules
|
||||
struct output_device *output_device_list;
|
||||
|
||||
/* ------------------------------- General use ------------------------------ */
|
||||
|
||||
struct output_device *
|
||||
outputs_device_get(uint64_t device_id);
|
||||
|
||||
/* ----------------------- Called by backend modules ------------------------ */
|
||||
|
||||
int
|
||||
outputs_device_start(struct output_device *device, output_status_cb cb, uint64_t rtptime);
|
||||
outputs_device_session_add(uint64_t device_id, void *session);
|
||||
|
||||
void
|
||||
outputs_device_stop(struct output_session *session);
|
||||
outputs_device_session_remove(uint64_t device_id);
|
||||
|
||||
int
|
||||
outputs_quality_subscribe(struct media_quality *quality);
|
||||
|
||||
void
|
||||
outputs_quality_unsubscribe(struct media_quality *quality);
|
||||
|
||||
void
|
||||
outputs_cb(int callback_id, uint64_t device_id, enum output_device_state);
|
||||
|
||||
void
|
||||
outputs_listener_notify(void);
|
||||
|
||||
/* ---------------------------- Called by player ---------------------------- */
|
||||
|
||||
// Ownership of *add is transferred, so don't address after calling. Instead you
|
||||
// can address the return value (which is not the same if the device was already
|
||||
// in the list.
|
||||
struct output_device *
|
||||
outputs_device_add(struct output_device *add, bool new_deselect, int default_volume);
|
||||
|
||||
void
|
||||
outputs_device_remove(struct output_device *remove);
|
||||
|
||||
int
|
||||
outputs_device_start(struct output_device *device, output_status_cb cb);
|
||||
|
||||
int
|
||||
outputs_device_stop(struct output_device *device, output_status_cb cb);
|
||||
|
||||
int
|
||||
outputs_device_stop_delayed(struct output_device *device, output_status_cb cb);
|
||||
|
||||
int
|
||||
outputs_device_flush(struct output_device *device, output_status_cb cb);
|
||||
|
||||
int
|
||||
outputs_device_probe(struct output_device *device, output_status_cb cb);
|
||||
|
||||
void
|
||||
outputs_device_free(struct output_device *device);
|
||||
|
||||
int
|
||||
outputs_device_volume_set(struct output_device *device, output_status_cb cb);
|
||||
|
||||
int
|
||||
outputs_device_volume_to_pct(struct output_device *device, const char *value);
|
||||
|
||||
void
|
||||
outputs_playback_start(uint64_t next_pkt, struct timespec *ts);
|
||||
int
|
||||
outputs_device_quality_set(struct output_device *device, struct media_quality *quality, output_status_cb cb);
|
||||
|
||||
void
|
||||
outputs_playback_stop(void);
|
||||
outputs_device_cb_set(struct output_device *device, output_status_cb cb);
|
||||
|
||||
void
|
||||
outputs_write(uint8_t *buf, uint64_t rtptime);
|
||||
outputs_device_free(struct output_device *device);
|
||||
|
||||
int
|
||||
outputs_flush(output_status_cb cb, uint64_t rtptime);
|
||||
outputs_flush(output_status_cb cb);
|
||||
|
||||
int
|
||||
outputs_stop(output_status_cb cb);
|
||||
|
||||
int
|
||||
outputs_stop_delayed_cancel(void);
|
||||
|
||||
void
|
||||
outputs_status_cb(struct output_session *session, output_status_cb cb);
|
||||
|
||||
struct output_metadata *
|
||||
outputs_metadata_prepare(int id);
|
||||
outputs_write(void *buf, size_t bufsize, int nsamples, struct media_quality *quality, struct timespec *pts);
|
||||
|
||||
void
|
||||
outputs_metadata_send(struct output_metadata *omd, uint64_t rtptime, uint64_t offset, int startup);
|
||||
outputs_metadata_send(uint32_t item_id, bool startup, output_metadata_finalize_cb cb);
|
||||
|
||||
void
|
||||
outputs_metadata_purge(void);
|
||||
|
||||
void
|
||||
outputs_metadata_prune(uint64_t rtptime);
|
||||
|
||||
void
|
||||
outputs_metadata_free(struct output_metadata *omd);
|
||||
|
||||
void
|
||||
outputs_authorize(enum output_types type, const char *pin);
|
||||
|
||||
|
1748
src/outputs/alsa.c
1748
src/outputs/alsa.c
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@ -33,8 +33,7 @@
|
||||
#include <stdint.h>
|
||||
#include <inttypes.h>
|
||||
|
||||
#include <event2/event.h>
|
||||
|
||||
#include "misc.h"
|
||||
#include "conffile.h"
|
||||
#include "logger.h"
|
||||
#include "player.h"
|
||||
@ -44,24 +43,12 @@ struct dummy_session
|
||||
{
|
||||
enum output_device_state state;
|
||||
|
||||
struct event *deferredev;
|
||||
output_status_cb defer_cb;
|
||||
|
||||
/* Do not dereference - only passed to the status cb */
|
||||
struct output_device *device;
|
||||
struct output_session *output_session;
|
||||
output_status_cb status_cb;
|
||||
uint64_t device_id;
|
||||
int callback_id;
|
||||
};
|
||||
|
||||
/* From player.c */
|
||||
extern struct event_base *evbase_player;
|
||||
|
||||
struct dummy_session *sessions;
|
||||
|
||||
/* Forwards */
|
||||
static void
|
||||
defer_cb(int fd, short what, void *arg);
|
||||
|
||||
/* ---------------------------- SESSION HANDLING ---------------------------- */
|
||||
|
||||
static void
|
||||
@ -70,9 +57,6 @@ dummy_session_free(struct dummy_session *ds)
|
||||
if (!ds)
|
||||
return;
|
||||
|
||||
event_free(ds->deferredev);
|
||||
|
||||
free(ds->output_session);
|
||||
free(ds);
|
||||
}
|
||||
|
||||
@ -82,86 +66,50 @@ dummy_session_cleanup(struct dummy_session *ds)
|
||||
// Normally some here code to remove from linked list - here we just say:
|
||||
sessions = NULL;
|
||||
|
||||
outputs_device_session_remove(ds->device_id);
|
||||
|
||||
dummy_session_free(ds);
|
||||
}
|
||||
|
||||
static struct dummy_session *
|
||||
dummy_session_make(struct output_device *device, output_status_cb cb)
|
||||
dummy_session_make(struct output_device *device, int callback_id)
|
||||
{
|
||||
struct output_session *os;
|
||||
struct dummy_session *ds;
|
||||
|
||||
os = calloc(1, sizeof(struct output_session));
|
||||
if (!os)
|
||||
{
|
||||
DPRINTF(E_LOG, L_LAUDIO, "Out of memory for dummy session (os)\n");
|
||||
return NULL;
|
||||
}
|
||||
CHECK_NULL(L_LAUDIO, ds = calloc(1, sizeof(struct dummy_session)));
|
||||
|
||||
ds = calloc(1, sizeof(struct dummy_session));
|
||||
if (!ds)
|
||||
{
|
||||
DPRINTF(E_LOG, L_LAUDIO, "Out of memory for dummy session (as)\n");
|
||||
free(os);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
ds->deferredev = evtimer_new(evbase_player, defer_cb, ds);
|
||||
if (!ds->deferredev)
|
||||
{
|
||||
DPRINTF(E_LOG, L_LAUDIO, "Out of memory for dummy deferred event\n");
|
||||
free(os);
|
||||
free(ds);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
os->session = ds;
|
||||
os->type = device->type;
|
||||
|
||||
ds->output_session = os;
|
||||
ds->state = OUTPUT_STATE_CONNECTED;
|
||||
ds->device = device;
|
||||
ds->status_cb = cb;
|
||||
ds->device_id = device->id;
|
||||
ds->callback_id = callback_id;
|
||||
|
||||
sessions = ds;
|
||||
|
||||
outputs_device_session_add(device->id, ds);
|
||||
|
||||
return ds;
|
||||
}
|
||||
|
||||
|
||||
/* ---------------------------- STATUS HANDLERS ----------------------------- */
|
||||
|
||||
// Maps our internal state to the generic output state and then makes a callback
|
||||
// to the player to tell that state
|
||||
static void
|
||||
defer_cb(int fd, short what, void *arg)
|
||||
{
|
||||
struct dummy_session *ds = arg;
|
||||
|
||||
if (ds->defer_cb)
|
||||
ds->defer_cb(ds->device, ds->output_session, ds->state);
|
||||
|
||||
if (ds->state == OUTPUT_STATE_STOPPED)
|
||||
dummy_session_cleanup(ds);
|
||||
}
|
||||
|
||||
static void
|
||||
dummy_status(struct dummy_session *ds)
|
||||
{
|
||||
ds->defer_cb = ds->status_cb;
|
||||
event_active(ds->deferredev, 0, 0);
|
||||
ds->status_cb = NULL;
|
||||
outputs_cb(ds->callback_id, ds->device_id, ds->state);
|
||||
|
||||
if (ds->state == OUTPUT_STATE_STOPPED)
|
||||
dummy_session_cleanup(ds);
|
||||
}
|
||||
|
||||
|
||||
/* ------------------ INTERFACE FUNCTIONS CALLED BY OUTPUTS.C --------------- */
|
||||
|
||||
static int
|
||||
dummy_device_start(struct output_device *device, output_status_cb cb, uint64_t rtptime)
|
||||
dummy_device_start(struct output_device *device, int callback_id)
|
||||
{
|
||||
struct dummy_session *ds;
|
||||
|
||||
ds = dummy_session_make(device, cb);
|
||||
ds = dummy_session_make(device, callback_id);
|
||||
if (!ds)
|
||||
return -1;
|
||||
|
||||
@ -170,25 +118,12 @@ dummy_device_start(struct output_device *device, output_status_cb cb, uint64_t r
|
||||
return 0;
|
||||
}
|
||||
|
||||
static void
|
||||
dummy_device_stop(struct output_session *session)
|
||||
{
|
||||
struct dummy_session *ds = session->session;
|
||||
|
||||
ds->state = OUTPUT_STATE_STOPPED;
|
||||
dummy_status(ds);
|
||||
}
|
||||
|
||||
static int
|
||||
dummy_device_probe(struct output_device *device, output_status_cb cb)
|
||||
dummy_device_stop(struct output_device *device, int callback_id)
|
||||
{
|
||||
struct dummy_session *ds;
|
||||
struct dummy_session *ds = device->session;
|
||||
|
||||
ds = dummy_session_make(device, cb);
|
||||
if (!ds)
|
||||
return -1;
|
||||
|
||||
ds->status_cb = cb;
|
||||
ds->callback_id = callback_id;
|
||||
ds->state = OUTPUT_STATE_STOPPED;
|
||||
|
||||
dummy_status(ds);
|
||||
@ -197,51 +132,55 @@ dummy_device_probe(struct output_device *device, output_status_cb cb)
|
||||
}
|
||||
|
||||
static int
|
||||
dummy_device_volume_set(struct output_device *device, output_status_cb cb)
|
||||
dummy_device_flush(struct output_device *device, int callback_id)
|
||||
{
|
||||
struct dummy_session *ds = device->session;
|
||||
|
||||
ds->callback_id = callback_id;
|
||||
ds->state = OUTPUT_STATE_STOPPED;
|
||||
|
||||
dummy_status(ds);
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int
|
||||
dummy_device_probe(struct output_device *device, int callback_id)
|
||||
{
|
||||
struct dummy_session *ds;
|
||||
|
||||
if (!device->session || !device->session->session)
|
||||
ds = dummy_session_make(device, callback_id);
|
||||
if (!ds)
|
||||
return -1;
|
||||
|
||||
ds->callback_id = callback_id;
|
||||
ds->state = OUTPUT_STATE_STOPPED;
|
||||
|
||||
dummy_status(ds);
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int
|
||||
dummy_device_volume_set(struct output_device *device, int callback_id)
|
||||
{
|
||||
struct dummy_session *ds = device->session;
|
||||
|
||||
if (!ds)
|
||||
return 0;
|
||||
|
||||
ds = device->session->session;
|
||||
|
||||
ds->status_cb = cb;
|
||||
ds->callback_id = callback_id;
|
||||
dummy_status(ds);
|
||||
|
||||
return 1;
|
||||
}
|
||||
|
||||
static void
|
||||
dummy_playback_start(uint64_t next_pkt, struct timespec *ts)
|
||||
dummy_device_cb_set(struct output_device *device, int callback_id)
|
||||
{
|
||||
struct dummy_session *ds = sessions;
|
||||
struct dummy_session *ds = device->session;
|
||||
|
||||
if (!sessions)
|
||||
return;
|
||||
|
||||
ds->state = OUTPUT_STATE_STREAMING;
|
||||
dummy_status(ds);
|
||||
}
|
||||
|
||||
static void
|
||||
dummy_playback_stop(void)
|
||||
{
|
||||
struct dummy_session *ds = sessions;
|
||||
|
||||
if (!sessions)
|
||||
return;
|
||||
|
||||
ds->state = OUTPUT_STATE_CONNECTED;
|
||||
dummy_status(ds);
|
||||
}
|
||||
|
||||
static void
|
||||
dummy_set_status_cb(struct output_session *session, output_status_cb cb)
|
||||
{
|
||||
struct dummy_session *ds = session->session;
|
||||
|
||||
ds->status_cb = cb;
|
||||
ds->callback_id = callback_id;
|
||||
}
|
||||
|
||||
static int
|
||||
@ -259,18 +198,12 @@ dummy_init(void)
|
||||
|
||||
nickname = cfg_getstr(cfg_audio, "nickname");
|
||||
|
||||
device = calloc(1, sizeof(struct output_device));
|
||||
if (!device)
|
||||
{
|
||||
DPRINTF(E_LOG, L_LAUDIO, "Out of memory for dummy device\n");
|
||||
return -1;
|
||||
}
|
||||
CHECK_NULL(L_LAUDIO, device = calloc(1, sizeof(struct output_device)));
|
||||
|
||||
device->id = 0;
|
||||
device->name = strdup(nickname);
|
||||
device->type = OUTPUT_TYPE_DUMMY;
|
||||
device->type_name = outputs_name(device->type);
|
||||
device->advertised = 1;
|
||||
device->has_video = 0;
|
||||
|
||||
DPRINTF(E_INFO, L_LAUDIO, "Adding dummy output device '%s'\n", nickname);
|
||||
@ -296,9 +229,8 @@ struct output_definition output_dummy =
|
||||
.deinit = dummy_deinit,
|
||||
.device_start = dummy_device_start,
|
||||
.device_stop = dummy_device_stop,
|
||||
.device_flush = dummy_device_flush,
|
||||
.device_probe = dummy_device_probe,
|
||||
.device_volume_set = dummy_device_volume_set,
|
||||
.playback_start = dummy_playback_start,
|
||||
.playback_stop = dummy_playback_stop,
|
||||
.status_cb = dummy_set_status_cb,
|
||||
.device_cb_set = dummy_device_cb_set,
|
||||
};
|
||||
|
@ -31,24 +31,23 @@
|
||||
#include <sys/stat.h>
|
||||
#include <unistd.h>
|
||||
|
||||
#include <event2/event.h>
|
||||
|
||||
#include "misc.h"
|
||||
#include "conffile.h"
|
||||
#include "logger.h"
|
||||
#include "player.h"
|
||||
#include "outputs.h"
|
||||
|
||||
#define FIFO_BUFFER_SIZE 65536 /* pipe capacity on Linux >= 2.6.11 */
|
||||
|
||||
#define FIFO_BUFFER_SIZE 65536 // pipe capacity on Linux >= 2.6.11
|
||||
#define FIFO_PACKET_SIZE 1408 // 352 samples/packet * 16 bit/sample * 2 channels
|
||||
|
||||
struct fifo_packet
|
||||
{
|
||||
/* pcm data */
|
||||
uint8_t samples[1408]; // STOB(AIRTUNES_V2_PACKET_SAMPLES)
|
||||
uint8_t *samples;
|
||||
size_t samples_size;
|
||||
|
||||
/* RTP-time of the first sample*/
|
||||
uint64_t rtptime;
|
||||
/* Presentation timestamp of the first sample */
|
||||
struct timespec pts;
|
||||
|
||||
struct fifo_packet *next;
|
||||
struct fifo_packet *prev;
|
||||
@ -59,8 +58,11 @@ struct fifo_buffer
|
||||
struct fifo_packet *head;
|
||||
struct fifo_packet *tail;
|
||||
};
|
||||
|
||||
static struct fifo_buffer buffer;
|
||||
|
||||
static struct media_quality fifo_quality = { 44100, 16, 2 };
|
||||
|
||||
|
||||
static void
|
||||
free_buffer()
|
||||
@ -91,24 +93,12 @@ struct fifo_session
|
||||
|
||||
int created;
|
||||
|
||||
struct event *deferredev;
|
||||
output_status_cb defer_cb;
|
||||
|
||||
/* Do not dereference - only passed to the status cb */
|
||||
struct output_device *device;
|
||||
struct output_session *output_session;
|
||||
output_status_cb status_cb;
|
||||
uint64_t device_id;
|
||||
int callback_id;
|
||||
};
|
||||
|
||||
/* From player.c */
|
||||
extern struct event_base *evbase_player;
|
||||
|
||||
static struct fifo_session *sessions;
|
||||
|
||||
/* Forwards */
|
||||
static void
|
||||
defer_cb(int fd, short what, void *arg);
|
||||
|
||||
|
||||
/* ---------------------------- FIFO HANDLING ---------------------------- */
|
||||
|
||||
@ -240,9 +230,6 @@ fifo_session_free(struct fifo_session *fifo_session)
|
||||
if (!fifo_session)
|
||||
return;
|
||||
|
||||
event_free(fifo_session->deferredev);
|
||||
|
||||
free(fifo_session->output_session);
|
||||
free(fifo_session);
|
||||
free_buffer();
|
||||
}
|
||||
@ -253,46 +240,21 @@ fifo_session_cleanup(struct fifo_session *fifo_session)
|
||||
// Normally some here code to remove from linked list - here we just say:
|
||||
sessions = NULL;
|
||||
|
||||
outputs_device_session_remove(fifo_session->device_id);
|
||||
|
||||
fifo_session_free(fifo_session);
|
||||
}
|
||||
|
||||
static struct fifo_session *
|
||||
fifo_session_make(struct output_device *device, output_status_cb cb)
|
||||
fifo_session_make(struct output_device *device, int callback_id)
|
||||
{
|
||||
struct output_session *output_session;
|
||||
struct fifo_session *fifo_session;
|
||||
|
||||
output_session = calloc(1, sizeof(struct output_session));
|
||||
if (!output_session)
|
||||
{
|
||||
DPRINTF(E_LOG, L_FIFO, "Out of memory (os)\n");
|
||||
return NULL;
|
||||
}
|
||||
CHECK_NULL(L_FIFO, fifo_session = calloc(1, sizeof(struct fifo_session)));
|
||||
|
||||
fifo_session = calloc(1, sizeof(struct fifo_session));
|
||||
if (!fifo_session)
|
||||
{
|
||||
DPRINTF(E_LOG, L_FIFO, "Out of memory (fs)\n");
|
||||
free(output_session);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
fifo_session->deferredev = evtimer_new(evbase_player, defer_cb, fifo_session);
|
||||
if (!fifo_session->deferredev)
|
||||
{
|
||||
DPRINTF(E_LOG, L_FIFO, "Out of memory for fifo deferred event\n");
|
||||
free(output_session);
|
||||
free(fifo_session);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
output_session->session = fifo_session;
|
||||
output_session->type = device->type;
|
||||
|
||||
fifo_session->output_session = output_session;
|
||||
fifo_session->state = OUTPUT_STATE_CONNECTED;
|
||||
fifo_session->device = device;
|
||||
fifo_session->status_cb = cb;
|
||||
fifo_session->device_id = device->id;
|
||||
fifo_session->callback_id = callback_id;
|
||||
|
||||
fifo_session->created = 0;
|
||||
fifo_session->path = device->extra_device_info;
|
||||
@ -301,44 +263,36 @@ fifo_session_make(struct output_device *device, output_status_cb cb)
|
||||
|
||||
sessions = fifo_session;
|
||||
|
||||
outputs_device_session_add(device->id, fifo_session);
|
||||
|
||||
return fifo_session;
|
||||
}
|
||||
|
||||
|
||||
/* ---------------------------- STATUS HANDLERS ----------------------------- */
|
||||
|
||||
// Maps our internal state to the generic output state and then makes a callback
|
||||
// to the player to tell that state
|
||||
static void
|
||||
defer_cb(int fd, short what, void *arg)
|
||||
{
|
||||
struct fifo_session *ds = arg;
|
||||
|
||||
if (ds->defer_cb)
|
||||
ds->defer_cb(ds->device, ds->output_session, ds->state);
|
||||
|
||||
if (ds->state == OUTPUT_STATE_STOPPED)
|
||||
fifo_session_cleanup(ds);
|
||||
}
|
||||
|
||||
static void
|
||||
fifo_status(struct fifo_session *fifo_session)
|
||||
{
|
||||
fifo_session->defer_cb = fifo_session->status_cb;
|
||||
event_active(fifo_session->deferredev, 0, 0);
|
||||
fifo_session->status_cb = NULL;
|
||||
}
|
||||
outputs_cb(fifo_session->callback_id, fifo_session->device_id, fifo_session->state);
|
||||
|
||||
if (fifo_session->state == OUTPUT_STATE_STOPPED)
|
||||
fifo_session_cleanup(fifo_session);
|
||||
}
|
||||
|
||||
/* ------------------ INTERFACE FUNCTIONS CALLED BY OUTPUTS.C --------------- */
|
||||
|
||||
static int
|
||||
fifo_device_start(struct output_device *device, output_status_cb cb, uint64_t rtptime)
|
||||
fifo_device_start(struct output_device *device, int callback_id)
|
||||
{
|
||||
struct fifo_session *fifo_session;
|
||||
int ret;
|
||||
|
||||
fifo_session = fifo_session_make(device, cb);
|
||||
ret = outputs_quality_subscribe(&fifo_quality);
|
||||
if (ret < 0)
|
||||
return -1;
|
||||
|
||||
fifo_session = fifo_session_make(device, callback_id);
|
||||
if (!fifo_session)
|
||||
return -1;
|
||||
|
||||
@ -351,25 +305,46 @@ fifo_device_start(struct output_device *device, output_status_cb cb, uint64_t rt
|
||||
return 0;
|
||||
}
|
||||
|
||||
static void
|
||||
fifo_device_stop(struct output_session *output_session)
|
||||
static int
|
||||
fifo_device_stop(struct output_device *device, int callback_id)
|
||||
{
|
||||
struct fifo_session *fifo_session = output_session->session;
|
||||
struct fifo_session *fifo_session = device->session;
|
||||
|
||||
outputs_quality_unsubscribe(&fifo_quality);
|
||||
|
||||
fifo_session->callback_id = callback_id;
|
||||
|
||||
fifo_close(fifo_session);
|
||||
free_buffer();
|
||||
|
||||
fifo_session->state = OUTPUT_STATE_STOPPED;
|
||||
fifo_status(fifo_session);
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int
|
||||
fifo_device_probe(struct output_device *device, output_status_cb cb)
|
||||
fifo_device_flush(struct output_device *device, int callback_id)
|
||||
{
|
||||
struct fifo_session *fifo_session = device->session;
|
||||
|
||||
fifo_empty(fifo_session);
|
||||
free_buffer();
|
||||
|
||||
fifo_session->callback_id = callback_id;
|
||||
fifo_session->state = OUTPUT_STATE_CONNECTED;
|
||||
fifo_status(fifo_session);
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int
|
||||
fifo_device_probe(struct output_device *device, int callback_id)
|
||||
{
|
||||
struct fifo_session *fifo_session;
|
||||
int ret;
|
||||
|
||||
fifo_session = fifo_session_make(device, cb);
|
||||
fifo_session = fifo_session_make(device, callback_id);
|
||||
if (!fifo_session)
|
||||
return -1;
|
||||
|
||||
@ -382,7 +357,7 @@ fifo_device_probe(struct output_device *device, output_status_cb cb)
|
||||
|
||||
fifo_close(fifo_session);
|
||||
|
||||
fifo_session->status_cb = cb;
|
||||
fifo_session->callback_id = callback_id;
|
||||
fifo_session->state = OUTPUT_STATE_STOPPED;
|
||||
|
||||
fifo_status(fifo_session);
|
||||
@ -391,104 +366,81 @@ fifo_device_probe(struct output_device *device, output_status_cb cb)
|
||||
}
|
||||
|
||||
static int
|
||||
fifo_device_volume_set(struct output_device *device, output_status_cb cb)
|
||||
fifo_device_volume_set(struct output_device *device, int callback_id)
|
||||
{
|
||||
struct fifo_session *fifo_session;
|
||||
struct fifo_session *fifo_session = device->session;
|
||||
|
||||
if (!device->session || !device->session->session)
|
||||
if (!fifo_session)
|
||||
return 0;
|
||||
|
||||
fifo_session = device->session->session;
|
||||
|
||||
fifo_session->status_cb = cb;
|
||||
fifo_session->callback_id = callback_id;
|
||||
fifo_status(fifo_session);
|
||||
|
||||
return 1;
|
||||
}
|
||||
|
||||
static void
|
||||
fifo_playback_start(uint64_t next_pkt, struct timespec *ts)
|
||||
fifo_device_cb_set(struct output_device *device, int callback_id)
|
||||
{
|
||||
struct fifo_session *fifo_session = device->session;
|
||||
|
||||
fifo_session->callback_id = callback_id;
|
||||
}
|
||||
|
||||
static void
|
||||
fifo_write(struct output_buffer *obuf)
|
||||
{
|
||||
struct fifo_session *fifo_session = sessions;
|
||||
struct fifo_packet *packet;
|
||||
struct timespec now;
|
||||
ssize_t bytes;
|
||||
int i;
|
||||
|
||||
if (!fifo_session)
|
||||
return;
|
||||
|
||||
for (i = 0; obuf->data[i].buffer; i++)
|
||||
{
|
||||
if (quality_is_equal(&fifo_quality, &obuf->data[i].quality))
|
||||
break;
|
||||
}
|
||||
|
||||
if (!obuf->data[i].buffer)
|
||||
{
|
||||
DPRINTF(E_LOG, L_FIFO, "Bug! Did not get audio in quality required\n");
|
||||
return;
|
||||
}
|
||||
|
||||
fifo_session->state = OUTPUT_STATE_STREAMING;
|
||||
fifo_status(fifo_session);
|
||||
}
|
||||
|
||||
static void
|
||||
fifo_playback_stop(void)
|
||||
{
|
||||
struct fifo_session *fifo_session = sessions;
|
||||
CHECK_NULL(L_FIFO, packet = calloc(1, sizeof(struct fifo_packet)));
|
||||
CHECK_NULL(L_FIFO, packet->samples = malloc(obuf->data[i].bufsize));
|
||||
|
||||
if (!fifo_session)
|
||||
return;
|
||||
memcpy(packet->samples, obuf->data[i].buffer, obuf->data[i].bufsize);
|
||||
packet->samples_size = obuf->data[i].bufsize;
|
||||
packet->pts = obuf->pts;
|
||||
|
||||
free_buffer();
|
||||
|
||||
fifo_session->state = OUTPUT_STATE_CONNECTED;
|
||||
fifo_status(fifo_session);
|
||||
}
|
||||
|
||||
static int
|
||||
fifo_flush(output_status_cb cb, uint64_t rtptime)
|
||||
{
|
||||
struct fifo_session *fifo_session = sessions;
|
||||
|
||||
if (!fifo_session)
|
||||
return 0;
|
||||
|
||||
fifo_empty(fifo_session);
|
||||
free_buffer();
|
||||
|
||||
fifo_session->status_cb = cb;
|
||||
fifo_session->state = OUTPUT_STATE_CONNECTED;
|
||||
fifo_status(fifo_session);
|
||||
return 1;
|
||||
}
|
||||
|
||||
static void
|
||||
fifo_write(uint8_t *buf, uint64_t rtptime)
|
||||
{
|
||||
struct fifo_session *fifo_session = sessions;
|
||||
size_t length = STOB(AIRTUNES_V2_PACKET_SAMPLES);
|
||||
ssize_t bytes;
|
||||
struct fifo_packet *packet;
|
||||
uint64_t cur_pos;
|
||||
struct timespec now;
|
||||
int ret;
|
||||
|
||||
if (!fifo_session || !fifo_session->device->selected)
|
||||
return;
|
||||
|
||||
packet = (struct fifo_packet *) calloc(1, sizeof(struct fifo_packet));
|
||||
memcpy(packet->samples, buf, sizeof(packet->samples));
|
||||
packet->rtptime = rtptime;
|
||||
if (buffer.head)
|
||||
{
|
||||
buffer.head->next = packet;
|
||||
packet->prev = buffer.head;
|
||||
}
|
||||
|
||||
buffer.head = packet;
|
||||
if (!buffer.tail)
|
||||
buffer.tail = packet;
|
||||
|
||||
ret = player_get_current_pos(&cur_pos, &now, 0);
|
||||
if (ret < 0)
|
||||
{
|
||||
DPRINTF(E_LOG, L_FIFO, "Could not get playback position\n");
|
||||
return;
|
||||
}
|
||||
now.tv_sec = obuf->pts.tv_sec - OUTPUTS_BUFFER_DURATION;
|
||||
now.tv_nsec = obuf->pts.tv_sec;
|
||||
|
||||
while (buffer.tail && buffer.tail->rtptime <= cur_pos)
|
||||
while (buffer.tail && (timespec_cmp(buffer.tail->pts, now) == -1))
|
||||
{
|
||||
bytes = write(fifo_session->output_fd, buffer.tail->samples, length);
|
||||
bytes = write(fifo_session->output_fd, buffer.tail->samples, buffer.tail->samples_size);
|
||||
if (bytes > 0)
|
||||
{
|
||||
packet = buffer.tail;
|
||||
buffer.tail = buffer.tail->next;
|
||||
free(packet->samples);
|
||||
free(packet);
|
||||
return;
|
||||
}
|
||||
@ -511,14 +463,6 @@ fifo_write(uint8_t *buf, uint64_t rtptime)
|
||||
}
|
||||
}
|
||||
|
||||
static void
|
||||
fifo_set_status_cb(struct output_session *session, output_status_cb cb)
|
||||
{
|
||||
struct fifo_session *fifo_session = session->session;
|
||||
|
||||
fifo_session->status_cb = cb;
|
||||
}
|
||||
|
||||
static int
|
||||
fifo_init(void)
|
||||
{
|
||||
@ -539,18 +483,12 @@ fifo_init(void)
|
||||
|
||||
memset(&buffer, 0, sizeof(struct fifo_buffer));
|
||||
|
||||
device = calloc(1, sizeof(struct output_device));
|
||||
if (!device)
|
||||
{
|
||||
DPRINTF(E_LOG, L_FIFO, "Out of memory for fifo device\n");
|
||||
return -1;
|
||||
}
|
||||
CHECK_NULL(L_FIFO, device = calloc(1, sizeof(struct output_device)));
|
||||
|
||||
device->id = 100;
|
||||
device->name = strdup(nickname);
|
||||
device->type = OUTPUT_TYPE_FIFO;
|
||||
device->type_name = outputs_name(device->type);
|
||||
device->advertised = 1;
|
||||
device->has_video = 0;
|
||||
device->extra_device_info = path;
|
||||
DPRINTF(E_INFO, L_FIFO, "Adding fifo output device '%s' with path '%s'\n", nickname, path);
|
||||
@ -576,11 +514,9 @@ struct output_definition output_fifo =
|
||||
.deinit = fifo_deinit,
|
||||
.device_start = fifo_device_start,
|
||||
.device_stop = fifo_device_stop,
|
||||
.device_flush = fifo_device_flush,
|
||||
.device_probe = fifo_device_probe,
|
||||
.device_volume_set = fifo_device_volume_set,
|
||||
.playback_start = fifo_playback_start,
|
||||
.playback_stop = fifo_playback_stop,
|
||||
.device_cb_set = fifo_device_cb_set,
|
||||
.write = fifo_write,
|
||||
.flush = fifo_flush,
|
||||
.status_cb = fifo_set_status_cb,
|
||||
};
|
||||
|
@ -58,21 +58,21 @@ struct pulse
|
||||
|
||||
struct pulse_session
|
||||
{
|
||||
uint64_t device_id;
|
||||
int callback_id;
|
||||
|
||||
char *devname;
|
||||
|
||||
pa_stream_state_t state;
|
||||
pa_stream *stream;
|
||||
|
||||
pa_buffer_attr attr;
|
||||
pa_volume_t volume;
|
||||
|
||||
struct media_quality quality;
|
||||
|
||||
int logcount;
|
||||
|
||||
char *devname;
|
||||
|
||||
/* Do not dereference - only passed to the status cb */
|
||||
struct output_device *device;
|
||||
struct output_session *output_session;
|
||||
output_status_cb status_cb;
|
||||
|
||||
struct pulse_session *next;
|
||||
};
|
||||
|
||||
@ -85,6 +85,9 @@ static struct pulse_session *sessions;
|
||||
// Internal list with indeces of the Pulseaudio devices (sinks) we have registered
|
||||
static uint32_t pulse_known_devices[PULSE_MAX_DEVICES];
|
||||
|
||||
static struct media_quality pulse_last_quality;
|
||||
static struct media_quality pulse_fallback_quality = { 44100, 16, 2 };
|
||||
|
||||
// Converts from 0 - 100 to Pulseaudio's scale
|
||||
static inline pa_volume_t
|
||||
pulse_from_device_volume(int device_volume)
|
||||
@ -113,10 +116,9 @@ pulse_session_free(struct pulse_session *ps)
|
||||
pa_threaded_mainloop_unlock(pulse.mainloop);
|
||||
}
|
||||
|
||||
if (ps->devname)
|
||||
free(ps->devname);
|
||||
outputs_quality_unsubscribe(&pulse_fallback_quality);
|
||||
|
||||
free(ps->output_session);
|
||||
free(ps->devname);
|
||||
|
||||
free(ps);
|
||||
}
|
||||
@ -139,43 +141,37 @@ pulse_session_cleanup(struct pulse_session *ps)
|
||||
p->next = ps->next;
|
||||
}
|
||||
|
||||
outputs_device_session_remove(ps->device_id);
|
||||
|
||||
pulse_session_free(ps);
|
||||
}
|
||||
|
||||
static struct pulse_session *
|
||||
pulse_session_make(struct output_device *device, output_status_cb cb)
|
||||
pulse_session_make(struct output_device *device, int callback_id)
|
||||
{
|
||||
struct output_session *os;
|
||||
struct pulse_session *ps;
|
||||
int ret;
|
||||
|
||||
os = calloc(1, sizeof(struct output_session));
|
||||
if (!os)
|
||||
ret = outputs_quality_subscribe(&pulse_fallback_quality);
|
||||
if (ret < 0)
|
||||
{
|
||||
DPRINTF(E_LOG, L_LAUDIO, "Out of memory (os)\n");
|
||||
DPRINTF(E_LOG, L_LAUDIO, "Could not subscribe to fallback audio quality\n");
|
||||
return NULL;
|
||||
}
|
||||
|
||||
ps = calloc(1, sizeof(struct pulse_session));
|
||||
if (!ps)
|
||||
{
|
||||
DPRINTF(E_LOG, L_LAUDIO, "Out of memory (ps)\n");
|
||||
free(os);
|
||||
return NULL;
|
||||
}
|
||||
CHECK_NULL(L_LAUDIO, ps = calloc(1, sizeof(struct pulse_session)));
|
||||
|
||||
os->session = ps;
|
||||
os->type = device->type;
|
||||
|
||||
ps->output_session = os;
|
||||
ps->state = PA_STREAM_UNCONNECTED;
|
||||
ps->device = device;
|
||||
ps->status_cb = cb;
|
||||
ps->device_id = device->id;
|
||||
ps->callback_id = callback_id;
|
||||
ps->volume = pulse_from_device_volume(device->volume);
|
||||
ps->devname = strdup(device->extra_device_info);
|
||||
|
||||
ps->next = sessions;
|
||||
sessions = ps;
|
||||
|
||||
outputs_device_session_add(device->id, ps);
|
||||
|
||||
return ps;
|
||||
}
|
||||
|
||||
@ -187,7 +183,6 @@ static enum command_state
|
||||
send_status(void *arg, int *ptr)
|
||||
{
|
||||
struct pulse_session *ps = arg;
|
||||
output_status_cb status_cb;
|
||||
enum output_device_state state;
|
||||
|
||||
switch (ps->state)
|
||||
@ -210,10 +205,8 @@ send_status(void *arg, int *ptr)
|
||||
state = OUTPUT_STATE_FAILED;
|
||||
}
|
||||
|
||||
status_cb = ps->status_cb;
|
||||
ps->status_cb = NULL;
|
||||
if (status_cb)
|
||||
status_cb(ps->device, ps->output_session, state);
|
||||
outputs_cb(ps->callback_id, ps->device_id, state);
|
||||
ps->callback_id = -1;
|
||||
|
||||
return COMMAND_PENDING; // Don't want the command module to clean up ps
|
||||
}
|
||||
@ -442,7 +435,6 @@ sinklist_cb(pa_context *ctx, const pa_sink_info *info, int eol, void *userdata)
|
||||
device->name = strdup(name);
|
||||
device->type = OUTPUT_TYPE_PULSE;
|
||||
device->type_name = outputs_name(device->type);
|
||||
device->advertised = 1;
|
||||
device->extra_device_info = strdup(info->name);
|
||||
|
||||
player_device_add(device);
|
||||
@ -572,25 +564,33 @@ pulse_free(void)
|
||||
}
|
||||
|
||||
static int
|
||||
stream_open(struct pulse_session *ps, pa_stream_notify_cb_t cb)
|
||||
stream_open(struct pulse_session *ps, struct media_quality *quality, pa_stream_notify_cb_t cb)
|
||||
{
|
||||
pa_stream_flags_t flags;
|
||||
pa_sample_spec ss;
|
||||
pa_cvolume cvol;
|
||||
int offset;
|
||||
int offset_ms;
|
||||
int ret;
|
||||
|
||||
DPRINTF(E_DBG, L_LAUDIO, "Opening Pulseaudio stream to '%s'\n", ps->devname);
|
||||
|
||||
ss.format = PA_SAMPLE_S16LE;
|
||||
ss.channels = 2;
|
||||
ss.rate = 44100;
|
||||
if (quality->bits_per_sample == 16)
|
||||
ss.format = PA_SAMPLE_S16LE;
|
||||
else if (quality->bits_per_sample == 24)
|
||||
ss.format = PA_SAMPLE_S24LE;
|
||||
else if (quality->bits_per_sample == 32)
|
||||
ss.format = PA_SAMPLE_S32LE;
|
||||
else
|
||||
ss.format = 0;
|
||||
|
||||
offset = cfg_getint(cfg_getsec(cfg, "audio"), "offset");
|
||||
if (abs(offset) > 44100)
|
||||
ss.channels = quality->channels;
|
||||
ss.rate = quality->sample_rate;
|
||||
|
||||
offset_ms = cfg_getint(cfg_getsec(cfg, "audio"), "offset_ms");
|
||||
if (abs(offset_ms) > 1000)
|
||||
{
|
||||
DPRINTF(E_LOG, L_LAUDIO, "The audio offset (%d) set in the configuration is out of bounds\n", offset);
|
||||
offset = 44100 * (offset/abs(offset));
|
||||
DPRINTF(E_LOG, L_LAUDIO, "The audio offset (%d) set in the configuration is out of bounds\n", offset_ms);
|
||||
offset_ms = 1000 * (offset_ms/abs(offset_ms));
|
||||
}
|
||||
|
||||
pa_threaded_mainloop_lock(pulse.mainloop);
|
||||
@ -602,7 +602,7 @@ stream_open(struct pulse_session *ps, pa_stream_notify_cb_t cb)
|
||||
|
||||
flags = PA_STREAM_INTERPOLATE_TIMING | PA_STREAM_AUTO_TIMING_UPDATE;
|
||||
|
||||
ps->attr.tlength = STOB(2 * ss.rate + AIRTUNES_V2_PACKET_SAMPLES - offset); // 2 second latency
|
||||
ps->attr.tlength = STOB((OUTPUTS_BUFFER_DURATION * 1000 + offset_ms) * ss.rate / 1000, quality->bits_per_sample, quality->channels);
|
||||
ps->attr.maxlength = 2 * ps->attr.tlength;
|
||||
ps->attr.prebuf = (uint32_t)-1;
|
||||
ps->attr.minreq = (uint32_t)-1;
|
||||
@ -625,7 +625,8 @@ stream_open(struct pulse_session *ps, pa_stream_notify_cb_t cb)
|
||||
unlock_and_fail:
|
||||
ret = pa_context_errno(pulse.context);
|
||||
|
||||
DPRINTF(E_LOG, L_LAUDIO, "Pulseaudio could not start '%s': %s\n", ps->devname, pa_strerror(ret));
|
||||
DPRINTF(E_LOG, L_LAUDIO, "Pulseaudio could not start '%s' using quality %d/%d/%d: %s\n",
|
||||
ps->devname, quality->sample_rate, quality->bits_per_sample, quality->channels, pa_strerror(ret));
|
||||
|
||||
pa_threaded_mainloop_unlock(pulse.mainloop);
|
||||
|
||||
@ -635,11 +636,15 @@ stream_open(struct pulse_session *ps, pa_stream_notify_cb_t cb)
|
||||
static void
|
||||
stream_close(struct pulse_session *ps, pa_stream_notify_cb_t cb)
|
||||
{
|
||||
if (!ps->stream)
|
||||
return;
|
||||
|
||||
pa_threaded_mainloop_lock(pulse.mainloop);
|
||||
|
||||
pa_stream_set_underflow_callback(ps->stream, NULL, NULL);
|
||||
pa_stream_set_overflow_callback(ps->stream, NULL, NULL);
|
||||
pa_stream_set_state_callback(ps->stream, cb, ps);
|
||||
|
||||
pa_stream_disconnect(ps->stream);
|
||||
pa_stream_unref(ps->stream);
|
||||
|
||||
@ -649,54 +654,169 @@ stream_close(struct pulse_session *ps, pa_stream_notify_cb_t cb)
|
||||
pa_threaded_mainloop_unlock(pulse.mainloop);
|
||||
}
|
||||
|
||||
static void
|
||||
playback_restart(struct pulse_session *ps, struct output_buffer *obuf)
|
||||
{
|
||||
int ret;
|
||||
|
||||
stream_close(ps, NULL);
|
||||
|
||||
// Negotiate quality (sample rate) with device - first we try to use the source quality
|
||||
ps->quality = obuf->data[0].quality;
|
||||
ret = stream_open(ps, &ps->quality, start_cb);
|
||||
if (ret < 0)
|
||||
{
|
||||
DPRINTF(E_INFO, L_LAUDIO, "Input quality (%d/%d/%d) not supported, falling back to default\n",
|
||||
ps->quality.sample_rate, ps->quality.bits_per_sample, ps->quality.channels);
|
||||
|
||||
ps->quality = pulse_fallback_quality;
|
||||
ret = stream_open(ps, &ps->quality, start_cb);
|
||||
if (ret < 0)
|
||||
{
|
||||
DPRINTF(E_LOG, L_LAUDIO, "Pulseaudio device failed setting fallback quality\n");
|
||||
ps->state = PA_STREAM_FAILED;
|
||||
pulse_session_shutdown(ps);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static void
|
||||
playback_write(struct pulse_session *ps, struct output_buffer *obuf)
|
||||
{
|
||||
int i;
|
||||
int ret;
|
||||
|
||||
// Find the quality we want
|
||||
for (i = 0; obuf->data[i].buffer; i++)
|
||||
{
|
||||
if (quality_is_equal(&ps->quality, &obuf->data[i].quality))
|
||||
break;
|
||||
}
|
||||
|
||||
if (!obuf->data[i].buffer)
|
||||
{
|
||||
DPRINTF(E_LOG, L_LAUDIO, "Output not delivering required data quality, aborting\n");
|
||||
ps->state = PA_STREAM_FAILED;
|
||||
pulse_session_shutdown(ps);
|
||||
return;
|
||||
}
|
||||
|
||||
pa_threaded_mainloop_lock(pulse.mainloop);
|
||||
|
||||
ret = pa_stream_write(ps->stream, obuf->data[i].buffer, obuf->data[i].bufsize, NULL, 0LL, PA_SEEK_RELATIVE);
|
||||
if (ret < 0)
|
||||
{
|
||||
ret = pa_context_errno(pulse.context);
|
||||
DPRINTF(E_LOG, L_LAUDIO, "Error writing Pulseaudio stream data to '%s': %s\n", ps->devname, pa_strerror(ret));
|
||||
ps->state = PA_STREAM_FAILED;
|
||||
pulse_session_shutdown(ps);
|
||||
goto unlock;
|
||||
}
|
||||
|
||||
unlock:
|
||||
pa_threaded_mainloop_unlock(pulse.mainloop);
|
||||
}
|
||||
|
||||
static void
|
||||
playback_resume(struct pulse_session *ps)
|
||||
{
|
||||
pa_operation* o;
|
||||
|
||||
pa_threaded_mainloop_lock(pulse.mainloop);
|
||||
|
||||
o = pa_stream_cork(ps->stream, 0, NULL, NULL);
|
||||
if (!o)
|
||||
{
|
||||
DPRINTF(E_LOG, L_LAUDIO, "Pulseaudio could not resume '%s': %s\n", ps->devname, pa_strerror(pa_context_errno(pulse.context)));
|
||||
goto unlock;
|
||||
}
|
||||
|
||||
pa_operation_unref(o);
|
||||
|
||||
unlock:
|
||||
pa_threaded_mainloop_unlock(pulse.mainloop);
|
||||
}
|
||||
|
||||
|
||||
/* ------------------ INTERFACE FUNCTIONS CALLED BY OUTPUTS.C --------------- */
|
||||
|
||||
static int
|
||||
pulse_device_start(struct output_device *device, output_status_cb cb, uint64_t rtptime)
|
||||
pulse_device_start(struct output_device *device, int callback_id)
|
||||
{
|
||||
struct pulse_session *ps;
|
||||
int ret;
|
||||
|
||||
DPRINTF(E_DBG, L_LAUDIO, "Pulseaudio starting '%s'\n", device->name);
|
||||
|
||||
ps = pulse_session_make(device, cb);
|
||||
ps = pulse_session_make(device, callback_id);
|
||||
if (!ps)
|
||||
return -1;
|
||||
|
||||
ret = stream_open(ps, start_cb);
|
||||
if (ret < 0)
|
||||
{
|
||||
pulse_session_cleanup(ps);
|
||||
return -1;
|
||||
}
|
||||
pulse_status(ps);
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
static void
|
||||
pulse_device_stop(struct output_session *session)
|
||||
static int
|
||||
pulse_device_stop(struct output_device *device, int callback_id)
|
||||
{
|
||||
struct pulse_session *ps = session->session;
|
||||
struct pulse_session *ps = device->session;
|
||||
|
||||
DPRINTF(E_DBG, L_LAUDIO, "Pulseaudio stopping '%s'\n", ps->devname);
|
||||
|
||||
ps->callback_id = callback_id;
|
||||
|
||||
stream_close(ps, close_cb);
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int
|
||||
pulse_device_probe(struct output_device *device, output_status_cb cb)
|
||||
pulse_device_flush(struct output_device *device, int callback_id)
|
||||
{
|
||||
struct pulse_session *ps = device->session;
|
||||
pa_operation* o;
|
||||
|
||||
DPRINTF(E_DBG, L_LAUDIO, "Pulseaudio flush\n");
|
||||
|
||||
pa_threaded_mainloop_lock(pulse.mainloop);
|
||||
|
||||
ps->callback_id = callback_id;
|
||||
|
||||
o = pa_stream_cork(ps->stream, 1, NULL, NULL);
|
||||
if (!o)
|
||||
{
|
||||
DPRINTF(E_LOG, L_LAUDIO, "Pulseaudio could not pause '%s': %s\n", ps->devname, pa_strerror(pa_context_errno(pulse.context)));
|
||||
return -1;
|
||||
}
|
||||
pa_operation_unref(o);
|
||||
|
||||
o = pa_stream_flush(ps->stream, flush_cb, ps);
|
||||
if (!o)
|
||||
{
|
||||
DPRINTF(E_LOG, L_LAUDIO, "Pulseaudio could not flush '%s': %s\n", ps->devname, pa_strerror(pa_context_errno(pulse.context)));
|
||||
return -1;
|
||||
}
|
||||
pa_operation_unref(o);
|
||||
|
||||
pa_threaded_mainloop_unlock(pulse.mainloop);
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int
|
||||
pulse_device_probe(struct output_device *device, int callback_id)
|
||||
{
|
||||
struct pulse_session *ps;
|
||||
int ret;
|
||||
|
||||
DPRINTF(E_DBG, L_LAUDIO, "Pulseaudio probing '%s'\n", device->name);
|
||||
|
||||
ps = pulse_session_make(device, cb);
|
||||
ps = pulse_session_make(device, callback_id);
|
||||
if (!ps)
|
||||
return -1;
|
||||
|
||||
ret = stream_open(ps, probe_cb);
|
||||
ret = stream_open(ps, &pulse_fallback_quality, probe_cb);
|
||||
if (ret < 0)
|
||||
{
|
||||
pulse_session_cleanup(ps);
|
||||
@ -712,18 +832,25 @@ pulse_device_free_extra(struct output_device *device)
|
||||
free(device->extra_device_info);
|
||||
}
|
||||
|
||||
static int
|
||||
pulse_device_volume_set(struct output_device *device, output_status_cb cb)
|
||||
static void
|
||||
pulse_device_cb_set(struct output_device *device, int callback_id)
|
||||
{
|
||||
struct pulse_session *ps;
|
||||
struct pulse_session *ps = device->session;
|
||||
|
||||
ps->callback_id = callback_id;
|
||||
}
|
||||
|
||||
static int
|
||||
pulse_device_volume_set(struct output_device *device, int callback_id)
|
||||
{
|
||||
struct pulse_session *ps = device->session;
|
||||
uint32_t idx;
|
||||
pa_operation* o;
|
||||
pa_cvolume cvol;
|
||||
|
||||
if (!sessions || !device->session || !device->session->session)
|
||||
if (!ps)
|
||||
return 0;
|
||||
|
||||
ps = device->session->session;
|
||||
idx = pa_stream_get_index(ps->stream);
|
||||
|
||||
ps->volume = pulse_from_device_volume(device->volume);
|
||||
@ -733,7 +860,7 @@ pulse_device_volume_set(struct output_device *device, output_status_cb cb)
|
||||
|
||||
pa_threaded_mainloop_lock(pulse.mainloop);
|
||||
|
||||
ps->status_cb = cb;
|
||||
ps->callback_id = callback_id;
|
||||
|
||||
o = pa_context_set_sink_input_volume(pulse.context, idx, &cvol, volume_cb, ps);
|
||||
if (!o)
|
||||
@ -750,141 +877,33 @@ pulse_device_volume_set(struct output_device *device, output_status_cb cb)
|
||||
}
|
||||
|
||||
static void
|
||||
pulse_write(uint8_t *buf, uint64_t rtptime)
|
||||
pulse_write(struct output_buffer *obuf)
|
||||
{
|
||||
struct pulse_session *ps;
|
||||
struct pulse_session *next;
|
||||
size_t length;
|
||||
int ret;
|
||||
|
||||
if (!sessions)
|
||||
return;
|
||||
|
||||
length = STOB(AIRTUNES_V2_PACKET_SAMPLES);
|
||||
|
||||
pa_threaded_mainloop_lock(pulse.mainloop);
|
||||
|
||||
for (ps = sessions; ps; ps = next)
|
||||
{
|
||||
next = ps->next;
|
||||
|
||||
if (ps->state != PA_STREAM_READY)
|
||||
// We have not set up a stream OR the quality changed, so we need to set it up again
|
||||
if (ps->state == PA_STREAM_UNCONNECTED || !quality_is_equal(&obuf->data[0].quality, &pulse_last_quality))
|
||||
{
|
||||
playback_restart(ps, obuf);
|
||||
pulse_last_quality = obuf->data[0].quality;
|
||||
continue; // Async, so the device won't be ready for writing just now
|
||||
}
|
||||
else if (ps->state != PA_STREAM_READY)
|
||||
continue;
|
||||
|
||||
ret = pa_stream_write(ps->stream, buf, length, NULL, 0LL, PA_SEEK_RELATIVE);
|
||||
if (ret < 0)
|
||||
{
|
||||
ret = pa_context_errno(pulse.context);
|
||||
DPRINTF(E_LOG, L_LAUDIO, "Error writing Pulseaudio stream data to '%s': %s\n", ps->devname, pa_strerror(ret));
|
||||
if (ps->stream && pa_stream_is_corked(ps->stream))
|
||||
playback_resume(ps);
|
||||
|
||||
ps->state = PA_STREAM_FAILED;
|
||||
pulse_session_shutdown(ps);
|
||||
|
||||
continue;
|
||||
}
|
||||
playback_write(ps, obuf);
|
||||
}
|
||||
|
||||
pa_threaded_mainloop_unlock(pulse.mainloop);
|
||||
}
|
||||
|
||||
static void
|
||||
pulse_playback_start(uint64_t next_pkt, struct timespec *ts)
|
||||
{
|
||||
struct pulse_session *ps;
|
||||
pa_operation* o;
|
||||
|
||||
pa_threaded_mainloop_lock(pulse.mainloop);
|
||||
|
||||
for (ps = sessions; ps; ps = ps->next)
|
||||
{
|
||||
o = pa_stream_cork(ps->stream, 0, NULL, NULL);
|
||||
if (!o)
|
||||
{
|
||||
DPRINTF(E_LOG, L_LAUDIO, "Pulseaudio could not resume '%s': %s\n", ps->devname, pa_strerror(pa_context_errno(pulse.context)));
|
||||
continue;
|
||||
}
|
||||
pa_operation_unref(o);
|
||||
}
|
||||
|
||||
pa_threaded_mainloop_unlock(pulse.mainloop);
|
||||
}
|
||||
|
||||
static void
|
||||
pulse_playback_stop(void)
|
||||
{
|
||||
struct pulse_session *ps;
|
||||
pa_operation* o;
|
||||
|
||||
pa_threaded_mainloop_lock(pulse.mainloop);
|
||||
|
||||
for (ps = sessions; ps; ps = ps->next)
|
||||
{
|
||||
o = pa_stream_cork(ps->stream, 1, NULL, NULL);
|
||||
if (!o)
|
||||
{
|
||||
DPRINTF(E_LOG, L_LAUDIO, "Pulseaudio could not pause '%s': %s\n", ps->devname, pa_strerror(pa_context_errno(pulse.context)));
|
||||
continue;
|
||||
}
|
||||
pa_operation_unref(o);
|
||||
|
||||
o = pa_stream_flush(ps->stream, NULL, NULL);
|
||||
if (!o)
|
||||
{
|
||||
DPRINTF(E_LOG, L_LAUDIO, "Pulseaudio could not flush '%s': %s\n", ps->devname, pa_strerror(pa_context_errno(pulse.context)));
|
||||
continue;
|
||||
}
|
||||
pa_operation_unref(o);
|
||||
}
|
||||
|
||||
pa_threaded_mainloop_unlock(pulse.mainloop);
|
||||
}
|
||||
|
||||
static int
|
||||
pulse_flush(output_status_cb cb, uint64_t rtptime)
|
||||
{
|
||||
struct pulse_session *ps;
|
||||
pa_operation* o;
|
||||
int i;
|
||||
|
||||
DPRINTF(E_DBG, L_LAUDIO, "Pulseaudio flush\n");
|
||||
|
||||
pa_threaded_mainloop_lock(pulse.mainloop);
|
||||
|
||||
i = 0;
|
||||
for (ps = sessions; ps; ps = ps->next)
|
||||
{
|
||||
i++;
|
||||
|
||||
ps->status_cb = cb;
|
||||
|
||||
o = pa_stream_cork(ps->stream, 1, NULL, NULL);
|
||||
if (!o)
|
||||
{
|
||||
DPRINTF(E_LOG, L_LAUDIO, "Pulseaudio could not pause '%s': %s\n", ps->devname, pa_strerror(pa_context_errno(pulse.context)));
|
||||
continue;
|
||||
}
|
||||
pa_operation_unref(o);
|
||||
|
||||
o = pa_stream_flush(ps->stream, flush_cb, ps);
|
||||
if (!o)
|
||||
{
|
||||
DPRINTF(E_LOG, L_LAUDIO, "Pulseaudio could not flush '%s': %s\n", ps->devname, pa_strerror(pa_context_errno(pulse.context)));
|
||||
continue;
|
||||
}
|
||||
pa_operation_unref(o);
|
||||
}
|
||||
|
||||
pa_threaded_mainloop_unlock(pulse.mainloop);
|
||||
|
||||
return i;
|
||||
}
|
||||
|
||||
static void
|
||||
pulse_set_status_cb(struct output_session *session, output_status_cb cb)
|
||||
{
|
||||
struct pulse_session *ps = session->session;
|
||||
|
||||
ps->status_cb = cb;
|
||||
}
|
||||
|
||||
static int
|
||||
@ -977,13 +996,11 @@ struct output_definition output_pulse =
|
||||
.deinit = pulse_deinit,
|
||||
.device_start = pulse_device_start,
|
||||
.device_stop = pulse_device_stop,
|
||||
.device_flush = pulse_device_flush,
|
||||
.device_probe = pulse_device_probe,
|
||||
.device_free_extra = pulse_device_free_extra,
|
||||
.device_cb_set = pulse_device_cb_set,
|
||||
.device_volume_set = pulse_device_volume_set,
|
||||
.playback_start = pulse_playback_start,
|
||||
.playback_stop = pulse_playback_stop,
|
||||
.write = pulse_write,
|
||||
.flush = pulse_flush,
|
||||
.status_cb = pulse_set_status_cb,
|
||||
};
|
||||
|
||||
|
1900
src/outputs/raop.c
1900
src/outputs/raop.c
File diff suppressed because it is too large
Load Diff
275
src/outputs/rtp_common.c
Normal file
275
src/outputs/rtp_common.c
Normal file
@ -0,0 +1,275 @@
|
||||
/*
|
||||
* Copyright (C) 2019- 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 <stdlib.h>
|
||||
#include <unistd.h>
|
||||
#include <string.h>
|
||||
#include <ctype.h>
|
||||
#include <errno.h>
|
||||
#include <stdint.h>
|
||||
#include <stdio.h>
|
||||
#include <stdarg.h>
|
||||
#include <limits.h>
|
||||
#include <sys/param.h>
|
||||
|
||||
#ifdef HAVE_ENDIAN_H
|
||||
# include <endian.h>
|
||||
#elif defined(HAVE_SYS_ENDIAN_H)
|
||||
# include <sys/endian.h>
|
||||
#elif defined(HAVE_LIBKERN_OSBYTEORDER_H)
|
||||
#include <libkern/OSByteOrder.h>
|
||||
#define htobe16(x) OSSwapHostToBigInt16(x)
|
||||
#define be16toh(x) OSSwapBigToHostInt16(x)
|
||||
#define htobe32(x) OSSwapHostToBigInt32(x)
|
||||
#endif
|
||||
|
||||
#include <gcrypt.h>
|
||||
|
||||
#include "logger.h"
|
||||
#include "misc.h"
|
||||
#include "rtp_common.h"
|
||||
|
||||
#define RTP_HEADER_LEN 12
|
||||
#define RTCP_SYNC_PACKET_LEN 20
|
||||
|
||||
// NTP timestamp definitions
|
||||
#define FRAC 4294967296. // 2^32 as a double
|
||||
#define NTP_EPOCH_DELTA 0x83aa7e80 // 2208988800 - that's 1970 - 1900 in seconds
|
||||
|
||||
struct ntp_timestamp
|
||||
{
|
||||
uint32_t sec;
|
||||
uint32_t frac;
|
||||
};
|
||||
|
||||
|
||||
static inline void
|
||||
timespec_to_ntp(struct timespec *ts, struct ntp_timestamp *ns)
|
||||
{
|
||||
/* Seconds since NTP Epoch (1900-01-01) */
|
||||
ns->sec = ts->tv_sec + NTP_EPOCH_DELTA;
|
||||
|
||||
ns->frac = (uint32_t)((double)ts->tv_nsec * 1e-9 * FRAC);
|
||||
}
|
||||
|
||||
static inline void
|
||||
ntp_to_timespec(struct ntp_timestamp *ns, struct timespec *ts)
|
||||
{
|
||||
/* Seconds since Unix Epoch (1970-01-01) */
|
||||
ts->tv_sec = ns->sec - NTP_EPOCH_DELTA;
|
||||
|
||||
ts->tv_nsec = (long)((double)ns->frac / (1e-9 * FRAC));
|
||||
}
|
||||
|
||||
struct rtp_session *
|
||||
rtp_session_new(struct media_quality *quality, int pktbuf_size, int sync_each_nsamples)
|
||||
{
|
||||
struct rtp_session *session;
|
||||
|
||||
CHECK_NULL(L_PLAYER, session = calloc(1, sizeof(struct rtp_session)));
|
||||
|
||||
// Random SSRC ID, RTP time start and sequence start
|
||||
gcry_randomize(&session->ssrc_id, sizeof(session->ssrc_id), GCRY_STRONG_RANDOM);
|
||||
gcry_randomize(&session->pos, sizeof(session->pos), GCRY_STRONG_RANDOM);
|
||||
gcry_randomize(&session->seqnum, sizeof(session->seqnum), GCRY_STRONG_RANDOM);
|
||||
|
||||
session->quality = *quality;
|
||||
|
||||
session->pktbuf_size = pktbuf_size;
|
||||
CHECK_NULL(L_PLAYER, session->pktbuf = calloc(session->pktbuf_size, sizeof(struct rtp_packet)));
|
||||
|
||||
if (sync_each_nsamples > 0)
|
||||
session->sync_each_nsamples = sync_each_nsamples;
|
||||
else if (sync_each_nsamples == 0)
|
||||
session->sync_each_nsamples = quality->sample_rate;
|
||||
|
||||
return session;
|
||||
}
|
||||
|
||||
void
|
||||
rtp_session_free(struct rtp_session *session)
|
||||
{
|
||||
int i;
|
||||
|
||||
for (i = 0; i < session->pktbuf_size; i++)
|
||||
free(session->pktbuf[i].data);
|
||||
|
||||
free(session->pktbuf);
|
||||
free(session->sync_packet_next.data);
|
||||
free(session);
|
||||
}
|
||||
|
||||
void
|
||||
rtp_session_flush(struct rtp_session *session)
|
||||
{
|
||||
session->pktbuf_len = 0;
|
||||
session->sync_counter = 0;
|
||||
}
|
||||
|
||||
// We don't want the caller to malloc payload for every packet, so instead we
|
||||
// will get him a packet from the ring buffer, thus in most cases reusing memory
|
||||
struct rtp_packet *
|
||||
rtp_packet_next(struct rtp_session *session, size_t payload_len, int samples, char type)
|
||||
{
|
||||
struct rtp_packet *pkt;
|
||||
uint16_t seq;
|
||||
uint32_t rtptime;
|
||||
uint32_t ssrc_id;
|
||||
|
||||
pkt = &session->pktbuf[session->pktbuf_next];
|
||||
|
||||
// When first filling up the buffer we malloc, but otherwise the existing data
|
||||
// allocation should in most cases suffice. If not, we realloc.
|
||||
if (!pkt->data || payload_len > pkt->payload_size)
|
||||
{
|
||||
pkt->data_size = RTP_HEADER_LEN + payload_len;
|
||||
if (!pkt->data)
|
||||
CHECK_NULL(L_PLAYER, pkt->data = malloc(pkt->data_size));
|
||||
else
|
||||
CHECK_NULL(L_PLAYER, pkt->data = realloc(pkt->data, pkt->data_size));
|
||||
pkt->header = pkt->data;
|
||||
pkt->payload = pkt->data + RTP_HEADER_LEN;
|
||||
pkt->payload_size = payload_len;
|
||||
}
|
||||
|
||||
pkt->samples = samples;
|
||||
pkt->payload_len = payload_len;
|
||||
pkt->data_len = RTP_HEADER_LEN + payload_len;
|
||||
pkt->seqnum = session->seqnum;
|
||||
|
||||
|
||||
// The RTP header is made of these 12 bytes (RFC 3550):
|
||||
// 0 1 2 3
|
||||
// 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
|
||||
// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
||||
// |V=2|P|X| CC |M| PT | sequence number |
|
||||
// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
||||
// | timestamp |
|
||||
// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
||||
// | synchronization source (SSRC) identifier |
|
||||
// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
||||
pkt->header[0] = 0x80; // Version = 2, P, X and CC are 0
|
||||
pkt->header[1] = type; // RTP payload type
|
||||
|
||||
seq = htobe16(session->seqnum);
|
||||
memcpy(pkt->header + 2, &seq, 2);
|
||||
|
||||
rtptime = htobe32(session->pos);
|
||||
memcpy(pkt->header + 4, &rtptime, 4);
|
||||
|
||||
ssrc_id = htobe32(session->ssrc_id);
|
||||
memcpy(pkt->header + 8, &ssrc_id, 4);
|
||||
|
||||
return pkt;
|
||||
}
|
||||
|
||||
void
|
||||
rtp_packet_commit(struct rtp_session *session, struct rtp_packet *pkt)
|
||||
{
|
||||
// Increase size of retransmit buffer since we just wrote a packet
|
||||
if (session->pktbuf_len < session->pktbuf_size)
|
||||
session->pktbuf_len++;
|
||||
|
||||
// Advance counters to prepare for next packet
|
||||
session->pktbuf_next = (session->pktbuf_next + 1) % session->pktbuf_size;
|
||||
session->seqnum++;
|
||||
session->pos += pkt->samples;
|
||||
session->sync_counter += pkt->samples;
|
||||
}
|
||||
|
||||
struct rtp_packet *
|
||||
rtp_packet_get(struct rtp_session *session, uint16_t seqnum)
|
||||
{
|
||||
uint16_t first;
|
||||
uint16_t last;
|
||||
size_t idx;
|
||||
|
||||
if (!session->seqnum || !session->pktbuf_len)
|
||||
return NULL;
|
||||
|
||||
last = session->seqnum - 1;
|
||||
first = session->seqnum - session->pktbuf_len;
|
||||
if (seqnum < first || seqnum > last)
|
||||
{
|
||||
DPRINTF(E_DBG, L_PLAYER, "Seqnum %" PRIu16 " not in buffer (have seqnum %" PRIu16 " to %" PRIu16 ")\n", seqnum, first, last);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
idx = (session->pktbuf_next - (session->seqnum - seqnum)) % session->pktbuf_size;
|
||||
|
||||
return &session->pktbuf[idx];
|
||||
}
|
||||
|
||||
bool
|
||||
rtp_sync_is_time(struct rtp_session *session)
|
||||
{
|
||||
if (session->sync_each_nsamples && session->sync_counter > session->sync_each_nsamples)
|
||||
{
|
||||
session->sync_counter = 0;
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
struct rtp_packet *
|
||||
rtp_sync_packet_next(struct rtp_session *session, struct rtcp_timestamp *cur_stamp, char type)
|
||||
{
|
||||
struct ntp_timestamp cur_ts;
|
||||
uint32_t rtptime;
|
||||
uint32_t cur_pos;
|
||||
|
||||
if (!session->sync_packet_next.data)
|
||||
{
|
||||
CHECK_NULL(L_PLAYER, session->sync_packet_next.data = malloc(RTCP_SYNC_PACKET_LEN));
|
||||
session->sync_packet_next.data_len = RTCP_SYNC_PACKET_LEN;
|
||||
}
|
||||
|
||||
session->sync_packet_next.data[0] = type;
|
||||
session->sync_packet_next.data[1] = 0xd4;
|
||||
session->sync_packet_next.data[2] = 0x00;
|
||||
session->sync_packet_next.data[3] = 0x07;
|
||||
|
||||
timespec_to_ntp(&cur_stamp->ts, &cur_ts);
|
||||
|
||||
cur_pos = htobe32(cur_stamp->pos);
|
||||
memcpy(session->sync_packet_next.data + 4, &cur_pos, 4);
|
||||
|
||||
cur_ts.sec = htobe32(cur_ts.sec);
|
||||
cur_ts.frac = htobe32(cur_ts.frac);
|
||||
memcpy(session->sync_packet_next.data + 8, &cur_ts.sec, 4);
|
||||
memcpy(session->sync_packet_next.data + 12, &cur_ts.frac, 4);
|
||||
|
||||
rtptime = htobe32(session->pos);
|
||||
memcpy(session->sync_packet_next.data + 16, &rtptime, 4);
|
||||
|
||||
/* DPRINTF(E_DBG, L_PLAYER, "SYNC PACKET cur_ts:%ld.%ld, next_pkt:%u, cur_pos:%u, type:0x%x, sync_counter:%d\n",
|
||||
cur_stamp->ts.tv_sec, cur_stamp->ts.tv_nsec,
|
||||
session->pos,
|
||||
cur_stamp->pos,
|
||||
session->sync_packet_next.data[0],
|
||||
session->sync_counter
|
||||
);
|
||||
*/
|
||||
return &session->sync_packet_next;
|
||||
}
|
||||
|
80
src/outputs/rtp_common.h
Normal file
80
src/outputs/rtp_common.h
Normal file
@ -0,0 +1,80 @@
|
||||
#ifndef __RTP_COMMON_H__
|
||||
#define __RTP_COMMON_H__
|
||||
|
||||
#include <stdint.h>
|
||||
#include <inttypes.h>
|
||||
#include <stdbool.h>
|
||||
|
||||
struct rtcp_timestamp
|
||||
{
|
||||
uint32_t pos;
|
||||
struct timespec ts;
|
||||
};
|
||||
|
||||
struct rtp_packet
|
||||
{
|
||||
uint16_t seqnum; // Sequence number
|
||||
int samples; // Number of samples in the packet
|
||||
|
||||
uint8_t *header; // Pointer to the RTP header
|
||||
|
||||
uint8_t *payload; // Pointer to the RTP payload
|
||||
size_t payload_size; // Size of allocated memory for RTP payload
|
||||
size_t payload_len; // Length of payload (must of course not exceed size)
|
||||
|
||||
uint8_t *data; // Pointer to the complete packet data
|
||||
size_t data_size; // Size of packet data
|
||||
size_t data_len; // Length of actual packet data
|
||||
};
|
||||
|
||||
// An RTP session is characterised by all the receivers belonging to the session
|
||||
// getting the same RTP and RTCP packets. So if you have clients that require
|
||||
// different sample rates or where only some can accept encrypted payloads then
|
||||
// you need multiple sessions.
|
||||
struct rtp_session
|
||||
{
|
||||
uint32_t ssrc_id;
|
||||
uint32_t pos;
|
||||
uint16_t seqnum;
|
||||
|
||||
struct media_quality quality;
|
||||
|
||||
// Packet buffer (ring buffer), used for retransmission
|
||||
struct rtp_packet *pktbuf;
|
||||
size_t pktbuf_next;
|
||||
size_t pktbuf_size;
|
||||
size_t pktbuf_len;
|
||||
|
||||
// Number of samples to elapse before sync'ing. If 0 we set it to the s/r, so
|
||||
// we sync once a second. If negative we won't sync.
|
||||
int sync_each_nsamples;
|
||||
int sync_counter;
|
||||
struct rtp_packet sync_packet_next;
|
||||
};
|
||||
|
||||
|
||||
struct rtp_session *
|
||||
rtp_session_new(struct media_quality *quality, int pktbuf_size, int sync_each_nsamples);
|
||||
|
||||
void
|
||||
rtp_session_free(struct rtp_session *session);
|
||||
|
||||
void
|
||||
rtp_session_flush(struct rtp_session *session);
|
||||
|
||||
struct rtp_packet *
|
||||
rtp_packet_next(struct rtp_session *session, size_t payload_len, int samples, char type);
|
||||
|
||||
void
|
||||
rtp_packet_commit(struct rtp_session *session, struct rtp_packet *pkt);
|
||||
|
||||
struct rtp_packet *
|
||||
rtp_packet_get(struct rtp_session *session, uint16_t seqnum);
|
||||
|
||||
bool
|
||||
rtp_sync_is_time(struct rtp_session *session);
|
||||
|
||||
struct rtp_packet *
|
||||
rtp_sync_packet_next(struct rtp_session *session, struct rtcp_timestamp *cur_stamp, char type);
|
||||
|
||||
#endif /* !__RTP_COMMON_H__ */
|
@ -24,7 +24,6 @@
|
||||
#include "outputs.h"
|
||||
#include "httpd_streaming.h"
|
||||
|
||||
|
||||
struct output_definition output_streaming =
|
||||
{
|
||||
.name = "mp3 streaming",
|
||||
|
2404
src/player.c
2404
src/player.c
File diff suppressed because it is too large
Load Diff
23
src/player.h
23
src/player.h
@ -7,14 +7,7 @@
|
||||
|
||||
#include "db.h"
|
||||
|
||||
/* AirTunes v2 packet interval in ns */
|
||||
/* (352 samples/packet * 1e9 ns/s) / 44100 samples/s = 7981859 ns/packet */
|
||||
# define AIRTUNES_V2_STREAM_PERIOD 7981859
|
||||
|
||||
/* AirTunes v2 number of samples per packet */
|
||||
#define AIRTUNES_V2_PACKET_SAMPLES 352
|
||||
|
||||
/* Maximum number of previously played songs that are remembered */
|
||||
// Maximum number of previously played songs that are remembered
|
||||
#define MAX_HISTORY_COUNT 20
|
||||
|
||||
enum play_status {
|
||||
@ -79,15 +72,11 @@ struct player_history
|
||||
uint32_t item_id[MAX_HISTORY_COUNT];
|
||||
};
|
||||
|
||||
|
||||
int
|
||||
player_get_current_pos(uint64_t *pos, struct timespec *ts, int commit);
|
||||
|
||||
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);
|
||||
@ -104,9 +93,6 @@ player_speaker_enable(uint64_t id);
|
||||
int
|
||||
player_speaker_disable(uint64_t id);
|
||||
|
||||
void
|
||||
player_speaker_status_trigger(void);
|
||||
|
||||
int
|
||||
player_playback_start(void);
|
||||
|
||||
@ -152,7 +138,6 @@ player_shuffle_set(int enable);
|
||||
int
|
||||
player_consume_set(int enable);
|
||||
|
||||
|
||||
void
|
||||
player_queue_clear_history(void);
|
||||
|
||||
@ -171,8 +156,8 @@ player_device_remove(void *device);
|
||||
void
|
||||
player_raop_verification_kickoff(char **arglist);
|
||||
|
||||
void
|
||||
player_metadata_send(void *imd, void *omd);
|
||||
const char *
|
||||
player_pmap(void *p);
|
||||
|
||||
int
|
||||
player_init(void);
|
||||
|
@ -718,8 +718,7 @@ 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, INPUT_FLAG_EOF);
|
||||
input_write(spotify_audio_buffer, NULL, INPUT_FLAG_EOF);
|
||||
|
||||
*retval = 0;
|
||||
return COMMAND_END;
|
||||
@ -1007,17 +1006,22 @@ logged_out(sp_session *sess)
|
||||
static int music_delivery(sp_session *sess, const sp_audioformat *format,
|
||||
const void *frames, int num_frames)
|
||||
{
|
||||
struct media_quality quality = { 0 };
|
||||
size_t size;
|
||||
int ret;
|
||||
|
||||
/* No support for resampling right now */
|
||||
if ((format->sample_rate != 44100) || (format->channels != 2))
|
||||
if ((format->sample_type != SP_SAMPLETYPE_INT16_NATIVE_ENDIAN) || (format->channels != 2))
|
||||
{
|
||||
DPRINTF(E_LOG, L_SPOTIFY, "Got music with unsupported samplerate or channels, stopping playback\n");
|
||||
DPRINTF(E_LOG, L_SPOTIFY, "Got music with unsupported sample format or number of channels, stopping playback\n");
|
||||
spotify_playback_stop_nonblock();
|
||||
return num_frames;
|
||||
}
|
||||
|
||||
quality.sample_rate = format->sample_rate;
|
||||
quality.bits_per_sample = 16;
|
||||
quality.channels = format->channels;
|
||||
|
||||
// Audio discontinuity, e.g. seek
|
||||
if (num_frames == 0)
|
||||
{
|
||||
@ -1037,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, INPUT_FLAG_NONBLOCK);
|
||||
input_write(spotify_audio_buffer, &quality, 0);
|
||||
|
||||
return num_frames;
|
||||
}
|
||||
|
250
src/transcode.c
250
src/transcode.c
@ -40,6 +40,7 @@
|
||||
#include "conffile.h"
|
||||
#include "db.h"
|
||||
#include "avio_evbuffer.h"
|
||||
#include "misc.h"
|
||||
#include "transcode.h"
|
||||
|
||||
// Interval between ICY metadata checks for streams, in seconds
|
||||
@ -75,12 +76,10 @@ struct settings_ctx
|
||||
|
||||
// Audio settings
|
||||
enum AVCodecID audio_codec;
|
||||
const char *audio_codec_name;
|
||||
int sample_rate;
|
||||
uint64_t channel_layout;
|
||||
int channels;
|
||||
enum AVSampleFormat sample_format;
|
||||
int byte_depth;
|
||||
bool wavheader;
|
||||
bool icy;
|
||||
|
||||
@ -178,47 +177,56 @@ struct encode_ctx
|
||||
uint8_t header[44];
|
||||
};
|
||||
|
||||
struct transcode_ctx
|
||||
{
|
||||
struct decode_ctx *decode_ctx;
|
||||
struct encode_ctx *encode_ctx;
|
||||
};
|
||||
|
||||
|
||||
/* -------------------------- PROFILE CONFIGURATION ------------------------ */
|
||||
|
||||
static int
|
||||
init_settings(struct settings_ctx *settings, enum transcode_profile profile)
|
||||
init_settings(struct settings_ctx *settings, enum transcode_profile profile, struct media_quality *quality)
|
||||
{
|
||||
const AVCodecDescriptor *codec_desc;
|
||||
|
||||
memset(settings, 0, sizeof(struct settings_ctx));
|
||||
|
||||
switch (profile)
|
||||
{
|
||||
case XCODE_PCM_NATIVE: // Sample rate and bit depth determined by source
|
||||
settings->encode_audio = 1;
|
||||
settings->icy = 1;
|
||||
break;
|
||||
|
||||
case XCODE_PCM16_HEADER:
|
||||
settings->wavheader = 1;
|
||||
case XCODE_PCM16_NOHEADER:
|
||||
case XCODE_PCM16:
|
||||
settings->encode_audio = 1;
|
||||
settings->format = "s16le";
|
||||
settings->audio_codec = AV_CODEC_ID_PCM_S16LE;
|
||||
settings->sample_rate = 44100;
|
||||
settings->channel_layout = AV_CH_LAYOUT_STEREO;
|
||||
settings->channels = 2;
|
||||
settings->sample_format = AV_SAMPLE_FMT_S16;
|
||||
settings->byte_depth = 2; // Bytes per sample = 16/8
|
||||
settings->icy = 1;
|
||||
break;
|
||||
|
||||
case XCODE_PCM24:
|
||||
settings->encode_audio = 1;
|
||||
settings->format = "s24le";
|
||||
settings->audio_codec = AV_CODEC_ID_PCM_S24LE;
|
||||
settings->sample_format = AV_SAMPLE_FMT_S32;
|
||||
break;
|
||||
|
||||
case XCODE_PCM32:
|
||||
settings->encode_audio = 1;
|
||||
settings->format = "s32le";
|
||||
settings->audio_codec = AV_CODEC_ID_PCM_S32LE;
|
||||
settings->sample_format = AV_SAMPLE_FMT_S32;
|
||||
break;
|
||||
|
||||
case XCODE_MP3:
|
||||
settings->encode_audio = 1;
|
||||
settings->format = "mp3";
|
||||
settings->audio_codec = AV_CODEC_ID_MP3;
|
||||
settings->sample_rate = 44100;
|
||||
settings->channel_layout = AV_CH_LAYOUT_STEREO;
|
||||
settings->channels = 2;
|
||||
settings->sample_format = AV_SAMPLE_FMT_S16P;
|
||||
settings->byte_depth = 2; // Bytes per sample = 16/8
|
||||
break;
|
||||
|
||||
case XCODE_OPUS:
|
||||
settings->encode_audio = 1;
|
||||
settings->format = "data"; // Means we get the raw packet from the encoder, no muxing
|
||||
settings->audio_codec = AV_CODEC_ID_OPUS;
|
||||
settings->sample_format = AV_SAMPLE_FMT_S16; // Only libopus support
|
||||
break;
|
||||
|
||||
case XCODE_JPEG:
|
||||
@ -226,6 +234,7 @@ init_settings(struct settings_ctx *settings, enum transcode_profile profile)
|
||||
settings->silent = 1;
|
||||
settings->format = "image2";
|
||||
settings->in_format = "mjpeg";
|
||||
settings->pix_fmt = AV_PIX_FMT_YUVJ420P;
|
||||
settings->video_codec = AV_CODEC_ID_MJPEG;
|
||||
break;
|
||||
|
||||
@ -233,6 +242,7 @@ init_settings(struct settings_ctx *settings, enum transcode_profile profile)
|
||||
settings->encode_video = 1;
|
||||
settings->silent = 1;
|
||||
settings->format = "image2";
|
||||
settings->pix_fmt = AV_PIX_FMT_RGB24;
|
||||
settings->video_codec = AV_CODEC_ID_PNG;
|
||||
break;
|
||||
|
||||
@ -241,16 +251,21 @@ init_settings(struct settings_ctx *settings, enum transcode_profile profile)
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (settings->audio_codec)
|
||||
if (quality && quality->sample_rate)
|
||||
{
|
||||
codec_desc = avcodec_descriptor_get(settings->audio_codec);
|
||||
settings->audio_codec_name = codec_desc->name;
|
||||
settings->sample_rate = quality->sample_rate;
|
||||
}
|
||||
|
||||
if (settings->video_codec)
|
||||
if (quality && quality->channels)
|
||||
{
|
||||
codec_desc = avcodec_descriptor_get(settings->video_codec);
|
||||
settings->video_codec_name = codec_desc->name;
|
||||
settings->channels = quality->channels;
|
||||
settings->channel_layout = av_get_default_channel_layout(quality->channels);
|
||||
}
|
||||
|
||||
if (quality && quality->bits_per_sample && (quality->bits_per_sample != 8 * av_get_bytes_per_sample(settings->sample_format)))
|
||||
{
|
||||
DPRINTF(E_LOG, L_XCODE, "Bug! Mismatch between profile and media quality\n");
|
||||
return -1;
|
||||
}
|
||||
|
||||
return 0;
|
||||
@ -279,6 +294,19 @@ stream_settings_set(struct stream_ctx *s, struct settings_ctx *settings, enum AV
|
||||
|
||||
/* -------------------------------- HELPERS -------------------------------- */
|
||||
|
||||
static enum AVSampleFormat
|
||||
bitdepth2format(int bits_per_sample)
|
||||
{
|
||||
if (bits_per_sample == 16)
|
||||
return AV_SAMPLE_FMT_S16;
|
||||
else if (bits_per_sample == 24)
|
||||
return AV_SAMPLE_FMT_S32;
|
||||
else if (bits_per_sample == 32)
|
||||
return AV_SAMPLE_FMT_S32;
|
||||
else
|
||||
return AV_SAMPLE_FMT_NONE;
|
||||
}
|
||||
|
||||
static inline char *
|
||||
err2str(int errnum)
|
||||
{
|
||||
@ -307,15 +335,18 @@ make_wav_header(struct encode_ctx *ctx, struct decode_ctx *src_ctx, off_t *est_s
|
||||
{
|
||||
uint32_t wav_len;
|
||||
int duration;
|
||||
int bps;
|
||||
|
||||
if (src_ctx->duration)
|
||||
duration = src_ctx->duration;
|
||||
else
|
||||
duration = 3 * 60 * 1000; /* 3 minutes, in ms */
|
||||
|
||||
wav_len = ctx->settings.channels * ctx->settings.byte_depth * ctx->settings.sample_rate * (duration / 1000);
|
||||
bps = av_get_bytes_per_sample(ctx->settings.sample_format);
|
||||
wav_len = ctx->settings.channels * bps * ctx->settings.sample_rate * (duration / 1000);
|
||||
|
||||
*est_size = wav_len + sizeof(ctx->header);
|
||||
if (est_size)
|
||||
*est_size = wav_len + sizeof(ctx->header);
|
||||
|
||||
memcpy(ctx->header, "RIFF", 4);
|
||||
add_le32(ctx->header + 4, 36 + wav_len);
|
||||
@ -324,9 +355,9 @@ make_wav_header(struct encode_ctx *ctx, struct decode_ctx *src_ctx, off_t *est_s
|
||||
add_le16(ctx->header + 20, 1);
|
||||
add_le16(ctx->header + 22, ctx->settings.channels); /* channels */
|
||||
add_le32(ctx->header + 24, ctx->settings.sample_rate); /* samplerate */
|
||||
add_le32(ctx->header + 28, ctx->settings.sample_rate * ctx->settings.channels * ctx->settings.byte_depth); /* byte rate */
|
||||
add_le16(ctx->header + 32, ctx->settings.channels * ctx->settings.byte_depth); /* block align */
|
||||
add_le16(ctx->header + 34, ctx->settings.byte_depth * 8); /* bits per sample */
|
||||
add_le32(ctx->header + 28, ctx->settings.sample_rate * ctx->settings.channels * bps); /* byte rate */
|
||||
add_le16(ctx->header + 32, ctx->settings.channels * bps); /* block align */
|
||||
add_le16(ctx->header + 34, 8 * bps); /* bits per sample */
|
||||
memcpy(ctx->header + 36, "data", 4);
|
||||
add_le32(ctx->header + 40, wav_len);
|
||||
}
|
||||
@ -356,20 +387,27 @@ stream_find(struct decode_ctx *ctx, unsigned int stream_index)
|
||||
* @out ctx A pre-allocated stream ctx where we save stream and codec info
|
||||
* @in output Output to add the stream to
|
||||
* @in codec_id What kind of codec should we use
|
||||
* @in codec_name Name of codec (only used for logging)
|
||||
* @return Negative on failure, otherwise zero
|
||||
*/
|
||||
static int
|
||||
stream_add(struct encode_ctx *ctx, struct stream_ctx *s, enum AVCodecID codec_id, const char *codec_name)
|
||||
stream_add(struct encode_ctx *ctx, struct stream_ctx *s, enum AVCodecID codec_id)
|
||||
{
|
||||
const AVCodecDescriptor *codec_desc;
|
||||
AVCodec *encoder;
|
||||
AVDictionary *options = NULL;
|
||||
int ret;
|
||||
|
||||
codec_desc = avcodec_descriptor_get(codec_id);
|
||||
if (!codec_desc)
|
||||
{
|
||||
DPRINTF(E_LOG, L_XCODE, "Invalid codec ID (%d)\n", codec_id);
|
||||
return -1;
|
||||
}
|
||||
|
||||
encoder = avcodec_find_encoder(codec_id);
|
||||
if (!encoder)
|
||||
{
|
||||
DPRINTF(E_LOG, L_XCODE, "Necessary encoder (%s) not found\n", codec_name);
|
||||
DPRINTF(E_LOG, L_XCODE, "Necessary encoder (%s) not found\n", codec_desc->name);
|
||||
return -1;
|
||||
}
|
||||
|
||||
@ -381,7 +419,7 @@ stream_add(struct encode_ctx *ctx, struct stream_ctx *s, enum AVCodecID codec_id
|
||||
if (!s->codec->pix_fmt)
|
||||
{
|
||||
s->codec->pix_fmt = avcodec_default_get_format(s->codec, encoder->pix_fmts);
|
||||
DPRINTF(E_DBG, L_XCODE, "Pixel format set to %s (encoder is %s)\n", av_get_pix_fmt_name(s->codec->pix_fmt), codec_name);
|
||||
DPRINTF(E_DBG, L_XCODE, "Pixel format set to %s (encoder is %s)\n", av_get_pix_fmt_name(s->codec->pix_fmt), codec_desc->name);
|
||||
}
|
||||
|
||||
if (ctx->ofmt_ctx->oformat->flags & AVFMT_GLOBALHEADER)
|
||||
@ -394,7 +432,7 @@ stream_add(struct encode_ctx *ctx, struct stream_ctx *s, enum AVCodecID codec_id
|
||||
ret = avcodec_open2(s->codec, NULL, &options);
|
||||
if (ret < 0)
|
||||
{
|
||||
DPRINTF(E_LOG, L_XCODE, "Cannot open encoder (%s): %s\n", codec_name, err2str(ret));
|
||||
DPRINTF(E_LOG, L_XCODE, "Cannot open encoder (%s): %s\n", codec_desc->name, err2str(ret));
|
||||
avcodec_free_context(&s->codec);
|
||||
return -1;
|
||||
}
|
||||
@ -403,7 +441,7 @@ stream_add(struct encode_ctx *ctx, struct stream_ctx *s, enum AVCodecID codec_id
|
||||
ret = avcodec_parameters_from_context(s->stream->codecpar, s->codec);
|
||||
if (ret < 0)
|
||||
{
|
||||
DPRINTF(E_LOG, L_XCODE, "Cannot copy stream parameters (%s): %s\n", codec_name, err2str(ret));
|
||||
DPRINTF(E_LOG, L_XCODE, "Cannot copy stream parameters (%s): %s\n", codec_desc->name, err2str(ret));
|
||||
avcodec_free_context(&s->codec);
|
||||
return -1;
|
||||
}
|
||||
@ -854,7 +892,7 @@ open_output(struct encode_ctx *ctx, struct decode_ctx *src_ctx)
|
||||
}
|
||||
|
||||
// Clear AVFMT_NOFILE bit, it is not allowed as we will set our own AVIOContext
|
||||
oformat->flags = ~AVFMT_NOFILE;
|
||||
oformat->flags &= ~AVFMT_NOFILE;
|
||||
|
||||
CHECK_NULL(L_XCODE, ctx->ofmt_ctx = avformat_alloc_context());
|
||||
|
||||
@ -876,14 +914,14 @@ open_output(struct encode_ctx *ctx, struct decode_ctx *src_ctx)
|
||||
|
||||
if (ctx->settings.encode_audio)
|
||||
{
|
||||
ret = stream_add(ctx, &ctx->audio_stream, ctx->settings.audio_codec, ctx->settings.audio_codec_name);
|
||||
ret = stream_add(ctx, &ctx->audio_stream, ctx->settings.audio_codec);
|
||||
if (ret < 0)
|
||||
goto out_free_streams;
|
||||
}
|
||||
|
||||
if (ctx->settings.encode_video)
|
||||
{
|
||||
ret = stream_add(ctx, &ctx->video_stream, ctx->settings.video_codec, ctx->settings.video_codec_name);
|
||||
ret = stream_add(ctx, &ctx->video_stream, ctx->settings.video_codec);
|
||||
if (ret < 0)
|
||||
goto out_free_streams;
|
||||
}
|
||||
@ -972,6 +1010,8 @@ open_filter(struct stream_ctx *out_stream, struct stream_ctx *in_stream)
|
||||
goto out_fail;
|
||||
}
|
||||
|
||||
DPRINTF(E_DBG, L_XCODE, "Created 'in' filter: %s\n", args);
|
||||
|
||||
snprintf(args, sizeof(args),
|
||||
"sample_fmts=%s:sample_rates=%d:channel_layouts=0x%"PRIx64,
|
||||
av_get_sample_fmt_name(out_stream->codec->sample_fmt), out_stream->codec->sample_rate,
|
||||
@ -984,6 +1024,8 @@ open_filter(struct stream_ctx *out_stream, struct stream_ctx *in_stream)
|
||||
goto out_fail;
|
||||
}
|
||||
|
||||
DPRINTF(E_DBG, L_XCODE, "Created 'format' filter: %s\n", args);
|
||||
|
||||
ret = avfilter_graph_create_filter(&buffersink_ctx, buffersink, "out", NULL, NULL, filter_graph);
|
||||
if (ret < 0)
|
||||
{
|
||||
@ -1122,7 +1164,7 @@ close_filters(struct encode_ctx *ctx)
|
||||
/* Setup */
|
||||
|
||||
struct decode_ctx *
|
||||
transcode_decode_setup(enum transcode_profile profile, enum data_kind data_kind, const char *path, struct evbuffer *evbuf, uint32_t song_length)
|
||||
transcode_decode_setup(enum transcode_profile profile, struct media_quality *quality, enum data_kind data_kind, const char *path, struct evbuffer *evbuf, uint32_t song_length)
|
||||
{
|
||||
struct decode_ctx *ctx;
|
||||
|
||||
@ -1133,7 +1175,7 @@ transcode_decode_setup(enum transcode_profile profile, enum data_kind data_kind,
|
||||
ctx->duration = song_length;
|
||||
ctx->data_kind = data_kind;
|
||||
|
||||
if ((init_settings(&ctx->settings, profile) < 0) || (open_input(ctx, path, evbuf) < 0))
|
||||
if ((init_settings(&ctx->settings, profile, quality) < 0) || (open_input(ctx, path, evbuf) < 0))
|
||||
goto fail_free;
|
||||
|
||||
return ctx;
|
||||
@ -1146,20 +1188,52 @@ transcode_decode_setup(enum transcode_profile profile, enum data_kind data_kind,
|
||||
}
|
||||
|
||||
struct encode_ctx *
|
||||
transcode_encode_setup(enum transcode_profile profile, struct decode_ctx *src_ctx, off_t *est_size, int width, int height)
|
||||
transcode_encode_setup(enum transcode_profile profile, struct media_quality *quality, struct decode_ctx *src_ctx, off_t *est_size, int width, int height)
|
||||
{
|
||||
struct encode_ctx *ctx;
|
||||
int bps;
|
||||
|
||||
CHECK_NULL(L_XCODE, ctx = calloc(1, sizeof(struct encode_ctx)));
|
||||
CHECK_NULL(L_XCODE, ctx->filt_frame = av_frame_alloc());
|
||||
CHECK_NULL(L_XCODE, ctx->encoded_pkt = av_packet_alloc());
|
||||
|
||||
if (init_settings(&ctx->settings, profile) < 0)
|
||||
if (init_settings(&ctx->settings, profile, quality) < 0)
|
||||
goto fail_free;
|
||||
|
||||
ctx->settings.width = width;
|
||||
ctx->settings.height = height;
|
||||
|
||||
// Caller did not specify a sample rate -> use same as source
|
||||
if (!ctx->settings.sample_rate && ctx->settings.encode_audio)
|
||||
{
|
||||
ctx->settings.sample_rate = src_ctx->audio_stream.codec->sample_rate;
|
||||
}
|
||||
|
||||
// Caller did not specify a sample format -> use same as source
|
||||
if (!ctx->settings.sample_format && ctx->settings.encode_audio)
|
||||
{
|
||||
bps = av_get_bytes_per_sample(src_ctx->audio_stream.codec->sample_fmt);
|
||||
if (bps == 4)
|
||||
{
|
||||
ctx->settings.sample_format = AV_SAMPLE_FMT_S32;
|
||||
ctx->settings.audio_codec = AV_CODEC_ID_PCM_S32LE;
|
||||
ctx->settings.format = "s32le";
|
||||
}
|
||||
else
|
||||
{
|
||||
ctx->settings.sample_format = AV_SAMPLE_FMT_S16;
|
||||
ctx->settings.audio_codec = AV_CODEC_ID_PCM_S16LE;
|
||||
ctx->settings.format = "s16le";
|
||||
}
|
||||
}
|
||||
|
||||
// Caller did not specify channels -> use same as source
|
||||
if (!ctx->settings.channels && ctx->settings.encode_audio)
|
||||
{
|
||||
ctx->settings.channels = src_ctx->audio_stream.codec->channels;
|
||||
ctx->settings.channel_layout = src_ctx->audio_stream.codec->channel_layout;
|
||||
}
|
||||
|
||||
if (ctx->settings.wavheader)
|
||||
make_wav_header(ctx, src_ctx, est_size);
|
||||
|
||||
@ -1170,7 +1244,10 @@ transcode_encode_setup(enum transcode_profile profile, struct decode_ctx *src_ct
|
||||
goto fail_close;
|
||||
|
||||
if (ctx->settings.icy && src_ctx->data_kind == DATA_KIND_HTTP)
|
||||
ctx->icy_interval = METADATA_ICY_INTERVAL * ctx->settings.channels * ctx->settings.byte_depth * ctx->settings.sample_rate;
|
||||
{
|
||||
bps = av_get_bytes_per_sample(ctx->settings.sample_format);
|
||||
ctx->icy_interval = METADATA_ICY_INTERVAL * ctx->settings.channels * bps * ctx->settings.sample_rate;
|
||||
}
|
||||
|
||||
return ctx;
|
||||
|
||||
@ -1184,20 +1261,20 @@ transcode_encode_setup(enum transcode_profile profile, struct decode_ctx *src_ct
|
||||
}
|
||||
|
||||
struct transcode_ctx *
|
||||
transcode_setup(enum transcode_profile profile, enum data_kind data_kind, const char *path, uint32_t song_length, off_t *est_size)
|
||||
transcode_setup(enum transcode_profile profile, struct media_quality *quality, enum data_kind data_kind, const char *path, uint32_t song_length, off_t *est_size)
|
||||
{
|
||||
struct transcode_ctx *ctx;
|
||||
|
||||
CHECK_NULL(L_XCODE, ctx = calloc(1, sizeof(struct transcode_ctx)));
|
||||
|
||||
ctx->decode_ctx = transcode_decode_setup(profile, data_kind, path, NULL, song_length);
|
||||
ctx->decode_ctx = transcode_decode_setup(profile, quality, data_kind, path, NULL, song_length);
|
||||
if (!ctx->decode_ctx)
|
||||
{
|
||||
free(ctx);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
ctx->encode_ctx = transcode_encode_setup(profile, ctx->decode_ctx, est_size, 0, 0);
|
||||
ctx->encode_ctx = transcode_encode_setup(profile, quality, ctx->decode_ctx, est_size, 0, 0);
|
||||
if (!ctx->encode_ctx)
|
||||
{
|
||||
transcode_decode_cleanup(&ctx->decode_ctx);
|
||||
@ -1209,26 +1286,34 @@ transcode_setup(enum transcode_profile profile, enum data_kind data_kind, const
|
||||
}
|
||||
|
||||
struct decode_ctx *
|
||||
transcode_decode_setup_raw(void)
|
||||
transcode_decode_setup_raw(enum transcode_profile profile, struct media_quality *quality)
|
||||
{
|
||||
const AVCodecDescriptor *codec_desc;
|
||||
struct decode_ctx *ctx;
|
||||
AVCodec *decoder;
|
||||
int ret;
|
||||
|
||||
CHECK_NULL(L_XCODE, ctx = calloc(1, sizeof(struct decode_ctx)));
|
||||
|
||||
if (init_settings(&ctx->settings, XCODE_PCM16_NOHEADER) < 0)
|
||||
if (init_settings(&ctx->settings, profile, quality) < 0)
|
||||
{
|
||||
goto out_free_ctx;
|
||||
}
|
||||
|
||||
codec_desc = avcodec_descriptor_get(ctx->settings.audio_codec);
|
||||
if (!codec_desc)
|
||||
{
|
||||
DPRINTF(E_LOG, L_XCODE, "Invalid codec ID (%d)\n", ctx->settings.audio_codec);
|
||||
goto out_free_ctx;
|
||||
}
|
||||
|
||||
// In raw mode we won't actually need to read or decode, but we still setup
|
||||
// the decode_ctx because transcode_encode_setup() gets info about the input
|
||||
// through this structure (TODO dont' do that)
|
||||
decoder = avcodec_find_decoder(ctx->settings.audio_codec);
|
||||
if (!decoder)
|
||||
{
|
||||
DPRINTF(E_LOG, L_XCODE, "Could not find decoder for: %s\n", ctx->settings.audio_codec_name);
|
||||
DPRINTF(E_LOG, L_XCODE, "Could not find decoder for: %s\n", codec_desc->name);
|
||||
goto out_free_ctx;
|
||||
}
|
||||
|
||||
@ -1243,7 +1328,7 @@ transcode_decode_setup_raw(void)
|
||||
ret = avcodec_parameters_from_context(ctx->audio_stream.stream->codecpar, ctx->audio_stream.codec);
|
||||
if (ret < 0)
|
||||
{
|
||||
DPRINTF(E_LOG, L_XCODE, "Cannot copy stream parameters (%s): %s\n", ctx->settings.audio_codec_name, err2str(ret));
|
||||
DPRINTF(E_LOG, L_XCODE, "Cannot copy stream parameters (%s): %s\n", codec_desc->name, err2str(ret));
|
||||
goto out_free_codec;
|
||||
}
|
||||
|
||||
@ -1373,6 +1458,9 @@ transcode_encode_cleanup(struct encode_ctx **ctx)
|
||||
void
|
||||
transcode_cleanup(struct transcode_ctx **ctx)
|
||||
{
|
||||
if (!*ctx)
|
||||
return;
|
||||
|
||||
transcode_encode_cleanup(&(*ctx)->encode_ctx);
|
||||
transcode_decode_cleanup(&(*ctx)->decode_ctx);
|
||||
free(*ctx);
|
||||
@ -1383,7 +1471,7 @@ transcode_cleanup(struct transcode_ctx **ctx)
|
||||
/* Encoding, decoding and transcoding */
|
||||
|
||||
int
|
||||
transcode_decode(void **frame, struct decode_ctx *dec_ctx)
|
||||
transcode_decode(transcode_frame **frame, struct decode_ctx *dec_ctx)
|
||||
{
|
||||
struct transcode_ctx ctx;
|
||||
int ret;
|
||||
@ -1414,7 +1502,7 @@ transcode_decode(void **frame, struct decode_ctx *dec_ctx)
|
||||
|
||||
// Filters and encodes
|
||||
int
|
||||
transcode_encode(struct evbuffer *evbuf, struct encode_ctx *ctx, void *frame, int eof)
|
||||
transcode_encode(struct evbuffer *evbuf, struct encode_ctx *ctx, transcode_frame *frame, int eof)
|
||||
{
|
||||
AVFrame *f = frame;
|
||||
struct stream_ctx *s;
|
||||
@ -1489,8 +1577,8 @@ transcode(struct evbuffer *evbuf, int *icy_timer, struct transcode_ctx *ctx, int
|
||||
return processed;
|
||||
}
|
||||
|
||||
void *
|
||||
transcode_frame_new(enum transcode_profile profile, uint8_t *data, size_t size)
|
||||
transcode_frame *
|
||||
transcode_frame_new(void *data, size_t size, int nsamples, struct media_quality *quality)
|
||||
{
|
||||
AVFrame *f;
|
||||
int ret;
|
||||
@ -1502,19 +1590,28 @@ transcode_frame_new(enum transcode_profile profile, uint8_t *data, size_t size)
|
||||
return NULL;
|
||||
}
|
||||
|
||||
f->nb_samples = size / 4;
|
||||
f->format = AV_SAMPLE_FMT_S16;
|
||||
f->channel_layout = AV_CH_LAYOUT_STEREO;
|
||||
f->format = bitdepth2format(quality->bits_per_sample);
|
||||
if (f->format == AV_SAMPLE_FMT_NONE)
|
||||
{
|
||||
DPRINTF(E_LOG, L_XCODE, "transcode_frame_new() called with unsupported bps (%d)\n", quality->bits_per_sample);
|
||||
av_frame_free(&f);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
f->sample_rate = quality->sample_rate;
|
||||
f->nb_samples = nsamples;
|
||||
f->channel_layout = av_get_default_channel_layout(quality->channels);
|
||||
#ifdef HAVE_FFMPEG
|
||||
f->channels = 2;
|
||||
f->channels = quality->channels;
|
||||
#endif
|
||||
f->pts = AV_NOPTS_VALUE;
|
||||
f->sample_rate = 44100;
|
||||
|
||||
ret = avcodec_fill_audio_frame(f, 2, f->format, data, size, 0);
|
||||
// We don't align because the frame won't be given directly to the encoder
|
||||
// anyway, it will first go through the filter (which might align it...?)
|
||||
ret = avcodec_fill_audio_frame(f, 2, f->format, data, size, 1);
|
||||
if (ret < 0)
|
||||
{
|
||||
DPRINTF(E_LOG, L_XCODE, "Error filling frame with rawbuf: %s\n", err2str(ret));
|
||||
DPRINTF(E_LOG, L_XCODE, "Error filling frame with rawbuf, size %zu, samples %d (%d/%d/2): %s\n", size, nsamples, quality->sample_rate, quality->bits_per_sample, err2str(ret));
|
||||
av_frame_free(&f);
|
||||
return NULL;
|
||||
}
|
||||
@ -1523,7 +1620,7 @@ transcode_frame_new(enum transcode_profile profile, uint8_t *data, size_t size)
|
||||
}
|
||||
|
||||
void
|
||||
transcode_frame_free(void *frame)
|
||||
transcode_frame_free(transcode_frame *frame)
|
||||
{
|
||||
AVFrame *f = frame;
|
||||
|
||||
@ -1645,6 +1742,29 @@ transcode_decode_query(struct decode_ctx *ctx, const char *query)
|
||||
return -1;
|
||||
}
|
||||
|
||||
int
|
||||
transcode_encode_query(struct encode_ctx *ctx, const char *query)
|
||||
{
|
||||
if (strcmp(query, "sample_rate") == 0)
|
||||
{
|
||||
if (ctx->audio_stream.stream)
|
||||
return ctx->audio_stream.stream->codecpar->sample_rate;
|
||||
}
|
||||
else if (strcmp(query, "bits_per_sample") == 0)
|
||||
{
|
||||
if (ctx->audio_stream.stream)
|
||||
return av_get_bits_per_sample(ctx->audio_stream.stream->codecpar->codec_id);
|
||||
}
|
||||
else if (strcmp(query, "channels") == 0)
|
||||
{
|
||||
if (ctx->audio_stream.stream)
|
||||
return ctx->audio_stream.stream->codecpar->channels;
|
||||
}
|
||||
|
||||
return -1;
|
||||
}
|
||||
|
||||
|
||||
/* Metadata */
|
||||
|
||||
struct http_icy_metadata *
|
||||
|
@ -5,15 +5,24 @@
|
||||
#include <event2/buffer.h>
|
||||
#include "db.h"
|
||||
#include "http.h"
|
||||
#include "misc.h"
|
||||
|
||||
enum transcode_profile
|
||||
{
|
||||
// Transcodes the best audio stream into PCM16 (does not add wav header)
|
||||
XCODE_PCM16_NOHEADER,
|
||||
// Transcodes the best audio stream into PCM16 (with wav header)
|
||||
// Used for errors
|
||||
XCODE_UNKNOWN = 0,
|
||||
// Decodes the best audio stream into PCM16 or PCM24, no resampling (does not add wav header)
|
||||
XCODE_PCM_NATIVE,
|
||||
// Decodes/resamples the best audio stream into PCM16 (with wav header)
|
||||
XCODE_PCM16_HEADER,
|
||||
// Decodes/resamples the best audio stream into PCM16/24/32 (no wav headers)
|
||||
XCODE_PCM16,
|
||||
XCODE_PCM24,
|
||||
XCODE_PCM32,
|
||||
// Transcodes the best audio stream into MP3
|
||||
XCODE_MP3,
|
||||
// Transcodes the best audio stream into OPUS
|
||||
XCODE_OPUS,
|
||||
// Transcodes the best video stream into JPEG/PNG
|
||||
XCODE_JPEG,
|
||||
XCODE_PNG,
|
||||
@ -21,20 +30,26 @@ enum transcode_profile
|
||||
|
||||
struct decode_ctx;
|
||||
struct encode_ctx;
|
||||
struct transcode_ctx;
|
||||
struct transcode_ctx
|
||||
{
|
||||
struct decode_ctx *decode_ctx;
|
||||
struct encode_ctx *encode_ctx;
|
||||
};
|
||||
|
||||
typedef void transcode_frame;
|
||||
|
||||
// Setting up
|
||||
struct decode_ctx *
|
||||
transcode_decode_setup(enum transcode_profile profile, enum data_kind data_kind, const char *path, struct evbuffer *evbuf, uint32_t song_length);
|
||||
transcode_decode_setup(enum transcode_profile profile, struct media_quality *quality, enum data_kind data_kind, const char *path, struct evbuffer *evbuf, uint32_t song_length);
|
||||
|
||||
struct encode_ctx *
|
||||
transcode_encode_setup(enum transcode_profile profile, struct decode_ctx *src_ctx, off_t *est_size, int width, int height);
|
||||
transcode_encode_setup(enum transcode_profile profile, struct media_quality *quality, struct decode_ctx *src_ctx, off_t *est_size, int width, int height);
|
||||
|
||||
struct transcode_ctx *
|
||||
transcode_setup(enum transcode_profile profile, enum data_kind data_kind, const char *path, uint32_t song_length, off_t *est_size);
|
||||
transcode_setup(enum transcode_profile profile, struct media_quality *quality, enum data_kind data_kind, const char *path, uint32_t song_length, off_t *est_size);
|
||||
|
||||
struct decode_ctx *
|
||||
transcode_decode_setup_raw(void);
|
||||
transcode_decode_setup_raw(enum transcode_profile profile, struct media_quality *quality);
|
||||
|
||||
int
|
||||
transcode_needed(const char *user_agent, const char *client_codecs, char *file_codectype);
|
||||
@ -60,7 +75,7 @@ transcode_cleanup(struct transcode_ctx **ctx);
|
||||
* @return Positive if OK, negative if error, 0 if EOF
|
||||
*/
|
||||
int
|
||||
transcode_decode(void **frame, struct decode_ctx *ctx);
|
||||
transcode_decode(transcode_frame **frame, struct decode_ctx *ctx);
|
||||
|
||||
/* Encodes and remuxes a frame. Also resamples if needed.
|
||||
*
|
||||
@ -71,7 +86,7 @@ transcode_decode(void **frame, struct decode_ctx *ctx);
|
||||
* @return Bytes added if OK, negative if error
|
||||
*/
|
||||
int
|
||||
transcode_encode(struct evbuffer *evbuf, struct encode_ctx *ctx, void *frame, int eof);
|
||||
transcode_encode(struct evbuffer *evbuf, struct encode_ctx *ctx, transcode_frame *frame, int eof);
|
||||
|
||||
/* Demuxes, decodes, encodes and remuxes from the input.
|
||||
*
|
||||
@ -87,17 +102,19 @@ int
|
||||
transcode(struct evbuffer *evbuf, int *icy_timer, struct transcode_ctx *ctx, int want_bytes);
|
||||
|
||||
/* Converts a buffer with raw data to a frame that can be passed directly to the
|
||||
* transcode_encode() function
|
||||
* transcode_encode() function. It does not copy, so if you free the data the
|
||||
* frame will become invalid.
|
||||
*
|
||||
* @in profile Tells the function what kind of frame to create
|
||||
* @in data Buffer with raw data
|
||||
* @in size Size of buffer
|
||||
* @in nsamples Number of samples in the buffer
|
||||
* @in quality Sample rate, bits per sample and channels
|
||||
* @return Opaque pointer to frame if OK, otherwise NULL
|
||||
*/
|
||||
void *
|
||||
transcode_frame_new(enum transcode_profile profile, uint8_t *data, size_t size);
|
||||
transcode_frame *
|
||||
transcode_frame_new(void *data, size_t size, int nsamples, struct media_quality *quality);
|
||||
void
|
||||
transcode_frame_free(void *frame);
|
||||
transcode_frame_free(transcode_frame *frame);
|
||||
|
||||
/* Seek to the specified position - next transcode() will return this packet
|
||||
*
|
||||
@ -117,6 +134,16 @@ transcode_seek(struct transcode_ctx *ctx, int ms);
|
||||
int
|
||||
transcode_decode_query(struct decode_ctx *ctx, const char *query);
|
||||
|
||||
/* Query for information (e.g. sample rate) about the output being produced by
|
||||
* the transcoding
|
||||
*
|
||||
* @in ctx Encode context
|
||||
* @in query Query - see implementation for supported queries
|
||||
* @return Negative if error, otherwise query dependent
|
||||
*/
|
||||
int
|
||||
transcode_encode_query(struct encode_ctx *ctx, const char *query);
|
||||
|
||||
// Metadata
|
||||
struct http_icy_metadata *
|
||||
transcode_metadata(struct transcode_ctx *ctx, int *changed);
|
||||
|
File diff suppressed because it is too large
Load Diff
Loading…
x
Reference in New Issue
Block a user