mirror of
https://github.com/owntone/owntone-server.git
synced 2025-01-28 15:06:02 -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=]])
|
[[LIBAV=-libav]], [[LIBAV=]])
|
||||||
dnl libav/ffmpeg requires many feature checks
|
dnl libav/ffmpeg requires many feature checks
|
||||||
FORK_MODULES_CHECK([FORKED], [LIBAV],
|
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],
|
[av_init_packet], [libavcodec/avcodec.h],
|
||||||
[dnl Checks for misc libav and ffmpeg API differences
|
[dnl Checks for misc libav and ffmpeg API differences
|
||||||
AC_MSG_CHECKING([whether libav libraries are ffmpeg])
|
AC_MSG_CHECKING([whether libav libraries are ffmpeg])
|
||||||
@ -231,37 +231,7 @@ FORK_MODULES_CHECK([FORKED], [LIBAV],
|
|||||||
[libavutil/avutil.h])
|
[libavutil/avutil.h])
|
||||||
FORK_CHECK_DECLS([avformat_network_init],
|
FORK_CHECK_DECLS([avformat_network_init],
|
||||||
[libavformat/avformat.h])
|
[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 *])
|
AC_CHECK_SIZEOF([void *])
|
||||||
|
|
||||||
|
@ -217,17 +217,23 @@ audio {
|
|||||||
# If not set, the value for "card" will be used.
|
# If not set, the value for "card" will be used.
|
||||||
# mixer_device = ""
|
# mixer_device = ""
|
||||||
|
|
||||||
# Synchronization
|
# Enable or disable audio resampling to keep local audio in sync with
|
||||||
# If your local audio is out of sync with AirPlay, you can adjust this
|
# e.g. Airplay. This feature relies on accurate ALSA measurements of
|
||||||
# value. Positive values correspond to moving local audio ahead,
|
# delay, and some devices don't provide that. If that is the case you
|
||||||
# negative correspond to delaying it. The unit is samples, where is
|
# are better off disabling the feature.
|
||||||
# 44100 = 1 second. The offset must be between -44100 and 44100.
|
# sync_disable = false
|
||||||
# offset = 0
|
|
||||||
|
|
||||||
# How often to check and correct for drift between ALSA and AirPlay.
|
# Here you can adjust when local audio is started relative to other
|
||||||
# The value is an integer expressed in seconds.
|
# speakers, e.g. Airplay. Negative values correspond to moving local
|
||||||
# Clamped to the range 1..20.
|
# audio ahead, positive correspond to delaying it. The unit is
|
||||||
# adjust_period_seconds = 10
|
# 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
|
# Pipe output
|
||||||
|
@ -47,12 +47,6 @@ if COND_LIBWEBSOCKETS
|
|||||||
LIBWEBSOCKETS_SRC=websocket.c websocket.h
|
LIBWEBSOCKETS_SRC=websocket.c websocket.h
|
||||||
endif
|
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 = \
|
GPERF_FILES = \
|
||||||
daap_query.gperf \
|
daap_query.gperf \
|
||||||
rsp_query.gperf \
|
rsp_query.gperf \
|
||||||
@ -118,8 +112,8 @@ forked_daapd_SOURCES = main.c \
|
|||||||
httpd_artworkapi.c httpd_artworkapi.h \
|
httpd_artworkapi.c httpd_artworkapi.h \
|
||||||
http.c http.h \
|
http.c http.h \
|
||||||
dmap_common.c dmap_common.h \
|
dmap_common.c dmap_common.h \
|
||||||
transcode.h artwork.h \
|
transcode.c transcode.h \
|
||||||
$(FFMPEG_SRC) \
|
artwork.c artwork.h \
|
||||||
misc.c misc.h \
|
misc.c misc.h \
|
||||||
misc_json.c misc_json.h \
|
misc_json.c misc_json.h \
|
||||||
rng.c rng.h \
|
rng.c rng.h \
|
||||||
@ -131,6 +125,7 @@ forked_daapd_SOURCES = main.c \
|
|||||||
input.h input.c \
|
input.h input.c \
|
||||||
inputs/file_http.c inputs/pipe.c \
|
inputs/file_http.c inputs/pipe.c \
|
||||||
outputs.h outputs.c \
|
outputs.h outputs.c \
|
||||||
|
outputs/rtp_common.h outputs/rtp_common.c \
|
||||||
outputs/raop.c $(RAOP_VERIFICATION_SRC) \
|
outputs/raop.c $(RAOP_VERIFICATION_SRC) \
|
||||||
outputs/streaming.c outputs/dummy.c outputs/fifo.c \
|
outputs/streaming.c outputs/dummy.c outputs/fifo.c \
|
||||||
$(ALSA_SRC) $(PULSEAUDIO_SRC) $(CHROMECAST_SRC) \
|
$(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);
|
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 (!xcode_decode)
|
||||||
{
|
{
|
||||||
if (path)
|
if (path)
|
||||||
@ -462,9 +462,9 @@ artwork_get(struct evbuffer *evbuf, char *path, struct evbuffer *inbuf, int max_
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (format_ok == ART_FMT_JPEG)
|
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
|
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)
|
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("no_decode", NULL, CFGF_NONE),
|
||||||
CFG_STR_LIST("force_decode", NULL, CFGF_NONE),
|
CFG_STR_LIST("force_decode", NULL, CFGF_NONE),
|
||||||
CFG_BOOL("pipe_autostart", cfg_true, 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_BOOL("rating_updates", cfg_false, CFGF_NONE),
|
||||||
CFG_END()
|
CFG_END()
|
||||||
};
|
};
|
||||||
@ -113,8 +115,10 @@ static cfg_opt_t sec_audio[] =
|
|||||||
CFG_STR("card", "default", CFGF_NONE),
|
CFG_STR("card", "default", CFGF_NONE),
|
||||||
CFG_STR("mixer", NULL, CFGF_NONE),
|
CFG_STR("mixer", NULL, CFGF_NONE),
|
||||||
CFG_STR("mixer_device", NULL, CFGF_NONE),
|
CFG_STR("mixer_device", NULL, CFGF_NONE),
|
||||||
CFG_INT("offset", 0, CFGF_NONE),
|
CFG_BOOL("sync_disable", cfg_false, CFGF_NONE),
|
||||||
CFG_INT("adjust_period_seconds", 10, 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()
|
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" \
|
"<h1>%s</h1>\n" \
|
||||||
"</body>\n</html>\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 {
|
struct content_type_map {
|
||||||
char *ext;
|
char *ext;
|
||||||
char *ctype;
|
char *ctype;
|
||||||
@ -1029,6 +1034,7 @@ httpd_request_parse(struct evhttp_request *req, struct httpd_uri_parsed *uri_par
|
|||||||
void
|
void
|
||||||
httpd_stream_file(struct evhttp_request *req, int id)
|
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 media_file_info *mfi;
|
||||||
struct stream_ctx *st;
|
struct stream_ctx *st;
|
||||||
void (*stream_cb)(int fd, short event, void *arg);
|
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;
|
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)
|
if (!st->xcode)
|
||||||
{
|
{
|
||||||
DPRINTF(E_WARN, L_HTTPD, "Transcoding setup failed, aborting streaming\n");
|
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)
|
if (ret != 0)
|
||||||
return ret;
|
return ret;
|
||||||
|
|
||||||
ret = player_now_playing(&id);
|
ret = player_playing_now(&id);
|
||||||
if (ret != 0)
|
if (ret != 0)
|
||||||
return HTTP_NOTFOUND;
|
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);
|
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)
|
if (ret < 0)
|
||||||
{
|
{
|
||||||
DPRINTF(E_WARN, L_DACP, "Could not find an id for rating\n");
|
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;
|
goto error;
|
||||||
}
|
}
|
||||||
|
|
||||||
ret = player_now_playing(&id);
|
ret = player_playing_now(&id);
|
||||||
if (ret < 0)
|
if (ret < 0)
|
||||||
goto no_artwork;
|
goto no_artwork;
|
||||||
|
|
||||||
|
@ -44,8 +44,13 @@ extern struct event_base *evbase_httpd;
|
|||||||
// Seconds between sending silence when player is idle
|
// Seconds between sending silence when player is idle
|
||||||
// (to prevent client from hanging up)
|
// (to prevent client from hanging up)
|
||||||
#define STREAMING_SILENCE_INTERVAL 1
|
#define STREAMING_SILENCE_INTERVAL 1
|
||||||
// Buffer size for transmitting from player to httpd thread
|
// How many bytes we try to read at a time from the httpd pipe
|
||||||
#define STREAMING_RAWBUF_SIZE (STOB(AIRTUNES_V2_PACKET_SAMPLES))
|
#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
|
// Linked list of mp3 streaming requests
|
||||||
struct streaming_session {
|
struct streaming_session {
|
||||||
@ -54,23 +59,24 @@ struct streaming_session {
|
|||||||
};
|
};
|
||||||
static struct streaming_session *streaming_sessions;
|
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
|
// Interval for sending silence when playback is paused
|
||||||
static uint8_t *streaming_silence_data;
|
|
||||||
static size_t streaming_silence_size;
|
|
||||||
static struct timeval streaming_silence_tv = { STREAMING_SILENCE_INTERVAL, 0 };
|
static struct timeval streaming_silence_tv = { STREAMING_SILENCE_INTERVAL, 0 };
|
||||||
|
|
||||||
// Input buffer, output buffer and encoding ctx for transcode
|
// 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 encode_ctx *streaming_encode_ctx;
|
||||||
static struct evbuffer *streaming_encoded_data;
|
static struct evbuffer *streaming_encoded_data;
|
||||||
|
static struct media_quality streaming_quality;
|
||||||
|
|
||||||
// Used for pushing events and data from the player
|
// Used for pushing events and data from the player
|
||||||
static struct event *streamingev;
|
static struct event *streamingev;
|
||||||
|
static struct event *metaev;
|
||||||
static struct player_status streaming_player_status;
|
static struct player_status streaming_player_status;
|
||||||
static int streaming_player_changed;
|
static int streaming_player_changed;
|
||||||
static int streaming_pipe[2];
|
static int streaming_pipe[2];
|
||||||
|
static int streaming_meta[2];
|
||||||
|
|
||||||
|
|
||||||
static void
|
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");
|
DPRINTF(E_INFO, L_STREAMING, "No more clients, will stop streaming\n");
|
||||||
event_del(streamingev);
|
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
|
static void
|
||||||
streaming_send_cb(evutil_socket_t fd, short event, void *arg)
|
streaming_send_cb(evutil_socket_t fd, short event, void *arg)
|
||||||
{
|
{
|
||||||
struct streaming_session *session;
|
struct streaming_session *session;
|
||||||
struct evbuffer *evbuf;
|
struct evbuffer *evbuf;
|
||||||
void *frame;
|
uint8_t rawbuf[STREAMING_READ_SIZE];
|
||||||
uint8_t *buf;
|
uint8_t *buf;
|
||||||
int len;
|
int len;
|
||||||
int ret;
|
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)
|
// Player wrote data to the pipe (EV_READ)
|
||||||
if (event & EV_READ)
|
if (event & EV_READ)
|
||||||
{
|
{
|
||||||
ret = read(streaming_pipe[0], &streaming_rawbuf, STREAMING_RAWBUF_SIZE);
|
while (1)
|
||||||
if (ret < 0)
|
|
||||||
return;
|
|
||||||
|
|
||||||
if (!streaming_sessions)
|
|
||||||
return;
|
|
||||||
|
|
||||||
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");
|
ret = read(fd, &rawbuf, sizeof(rawbuf));
|
||||||
return;
|
if (ret <= 0)
|
||||||
}
|
break;
|
||||||
|
|
||||||
ret = transcode_encode(streaming_encoded_data, streaming_encode_ctx, frame, 0);
|
ret = encode_buffer(rawbuf, ret);
|
||||||
transcode_frame_free(frame);
|
if (ret < 0)
|
||||||
if (ret < 0)
|
return;
|
||||||
return;
|
}
|
||||||
}
|
}
|
||||||
// Event timed out, let's see what the player is doing and send silence if it is paused
|
// Event timed out, let's see what the player is doing and send silence if it is paused
|
||||||
else
|
else
|
||||||
@ -155,16 +248,18 @@ streaming_send_cb(evutil_socket_t fd, short event, void *arg)
|
|||||||
player_get_status(&streaming_player_status);
|
player_get_status(&streaming_player_status);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!streaming_sessions)
|
|
||||||
return;
|
|
||||||
|
|
||||||
if (streaming_player_status.status != PLAY_PAUSED)
|
if (streaming_player_status.status != PLAY_PAUSED)
|
||||||
return;
|
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);
|
len = evbuffer_get_length(streaming_encoded_data);
|
||||||
|
if (len == 0)
|
||||||
|
return;
|
||||||
|
|
||||||
// Send data
|
// Send data
|
||||||
evbuf = evbuffer_new();
|
evbuf = evbuffer_new();
|
||||||
@ -179,6 +274,7 @@ streaming_send_cb(evutil_socket_t fd, short event, void *arg)
|
|||||||
else
|
else
|
||||||
evhttp_send_reply_chunk(session->req, streaming_encoded_data);
|
evhttp_send_reply_chunk(session->req, streaming_encoded_data);
|
||||||
}
|
}
|
||||||
|
|
||||||
evbuffer_free(evbuf);
|
evbuffer_free(evbuf);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -191,14 +287,24 @@ player_change_cb(short event_mask)
|
|||||||
|
|
||||||
// Thread: player (also prone to race conditions, mostly during deinit)
|
// Thread: player (also prone to race conditions, mostly during deinit)
|
||||||
void
|
void
|
||||||
streaming_write(uint8_t *buf, uint64_t rtptime)
|
streaming_write(struct output_buffer *obuf)
|
||||||
{
|
{
|
||||||
int ret;
|
int ret;
|
||||||
|
|
||||||
if (!streaming_sessions)
|
if (!streaming_sessions)
|
||||||
return;
|
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 (ret < 0)
|
||||||
{
|
{
|
||||||
if (errno == EAGAIN)
|
if (errno == EAGAIN)
|
||||||
@ -219,9 +325,9 @@ streaming_request(struct evhttp_request *req, struct httpd_uri_parsed *uri_parse
|
|||||||
char *address;
|
char *address;
|
||||||
ev_uint16_t port;
|
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");
|
evhttp_send_error(req, HTTP_NOTFOUND, "Not Found");
|
||||||
return -1;
|
return -1;
|
||||||
@ -258,7 +364,10 @@ streaming_request(struct evhttp_request *req, struct httpd_uri_parsed *uri_parse
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (!streaming_sessions)
|
if (!streaming_sessions)
|
||||||
event_add(streamingev, &streaming_silence_tv);
|
{
|
||||||
|
event_add(streamingev, &streaming_silence_tv);
|
||||||
|
event_add(metaev, NULL);
|
||||||
|
}
|
||||||
|
|
||||||
session->req = req;
|
session->req = req;
|
||||||
session->next = streaming_sessions;
|
session->next = streaming_sessions;
|
||||||
@ -284,26 +393,8 @@ streaming_is_request(const char *path)
|
|||||||
int
|
int
|
||||||
streaming_init(void)
|
streaming_init(void)
|
||||||
{
|
{
|
||||||
struct decode_ctx *decode_ctx;
|
|
||||||
void *frame;
|
|
||||||
int remaining;
|
|
||||||
int ret;
|
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
|
// Non-blocking because otherwise httpd and player thread may deadlock
|
||||||
#ifdef HAVE_PIPE2
|
#ifdef HAVE_PIPE2
|
||||||
ret = pipe2(streaming_pipe, O_CLOEXEC | O_NONBLOCK);
|
ret = pipe2(streaming_pipe, O_CLOEXEC | O_NONBLOCK);
|
||||||
@ -318,7 +409,23 @@ streaming_init(void)
|
|||||||
if (ret < 0)
|
if (ret < 0)
|
||||||
{
|
{
|
||||||
DPRINTF(E_FATAL, L_STREAMING, "Could not create pipe: %s\n", strerror(errno));
|
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
|
// 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)
|
if (ret < 0)
|
||||||
{
|
{
|
||||||
DPRINTF(E_FATAL, L_STREAMING, "Could not add listener\n");
|
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
|
// Initialize buffer for encoded mp3 audio and event for pipe reading
|
||||||
streaming_encoded_data = evbuffer_new();
|
CHECK_NULL(L_STREAMING, 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;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Encode some silence which will be used for playback pause and put in a permanent buffer
|
CHECK_NULL(L_STREAMING, streamingev = event_new(evbase_httpd, streaming_pipe[0], EV_TIMEOUT | EV_READ | EV_PERSIST, streaming_send_cb, NULL));
|
||||||
remaining = STREAMING_SILENCE_INTERVAL * STOB(44100);
|
CHECK_NULL(L_STREAMING, metaev = event_new(evbase_httpd, streaming_meta[0], EV_READ | EV_PERSIST, streaming_meta_cb, NULL));
|
||||||
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;
|
|
||||||
|
|
||||||
return 0;
|
return 0;
|
||||||
|
|
||||||
silence_fail:
|
error:
|
||||||
event_free(streamingev);
|
|
||||||
evbuffer_free(streaming_encoded_data);
|
|
||||||
event_fail:
|
|
||||||
listener_remove(player_change_cb);
|
|
||||||
listener_fail:
|
|
||||||
close(streaming_pipe[0]);
|
close(streaming_pipe[0]);
|
||||||
close(streaming_pipe[1]);
|
close(streaming_pipe[1]);
|
||||||
pipe_fail:
|
close(streaming_meta[0]);
|
||||||
transcode_encode_cleanup(&streaming_encode_ctx);
|
close(streaming_meta[1]);
|
||||||
|
|
||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
@ -407,9 +459,6 @@ streaming_deinit(void)
|
|||||||
struct streaming_session *session;
|
struct streaming_session *session;
|
||||||
struct streaming_session *next;
|
struct streaming_session *next;
|
||||||
|
|
||||||
if (!streaming_initialized)
|
|
||||||
return;
|
|
||||||
|
|
||||||
session = streaming_sessions;
|
session = streaming_sessions;
|
||||||
streaming_sessions = NULL; // Stops writing and sending
|
streaming_sessions = NULL; // Stops writing and sending
|
||||||
|
|
||||||
@ -428,8 +477,9 @@ streaming_deinit(void)
|
|||||||
|
|
||||||
close(streaming_pipe[0]);
|
close(streaming_pipe[0]);
|
||||||
close(streaming_pipe[1]);
|
close(streaming_pipe[1]);
|
||||||
|
close(streaming_meta[0]);
|
||||||
|
close(streaming_meta[1]);
|
||||||
|
|
||||||
transcode_encode_cleanup(&streaming_encode_ctx);
|
transcode_encode_cleanup(&streaming_encode_ctx);
|
||||||
evbuffer_free(streaming_encoded_data);
|
evbuffer_free(streaming_encoded_data);
|
||||||
free(streaming_silence_data);
|
|
||||||
}
|
}
|
||||||
|
@ -3,6 +3,7 @@
|
|||||||
#define __HTTPD_STREAMING_H__
|
#define __HTTPD_STREAMING_H__
|
||||||
|
|
||||||
#include "httpd.h"
|
#include "httpd.h"
|
||||||
|
#include "outputs.h"
|
||||||
|
|
||||||
/* httpd_streaming takes care of incoming requests to /stream.mp3
|
/* httpd_streaming takes care of incoming requests to /stream.mp3
|
||||||
* It will receive decoded audio from the player, and encode it, and
|
* It will receive decoded audio from the player, and encode it, and
|
||||||
@ -11,7 +12,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
void
|
void
|
||||||
streaming_write(uint8_t *buf, uint64_t rtptime);
|
streaming_write(struct output_buffer *obuf);
|
||||||
|
|
||||||
int
|
int
|
||||||
streaming_request(struct evhttp_request *req, struct httpd_uri_parsed *uri_parsed);
|
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>
|
# include <config.h>
|
||||||
#endif
|
#endif
|
||||||
#include <event2/buffer.h>
|
#include <event2/buffer.h>
|
||||||
#include "transcode.h"
|
#include "db.h"
|
||||||
|
#include "misc.h"
|
||||||
|
|
||||||
// Must be in sync with inputs[] in input.c
|
// Must be in sync with inputs[] in input.c
|
||||||
enum input_types
|
enum input_types
|
||||||
@ -21,83 +22,74 @@ enum input_types
|
|||||||
|
|
||||||
enum input_flags
|
enum input_flags
|
||||||
{
|
{
|
||||||
// Write to input buffer must not block
|
// Flags that input is closing current source
|
||||||
INPUT_FLAG_NONBLOCK = (1 << 0),
|
INPUT_FLAG_START_NEXT = (1 << 0),
|
||||||
// Flags end of file
|
// Flags end of file
|
||||||
INPUT_FLAG_EOF = (1 << 1),
|
INPUT_FLAG_EOF = (1 << 1),
|
||||||
// Flags error reading file
|
// Flags error reading file
|
||||||
INPUT_FLAG_ERROR = (1 << 2),
|
INPUT_FLAG_ERROR = (1 << 2),
|
||||||
// Flags possible new stream metadata
|
// 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 */
|
// Type of input
|
||||||
uint32_t id;
|
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;
|
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;
|
uint32_t len_ms;
|
||||||
|
|
||||||
enum data_kind data_kind;
|
enum data_kind data_kind;
|
||||||
enum media_kind media_kind;
|
enum media_kind media_kind;
|
||||||
char *path;
|
char *path;
|
||||||
|
|
||||||
/* Start time of the media item as rtp-time
|
// Flags that the input has been opened (i.e. needs to be closed)
|
||||||
The stream-start is the rtp-time the media item did or would have
|
bool open;
|
||||||
started playing (after seek or pause), therefor the elapsed time of the
|
|
||||||
media item is always:
|
|
||||||
elapsed time = current rtptime - stream-start */
|
|
||||||
uint64_t stream_start;
|
|
||||||
|
|
||||||
/* Output start time of the media item as rtp-time
|
// The below is private data for the input backend. It is optional for the
|
||||||
The output start time is the rtp-time of the first audio packet send
|
// backend to use, so nothing in the input or player should depend on it!
|
||||||
to the audio outputs.
|
//
|
||||||
It differs from stream-start especially after a seek, where the first audio
|
// Opaque pointer to data that the input backend sets up when start() is
|
||||||
packet has the next rtp-time as output start and stream start becomes the
|
// called, and that is cleaned up by the backend when stop() is called
|
||||||
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()
|
|
||||||
*/
|
|
||||||
void *input_ctx;
|
void *input_ctx;
|
||||||
|
// Private evbuf. Alloc'ed by backend at start() and free'd at stop()
|
||||||
/* Input has completed setup of the source
|
struct evbuffer *evbuf;
|
||||||
*/
|
// Private source quality storage
|
||||||
int setup_done;
|
struct media_quality quality;
|
||||||
|
|
||||||
struct player_source *play_next;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
typedef int (*input_cb)(void);
|
typedef int (*input_cb)(void);
|
||||||
|
|
||||||
struct input_metadata
|
struct input_metadata
|
||||||
{
|
{
|
||||||
|
// queue_item id
|
||||||
uint32_t 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;
|
// Sets new song length (input will also update queue_item)
|
||||||
uint64_t offset;
|
uint32_t len_ms;
|
||||||
|
|
||||||
// The player will update queue_item with the below
|
|
||||||
uint32_t song_length;
|
|
||||||
|
|
||||||
|
// Input can update queue_item with the below
|
||||||
char *artist;
|
char *artist;
|
||||||
char *title;
|
char *title;
|
||||||
char *album;
|
char *album;
|
||||||
char *genre;
|
char *genre;
|
||||||
char *artwork_url;
|
char *artwork_url;
|
||||||
|
|
||||||
|
// Indicates whether we are starting playback. Just passed on to output.
|
||||||
|
int startup;
|
||||||
};
|
};
|
||||||
|
|
||||||
struct input_definition
|
struct input_definition
|
||||||
@ -112,65 +104,75 @@ struct input_definition
|
|||||||
char disabled;
|
char disabled;
|
||||||
|
|
||||||
// Prepare a playback session
|
// Prepare a playback session
|
||||||
int (*setup)(struct player_source *ps);
|
int (*setup)(struct input_source *source);
|
||||||
|
|
||||||
// Starts playback loop (must be defined)
|
// One iteration of the playback loop (= a read operation from source)
|
||||||
int (*start)(struct player_source *ps);
|
int (*play)(struct input_source *source);
|
||||||
|
|
||||||
// Cleans up when playback loop has ended
|
// Cleans up (only required when stopping source before it ends itself)
|
||||||
int (*stop)(struct player_source *ps);
|
int (*stop)(struct input_source *source);
|
||||||
|
|
||||||
// Changes the playback position
|
// Changes the playback position
|
||||||
int (*seek)(struct player_source *ps, int seek_ms);
|
int (*seek)(struct input_source *source, int seek_ms);
|
||||||
|
|
||||||
// Return metadata
|
// 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
|
// Initialization function called during startup
|
||||||
int (*init)(void);
|
int (*init)(void);
|
||||||
|
|
||||||
// Deinitialization function called at shutdown
|
// Deinitialization function called at shutdown
|
||||||
void (*deinit)(void);
|
void (*deinit)(void);
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|
||||||
/*
|
|
||||||
* Input modules should use this to test if playback should end
|
/* ---------------------- Interface towards input backends ------------------ */
|
||||||
*/
|
/* Thread: input and spotify */
|
||||||
int input_loop_break;
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Transfer stream data to the player's input buffer. The input evbuf will be
|
* Transfer stream data to the player's input buffer. Data must be PCM-LE
|
||||||
* drained on succesful write. This is to avoid copying memory. If the player's
|
* samples. The input evbuf will be drained on succesful write. This is to avoid
|
||||||
* input buffer is full the function will block until the write can be made
|
* copying memory.
|
||||||
* (unless INPUT_FILE_NONBLOCK is set).
|
|
||||||
*
|
*
|
||||||
* @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_*
|
* @in flags One or more INPUT_FLAG_*
|
||||||
* @return 0 on success, EAGAIN if buffer was full (and _NONBLOCK is set),
|
* @return 0 on success, EAGAIN if buffer was full, -1 on error
|
||||||
* -1 on error
|
|
||||||
*/
|
*/
|
||||||
int
|
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()
|
* Input modules can use this to wait for the input_buffer to be ready for
|
||||||
* would have done)
|
* 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);
|
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
|
* 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.
|
* buffer. Should only be called by the player thread. Will not block.
|
||||||
*
|
*
|
||||||
* @in data Output buffer
|
* @in data Output buffer
|
||||||
* @in size How much data to move to the 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
|
* @return Number of bytes moved, -1 on error
|
||||||
*/
|
*/
|
||||||
int
|
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
|
* 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);
|
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
|
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
|
* Same as input_seek(), just non-blocking and does not offer seek.
|
||||||
* 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.
|
* @in item_id Queue item id to start playing
|
||||||
*/
|
*/
|
||||||
int
|
void
|
||||||
input_start(struct player_source *ps);
|
input_start(uint32_t item_id);
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Pauses playback of the given player source (stops playback loop) and flushes
|
* Stops the input and clears everything. Flushes the input buffer.
|
||||||
* the input buffer
|
|
||||||
*/
|
*/
|
||||||
int
|
void
|
||||||
input_pause(struct player_source *ps);
|
input_stop(void);
|
||||||
|
|
||||||
/*
|
|
||||||
* Stops playback loop (if running), flushes input buffer and cleans up the
|
|
||||||
* player source
|
|
||||||
*/
|
|
||||||
int
|
|
||||||
input_stop(struct player_source *ps);
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Seeks playback position to seek_ms. Returns actual seek position, 0 on
|
|
||||||
* unseekable, -1 on error. May block.
|
|
||||||
*/
|
|
||||||
int
|
|
||||||
input_seek(struct player_source *ps, int seek_ms);
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Flush input buffer. Output flags will be the same as input_read().
|
* 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
|
void
|
||||||
input_flush(short *flags);
|
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
|
* Free the entire struct
|
||||||
*/
|
*/
|
||||||
|
@ -26,93 +26,107 @@
|
|||||||
#include "transcode.h"
|
#include "transcode.h"
|
||||||
#include "http.h"
|
#include "http.h"
|
||||||
#include "misc.h"
|
#include "misc.h"
|
||||||
|
#include "logger.h"
|
||||||
#include "input.h"
|
#include "input.h"
|
||||||
|
|
||||||
static int
|
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);
|
struct transcode_ctx *ctx;
|
||||||
if (!ps->input_ctx)
|
|
||||||
|
ctx = transcode_setup(XCODE_PCM_NATIVE, NULL, source->data_kind, source->path, source->len_ms, NULL);
|
||||||
|
if (!ctx)
|
||||||
return -1;
|
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;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
static int
|
static int
|
||||||
setup_http(struct player_source *ps)
|
setup_http(struct input_source *source)
|
||||||
{
|
{
|
||||||
char *url;
|
char *url;
|
||||||
|
|
||||||
if (http_stream_setup(&url, ps->path) < 0)
|
if (http_stream_setup(&url, source->path) < 0)
|
||||||
return -1;
|
return -1;
|
||||||
|
|
||||||
free(ps->path);
|
free(source->path);
|
||||||
ps->path = url;
|
source->path = url;
|
||||||
|
|
||||||
return setup(ps);
|
return setup(source);
|
||||||
}
|
}
|
||||||
|
|
||||||
static int
|
static int
|
||||||
start(struct player_source *ps)
|
stop(struct input_source *source)
|
||||||
{
|
{
|
||||||
struct evbuffer *evbuf;
|
struct transcode_ctx *ctx = source->input_ctx;
|
||||||
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;
|
|
||||||
|
|
||||||
transcode_cleanup(&ctx);
|
transcode_cleanup(&ctx);
|
||||||
|
|
||||||
ps->input_ctx = NULL;
|
if (source->evbuf)
|
||||||
ps->setup_done = 0;
|
evbuffer_free(source->evbuf);
|
||||||
|
|
||||||
|
source->input_ctx = NULL;
|
||||||
|
source->evbuf = NULL;
|
||||||
|
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
static int
|
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
|
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;
|
struct http_icy_metadata *m;
|
||||||
int changed;
|
int changed;
|
||||||
|
|
||||||
m = transcode_metadata(ps->input_ctx, &changed);
|
m = transcode_metadata(source->input_ctx, &changed);
|
||||||
if (!m)
|
if (!m)
|
||||||
return -1;
|
return -1;
|
||||||
|
|
||||||
@ -140,7 +154,7 @@ struct input_definition input_file =
|
|||||||
.type = INPUT_TYPE_FILE,
|
.type = INPUT_TYPE_FILE,
|
||||||
.disabled = 0,
|
.disabled = 0,
|
||||||
.setup = setup,
|
.setup = setup,
|
||||||
.start = start,
|
.play = play,
|
||||||
.stop = stop,
|
.stop = stop,
|
||||||
.seek = seek,
|
.seek = seek,
|
||||||
};
|
};
|
||||||
@ -151,7 +165,7 @@ struct input_definition input_http =
|
|||||||
.type = INPUT_TYPE_HTTP,
|
.type = INPUT_TYPE_HTTP,
|
||||||
.disabled = 0,
|
.disabled = 0,
|
||||||
.setup = setup_http,
|
.setup = setup_http,
|
||||||
.start = start,
|
.play = play,
|
||||||
.stop = stop,
|
.stop = stop,
|
||||||
.metadata_get = metadata_get_http,
|
.metadata_get = metadata_get_http,
|
||||||
};
|
};
|
||||||
|
@ -103,6 +103,9 @@ static pthread_t tid_pipe;
|
|||||||
static struct event_base *evbase_pipe;
|
static struct event_base *evbase_pipe;
|
||||||
static struct commands_base *cmdbase;
|
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
|
// From config - should we watch library pipes for data or only start on request
|
||||||
static int pipe_autostart;
|
static int pipe_autostart;
|
||||||
// The mfi id of the pipe autostarted by the pipe thread
|
// 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
|
// True if there is new metadata to push to the player
|
||||||
static bool pipe_metadata_is_new;
|
static bool pipe_metadata_is_new;
|
||||||
|
|
||||||
/* -------------------------------- HELPERS ------------------------------- */
|
/* -------------------------------- HELPERS --------------------------------- */
|
||||||
|
|
||||||
static struct pipe *
|
static struct pipe *
|
||||||
pipe_create(const char *path, int id, enum pipetype type, event_callback_fn cb)
|
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)
|
if (!start || !pos || !end)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
m->rtptime = start; // Not actually used - we have our own rtptime
|
if (pos > start)
|
||||||
m->offset = (pos > start) ? (pos - start) : 0;
|
m->pos_ms = (pos - start) * 1000 / pipe_sample_rate;
|
||||||
m->song_length = (end - start) * 10 / 441; // Convert to ms based on 44100
|
if (end > start)
|
||||||
|
m->len_ms = (end - start) * 1000 / pipe_sample_rate;
|
||||||
}
|
}
|
||||||
|
|
||||||
static void
|
static void
|
||||||
@ -497,8 +501,8 @@ pipe_metadata_parse(struct input_metadata *m, struct evbuffer *evbuf)
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/* ----------------------------- PIPE WATCHING ---------------------------- */
|
/* ------------------------------ PIPE WATCHING ----------------------------- */
|
||||||
/* Thread: pipe */
|
/* Thread: pipe */
|
||||||
|
|
||||||
// Some data arrived on a pipe we watch - let's autostart playback
|
// Some data arrived on a pipe we watch - let's autostart playback
|
||||||
static void
|
static void
|
||||||
@ -614,8 +618,8 @@ pipe_thread_run(void *arg)
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/* -------------------------- METADATA PIPE HANDLING ---------------------- */
|
/* --------------------------- METADATA PIPE HANDLING ----------------------- */
|
||||||
/* Thread: worker */
|
/* Thread: worker */
|
||||||
|
|
||||||
static void
|
static void
|
||||||
pipe_metadata_watch_del(void *arg)
|
pipe_metadata_watch_del(void *arg)
|
||||||
@ -703,8 +707,8 @@ pipe_metadata_watch_add(void *arg)
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/* ---------------------- PIPE WATCH THREAD START/STOP -------------------- */
|
/* ----------------------- PIPE WATCH THREAD START/STOP --------------------- */
|
||||||
/* Thread: filescanner */
|
/* Thread: filescanner */
|
||||||
|
|
||||||
static void
|
static void
|
||||||
pipe_thread_start(void)
|
pipe_thread_start(void)
|
||||||
@ -799,87 +803,47 @@ pipe_listener_cb(short event_mask)
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/* -------------------------- PIPE INPUT INTERFACE ------------------------ */
|
/* --------------------------- PIPE INPUT INTERFACE ------------------------- */
|
||||||
/* Thread: player/input */
|
/* Thread: input */
|
||||||
|
|
||||||
static int
|
static int
|
||||||
setup(struct player_source *ps)
|
setup(struct input_source *source)
|
||||||
{
|
{
|
||||||
struct pipe *pipe;
|
struct pipe *pipe;
|
||||||
int fd;
|
int fd;
|
||||||
|
|
||||||
fd = pipe_open(ps->path, 0);
|
fd = pipe_open(source->path, 0);
|
||||||
if (fd < 0)
|
if (fd < 0)
|
||||||
return -1;
|
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->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;
|
source->input_ctx = pipe;
|
||||||
ps->setup_done = 1;
|
|
||||||
|
source->quality.sample_rate = pipe_sample_rate;
|
||||||
|
source->quality.bits_per_sample = pipe_bits_per_sample;
|
||||||
|
source->quality.channels = 2;
|
||||||
|
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
static int
|
static int
|
||||||
start(struct player_source *ps)
|
stop(struct input_source *source)
|
||||||
{
|
{
|
||||||
struct pipe *pipe = ps->input_ctx;
|
struct pipe *pipe = source->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;
|
|
||||||
union pipe_arg *cmdarg;
|
union pipe_arg *cmdarg;
|
||||||
|
|
||||||
DPRINTF(E_DBG, L_PLAYER, "Stopping pipe\n");
|
DPRINTF(E_DBG, L_PLAYER, "Stopping pipe\n");
|
||||||
|
|
||||||
|
if (source->evbuf)
|
||||||
|
evbuffer_free(source->evbuf);
|
||||||
|
|
||||||
pipe_close(pipe->fd);
|
pipe_close(pipe->fd);
|
||||||
|
|
||||||
// Reset the pipe and start watching it again for new data. Must be async or
|
// 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);
|
pipe_free(pipe);
|
||||||
|
|
||||||
ps->input_ctx = NULL;
|
source->input_ctx = NULL;
|
||||||
ps->setup_done = 0;
|
source->evbuf = NULL;
|
||||||
|
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
static int
|
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);
|
pthread_mutex_lock(&pipe_metadata_lock);
|
||||||
|
|
||||||
if (pipe_metadata_parsed.artist)
|
*metadata = pipe_metadata_parsed;
|
||||||
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);
|
|
||||||
|
|
||||||
pthread_mutex_unlock(&pipe_metadata_lock);
|
pthread_mutex_unlock(&pipe_metadata_lock);
|
||||||
|
|
||||||
@ -945,6 +928,20 @@ init(void)
|
|||||||
CHECK_ERR(L_PLAYER, listener_add(pipe_listener_cb, LISTENER_DATABASE));
|
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;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -966,7 +963,7 @@ struct input_definition input_pipe =
|
|||||||
.type = INPUT_TYPE_PIPE,
|
.type = INPUT_TYPE_PIPE,
|
||||||
.disabled = 0,
|
.disabled = 0,
|
||||||
.setup = setup,
|
.setup = setup,
|
||||||
.start = start,
|
.play = play,
|
||||||
.stop = stop,
|
.stop = stop,
|
||||||
.metadata_get = metadata_get,
|
.metadata_get = metadata_get,
|
||||||
.init = init,
|
.init = init,
|
||||||
|
@ -31,12 +31,12 @@
|
|||||||
#define SPOTIFY_SETUP_RETRY_WAIT 500000
|
#define SPOTIFY_SETUP_RETRY_WAIT 500000
|
||||||
|
|
||||||
static int
|
static int
|
||||||
setup(struct player_source *ps)
|
setup(struct input_source *source)
|
||||||
{
|
{
|
||||||
int i = 0;
|
int i = 0;
|
||||||
int ret;
|
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)
|
if (i >= SPOTIFY_SETUP_RETRIES)
|
||||||
break;
|
break;
|
||||||
@ -49,32 +49,15 @@ setup(struct player_source *ps)
|
|||||||
if (ret < 0)
|
if (ret < 0)
|
||||||
return -1;
|
return -1;
|
||||||
|
|
||||||
ps->setup_done = 1;
|
ret = spotify_playback_play();
|
||||||
|
if (ret < 0)
|
||||||
|
return -1;
|
||||||
|
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
static int
|
static int
|
||||||
start(struct player_source *ps)
|
stop(struct input_source *source)
|
||||||
{
|
|
||||||
int ret;
|
|
||||||
|
|
||||||
ret = spotify_playback_play();
|
|
||||||
if (ret < 0)
|
|
||||||
return -1;
|
|
||||||
|
|
||||||
while (!input_loop_break)
|
|
||||||
{
|
|
||||||
input_wait();
|
|
||||||
}
|
|
||||||
|
|
||||||
ret = spotify_playback_pause();
|
|
||||||
|
|
||||||
return ret;
|
|
||||||
}
|
|
||||||
|
|
||||||
static int
|
|
||||||
stop(struct player_source *ps)
|
|
||||||
{
|
{
|
||||||
int ret;
|
int ret;
|
||||||
|
|
||||||
@ -82,13 +65,11 @@ stop(struct player_source *ps)
|
|||||||
if (ret < 0)
|
if (ret < 0)
|
||||||
return -1;
|
return -1;
|
||||||
|
|
||||||
ps->setup_done = 0;
|
|
||||||
|
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
static int
|
static int
|
||||||
seek(struct player_source *ps, int seek_ms)
|
seek(struct input_source *source, int seek_ms)
|
||||||
{
|
{
|
||||||
int ret;
|
int ret;
|
||||||
|
|
||||||
@ -105,7 +86,6 @@ struct input_definition input_spotify =
|
|||||||
.type = INPUT_TYPE_SPOTIFY,
|
.type = INPUT_TYPE_SPOTIFY,
|
||||||
.disabled = 0,
|
.disabled = 0,
|
||||||
.setup = setup,
|
.setup = setup,
|
||||||
.start = start,
|
|
||||||
.stop = stop,
|
.stop = stop,
|
||||||
.seek = seek,
|
.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++)
|
for (i = 0; i < ctx->nb_streams; i++)
|
||||||
{
|
{
|
||||||
#if HAVE_DECL_AVCODEC_PARAMETERS_FROM_CONTEXT
|
|
||||||
codec_type = ctx->streams[i]->codecpar->codec_type;
|
codec_type = ctx->streams[i]->codecpar->codec_type;
|
||||||
codec_id = ctx->streams[i]->codecpar->codec_id;
|
codec_id = ctx->streams[i]->codecpar->codec_id;
|
||||||
sample_rate = ctx->streams[i]->codecpar->sample_rate;
|
sample_rate = ctx->streams[i]->codecpar->sample_rate;
|
||||||
sample_fmt = ctx->streams[i]->codecpar->format;
|
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)
|
switch (codec_type)
|
||||||
{
|
{
|
||||||
case AVMEDIA_TYPE_VIDEO:
|
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)
|
if (ctx->streams[i]->disposition & AV_DISPOSITION_ATTACHED_PIC)
|
||||||
{
|
{
|
||||||
DPRINTF(E_DBG, L_SCAN, "Found embedded artwork (stream %d)\n", i);
|
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;
|
break;
|
||||||
}
|
}
|
||||||
#endif
|
|
||||||
// We treat these as audio no matter what
|
// We treat these as audio no matter what
|
||||||
if (mfi->compilation || (mfi->media_kind & (MEDIA_KIND_PODCAST | MEDIA_KIND_AUDIOBOOK)))
|
if (mfi->compilation || (mfi->media_kind & (MEDIA_KIND_PODCAST | MEDIA_KIND_AUDIOBOOK)))
|
||||||
break;
|
break;
|
||||||
|
120
src/misc.c
120
src/misc.c
@ -1040,6 +1040,46 @@ murmur_hash64(const void *key, int len, uint32_t seed)
|
|||||||
#endif
|
#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
|
bool
|
||||||
peer_address_is_trusted(const char *addr)
|
peer_address_is_trusted(const char *addr)
|
||||||
{
|
{
|
||||||
@ -1077,6 +1117,86 @@ peer_address_is_trusted(const char *addr)
|
|||||||
return false;
|
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
|
int
|
||||||
clock_gettime_with_res(clockid_t clock_id, struct timespec *tp, struct timespec *res)
|
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>
|
#include <pthread.h>
|
||||||
|
|
||||||
/* Samples to bytes, bytes to samples */
|
/* Samples to bytes, bytes to samples */
|
||||||
#define STOB(s) ((s) * 4)
|
#define STOB(s, bits, c) ((s) * (c) * (bits) / 8)
|
||||||
#define BTOS(b) ((b) / 4)
|
#define BTOS(b, bits, c) ((b) / ((c) * (bits) / 8))
|
||||||
|
|
||||||
#define ARRAY_SIZE(x) ((unsigned int)(sizeof(x) / sizeof((x)[0])))
|
#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 {
|
struct onekeyval {
|
||||||
char *name;
|
char *name;
|
||||||
char *value;
|
char *value;
|
||||||
@ -30,6 +46,15 @@ struct keyval {
|
|||||||
struct onekeyval *tail;
|
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 **
|
char **
|
||||||
buildopts_get(void);
|
buildopts_get(void);
|
||||||
@ -114,10 +139,28 @@ b64_encode(const uint8_t *in, size_t len);
|
|||||||
uint64_t
|
uint64_t
|
||||||
murmur_hash64(const void *key, int len, uint32_t seed);
|
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
|
// Checks if the address is in a network that is configured as trusted
|
||||||
bool
|
bool
|
||||||
peer_address_is_trusted(const char *addr);
|
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
|
#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__
|
#ifndef __OUTPUTS_H__
|
||||||
#define __OUTPUTS_H__
|
#define __OUTPUTS_H__
|
||||||
|
|
||||||
|
#include <stdbool.h>
|
||||||
#include <time.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,
|
/* 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
|
* 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
|
* 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
|
* callback from the output once the command has been executed. Commands marked
|
||||||
* with * may make multiple callbacks if multiple sessions are affected.
|
* with * may make multiple callbacks if multiple sessions are affected.
|
||||||
* (TODO should callbacks always be deferred?)
|
|
||||||
*
|
*
|
||||||
* PLAYER OUTPUT PLAYER CB
|
* PLAYER OUTPUT PLAYER CB
|
||||||
* speaker_activate -> device_start -> device_activate_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
|
// Must be in sync with outputs[] in outputs.c
|
||||||
enum output_types
|
enum output_types
|
||||||
{
|
{
|
||||||
@ -113,35 +138,59 @@ struct output_device
|
|||||||
int volume;
|
int volume;
|
||||||
int relvol;
|
int relvol;
|
||||||
|
|
||||||
|
// Quality of audio output
|
||||||
|
struct media_quality quality;
|
||||||
|
|
||||||
// Address
|
// Address
|
||||||
char *v4_address;
|
char *v4_address;
|
||||||
char *v6_address;
|
char *v6_address;
|
||||||
short v4_port;
|
short v4_port;
|
||||||
short v6_port;
|
short v6_port;
|
||||||
|
|
||||||
|
struct event *stop_timer;
|
||||||
|
|
||||||
// Opaque pointers to device and session data
|
// Opaque pointers to device and session data
|
||||||
void *extra_device_info;
|
void *extra_device_info;
|
||||||
struct output_session *session;
|
void *session;
|
||||||
|
|
||||||
struct output_device *next;
|
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
|
struct output_metadata
|
||||||
{
|
{
|
||||||
enum output_types type;
|
enum output_types type;
|
||||||
void *metadata;
|
uint32_t item_id;
|
||||||
struct output_metadata *next;
|
|
||||||
|
// 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
|
struct output_definition
|
||||||
{
|
{
|
||||||
@ -166,94 +215,137 @@ struct output_definition
|
|||||||
void (*deinit)(void);
|
void (*deinit)(void);
|
||||||
|
|
||||||
// Prepare a playback session on device and call back
|
// 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
|
// Close a session prepared by device_start and call back
|
||||||
void (*device_stop)(struct output_session *session);
|
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
|
// Test the connection to a device and call back
|
||||||
int (*device_probe)(struct output_device *device, output_status_cb cb);
|
int (*device_probe)(struct output_device *device, int callback_id);
|
||||||
|
|
||||||
// Free the private device data
|
|
||||||
void (*device_free_extra)(struct output_device *device);
|
|
||||||
|
|
||||||
// Set the volume and call back
|
// 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
|
// Convert device internal representation of volume to our pct scale
|
||||||
int (*device_volume_to_pct)(struct output_device *device, const char *volume);
|
int (*device_volume_to_pct)(struct output_device *device, const char *volume);
|
||||||
|
|
||||||
// Start/stop playback on devices that were started
|
// Request a change of quality from the device
|
||||||
void (*playback_start)(uint64_t next_pkt, struct timespec *ts);
|
int (*device_quality_set)(struct output_device *device, struct media_quality *quality, int callback_id);
|
||||||
void (*playback_stop)(void);
|
|
||||||
|
// 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
|
// Write stream data to the output devices
|
||||||
void (*write)(uint8_t *buf, uint64_t rtptime);
|
void (*write)(struct output_buffer *buffer);
|
||||||
|
|
||||||
// Flush all sessions, the return must be number of sessions pending the flush
|
|
||||||
int (*flush)(output_status_cb cb, uint64_t rtptime);
|
|
||||||
|
|
||||||
// Authorize an output with a pin-code (probably coming from the filescanner)
|
// Authorize an output with a pin-code (probably coming from the filescanner)
|
||||||
void (*authorize)(const char *pin);
|
void (*authorize)(const char *pin);
|
||||||
|
|
||||||
// Change the call back associated with a session
|
// Called from worker thread for async preparation of metadata (e.g. getting
|
||||||
void (*status_cb)(struct output_session *session, output_status_cb cb);
|
// 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
|
// Send metadata to outputs. Ownership of *metadata is transferred.
|
||||||
void *(*metadata_prepare)(int id);
|
void (*metadata_send)(struct output_metadata *metadata);
|
||||||
void (*metadata_send)(void *metadata, uint64_t rtptime, uint64_t offset, int startup);
|
|
||||||
|
// Output will cleanup all metadata (so basically like flush but for metadata)
|
||||||
void (*metadata_purge)(void);
|
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
|
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
|
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
|
int
|
||||||
outputs_device_probe(struct output_device *device, output_status_cb cb);
|
outputs_device_probe(struct output_device *device, output_status_cb cb);
|
||||||
|
|
||||||
void
|
|
||||||
outputs_device_free(struct output_device *device);
|
|
||||||
|
|
||||||
int
|
int
|
||||||
outputs_device_volume_set(struct output_device *device, output_status_cb cb);
|
outputs_device_volume_set(struct output_device *device, output_status_cb cb);
|
||||||
|
|
||||||
int
|
int
|
||||||
outputs_device_volume_to_pct(struct output_device *device, const char *value);
|
outputs_device_volume_to_pct(struct output_device *device, const char *value);
|
||||||
|
|
||||||
void
|
int
|
||||||
outputs_playback_start(uint64_t next_pkt, struct timespec *ts);
|
outputs_device_quality_set(struct output_device *device, struct media_quality *quality, output_status_cb cb);
|
||||||
|
|
||||||
void
|
void
|
||||||
outputs_playback_stop(void);
|
outputs_device_cb_set(struct output_device *device, output_status_cb cb);
|
||||||
|
|
||||||
void
|
void
|
||||||
outputs_write(uint8_t *buf, uint64_t rtptime);
|
outputs_device_free(struct output_device *device);
|
||||||
|
|
||||||
int
|
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
|
void
|
||||||
outputs_status_cb(struct output_session *session, output_status_cb cb);
|
outputs_write(void *buf, size_t bufsize, int nsamples, struct media_quality *quality, struct timespec *pts);
|
||||||
|
|
||||||
struct output_metadata *
|
|
||||||
outputs_metadata_prepare(int id);
|
|
||||||
|
|
||||||
void
|
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
|
void
|
||||||
outputs_metadata_purge(void);
|
outputs_metadata_purge(void);
|
||||||
|
|
||||||
void
|
|
||||||
outputs_metadata_prune(uint64_t rtptime);
|
|
||||||
|
|
||||||
void
|
|
||||||
outputs_metadata_free(struct output_metadata *omd);
|
|
||||||
|
|
||||||
void
|
void
|
||||||
outputs_authorize(enum output_types type, const char *pin);
|
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 <stdint.h>
|
||||||
#include <inttypes.h>
|
#include <inttypes.h>
|
||||||
|
|
||||||
#include <event2/event.h>
|
#include "misc.h"
|
||||||
|
|
||||||
#include "conffile.h"
|
#include "conffile.h"
|
||||||
#include "logger.h"
|
#include "logger.h"
|
||||||
#include "player.h"
|
#include "player.h"
|
||||||
@ -44,24 +43,12 @@ struct dummy_session
|
|||||||
{
|
{
|
||||||
enum output_device_state state;
|
enum output_device_state state;
|
||||||
|
|
||||||
struct event *deferredev;
|
uint64_t device_id;
|
||||||
output_status_cb defer_cb;
|
int callback_id;
|
||||||
|
|
||||||
/* Do not dereference - only passed to the status cb */
|
|
||||||
struct output_device *device;
|
|
||||||
struct output_session *output_session;
|
|
||||||
output_status_cb status_cb;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
/* From player.c */
|
|
||||||
extern struct event_base *evbase_player;
|
|
||||||
|
|
||||||
struct dummy_session *sessions;
|
struct dummy_session *sessions;
|
||||||
|
|
||||||
/* Forwards */
|
|
||||||
static void
|
|
||||||
defer_cb(int fd, short what, void *arg);
|
|
||||||
|
|
||||||
/* ---------------------------- SESSION HANDLING ---------------------------- */
|
/* ---------------------------- SESSION HANDLING ---------------------------- */
|
||||||
|
|
||||||
static void
|
static void
|
||||||
@ -70,9 +57,6 @@ dummy_session_free(struct dummy_session *ds)
|
|||||||
if (!ds)
|
if (!ds)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
event_free(ds->deferredev);
|
|
||||||
|
|
||||||
free(ds->output_session);
|
|
||||||
free(ds);
|
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:
|
// Normally some here code to remove from linked list - here we just say:
|
||||||
sessions = NULL;
|
sessions = NULL;
|
||||||
|
|
||||||
|
outputs_device_session_remove(ds->device_id);
|
||||||
|
|
||||||
dummy_session_free(ds);
|
dummy_session_free(ds);
|
||||||
}
|
}
|
||||||
|
|
||||||
static struct dummy_session *
|
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;
|
struct dummy_session *ds;
|
||||||
|
|
||||||
os = calloc(1, sizeof(struct output_session));
|
CHECK_NULL(L_LAUDIO, ds = calloc(1, sizeof(struct dummy_session)));
|
||||||
if (!os)
|
|
||||||
{
|
|
||||||
DPRINTF(E_LOG, L_LAUDIO, "Out of memory for dummy session (os)\n");
|
|
||||||
return NULL;
|
|
||||||
}
|
|
||||||
|
|
||||||
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->state = OUTPUT_STATE_CONNECTED;
|
||||||
ds->device = device;
|
ds->device_id = device->id;
|
||||||
ds->status_cb = cb;
|
ds->callback_id = callback_id;
|
||||||
|
|
||||||
sessions = ds;
|
sessions = ds;
|
||||||
|
|
||||||
|
outputs_device_session_add(device->id, ds);
|
||||||
|
|
||||||
return ds;
|
return ds;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/* ---------------------------- STATUS HANDLERS ----------------------------- */
|
/* ---------------------------- 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
|
static void
|
||||||
dummy_status(struct dummy_session *ds)
|
dummy_status(struct dummy_session *ds)
|
||||||
{
|
{
|
||||||
ds->defer_cb = ds->status_cb;
|
outputs_cb(ds->callback_id, ds->device_id, ds->state);
|
||||||
event_active(ds->deferredev, 0, 0);
|
|
||||||
ds->status_cb = NULL;
|
if (ds->state == OUTPUT_STATE_STOPPED)
|
||||||
|
dummy_session_cleanup(ds);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/* ------------------ INTERFACE FUNCTIONS CALLED BY OUTPUTS.C --------------- */
|
/* ------------------ INTERFACE FUNCTIONS CALLED BY OUTPUTS.C --------------- */
|
||||||
|
|
||||||
static int
|
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;
|
struct dummy_session *ds;
|
||||||
|
|
||||||
ds = dummy_session_make(device, cb);
|
ds = dummy_session_make(device, callback_id);
|
||||||
if (!ds)
|
if (!ds)
|
||||||
return -1;
|
return -1;
|
||||||
|
|
||||||
@ -170,25 +118,12 @@ dummy_device_start(struct output_device *device, output_status_cb cb, uint64_t r
|
|||||||
return 0;
|
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
|
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);
|
ds->callback_id = callback_id;
|
||||||
if (!ds)
|
|
||||||
return -1;
|
|
||||||
|
|
||||||
ds->status_cb = cb;
|
|
||||||
ds->state = OUTPUT_STATE_STOPPED;
|
ds->state = OUTPUT_STATE_STOPPED;
|
||||||
|
|
||||||
dummy_status(ds);
|
dummy_status(ds);
|
||||||
@ -197,51 +132,55 @@ dummy_device_probe(struct output_device *device, output_status_cb cb)
|
|||||||
}
|
}
|
||||||
|
|
||||||
static int
|
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;
|
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;
|
return 0;
|
||||||
|
|
||||||
ds = device->session->session;
|
ds->callback_id = callback_id;
|
||||||
|
|
||||||
ds->status_cb = cb;
|
|
||||||
dummy_status(ds);
|
dummy_status(ds);
|
||||||
|
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
static void
|
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)
|
ds->callback_id = callback_id;
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
static int
|
static int
|
||||||
@ -259,18 +198,12 @@ dummy_init(void)
|
|||||||
|
|
||||||
nickname = cfg_getstr(cfg_audio, "nickname");
|
nickname = cfg_getstr(cfg_audio, "nickname");
|
||||||
|
|
||||||
device = calloc(1, sizeof(struct output_device));
|
CHECK_NULL(L_LAUDIO, device = calloc(1, sizeof(struct output_device)));
|
||||||
if (!device)
|
|
||||||
{
|
|
||||||
DPRINTF(E_LOG, L_LAUDIO, "Out of memory for dummy device\n");
|
|
||||||
return -1;
|
|
||||||
}
|
|
||||||
|
|
||||||
device->id = 0;
|
device->id = 0;
|
||||||
device->name = strdup(nickname);
|
device->name = strdup(nickname);
|
||||||
device->type = OUTPUT_TYPE_DUMMY;
|
device->type = OUTPUT_TYPE_DUMMY;
|
||||||
device->type_name = outputs_name(device->type);
|
device->type_name = outputs_name(device->type);
|
||||||
device->advertised = 1;
|
|
||||||
device->has_video = 0;
|
device->has_video = 0;
|
||||||
|
|
||||||
DPRINTF(E_INFO, L_LAUDIO, "Adding dummy output device '%s'\n", nickname);
|
DPRINTF(E_INFO, L_LAUDIO, "Adding dummy output device '%s'\n", nickname);
|
||||||
@ -296,9 +229,8 @@ struct output_definition output_dummy =
|
|||||||
.deinit = dummy_deinit,
|
.deinit = dummy_deinit,
|
||||||
.device_start = dummy_device_start,
|
.device_start = dummy_device_start,
|
||||||
.device_stop = dummy_device_stop,
|
.device_stop = dummy_device_stop,
|
||||||
|
.device_flush = dummy_device_flush,
|
||||||
.device_probe = dummy_device_probe,
|
.device_probe = dummy_device_probe,
|
||||||
.device_volume_set = dummy_device_volume_set,
|
.device_volume_set = dummy_device_volume_set,
|
||||||
.playback_start = dummy_playback_start,
|
.device_cb_set = dummy_device_cb_set,
|
||||||
.playback_stop = dummy_playback_stop,
|
|
||||||
.status_cb = dummy_set_status_cb,
|
|
||||||
};
|
};
|
||||||
|
@ -31,24 +31,23 @@
|
|||||||
#include <sys/stat.h>
|
#include <sys/stat.h>
|
||||||
#include <unistd.h>
|
#include <unistd.h>
|
||||||
|
|
||||||
#include <event2/event.h>
|
|
||||||
|
|
||||||
#include "misc.h"
|
#include "misc.h"
|
||||||
#include "conffile.h"
|
#include "conffile.h"
|
||||||
#include "logger.h"
|
#include "logger.h"
|
||||||
#include "player.h"
|
#include "player.h"
|
||||||
#include "outputs.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
|
struct fifo_packet
|
||||||
{
|
{
|
||||||
/* pcm data */
|
/* pcm data */
|
||||||
uint8_t samples[1408]; // STOB(AIRTUNES_V2_PACKET_SAMPLES)
|
uint8_t *samples;
|
||||||
|
size_t samples_size;
|
||||||
|
|
||||||
/* RTP-time of the first sample*/
|
/* Presentation timestamp of the first sample */
|
||||||
uint64_t rtptime;
|
struct timespec pts;
|
||||||
|
|
||||||
struct fifo_packet *next;
|
struct fifo_packet *next;
|
||||||
struct fifo_packet *prev;
|
struct fifo_packet *prev;
|
||||||
@ -59,8 +58,11 @@ struct fifo_buffer
|
|||||||
struct fifo_packet *head;
|
struct fifo_packet *head;
|
||||||
struct fifo_packet *tail;
|
struct fifo_packet *tail;
|
||||||
};
|
};
|
||||||
|
|
||||||
static struct fifo_buffer buffer;
|
static struct fifo_buffer buffer;
|
||||||
|
|
||||||
|
static struct media_quality fifo_quality = { 44100, 16, 2 };
|
||||||
|
|
||||||
|
|
||||||
static void
|
static void
|
||||||
free_buffer()
|
free_buffer()
|
||||||
@ -91,24 +93,12 @@ struct fifo_session
|
|||||||
|
|
||||||
int created;
|
int created;
|
||||||
|
|
||||||
struct event *deferredev;
|
uint64_t device_id;
|
||||||
output_status_cb defer_cb;
|
int callback_id;
|
||||||
|
|
||||||
/* Do not dereference - only passed to the status cb */
|
|
||||||
struct output_device *device;
|
|
||||||
struct output_session *output_session;
|
|
||||||
output_status_cb status_cb;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
/* From player.c */
|
|
||||||
extern struct event_base *evbase_player;
|
|
||||||
|
|
||||||
static struct fifo_session *sessions;
|
static struct fifo_session *sessions;
|
||||||
|
|
||||||
/* Forwards */
|
|
||||||
static void
|
|
||||||
defer_cb(int fd, short what, void *arg);
|
|
||||||
|
|
||||||
|
|
||||||
/* ---------------------------- FIFO HANDLING ---------------------------- */
|
/* ---------------------------- FIFO HANDLING ---------------------------- */
|
||||||
|
|
||||||
@ -240,9 +230,6 @@ fifo_session_free(struct fifo_session *fifo_session)
|
|||||||
if (!fifo_session)
|
if (!fifo_session)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
event_free(fifo_session->deferredev);
|
|
||||||
|
|
||||||
free(fifo_session->output_session);
|
|
||||||
free(fifo_session);
|
free(fifo_session);
|
||||||
free_buffer();
|
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:
|
// Normally some here code to remove from linked list - here we just say:
|
||||||
sessions = NULL;
|
sessions = NULL;
|
||||||
|
|
||||||
|
outputs_device_session_remove(fifo_session->device_id);
|
||||||
|
|
||||||
fifo_session_free(fifo_session);
|
fifo_session_free(fifo_session);
|
||||||
}
|
}
|
||||||
|
|
||||||
static struct 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;
|
struct fifo_session *fifo_session;
|
||||||
|
|
||||||
output_session = calloc(1, sizeof(struct output_session));
|
CHECK_NULL(L_FIFO, fifo_session = calloc(1, sizeof(struct fifo_session)));
|
||||||
if (!output_session)
|
|
||||||
{
|
|
||||||
DPRINTF(E_LOG, L_FIFO, "Out of memory (os)\n");
|
|
||||||
return NULL;
|
|
||||||
}
|
|
||||||
|
|
||||||
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->state = OUTPUT_STATE_CONNECTED;
|
||||||
fifo_session->device = device;
|
fifo_session->device_id = device->id;
|
||||||
fifo_session->status_cb = cb;
|
fifo_session->callback_id = callback_id;
|
||||||
|
|
||||||
fifo_session->created = 0;
|
fifo_session->created = 0;
|
||||||
fifo_session->path = device->extra_device_info;
|
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;
|
sessions = fifo_session;
|
||||||
|
|
||||||
|
outputs_device_session_add(device->id, fifo_session);
|
||||||
|
|
||||||
return fifo_session;
|
return fifo_session;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/* ---------------------------- STATUS HANDLERS ----------------------------- */
|
/* ---------------------------- 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
|
static void
|
||||||
fifo_status(struct fifo_session *fifo_session)
|
fifo_status(struct fifo_session *fifo_session)
|
||||||
{
|
{
|
||||||
fifo_session->defer_cb = fifo_session->status_cb;
|
outputs_cb(fifo_session->callback_id, fifo_session->device_id, fifo_session->state);
|
||||||
event_active(fifo_session->deferredev, 0, 0);
|
|
||||||
fifo_session->status_cb = NULL;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
if (fifo_session->state == OUTPUT_STATE_STOPPED)
|
||||||
|
fifo_session_cleanup(fifo_session);
|
||||||
|
}
|
||||||
|
|
||||||
/* ------------------ INTERFACE FUNCTIONS CALLED BY OUTPUTS.C --------------- */
|
/* ------------------ INTERFACE FUNCTIONS CALLED BY OUTPUTS.C --------------- */
|
||||||
|
|
||||||
static int
|
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;
|
struct fifo_session *fifo_session;
|
||||||
int ret;
|
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)
|
if (!fifo_session)
|
||||||
return -1;
|
return -1;
|
||||||
|
|
||||||
@ -351,25 +305,46 @@ fifo_device_start(struct output_device *device, output_status_cb cb, uint64_t rt
|
|||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
static void
|
static int
|
||||||
fifo_device_stop(struct output_session *output_session)
|
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);
|
fifo_close(fifo_session);
|
||||||
free_buffer();
|
free_buffer();
|
||||||
|
|
||||||
fifo_session->state = OUTPUT_STATE_STOPPED;
|
fifo_session->state = OUTPUT_STATE_STOPPED;
|
||||||
fifo_status(fifo_session);
|
fifo_status(fifo_session);
|
||||||
|
|
||||||
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
static int
|
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;
|
struct fifo_session *fifo_session;
|
||||||
int ret;
|
int ret;
|
||||||
|
|
||||||
fifo_session = fifo_session_make(device, cb);
|
fifo_session = fifo_session_make(device, callback_id);
|
||||||
if (!fifo_session)
|
if (!fifo_session)
|
||||||
return -1;
|
return -1;
|
||||||
|
|
||||||
@ -382,7 +357,7 @@ fifo_device_probe(struct output_device *device, output_status_cb cb)
|
|||||||
|
|
||||||
fifo_close(fifo_session);
|
fifo_close(fifo_session);
|
||||||
|
|
||||||
fifo_session->status_cb = cb;
|
fifo_session->callback_id = callback_id;
|
||||||
fifo_session->state = OUTPUT_STATE_STOPPED;
|
fifo_session->state = OUTPUT_STATE_STOPPED;
|
||||||
|
|
||||||
fifo_status(fifo_session);
|
fifo_status(fifo_session);
|
||||||
@ -391,104 +366,81 @@ fifo_device_probe(struct output_device *device, output_status_cb cb)
|
|||||||
}
|
}
|
||||||
|
|
||||||
static int
|
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;
|
return 0;
|
||||||
|
|
||||||
fifo_session = device->session->session;
|
fifo_session->callback_id = callback_id;
|
||||||
|
|
||||||
fifo_session->status_cb = cb;
|
|
||||||
fifo_status(fifo_session);
|
fifo_status(fifo_session);
|
||||||
|
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
static void
|
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_session *fifo_session = sessions;
|
||||||
|
struct fifo_packet *packet;
|
||||||
|
struct timespec now;
|
||||||
|
ssize_t bytes;
|
||||||
|
int i;
|
||||||
|
|
||||||
if (!fifo_session)
|
if (!fifo_session)
|
||||||
return;
|
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_session->state = OUTPUT_STATE_STREAMING;
|
||||||
fifo_status(fifo_session);
|
|
||||||
}
|
|
||||||
|
|
||||||
static void
|
CHECK_NULL(L_FIFO, packet = calloc(1, sizeof(struct fifo_packet)));
|
||||||
fifo_playback_stop(void)
|
CHECK_NULL(L_FIFO, packet->samples = malloc(obuf->data[i].bufsize));
|
||||||
{
|
|
||||||
struct fifo_session *fifo_session = sessions;
|
|
||||||
|
|
||||||
if (!fifo_session)
|
memcpy(packet->samples, obuf->data[i].buffer, obuf->data[i].bufsize);
|
||||||
return;
|
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)
|
if (buffer.head)
|
||||||
{
|
{
|
||||||
buffer.head->next = packet;
|
buffer.head->next = packet;
|
||||||
packet->prev = buffer.head;
|
packet->prev = buffer.head;
|
||||||
}
|
}
|
||||||
|
|
||||||
buffer.head = packet;
|
buffer.head = packet;
|
||||||
if (!buffer.tail)
|
if (!buffer.tail)
|
||||||
buffer.tail = packet;
|
buffer.tail = packet;
|
||||||
|
|
||||||
ret = player_get_current_pos(&cur_pos, &now, 0);
|
now.tv_sec = obuf->pts.tv_sec - OUTPUTS_BUFFER_DURATION;
|
||||||
if (ret < 0)
|
now.tv_nsec = obuf->pts.tv_sec;
|
||||||
{
|
|
||||||
DPRINTF(E_LOG, L_FIFO, "Could not get playback position\n");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
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)
|
if (bytes > 0)
|
||||||
{
|
{
|
||||||
packet = buffer.tail;
|
packet = buffer.tail;
|
||||||
buffer.tail = buffer.tail->next;
|
buffer.tail = buffer.tail->next;
|
||||||
|
free(packet->samples);
|
||||||
free(packet);
|
free(packet);
|
||||||
return;
|
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
|
static int
|
||||||
fifo_init(void)
|
fifo_init(void)
|
||||||
{
|
{
|
||||||
@ -539,18 +483,12 @@ fifo_init(void)
|
|||||||
|
|
||||||
memset(&buffer, 0, sizeof(struct fifo_buffer));
|
memset(&buffer, 0, sizeof(struct fifo_buffer));
|
||||||
|
|
||||||
device = calloc(1, sizeof(struct output_device));
|
CHECK_NULL(L_FIFO, device = calloc(1, sizeof(struct output_device)));
|
||||||
if (!device)
|
|
||||||
{
|
|
||||||
DPRINTF(E_LOG, L_FIFO, "Out of memory for fifo device\n");
|
|
||||||
return -1;
|
|
||||||
}
|
|
||||||
|
|
||||||
device->id = 100;
|
device->id = 100;
|
||||||
device->name = strdup(nickname);
|
device->name = strdup(nickname);
|
||||||
device->type = OUTPUT_TYPE_FIFO;
|
device->type = OUTPUT_TYPE_FIFO;
|
||||||
device->type_name = outputs_name(device->type);
|
device->type_name = outputs_name(device->type);
|
||||||
device->advertised = 1;
|
|
||||||
device->has_video = 0;
|
device->has_video = 0;
|
||||||
device->extra_device_info = path;
|
device->extra_device_info = path;
|
||||||
DPRINTF(E_INFO, L_FIFO, "Adding fifo output device '%s' with path '%s'\n", nickname, 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,
|
.deinit = fifo_deinit,
|
||||||
.device_start = fifo_device_start,
|
.device_start = fifo_device_start,
|
||||||
.device_stop = fifo_device_stop,
|
.device_stop = fifo_device_stop,
|
||||||
|
.device_flush = fifo_device_flush,
|
||||||
.device_probe = fifo_device_probe,
|
.device_probe = fifo_device_probe,
|
||||||
.device_volume_set = fifo_device_volume_set,
|
.device_volume_set = fifo_device_volume_set,
|
||||||
.playback_start = fifo_playback_start,
|
.device_cb_set = fifo_device_cb_set,
|
||||||
.playback_stop = fifo_playback_stop,
|
|
||||||
.write = fifo_write,
|
.write = fifo_write,
|
||||||
.flush = fifo_flush,
|
|
||||||
.status_cb = fifo_set_status_cb,
|
|
||||||
};
|
};
|
||||||
|
@ -58,21 +58,21 @@ struct pulse
|
|||||||
|
|
||||||
struct pulse_session
|
struct pulse_session
|
||||||
{
|
{
|
||||||
|
uint64_t device_id;
|
||||||
|
int callback_id;
|
||||||
|
|
||||||
|
char *devname;
|
||||||
|
|
||||||
pa_stream_state_t state;
|
pa_stream_state_t state;
|
||||||
pa_stream *stream;
|
pa_stream *stream;
|
||||||
|
|
||||||
pa_buffer_attr attr;
|
pa_buffer_attr attr;
|
||||||
pa_volume_t volume;
|
pa_volume_t volume;
|
||||||
|
|
||||||
|
struct media_quality quality;
|
||||||
|
|
||||||
int logcount;
|
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;
|
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
|
// Internal list with indeces of the Pulseaudio devices (sinks) we have registered
|
||||||
static uint32_t pulse_known_devices[PULSE_MAX_DEVICES];
|
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
|
// Converts from 0 - 100 to Pulseaudio's scale
|
||||||
static inline pa_volume_t
|
static inline pa_volume_t
|
||||||
pulse_from_device_volume(int device_volume)
|
pulse_from_device_volume(int device_volume)
|
||||||
@ -113,10 +116,9 @@ pulse_session_free(struct pulse_session *ps)
|
|||||||
pa_threaded_mainloop_unlock(pulse.mainloop);
|
pa_threaded_mainloop_unlock(pulse.mainloop);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (ps->devname)
|
outputs_quality_unsubscribe(&pulse_fallback_quality);
|
||||||
free(ps->devname);
|
|
||||||
|
|
||||||
free(ps->output_session);
|
free(ps->devname);
|
||||||
|
|
||||||
free(ps);
|
free(ps);
|
||||||
}
|
}
|
||||||
@ -139,43 +141,37 @@ pulse_session_cleanup(struct pulse_session *ps)
|
|||||||
p->next = ps->next;
|
p->next = ps->next;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
outputs_device_session_remove(ps->device_id);
|
||||||
|
|
||||||
pulse_session_free(ps);
|
pulse_session_free(ps);
|
||||||
}
|
}
|
||||||
|
|
||||||
static struct pulse_session *
|
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;
|
struct pulse_session *ps;
|
||||||
|
int ret;
|
||||||
|
|
||||||
os = calloc(1, sizeof(struct output_session));
|
ret = outputs_quality_subscribe(&pulse_fallback_quality);
|
||||||
if (!os)
|
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;
|
return NULL;
|
||||||
}
|
}
|
||||||
|
|
||||||
ps = calloc(1, sizeof(struct pulse_session));
|
CHECK_NULL(L_LAUDIO, ps = calloc(1, sizeof(struct pulse_session)));
|
||||||
if (!ps)
|
|
||||||
{
|
|
||||||
DPRINTF(E_LOG, L_LAUDIO, "Out of memory (ps)\n");
|
|
||||||
free(os);
|
|
||||||
return NULL;
|
|
||||||
}
|
|
||||||
|
|
||||||
os->session = ps;
|
|
||||||
os->type = device->type;
|
|
||||||
|
|
||||||
ps->output_session = os;
|
|
||||||
ps->state = PA_STREAM_UNCONNECTED;
|
ps->state = PA_STREAM_UNCONNECTED;
|
||||||
ps->device = device;
|
ps->device_id = device->id;
|
||||||
ps->status_cb = cb;
|
ps->callback_id = callback_id;
|
||||||
ps->volume = pulse_from_device_volume(device->volume);
|
ps->volume = pulse_from_device_volume(device->volume);
|
||||||
ps->devname = strdup(device->extra_device_info);
|
ps->devname = strdup(device->extra_device_info);
|
||||||
|
|
||||||
ps->next = sessions;
|
ps->next = sessions;
|
||||||
sessions = ps;
|
sessions = ps;
|
||||||
|
|
||||||
|
outputs_device_session_add(device->id, ps);
|
||||||
|
|
||||||
return ps;
|
return ps;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -187,7 +183,6 @@ static enum command_state
|
|||||||
send_status(void *arg, int *ptr)
|
send_status(void *arg, int *ptr)
|
||||||
{
|
{
|
||||||
struct pulse_session *ps = arg;
|
struct pulse_session *ps = arg;
|
||||||
output_status_cb status_cb;
|
|
||||||
enum output_device_state state;
|
enum output_device_state state;
|
||||||
|
|
||||||
switch (ps->state)
|
switch (ps->state)
|
||||||
@ -210,10 +205,8 @@ send_status(void *arg, int *ptr)
|
|||||||
state = OUTPUT_STATE_FAILED;
|
state = OUTPUT_STATE_FAILED;
|
||||||
}
|
}
|
||||||
|
|
||||||
status_cb = ps->status_cb;
|
outputs_cb(ps->callback_id, ps->device_id, state);
|
||||||
ps->status_cb = NULL;
|
ps->callback_id = -1;
|
||||||
if (status_cb)
|
|
||||||
status_cb(ps->device, ps->output_session, state);
|
|
||||||
|
|
||||||
return COMMAND_PENDING; // Don't want the command module to clean up ps
|
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->name = strdup(name);
|
||||||
device->type = OUTPUT_TYPE_PULSE;
|
device->type = OUTPUT_TYPE_PULSE;
|
||||||
device->type_name = outputs_name(device->type);
|
device->type_name = outputs_name(device->type);
|
||||||
device->advertised = 1;
|
|
||||||
device->extra_device_info = strdup(info->name);
|
device->extra_device_info = strdup(info->name);
|
||||||
|
|
||||||
player_device_add(device);
|
player_device_add(device);
|
||||||
@ -572,25 +564,33 @@ pulse_free(void)
|
|||||||
}
|
}
|
||||||
|
|
||||||
static int
|
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_stream_flags_t flags;
|
||||||
pa_sample_spec ss;
|
pa_sample_spec ss;
|
||||||
pa_cvolume cvol;
|
pa_cvolume cvol;
|
||||||
int offset;
|
int offset_ms;
|
||||||
int ret;
|
int ret;
|
||||||
|
|
||||||
DPRINTF(E_DBG, L_LAUDIO, "Opening Pulseaudio stream to '%s'\n", ps->devname);
|
DPRINTF(E_DBG, L_LAUDIO, "Opening Pulseaudio stream to '%s'\n", ps->devname);
|
||||||
|
|
||||||
ss.format = PA_SAMPLE_S16LE;
|
if (quality->bits_per_sample == 16)
|
||||||
ss.channels = 2;
|
ss.format = PA_SAMPLE_S16LE;
|
||||||
ss.rate = 44100;
|
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");
|
ss.channels = quality->channels;
|
||||||
if (abs(offset) > 44100)
|
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);
|
DPRINTF(E_LOG, L_LAUDIO, "The audio offset (%d) set in the configuration is out of bounds\n", offset_ms);
|
||||||
offset = 44100 * (offset/abs(offset));
|
offset_ms = 1000 * (offset_ms/abs(offset_ms));
|
||||||
}
|
}
|
||||||
|
|
||||||
pa_threaded_mainloop_lock(pulse.mainloop);
|
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;
|
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.maxlength = 2 * ps->attr.tlength;
|
||||||
ps->attr.prebuf = (uint32_t)-1;
|
ps->attr.prebuf = (uint32_t)-1;
|
||||||
ps->attr.minreq = (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:
|
unlock_and_fail:
|
||||||
ret = pa_context_errno(pulse.context);
|
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);
|
pa_threaded_mainloop_unlock(pulse.mainloop);
|
||||||
|
|
||||||
@ -635,11 +636,15 @@ stream_open(struct pulse_session *ps, pa_stream_notify_cb_t cb)
|
|||||||
static void
|
static void
|
||||||
stream_close(struct pulse_session *ps, pa_stream_notify_cb_t cb)
|
stream_close(struct pulse_session *ps, pa_stream_notify_cb_t cb)
|
||||||
{
|
{
|
||||||
|
if (!ps->stream)
|
||||||
|
return;
|
||||||
|
|
||||||
pa_threaded_mainloop_lock(pulse.mainloop);
|
pa_threaded_mainloop_lock(pulse.mainloop);
|
||||||
|
|
||||||
pa_stream_set_underflow_callback(ps->stream, NULL, NULL);
|
pa_stream_set_underflow_callback(ps->stream, NULL, NULL);
|
||||||
pa_stream_set_overflow_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_set_state_callback(ps->stream, cb, ps);
|
||||||
|
|
||||||
pa_stream_disconnect(ps->stream);
|
pa_stream_disconnect(ps->stream);
|
||||||
pa_stream_unref(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);
|
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 --------------- */
|
/* ------------------ INTERFACE FUNCTIONS CALLED BY OUTPUTS.C --------------- */
|
||||||
|
|
||||||
static int
|
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;
|
struct pulse_session *ps;
|
||||||
int ret;
|
|
||||||
|
|
||||||
DPRINTF(E_DBG, L_LAUDIO, "Pulseaudio starting '%s'\n", device->name);
|
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)
|
if (!ps)
|
||||||
return -1;
|
return -1;
|
||||||
|
|
||||||
ret = stream_open(ps, start_cb);
|
pulse_status(ps);
|
||||||
if (ret < 0)
|
|
||||||
{
|
|
||||||
pulse_session_cleanup(ps);
|
|
||||||
return -1;
|
|
||||||
}
|
|
||||||
|
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
static void
|
static int
|
||||||
pulse_device_stop(struct output_session *session)
|
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);
|
DPRINTF(E_DBG, L_LAUDIO, "Pulseaudio stopping '%s'\n", ps->devname);
|
||||||
|
|
||||||
|
ps->callback_id = callback_id;
|
||||||
|
|
||||||
stream_close(ps, close_cb);
|
stream_close(ps, close_cb);
|
||||||
|
|
||||||
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
static int
|
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;
|
struct pulse_session *ps;
|
||||||
int ret;
|
int ret;
|
||||||
|
|
||||||
DPRINTF(E_DBG, L_LAUDIO, "Pulseaudio probing '%s'\n", device->name);
|
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)
|
if (!ps)
|
||||||
return -1;
|
return -1;
|
||||||
|
|
||||||
ret = stream_open(ps, probe_cb);
|
ret = stream_open(ps, &pulse_fallback_quality, probe_cb);
|
||||||
if (ret < 0)
|
if (ret < 0)
|
||||||
{
|
{
|
||||||
pulse_session_cleanup(ps);
|
pulse_session_cleanup(ps);
|
||||||
@ -712,18 +832,25 @@ pulse_device_free_extra(struct output_device *device)
|
|||||||
free(device->extra_device_info);
|
free(device->extra_device_info);
|
||||||
}
|
}
|
||||||
|
|
||||||
static int
|
static void
|
||||||
pulse_device_volume_set(struct output_device *device, output_status_cb cb)
|
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;
|
uint32_t idx;
|
||||||
pa_operation* o;
|
pa_operation* o;
|
||||||
pa_cvolume cvol;
|
pa_cvolume cvol;
|
||||||
|
|
||||||
if (!sessions || !device->session || !device->session->session)
|
if (!ps)
|
||||||
return 0;
|
return 0;
|
||||||
|
|
||||||
ps = device->session->session;
|
|
||||||
idx = pa_stream_get_index(ps->stream);
|
idx = pa_stream_get_index(ps->stream);
|
||||||
|
|
||||||
ps->volume = pulse_from_device_volume(device->volume);
|
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);
|
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);
|
o = pa_context_set_sink_input_volume(pulse.context, idx, &cvol, volume_cb, ps);
|
||||||
if (!o)
|
if (!o)
|
||||||
@ -750,141 +877,33 @@ pulse_device_volume_set(struct output_device *device, output_status_cb cb)
|
|||||||
}
|
}
|
||||||
|
|
||||||
static void
|
static void
|
||||||
pulse_write(uint8_t *buf, uint64_t rtptime)
|
pulse_write(struct output_buffer *obuf)
|
||||||
{
|
{
|
||||||
struct pulse_session *ps;
|
struct pulse_session *ps;
|
||||||
struct pulse_session *next;
|
struct pulse_session *next;
|
||||||
size_t length;
|
|
||||||
int ret;
|
|
||||||
|
|
||||||
if (!sessions)
|
if (!sessions)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
length = STOB(AIRTUNES_V2_PACKET_SAMPLES);
|
|
||||||
|
|
||||||
pa_threaded_mainloop_lock(pulse.mainloop);
|
|
||||||
|
|
||||||
for (ps = sessions; ps; ps = next)
|
for (ps = sessions; ps; ps = next)
|
||||||
{
|
{
|
||||||
next = 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;
|
continue;
|
||||||
|
|
||||||
ret = pa_stream_write(ps->stream, buf, length, NULL, 0LL, PA_SEEK_RELATIVE);
|
if (ps->stream && pa_stream_is_corked(ps->stream))
|
||||||
if (ret < 0)
|
playback_resume(ps);
|
||||||
{
|
|
||||||
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;
|
playback_write(ps, obuf);
|
||||||
pulse_session_shutdown(ps);
|
|
||||||
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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
|
static int
|
||||||
@ -977,13 +996,11 @@ struct output_definition output_pulse =
|
|||||||
.deinit = pulse_deinit,
|
.deinit = pulse_deinit,
|
||||||
.device_start = pulse_device_start,
|
.device_start = pulse_device_start,
|
||||||
.device_stop = pulse_device_stop,
|
.device_stop = pulse_device_stop,
|
||||||
|
.device_flush = pulse_device_flush,
|
||||||
.device_probe = pulse_device_probe,
|
.device_probe = pulse_device_probe,
|
||||||
.device_free_extra = pulse_device_free_extra,
|
.device_free_extra = pulse_device_free_extra,
|
||||||
|
.device_cb_set = pulse_device_cb_set,
|
||||||
.device_volume_set = pulse_device_volume_set,
|
.device_volume_set = pulse_device_volume_set,
|
||||||
.playback_start = pulse_playback_start,
|
|
||||||
.playback_stop = pulse_playback_stop,
|
|
||||||
.write = pulse_write,
|
.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 "outputs.h"
|
||||||
#include "httpd_streaming.h"
|
#include "httpd_streaming.h"
|
||||||
|
|
||||||
|
|
||||||
struct output_definition output_streaming =
|
struct output_definition output_streaming =
|
||||||
{
|
{
|
||||||
.name = "mp3 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"
|
#include "db.h"
|
||||||
|
|
||||||
/* AirTunes v2 packet interval in ns */
|
// Maximum number of previously played songs that are remembered
|
||||||
/* (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 */
|
|
||||||
#define MAX_HISTORY_COUNT 20
|
#define MAX_HISTORY_COUNT 20
|
||||||
|
|
||||||
enum play_status {
|
enum play_status {
|
||||||
@ -79,15 +72,11 @@ struct player_history
|
|||||||
uint32_t item_id[MAX_HISTORY_COUNT];
|
uint32_t item_id[MAX_HISTORY_COUNT];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
int
|
|
||||||
player_get_current_pos(uint64_t *pos, struct timespec *ts, int commit);
|
|
||||||
|
|
||||||
int
|
int
|
||||||
player_get_status(struct player_status *status);
|
player_get_status(struct player_status *status);
|
||||||
|
|
||||||
int
|
int
|
||||||
player_now_playing(uint32_t *id);
|
player_playing_now(uint32_t *id);
|
||||||
|
|
||||||
void
|
void
|
||||||
player_speaker_enumerate(spk_enum_cb cb, void *arg);
|
player_speaker_enumerate(spk_enum_cb cb, void *arg);
|
||||||
@ -104,9 +93,6 @@ player_speaker_enable(uint64_t id);
|
|||||||
int
|
int
|
||||||
player_speaker_disable(uint64_t id);
|
player_speaker_disable(uint64_t id);
|
||||||
|
|
||||||
void
|
|
||||||
player_speaker_status_trigger(void);
|
|
||||||
|
|
||||||
int
|
int
|
||||||
player_playback_start(void);
|
player_playback_start(void);
|
||||||
|
|
||||||
@ -152,7 +138,6 @@ player_shuffle_set(int enable);
|
|||||||
int
|
int
|
||||||
player_consume_set(int enable);
|
player_consume_set(int enable);
|
||||||
|
|
||||||
|
|
||||||
void
|
void
|
||||||
player_queue_clear_history(void);
|
player_queue_clear_history(void);
|
||||||
|
|
||||||
@ -171,8 +156,8 @@ player_device_remove(void *device);
|
|||||||
void
|
void
|
||||||
player_raop_verification_kickoff(char **arglist);
|
player_raop_verification_kickoff(char **arglist);
|
||||||
|
|
||||||
void
|
const char *
|
||||||
player_metadata_send(void *imd, void *omd);
|
player_pmap(void *p);
|
||||||
|
|
||||||
int
|
int
|
||||||
player_init(void);
|
player_init(void);
|
||||||
|
@ -718,8 +718,7 @@ playback_eot(void *arg, int *retval)
|
|||||||
|
|
||||||
g_state = SPOTIFY_STATE_STOPPING;
|
g_state = SPOTIFY_STATE_STOPPING;
|
||||||
|
|
||||||
// TODO 1) This will block for a while, but perhaps ok?
|
input_write(spotify_audio_buffer, NULL, INPUT_FLAG_EOF);
|
||||||
input_write(spotify_audio_buffer, INPUT_FLAG_EOF);
|
|
||||||
|
|
||||||
*retval = 0;
|
*retval = 0;
|
||||||
return COMMAND_END;
|
return COMMAND_END;
|
||||||
@ -1007,17 +1006,22 @@ logged_out(sp_session *sess)
|
|||||||
static int music_delivery(sp_session *sess, const sp_audioformat *format,
|
static int music_delivery(sp_session *sess, const sp_audioformat *format,
|
||||||
const void *frames, int num_frames)
|
const void *frames, int num_frames)
|
||||||
{
|
{
|
||||||
|
struct media_quality quality = { 0 };
|
||||||
size_t size;
|
size_t size;
|
||||||
int ret;
|
int ret;
|
||||||
|
|
||||||
/* No support for resampling right now */
|
/* 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();
|
spotify_playback_stop_nonblock();
|
||||||
return num_frames;
|
return num_frames;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
quality.sample_rate = format->sample_rate;
|
||||||
|
quality.bits_per_sample = 16;
|
||||||
|
quality.channels = format->channels;
|
||||||
|
|
||||||
// Audio discontinuity, e.g. seek
|
// Audio discontinuity, e.g. seek
|
||||||
if (num_frames == 0)
|
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
|
// 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
|
// 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.
|
// 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;
|
return num_frames;
|
||||||
}
|
}
|
||||||
|
250
src/transcode.c
250
src/transcode.c
@ -40,6 +40,7 @@
|
|||||||
#include "conffile.h"
|
#include "conffile.h"
|
||||||
#include "db.h"
|
#include "db.h"
|
||||||
#include "avio_evbuffer.h"
|
#include "avio_evbuffer.h"
|
||||||
|
#include "misc.h"
|
||||||
#include "transcode.h"
|
#include "transcode.h"
|
||||||
|
|
||||||
// Interval between ICY metadata checks for streams, in seconds
|
// Interval between ICY metadata checks for streams, in seconds
|
||||||
@ -75,12 +76,10 @@ struct settings_ctx
|
|||||||
|
|
||||||
// Audio settings
|
// Audio settings
|
||||||
enum AVCodecID audio_codec;
|
enum AVCodecID audio_codec;
|
||||||
const char *audio_codec_name;
|
|
||||||
int sample_rate;
|
int sample_rate;
|
||||||
uint64_t channel_layout;
|
uint64_t channel_layout;
|
||||||
int channels;
|
int channels;
|
||||||
enum AVSampleFormat sample_format;
|
enum AVSampleFormat sample_format;
|
||||||
int byte_depth;
|
|
||||||
bool wavheader;
|
bool wavheader;
|
||||||
bool icy;
|
bool icy;
|
||||||
|
|
||||||
@ -178,47 +177,56 @@ struct encode_ctx
|
|||||||
uint8_t header[44];
|
uint8_t header[44];
|
||||||
};
|
};
|
||||||
|
|
||||||
struct transcode_ctx
|
|
||||||
{
|
|
||||||
struct decode_ctx *decode_ctx;
|
|
||||||
struct encode_ctx *encode_ctx;
|
|
||||||
};
|
|
||||||
|
|
||||||
|
|
||||||
/* -------------------------- PROFILE CONFIGURATION ------------------------ */
|
/* -------------------------- PROFILE CONFIGURATION ------------------------ */
|
||||||
|
|
||||||
static int
|
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));
|
memset(settings, 0, sizeof(struct settings_ctx));
|
||||||
|
|
||||||
switch (profile)
|
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:
|
case XCODE_PCM16_HEADER:
|
||||||
settings->wavheader = 1;
|
settings->wavheader = 1;
|
||||||
case XCODE_PCM16_NOHEADER:
|
case XCODE_PCM16:
|
||||||
settings->encode_audio = 1;
|
settings->encode_audio = 1;
|
||||||
settings->format = "s16le";
|
settings->format = "s16le";
|
||||||
settings->audio_codec = AV_CODEC_ID_PCM_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->sample_format = AV_SAMPLE_FMT_S16;
|
||||||
settings->byte_depth = 2; // Bytes per sample = 16/8
|
break;
|
||||||
settings->icy = 1;
|
|
||||||
|
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;
|
break;
|
||||||
|
|
||||||
case XCODE_MP3:
|
case XCODE_MP3:
|
||||||
settings->encode_audio = 1;
|
settings->encode_audio = 1;
|
||||||
settings->format = "mp3";
|
settings->format = "mp3";
|
||||||
settings->audio_codec = AV_CODEC_ID_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->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;
|
break;
|
||||||
|
|
||||||
case XCODE_JPEG:
|
case XCODE_JPEG:
|
||||||
@ -226,6 +234,7 @@ init_settings(struct settings_ctx *settings, enum transcode_profile profile)
|
|||||||
settings->silent = 1;
|
settings->silent = 1;
|
||||||
settings->format = "image2";
|
settings->format = "image2";
|
||||||
settings->in_format = "mjpeg";
|
settings->in_format = "mjpeg";
|
||||||
|
settings->pix_fmt = AV_PIX_FMT_YUVJ420P;
|
||||||
settings->video_codec = AV_CODEC_ID_MJPEG;
|
settings->video_codec = AV_CODEC_ID_MJPEG;
|
||||||
break;
|
break;
|
||||||
|
|
||||||
@ -233,6 +242,7 @@ init_settings(struct settings_ctx *settings, enum transcode_profile profile)
|
|||||||
settings->encode_video = 1;
|
settings->encode_video = 1;
|
||||||
settings->silent = 1;
|
settings->silent = 1;
|
||||||
settings->format = "image2";
|
settings->format = "image2";
|
||||||
|
settings->pix_fmt = AV_PIX_FMT_RGB24;
|
||||||
settings->video_codec = AV_CODEC_ID_PNG;
|
settings->video_codec = AV_CODEC_ID_PNG;
|
||||||
break;
|
break;
|
||||||
|
|
||||||
@ -241,16 +251,21 @@ init_settings(struct settings_ctx *settings, enum transcode_profile profile)
|
|||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (settings->audio_codec)
|
if (quality && quality->sample_rate)
|
||||||
{
|
{
|
||||||
codec_desc = avcodec_descriptor_get(settings->audio_codec);
|
settings->sample_rate = quality->sample_rate;
|
||||||
settings->audio_codec_name = codec_desc->name;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (settings->video_codec)
|
if (quality && quality->channels)
|
||||||
{
|
{
|
||||||
codec_desc = avcodec_descriptor_get(settings->video_codec);
|
settings->channels = quality->channels;
|
||||||
settings->video_codec_name = codec_desc->name;
|
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;
|
return 0;
|
||||||
@ -279,6 +294,19 @@ stream_settings_set(struct stream_ctx *s, struct settings_ctx *settings, enum AV
|
|||||||
|
|
||||||
/* -------------------------------- HELPERS -------------------------------- */
|
/* -------------------------------- 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 *
|
static inline char *
|
||||||
err2str(int errnum)
|
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;
|
uint32_t wav_len;
|
||||||
int duration;
|
int duration;
|
||||||
|
int bps;
|
||||||
|
|
||||||
if (src_ctx->duration)
|
if (src_ctx->duration)
|
||||||
duration = src_ctx->duration;
|
duration = src_ctx->duration;
|
||||||
else
|
else
|
||||||
duration = 3 * 60 * 1000; /* 3 minutes, in ms */
|
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);
|
memcpy(ctx->header, "RIFF", 4);
|
||||||
add_le32(ctx->header + 4, 36 + wav_len);
|
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 + 20, 1);
|
||||||
add_le16(ctx->header + 22, ctx->settings.channels); /* channels */
|
add_le16(ctx->header + 22, ctx->settings.channels); /* channels */
|
||||||
add_le32(ctx->header + 24, ctx->settings.sample_rate); /* samplerate */
|
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_le32(ctx->header + 28, ctx->settings.sample_rate * ctx->settings.channels * bps); /* byte rate */
|
||||||
add_le16(ctx->header + 32, ctx->settings.channels * ctx->settings.byte_depth); /* block align */
|
add_le16(ctx->header + 32, ctx->settings.channels * bps); /* block align */
|
||||||
add_le16(ctx->header + 34, ctx->settings.byte_depth * 8); /* bits per sample */
|
add_le16(ctx->header + 34, 8 * bps); /* bits per sample */
|
||||||
memcpy(ctx->header + 36, "data", 4);
|
memcpy(ctx->header + 36, "data", 4);
|
||||||
add_le32(ctx->header + 40, wav_len);
|
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
|
* @out ctx A pre-allocated stream ctx where we save stream and codec info
|
||||||
* @in output Output to add the stream to
|
* @in output Output to add the stream to
|
||||||
* @in codec_id What kind of codec should we use
|
* @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
|
* @return Negative on failure, otherwise zero
|
||||||
*/
|
*/
|
||||||
static int
|
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;
|
AVCodec *encoder;
|
||||||
AVDictionary *options = NULL;
|
AVDictionary *options = NULL;
|
||||||
int ret;
|
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);
|
encoder = avcodec_find_encoder(codec_id);
|
||||||
if (!encoder)
|
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;
|
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)
|
if (!s->codec->pix_fmt)
|
||||||
{
|
{
|
||||||
s->codec->pix_fmt = avcodec_default_get_format(s->codec, encoder->pix_fmts);
|
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)
|
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);
|
ret = avcodec_open2(s->codec, NULL, &options);
|
||||||
if (ret < 0)
|
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);
|
avcodec_free_context(&s->codec);
|
||||||
return -1;
|
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);
|
ret = avcodec_parameters_from_context(s->stream->codecpar, s->codec);
|
||||||
if (ret < 0)
|
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);
|
avcodec_free_context(&s->codec);
|
||||||
return -1;
|
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
|
// 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());
|
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)
|
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)
|
if (ret < 0)
|
||||||
goto out_free_streams;
|
goto out_free_streams;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (ctx->settings.encode_video)
|
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)
|
if (ret < 0)
|
||||||
goto out_free_streams;
|
goto out_free_streams;
|
||||||
}
|
}
|
||||||
@ -972,6 +1010,8 @@ open_filter(struct stream_ctx *out_stream, struct stream_ctx *in_stream)
|
|||||||
goto out_fail;
|
goto out_fail;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
DPRINTF(E_DBG, L_XCODE, "Created 'in' filter: %s\n", args);
|
||||||
|
|
||||||
snprintf(args, sizeof(args),
|
snprintf(args, sizeof(args),
|
||||||
"sample_fmts=%s:sample_rates=%d:channel_layouts=0x%"PRIx64,
|
"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,
|
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;
|
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);
|
ret = avfilter_graph_create_filter(&buffersink_ctx, buffersink, "out", NULL, NULL, filter_graph);
|
||||||
if (ret < 0)
|
if (ret < 0)
|
||||||
{
|
{
|
||||||
@ -1122,7 +1164,7 @@ close_filters(struct encode_ctx *ctx)
|
|||||||
/* Setup */
|
/* Setup */
|
||||||
|
|
||||||
struct decode_ctx *
|
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;
|
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->duration = song_length;
|
||||||
ctx->data_kind = data_kind;
|
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;
|
goto fail_free;
|
||||||
|
|
||||||
return ctx;
|
return ctx;
|
||||||
@ -1146,20 +1188,52 @@ transcode_decode_setup(enum transcode_profile profile, enum data_kind data_kind,
|
|||||||
}
|
}
|
||||||
|
|
||||||
struct encode_ctx *
|
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;
|
struct encode_ctx *ctx;
|
||||||
|
int bps;
|
||||||
|
|
||||||
CHECK_NULL(L_XCODE, ctx = calloc(1, sizeof(struct encode_ctx)));
|
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->filt_frame = av_frame_alloc());
|
||||||
CHECK_NULL(L_XCODE, ctx->encoded_pkt = av_packet_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;
|
goto fail_free;
|
||||||
|
|
||||||
ctx->settings.width = width;
|
ctx->settings.width = width;
|
||||||
ctx->settings.height = height;
|
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)
|
if (ctx->settings.wavheader)
|
||||||
make_wav_header(ctx, src_ctx, est_size);
|
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;
|
goto fail_close;
|
||||||
|
|
||||||
if (ctx->settings.icy && src_ctx->data_kind == DATA_KIND_HTTP)
|
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;
|
return ctx;
|
||||||
|
|
||||||
@ -1184,20 +1261,20 @@ transcode_encode_setup(enum transcode_profile profile, struct decode_ctx *src_ct
|
|||||||
}
|
}
|
||||||
|
|
||||||
struct transcode_ctx *
|
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;
|
struct transcode_ctx *ctx;
|
||||||
|
|
||||||
CHECK_NULL(L_XCODE, ctx = calloc(1, sizeof(struct transcode_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)
|
if (!ctx->decode_ctx)
|
||||||
{
|
{
|
||||||
free(ctx);
|
free(ctx);
|
||||||
return NULL;
|
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)
|
if (!ctx->encode_ctx)
|
||||||
{
|
{
|
||||||
transcode_decode_cleanup(&ctx->decode_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 *
|
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;
|
struct decode_ctx *ctx;
|
||||||
AVCodec *decoder;
|
AVCodec *decoder;
|
||||||
int ret;
|
int ret;
|
||||||
|
|
||||||
CHECK_NULL(L_XCODE, ctx = calloc(1, sizeof(struct decode_ctx)));
|
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;
|
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
|
// 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
|
// the decode_ctx because transcode_encode_setup() gets info about the input
|
||||||
// through this structure (TODO dont' do that)
|
// through this structure (TODO dont' do that)
|
||||||
decoder = avcodec_find_decoder(ctx->settings.audio_codec);
|
decoder = avcodec_find_decoder(ctx->settings.audio_codec);
|
||||||
if (!decoder)
|
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;
|
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);
|
ret = avcodec_parameters_from_context(ctx->audio_stream.stream->codecpar, ctx->audio_stream.codec);
|
||||||
if (ret < 0)
|
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;
|
goto out_free_codec;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1373,6 +1458,9 @@ transcode_encode_cleanup(struct encode_ctx **ctx)
|
|||||||
void
|
void
|
||||||
transcode_cleanup(struct transcode_ctx **ctx)
|
transcode_cleanup(struct transcode_ctx **ctx)
|
||||||
{
|
{
|
||||||
|
if (!*ctx)
|
||||||
|
return;
|
||||||
|
|
||||||
transcode_encode_cleanup(&(*ctx)->encode_ctx);
|
transcode_encode_cleanup(&(*ctx)->encode_ctx);
|
||||||
transcode_decode_cleanup(&(*ctx)->decode_ctx);
|
transcode_decode_cleanup(&(*ctx)->decode_ctx);
|
||||||
free(*ctx);
|
free(*ctx);
|
||||||
@ -1383,7 +1471,7 @@ transcode_cleanup(struct transcode_ctx **ctx)
|
|||||||
/* Encoding, decoding and transcoding */
|
/* Encoding, decoding and transcoding */
|
||||||
|
|
||||||
int
|
int
|
||||||
transcode_decode(void **frame, struct decode_ctx *dec_ctx)
|
transcode_decode(transcode_frame **frame, struct decode_ctx *dec_ctx)
|
||||||
{
|
{
|
||||||
struct transcode_ctx ctx;
|
struct transcode_ctx ctx;
|
||||||
int ret;
|
int ret;
|
||||||
@ -1414,7 +1502,7 @@ transcode_decode(void **frame, struct decode_ctx *dec_ctx)
|
|||||||
|
|
||||||
// Filters and encodes
|
// Filters and encodes
|
||||||
int
|
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;
|
AVFrame *f = frame;
|
||||||
struct stream_ctx *s;
|
struct stream_ctx *s;
|
||||||
@ -1489,8 +1577,8 @@ transcode(struct evbuffer *evbuf, int *icy_timer, struct transcode_ctx *ctx, int
|
|||||||
return processed;
|
return processed;
|
||||||
}
|
}
|
||||||
|
|
||||||
void *
|
transcode_frame *
|
||||||
transcode_frame_new(enum transcode_profile profile, uint8_t *data, size_t size)
|
transcode_frame_new(void *data, size_t size, int nsamples, struct media_quality *quality)
|
||||||
{
|
{
|
||||||
AVFrame *f;
|
AVFrame *f;
|
||||||
int ret;
|
int ret;
|
||||||
@ -1502,19 +1590,28 @@ transcode_frame_new(enum transcode_profile profile, uint8_t *data, size_t size)
|
|||||||
return NULL;
|
return NULL;
|
||||||
}
|
}
|
||||||
|
|
||||||
f->nb_samples = size / 4;
|
f->format = bitdepth2format(quality->bits_per_sample);
|
||||||
f->format = AV_SAMPLE_FMT_S16;
|
if (f->format == AV_SAMPLE_FMT_NONE)
|
||||||
f->channel_layout = AV_CH_LAYOUT_STEREO;
|
{
|
||||||
|
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
|
#ifdef HAVE_FFMPEG
|
||||||
f->channels = 2;
|
f->channels = quality->channels;
|
||||||
#endif
|
#endif
|
||||||
f->pts = AV_NOPTS_VALUE;
|
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)
|
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);
|
av_frame_free(&f);
|
||||||
return NULL;
|
return NULL;
|
||||||
}
|
}
|
||||||
@ -1523,7 +1620,7 @@ transcode_frame_new(enum transcode_profile profile, uint8_t *data, size_t size)
|
|||||||
}
|
}
|
||||||
|
|
||||||
void
|
void
|
||||||
transcode_frame_free(void *frame)
|
transcode_frame_free(transcode_frame *frame)
|
||||||
{
|
{
|
||||||
AVFrame *f = frame;
|
AVFrame *f = frame;
|
||||||
|
|
||||||
@ -1645,6 +1742,29 @@ transcode_decode_query(struct decode_ctx *ctx, const char *query)
|
|||||||
return -1;
|
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 */
|
/* Metadata */
|
||||||
|
|
||||||
struct http_icy_metadata *
|
struct http_icy_metadata *
|
||||||
|
@ -5,15 +5,24 @@
|
|||||||
#include <event2/buffer.h>
|
#include <event2/buffer.h>
|
||||||
#include "db.h"
|
#include "db.h"
|
||||||
#include "http.h"
|
#include "http.h"
|
||||||
|
#include "misc.h"
|
||||||
|
|
||||||
enum transcode_profile
|
enum transcode_profile
|
||||||
{
|
{
|
||||||
// Transcodes the best audio stream into PCM16 (does not add wav header)
|
// Used for errors
|
||||||
XCODE_PCM16_NOHEADER,
|
XCODE_UNKNOWN = 0,
|
||||||
// Transcodes the best audio stream into PCM16 (with wav header)
|
// 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,
|
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
|
// Transcodes the best audio stream into MP3
|
||||||
XCODE_MP3,
|
XCODE_MP3,
|
||||||
|
// Transcodes the best audio stream into OPUS
|
||||||
|
XCODE_OPUS,
|
||||||
// Transcodes the best video stream into JPEG/PNG
|
// Transcodes the best video stream into JPEG/PNG
|
||||||
XCODE_JPEG,
|
XCODE_JPEG,
|
||||||
XCODE_PNG,
|
XCODE_PNG,
|
||||||
@ -21,20 +30,26 @@ enum transcode_profile
|
|||||||
|
|
||||||
struct decode_ctx;
|
struct decode_ctx;
|
||||||
struct encode_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
|
// Setting up
|
||||||
struct decode_ctx *
|
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 *
|
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 *
|
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 *
|
struct decode_ctx *
|
||||||
transcode_decode_setup_raw(void);
|
transcode_decode_setup_raw(enum transcode_profile profile, struct media_quality *quality);
|
||||||
|
|
||||||
int
|
int
|
||||||
transcode_needed(const char *user_agent, const char *client_codecs, char *file_codectype);
|
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
|
* @return Positive if OK, negative if error, 0 if EOF
|
||||||
*/
|
*/
|
||||||
int
|
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.
|
/* 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
|
* @return Bytes added if OK, negative if error
|
||||||
*/
|
*/
|
||||||
int
|
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.
|
/* 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);
|
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
|
/* 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 data Buffer with raw data
|
||||||
* @in size Size of buffer
|
* @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
|
* @return Opaque pointer to frame if OK, otherwise NULL
|
||||||
*/
|
*/
|
||||||
void *
|
transcode_frame *
|
||||||
transcode_frame_new(enum transcode_profile profile, uint8_t *data, size_t size);
|
transcode_frame_new(void *data, size_t size, int nsamples, struct media_quality *quality);
|
||||||
void
|
void
|
||||||
transcode_frame_free(void *frame);
|
transcode_frame_free(transcode_frame *frame);
|
||||||
|
|
||||||
/* Seek to the specified position - next transcode() will return this packet
|
/* Seek to the specified position - next transcode() will return this packet
|
||||||
*
|
*
|
||||||
@ -117,6 +134,16 @@ transcode_seek(struct transcode_ctx *ctx, int ms);
|
|||||||
int
|
int
|
||||||
transcode_decode_query(struct decode_ctx *ctx, const char *query);
|
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
|
// Metadata
|
||||||
struct http_icy_metadata *
|
struct http_icy_metadata *
|
||||||
transcode_metadata(struct transcode_ctx *ctx, int *changed);
|
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