diff --git a/configure.ac b/configure.ac index dfc3ca1b..9ec5eb10 100644 --- a/configure.ac +++ b/configure.ac @@ -210,7 +210,7 @@ AC_ARG_WITH([libav], [AS_HELP_STRING([--with-libav], [[LIBAV=-libav]], [[LIBAV=]]) dnl libav/ffmpeg requires many feature checks FORK_MODULES_CHECK([FORKED], [LIBAV], - [libavformat$LIBAV libavcodec$LIBAV libswscale$LIBAV libavutil$LIBAV libavfilter$LIBAV], + [libavformat$LIBAV libavcodec$LIBAV libavutil$LIBAV libavfilter$LIBAV], [av_init_packet], [libavcodec/avcodec.h], [dnl Checks for misc libav and ffmpeg API differences AC_MSG_CHECKING([whether libav libraries are ffmpeg]) @@ -231,37 +231,7 @@ FORK_MODULES_CHECK([FORKED], [LIBAV], [libavutil/avutil.h]) FORK_CHECK_DECLS([avformat_network_init], [libavformat/avformat.h]) - dnl Check if we have modern or legacy AV api - FORK_CHECK_DECLS([avcodec_send_packet, avcodec_parameters_from_context], - [libavcodec/avcodec.h], [[libav_modern_api=yes]], [[libav_modern_api=no]]) - dnl The below we need to know only if we are going to use the legacy AV api - FORK_CHECK_DECLS([av_buffersrc_add_frame_flags], - [libavfilter/buffersrc.h]) - FORK_CHECK_DECLS([av_buffersink_get_frame], - [libavfilter/buffersink.h]) - FORK_CHECK_DECLS([avfilter_graph_parse_ptr], - [libavfilter/avfilter.h]) - FORK_CHECK_DECLS([av_packet_unref], - [libavcodec/avcodec.h]) - FORK_CHECK_DECLS([av_packet_rescale_ts], - [libavcodec/avcodec.h]) - FORK_CHECK_DECLS([avformat_alloc_output_context2], - [libavformat/avformat.h]) - FORK_CHECK_DECLS([av_frame_alloc], - [libavutil/frame.h]) - FORK_CHECK_DECLS([av_frame_get_best_effort_timestamp], - [libavutil/frame.h]) - FORK_CHECK_DECLS([av_image_fill_arrays], - [libavutil/imgutils.h]) - FORK_CHECK_DECLS([av_image_get_buffer_size], - [libavutil/imgutils.h]) - AC_CHECK_HEADERS([libavutil/channel_layout.h libavutil/mathematics.h]) ]) -dnl Option to choose old ffmpeg/libav API even if modern api was found -FORK_ARG_DISABLE([use of ffmpeg/libav API with avcodec_send_packet() and family], - [avcodecsend], [USE_AVCODEC_SEND]) -AM_CONDITIONAL([COND_FFMPEG_LEGACY], - [[test "x$libav_modern_api" = "xno" || test "x$enable_avcodecsend" = "xno" ]]) AC_CHECK_SIZEOF([void *]) diff --git a/forked-daapd.conf.in b/forked-daapd.conf.in index f701a2fa..ecdeacbe 100644 --- a/forked-daapd.conf.in +++ b/forked-daapd.conf.in @@ -217,17 +217,23 @@ audio { # If not set, the value for "card" will be used. # mixer_device = "" - # Synchronization - # If your local audio is out of sync with AirPlay, you can adjust this - # value. Positive values correspond to moving local audio ahead, - # negative correspond to delaying it. The unit is samples, where is - # 44100 = 1 second. The offset must be between -44100 and 44100. -# offset = 0 + # Enable or disable audio resampling to keep local audio in sync with + # e.g. Airplay. This feature relies on accurate ALSA measurements of + # delay, and some devices don't provide that. If that is the case you + # are better off disabling the feature. +# sync_disable = false - # How often to check and correct for drift between ALSA and AirPlay. - # The value is an integer expressed in seconds. - # Clamped to the range 1..20. -# adjust_period_seconds = 10 + # Here you can adjust when local audio is started relative to other + # speakers, e.g. Airplay. Negative values correspond to moving local + # audio ahead, positive correspond to delaying it. The unit is + # milliseconds. The offset must be between -1000 and 1000 (+/- 1 sec). +# offset_ms = 0 + + # To calculate what and if resampling is required, local audio delay is + # measured each second. After a period the collected measurements are + # used to estimate drift and latency, which determines if corrections + # are required. This setting sets the length of that period in seconds. +# adjust_period_seconds = 100 } # Pipe output diff --git a/src/Makefile.am b/src/Makefile.am index c441591e..36bfea6f 100644 --- a/src/Makefile.am +++ b/src/Makefile.am @@ -47,12 +47,6 @@ if COND_LIBWEBSOCKETS LIBWEBSOCKETS_SRC=websocket.c websocket.h endif -if COND_FFMPEG_LEGACY -FFMPEG_SRC=transcode_legacy.c artwork_legacy.c ffmpeg-compat.h -else -FFMPEG_SRC=transcode.c artwork.c -endif - GPERF_FILES = \ daap_query.gperf \ rsp_query.gperf \ @@ -118,8 +112,8 @@ forked_daapd_SOURCES = main.c \ httpd_artworkapi.c httpd_artworkapi.h \ http.c http.h \ dmap_common.c dmap_common.h \ - transcode.h artwork.h \ - $(FFMPEG_SRC) \ + transcode.c transcode.h \ + artwork.c artwork.h \ misc.c misc.h \ misc_json.c misc_json.h \ rng.c rng.h \ @@ -131,6 +125,7 @@ forked_daapd_SOURCES = main.c \ input.h input.c \ inputs/file_http.c inputs/pipe.c \ outputs.h outputs.c \ + outputs/rtp_common.h outputs/rtp_common.c \ outputs/raop.c $(RAOP_VERIFICATION_SRC) \ outputs/streaming.c outputs/dummy.c outputs/fifo.c \ $(ALSA_SRC) $(PULSEAUDIO_SRC) $(CHROMECAST_SRC) \ diff --git a/src/artwork.c b/src/artwork.c index e0858d30..fe1267e5 100644 --- a/src/artwork.c +++ b/src/artwork.c @@ -408,7 +408,7 @@ artwork_get(struct evbuffer *evbuf, char *path, struct evbuffer *inbuf, int max_ DPRINTF(E_SPAM, L_ART, "Getting artwork (max destination width %d height %d)\n", max_w, max_h); - xcode_decode = transcode_decode_setup(XCODE_JPEG, DATA_KIND_FILE, path, inbuf, 0); // Covers XCODE_PNG too + xcode_decode = transcode_decode_setup(XCODE_JPEG, NULL, DATA_KIND_FILE, path, inbuf, 0); // Covers XCODE_PNG too if (!xcode_decode) { if (path) @@ -462,9 +462,9 @@ artwork_get(struct evbuffer *evbuf, char *path, struct evbuffer *inbuf, int max_ } if (format_ok == ART_FMT_JPEG) - xcode_encode = transcode_encode_setup(XCODE_JPEG, xcode_decode, NULL, target_w, target_h); + xcode_encode = transcode_encode_setup(XCODE_JPEG, NULL, xcode_decode, NULL, target_w, target_h); else - xcode_encode = transcode_encode_setup(XCODE_PNG, xcode_decode, NULL, target_w, target_h); + xcode_encode = transcode_encode_setup(XCODE_PNG, NULL, xcode_decode, NULL, target_w, target_h); if (!xcode_encode) { diff --git a/src/artwork_legacy.c b/src/artwork_legacy.c deleted file mode 100644 index 777ded57..00000000 --- a/src/artwork_legacy.c +++ /dev/null @@ -1,1630 +0,0 @@ -/* - * Copyright (C) 2015-2016 Espen Jürgensen - * Copyright (C) 2010-2011 Julien BLACHE - * - * 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 -#endif - -#include -#include -#include -#include -#include -#include -#include -#include - -#include -#include -#include -#include - -#include "db.h" -#include "misc.h" -#include "logger.h" -#include "conffile.h" -#include "cache.h" -#include "http.h" - -#include "avio_evbuffer.h" -#include "artwork.h" - -#ifdef HAVE_SPOTIFY_H -# include "spotify.h" -#endif - -#include "ffmpeg-compat.h" - -/* This artwork module will look for artwork by consulting a set of sources one - * at a time. A source is for instance the local library, the cache or a cover - * art database. For each source there is a handler function, which will do the - * actual work of getting the artwork. - * - * There are two types of handlers: item and group. Item handlers are capable of - * finding artwork for a single item (a dbmfi), while group handlers can get for - * an album or artist (a persistentid). - * - * An artwork source handler must return one of the following: - * - * ART_FMT_JPEG (positive) Found a jpeg - * ART_FMT_PNG (positive) Found a png - * ART_E_NONE (zero) No artwork found - * ART_E_ERROR (negative) An error occurred while searching for artwork - * ART_E_ABORT (negative) Caller should abort artwork search (may be returned by cache) - */ -#define ART_E_NONE 0 -#define ART_E_ERROR -1 -#define ART_E_ABORT -2 - -enum artwork_cache -{ - NEVER = 0, // No caching of any results - ON_SUCCESS = 1, // Cache if artwork found - ON_FAILURE = 2, // Cache if artwork not found (so we don't keep asking) -}; - -/* This struct contains the data available to the handler, as well as a char - * buffer where the handler should output the path to the artwork (if it is - * local - otherwise the buffer can be left empty). The purpose of supplying the - * path is that the filescanner can then clear the cache in case the file - * changes. - */ -struct artwork_ctx { - // Handler should output path here if artwork is local - char path[PATH_MAX]; - // Handler should output artwork data to this evbuffer - struct evbuffer *evbuf; - - // Input data to handler, requested width and height - int max_w; - int max_h; - // Input data to handler, did user configure to look for individual artwork - int individual; - - // Input data for item handlers - struct db_media_file_info *dbmfi; - int id; - // Input data for group handlers - int64_t persistentid; - - // Not to be used by handler - query for item or group - struct query_params qp; - // Not to be used by handler - should the result be cached - enum artwork_cache cache; -}; - -/* Definition of an artwork source. Covers both item and group sources. - */ -struct artwork_source { - // Name of the source, e.g. "cache" - const char *name; - - // The handler - int (*handler)(struct artwork_ctx *ctx); - - // What data_kinds the handler can work with, combined with (1 << A) | (1 << B) - int data_kinds; - - // When should results from the source be cached? - enum artwork_cache cache; -}; - -/* File extensions that we look for or accept - */ -static const char *cover_extension[] = - { - "jpg", "png", - }; - - -/* ----------------- DECLARE AND CONFIGURE SOURCE HANDLERS ----------------- */ - -/* Forward - group handlers */ -static int source_group_cache_get(struct artwork_ctx *ctx); -static int source_group_dir_get(struct artwork_ctx *ctx); -/* Forward - item handlers */ -static int source_item_cache_get(struct artwork_ctx *ctx); -static int source_item_embedded_get(struct artwork_ctx *ctx); -static int source_item_own_get(struct artwork_ctx *ctx); -static int source_item_stream_get(struct artwork_ctx *ctx); -static int source_item_spotify_get(struct artwork_ctx *ctx); -static int source_item_ownpl_get(struct artwork_ctx *ctx); - -/* List of sources that can provide artwork for a group (i.e. usually an album - * identified by a persistentid). The source handlers will be called in the - * order of this list. Must be terminated by a NULL struct. - */ -static struct artwork_source artwork_group_source[] = - { - { - .name = "cache", - .handler = source_group_cache_get, - .cache = ON_FAILURE, - }, - { - .name = "directory", - .handler = source_group_dir_get, - .cache = ON_SUCCESS | ON_FAILURE, - }, - { - .name = NULL, - .handler = NULL, - .cache = 0, - } - }; - -/* List of sources that can provide artwork for an item (a track characterized - * by a dbmfi). The source handlers will be called in the order of this list. - * The handler will only be called if the data_kind matches. Must be terminated - * by a NULL struct. - */ -static struct artwork_source artwork_item_source[] = - { - { - .name = "cache", - .handler = source_item_cache_get, - .data_kinds = (1 << DATA_KIND_FILE) | (1 << DATA_KIND_SPOTIFY), - .cache = ON_FAILURE, - }, - { - .name = "embedded", - .handler = source_item_embedded_get, - .data_kinds = (1 << DATA_KIND_FILE), - .cache = ON_SUCCESS | ON_FAILURE, - }, - { - .name = "own", - .handler = source_item_own_get, - .data_kinds = (1 << DATA_KIND_FILE), - .cache = ON_SUCCESS | ON_FAILURE, - }, - { - .name = "stream", - .handler = source_item_stream_get, - .data_kinds = (1 << DATA_KIND_HTTP), - .cache = NEVER, - }, - { - .name = "Spotify", - .handler = source_item_spotify_get, - .data_kinds = (1 << DATA_KIND_SPOTIFY), - .cache = ON_SUCCESS, - }, - { - .name = "playlist own", - .handler = source_item_ownpl_get, - .data_kinds = (1 << DATA_KIND_HTTP), - .cache = ON_SUCCESS | ON_FAILURE, - }, - { - .name = NULL, - .handler = NULL, - .data_kinds = 0, - .cache = 0, - } - }; - - - -/* -------------------------------- HELPERS -------------------------------- */ - -/* Reads an artwork file from the filesystem straight into an evbuf - * TODO Use evbuffer_add_file or evbuffer_read? - * - * @out evbuf Image data - * @in path Path to the artwork - * @return 0 on success, -1 on error - */ -static int -artwork_read(struct evbuffer *evbuf, char *path) -{ - uint8_t buf[4096]; - struct stat sb; - int fd; - int ret; - - fd = open(path, O_RDONLY); - if (fd < 0) - { - DPRINTF(E_WARN, L_ART, "Could not open artwork file '%s': %s\n", path, strerror(errno)); - - return -1; - } - - ret = fstat(fd, &sb); - if (ret < 0) - { - DPRINTF(E_WARN, L_ART, "Could not stat() artwork file '%s': %s\n", path, strerror(errno)); - - goto out_fail; - } - - ret = evbuffer_expand(evbuf, sb.st_size); - if (ret < 0) - { - DPRINTF(E_LOG, L_ART, "Out of memory for artwork\n"); - - goto out_fail; - } - - while ((ret = read(fd, buf, sizeof(buf))) > 0) - evbuffer_add(evbuf, buf, ret); - - close(fd); - - return 0; - - out_fail: - close(fd); - return -1; -} - -/* Will the source image fit inside requested size. If not, what size should it - * be rescaled to to maintain aspect ratio. - * - * @in src Image source - * @in max_w Requested width - * @in max_h Requested height - * @out target_w Rescaled width - * @out target_h Rescaled height - * @return 0 no rescaling needed, 1 rescaling needed - */ -static int -rescale_needed(AVCodecContext *src, int max_w, int max_h, int *target_w, int *target_h) -{ - DPRINTF(E_DBG, L_ART, "Original image dimensions: w %d h %d\n", src->width, src->height); - - *target_w = src->width; - *target_h = src->height; - - if ((src->width == 0) || (src->height == 0)) /* Unknown source size, can't rescale */ - return 0; - - if ((max_w <= 0) || (max_h <= 0)) /* No valid target dimensions, use original */ - return 0; - - if ((src->width <= max_w) && (src->height <= max_h)) /* Smaller than target */ - return 0; - - if (src->width * max_h > src->height * max_w) /* Wider aspect ratio than target */ - { - *target_w = max_w; - *target_h = (double)max_w * ((double)src->height / (double)src->width); - } - else /* Taller or equal aspect ratio */ - { - *target_w = (double)max_h * ((double)src->width / (double)src->height); - *target_h = max_h; - } - - DPRINTF(E_DBG, L_ART, "Raw destination width %d height %d\n", *target_w, *target_h); - - if ((*target_h > max_h) && (max_h > 0)) - *target_h = max_h; - - /* PNG prefers even row count */ - *target_w += *target_w % 2; - - if ((*target_w > max_w) && (max_w > 0)) - *target_w = max_w - (max_w % 2); - - DPRINTF(E_DBG, L_ART, "Destination width %d height %d\n", *target_w, *target_h); - - return 1; -} - -/* Rescale an image - * - * @out evbuf Rescaled image data - * @in src_ctx Image source - * @in s Index of stream containing image - * @in out_w Rescaled width - * @in out_h Rescaled height - * @return ART_FMT_* on success, -1 on error - */ -static int -artwork_rescale(struct evbuffer *evbuf, AVFormatContext *src_ctx, int s, int out_w, int out_h) -{ - uint8_t *buf; - - AVCodecContext *src; - - AVFormatContext *dst_ctx; - AVCodecContext *dst; - AVOutputFormat *dst_fmt; - AVStream *dst_st; - - AVCodec *img_decoder; - AVCodec *img_encoder; - - AVFrame *i_frame; - AVFrame *o_frame; - - struct SwsContext *swsctx; - - AVPacket pkt; - int have_frame; - int ret; - - src = src_ctx->streams[s]->codec; - - // Avoids threading issue in both ffmpeg and libav that prevents decoding embedded png's - src->thread_count = 1; - - img_decoder = avcodec_find_decoder(src->codec_id); - if (!img_decoder) - { - DPRINTF(E_LOG, L_ART, "No suitable decoder found for artwork %s\n", src_ctx->filename); - - return -1; - } - - ret = avcodec_open2(src, img_decoder, NULL); - if (ret < 0) - { - DPRINTF(E_LOG, L_ART, "Could not open codec for decoding: %s\n", strerror(AVUNERROR(ret))); - - return -1; - } - - if (src->pix_fmt < 0) - { - DPRINTF(E_LOG, L_ART, "Unknown pixel format for artwork %s\n", src_ctx->filename); - - ret = -1; - goto out_close_src; - } - - /* Set up output */ - dst_fmt = av_guess_format("image2", NULL, NULL); - if (!dst_fmt) - { - DPRINTF(E_LOG, L_ART, "ffmpeg image2 muxer not available\n"); - - ret = -1; - goto out_close_src; - } - - dst_fmt->video_codec = AV_CODEC_ID_NONE; - - /* Try to keep same codec if possible */ - if (src->codec_id == AV_CODEC_ID_PNG) - dst_fmt->video_codec = AV_CODEC_ID_PNG; - else if (src->codec_id == AV_CODEC_ID_MJPEG) - dst_fmt->video_codec = AV_CODEC_ID_MJPEG; - - /* If not possible, select new codec */ - if (dst_fmt->video_codec == AV_CODEC_ID_NONE) - { - dst_fmt->video_codec = AV_CODEC_ID_PNG; - } - - img_encoder = avcodec_find_encoder(dst_fmt->video_codec); - if (!img_encoder) - { - DPRINTF(E_LOG, L_ART, "No suitable encoder found for codec ID %d\n", dst_fmt->video_codec); - - ret = -1; - goto out_close_src; - } - - dst_ctx = avformat_alloc_context(); - if (!dst_ctx) - { - DPRINTF(E_LOG, L_ART, "Out of memory for format context\n"); - - ret = -1; - goto out_close_src; - } - - dst_ctx->oformat = dst_fmt; - - dst_fmt->flags &= ~AVFMT_NOFILE; - - dst_st = avformat_new_stream(dst_ctx, NULL); - if (!dst_st) - { - DPRINTF(E_LOG, L_ART, "Out of memory for new output stream\n"); - - ret = -1; - goto out_free_dst_ctx; - } - - dst = dst_st->codec; - - avcodec_get_context_defaults3(dst, NULL); - - if (dst_fmt->flags & AVFMT_GLOBALHEADER) - dst->flags |= CODEC_FLAG_GLOBAL_HEADER; - - dst->codec_id = dst_fmt->video_codec; - dst->codec_type = AVMEDIA_TYPE_VIDEO; - - dst->pix_fmt = avcodec_default_get_format(dst, img_encoder->pix_fmts); - if (dst->pix_fmt < 0) - { - DPRINTF(E_LOG, L_ART, "Could not determine best pixel format\n"); - - ret = -1; - goto out_free_dst_ctx; - } - - dst->time_base.num = 1; - dst->time_base.den = 25; - - dst->width = out_w; - dst->height = out_h; - - /* Open encoder */ - ret = avcodec_open2(dst, img_encoder, NULL); - if (ret < 0) - { - DPRINTF(E_LOG, L_ART, "Could not open codec for encoding: %s\n", strerror(AVUNERROR(ret))); - - ret = -1; - goto out_free_dst_ctx; - } - - i_frame = av_frame_alloc(); - o_frame = av_frame_alloc(); - if (!i_frame || !o_frame) - { - DPRINTF(E_LOG, L_ART, "Could not allocate input/output frame\n"); - - ret = -1; - goto out_free_frames; - } - - ret = av_image_get_buffer_size(dst->pix_fmt, src->width, src->height, 1); - - DPRINTF(E_DBG, L_ART, "Artwork buffer size: %d\n", ret); - - buf = (uint8_t *)av_malloc(ret); - if (!buf) - { - DPRINTF(E_LOG, L_ART, "Out of memory for artwork buffer\n"); - - ret = -1; - goto out_free_frames; - } - -#if HAVE_DECL_AV_IMAGE_FILL_ARRAYS - av_image_fill_arrays(o_frame->data, o_frame->linesize, buf, dst->pix_fmt, src->width, src->height, 1); -#else - avpicture_fill((AVPicture *)o_frame, buf, dst->pix_fmt, src->width, src->height); -#endif - - o_frame->height = dst->height; - o_frame->width = dst->width; - o_frame->format = dst->pix_fmt; - - swsctx = sws_getContext(src->width, src->height, src->pix_fmt, - dst->width, dst->height, dst->pix_fmt, - SWS_BICUBIC, NULL, NULL, NULL); - if (!swsctx) - { - DPRINTF(E_LOG, L_ART, "Could not get SWS context\n"); - - ret = -1; - goto out_free_buf; - } - - /* Get frame */ - have_frame = 0; - while (av_read_frame(src_ctx, &pkt) == 0) - { - if (pkt.stream_index != s) - { - av_packet_unref(&pkt); - continue; - } - - avcodec_decode_video2(src, i_frame, &have_frame, &pkt); - break; - } - - if (!have_frame) - { - DPRINTF(E_LOG, L_ART, "Could not decode artwork\n"); - - av_packet_unref(&pkt); - sws_freeContext(swsctx); - - ret = -1; - goto out_free_buf; - } - - /* Scale */ - sws_scale(swsctx, (const uint8_t * const *)i_frame->data, i_frame->linesize, 0, src->height, o_frame->data, o_frame->linesize); - - sws_freeContext(swsctx); - av_packet_unref(&pkt); - - /* Open output file */ - dst_ctx->pb = avio_output_evbuffer_open(evbuf); - if (!dst_ctx->pb) - { - DPRINTF(E_LOG, L_ART, "Could not open artwork destination buffer\n"); - - ret = -1; - goto out_free_buf; - } - - /* Encode frame */ - av_init_packet(&pkt); - pkt.data = NULL; - pkt.size = 0; - - ret = avcodec_encode_video2(dst, &pkt, o_frame, &have_frame); - if (ret < 0) - { - DPRINTF(E_LOG, L_ART, "Could not encode artwork\n"); - - ret = -1; - goto out_fclose_dst; - } - - ret = avformat_write_header(dst_ctx, NULL); - if (ret != 0) - { - DPRINTF(E_LOG, L_ART, "Could not write artwork header: %s\n", strerror(AVUNERROR(ret))); - - ret = -1; - goto out_fclose_dst; - } - - ret = av_interleaved_write_frame(dst_ctx, &pkt); - - if (ret != 0) - { - DPRINTF(E_LOG, L_ART, "Error writing artwork\n"); - - ret = -1; - goto out_fclose_dst; - } - - ret = av_write_trailer(dst_ctx); - if (ret != 0) - { - DPRINTF(E_LOG, L_ART, "Could not write artwork trailer: %s\n", strerror(AVUNERROR(ret))); - - ret = -1; - goto out_fclose_dst; - } - - switch (dst_fmt->video_codec) - { - case AV_CODEC_ID_PNG: - ret = ART_FMT_PNG; - break; - - case AV_CODEC_ID_MJPEG: - ret = ART_FMT_JPEG; - break; - - default: - DPRINTF(E_LOG, L_ART, "Unhandled rescale output format\n"); - ret = -1; - break; - } - - out_fclose_dst: - avio_evbuffer_close(dst_ctx->pb); - av_packet_unref(&pkt); - - out_free_buf: - av_free(buf); - - out_free_frames: - if (i_frame) - av_frame_free(&i_frame); - if (o_frame) - av_frame_free(&o_frame); - avcodec_close(dst); - - out_free_dst_ctx: - avformat_free_context(dst_ctx); - - out_close_src: - avcodec_close(src); - - return ret; -} - -/* Get an artwork file from the filesystem. Will rescale if needed. - * - * @out evbuf Image data - * @in path Path to the artwork - * @in max_w Requested width - * @in max_h Requested height - * @return ART_FMT_* on success, ART_E_ERROR on error - */ -static int -artwork_get(struct evbuffer *evbuf, char *path, int max_w, int max_h) -{ - AVFormatContext *src_ctx; - int s; - int target_w; - int target_h; - int format_ok; - int ret; - - DPRINTF(E_SPAM, L_ART, "Getting artwork (max destination width %d height %d)\n", max_w, max_h); - - src_ctx = NULL; - - ret = avformat_open_input(&src_ctx, path, NULL, NULL); - if (ret < 0) - { - DPRINTF(E_WARN, L_ART, "Cannot open artwork file '%s': %s\n", path, strerror(AVUNERROR(ret))); - - return ART_E_ERROR; - } - - ret = avformat_find_stream_info(src_ctx, NULL); - if (ret < 0) - { - DPRINTF(E_WARN, L_ART, "Cannot get stream info: %s\n", strerror(AVUNERROR(ret))); - - avformat_close_input(&src_ctx); - return ART_E_ERROR; - } - - format_ok = 0; - for (s = 0; s < src_ctx->nb_streams; s++) - { - if (src_ctx->streams[s]->codec->codec_id == AV_CODEC_ID_PNG) - { - format_ok = ART_FMT_PNG; - break; - } - else if (src_ctx->streams[s]->codec->codec_id == AV_CODEC_ID_MJPEG) - { - format_ok = ART_FMT_JPEG; - break; - } - } - - if (s == src_ctx->nb_streams) - { - DPRINTF(E_LOG, L_ART, "Artwork file '%s' not a PNG or JPEG file\n", path); - - avformat_close_input(&src_ctx); - return ART_E_ERROR; - } - - ret = rescale_needed(src_ctx->streams[s]->codec, max_w, max_h, &target_w, &target_h); - - /* Fastpath */ - if (!ret && format_ok) - { - ret = artwork_read(evbuf, path); - if (ret == 0) - ret = format_ok; - } - else - ret = artwork_rescale(evbuf, src_ctx, s, target_w, target_h); - - avformat_close_input(&src_ctx); - - if (ret < 0) - { - if (evbuffer_get_length(evbuf) > 0) - evbuffer_drain(evbuf, evbuffer_get_length(evbuf)); - - ret = ART_E_ERROR; - } - - return ret; -} - -/* Looks for an artwork file in a directory. Will rescale if needed. - * - * @out evbuf Image data - * @in dir Directory to search - * @in max_w Requested width - * @in max_h Requested height - * @out out_path Path to the artwork file if found, must be a char[PATH_MAX] buffer - * @return ART_FMT_* on success, ART_E_NONE on nothing found, ART_E_ERROR on error - */ -static int -artwork_get_dir_image(struct evbuffer *evbuf, char *dir, int max_w, int max_h, char *out_path) -{ - char path[PATH_MAX]; - char parentdir[PATH_MAX]; - int i; - int j; - int len; - int ret; - cfg_t *lib; - int nbasenames; - int nextensions; - char *ptr; - - ret = snprintf(path, sizeof(path), "%s", dir); - if ((ret < 0) || (ret >= sizeof(path))) - { - DPRINTF(E_LOG, L_ART, "Artwork path exceeds PATH_MAX (%s)\n", dir); - return ART_E_ERROR; - } - - len = strlen(path); - - lib = cfg_getsec(cfg, "library"); - nbasenames = cfg_size(lib, "artwork_basenames"); - - if (nbasenames == 0) - return ART_E_NONE; - - nextensions = sizeof(cover_extension) / sizeof(cover_extension[0]); - - for (i = 0; i < nbasenames; i++) - { - for (j = 0; j < nextensions; j++) - { - ret = snprintf(path + len, sizeof(path) - len, "/%s.%s", cfg_getnstr(lib, "artwork_basenames", i), cover_extension[j]); - if ((ret < 0) || (ret >= sizeof(path) - len)) - { - DPRINTF(E_LOG, L_ART, "Artwork path will exceed PATH_MAX (%s/%s)\n", dir, cfg_getnstr(lib, "artwork_basenames", i)); - continue; - } - - DPRINTF(E_SPAM, L_ART, "Trying directory artwork file %s\n", path); - - ret = access(path, F_OK); - if (ret < 0) - continue; - - // If artwork file exists (ret == 0), exit the loop - break; - } - - // In case the previous loop exited early, we found an existing artwork file and exit the outer loop - if (j < nextensions) - break; - } - - // If the loop for directory artwork did not exit early, look for parent directory artwork - if (i == nbasenames) - { - ptr = strrchr(path, '/'); - if (ptr) - *ptr = '\0'; - - ptr = strrchr(path, '/'); - if ((!ptr) || (strlen(ptr) <= 1)) - { - DPRINTF(E_LOG, L_ART, "Could not find parent dir name (%s)\n", path); - return ART_E_ERROR; - } - strcpy(parentdir, ptr + 1); - - len = strlen(path); - - for (i = 0; i < nextensions; i++) - { - ret = snprintf(path + len, sizeof(path) - len, "/%s.%s", parentdir, cover_extension[i]); - if ((ret < 0) || (ret >= sizeof(path) - len)) - { - DPRINTF(E_LOG, L_ART, "Artwork path will exceed PATH_MAX (%s)\n", parentdir); - continue; - } - - DPRINTF(E_SPAM, L_ART, "Trying parent directory artwork file %s\n", path); - - ret = access(path, F_OK); - if (ret < 0) - continue; - - break; - } - - if (i == nextensions) - return ART_E_NONE; - } - - snprintf(out_path, PATH_MAX, "%s", path); - - return artwork_get(evbuf, path, max_w, max_h); -} - - -/* ---------------------- SOURCE HANDLER IMPLEMENTATION -------------------- */ - -/* Looks in the cache for group artwork - */ -static int -source_group_cache_get(struct artwork_ctx *ctx) -{ - int format; - int cached; - int ret; - - ret = cache_artwork_get(CACHE_ARTWORK_GROUP, ctx->persistentid, ctx->max_w, ctx->max_h, &cached, &format, ctx->evbuf); - if (ret < 0) - return ART_E_ERROR; - - if (!cached) - return ART_E_NONE; - - if (!format) - return ART_E_ABORT; - - return format; -} - -/* Looks for cover files in a directory, so if dir is /foo/bar and the user has - * configured the cover file names "cover" and "artwork" it will look for - * /foo/bar/cover.{png,jpg}, /foo/bar/artwork.{png,jpg} and also - * /foo/bar/bar.{png,jpg} (so-called parentdir artwork) - */ -static int -source_group_dir_get(struct artwork_ctx *ctx) -{ - struct query_params qp; - char *dir; - int ret; - - /* Image is not in the artwork cache. Try directory artwork first */ - memset(&qp, 0, sizeof(struct query_params)); - - qp.type = Q_GROUP_DIRS; - qp.persistentid = ctx->persistentid; - - ret = db_query_start(&qp); - if (ret < 0) - { - DPRINTF(E_LOG, L_ART, "Could not start Q_GROUP_DIRS query\n"); - return ART_E_ERROR; - } - - while (((ret = db_query_fetch_string(&qp, &dir)) == 0) && (dir)) - { - /* The db query may return non-directories (eg if item is an internet stream or Spotify) */ - if (access(dir, F_OK) < 0) - continue; - - ret = artwork_get_dir_image(ctx->evbuf, dir, ctx->max_w, ctx->max_h, ctx->path); - if (ret > 0) - { - db_query_end(&qp); - return ret; - } - } - - db_query_end(&qp); - - if (ret < 0) - { - DPRINTF(E_LOG, L_ART, "Error fetching Q_GROUP_DIRS results\n"); - return ART_E_ERROR; - } - - return ART_E_NONE; -} - -/* Looks in the cache for item artwork. Only relevant if configured to look for - * individual artwork. - */ -static int -source_item_cache_get(struct artwork_ctx *ctx) -{ - int format; - int cached; - int ret; - - if (!ctx->individual) - return ART_E_NONE; - - ret = cache_artwork_get(CACHE_ARTWORK_INDIVIDUAL, ctx->id, ctx->max_w, ctx->max_h, &cached, &format, ctx->evbuf); - if (ret < 0) - return ART_E_ERROR; - - if (!cached) - return ART_E_NONE; - - if (!format) - return ART_E_ABORT; - - return format; -} - -/* Get an embedded artwork file from a media file. Will rescale if needed. - */ -static int -source_item_embedded_get(struct artwork_ctx *ctx) -{ - AVFormatContext *src_ctx; - AVStream *src_st; - int s; - int target_w; - int target_h; - int format; - int ret; - - DPRINTF(E_SPAM, L_ART, "Trying embedded artwork in %s\n", ctx->dbmfi->path); - - src_ctx = NULL; - - ret = avformat_open_input(&src_ctx, ctx->dbmfi->path, NULL, NULL); - if (ret < 0) - { - DPRINTF(E_WARN, L_ART, "Cannot open media file '%s': %s\n", ctx->dbmfi->path, strerror(AVUNERROR(ret))); - return ART_E_ERROR; - } - - ret = avformat_find_stream_info(src_ctx, NULL); - if (ret < 0) - { - DPRINTF(E_WARN, L_ART, "Cannot get stream info: %s\n", strerror(AVUNERROR(ret))); - avformat_close_input(&src_ctx); - return ART_E_ERROR; - } - - format = 0; - for (s = 0; s < src_ctx->nb_streams; s++) - { - if (src_ctx->streams[s]->disposition & AV_DISPOSITION_ATTACHED_PIC) - { - if (src_ctx->streams[s]->codec->codec_id == AV_CODEC_ID_PNG) - { - format = ART_FMT_PNG; - break; - } - else if (src_ctx->streams[s]->codec->codec_id == AV_CODEC_ID_MJPEG) - { - format = ART_FMT_JPEG; - break; - } - } - } - - if (s == src_ctx->nb_streams) - { - avformat_close_input(&src_ctx); - return ART_E_NONE; - } - - src_st = src_ctx->streams[s]; - - ret = rescale_needed(src_st->codec, ctx->max_w, ctx->max_h, &target_w, &target_h); - - /* Fastpath */ - if (!ret && format) - { - DPRINTF(E_SPAM, L_ART, "Artwork not too large, using original image\n"); - - ret = evbuffer_add(ctx->evbuf, src_st->attached_pic.data, src_st->attached_pic.size); - if (ret < 0) - DPRINTF(E_LOG, L_ART, "Could not add embedded image to event buffer\n"); - else - ret = format; - } - else - { - DPRINTF(E_SPAM, L_ART, "Artwork too large, rescaling image\n"); - - ret = artwork_rescale(ctx->evbuf, src_ctx, s, target_w, target_h); - } - - avformat_close_input(&src_ctx); - - if (ret < 0) - { - if (evbuffer_get_length(ctx->evbuf) > 0) - evbuffer_drain(ctx->evbuf, evbuffer_get_length(ctx->evbuf)); - - ret = ART_E_ERROR; - } - else - snprintf(ctx->path, sizeof(ctx->path), "%s", ctx->dbmfi->path); - - return ret; -} - -/* Looks for basename(in_path).{png,jpg}, so if in_path is /foo/bar.mp3 it - * will look for /foo/bar.png and /foo/bar.jpg - */ -static int -source_item_own_get(struct artwork_ctx *ctx) -{ - char path[PATH_MAX]; - char *ptr; - int len; - int nextensions; - int i; - int ret; - - ret = snprintf(path, sizeof(path), "%s", ctx->dbmfi->path); - if ((ret < 0) || (ret >= sizeof(path))) - { - DPRINTF(E_LOG, L_ART, "Artwork path exceeds PATH_MAX (%s)\n", ctx->dbmfi->path); - return ART_E_ERROR; - } - - ptr = strrchr(path, '.'); - if (ptr) - *ptr = '\0'; - - len = strlen(path); - - nextensions = sizeof(cover_extension) / sizeof(cover_extension[0]); - - for (i = 0; i < nextensions; i++) - { - ret = snprintf(path + len, sizeof(path) - len, ".%s", cover_extension[i]); - if ((ret < 0) || (ret >= sizeof(path) - len)) - { - DPRINTF(E_LOG, L_ART, "Artwork path will exceed PATH_MAX (%s)\n", ctx->dbmfi->path); - continue; - } - - DPRINTF(E_SPAM, L_ART, "Trying own artwork file %s\n", path); - - ret = access(path, F_OK); - if (ret < 0) - continue; - - break; - } - - if (i == nextensions) - return ART_E_NONE; - - snprintf(ctx->path, sizeof(ctx->path), "%s", path); - - return artwork_get(ctx->evbuf, path, ctx->max_w, ctx->max_h); -} - -/* - * Downloads the artwork pointed to by the ICY metadata tag in an internet radio - * stream (the StreamUrl tag). The path will be converted back to the id, which - * is given to the player. If the id is currently being played, and there is a - * valid ICY metadata artwork URL available, it will be returned to this - * function, which will then use the http client to get the artwork. Notice: No - * rescaling is done. - */ -static int -source_item_stream_get(struct artwork_ctx *ctx) -{ - struct http_client_ctx client; - struct db_queue_item *queue_item; - struct keyval *kv; - const char *content_type; - char *url; - char *ext; - int len; - int ret; - - DPRINTF(E_SPAM, L_ART, "Trying internet stream artwork in %s\n", ctx->dbmfi->path); - - ret = ART_E_NONE; - - queue_item = db_queue_fetch_byfileid(ctx->id); - if (!queue_item || !queue_item->artwork_url) - { - free_queue_item(queue_item, 0); - return ART_E_NONE; - } - - url = strdup(queue_item->artwork_url); - free_queue_item(queue_item, 0); - - len = strlen(url); - if ((len < 14) || (len > PATH_MAX)) // Can't be shorter than http://a/1.jpg - goto out_url; - - ext = strrchr(url, '.'); - if (!ext) - goto out_url; - if ((strcmp(ext, ".jpg") != 0) && (strcmp(ext, ".png") != 0)) - goto out_url; - - cache_artwork_read(ctx->evbuf, url, &ret); - if (ret > 0) - goto out_url; - - kv = keyval_alloc(); - if (!kv) - goto out_url; - - memset(&client, 0, sizeof(struct http_client_ctx)); - client.url = url; - client.input_headers = kv; - client.input_body = ctx->evbuf; - - if (http_client_request(&client) < 0) - goto out_kv; - - content_type = keyval_get(kv, "Content-Type"); - if (content_type && (strcmp(content_type, "image/jpeg") == 0)) - ret = ART_FMT_JPEG; - else if (content_type && (strcmp(content_type, "image/png") == 0)) - ret = ART_FMT_PNG; - - if (ret > 0) - { - DPRINTF(E_SPAM, L_ART, "Found internet stream artwork in %s (%s)\n", url, content_type); - cache_artwork_stash(ctx->evbuf, url, ret); - } - - out_kv: - keyval_clear(kv); - free(kv); - - out_url: - free(url); - - return ret; -} - -#ifdef HAVE_SPOTIFY_H -static int -source_item_spotify_get(struct artwork_ctx *ctx) -{ - AVFormatContext *src_ctx; - AVIOContext *avio; - AVInputFormat *ifmt; - struct evbuffer *raw; - struct evbuffer *evbuf; - int target_w; - int target_h; - int ret; - - raw = evbuffer_new(); - evbuf = evbuffer_new(); - if (!raw || !evbuf) - { - DPRINTF(E_LOG, L_ART, "Out of memory for Spotify evbuf\n"); - return ART_E_ERROR; - } - - ret = spotify_artwork_get(raw, ctx->dbmfi->path, ctx->max_w, ctx->max_h); - if (ret < 0) - { - DPRINTF(E_WARN, L_ART, "No artwork from Spotify for %s\n", ctx->dbmfi->path); - evbuffer_free(raw); - evbuffer_free(evbuf); - return ART_E_NONE; - } - - // Make a refbuf of raw for ffmpeg image size probing and possibly rescaling. - // We keep raw around in case rescaling is not necessary. -#ifdef HAVE_LIBEVENT2_OLD - uint8_t *buf = evbuffer_pullup(raw, -1); - if (!buf) - { - DPRINTF(E_LOG, L_ART, "Could not pullup raw artwork\n"); - goto out_free_evbuf; - } - - ret = evbuffer_add_reference(evbuf, buf, evbuffer_get_length(raw), NULL, NULL); -#else - ret = evbuffer_add_buffer_reference(evbuf, raw); -#endif - if (ret < 0) - { - DPRINTF(E_LOG, L_ART, "Could not copy/ref raw image for ffmpeg\n"); - goto out_free_evbuf; - } - - // Now evbuf will be processed by ffmpeg, since it probably needs to be rescaled - src_ctx = avformat_alloc_context(); - if (!src_ctx) - { - DPRINTF(E_LOG, L_ART, "Out of memory for source context\n"); - goto out_free_evbuf; - } - - avio = avio_input_evbuffer_open(evbuf); - if (!avio) - { - DPRINTF(E_LOG, L_ART, "Could not alloc input evbuffer\n"); - goto out_free_ctx; - } - - src_ctx->pb = avio; - - ifmt = av_find_input_format("mjpeg"); - if (!ifmt) - { - DPRINTF(E_LOG, L_ART, "Could not find mjpeg input format\n"); - goto out_close_avio; - } - - ret = avformat_open_input(&src_ctx, NULL, ifmt, NULL); - if (ret < 0) - { - DPRINTF(E_LOG, L_ART, "Could not open input\n"); - goto out_close_avio; - } - - ret = avformat_find_stream_info(src_ctx, NULL); - if (ret < 0) - { - DPRINTF(E_LOG, L_ART, "Could not find stream info\n"); - goto out_close_input; - } - - ret = rescale_needed(src_ctx->streams[0]->codec, ctx->max_w, ctx->max_h, &target_w, &target_h); - if (!ret) - ret = evbuffer_add_buffer(ctx->evbuf, raw); - else - ret = artwork_rescale(ctx->evbuf, src_ctx, 0, target_w, target_h); - if (ret < 0) - { - DPRINTF(E_LOG, L_ART, "Could not add or rescale image to output evbuf\n"); - goto out_close_input; - } - - avformat_close_input(&src_ctx); - avio_evbuffer_close(avio); - evbuffer_free(evbuf); - evbuffer_free(raw); - - return ART_FMT_JPEG; - - out_close_input: - avformat_close_input(&src_ctx); - out_close_avio: - avio_evbuffer_close(avio); - out_free_ctx: - if (src_ctx) - avformat_free_context(src_ctx); - out_free_evbuf: - evbuffer_free(evbuf); - evbuffer_free(raw); - - return ART_E_ERROR; - -} -#else -static int -source_item_spotify_get(struct artwork_ctx *ctx) -{ - return ART_E_ERROR; -} -#endif - -/* First looks of the mfi->path is in any playlist, and if so looks in the dir - * of the playlist file (m3u et al) to see if there is any artwork. So if the - * playlist is /foo/bar.m3u it will look for /foo/bar.png and /foo/bar.jpg. - */ -static int -source_item_ownpl_get(struct artwork_ctx *ctx) -{ - struct query_params qp; - struct db_playlist_info dbpli; - char filter[PATH_MAX + 64]; - char *mfi_path; - int format; - int ret; - - ret = snprintf(filter, sizeof(filter), "(filepath = '%s')", ctx->dbmfi->path); - if ((ret < 0) || (ret >= sizeof(filter))) - { - DPRINTF(E_LOG, L_ART, "Artwork path exceeds PATH_MAX (%s)\n", ctx->dbmfi->path); - return ART_E_ERROR; - } - - memset(&qp, 0, sizeof(struct query_params)); - qp.type = Q_FIND_PL; - qp.filter = filter; - - ret = db_query_start(&qp); - if (ret < 0) - { - DPRINTF(E_LOG, L_ART, "Could not start ownpl query\n"); - return ART_E_ERROR; - } - - mfi_path = ctx->dbmfi->path; - - format = ART_E_NONE; - while (((ret = db_query_fetch_pl(&qp, &dbpli, 0)) == 0) && (dbpli.id) && (format == ART_E_NONE)) - { - if (!dbpli.path) - continue; - - ctx->dbmfi->path = dbpli.path; - format = source_item_own_get(ctx); - } - - ctx->dbmfi->path = mfi_path; - - if ((ret < 0) || (format < 0)) - format = ART_E_ERROR; - - db_query_end(&qp); - - return format; -} - - -/* --------------------------- SOURCE PROCESSING --------------------------- */ - -static int -process_items(struct artwork_ctx *ctx, int item_mode) -{ - struct db_media_file_info dbmfi; - uint32_t data_kind; - int i; - int ret; - - ret = db_query_start(&ctx->qp); - if (ret < 0) - { - DPRINTF(E_LOG, L_ART, "Could not start query (type=%d)\n", ctx->qp.type); - ctx->cache = NEVER; - return -1; - } - - while (((ret = db_query_fetch_file(&ctx->qp, &dbmfi)) == 0) && (dbmfi.id)) - { - // Save the first songalbumid, might need it for process_group() if this search doesn't give anything - if (!ctx->persistentid) - safe_atoi64(dbmfi.songalbumid, &ctx->persistentid); - - if (item_mode && !ctx->individual) - goto no_artwork; - - ret = (safe_atoi32(dbmfi.id, &ctx->id) < 0) || - (safe_atou32(dbmfi.data_kind, &data_kind) < 0) || - (data_kind > 30); - if (ret) - { - DPRINTF(E_LOG, L_ART, "Error converting dbmfi id or data_kind to number\n"); - continue; - } - - for (i = 0; artwork_item_source[i].handler; i++) - { - if ((artwork_item_source[i].data_kinds & (1 << data_kind)) == 0) - continue; - - // If just one handler says we should not cache a negative result then we obey that - if ((artwork_item_source[i].cache & ON_FAILURE) == 0) - ctx->cache = NEVER; - - DPRINTF(E_SPAM, L_ART, "Checking item source '%s'\n", artwork_item_source[i].name); - - ctx->dbmfi = &dbmfi; - ret = artwork_item_source[i].handler(ctx); - ctx->dbmfi = NULL; - - if (ret > 0) - { - DPRINTF(E_DBG, L_ART, "Artwork for '%s' found in source '%s'\n", dbmfi.title, artwork_item_source[i].name); - ctx->cache = (artwork_item_source[i].cache & ON_SUCCESS); - db_query_end(&ctx->qp); - return ret; - } - else if (ret == ART_E_ABORT) - { - DPRINTF(E_DBG, L_ART, "Source '%s' stopped search for artwork for '%s'\n", artwork_item_source[i].name, dbmfi.title); - ctx->cache = NEVER; - break; - } - else if (ret == ART_E_ERROR) - { - DPRINTF(E_LOG, L_ART, "Source '%s' returned an error for '%s'\n", artwork_item_source[i].name, dbmfi.title); - ctx->cache = NEVER; - } - } - } - - if (ret < 0) - { - DPRINTF(E_LOG, L_ART, "Error fetching results\n"); - ctx->cache = NEVER; - } - - no_artwork: - db_query_end(&ctx->qp); - - return -1; -} - -static int -process_group(struct artwork_ctx *ctx) -{ - int i; - int ret; - - if (!ctx->persistentid) - { - DPRINTF(E_LOG, L_ART, "Bug! No persistentid in call to process_group()\n"); - ctx->cache = NEVER; - return -1; - } - - for (i = 0; artwork_group_source[i].handler; i++) - { - // If just one handler says we should not cache a negative result then we obey that - if ((artwork_group_source[i].cache & ON_FAILURE) == 0) - ctx->cache = NEVER; - - DPRINTF(E_SPAM, L_ART, "Checking group source '%s'\n", artwork_group_source[i].name); - - ret = artwork_group_source[i].handler(ctx); - if (ret > 0) - { - DPRINTF(E_DBG, L_ART, "Artwork for group %" PRIi64 " found in source '%s'\n", ctx->persistentid, artwork_group_source[i].name); - ctx->cache = (artwork_group_source[i].cache & ON_SUCCESS); - return ret; - } - else if (ret == ART_E_ABORT) - { - DPRINTF(E_DBG, L_ART, "Source '%s' stopped search for artwork for group %" PRIi64 "\n", artwork_group_source[i].name, ctx->persistentid); - ctx->cache = NEVER; - return -1; - } - else if (ret == ART_E_ERROR) - { - DPRINTF(E_LOG, L_ART, "Source '%s' returned an error for group %" PRIi64 "\n", artwork_group_source[i].name, ctx->persistentid); - ctx->cache = NEVER; - } - } - - ret = process_items(ctx, 0); - - return ret; -} - - -/* ------------------------------ ARTWORK API ------------------------------ */ - -int -artwork_get_item(struct evbuffer *evbuf, int id, int max_w, int max_h) -{ - struct artwork_ctx ctx; - char filter[32]; - int ret; - - DPRINTF(E_DBG, L_ART, "Artwork request for item %d\n", id); - - if (id == DB_MEDIA_FILE_NON_PERSISTENT_ID) - return -1; - - memset(&ctx, 0, sizeof(struct artwork_ctx)); - - ctx.qp.type = Q_ITEMS; - ctx.qp.filter = filter; - ctx.evbuf = evbuf; - ctx.max_w = max_w; - ctx.max_h = max_h; - ctx.cache = ON_FAILURE; - ctx.individual = cfg_getbool(cfg_getsec(cfg, "library"), "artwork_individual"); - - ret = snprintf(filter, sizeof(filter), "id = %d", id); - if ((ret < 0) || (ret >= sizeof(filter))) - { - DPRINTF(E_LOG, L_ART, "Could not build filter for file id %d; no artwork will be sent\n", id); - return -1; - } - - // Note: process_items will set ctx.persistentid for the following process_group() - // - and do nothing else if artwork_individual is not configured by user - ret = process_items(&ctx, 1); - if (ret > 0) - { - if (ctx.cache == ON_SUCCESS) - cache_artwork_add(CACHE_ARTWORK_INDIVIDUAL, id, max_w, max_h, ret, ctx.path, evbuf); - - return ret; - } - - ctx.qp.type = Q_GROUP_ITEMS; - ctx.qp.persistentid = ctx.persistentid; - - ret = process_group(&ctx); - if (ret > 0) - { - if (ctx.cache == ON_SUCCESS) - cache_artwork_add(CACHE_ARTWORK_GROUP, ctx.persistentid, max_w, max_h, ret, ctx.path, evbuf); - - return ret; - } - - DPRINTF(E_DBG, L_ART, "No artwork found for item %d\n", id); - - if (ctx.cache == ON_FAILURE) - cache_artwork_add(CACHE_ARTWORK_GROUP, ctx.persistentid, max_w, max_h, 0, "", evbuf); - - return -1; -} - -int -artwork_get_group(struct evbuffer *evbuf, int id, int max_w, int max_h) -{ - struct artwork_ctx ctx; - int ret; - - DPRINTF(E_DBG, L_ART, "Artwork request for group %d\n", id); - - memset(&ctx, 0, sizeof(struct artwork_ctx)); - - /* Get the persistent id for the given group id */ - ret = db_group_persistentid_byid(id, &ctx.persistentid); - if (ret < 0) - { - DPRINTF(E_LOG, L_ART, "Error fetching persistent id for group id %d\n", id); - return -1; - } - - ctx.qp.type = Q_GROUP_ITEMS; - ctx.qp.persistentid = ctx.persistentid; - ctx.evbuf = evbuf; - ctx.max_w = max_w; - ctx.max_h = max_h; - ctx.cache = ON_FAILURE; - ctx.individual = cfg_getbool(cfg_getsec(cfg, "library"), "artwork_individual"); - - ret = process_group(&ctx); - if (ret > 0) - { - if (ctx.cache == ON_SUCCESS) - cache_artwork_add(CACHE_ARTWORK_GROUP, ctx.persistentid, max_w, max_h, ret, ctx.path, evbuf); - - return ret; - } - - DPRINTF(E_DBG, L_ART, "No artwork found for group %d\n", id); - - if (ctx.cache == ON_FAILURE) - cache_artwork_add(CACHE_ARTWORK_GROUP, ctx.persistentid, max_w, max_h, 0, "", evbuf); - - return -1; -} - -/* Checks if the file is an artwork file */ -int -artwork_file_is_artwork(const char *filename) -{ - cfg_t *lib; - int n; - int i; - int j; - int ret; - char artwork[PATH_MAX]; - - lib = cfg_getsec(cfg, "library"); - n = cfg_size(lib, "artwork_basenames"); - - for (i = 0; i < n; i++) - { - for (j = 0; j < (sizeof(cover_extension) / sizeof(cover_extension[0])); j++) - { - ret = snprintf(artwork, sizeof(artwork), "%s.%s", cfg_getnstr(lib, "artwork_basenames", i), cover_extension[j]); - if ((ret < 0) || (ret >= sizeof(artwork))) - { - DPRINTF(E_INFO, L_ART, "Artwork path exceeds PATH_MAX (%s.%s)\n", cfg_getnstr(lib, "artwork_basenames", i), cover_extension[j]); - continue; - } - - if (strcmp(artwork, filename) == 0) - return 1; - } - - if (j < (sizeof(cover_extension) / sizeof(cover_extension[0]))) - break; - } - - return 0; -} diff --git a/src/conffile.c b/src/conffile.c index a0afe934..42a26040 100644 --- a/src/conffile.c +++ b/src/conffile.c @@ -100,6 +100,8 @@ static cfg_opt_t sec_library[] = CFG_STR_LIST("no_decode", NULL, CFGF_NONE), CFG_STR_LIST("force_decode", NULL, CFGF_NONE), CFG_BOOL("pipe_autostart", cfg_true, CFGF_NONE), + CFG_INT("pipe_sample_rate", 44100, CFGF_NONE), + CFG_INT("pipe_bits_per_sample", 16, CFGF_NONE), CFG_BOOL("rating_updates", cfg_false, CFGF_NONE), CFG_END() }; @@ -113,8 +115,10 @@ static cfg_opt_t sec_audio[] = CFG_STR("card", "default", CFGF_NONE), CFG_STR("mixer", NULL, CFGF_NONE), CFG_STR("mixer_device", NULL, CFGF_NONE), - CFG_INT("offset", 0, CFGF_NONE), - CFG_INT("adjust_period_seconds", 10, CFGF_NONE), + CFG_BOOL("sync_disable", cfg_false, CFGF_NONE), + CFG_INT("offset", 0, CFGF_NONE), // deprecated + CFG_INT("offset_ms", 0, CFGF_NONE), + CFG_INT("adjust_period_seconds", 100, CFGF_NONE), CFG_END() }; diff --git a/src/ffmpeg-compat.h b/src/ffmpeg-compat.h deleted file mode 100644 index 88d4868b..00000000 --- a/src/ffmpeg-compat.h +++ /dev/null @@ -1,105 +0,0 @@ -#ifndef __FFMPEG_COMPAT_H__ -#define __FFMPEG_COMPAT_H__ - -#ifdef HAVE_LIBAVUTIL_CHANNEL_LAYOUT_H -# include -#endif - -#ifdef HAVE_LIBAVUTIL_MATHEMATICS_H -# include -#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 - -__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__ */ diff --git a/src/httpd.c b/src/httpd.c index 5a9fe26d..fbc73443 100644 --- a/src/httpd.c +++ b/src/httpd.c @@ -80,6 +80,11 @@ "

%s

\n" \ "\n\n" +#define HTTPD_STREAM_SAMPLE_RATE 44100 +#define HTTPD_STREAM_BPS 16 +#define HTTPD_STREAM_CHANNELS 2 + + struct content_type_map { char *ext; char *ctype; @@ -1029,6 +1034,7 @@ httpd_request_parse(struct evhttp_request *req, struct httpd_uri_parsed *uri_par void httpd_stream_file(struct evhttp_request *req, int id) { + struct media_quality quality = { HTTPD_STREAM_SAMPLE_RATE, HTTPD_STREAM_BPS, HTTPD_STREAM_CHANNELS }; struct media_file_info *mfi; struct stream_ctx *st; void (*stream_cb)(int fd, short event, void *arg); @@ -1128,7 +1134,7 @@ httpd_stream_file(struct evhttp_request *req, int id) stream_cb = stream_chunk_xcode_cb; - st->xcode = transcode_setup(XCODE_PCM16_HEADER, mfi->data_kind, mfi->path, mfi->song_length, &st->size); + st->xcode = transcode_setup(XCODE_PCM16_HEADER, &quality, mfi->data_kind, mfi->path, mfi->song_length, &st->size); if (!st->xcode) { DPRINTF(E_WARN, L_HTTPD, "Transcoding setup failed, aborting streaming\n"); diff --git a/src/httpd_artworkapi.c b/src/httpd_artworkapi.c index df6d0827..d7198b1a 100644 --- a/src/httpd_artworkapi.c +++ b/src/httpd_artworkapi.c @@ -88,7 +88,7 @@ artworkapi_reply_nowplaying(struct httpd_request *hreq) if (ret != 0) return ret; - ret = player_now_playing(&id); + ret = player_playing_now(&id); if (ret != 0) return HTTP_NOTFOUND; diff --git a/src/httpd_dacp.c b/src/httpd_dacp.c index d8dde6b6..8364e9c6 100644 --- a/src/httpd_dacp.c +++ b/src/httpd_dacp.c @@ -1083,7 +1083,7 @@ dacp_propset_userrating(const char *value, struct httpd_request *hreq) { DPRINTF(E_WARN, L_DACP, "Invalid id %d for rating, defaulting to player id\n", itemid); - ret = player_now_playing(&itemid); + ret = player_playing_now(&itemid); if (ret < 0) { DPRINTF(E_WARN, L_DACP, "Could not find an id for rating\n"); @@ -2277,7 +2277,7 @@ dacp_reply_nowplayingartwork(struct httpd_request *hreq) goto error; } - ret = player_now_playing(&id); + ret = player_playing_now(&id); if (ret < 0) goto no_artwork; diff --git a/src/httpd_streaming.c b/src/httpd_streaming.c index 547b624d..0e7980e8 100644 --- a/src/httpd_streaming.c +++ b/src/httpd_streaming.c @@ -44,8 +44,13 @@ extern struct event_base *evbase_httpd; // Seconds between sending silence when player is idle // (to prevent client from hanging up) #define STREAMING_SILENCE_INTERVAL 1 -// Buffer size for transmitting from player to httpd thread -#define STREAMING_RAWBUF_SIZE (STOB(AIRTUNES_V2_PACKET_SAMPLES)) +// How many bytes we try to read at a time from the httpd pipe +#define STREAMING_READ_SIZE STOB(352, 16, 2) + +#define STREAMING_MP3_SAMPLE_RATE 44100 +#define STREAMING_MP3_BPS 16 +#define STREAMING_MP3_CHANNELS 2 + // Linked list of mp3 streaming requests struct streaming_session { @@ -54,23 +59,24 @@ struct streaming_session { }; static struct streaming_session *streaming_sessions; -static int streaming_initialized; +// Means we're not able to encode to mp3 +static bool streaming_not_supported; -// Buffers and interval for sending silence when playback is paused -static uint8_t *streaming_silence_data; -static size_t streaming_silence_size; +// Interval for sending silence when playback is paused static struct timeval streaming_silence_tv = { STREAMING_SILENCE_INTERVAL, 0 }; // Input buffer, output buffer and encoding ctx for transcode -static uint8_t streaming_rawbuf[STREAMING_RAWBUF_SIZE]; static struct encode_ctx *streaming_encode_ctx; static struct evbuffer *streaming_encoded_data; +static struct media_quality streaming_quality; // Used for pushing events and data from the player static struct event *streamingev; +static struct event *metaev; static struct player_status streaming_player_status; static int streaming_player_changed; static int streaming_pipe[2]; +static int streaming_meta[2]; static void @@ -111,15 +117,110 @@ streaming_fail_cb(struct evhttp_connection *evcon, void *arg) { DPRINTF(E_INFO, L_STREAMING, "No more clients, will stop streaming\n"); event_del(streamingev); + event_del(metaev); } } +static void +streaming_end(void) +{ + struct streaming_session *session; + struct evhttp_connection *evcon; + + for (session = streaming_sessions; streaming_sessions; session = streaming_sessions) + { + evcon = evhttp_request_get_connection(session->req); + if (evcon) + evhttp_connection_set_closecb(evcon, NULL, NULL); + evhttp_send_reply_end(session->req); + + streaming_sessions = session->next; + free(session); + } + + event_del(streamingev); + event_del(metaev); +} + +static void +streaming_meta_cb(evutil_socket_t fd, short event, void *arg) +{ + struct media_quality mp3_quality = { STREAMING_MP3_SAMPLE_RATE, STREAMING_MP3_BPS, STREAMING_MP3_CHANNELS }; + struct media_quality quality; + struct decode_ctx *decode_ctx; + int ret; + + transcode_encode_cleanup(&streaming_encode_ctx); + + ret = read(fd, &quality, sizeof(struct media_quality)); + if (ret != sizeof(struct media_quality)) + goto error; + + decode_ctx = NULL; + if (quality.bits_per_sample == 16) + decode_ctx = transcode_decode_setup_raw(XCODE_PCM16, &quality); + else if (quality.bits_per_sample == 24) + decode_ctx = transcode_decode_setup_raw(XCODE_PCM24, &quality); + else if (quality.bits_per_sample == 32) + decode_ctx = transcode_decode_setup_raw(XCODE_PCM32, &quality); + + if (!decode_ctx) + goto error; + + streaming_encode_ctx = transcode_encode_setup(XCODE_MP3, &mp3_quality, decode_ctx, NULL, 0, 0); + transcode_decode_cleanup(&decode_ctx); + if (!streaming_encode_ctx) + { + DPRINTF(E_LOG, L_STREAMING, "Will not be able to stream MP3, libav does not support MP3 encoding\n"); + streaming_not_supported = 1; + return; + } + + streaming_quality = quality; + streaming_not_supported = 0; + + return; + + error: + DPRINTF(E_LOG, L_STREAMING, "Unknown or unsupported quality of input data (%d/%d/%d), cannot MP3 encode\n", quality.sample_rate, quality.bits_per_sample, quality.channels); + streaming_not_supported = 1; + streaming_end(); +} + +static int +encode_buffer(uint8_t *buffer, size_t size) +{ + transcode_frame *frame; + int samples; + int ret; + + if (streaming_not_supported || streaming_quality.channels == 0) + { + DPRINTF(E_LOG, L_STREAMING, "Streaming unsuppored or quality is zero\n"); + return -1; + } + + samples = BTOS(size, streaming_quality.bits_per_sample, streaming_quality.channels); + + frame = transcode_frame_new(buffer, size, samples, &streaming_quality); + if (!frame) + { + DPRINTF(E_LOG, L_STREAMING, "Could not convert raw PCM to frame\n"); + return -1; + } + + ret = transcode_encode(streaming_encoded_data, streaming_encode_ctx, frame, 0); + transcode_frame_free(frame); + + return ret; +} + static void streaming_send_cb(evutil_socket_t fd, short event, void *arg) { struct streaming_session *session; struct evbuffer *evbuf; - void *frame; + uint8_t rawbuf[STREAMING_READ_SIZE]; uint8_t *buf; int len; int ret; @@ -127,24 +228,16 @@ streaming_send_cb(evutil_socket_t fd, short event, void *arg) // Player wrote data to the pipe (EV_READ) if (event & EV_READ) { - ret = read(streaming_pipe[0], &streaming_rawbuf, STREAMING_RAWBUF_SIZE); - if (ret < 0) - return; - - if (!streaming_sessions) - return; - - frame = transcode_frame_new(XCODE_MP3, streaming_rawbuf, STREAMING_RAWBUF_SIZE); - if (!frame) + while (1) { - DPRINTF(E_LOG, L_STREAMING, "Could not convert raw PCM to frame\n"); - return; - } + ret = read(fd, &rawbuf, sizeof(rawbuf)); + if (ret <= 0) + break; - ret = transcode_encode(streaming_encoded_data, streaming_encode_ctx, frame, 0); - transcode_frame_free(frame); - if (ret < 0) - return; + ret = encode_buffer(rawbuf, ret); + if (ret < 0) + return; + } } // Event timed out, let's see what the player is doing and send silence if it is paused else @@ -155,16 +248,18 @@ streaming_send_cb(evutil_socket_t fd, short event, void *arg) player_get_status(&streaming_player_status); } - if (!streaming_sessions) - return; - if (streaming_player_status.status != PLAY_PAUSED) return; - evbuffer_add(streaming_encoded_data, streaming_silence_data, streaming_silence_size); + memset(&rawbuf, 0, sizeof(rawbuf)); + ret = encode_buffer(rawbuf, sizeof(rawbuf)); + if (ret < 0) + return; } len = evbuffer_get_length(streaming_encoded_data); + if (len == 0) + return; // Send data evbuf = evbuffer_new(); @@ -179,6 +274,7 @@ streaming_send_cb(evutil_socket_t fd, short event, void *arg) else evhttp_send_reply_chunk(session->req, streaming_encoded_data); } + evbuffer_free(evbuf); } @@ -191,14 +287,24 @@ player_change_cb(short event_mask) // Thread: player (also prone to race conditions, mostly during deinit) void -streaming_write(uint8_t *buf, uint64_t rtptime) +streaming_write(struct output_buffer *obuf) { int ret; if (!streaming_sessions) return; - ret = write(streaming_pipe[1], buf, STREAMING_RAWBUF_SIZE); + if (!quality_is_equal(&obuf->data[0].quality, &streaming_quality)) + { + ret = write(streaming_meta[1], &obuf->data[0].quality, sizeof(struct media_quality)); + if (ret < 0) + { + DPRINTF(E_LOG, L_STREAMING, "Error writing to streaming pipe: %s\n", strerror(errno)); + return; + } + } + + ret = write(streaming_pipe[1], obuf->data[0].buffer, obuf->data[0].bufsize); if (ret < 0) { if (errno == EAGAIN) @@ -219,9 +325,9 @@ streaming_request(struct evhttp_request *req, struct httpd_uri_parsed *uri_parse char *address; ev_uint16_t port; - if (!streaming_initialized) + if (streaming_not_supported) { - DPRINTF(E_LOG, L_STREAMING, "Got mp3 streaming request, but cannot encode to mp3\n"); + DPRINTF(E_LOG, L_STREAMING, "Got MP3 streaming request, but cannot encode to MP3\n"); evhttp_send_error(req, HTTP_NOTFOUND, "Not Found"); return -1; @@ -258,7 +364,10 @@ streaming_request(struct evhttp_request *req, struct httpd_uri_parsed *uri_parse } if (!streaming_sessions) - event_add(streamingev, &streaming_silence_tv); + { + event_add(streamingev, &streaming_silence_tv); + event_add(metaev, NULL); + } session->req = req; session->next = streaming_sessions; @@ -284,26 +393,8 @@ streaming_is_request(const char *path) int streaming_init(void) { - struct decode_ctx *decode_ctx; - void *frame; - int remaining; int ret; - decode_ctx = transcode_decode_setup_raw(); - if (!decode_ctx) - { - DPRINTF(E_LOG, L_STREAMING, "Could not create decoding context\n"); - return -1; - } - - streaming_encode_ctx = transcode_encode_setup(XCODE_MP3, decode_ctx, NULL, 0, 0); - transcode_decode_cleanup(&decode_ctx); - if (!streaming_encode_ctx) - { - DPRINTF(E_LOG, L_STREAMING, "Will not be able to stream mp3, libav does not support mp3 encoding\n"); - return -1; - } - // Non-blocking because otherwise httpd and player thread may deadlock #ifdef HAVE_PIPE2 ret = pipe2(streaming_pipe, O_CLOEXEC | O_NONBLOCK); @@ -318,7 +409,23 @@ streaming_init(void) if (ret < 0) { DPRINTF(E_FATAL, L_STREAMING, "Could not create pipe: %s\n", strerror(errno)); - goto pipe_fail; + goto error; + } + +#ifdef HAVE_PIPE2 + ret = pipe2(streaming_meta, O_CLOEXEC | O_NONBLOCK); +#else + if ( pipe(streaming_meta) < 0 || + fcntl(streaming_meta[0], F_SETFL, O_CLOEXEC | O_NONBLOCK) < 0 || + fcntl(streaming_meta[1], F_SETFL, O_CLOEXEC | O_NONBLOCK) < 0 ) + ret = -1; + else + ret = 0; +#endif + if (ret < 0) + { + DPRINTF(E_FATAL, L_STREAMING, "Could not create pipe: %s\n", strerror(errno)); + goto error; } // Listen to playback changes so we don't have to poll to check for pausing @@ -326,77 +433,22 @@ streaming_init(void) if (ret < 0) { DPRINTF(E_FATAL, L_STREAMING, "Could not add listener\n"); - goto listener_fail; + goto error; } // Initialize buffer for encoded mp3 audio and event for pipe reading - streaming_encoded_data = evbuffer_new(); - streamingev = event_new(evbase_httpd, streaming_pipe[0], EV_TIMEOUT | EV_READ | EV_PERSIST, streaming_send_cb, NULL); - if (!streaming_encoded_data || !streamingev) - { - DPRINTF(E_LOG, L_STREAMING, "Out of memory for encoded_data or event\n"); - goto event_fail; - } + CHECK_NULL(L_STREAMING, streaming_encoded_data = evbuffer_new()); - // Encode some silence which will be used for playback pause and put in a permanent buffer - remaining = STREAMING_SILENCE_INTERVAL * STOB(44100); - while (remaining > STREAMING_RAWBUF_SIZE) - { - frame = transcode_frame_new(XCODE_MP3, streaming_rawbuf, STREAMING_RAWBUF_SIZE); - if (!frame) - { - DPRINTF(E_LOG, L_STREAMING, "Could not convert raw PCM to frame\n"); - goto silence_fail; - } - - ret = transcode_encode(streaming_encoded_data, streaming_encode_ctx, frame, 0); - transcode_frame_free(frame); - if (ret < 0) - { - DPRINTF(E_LOG, L_STREAMING, "Could not encode silence buffer\n"); - goto silence_fail; - } - - remaining -= STREAMING_RAWBUF_SIZE; - } - - streaming_silence_size = evbuffer_get_length(streaming_encoded_data); - if (streaming_silence_size == 0) - { - DPRINTF(E_LOG, L_STREAMING, "The encoder didn't encode any silence\n"); - goto silence_fail; - } - - streaming_silence_data = malloc(streaming_silence_size); - if (!streaming_silence_data) - { - DPRINTF(E_LOG, L_STREAMING, "Out of memory for streaming_silence_data\n"); - goto silence_fail; - } - - ret = evbuffer_remove(streaming_encoded_data, streaming_silence_data, streaming_silence_size); - if (ret != streaming_silence_size) - { - DPRINTF(E_LOG, L_STREAMING, "Unknown error while copying silence buffer\n"); - free(streaming_silence_data); - goto silence_fail; - } - - // All done - streaming_initialized = 1; + CHECK_NULL(L_STREAMING, streamingev = event_new(evbase_httpd, streaming_pipe[0], EV_TIMEOUT | EV_READ | EV_PERSIST, streaming_send_cb, NULL)); + CHECK_NULL(L_STREAMING, metaev = event_new(evbase_httpd, streaming_meta[0], EV_READ | EV_PERSIST, streaming_meta_cb, NULL)); return 0; - silence_fail: - event_free(streamingev); - evbuffer_free(streaming_encoded_data); - event_fail: - listener_remove(player_change_cb); - listener_fail: + error: close(streaming_pipe[0]); close(streaming_pipe[1]); - pipe_fail: - transcode_encode_cleanup(&streaming_encode_ctx); + close(streaming_meta[0]); + close(streaming_meta[1]); return -1; } @@ -407,9 +459,6 @@ streaming_deinit(void) struct streaming_session *session; struct streaming_session *next; - if (!streaming_initialized) - return; - session = streaming_sessions; streaming_sessions = NULL; // Stops writing and sending @@ -428,8 +477,9 @@ streaming_deinit(void) close(streaming_pipe[0]); close(streaming_pipe[1]); + close(streaming_meta[0]); + close(streaming_meta[1]); transcode_encode_cleanup(&streaming_encode_ctx); evbuffer_free(streaming_encoded_data); - free(streaming_silence_data); } diff --git a/src/httpd_streaming.h b/src/httpd_streaming.h index fed2838a..3df82023 100644 --- a/src/httpd_streaming.h +++ b/src/httpd_streaming.h @@ -3,6 +3,7 @@ #define __HTTPD_STREAMING_H__ #include "httpd.h" +#include "outputs.h" /* httpd_streaming takes care of incoming requests to /stream.mp3 * It will receive decoded audio from the player, and encode it, and @@ -11,7 +12,7 @@ */ void -streaming_write(uint8_t *buf, uint64_t rtptime); +streaming_write(struct output_buffer *obuf); int streaming_request(struct evhttp_request *req, struct httpd_uri_parsed *uri_parsed); diff --git a/src/input.c b/src/input.c index 8c8dd3d5..aa062318 100644 --- a/src/input.c +++ b/src/input.c @@ -22,6 +22,7 @@ #include #include +#include #include #include #include @@ -37,14 +38,18 @@ #include "misc.h" #include "logger.h" +#include "commands.h" #include "input.h" -// Disallow further writes to the buffer when its size is larger than this threshold -#define INPUT_BUFFER_THRESHOLD STOB(88200) -// How long (in sec) to wait for player read before looping in playback thread +// Disallow further writes to the buffer when its size exceeds this threshold. +// The below gives us room to buffer 2 seconds of 48000/16/2 audio. +#define INPUT_BUFFER_THRESHOLD STOB(96000, 16, 2) +// How long (in sec) to wait for player read before looping #define INPUT_LOOP_TIMEOUT 1 +// How long (in sec) to keep an input open without the player reading from it +#define INPUT_OPEN_TIMEOUT 600 -#define DEBUG 1 //TODO disable +//#define DEBUG_INPUT 1 extern struct input_definition input_file; extern struct input_definition input_http; @@ -64,83 +69,82 @@ static struct input_definition *inputs[] = { NULL }; +struct marker +{ + // Position of marker measured in bytes + uint64_t pos; + + // Type of marker + enum input_flags flag; + + // Data associated with the marker, e.g. quality or metadata struct + void *data; + + // Reverse linked list, yay! + struct marker *prev; +}; + struct input_buffer { // Raw pcm stream data struct evbuffer *evbuf; - // If non-zero, remaining length of buffer until EOF - size_t eof; - // If non-zero, remaining length of buffer until read error occurred - size_t error; - // If non-zero, remaining length of buffer until (possible) new metadata - size_t metadata; + // If an input makes a write with a flag or a changed sample rate etc, we add + // a marker to head, and when we read we check from the tail to see if there + // are updates to the player. + struct marker *marker_tail; // Optional callback to player if buffer is full input_cb full_cb; + // Quality of write/read data + struct media_quality cur_write_quality; + struct media_quality cur_read_quality; + + size_t bytes_written; + size_t bytes_read; + // Locks for sharing the buffer between input and player thread pthread_mutex_t mutex; pthread_cond_t cond; }; +struct input_arg +{ + uint32_t item_id; + int seek_ms; + struct input_metadata *metadata; +}; + /* --- Globals --- */ // Input thread static pthread_t tid_input; +// Event base, cmdbase and event we use to iterate in the playback loop +static struct event_base *evbase_input; +static struct commands_base *cmdbase; +static struct event *input_ev; +static bool input_initialized; + +// The source we are reading now +static struct input_source input_now_reading; + // Input buffer static struct input_buffer input_buffer; // Timeout waiting in playback loop static struct timespec input_loop_timeout = { INPUT_LOOP_TIMEOUT, 0 }; -#ifdef DEBUG +// Timeout waiting for player read +static struct timeval input_open_timeout = { INPUT_OPEN_TIMEOUT, 0 }; +static struct event *input_open_timeout_ev; + +#ifdef DEBUG_INPUT static size_t debug_elapsed; #endif -/* ------------------------------ MISC HELPERS ---------------------------- */ - -static short -flags_set(size_t len) -{ - short flags = 0; - - if (input_buffer.error) - { - if (len >= input_buffer.error) - { - flags |= INPUT_FLAG_ERROR; - input_buffer.error = 0; - } - else - input_buffer.error -= len; - } - - if (input_buffer.eof) - { - if (len >= input_buffer.eof) - { - flags |= INPUT_FLAG_EOF; - input_buffer.eof = 0; - } - else - input_buffer.eof -= len; - } - - if (input_buffer.metadata) - { - if (len >= input_buffer.metadata) - { - flags |= INPUT_FLAG_METADATA; - input_buffer.metadata = 0; - } - else - input_buffer.metadata -= len; - } - - return flags; -} +/* ------------------------------- MISC HELPERS ----------------------------- */ static int map_data_kind(int data_kind) @@ -166,88 +170,360 @@ map_data_kind(int data_kind) } } -static int -source_check_and_map(struct player_source *ps, const char *action, char check_setup) +static void +metadata_free(struct input_metadata *metadata, int content_only) { - int type; + free(metadata->artist); + free(metadata->title); + free(metadata->album); + free(metadata->genre); + free(metadata->artwork_url); -#ifdef DEBUG - DPRINTF(E_DBG, L_PLAYER, "Action is %s\n", action); -#endif - - if (!ps) - { - DPRINTF(E_LOG, L_PLAYER, "Stream %s called with invalid player source\n", action); - return -1; - } - - if (check_setup && !ps->setup_done) - { - DPRINTF(E_LOG, L_PLAYER, "Given player source not setup, %s not possible\n", action); - return -1; - } - - type = map_data_kind(ps->data_kind); - if (type < 0) - { - DPRINTF(E_LOG, L_PLAYER, "Unsupported input type, %s not possible\n", action); - return -1; - } - - return type; + if (!content_only) + free(metadata); + else + memset(metadata, 0, sizeof(struct input_metadata)); } -/* ----------------------------- PLAYBACK LOOP ---------------------------- */ -/* Thread: input */ - -// TODO Thread safety of ps? -static void * -playback(void *arg) +static struct input_metadata * +metadata_get(struct input_source *source) { - struct player_source *ps = arg; - int type; + struct input_metadata *metadata; + struct db_queue_item *queue_item; int ret; - type = source_check_and_map(ps, "start", 1); - if ((type < 0) || (inputs[type]->disabled)) - goto thread_exit; + if (!inputs[source->type]->metadata_get) + return NULL; - // Loops until input_loop_break is set or no more input, e.g. EOF - ret = inputs[type]->start(ps); + metadata = calloc(1, sizeof(struct input_metadata)); + + ret = inputs[source->type]->metadata_get(metadata, source); if (ret < 0) - input_write(NULL, INPUT_FLAG_ERROR); + goto out_free_metadata; -#ifdef DEBUG - DPRINTF(E_DBG, L_PLAYER, "Playback loop stopped (break is %d, ret %d)\n", input_loop_break, ret); -#endif + queue_item = db_queue_fetch_byitemid(source->item_id); + if (!queue_item) + { + DPRINTF(E_LOG, L_PLAYER, "Bug! Input source item_id does not match anything in queue\n"); + goto out_free_metadata; + } - thread_exit: - pthread_exit(NULL); + // Update queue item if metadata changed + if (metadata->artist || metadata->title || metadata->album || metadata->genre || metadata->artwork_url || metadata->len_ms) + { + // Since we won't be using the metadata struct values for anything else + // than this we just swap pointers + if (metadata->artist) + swap_pointers(&queue_item->artist, &metadata->artist); + if (metadata->title) + swap_pointers(&queue_item->title, &metadata->title); + if (metadata->album) + swap_pointers(&queue_item->album, &metadata->album); + if (metadata->genre) + swap_pointers(&queue_item->genre, &metadata->genre); + if (metadata->artwork_url) + swap_pointers(&queue_item->artwork_url, &metadata->artwork_url); + if (metadata->len_ms) + queue_item->song_length = metadata->len_ms; + + ret = db_queue_update_item(queue_item); + if (ret < 0) + DPRINTF(E_LOG, L_PLAYER, "Database error while updating queue with new metadata\n"); + } + + free_queue_item(queue_item, 0); + + return metadata; + + out_free_metadata: + metadata_free(metadata, 0); + return NULL; } -void -input_wait(void) +static void +marker_free(struct marker *marker) { - struct timespec ts; + if (!marker) + return; + + if (marker->flag == INPUT_FLAG_METADATA && marker->data) + metadata_free(marker->data, 0); + + if (marker->flag == INPUT_FLAG_QUALITY && marker->data) + free(marker->data); + + free(marker); +} + +static void +marker_add(size_t pos, short flag, void *flagdata) +{ + struct marker *head; + struct marker *marker; + + CHECK_NULL(L_PLAYER, marker = calloc(1, sizeof(struct marker))); + + marker->pos = pos; + marker->flag = flag; + marker->data = flagdata; + + for (head = input_buffer.marker_tail; head && head->prev; head = head->prev) + ; // Fast forward to the head + + if (!head) + input_buffer.marker_tail = marker; + else + head->prev = marker; +} + +static void +markers_set(short flags, size_t write_size) +{ + struct media_quality *quality; + struct input_metadata *metadata; + + if (flags & INPUT_FLAG_QUALITY) + { + quality = malloc(sizeof(struct media_quality)); + *quality = input_buffer.cur_write_quality; + marker_add(input_buffer.bytes_written - write_size, INPUT_FLAG_QUALITY, quality); + } + + if (flags & (INPUT_FLAG_EOF | INPUT_FLAG_ERROR)) + { + // This controls when the player will open the next track in the queue + if (input_buffer.bytes_read + INPUT_BUFFER_THRESHOLD < input_buffer.bytes_written) + // The player's read is behind, tell it to open when it reaches where + // we are minus the buffer size + marker_add(input_buffer.bytes_written - INPUT_BUFFER_THRESHOLD, INPUT_FLAG_START_NEXT, NULL); + else + // The player's read is close to our write, so open right away + marker_add(input_buffer.bytes_read, INPUT_FLAG_START_NEXT, NULL); + + marker_add(input_buffer.bytes_written, flags & (INPUT_FLAG_EOF | INPUT_FLAG_ERROR), NULL); + } + + if (flags & INPUT_FLAG_METADATA) + { + metadata = metadata_get(&input_now_reading); + if (metadata) + marker_add(input_buffer.bytes_written, INPUT_FLAG_METADATA, metadata); + } +} + + +/* ------------------------- INPUT SOURCE HANDLING -------------------------- */ + +static void +clear(struct input_source *source) +{ + free(source->path); + memset(source, 0, sizeof(struct input_source)); +} + +static void +flush(short *flags) +{ + struct marker *marker; + size_t len; pthread_mutex_lock(&input_buffer.mutex); - ts = timespec_reltoabs(input_loop_timeout); - pthread_cond_timedwait(&input_buffer.cond, &input_buffer.mutex, &ts); + // We will return an OR of all the unread marker flags + *flags = 0; + for (marker = input_buffer.marker_tail; marker; marker = input_buffer.marker_tail) + { + *flags |= marker->flag; + input_buffer.marker_tail = marker->prev; + marker_free(marker); + } + + len = evbuffer_get_length(input_buffer.evbuf); + + evbuffer_drain(input_buffer.evbuf, len); + + memset(&input_buffer.cur_read_quality, 0, sizeof(struct media_quality)); + memset(&input_buffer.cur_write_quality, 0, sizeof(struct media_quality)); + + input_buffer.bytes_read = 0; + input_buffer.bytes_written = 0; + + input_buffer.full_cb = NULL; pthread_mutex_unlock(&input_buffer.mutex); + +#ifdef DEBUG_INPUT + DPRINTF(E_DBG, L_PLAYER, "Flushing %zu bytes with flags %d\n", len, *flags); +#endif } +static void +stop(void) +{ + short flags; + int type; + + event_del(input_open_timeout_ev); + event_del(input_ev); + + type = input_now_reading.type; + + if (inputs[type]->stop && input_now_reading.open) + inputs[type]->stop(&input_now_reading); + + flush(&flags); + + clear(&input_now_reading); +} + +static int +seek(struct input_source *source, int seek_ms) +{ + if (inputs[source->type]->seek) + return inputs[source->type]->seek(source, seek_ms); + else + return 0; +} + +// On error returns -1, on success + seek given + seekable returns the position +// that the seek gave us, otherwise returns 0. +static int +setup(struct input_source *source, struct db_queue_item *queue_item, int seek_ms) +{ + int type; + int ret; + + type = map_data_kind(queue_item->data_kind); + if ((type < 0) || (inputs[type]->disabled)) + goto setup_error; + + source->type = type; + source->data_kind = queue_item->data_kind; + source->media_kind = queue_item->media_kind; + source->item_id = queue_item->id; + source->id = queue_item->file_id; + source->len_ms = queue_item->song_length; + source->path = safe_strdup(queue_item->path); + + DPRINTF(E_DBG, L_PLAYER, "Setting up input item '%s' (item id %" PRIu32 ")\n", source->path, source->item_id); + + if (inputs[type]->setup) + { + ret = inputs[type]->setup(source); + if (ret < 0) + goto setup_error; + } + + source->open = true; + + if (seek_ms > 0) + { + ret = seek(source, seek_ms); + if (ret < 0) + goto seek_error; + } + else + ret = 0; + + return ret; + + seek_error: + stop(); + setup_error: + clear(source); + return -1; +} + +static enum command_state +start(void *arg, int *retval) +{ + struct input_arg *cmdarg = arg; + struct db_queue_item *queue_item; + short flags; + int ret; + + // If we are asked to start the item that is currently open we can just seek + if (input_now_reading.open && cmdarg->item_id == input_now_reading.item_id) + { + flush(&flags); + + ret = seek(&input_now_reading, cmdarg->seek_ms); + if (ret < 0) + DPRINTF(E_WARN, L_PLAYER, "Ignoring failed seek to %d ms in '%s'\n", cmdarg->seek_ms, input_now_reading.path); + } + else + { + if (input_now_reading.open) + stop(); + + // Get the queue_item from the db + queue_item = db_queue_fetch_byitemid(cmdarg->item_id); + if (!queue_item) + { + DPRINTF(E_LOG, L_PLAYER, "Input start was called with an item id that has disappeared (id=%d)\n", cmdarg->item_id); + goto error; + } + + ret = setup(&input_now_reading, queue_item, cmdarg->seek_ms); + free_queue_item(queue_item, 0); + if (ret < 0) + goto error; + } + + DPRINTF(E_DBG, L_PLAYER, "Starting input read loop for item '%s' (item id %" PRIu32 "), seek %d\n", + input_now_reading.path, input_now_reading.item_id, cmdarg->seek_ms); + + event_add(input_open_timeout_ev, &input_open_timeout); + event_active(input_ev, 0, 0); + + *retval = ret; // Return is the seek result + return COMMAND_END; + + error: + input_write(NULL, NULL, INPUT_FLAG_ERROR); + clear(&input_now_reading); + *retval = -1; + return COMMAND_END; +} + +static enum command_state +stop_cmd(void *arg, int *retval) +{ + stop(); + + *retval = 0; + return COMMAND_END; +} + +static void +timeout_cb(int fd, short what, void *arg) +{ + if (input_buffer.bytes_read > 0) + return; + + DPRINTF(E_WARN, L_PLAYER, "Timed out after %d sec without any reading from input source\n", INPUT_OPEN_TIMEOUT); + + stop(); +} + +/* ---------------------- Interface towards input backends ------------------ */ +/* Thread: input and spotify */ + // Called by input modules from within the playback loop int -input_write(struct evbuffer *evbuf, short flags) +input_write(struct evbuffer *evbuf, struct media_quality *quality, short flags) { - struct timespec ts; + bool read_end; + size_t len; int ret; pthread_mutex_lock(&input_buffer.mutex); - while ( (!input_loop_break) && (evbuffer_get_length(input_buffer.evbuf) > INPUT_BUFFER_THRESHOLD) && evbuf ) + read_end = (flags & (INPUT_FLAG_EOF | INPUT_FLAG_ERROR)); + if (read_end) + input_now_reading.open = false; + + if ((evbuffer_get_length(input_buffer.evbuf) > INPUT_BUFFER_THRESHOLD) && evbuf) { if (input_buffer.full_cb) { @@ -255,78 +531,197 @@ input_write(struct evbuffer *evbuf, short flags) input_buffer.full_cb = NULL; } - if (flags & INPUT_FLAG_NONBLOCK) + // In case of EOF or error the input is always allowed to write, even if the + // buffer is full. There is no point in holding back the input in that case. + if (!read_end) { pthread_mutex_unlock(&input_buffer.mutex); return EAGAIN; } - - ts = timespec_reltoabs(input_loop_timeout); - pthread_cond_timedwait(&input_buffer.cond, &input_buffer.mutex, &ts); } - if (input_loop_break) + if (quality && !quality_is_equal(quality, &input_buffer.cur_write_quality)) { - pthread_mutex_unlock(&input_buffer.mutex); - return 0; + input_buffer.cur_write_quality = *quality; + flags |= INPUT_FLAG_QUALITY; } + ret = 0; + len = 0; if (evbuf) - ret = evbuffer_add_buffer(input_buffer.evbuf, evbuf); - else - ret = 0; + { + len = evbuffer_get_length(evbuf); + input_buffer.bytes_written += len; + ret = evbuffer_add_buffer(input_buffer.evbuf, evbuf); + if (ret < 0) + { + DPRINTF(E_LOG, L_PLAYER, "Error adding stream data to input buffer, stopping\n"); + input_stop(); + flags |= INPUT_FLAG_ERROR; + } + } - if (ret < 0) - DPRINTF(E_LOG, L_PLAYER, "Error adding stream data to input buffer\n"); - - if (!input_buffer.error && (flags & INPUT_FLAG_ERROR)) - input_buffer.error = evbuffer_get_length(input_buffer.evbuf); - if (!input_buffer.eof && (flags & INPUT_FLAG_EOF)) - input_buffer.eof = evbuffer_get_length(input_buffer.evbuf); - if (!input_buffer.metadata && (flags & INPUT_FLAG_METADATA)) - input_buffer.metadata = evbuffer_get_length(input_buffer.evbuf); + if (flags) + markers_set(flags, len); pthread_mutex_unlock(&input_buffer.mutex); return ret; } - -/* -------------------- Interface towards player thread ------------------- */ -/* Thread: player */ - int -input_read(void *data, size_t size, short *flags) +input_wait(void) { - int len; - - *flags = 0; - - if (!tid_input) - { - DPRINTF(E_LOG, L_PLAYER, "Bug! Read called, but playback not running\n"); - return -1; - } + struct timespec ts; pthread_mutex_lock(&input_buffer.mutex); -#ifdef DEBUG - debug_elapsed += size; - if (debug_elapsed > STOB(441000)) // 10 sec + // Is the buffer full? Then wait for a read or for loop_timeout to elapse + if (evbuffer_get_length(input_buffer.evbuf) > INPUT_BUFFER_THRESHOLD) { - DPRINTF(E_DBG, L_PLAYER, "Input buffer has %zu bytes\n", evbuffer_get_length(input_buffer.evbuf)); - debug_elapsed = 0; + if (input_buffer.full_cb) + { + input_buffer.full_cb(); + input_buffer.full_cb = NULL; + } + + ts = timespec_reltoabs(input_loop_timeout); + pthread_cond_timedwait(&input_buffer.cond, &input_buffer.mutex, &ts); + + if (evbuffer_get_length(input_buffer.evbuf) > INPUT_BUFFER_THRESHOLD) + { + pthread_mutex_unlock(&input_buffer.mutex); + return -1; + } + } + + pthread_mutex_unlock(&input_buffer.mutex); + return 0; +} + +/*void +input_next(void) +{ + commands_exec_async(cmdbase, next, NULL); +}*/ + +/* ---------------------------------- MAIN ---------------------------------- */ +/* Thread: input */ + +static void * +input(void *arg) +{ + int ret; + + ret = db_perthread_init(); + if (ret < 0) + { + DPRINTF(E_LOG, L_MAIN, "Error: DB init failed (input thread)\n"); + pthread_exit(NULL); + } + + input_initialized = true; + + event_base_dispatch(evbase_input); + + if (input_initialized) + { + DPRINTF(E_LOG, L_MAIN, "Input event loop terminated ahead of time!\n"); + input_initialized = false; + } + + db_perthread_deinit(); + + pthread_exit(NULL); +} + +static void +play(evutil_socket_t fd, short flags, void *arg) +{ + struct timeval tv = { 0, 0 }; + int ret; + + // Spotify runs in its own thread, so no reading is done by the input thread, + // thus there is no reason to activate input_ev + if (!inputs[input_now_reading.type]->play) + return; + + // Return will be negative if there is an error or EOF. Here, we just don't + // loop any more. input_write() will pass the message to the player. + ret = inputs[input_now_reading.type]->play(&input_now_reading); + if (ret < 0) + { + input_now_reading.open = false; + return; // Error or EOF, so don't come back + } + + event_add(input_ev, &tv); +} + + +/* ---------------------- Interface towards player thread ------------------- */ +/* Thread: player */ + +int +input_read(void *data, size_t size, short *flag, void **flagdata) +{ + struct marker *marker; + int len; + + *flag = 0; + + pthread_mutex_lock(&input_buffer.mutex); + + // First we check if there is a marker in the requested samples. If there is, + // we only return data up until that marker. That way we don't have to deal + // with multiple markers, and we don't return data that contains mixed sample + // rates, bits per sample or an EOF in the middle. + marker = input_buffer.marker_tail; + if (marker && marker->pos <= input_buffer.bytes_read + size) + { + *flag = marker->flag; + *flagdata = marker->data; + + size = marker->pos - input_buffer.bytes_read; + input_buffer.marker_tail = marker->prev; + free(marker); } -#endif len = evbuffer_remove(input_buffer.evbuf, data, size); if (len < 0) { DPRINTF(E_LOG, L_PLAYER, "Error reading stream data from input buffer\n"); + *flag = INPUT_FLAG_ERROR; goto out_unlock; } - *flags = flags_set(len); + input_buffer.bytes_read += len; + +#ifdef DEBUG_INPUT + // Logs if flags present or each 10 seconds + + if (*flag & INPUT_FLAG_QUALITY) + input_buffer.cur_read_quality = *((struct media_quality *)(*flagdata)); + + size_t one_sec_size = STOB(input_buffer.cur_read_quality.sample_rate, input_buffer.cur_read_quality.bits_per_sample, input_buffer.cur_read_quality.channels); + debug_elapsed += len; + if (*flag || (debug_elapsed > 10 * one_sec_size)) + { + debug_elapsed = 0; + DPRINTF(E_DBG, L_PLAYER, "READ %zu bytes (%d/%d/%d), WROTE %zu bytes (%d/%d/%d), SIZE %zu (=%zu), FLAGS %04x\n", + input_buffer.bytes_read, + input_buffer.cur_read_quality.sample_rate, + input_buffer.cur_read_quality.bits_per_sample, + input_buffer.cur_read_quality.channels, + input_buffer.bytes_written, + input_buffer.cur_write_quality.sample_rate, + input_buffer.cur_write_quality.bits_per_sample, + input_buffer.cur_write_quality.channels, + evbuffer_get_length(input_buffer.evbuf), + input_buffer.bytes_written - input_buffer.bytes_read, + *flag); + } +#endif out_unlock: pthread_cond_signal(&input_buffer.cond); @@ -345,191 +740,47 @@ input_buffer_full_cb(input_cb cb) } int -input_setup(struct player_source *ps) +input_seek(uint32_t item_id, int seek_ms) { - int type; + struct input_arg cmdarg; - type = source_check_and_map(ps, "setup", 0); - if ((type < 0) || (inputs[type]->disabled)) - return -1; + cmdarg.item_id = item_id; + cmdarg.seek_ms = seek_ms; - if (!inputs[type]->setup) - return 0; - - return inputs[type]->setup(ps); + return commands_exec_sync(cmdbase, start, NULL, &cmdarg); } -int -input_start(struct player_source *ps) +void +input_start(uint32_t item_id) { - int ret; + struct input_arg *cmdarg; - if (tid_input) - { - DPRINTF(E_WARN, L_PLAYER, "Input start called, but playback already running\n"); - return 0; - } + CHECK_NULL(L_PLAYER, cmdarg = malloc(sizeof(struct input_arg))); - input_loop_break = 0; + cmdarg->item_id = item_id; + cmdarg->seek_ms = 0; - ret = pthread_create(&tid_input, NULL, playback, ps); - if (ret < 0) - { - DPRINTF(E_LOG, L_PLAYER, "Could not spawn input thread: %s\n", strerror(errno)); - return -1; - } - -#if defined(HAVE_PTHREAD_SETNAME_NP) - pthread_setname_np(tid_input, "input"); -#elif defined(HAVE_PTHREAD_SET_NAME_NP) - pthread_set_name_np(tid_input, "input"); -#endif - - return 0; + commands_exec_async(cmdbase, start, cmdarg); } -int -input_pause(struct player_source *ps) +void +input_stop(void) { - short flags; - int ret; - -#ifdef DEBUG - DPRINTF(E_DBG, L_PLAYER, "Pause called, stopping playback loop\n"); -#endif - - if (!tid_input) - return -1; - - pthread_mutex_lock(&input_buffer.mutex); - - input_loop_break = 1; - - pthread_cond_signal(&input_buffer.cond); - pthread_mutex_unlock(&input_buffer.mutex); - - // TODO What if input thread is hanging waiting for source? Kill thread? - ret = pthread_join(tid_input, NULL); - if (ret != 0) - { - DPRINTF(E_LOG, L_PLAYER, "Could not join input thread: %s\n", strerror(errno)); - return -1; - } - - tid_input = 0; - - input_flush(&flags); - - return 0; -} - -int -input_stop(struct player_source *ps) -{ - int type; - - if (tid_input) - input_pause(ps); - - if (!ps) - return 0; - - type = source_check_and_map(ps, "stop", 1); - if ((type < 0) || (inputs[type]->disabled)) - return -1; - - if (!inputs[type]->stop) - return 0; - - return inputs[type]->stop(ps); -} - -int -input_seek(struct player_source *ps, int seek_ms) -{ - int type; - - type = source_check_and_map(ps, "seek", 1); - if ((type < 0) || (inputs[type]->disabled)) - return -1; - - if (!inputs[type]->seek) - return 0; - - if (tid_input) - input_pause(ps); - - return inputs[type]->seek(ps, seek_ms); + commands_exec_async(cmdbase, stop_cmd, NULL); } void input_flush(short *flags) { - size_t len; - - pthread_mutex_lock(&input_buffer.mutex); - - len = evbuffer_get_length(input_buffer.evbuf); - - evbuffer_drain(input_buffer.evbuf, len); - - *flags = flags_set(len); - - input_buffer.error = 0; - input_buffer.eof = 0; - input_buffer.metadata = 0; - input_buffer.full_cb = NULL; - - pthread_mutex_unlock(&input_buffer.mutex); - -#ifdef DEBUG - DPRINTF(E_DBG, L_PLAYER, "Flushing %zu bytes with flags %d\n", len, *flags); -#endif -} - -int -input_metadata_get(struct input_metadata *metadata, struct player_source *ps, int startup, uint64_t rtptime) -{ - int type; - - if (!metadata || !ps || !ps->stream_start || !ps->output_start) - { - DPRINTF(E_LOG, L_PLAYER, "Bug! Unhandled case in input_metadata_get()\n"); - return -1; - } - - memset(metadata, 0, sizeof(struct input_metadata)); - - metadata->item_id = ps->item_id; - - metadata->startup = startup; - metadata->offset = ps->output_start - ps->stream_start; - metadata->rtptime = ps->stream_start; - - // Note that the source may overwrite the above progress metadata - type = source_check_and_map(ps, "metadata_get", 1); - if ((type < 0) || (inputs[type]->disabled)) - return -1; - - if (!inputs[type]->metadata_get) - return 0; - - return inputs[type]->metadata_get(metadata, ps, rtptime); + // Flush should be thread safe + flush(flags); } +// Not currently used, perhaps remove? void input_metadata_free(struct input_metadata *metadata, int content_only) { - free(metadata->artist); - free(metadata->title); - free(metadata->album); - free(metadata->genre); - free(metadata->artwork_url); - - if (!content_only) - free(metadata); - else - memset(metadata, 0, sizeof(struct input_metadata)); + metadata_free(metadata, content_only); } int @@ -543,12 +794,10 @@ input_init(void) pthread_mutex_init(&input_buffer.mutex, NULL); pthread_cond_init(&input_buffer.cond, NULL); - input_buffer.evbuf = evbuffer_new(); - if (!input_buffer.evbuf) - { - DPRINTF(E_LOG, L_PLAYER, "Out of memory for input buffer\n"); - return -1; - } + CHECK_NULL(L_PLAYER, evbase_input = event_base_new()); + CHECK_NULL(L_PLAYER, input_buffer.evbuf = evbuffer_new()); + CHECK_NULL(L_PLAYER, input_ev = event_new(evbase_input, -1, EV_PERSIST, play, NULL)); + CHECK_NULL(L_PLAYER, input_open_timeout_ev = evtimer_new(evbase_input, timeout_cb, NULL)); no_input = 1; for (i = 0; inputs[i]; i++) @@ -556,7 +805,7 @@ input_init(void) if (inputs[i]->type != i) { DPRINTF(E_FATAL, L_PLAYER, "BUG! Input definitions are misaligned with input enum\n"); - return -1; + goto input_fail; } if (!inputs[i]->init) @@ -573,17 +822,41 @@ input_init(void) } if (no_input) - return -1; + goto input_fail; + + cmdbase = commands_base_new(evbase_input, NULL); + + ret = pthread_create(&tid_input, NULL, input, NULL); + if (ret < 0) + { + DPRINTF(E_LOG, L_MAIN, "Could not spawn input thread: %s\n", strerror(errno)); + goto thread_fail; + } + +#if defined(HAVE_PTHREAD_SETNAME_NP) + pthread_setname_np(tid_input, "input"); +#elif defined(HAVE_PTHREAD_SET_NAME_NP) + pthread_set_name_np(tid_input, "input"); +#endif return 0; + + thread_fail: + commands_base_free(cmdbase); + input_fail: + evbuffer_free(input_buffer.evbuf); + event_base_free(evbase_input); + return -1; } void input_deinit(void) { int i; + int ret; - input_stop(NULL); +// TODO ok to do from here? + input_stop(); for (i = 0; inputs[i]; i++) { @@ -594,9 +867,20 @@ input_deinit(void) inputs[i]->deinit(); } + input_initialized = false; + commands_base_destroy(cmdbase); + + ret = pthread_join(tid_input, NULL); + if (ret != 0) + { + DPRINTF(E_FATAL, L_MAIN, "Could not join input thread: %s\n", strerror(errno)); + return; + } + pthread_cond_destroy(&input_buffer.cond); pthread_mutex_destroy(&input_buffer.mutex); evbuffer_free(input_buffer.evbuf); + event_base_free(evbase_input); } diff --git a/src/input.h b/src/input.h index c988d858..18407f6d 100644 --- a/src/input.h +++ b/src/input.h @@ -6,7 +6,8 @@ # include #endif #include -#include "transcode.h" +#include "db.h" +#include "misc.h" // Must be in sync with inputs[] in input.c enum input_types @@ -21,83 +22,74 @@ enum input_types enum input_flags { - // Write to input buffer must not block - INPUT_FLAG_NONBLOCK = (1 << 0), + // Flags that input is closing current source + INPUT_FLAG_START_NEXT = (1 << 0), // Flags end of file - INPUT_FLAG_EOF = (1 << 1), + INPUT_FLAG_EOF = (1 << 1), // Flags error reading file - INPUT_FLAG_ERROR = (1 << 2), + INPUT_FLAG_ERROR = (1 << 2), // Flags possible new stream metadata - INPUT_FLAG_METADATA = (1 << 3), + INPUT_FLAG_METADATA = (1 << 3), + // Flags new stream quality + INPUT_FLAG_QUALITY = (1 << 4), }; -struct player_source +struct input_source { - /* Id of the file/item in the files database */ - uint32_t id; + // Type of input + enum input_types type; - /* Item-Id of the file/item in the queue */ + // Item-Id of the file/item in the queue uint32_t item_id; - /* Length of the file/item in milliseconds */ + // Id of the file/item in the files database + uint32_t id; + + // Length of the file/item in milliseconds uint32_t len_ms; enum data_kind data_kind; enum media_kind media_kind; char *path; - /* Start time of the media item as rtp-time - The stream-start is the rtp-time the media item did or would have - started playing (after seek or pause), therefor the elapsed time of the - media item is always: - elapsed time = current rtptime - stream-start */ - uint64_t stream_start; + // Flags that the input has been opened (i.e. needs to be closed) + bool open; - /* Output start time of the media item as rtp-time - The output start time is the rtp-time of the first audio packet send - to the audio outputs. - It differs from stream-start especially after a seek, where the first audio - packet has the next rtp-time as output start and stream start becomes the - rtp-time the media item would have been started playing if the seek did - not happen. */ - uint64_t output_start; - - /* End time of media item as rtp-time - The end time is set if the reading (source_read) of the media item reached - end of file, until then it is 0. */ - uint64_t end; - - /* Opaque pointer to data that the input sets up when called with setup(), and - * which is cleaned up by the input with stop() - */ + // The below is private data for the input backend. It is optional for the + // backend to use, so nothing in the input or player should depend on it! + // + // Opaque pointer to data that the input backend sets up when start() is + // called, and that is cleaned up by the backend when stop() is called void *input_ctx; - - /* Input has completed setup of the source - */ - int setup_done; - - struct player_source *play_next; + // Private evbuf. Alloc'ed by backend at start() and free'd at stop() + struct evbuffer *evbuf; + // Private source quality storage + struct media_quality quality; }; typedef int (*input_cb)(void); struct input_metadata { + // queue_item id uint32_t item_id; - int startup; + // Input can override the default player progress by setting this + // FIXME only implemented for Airplay speakers currently + uint32_t pos_ms; - uint64_t rtptime; - uint64_t offset; - - // The player will update queue_item with the below - uint32_t song_length; + // Sets new song length (input will also update queue_item) + uint32_t len_ms; + // Input can update queue_item with the below char *artist; char *title; char *album; char *genre; char *artwork_url; + + // Indicates whether we are starting playback. Just passed on to output. + int startup; }; struct input_definition @@ -112,65 +104,75 @@ struct input_definition char disabled; // Prepare a playback session - int (*setup)(struct player_source *ps); + int (*setup)(struct input_source *source); - // Starts playback loop (must be defined) - int (*start)(struct player_source *ps); + // One iteration of the playback loop (= a read operation from source) + int (*play)(struct input_source *source); - // Cleans up when playback loop has ended - int (*stop)(struct player_source *ps); + // Cleans up (only required when stopping source before it ends itself) + int (*stop)(struct input_source *source); // Changes the playback position - int (*seek)(struct player_source *ps, int seek_ms); + int (*seek)(struct input_source *source, int seek_ms); // Return metadata - int (*metadata_get)(struct input_metadata *metadata, struct player_source *ps, uint64_t rtptime); + int (*metadata_get)(struct input_metadata *metadata, struct input_source *source); // Initialization function called during startup int (*init)(void); // Deinitialization function called at shutdown void (*deinit)(void); - }; -/* - * Input modules should use this to test if playback should end - */ -int input_loop_break; + +/* ---------------------- Interface towards input backends ------------------ */ +/* Thread: input and spotify */ /* - * Transfer stream data to the player's input buffer. The input evbuf will be - * drained on succesful write. This is to avoid copying memory. If the player's - * input buffer is full the function will block until the write can be made - * (unless INPUT_FILE_NONBLOCK is set). + * Transfer stream data to the player's input buffer. Data must be PCM-LE + * samples. The input evbuf will be drained on succesful write. This is to avoid + * copying memory. * - * @in evbuf Raw audio data to write + * @in evbuf Raw PCM_LE audio data to write + * @in evbuf Quality of the PCM (sample rate etc.) * @in flags One or more INPUT_FLAG_* - * @return 0 on success, EAGAIN if buffer was full (and _NONBLOCK is set), - * -1 on error + * @return 0 on success, EAGAIN if buffer was full, -1 on error */ int -input_write(struct evbuffer *evbuf, short flags); +input_write(struct evbuffer *evbuf, struct media_quality *quality, short flags); /* - * Input modules can use this to wait in the playback loop (like input_write() - * would have done) + * Input modules can use this to wait for the input_buffer to be ready for + * writing. The wait is max INPUT_LOOP_TIMEOUT, which allows the event base to + * loop and process pending commands once in a while. */ -void +int input_wait(void); +/* + * Async switch to the next song in the queue. Mostly for internal use, but + * might be relevant some day externally? + */ +//void +//input_next(void); + + +/* ---------------------- Interface towards player thread ------------------- */ +/* Thread: player */ + /* * Move a chunk of stream data from the player's input buffer to an output * buffer. Should only be called by the player thread. Will not block. * * @in data Output buffer * @in size How much data to move to the output buffer - * @out flags Flags INPUT_FLAG_* + * @out flag Flag INPUT_FLAG_* + * @out flagdata Data associated with the flag, e.g. quality or metadata struct * @return Number of bytes moved, -1 on error */ int -input_read(void *data, size_t size, short *flags); +input_read(void *data, size_t size, short *flag, void **flagdata); /* * Player can set this to get a callback from the input when the input buffer @@ -182,39 +184,31 @@ void input_buffer_full_cb(input_cb cb); /* - * Initializes the given player source for playback + * Tells the input to start, i.e. after calling this function the input buffer + * will begin to fill up, and should be read periodically with input_read(). If + * called while another item is still open, it will be closed and the input + * buffer will be flushed. This operation blocks. + * + * @in item_id Queue item id to start playing + * @in seek_ms Position to start playing + * @return Actual seek position if seekable, 0 otherwise, -1 on error */ int -input_setup(struct player_source *ps); +input_seek(uint32_t item_id, int seek_ms); /* - * Tells the input to start or resume playback, i.e. after calling this function - * the input buffer will begin to fill up, and should be read periodically with - * input_read(). Before calling this input_setup() must have been called. + * Same as input_seek(), just non-blocking and does not offer seek. + * + * @in item_id Queue item id to start playing */ -int -input_start(struct player_source *ps); +void +input_start(uint32_t item_id); /* - * Pauses playback of the given player source (stops playback loop) and flushes - * the input buffer + * Stops the input and clears everything. Flushes the input buffer. */ -int -input_pause(struct player_source *ps); - -/* - * Stops playback loop (if running), flushes input buffer and cleans up the - * player source - */ -int -input_stop(struct player_source *ps); - -/* - * Seeks playback position to seek_ms. Returns actual seek position, 0 on - * unseekable, -1 on error. May block. - */ -int -input_seek(struct player_source *ps, int seek_ms); +void +input_stop(void); /* * Flush input buffer. Output flags will be the same as input_read(). @@ -222,12 +216,6 @@ input_seek(struct player_source *ps, int seek_ms); void input_flush(short *flags); -/* - * Gets metadata from the input, returns 0 if metadata is set, otherwise -1 - */ -int -input_metadata_get(struct input_metadata *metadata, struct player_source *ps, int startup, uint64_t rtptime); - /* * Free the entire struct */ diff --git a/src/inputs/file_http.c b/src/inputs/file_http.c index 7422eacb..6864e103 100644 --- a/src/inputs/file_http.c +++ b/src/inputs/file_http.c @@ -26,93 +26,107 @@ #include "transcode.h" #include "http.h" #include "misc.h" +#include "logger.h" #include "input.h" static int -setup(struct player_source *ps) +setup(struct input_source *source) { - ps->input_ctx = transcode_setup(XCODE_PCM16_NOHEADER, ps->data_kind, ps->path, ps->len_ms, NULL); - if (!ps->input_ctx) + struct transcode_ctx *ctx; + + ctx = transcode_setup(XCODE_PCM_NATIVE, NULL, source->data_kind, source->path, source->len_ms, NULL); + if (!ctx) return -1; - ps->setup_done = 1; + CHECK_NULL(L_PLAYER, source->evbuf = evbuffer_new()); + + source->quality.sample_rate = transcode_encode_query(ctx->encode_ctx, "sample_rate"); + source->quality.bits_per_sample = transcode_encode_query(ctx->encode_ctx, "bits_per_sample"); + source->quality.channels = transcode_encode_query(ctx->encode_ctx, "channels"); + + source->input_ctx = ctx; return 0; } static int -setup_http(struct player_source *ps) +setup_http(struct input_source *source) { char *url; - if (http_stream_setup(&url, ps->path) < 0) + if (http_stream_setup(&url, source->path) < 0) return -1; - free(ps->path); - ps->path = url; + free(source->path); + source->path = url; - return setup(ps); + return setup(source); } static int -start(struct player_source *ps) +stop(struct input_source *source) { - struct evbuffer *evbuf; - short flags; - int ret; - int icy_timer; - - evbuf = evbuffer_new(); - - ret = -1; - flags = 0; - while (!input_loop_break && !(flags & INPUT_FLAG_EOF)) - { - // We set "wanted" to 1 because the read size doesn't matter to us - // TODO optimize? - ret = transcode(evbuf, &icy_timer, ps->input_ctx, 1); - if (ret < 0) - break; - - flags = ((ret == 0) ? INPUT_FLAG_EOF : 0) | - (icy_timer ? INPUT_FLAG_METADATA : 0); - - ret = input_write(evbuf, flags); - if (ret < 0) - break; - } - - evbuffer_free(evbuf); - - return ret; -} - -static int -stop(struct player_source *ps) -{ - struct transcode_ctx *ctx = ps->input_ctx; + struct transcode_ctx *ctx = source->input_ctx; transcode_cleanup(&ctx); - ps->input_ctx = NULL; - ps->setup_done = 0; + if (source->evbuf) + evbuffer_free(source->evbuf); + + source->input_ctx = NULL; + source->evbuf = NULL; return 0; } static int -seek(struct player_source *ps, int seek_ms) +play(struct input_source *source) { - return transcode_seek(ps->input_ctx, seek_ms); + struct transcode_ctx *ctx = source->input_ctx; + int icy_timer; + int ret; + short flags; + + ret = input_wait(); + if (ret < 0) + return 0; // Loop, input_buffer is not ready for writing + + // We set "wanted" to 1 because the read size doesn't matter to us + // TODO optimize? + ret = transcode(source->evbuf, &icy_timer, ctx, 1); + if (ret == 0) + { + input_write(source->evbuf, &source->quality, INPUT_FLAG_EOF); + stop(source); + return -1; + } + else if (ret < 0) + { + input_write(NULL, NULL, INPUT_FLAG_ERROR); + stop(source); + return -1; + } + + flags = (icy_timer ? INPUT_FLAG_METADATA : 0); + + input_write(source->evbuf, &source->quality, flags); + + return 0; } static int -metadata_get_http(struct input_metadata *metadata, struct player_source *ps, uint64_t rtptime) +seek(struct input_source *source, int seek_ms) +{ + return transcode_seek(source->input_ctx, seek_ms); +} + +static int +metadata_get_http(struct input_metadata *metadata, struct input_source *source) { struct http_icy_metadata *m; int changed; - m = transcode_metadata(ps->input_ctx, &changed); + m = transcode_metadata(source->input_ctx, &changed); if (!m) return -1; @@ -140,7 +154,7 @@ struct input_definition input_file = .type = INPUT_TYPE_FILE, .disabled = 0, .setup = setup, - .start = start, + .play = play, .stop = stop, .seek = seek, }; @@ -151,7 +165,7 @@ struct input_definition input_http = .type = INPUT_TYPE_HTTP, .disabled = 0, .setup = setup_http, - .start = start, + .play = play, .stop = stop, .metadata_get = metadata_get_http, }; diff --git a/src/inputs/pipe.c b/src/inputs/pipe.c index e1128912..85caaf55 100644 --- a/src/inputs/pipe.c +++ b/src/inputs/pipe.c @@ -103,6 +103,9 @@ static pthread_t tid_pipe; static struct event_base *evbase_pipe; static struct commands_base *cmdbase; +// From config - the sample rate and bps of the pipe input +static int pipe_sample_rate; +static int pipe_bits_per_sample; // From config - should we watch library pipes for data or only start on request static int pipe_autostart; // The mfi id of the pipe autostarted by the pipe thread @@ -122,7 +125,7 @@ static pthread_mutex_t pipe_metadata_lock; // True if there is new metadata to push to the player static bool pipe_metadata_is_new; -/* -------------------------------- HELPERS ------------------------------- */ +/* -------------------------------- HELPERS --------------------------------- */ static struct pipe * pipe_create(const char *path, int id, enum pipetype type, event_callback_fn cb) @@ -305,9 +308,10 @@ parse_progress(struct input_metadata *m, char *progress) if (!start || !pos || !end) return; - m->rtptime = start; // Not actually used - we have our own rtptime - m->offset = (pos > start) ? (pos - start) : 0; - m->song_length = (end - start) * 10 / 441; // Convert to ms based on 44100 + if (pos > start) + m->pos_ms = (pos - start) * 1000 / pipe_sample_rate; + if (end > start) + m->len_ms = (end - start) * 1000 / pipe_sample_rate; } static void @@ -497,8 +501,8 @@ pipe_metadata_parse(struct input_metadata *m, struct evbuffer *evbuf) } -/* ----------------------------- PIPE WATCHING ---------------------------- */ -/* Thread: pipe */ +/* ------------------------------ PIPE WATCHING ----------------------------- */ +/* Thread: pipe */ // Some data arrived on a pipe we watch - let's autostart playback static void @@ -614,8 +618,8 @@ pipe_thread_run(void *arg) } -/* -------------------------- METADATA PIPE HANDLING ---------------------- */ -/* Thread: worker */ +/* --------------------------- METADATA PIPE HANDLING ----------------------- */ +/* Thread: worker */ static void pipe_metadata_watch_del(void *arg) @@ -703,8 +707,8 @@ pipe_metadata_watch_add(void *arg) } -/* ---------------------- PIPE WATCH THREAD START/STOP -------------------- */ -/* Thread: filescanner */ +/* ----------------------- PIPE WATCH THREAD START/STOP --------------------- */ +/* Thread: filescanner */ static void pipe_thread_start(void) @@ -799,87 +803,47 @@ pipe_listener_cb(short event_mask) } -/* -------------------------- PIPE INPUT INTERFACE ------------------------ */ -/* Thread: player/input */ +/* --------------------------- PIPE INPUT INTERFACE ------------------------- */ +/* Thread: input */ static int -setup(struct player_source *ps) +setup(struct input_source *source) { struct pipe *pipe; int fd; - fd = pipe_open(ps->path, 0); + fd = pipe_open(source->path, 0); if (fd < 0) return -1; - CHECK_NULL(L_PLAYER, pipe = pipe_create(ps->path, ps->id, PIPE_PCM, NULL)); + CHECK_NULL(L_PLAYER, pipe = pipe_create(source->path, source->id, PIPE_PCM, NULL)); + CHECK_NULL(L_PLAYER, source->evbuf = evbuffer_new()); + pipe->fd = fd; - pipe->is_autostarted = (ps->id == pipe_autostart_id); + pipe->is_autostarted = (source->id == pipe_autostart_id); - worker_execute(pipe_metadata_watch_add, ps->path, strlen(ps->path) + 1, 0); + worker_execute(pipe_metadata_watch_add, source->path, strlen(source->path) + 1, 0); - ps->input_ctx = pipe; - ps->setup_done = 1; + source->input_ctx = pipe; + + source->quality.sample_rate = pipe_sample_rate; + source->quality.bits_per_sample = pipe_bits_per_sample; + source->quality.channels = 2; return 0; } static int -start(struct player_source *ps) +stop(struct input_source *source) { - struct pipe *pipe = ps->input_ctx; - struct evbuffer *evbuf; - short flags; - int ret; - - evbuf = evbuffer_new(); - if (!evbuf) - { - DPRINTF(E_LOG, L_PLAYER, "Out of memory for pipe evbuf\n"); - return -1; - } - - ret = -1; - while (!input_loop_break) - { - ret = evbuffer_read(evbuf, pipe->fd, PIPE_READ_MAX); - if ((ret == 0) && (pipe->is_autostarted)) - { - input_write(evbuf, INPUT_FLAG_EOF); // Autostop - break; - } - else if ((ret == 0) || ((ret < 0) && (errno == EAGAIN))) - { - input_wait(); - continue; - } - else if (ret < 0) - { - DPRINTF(E_LOG, L_PLAYER, "Could not read from pipe '%s': %s\n", ps->path, strerror(errno)); - break; - } - - flags = (pipe_metadata_is_new ? INPUT_FLAG_METADATA : 0); - pipe_metadata_is_new = 0; - - ret = input_write(evbuf, flags); - if (ret < 0) - break; - } - - evbuffer_free(evbuf); - - return ret; -} - -static int -stop(struct player_source *ps) -{ - struct pipe *pipe = ps->input_ctx; + struct pipe *pipe = source->input_ctx; union pipe_arg *cmdarg; DPRINTF(E_DBG, L_PLAYER, "Stopping pipe\n"); + if (source->evbuf) + evbuffer_free(source->evbuf); + pipe_close(pipe->fd); // Reset the pipe and start watching it again for new data. Must be async or @@ -895,37 +859,56 @@ stop(struct player_source *ps) pipe_free(pipe); - ps->input_ctx = NULL; - ps->setup_done = 0; + source->input_ctx = NULL; + source->evbuf = NULL; return 0; } static int -metadata_get(struct input_metadata *metadata, struct player_source *ps, uint64_t rtptime) +play(struct input_source *source) +{ + struct pipe *pipe = source->input_ctx; + short flags; + int ret; + + ret = input_wait(); + if (ret < 0) + return 0; // Loop, input_buffer is not ready for writing + + ret = evbuffer_read(source->evbuf, pipe->fd, PIPE_READ_MAX); + if ((ret == 0) && (pipe->is_autostarted)) + { + input_write(source->evbuf, NULL, INPUT_FLAG_EOF); // Autostop + stop(source); + return -1; + } + else if ((ret == 0) || ((ret < 0) && (errno == EAGAIN))) + { + return 0; // Loop + } + else if (ret < 0) + { + DPRINTF(E_LOG, L_PLAYER, "Could not read from pipe '%s': %s\n", source->path, strerror(errno)); + input_write(NULL, NULL, INPUT_FLAG_ERROR); + stop(source); + return -1; + } + + flags = (pipe_metadata_is_new ? INPUT_FLAG_METADATA : 0); + pipe_metadata_is_new = 0; + + input_write(source->evbuf, &source->quality, flags); + + return 0; +} + +static int +metadata_get(struct input_metadata *metadata, struct input_source *source) { pthread_mutex_lock(&pipe_metadata_lock); - if (pipe_metadata_parsed.artist) - swap_pointers(&metadata->artist, &pipe_metadata_parsed.artist); - if (pipe_metadata_parsed.title) - swap_pointers(&metadata->title, &pipe_metadata_parsed.title); - if (pipe_metadata_parsed.album) - swap_pointers(&metadata->album, &pipe_metadata_parsed.album); - if (pipe_metadata_parsed.genre) - swap_pointers(&metadata->genre, &pipe_metadata_parsed.genre); - if (pipe_metadata_parsed.artwork_url) - swap_pointers(&metadata->artwork_url, &pipe_metadata_parsed.artwork_url); - - if (pipe_metadata_parsed.song_length) - { - if (rtptime > ps->stream_start) - metadata->rtptime = rtptime - pipe_metadata_parsed.offset; - metadata->offset = pipe_metadata_parsed.offset; - metadata->song_length = pipe_metadata_parsed.song_length; - } - - input_metadata_free(&pipe_metadata_parsed, 1); + *metadata = pipe_metadata_parsed; pthread_mutex_unlock(&pipe_metadata_lock); @@ -945,6 +928,20 @@ init(void) CHECK_ERR(L_PLAYER, listener_add(pipe_listener_cb, LISTENER_DATABASE)); } + pipe_sample_rate = cfg_getint(cfg_getsec(cfg, "library"), "pipe_sample_rate"); + if (pipe_sample_rate != 44100 && pipe_sample_rate != 48000 && pipe_sample_rate != 96000) + { + DPRINTF(E_FATAL, L_PLAYER, "The configuration of pipe_sample_rate is invalid: %d\n", pipe_sample_rate); + return -1; + } + + pipe_bits_per_sample = cfg_getint(cfg_getsec(cfg, "library"), "pipe_bits_per_sample"); + if (pipe_bits_per_sample != 16 && pipe_bits_per_sample != 24) + { + DPRINTF(E_FATAL, L_PLAYER, "The configuration of pipe_bits_per_sample is invalid: %d\n", pipe_bits_per_sample); + return -1; + } + return 0; } @@ -966,7 +963,7 @@ struct input_definition input_pipe = .type = INPUT_TYPE_PIPE, .disabled = 0, .setup = setup, - .start = start, + .play = play, .stop = stop, .metadata_get = metadata_get, .init = init, diff --git a/src/inputs/spotify.c b/src/inputs/spotify.c index a52ebc07..60cd3ecc 100644 --- a/src/inputs/spotify.c +++ b/src/inputs/spotify.c @@ -31,12 +31,12 @@ #define SPOTIFY_SETUP_RETRY_WAIT 500000 static int -setup(struct player_source *ps) +setup(struct input_source *source) { int i = 0; int ret; - while((ret = spotify_playback_setup(ps->path)) == SPOTIFY_SETUP_ERROR_IS_LOADING) + while((ret = spotify_playback_setup(source->path)) == SPOTIFY_SETUP_ERROR_IS_LOADING) { if (i >= SPOTIFY_SETUP_RETRIES) break; @@ -49,32 +49,15 @@ setup(struct player_source *ps) if (ret < 0) return -1; - ps->setup_done = 1; + ret = spotify_playback_play(); + if (ret < 0) + return -1; return 0; } static int -start(struct player_source *ps) -{ - int ret; - - ret = spotify_playback_play(); - if (ret < 0) - return -1; - - while (!input_loop_break) - { - input_wait(); - } - - ret = spotify_playback_pause(); - - return ret; -} - -static int -stop(struct player_source *ps) +stop(struct input_source *source) { int ret; @@ -82,13 +65,11 @@ stop(struct player_source *ps) if (ret < 0) return -1; - ps->setup_done = 0; - return 0; } static int -seek(struct player_source *ps, int seek_ms) +seek(struct input_source *source, int seek_ms) { int ret; @@ -105,7 +86,6 @@ struct input_definition input_spotify = .type = INPUT_TYPE_SPOTIFY, .disabled = 0, .setup = setup, - .start = start, .stop = stop, .seek = seek, }; diff --git a/src/library/filescanner_ffmpeg.c b/src/library/filescanner_ffmpeg.c index a19cfbd1..f0d14b81 100644 --- a/src/library/filescanner_ffmpeg.c +++ b/src/library/filescanner_ffmpeg.c @@ -416,21 +416,13 @@ scan_metadata_ffmpeg(const char *file, struct media_file_info *mfi) for (i = 0; i < ctx->nb_streams; i++) { -#if HAVE_DECL_AVCODEC_PARAMETERS_FROM_CONTEXT codec_type = ctx->streams[i]->codecpar->codec_type; codec_id = ctx->streams[i]->codecpar->codec_id; sample_rate = ctx->streams[i]->codecpar->sample_rate; sample_fmt = ctx->streams[i]->codecpar->format; -#else - codec_type = ctx->streams[i]->codec->codec_type; - codec_id = ctx->streams[i]->codec->codec_id; - sample_rate = ctx->streams[i]->codec->sample_rate; - sample_fmt = ctx->streams[i]->codec->sample_fmt; -#endif switch (codec_type) { case AVMEDIA_TYPE_VIDEO: -#if LIBAVFORMAT_VERSION_MAJOR >= 55 || (LIBAVFORMAT_VERSION_MAJOR == 54 && LIBAVFORMAT_VERSION_MINOR >= 6) if (ctx->streams[i]->disposition & AV_DISPOSITION_ATTACHED_PIC) { DPRINTF(E_DBG, L_SCAN, "Found embedded artwork (stream %d)\n", i); @@ -438,7 +430,7 @@ scan_metadata_ffmpeg(const char *file, struct media_file_info *mfi) break; } -#endif + // We treat these as audio no matter what if (mfi->compilation || (mfi->media_kind & (MEDIA_KIND_PODCAST | MEDIA_KIND_AUDIOBOOK))) break; diff --git a/src/misc.c b/src/misc.c index 2262c7f2..c0e283e2 100644 --- a/src/misc.c +++ b/src/misc.c @@ -1040,6 +1040,46 @@ murmur_hash64(const void *key, int len, uint32_t seed) #endif +int +linear_regression(double *m, double *b, double *r2, const double *x, const double *y, int n) +{ + double x_val; + double sum_x = 0; + double sum_x2 = 0; + double sum_y = 0; + double sum_y2 = 0; + double sum_xy = 0; + double denom; + int i; + + for (i = 0; i < n; i++) + { + x_val = x ? x[i] : (double)i; + sum_x += x_val; + sum_x2 += x_val * x_val; + sum_y += y[i]; + sum_y2 += y[i] * y[i]; + sum_xy += x_val * y[i]; + } + + denom = (n * sum_x2 - sum_x * sum_x); + if (denom == 0) + return -1; + + *m = (n * sum_xy - sum_x * sum_y) / denom; + *b = (sum_y * sum_x2 - sum_x * sum_xy) / denom; + if (r2) + *r2 = (sum_xy - (sum_x * sum_y)/n) * (sum_xy - (sum_x * sum_y)/n) / ((sum_x2 - (sum_x * sum_x)/n) * (sum_y2 - (sum_y * sum_y)/n)); + + return 0; +} + +bool +quality_is_equal(struct media_quality *a, struct media_quality *b) +{ + return (a->sample_rate == b->sample_rate && a->bits_per_sample == b->bits_per_sample && a->channels == b->channels); +} + bool peer_address_is_trusted(const char *addr) { @@ -1077,6 +1117,86 @@ peer_address_is_trusted(const char *addr) return false; } +int +ringbuffer_init(struct ringbuffer *buf, size_t size) +{ + memset(buf, 0, sizeof(struct ringbuffer)); + + CHECK_NULL(L_MISC, buf->buffer = malloc(size)); + buf->size = size; + buf->write_avail = size; + return 0; +} + +void +ringbuffer_free(struct ringbuffer *buf, bool content_only) +{ + if (!buf) + return; + + free(buf->buffer); + + if (content_only) + memset(buf, 0, sizeof(struct ringbuffer)); + else + free(buf); +} + +size_t +ringbuffer_write(struct ringbuffer *buf, const void* src, size_t srclen) +{ + int remaining; + + if (buf->write_avail == 0 || srclen == 0) + return 0; + + if (srclen > buf->write_avail) + srclen = buf->write_avail; + + remaining = buf->size - buf->write_pos; + if (srclen > remaining) + { + memcpy(buf->buffer + buf->write_pos, src, remaining); + memcpy(buf->buffer, src + remaining, srclen - remaining); + } + else + { + memcpy(buf->buffer + buf->write_pos, src, srclen); + } + + buf->write_pos = (buf->write_pos + srclen) % buf->size; + + buf->write_avail -= srclen; + buf->read_avail += srclen; + + return srclen; +} + +size_t +ringbuffer_read(uint8_t **dst, size_t dstlen, struct ringbuffer *buf) +{ + int remaining; + + *dst = buf->buffer + buf->read_pos; + + if (buf->read_avail == 0 || dstlen == 0) + return 0; + + remaining = buf->size - buf->read_pos; + + // The number of bytes we will return will be MIN(dstlen, remaining, read_avail) + if (dstlen > remaining) + dstlen = remaining; + if (dstlen > buf->read_avail) + dstlen = buf->read_avail; + + buf->read_pos = (buf->read_pos + dstlen) % buf->size; + + buf->write_avail += dstlen; + buf->read_avail -= dstlen; + + return dstlen; +} int clock_gettime_with_res(clockid_t clock_id, struct timespec *tp, struct timespec *res) diff --git a/src/misc.h b/src/misc.h index 0fb6416f..d5791148 100644 --- a/src/misc.h +++ b/src/misc.h @@ -12,11 +12,27 @@ #include /* Samples to bytes, bytes to samples */ -#define STOB(s) ((s) * 4) -#define BTOS(b) ((b) / 4) +#define STOB(s, bits, c) ((s) * (c) * (bits) / 8) +#define BTOS(b, bits, c) ((b) / ((c) * (bits) / 8)) #define ARRAY_SIZE(x) ((unsigned int)(sizeof(x) / sizeof((x)[0]))) +#ifndef MIN +# define MIN(a, b) (((a) < (b)) ? (a) : (b)) +#endif + +#ifndef MAX +# define MAX(a, b) (((a) > (b)) ? (a) : (b)) +#endif + + +// Remember to adjust quality_is_equal() if adding elements +struct media_quality { + int sample_rate; + int bits_per_sample; + int channels; +}; + struct onekeyval { char *name; char *value; @@ -30,6 +46,15 @@ struct keyval { struct onekeyval *tail; }; +struct ringbuffer { + uint8_t *buffer; + size_t size; + size_t write_avail; + size_t read_avail; + size_t write_pos; + size_t read_pos; +}; + char ** buildopts_get(void); @@ -114,10 +139,28 @@ b64_encode(const uint8_t *in, size_t len); uint64_t murmur_hash64(const void *key, int len, uint32_t seed); +int +linear_regression(double *m, double *b, double *r, const double *x, const double *y, int n); + +bool +quality_is_equal(struct media_quality *a, struct media_quality *b); + // Checks if the address is in a network that is configured as trusted bool peer_address_is_trusted(const char *addr); +int +ringbuffer_init(struct ringbuffer *buf, size_t size); + +void +ringbuffer_free(struct ringbuffer *buf, bool content_only); + +size_t +ringbuffer_write(struct ringbuffer *buf, const void* src, size_t srclen); + +size_t +ringbuffer_read(uint8_t **dst, size_t dstlen, struct ringbuffer *buf); + #ifndef HAVE_CLOCK_GETTIME diff --git a/src/outputs.c b/src/outputs.c index e269c761..1fa99109 100644 --- a/src/outputs.c +++ b/src/outputs.c @@ -22,13 +22,22 @@ #include #include +#include #include #include #include #include #include +#include + #include "logger.h" +#include "misc.h" +#include "transcode.h" +#include "listener.h" +#include "db.h" +#include "player.h" //TODO remove me when player_pmap is removed again +#include "worker.h" #include "outputs.h" extern struct output_definition output_raop; @@ -45,6 +54,9 @@ extern struct output_definition output_pulse; extern struct output_definition output_cast; #endif +/* From player.c */ +extern struct event_base *evbase_player; + // Must be in sync with enum output_types static struct output_definition *outputs[] = { &output_raop, @@ -63,38 +75,780 @@ static struct output_definition *outputs[] = { NULL }; -int -outputs_device_start(struct output_device *device, output_status_cb cb, uint64_t rtptime) +// When we stop, we keep the outputs open for a while, just in case we are +// actually just restarting. This timeout determines how long we wait before +// full stop. +// (value is in seconds) +#define OUTPUTS_STOP_TIMEOUT 10 + +#define OUTPUTS_MAX_CALLBACKS 64 + +struct outputs_callback_register { - if (outputs[device->type]->disabled) + output_status_cb cb; + struct output_device *device; + + // We have received the callback with the result from the backend + bool ready; + + // We store a device_id to avoid the risk of dangling device pointer + uint64_t device_id; + enum output_device_state state; +}; + +struct output_quality_subscription +{ + int count; + struct media_quality quality; + struct encode_ctx *encode_ctx; +}; + +static struct outputs_callback_register outputs_cb_register[OUTPUTS_MAX_CALLBACKS]; +static struct event *outputs_deferredev; +static struct timeval outputs_stop_timeout = { OUTPUTS_STOP_TIMEOUT, 0 }; + +// Last element is a zero terminator +static struct output_quality_subscription output_quality_subscriptions[OUTPUTS_MAX_QUALITY_SUBSCRIPTIONS + 1]; +static bool outputs_got_new_subscription; + + +/* ------------------------------- MISC HELPERS ----------------------------- */ + +static output_status_cb +callback_get(struct output_device *device) +{ + int callback_id; + + for (callback_id = 0; callback_id < ARRAY_SIZE(outputs_cb_register); callback_id++) + { + if (outputs_cb_register[callback_id].device == device) + return outputs_cb_register[callback_id].cb; + } + + return NULL; +} + +static void +callback_remove(struct output_device *device) +{ + int callback_id; + + if (!device) + return; + + for (callback_id = 0; callback_id < ARRAY_SIZE(outputs_cb_register); callback_id++) + { + if (outputs_cb_register[callback_id].device == device) + { + DPRINTF(E_DBG, L_PLAYER, "Removing callback to %s, id %d\n", player_pmap(outputs_cb_register[callback_id].cb), callback_id); + memset(&outputs_cb_register[callback_id], 0, sizeof(struct outputs_callback_register)); + } + } +} + +static int +callback_add(struct output_device *device, output_status_cb cb) +{ + int callback_id; + + if (!cb) return -1; - if (outputs[device->type]->device_start) - return outputs[device->type]->device_start(device, cb, rtptime); + // We will replace any previously registered callbacks, since that's what the + // player expects + callback_remove(device); + + // Find a free slot in the queue + for (callback_id = 0; callback_id < ARRAY_SIZE(outputs_cb_register); callback_id++) + { + if (outputs_cb_register[callback_id].cb == NULL) + break; + } + + if (callback_id == ARRAY_SIZE(outputs_cb_register)) + { + DPRINTF(E_LOG, L_PLAYER, "Output callback queue is full! (size is %d)\n", OUTPUTS_MAX_CALLBACKS); + return -1; + } + + outputs_cb_register[callback_id].cb = cb; + outputs_cb_register[callback_id].device = device; // Don't dereference this later, it might become invalid! + + DPRINTF(E_DBG, L_PLAYER, "Registered callback to %s with id %d (device %p, %s)\n", player_pmap(cb), callback_id, device, device->name); + + int active = 0; + for (int i = 0; i < ARRAY_SIZE(outputs_cb_register); i++) + if (outputs_cb_register[i].cb) + active++; + + DPRINTF(E_DBG, L_PLAYER, "Number of active callbacks: %d\n", active); + + return callback_id; +}; + +static void +deferred_cb(int fd, short what, void *arg) +{ + struct output_device *device; + output_status_cb cb; + enum output_device_state state; + int callback_id; + + for (callback_id = 0; callback_id < ARRAY_SIZE(outputs_cb_register); callback_id++) + { + if (outputs_cb_register[callback_id].ready) + { + // Must copy before making callback, since you never know what the + // callback might result in (could call back in) + cb = outputs_cb_register[callback_id].cb; + state = outputs_cb_register[callback_id].state; + + // Will be NULL if the device has disappeared + device = outputs_device_get(outputs_cb_register[callback_id].device_id); + + memset(&outputs_cb_register[callback_id], 0, sizeof(struct outputs_callback_register)); + + // The device has left the building (stopped/failed), and the backend + // is not using it any more + if (!device->advertised && !device->session) + { + outputs_device_remove(device); + device = NULL; + } + + DPRINTF(E_DBG, L_PLAYER, "Making deferred callback to %s, id was %d\n", player_pmap(cb), callback_id); + + cb(device, state); + } + } + + for (int i = 0; i < ARRAY_SIZE(outputs_cb_register); i++) + { + if (outputs_cb_register[i].cb) + DPRINTF(E_DBG, L_PLAYER, "%d. Active callback: %s\n", i, player_pmap(outputs_cb_register[i].cb)); + } +} + +static void +stop_timer_cb(int fd, short what, void *arg) +{ + struct output_device *device = arg; + output_status_cb cb = callback_get(device); + + outputs_device_stop(device, cb); +} + +static void +device_stop_cb(struct output_device *device, enum output_device_state status) +{ + if (status == OUTPUT_STATE_FAILED) + DPRINTF(E_WARN, L_PLAYER, "Failed to stop device\n"); else + DPRINTF(E_INFO, L_PLAYER, "Device stopped properly\n"); +} + +static enum transcode_profile +quality_to_xcode(struct media_quality *quality) +{ + if (quality->bits_per_sample == 16) + return XCODE_PCM16; + if (quality->bits_per_sample == 24) + return XCODE_PCM24; + if (quality->bits_per_sample == 32) + return XCODE_PCM32; + + return XCODE_UNKNOWN; +} + +static int +encoding_reset(struct media_quality *quality) +{ + struct output_quality_subscription *subscription; + struct decode_ctx *decode_ctx; + enum transcode_profile profile; + int i; + + profile = quality_to_xcode(quality); + if (profile == XCODE_UNKNOWN) + { + DPRINTF(E_LOG, L_PLAYER, "Could not create subscription decoding context, invalid quality (%d/%d/%d)\n", + quality->sample_rate, quality->bits_per_sample, quality->channels); + return -1; + } + + decode_ctx = transcode_decode_setup_raw(profile, quality); + if (!decode_ctx) + { + DPRINTF(E_LOG, L_PLAYER, "Could not create subscription decoding context (profile %d)\n", profile); + return -1; + } + + for (i = 0; output_quality_subscriptions[i].count > 0; i++) + { + subscription = &output_quality_subscriptions[i]; // Just for short-hand + + transcode_encode_cleanup(&subscription->encode_ctx); // Will also point the ctx to NULL + + if (quality_is_equal(quality, &subscription->quality)) + continue; // No resampling required + + profile = quality_to_xcode(&subscription->quality); + if (profile != XCODE_UNKNOWN) + subscription->encode_ctx = transcode_encode_setup(profile, &subscription->quality, decode_ctx, NULL, 0, 0); + else + DPRINTF(E_LOG, L_PLAYER, "Could not setup resampling to %d/%d/%d for output\n", + subscription->quality.sample_rate, subscription->quality.bits_per_sample, subscription->quality.channels); + } + + transcode_decode_cleanup(&decode_ctx); + + return 0; +} + +static void +buffer_fill(struct output_buffer *obuf, void *buf, size_t bufsize, struct media_quality *quality, int nsamples, struct timespec *pts) +{ + transcode_frame *frame; + int ret; + int i; + int n; + + obuf->write_counter++; + obuf->pts = *pts; + + // The resampling/encoding (transcode) contexts work for a given input quality, + // so if the quality changes we need to reset the contexts. We also do that if + // we have received a subscription for a new quality. + if (!quality_is_equal(quality, &obuf->data[0].quality) || outputs_got_new_subscription) + { + encoding_reset(quality); + outputs_got_new_subscription = false; + } + + // The first element of the output_buffer is always just the raw input data + // TODO can we avoid the copy below? we can't use evbuffer_add_buffer_reference, + // because then the outputs can't use it and we would need to copy there instead + evbuffer_add(obuf->data[0].evbuf, buf, bufsize); + obuf->data[0].buffer = buf; + obuf->data[0].bufsize = bufsize; + obuf->data[0].quality = *quality; + obuf->data[0].samples = nsamples; + + for (i = 0, n = 1; output_quality_subscriptions[i].count > 0; i++) + { + if (quality_is_equal(&output_quality_subscriptions[i].quality, quality)) + continue; // Skip, no resampling required and we have the data in element 0 + + if (!output_quality_subscriptions[i].encode_ctx) + continue; + + frame = transcode_frame_new(buf, bufsize, nsamples, quality); + if (!frame) + continue; + + ret = transcode_encode(obuf->data[n].evbuf, output_quality_subscriptions[i].encode_ctx, frame, 0); + transcode_frame_free(frame); + if (ret < 0) + continue; + + obuf->data[n].buffer = evbuffer_pullup(obuf->data[n].evbuf, -1); + obuf->data[n].bufsize = evbuffer_get_length(obuf->data[n].evbuf); + obuf->data[n].quality = output_quality_subscriptions[i].quality; + obuf->data[n].samples = BTOS(obuf->data[n].bufsize, obuf->data[n].quality.bits_per_sample, obuf->data[n].quality.channels); + n++; + } +} + +static void +buffer_drain(struct output_buffer *obuf) +{ + int i; + + for (i = 0; obuf->data[i].buffer; i++) + { + evbuffer_drain(obuf->data[i].evbuf, obuf->data[i].bufsize); + obuf->data[i].buffer = NULL; + obuf->data[i].bufsize = 0; + // We don't reset quality and samples, would be a waste of time + } +} + +static void +device_list_sort(void) +{ + struct output_device *device; + struct output_device *next; + struct output_device *prev; + int swaps; + + // Swap sorting since even the most inefficient sorting should do fine here + do + { + swaps = 0; + prev = NULL; + for (device = output_device_list; device && device->next; device = device->next) + { + next = device->next; + if ( (outputs_priority(device) > outputs_priority(next)) || + (outputs_priority(device) == outputs_priority(next) && strcasecmp(device->name, next->name) > 0) ) + { + if (device == output_device_list) + output_device_list = next; + if (prev) + prev->next = next; + + device->next = next->next; + next->next = device; + swaps++; + } + prev = device; + } + } + while (swaps > 0); +} + +static void +metadata_cb_send(int fd, short what, void *arg) +{ + struct output_metadata *metadata = arg; + int ret; + + event_free(metadata->ev); + metadata->ev = NULL; + + ret = metadata->finalize_cb(metadata); + if (ret < 0) + return; + + outputs[metadata->type]->metadata_send(metadata); +} + +// *** Worker thread *** +static void +metadata_cb_prepare(void *arg) +{ + struct output_metadata *metadata = *((struct output_metadata **)arg); + + metadata->priv = outputs[metadata->type]->metadata_prepare(metadata); + if (!metadata->priv) + { + event_free(metadata->ev); + free(metadata); + return; + } + + // Metadata is prepared, let the player thread do the actual sending + event_active(metadata->ev, 0, 0); +} + +static void +metadata_send(enum output_types type, uint32_t item_id, bool startup, output_metadata_finalize_cb cb) +{ + struct output_metadata *metadata; + + CHECK_NULL(L_PLAYER, metadata = calloc(1, sizeof(struct output_metadata))); + + metadata->type = type; + metadata->item_id = item_id; + metadata->startup = startup; + metadata->finalize_cb = cb; + + metadata->ev = event_new(evbase_player, -1, 0, metadata_cb_send, metadata); + + if (outputs[type]->metadata_prepare) + worker_execute(metadata_cb_prepare, &metadata, sizeof(struct output_metadata *), 0); + else + outputs[type]->metadata_send(metadata); +} + + +/* ----------------------------------- API ---------------------------------- */ + +struct output_device * +outputs_device_get(uint64_t device_id) +{ + struct output_device *device; + + for (device = output_device_list; device; device = device->next) + { + if (device_id == device->id) + return device; + } + + DPRINTF(E_LOG, L_PLAYER, "Output device with id %" PRIu64 " has disappeared from our list\n", device_id); + return NULL; +} + +/* ----------------------- Called by backend modules ------------------------ */ + +// Sessions free their sessions themselves, but should not touch the device, +// since they can't know for sure that it is still valid in memory +int +outputs_device_session_add(uint64_t device_id, void *session) +{ + struct output_device *device; + + device = outputs_device_get(device_id); + if (!device) return -1; + + device->session = session; + return 0; } void -outputs_device_stop(struct output_session *session) +outputs_device_session_remove(uint64_t device_id) { - if (outputs[session->type]->disabled) + struct output_device *device; + + device = outputs_device_get(device_id); + if (device) + device->session = NULL; + + return; +} + +int +outputs_quality_subscribe(struct media_quality *quality) +{ + int i; + + // If someone else is already subscribing to this quality we just increase the + // reference count. + for (i = 0; output_quality_subscriptions[i].count > 0; i++) + { + if (!quality_is_equal(quality, &output_quality_subscriptions[i].quality)) + continue; + + output_quality_subscriptions[i].count++; + + DPRINTF(E_DBG, L_PLAYER, "Subscription request for quality %d/%d/%d (now %d subscribers)\n", + quality->sample_rate, quality->bits_per_sample, quality->channels, output_quality_subscriptions[i].count); + + return 0; + } + + if (i >= (ARRAY_SIZE(output_quality_subscriptions) - 1)) + { + DPRINTF(E_LOG, L_PLAYER, "Bug! The number of different quality levels requested by outputs is too high\n"); + return -1; + } + + output_quality_subscriptions[i].quality = *quality; + output_quality_subscriptions[i].count++; + + DPRINTF(E_DBG, L_PLAYER, "Subscription request for quality %d/%d/%d (now %d subscribers)\n", + quality->sample_rate, quality->bits_per_sample, quality->channels, output_quality_subscriptions[i].count); + + // Better way of signaling this? + outputs_got_new_subscription = true; + + return 0; +} + +void +outputs_quality_unsubscribe(struct media_quality *quality) +{ + int i; + + // Find subscription + for (i = 0; output_quality_subscriptions[i].count > 0; i++) + { + if (quality_is_equal(quality, &output_quality_subscriptions[i].quality)) + break; + } + + if (output_quality_subscriptions[i].count == 0) + { + DPRINTF(E_LOG, L_PLAYER, "Bug! Unsubscription request for a quality level that there is no subscription for\n"); + return; + } + + output_quality_subscriptions[i].count--; + + DPRINTF(E_DBG, L_PLAYER, "Unsubscription request for quality %d/%d/%d (now %d subscribers)\n", + quality->sample_rate, quality->bits_per_sample, quality->channels, output_quality_subscriptions[i].count); + + if (output_quality_subscriptions[i].count > 0) return; - if (outputs[session->type]->device_stop) - outputs[session->type]->device_stop(session); + transcode_encode_cleanup(&output_quality_subscriptions[i].encode_ctx); + + // Shift elements + for (; i < ARRAY_SIZE(output_quality_subscriptions) - 1; i++) + output_quality_subscriptions[i] = output_quality_subscriptions[i + 1]; +} + +// Output backends call back through the below wrapper to make sure that: +// 1. Callbacks are always deferred +// 2. The callback never has a dangling pointer to a device (a device that has been removed from our list) +void +outputs_cb(int callback_id, uint64_t device_id, enum output_device_state state) +{ + if (callback_id < 0) + return; + + if (!(callback_id < ARRAY_SIZE(outputs_cb_register)) || !outputs_cb_register[callback_id].cb) + { + DPRINTF(E_LOG, L_PLAYER, "Bug! Output backend called us with an illegal callback id (%d)\n", callback_id); + return; + } + + DPRINTF(E_DBG, L_PLAYER, "Callback request received, id is %i\n", callback_id); + + outputs_cb_register[callback_id].ready = true; + outputs_cb_register[callback_id].device_id = device_id; + outputs_cb_register[callback_id].state = state; + event_active(outputs_deferredev, 0, 0); +} + +// Maybe not so great, seems it would be better if integrated into the callback +// mechanism so that the notifications where at least deferred +void +outputs_listener_notify(void) +{ + listener_notify(LISTENER_SPEAKER); +} + + +/* ---------------------------- Called by player ---------------------------- */ + +struct output_device * +outputs_device_add(struct output_device *add, bool new_deselect, int default_volume) +{ + struct output_device *device; + char *keep_name; + int ret; + + for (device = output_device_list; device; device = device->next) + { + if (device->id == add->id) + break; + } + + // New device + if (!device) + { + device = add; + + device->stop_timer = evtimer_new(evbase_player, stop_timer_cb, device); + + keep_name = strdup(device->name); + ret = db_speaker_get(device, device->id); + if (ret < 0) + { + device->selected = 0; + device->volume = default_volume; + } + + free(device->name); + device->name = keep_name; + + if (new_deselect) + device->selected = 0; + + device->next = output_device_list; + output_device_list = device; + } + // Update to a device already in the list + else + { + if (add->v4_address) + { + free(device->v4_address); + + device->v4_address = add->v4_address; + device->v4_port = add->v4_port; + + // Address is ours now + add->v4_address = NULL; + } + + if (add->v6_address) + { + free(device->v6_address); + + device->v6_address = add->v6_address; + device->v6_port = add->v6_port; + + // Address is ours now + add->v6_address = NULL; + } + + free(device->name); + device->name = add->name; + add->name = NULL; + + device->has_password = add->has_password; + device->password = add->password; + + outputs_device_free(add); + } + + device_list_sort(); + + device->advertised = 1; + + listener_notify(LISTENER_SPEAKER); + + return device; +} + +void +outputs_device_remove(struct output_device *remove) +{ + struct output_device *device; + struct output_device *prev; + int ret; + + // Device stop should be able to handle that we invalidate the device, even + // if it is an async stop. It might call outputs_device_session_remove(), but + // that just won't do anything since the id will be unknown. + if (remove->session) + outputs_device_stop(remove, device_stop_cb); + + prev = NULL; + for (device = output_device_list; device; device = device->next) + { + if (device == remove) + break; + + prev = device; + } + + if (!device) + return; + + // Save device volume + ret = db_speaker_save(remove); + if (ret < 0) + DPRINTF(E_LOG, L_PLAYER, "Could not save state for %s device '%s'\n", remove->type_name, remove->name); + + DPRINTF(E_INFO, L_PLAYER, "Removing %s device '%s'; stopped advertising\n", remove->type_name, remove->name); + + if (!prev) + output_device_list = remove->next; + else + prev->next = remove->next; + + outputs_device_free(remove); + + listener_notify(LISTENER_SPEAKER); +} + +int +outputs_device_start(struct output_device *device, output_status_cb cb) +{ + if (outputs[device->type]->disabled || !outputs[device->type]->device_start) + return -1; + + if (device->session) + { + DPRINTF(E_LOG, L_PLAYER, "Bug! outputs_device_start() called for a device that already has a session\n"); + return -1; + } + + return outputs[device->type]->device_start(device, callback_add(device, cb)); +} + +int +outputs_device_stop(struct output_device *device, output_status_cb cb) +{ + if (outputs[device->type]->disabled || !outputs[device->type]->device_stop) + return -1; + + if (!device->session) + { + DPRINTF(E_LOG, L_PLAYER, "Bug! outputs_device_stop() called for a device that has no session\n"); + return -1; + } + + return outputs[device->type]->device_stop(device, callback_add(device, cb)); +} + +int +outputs_device_stop_delayed(struct output_device *device, output_status_cb cb) +{ + if (outputs[device->type]->disabled || !outputs[device->type]->device_stop) + return -1; + + if (!device->session) + { + DPRINTF(E_LOG, L_PLAYER, "Bug! outputs_device_stop_delayed() called for a device that has no session\n"); + return -1; + } + + outputs[device->type]->device_cb_set(device, callback_add(device, cb)); + + event_add(device->stop_timer, &outputs_stop_timeout); + + return 0; +} + +int +outputs_device_flush(struct output_device *device, output_status_cb cb) +{ + if (outputs[device->type]->disabled || !outputs[device->type]->device_flush) + return -1; + + if (!device->session) + return -1; + + return outputs[device->type]->device_flush(device, callback_add(device, cb)); } int outputs_device_probe(struct output_device *device, output_status_cb cb) { - if (outputs[device->type]->disabled) + if (outputs[device->type]->disabled || !outputs[device->type]->device_probe) return -1; - if (outputs[device->type]->device_probe) - return outputs[device->type]->device_probe(device, cb); - else + if (device->session) + { + DPRINTF(E_LOG, L_PLAYER, "Bug! outputs_device_probe() called for a device that already has a session\n"); + return -1; + } + + return outputs[device->type]->device_probe(device, callback_add(device, cb)); +} + +int +outputs_device_volume_set(struct output_device *device, output_status_cb cb) +{ + if (outputs[device->type]->disabled || !outputs[device->type]->device_volume_set) return -1; + + return outputs[device->type]->device_volume_set(device, callback_add(device, cb)); +} + +int +outputs_device_volume_to_pct(struct output_device *device, const char *volume) +{ + if (outputs[device->type]->disabled || !outputs[device->type]->device_volume_to_pct) + return -1; + + return outputs[device->type]->device_volume_to_pct(device, volume); +} + +int +outputs_device_quality_set(struct output_device *device, struct media_quality *quality, output_status_cb cb) +{ + if (outputs[device->type]->disabled || !outputs[device->type]->device_quality_set) + return -1; + + return outputs[device->type]->device_quality_set(device, quality, callback_add(device, cb)); +} + +void +outputs_device_cb_set(struct output_device *device, output_status_cb cb) +{ + if (outputs[device->type]->disabled || !outputs[device->type]->device_cb_set) + return; + + if (!device->session) + return; + + outputs[device->type]->device_cb_set(device, callback_add(device, cb)); } void @@ -112,6 +866,9 @@ outputs_device_free(struct output_device *device) if (outputs[device->type]->device_free_extra) outputs[device->type]->device_free_extra(device); + if (device->stop_timer) + event_free(device->stop_timer); + free(device->name); free(device->auth_key); free(device->v4_address); @@ -121,161 +878,87 @@ outputs_device_free(struct output_device *device) } int -outputs_device_volume_set(struct output_device *device, output_status_cb cb) +outputs_flush(output_status_cb cb) { - if (outputs[device->type]->disabled) - return -1; + struct output_device *device; + int count = 0; + int ret; - if (outputs[device->type]->device_volume_set) - return outputs[device->type]->device_volume_set(device, cb); - else - return -1; + for (device = output_device_list; device; device = device->next) + { + ret = outputs_device_flush(device, cb); + if (ret < 0) + continue; + + count++; + } + + return count; } int -outputs_device_volume_to_pct(struct output_device *device, const char *volume) +outputs_stop(output_status_cb cb) { - if (outputs[device->type]->disabled) - return -1; + struct output_device *device; + int count = 0; + int ret; - if (outputs[device->type]->device_volume_to_pct) - return outputs[device->type]->device_volume_to_pct(device, volume); - else - return -1; -} - -void -outputs_playback_start(uint64_t next_pkt, struct timespec *ts) -{ - int i; - - for (i = 0; outputs[i]; i++) + for (device = output_device_list; device; device = device->next) { - if (outputs[i]->disabled) + if (!device->session) continue; - if (outputs[i]->playback_start) - outputs[i]->playback_start(next_pkt, ts); - } -} - -void -outputs_playback_stop(void) -{ - int i; - - for (i = 0; outputs[i]; i++) - { - if (outputs[i]->disabled) + ret = outputs_device_stop(device, cb); + if (ret < 0) continue; - if (outputs[i]->playback_stop) - outputs[i]->playback_stop(); + count++; } + + return count; +} + +int +outputs_stop_delayed_cancel(void) +{ + struct output_device *device; + + for (device = output_device_list; device; device = device->next) + event_del(device->stop_timer); + + return 0; } void -outputs_write(uint8_t *buf, uint64_t rtptime) +outputs_write(void *buf, size_t bufsize, int nsamples, struct media_quality *quality, struct timespec *pts) { int i; + buffer_fill(&output_buffer, buf, bufsize, quality, nsamples, pts); + for (i = 0; outputs[i]; i++) { if (outputs[i]->disabled) continue; if (outputs[i]->write) - outputs[i]->write(buf, rtptime); - } -} - -int -outputs_flush(output_status_cb cb, uint64_t rtptime) -{ - int ret; - int i; - - ret = 0; - for (i = 0; outputs[i]; i++) - { - if (outputs[i]->disabled) - continue; - - if (outputs[i]->flush) - ret += outputs[i]->flush(cb, rtptime); + outputs[i]->write(&output_buffer); } - return ret; + buffer_drain(&output_buffer); } void -outputs_status_cb(struct output_session *session, output_status_cb cb) +outputs_metadata_send(uint32_t item_id, bool startup, output_metadata_finalize_cb cb) { - if (outputs[session->type]->disabled) - return; - - if (outputs[session->type]->status_cb) - outputs[session->type]->status_cb(session, cb); -} - -struct output_metadata * -outputs_metadata_prepare(int id) -{ - struct output_metadata *omd; - struct output_metadata *new; - void *metadata; - int i; - - omd = NULL; - for (i = 0; outputs[i]; i++) - { - if (outputs[i]->disabled) - continue; - - if (!outputs[i]->metadata_prepare) - continue; - - metadata = outputs[i]->metadata_prepare(id); - if (!metadata) - continue; - - new = calloc(1, sizeof(struct output_metadata)); - if (!new) - return omd; - - if (omd) - new->next = omd; - omd = new; - omd->type = i; - omd->metadata = metadata; - } - - return omd; -} - -void -outputs_metadata_send(struct output_metadata *omd, uint64_t rtptime, uint64_t offset, int startup) -{ - struct output_metadata *ptr; int i; for (i = 0; outputs[i]; i++) { - if (outputs[i]->disabled) + if (outputs[i]->disabled || !outputs[i]->metadata_send) continue; - if (!outputs[i]->metadata_send) - continue; - - // Go through linked list to find appropriate metadata for type - for (ptr = omd; ptr; ptr = ptr->next) - if (ptr->type == i) - break; - - if (!ptr) - continue; - - outputs[i]->metadata_send(ptr->metadata, rtptime, offset, startup); + metadata_send(i, item_id, startup, cb); } } @@ -286,41 +969,10 @@ outputs_metadata_purge(void) for (i = 0; outputs[i]; i++) { - if (outputs[i]->disabled) + if (outputs[i]->disabled || !outputs[i]->metadata_purge) continue; - if (outputs[i]->metadata_purge) - outputs[i]->metadata_purge(); - } -} - -void -outputs_metadata_prune(uint64_t rtptime) -{ - int i; - - for (i = 0; outputs[i]; i++) - { - if (outputs[i]->disabled) - continue; - - if (outputs[i]->metadata_prune) - outputs[i]->metadata_prune(rtptime); - } -} - -void -outputs_metadata_free(struct output_metadata *omd) -{ - struct output_metadata *ptr; - - if (!omd) - return; - - for (ptr = omd; omd; ptr = omd) - { - omd = ptr->next; - free(ptr); + outputs[i]->metadata_purge(); } } @@ -353,6 +1005,8 @@ outputs_init(void) int ret; int i; + CHECK_NULL(L_PLAYER, outputs_deferredev = evtimer_new(evbase_player, deferred_cb, NULL)); + no_output = 1; for (i = 0; outputs[i]; i++) { @@ -378,6 +1032,9 @@ outputs_init(void) if (no_output) return -1; + for (i = 0; i < ARRAY_SIZE(output_buffer.data); i++) + output_buffer.data[i].evbuf = evbuffer_new(); + return 0; } @@ -386,6 +1043,8 @@ outputs_deinit(void) { int i; + evtimer_del(outputs_deferredev); + for (i = 0; outputs[i]; i++) { if (outputs[i]->disabled) @@ -394,5 +1053,16 @@ outputs_deinit(void) if (outputs[i]->deinit) outputs[i]->deinit(); } + + // In case some outputs forgot to unsubscribe + for (i = 0; i < ARRAY_SIZE(output_quality_subscriptions); i++) + if (output_quality_subscriptions[i].count > 0) + { + transcode_encode_cleanup(&output_quality_subscriptions[i].encode_ctx); + memset(&output_quality_subscriptions[i], 0, sizeof(struct output_quality_subscription)); + } + + for (i = 0; i < ARRAY_SIZE(output_buffer.data); i++) + evbuffer_free(output_buffer.data[i].evbuf); } diff --git a/src/outputs.h b/src/outputs.h index 48d91ff1..2afc9362 100644 --- a/src/outputs.h +++ b/src/outputs.h @@ -2,7 +2,11 @@ #ifndef __OUTPUTS_H__ #define __OUTPUTS_H__ +#include #include +#include +#include +#include "misc.h" /* Outputs is a generic interface between the player and a media output method, * like for instance AirPlay (raop) or ALSA. The purpose of the interface is to @@ -19,7 +23,6 @@ * Here is the sequence of commands from the player to the outputs, and the * callback from the output once the command has been executed. Commands marked * with * may make multiple callbacks if multiple sessions are affected. - * (TODO should callbacks always be deferred?) * * PLAYER OUTPUT PLAYER CB * speaker_activate -> device_start -> device_activate_cb @@ -46,6 +49,28 @@ * */ +// If an output requires a specific quality (like Airplay 1 devices often +// require 44100/16) then it should make a subscription request to the output +// module, which will then make sure to include this quality when it writes the +// audio. The below sets the maximum number of *different* subscriptions +// allowed. Note that multiple outputs requesting the *same* quality only counts +// as one. +#define OUTPUTS_MAX_QUALITY_SUBSCRIPTIONS 5 + +// Number of seconds the outputs should buffer before starting playback. Note +// this value cannot freely be changed because 1) some Airplay devices ignore +// the values we give and stick to 2 seconds, 2) those devices that can handle +// different values can only do so within a limited range (maybe max 3 secs) +#define OUTPUTS_BUFFER_DURATION 2 + +// Forward declarations +struct output_device; +struct output_metadata; +enum output_device_state; + +typedef void (*output_status_cb)(struct output_device *device, enum output_device_state status); +typedef int (*output_metadata_finalize_cb)(struct output_metadata *metadata); + // Must be in sync with outputs[] in outputs.c enum output_types { @@ -113,35 +138,59 @@ struct output_device int volume; int relvol; + // Quality of audio output + struct media_quality quality; + // Address char *v4_address; char *v6_address; short v4_port; short v6_port; + struct event *stop_timer; + // Opaque pointers to device and session data void *extra_device_info; - struct output_session *session; + void *session; struct output_device *next; }; -// Except for the type, sessions are opaque outside of the output backend -struct output_session -{ - enum output_types type; - void *session; -}; - -// Linked list of metadata prepared by each output backend struct output_metadata { enum output_types type; - void *metadata; - struct output_metadata *next; + uint32_t item_id; + + // Progress data, filled out by finalize_cb() + uint32_t pos_ms; + uint32_t len_ms; + struct timespec pts; + bool startup; + + // Private output data made by the metadata_prepare() + void *priv; + + struct event *ev; + + // Finalize before right before sending, e.g. set playback position + output_metadata_finalize_cb finalize_cb; }; -typedef void (*output_status_cb)(struct output_device *device, struct output_session *session, enum output_device_state status); +struct output_data +{ + struct media_quality quality; + struct evbuffer *evbuf; + uint8_t *buffer; + size_t bufsize; + int samples; +}; + +struct output_buffer +{ + uint32_t write_counter; // REMOVE ME? not used for anything + struct timespec pts; + struct output_data data[OUTPUTS_MAX_QUALITY_SUBSCRIPTIONS + 1]; +} output_buffer; struct output_definition { @@ -166,94 +215,137 @@ struct output_definition void (*deinit)(void); // Prepare a playback session on device and call back - int (*device_start)(struct output_device *device, output_status_cb cb, uint64_t rtptime); + int (*device_start)(struct output_device *device, int callback_id); - // Close a session prepared by device_start - void (*device_stop)(struct output_session *session); + // Close a session prepared by device_start and call back + int (*device_stop)(struct output_device *device, int callback_id); + + // Flush device session and call back + int (*device_flush)(struct output_device *device, int callback_id); // Test the connection to a device and call back - int (*device_probe)(struct output_device *device, output_status_cb cb); - - // Free the private device data - void (*device_free_extra)(struct output_device *device); + int (*device_probe)(struct output_device *device, int callback_id); // Set the volume and call back - int (*device_volume_set)(struct output_device *device, output_status_cb cb); + int (*device_volume_set)(struct output_device *device, int callback_id); // Convert device internal representation of volume to our pct scale int (*device_volume_to_pct)(struct output_device *device, const char *volume); - // Start/stop playback on devices that were started - void (*playback_start)(uint64_t next_pkt, struct timespec *ts); - void (*playback_stop)(void); + // Request a change of quality from the device + int (*device_quality_set)(struct output_device *device, struct media_quality *quality, int callback_id); + + // Change the call back associated with a device + void (*device_cb_set)(struct output_device *device, int callback_id); + + // Free the private device data + void (*device_free_extra)(struct output_device *device); // Write stream data to the output devices - void (*write)(uint8_t *buf, uint64_t rtptime); - - // Flush all sessions, the return must be number of sessions pending the flush - int (*flush)(output_status_cb cb, uint64_t rtptime); + void (*write)(struct output_buffer *buffer); // Authorize an output with a pin-code (probably coming from the filescanner) void (*authorize)(const char *pin); - // Change the call back associated with a session - void (*status_cb)(struct output_session *session, output_status_cb cb); + // Called from worker thread for async preparation of metadata (e.g. getting + // artwork, which might involce downloading image data). The prepared data is + // saved to metadata->data, which metadata_send() can use. + void *(*metadata_prepare)(struct output_metadata *metadata); - // Metadata - void *(*metadata_prepare)(int id); - void (*metadata_send)(void *metadata, uint64_t rtptime, uint64_t offset, int startup); + // Send metadata to outputs. Ownership of *metadata is transferred. + void (*metadata_send)(struct output_metadata *metadata); + + // Output will cleanup all metadata (so basically like flush but for metadata) void (*metadata_purge)(void); - void (*metadata_prune)(uint64_t rtptime); }; +// Our main list of devices, not for use by backend modules +struct output_device *output_device_list; + +/* ------------------------------- General use ------------------------------ */ + +struct output_device * +outputs_device_get(uint64_t device_id); + +/* ----------------------- Called by backend modules ------------------------ */ + int -outputs_device_start(struct output_device *device, output_status_cb cb, uint64_t rtptime); +outputs_device_session_add(uint64_t device_id, void *session); void -outputs_device_stop(struct output_session *session); +outputs_device_session_remove(uint64_t device_id); + +int +outputs_quality_subscribe(struct media_quality *quality); + +void +outputs_quality_unsubscribe(struct media_quality *quality); + +void +outputs_cb(int callback_id, uint64_t device_id, enum output_device_state); + +void +outputs_listener_notify(void); + +/* ---------------------------- Called by player ---------------------------- */ + +// Ownership of *add is transferred, so don't address after calling. Instead you +// can address the return value (which is not the same if the device was already +// in the list. +struct output_device * +outputs_device_add(struct output_device *add, bool new_deselect, int default_volume); + +void +outputs_device_remove(struct output_device *remove); + +int +outputs_device_start(struct output_device *device, output_status_cb cb); + +int +outputs_device_stop(struct output_device *device, output_status_cb cb); + +int +outputs_device_stop_delayed(struct output_device *device, output_status_cb cb); + +int +outputs_device_flush(struct output_device *device, output_status_cb cb); int outputs_device_probe(struct output_device *device, output_status_cb cb); -void -outputs_device_free(struct output_device *device); - int outputs_device_volume_set(struct output_device *device, output_status_cb cb); int outputs_device_volume_to_pct(struct output_device *device, const char *value); -void -outputs_playback_start(uint64_t next_pkt, struct timespec *ts); +int +outputs_device_quality_set(struct output_device *device, struct media_quality *quality, output_status_cb cb); void -outputs_playback_stop(void); +outputs_device_cb_set(struct output_device *device, output_status_cb cb); void -outputs_write(uint8_t *buf, uint64_t rtptime); +outputs_device_free(struct output_device *device); int -outputs_flush(output_status_cb cb, uint64_t rtptime); +outputs_flush(output_status_cb cb); + +int +outputs_stop(output_status_cb cb); + +int +outputs_stop_delayed_cancel(void); void -outputs_status_cb(struct output_session *session, output_status_cb cb); - -struct output_metadata * -outputs_metadata_prepare(int id); +outputs_write(void *buf, size_t bufsize, int nsamples, struct media_quality *quality, struct timespec *pts); void -outputs_metadata_send(struct output_metadata *omd, uint64_t rtptime, uint64_t offset, int startup); +outputs_metadata_send(uint32_t item_id, bool startup, output_metadata_finalize_cb cb); void outputs_metadata_purge(void); -void -outputs_metadata_prune(uint64_t rtptime); - -void -outputs_metadata_free(struct output_metadata *omd); - void outputs_authorize(enum output_types type, const char *pin); diff --git a/src/outputs/alsa.c b/src/outputs/alsa.c index 50ef5d93..d076b74d 100644 --- a/src/outputs/alsa.c +++ b/src/outputs/alsa.c @@ -1,5 +1,5 @@ /* - * Copyright (C) 2015-2016 Espen Jürgensen + * Copyright (C) 2015-2019 Espen Jürgensen * Copyright (C) 2010 Julien BLACHE * * This program is free software; you can redistribute it and/or modify @@ -29,7 +29,6 @@ #include #include -#include #include #include "misc.h" @@ -38,37 +37,28 @@ #include "player.h" #include "outputs.h" -#define PACKET_SIZE STOB(AIRTUNES_V2_PACKET_SAMPLES) -// The maximum number of samples that the output is allowed to get behind (or -// ahead) of the player position, before compensation is attempted -#define ALSA_MAX_LATENCY 352 +// We measure latency each second, and after a number of measurements determined +// by adjust_period_seconds we try to determine drift and latency. If both are +// below the two thresholds set by the below, we don't do anything. Otherwise we +// may attempt compensation by resampling. Latency is measured in samples, and +// drift is change of latency per second. Both are floats. +#define ALSA_MAX_LATENCY 480.0 +#define ALSA_MAX_DRIFT 16.0 // If latency is jumping up and down we don't do compensation since we probably -// wouldn't do a good job. This sets the maximum the latency is allowed to vary -// within the 10 seconds where we measure latency each second. -#define ALSA_MAX_LATENCY_VARIANCE 352 +// wouldn't do a good job. We use linear regression to determine the trend, but +// if r2 is below this value we won't attempt to correct sync. +#define ALSA_MAX_VARIANCE 0.3 -// TODO Unglobalise these and add support for multiple sound cards -static char *card_name; -static char *mixer_name; -static char *mixer_device_name; -static snd_pcm_t *hdl; -static snd_mixer_t *mixer_hdl; -static snd_mixer_elem_t *vol_elem; -static long vol_min; -static long vol_max; -static int offset; -static int adjust_period_seconds; +// We correct latency by adjusting the sample rate in steps. However, if the +// latency keeps drifting we give up after reaching this step. +#define ALSA_RESAMPLE_STEP_MAX 8 +// The sample rate gets adjusted by a multiple of this number. The number of +// multiples depends on the sample rate, i.e. a low sample rate may get stepped +// by 16, while high one would get stepped by 4 x 16 +#define ALSA_RESAMPLE_STEP_MULTIPLE 2 #define ALSA_F_STARTED (1 << 15) -enum alsa_state -{ - ALSA_STATE_FAILED = 0, - ALSA_STATE_STOPPED = 1, - ALSA_STATE_STARTED = ALSA_F_STARTED, - ALSA_STATE_STREAMING = ALSA_F_STARTED | 0x01, -}; - enum alsa_sync_state { ALSA_SYNC_OK, @@ -78,73 +68,757 @@ enum alsa_sync_state struct alsa_session { - enum alsa_state state; + enum output_device_state state; - char *devname; + uint64_t device_id; + int callback_id; - uint64_t pos; - uint64_t start_pos; + const char *devname; + const char *card_name; + const char *mixer_name; + const char *mixer_device_name; - int32_t last_latency; - int sync_counter; - unsigned source_sample_rate; // raw input audio sample rate in Hz - unsigned target_sample_rate; // output rate in Hz to configure ALSA device + snd_pcm_status_t *pcm_status; - // An array that will hold the packets we prebuffer. The length of the array - // is prebuf_len (measured in rtp_packets) - uint8_t *prebuf; - uint32_t prebuf_len; - uint32_t prebuf_head; - uint32_t prebuf_tail; + struct media_quality quality; + + int buffer_nsamp; + + uint32_t pos; + + uint32_t last_pos; + uint32_t last_buflen; + + struct timespec last_pts; + + // Used for syncing with the clock + struct timespec stamp_pts; + uint64_t stamp_pos; + + // Array of latency calculations, where latency_counter tells how many are + // currently in the array + double *latency_history; + int latency_counter; + + int sync_resample_step; + + // Here we buffer samples during startup + struct ringbuffer prebuf; + + int offset_ms; int volume; + long vol_min; + long vol_max; - struct event *deferredev; - output_status_cb defer_cb; - - /* Do not dereference - only passed to the status cb */ - struct output_device *device; - struct output_session *output_session; - output_status_cb status_cb; + snd_pcm_t *hdl; + snd_mixer_t *mixer_hdl; + snd_mixer_elem_t *vol_elem; struct alsa_session *next; }; -/* From player.c */ -extern struct event_base *evbase_player; - static struct alsa_session *sessions; -/* Forwards */ +static bool alsa_sync_disable; +static int alsa_latency_history_size; + +// We will try to play the music with the source quality, but if the card +// doesn't support that we resample to the fallback quality +static struct media_quality alsa_fallback_quality = { 44100, 16, 2 }; +static struct media_quality alsa_last_quality; + + +/* -------------------------------- FORWARDS -------------------------------- */ + static void -defer_cb(int fd, short what, void *arg); +alsa_status(struct alsa_session *as); + + +/* ------------------------------- MISC HELPERS ----------------------------- */ + +static void +dump_config(struct alsa_session *as) +{ + snd_output_t *output; + char *debug_pcm_cfg; + int ret; + + // Dump PCM config data for E_DBG logging + ret = snd_output_buffer_open(&output); + if (ret == 0) + { + if (snd_pcm_dump_setup(as->hdl, output) == 0) + { + snd_output_buffer_string(output, &debug_pcm_cfg); + DPRINTF(E_DBG, L_LAUDIO, "Dump of sound device config:\n%s\n", debug_pcm_cfg); + } + + snd_output_close(output); + } +} + +static int +mixer_open(struct alsa_session *as) +{ + snd_mixer_elem_t *elem; + snd_mixer_elem_t *master; + snd_mixer_elem_t *pcm; + snd_mixer_elem_t *custom; + snd_mixer_selem_id_t *sid; + int ret; + + ret = snd_mixer_open(&as->mixer_hdl, 0); + if (ret < 0) + { + DPRINTF(E_LOG, L_LAUDIO, "Failed to open mixer: %s\n", snd_strerror(ret)); + as->mixer_hdl = NULL; + return -1; + } + + ret = snd_mixer_attach(as->mixer_hdl, as->mixer_device_name); + if (ret < 0) + { + DPRINTF(E_LOG, L_LAUDIO, "Failed to attach mixer: %s\n", snd_strerror(ret)); + goto out_close; + } + + ret = snd_mixer_selem_register(as->mixer_hdl, NULL, NULL); + if (ret < 0) + { + DPRINTF(E_LOG, L_LAUDIO, "Failed to register mixer: %s\n", snd_strerror(ret)); + goto out_detach; + } + + ret = snd_mixer_load(as->mixer_hdl); + if (ret < 0) + { + DPRINTF(E_LOG, L_LAUDIO, "Failed to load mixer: %s\n", snd_strerror(ret)); + goto out_detach; + } + + // Grab interesting elements + snd_mixer_selem_id_alloca(&sid); + + pcm = NULL; + master = NULL; + custom = NULL; + for (elem = snd_mixer_first_elem(as->mixer_hdl); elem; elem = snd_mixer_elem_next(elem)) + { + snd_mixer_selem_get_id(elem, sid); + + if (as->mixer_name && (strcmp(snd_mixer_selem_id_get_name(sid), as->mixer_name) == 0)) + { + custom = elem; + break; + } + else if (strcmp(snd_mixer_selem_id_get_name(sid), "PCM") == 0) + pcm = elem; + else if (strcmp(snd_mixer_selem_id_get_name(sid), "Master") == 0) + master = elem; + } + + if (as->mixer_name) + { + if (custom) + as->vol_elem = custom; + else + { + DPRINTF(E_LOG, L_LAUDIO, "Failed to open configured mixer element '%s'\n", as->mixer_name); + + goto out_detach; + } + } + else if (pcm) + as->vol_elem = pcm; + else if (master) + as->vol_elem = master; + else + { + DPRINTF(E_LOG, L_LAUDIO, "Failed to open PCM or Master mixer element\n"); + + goto out_detach; + } + + // Get min & max volume + snd_mixer_selem_get_playback_volume_range(as->vol_elem, &as->vol_min, &as->vol_max); + + return 0; + + out_detach: + snd_mixer_detach(as->mixer_hdl, as->devname); + out_close: + snd_mixer_close(as->mixer_hdl); + as->mixer_hdl = NULL; + as->vol_elem = NULL; + + return -1; +} + +static int +device_open(struct alsa_session *as) +{ + snd_pcm_hw_params_t *hw_params; + snd_pcm_uframes_t bufsize; + int ret; + + hw_params = NULL; + + ret = snd_pcm_open(&as->hdl, as->devname, SND_PCM_STREAM_PLAYBACK, 0); + if (ret < 0) + { + DPRINTF(E_LOG, L_LAUDIO, "Could not open playback device: %s\n", snd_strerror(ret)); + return -1; + } + + // HW params + ret = snd_pcm_hw_params_malloc(&hw_params); + if (ret < 0) + { + DPRINTF(E_LOG, L_LAUDIO, "Could not allocate hw params: %s\n", snd_strerror(ret)); + goto out_fail; + } + + ret = snd_pcm_hw_params_any(as->hdl, hw_params); + if (ret < 0) + { + DPRINTF(E_LOG, L_LAUDIO, "Could not retrieve hw params: %s\n", snd_strerror(ret)); + goto out_fail; + } + + ret = snd_pcm_hw_params_set_access(as->hdl, hw_params, SND_PCM_ACCESS_RW_INTERLEAVED); + if (ret < 0) + { + DPRINTF(E_LOG, L_LAUDIO, "Could not set access method: %s\n", snd_strerror(ret)); + goto out_fail; + } + + ret = snd_pcm_hw_params_get_buffer_size_max(hw_params, &bufsize); + if (ret < 0) + { + DPRINTF(E_LOG, L_LAUDIO, "Could not get max buffer size: %s\n", snd_strerror(ret)); + goto out_fail; + } + + ret = snd_pcm_hw_params_set_buffer_size_max(as->hdl, hw_params, &bufsize); + if (ret < 0) + { + DPRINTF(E_LOG, L_LAUDIO, "Could not set buffer size to max: %s\n", snd_strerror(ret)); + goto out_fail; + } + + ret = snd_pcm_hw_params(as->hdl, hw_params); + if (ret < 0) + { + DPRINTF(E_LOG, L_LAUDIO, "Could not set hw params: %s\n", snd_strerror(ret)); + goto out_fail; + } + + snd_pcm_hw_params_free(hw_params); + hw_params = NULL; + + ret = mixer_open(as); + if (ret < 0) + { + DPRINTF(E_LOG, L_LAUDIO, "Could not open mixer\n"); + goto out_fail; + } + + return 0; + + out_fail: + if (hw_params) + snd_pcm_hw_params_free(hw_params); + + snd_pcm_close(as->hdl); + as->hdl = NULL; + + return -1; +} + +static int +device_quality_set(struct alsa_session *as, struct media_quality *quality, char **errmsg) +{ + snd_pcm_hw_params_t *hw_params; + snd_pcm_format_t format; + int ret; + + ret = snd_pcm_hw_params_malloc(&hw_params); + if (ret < 0) + { + *errmsg = safe_asprintf("Could not allocate hw params: %s", snd_strerror(ret)); + return -1; + } + + ret = snd_pcm_hw_params_any(as->hdl, hw_params); + if (ret < 0) + { + *errmsg = safe_asprintf("Could not retrieve hw params: %s", snd_strerror(ret)); + goto free_params; + } + + ret = snd_pcm_hw_params_set_rate(as->hdl, hw_params, quality->sample_rate, 0); + if (ret < 0) + { + *errmsg = safe_asprintf("Hardware doesn't support %d Hz: %s", quality->sample_rate, snd_strerror(ret)); + goto free_params; + } + + switch (quality->bits_per_sample) + { + case 16: + format = SND_PCM_FORMAT_S16_LE; + break; + case 24: + format = SND_PCM_FORMAT_S24_LE; + break; + case 32: + format = SND_PCM_FORMAT_S32_LE; + break; + default: + *errmsg = safe_asprintf("Unrecognized number of bits per sample: %d", quality->bits_per_sample); + goto free_params; + } + + ret = snd_pcm_hw_params_set_format(as->hdl, hw_params, format); + if (ret < 0) + { + *errmsg = safe_asprintf("Could not set %d bits per sample: %s", quality->bits_per_sample, snd_strerror(ret)); + goto free_params; + } + + ret = snd_pcm_hw_params_set_channels(as->hdl, hw_params, quality->channels); + if (ret < 0) + { + *errmsg = safe_asprintf("Could not set channel number (%d): %s", quality->channels, snd_strerror(ret)); + goto free_params; + } + + ret = snd_pcm_hw_params(as->hdl, hw_params); + if (ret < 0) + { + *errmsg = safe_asprintf("Could not set hw params: %s\n", snd_strerror(ret)); + goto free_params; + } + + snd_pcm_hw_params_free(hw_params); + return 0; + + free_params: + snd_pcm_hw_params_free(hw_params); + return -1; +} + +static int +device_configure(struct alsa_session *as) +{ + snd_pcm_sw_params_t *sw_params; + int ret; + + ret = snd_pcm_sw_params_malloc(&sw_params); + if (ret < 0) + { + DPRINTF(E_LOG, L_LAUDIO, "Could not allocate sw params: %s\n", snd_strerror(ret)); + goto out_fail; + } + + ret = snd_pcm_sw_params_current(as->hdl, sw_params); + if (ret < 0) + { + DPRINTF(E_LOG, L_LAUDIO, "Could not retrieve current sw params: %s\n", snd_strerror(ret)); + goto out_fail; + } + + ret = snd_pcm_sw_params_set_tstamp_type(as->hdl, sw_params, SND_PCM_TSTAMP_TYPE_MONOTONIC); + if (ret < 0) + { + DPRINTF(E_LOG, L_LAUDIO, "Could not set tstamp type: %s\n", snd_strerror(ret)); + goto out_fail; + } + + ret = snd_pcm_sw_params_set_tstamp_mode(as->hdl, sw_params, SND_PCM_TSTAMP_ENABLE); + if (ret < 0) + { + DPRINTF(E_LOG, L_LAUDIO, "Could not set tstamp mode: %s\n", snd_strerror(ret)); + goto out_fail; + } + + ret = snd_pcm_sw_params(as->hdl, sw_params); + if (ret < 0) + { + DPRINTF(E_LOG, L_LAUDIO, "Could not set sw params: %s\n", snd_strerror(ret)); + goto out_fail; + } + + snd_pcm_sw_params_free(sw_params); + + return 0; + + out_fail: + snd_pcm_sw_params_free(sw_params); + + return -1; +} + +static void +device_close(struct alsa_session *as) +{ + snd_pcm_close(as->hdl); + as->hdl = NULL; + + if (as->mixer_hdl) + { + snd_mixer_detach(as->mixer_hdl, as->devname); + snd_mixer_close(as->mixer_hdl); + + as->mixer_hdl = NULL; + as->vol_elem = NULL; + } +} + +static void +playback_restart(struct alsa_session *as, struct output_buffer *obuf) +{ + struct timespec ts; + snd_pcm_state_t state; + snd_pcm_sframes_t offset_nsamp; + size_t size; + char *errmsg; + int ret; + + DPRINTF(E_INFO, L_LAUDIO, "Starting ALSA device '%s'\n", as->devname); + + state = snd_pcm_state(as->hdl); + if (state != SND_PCM_STATE_PREPARED) + { + if (state == SND_PCM_STATE_RUNNING) + snd_pcm_drop(as->hdl); // FIXME not great to do this during playback - would mean new quality drops audio? + + ret = snd_pcm_prepare(as->hdl); + if (ret < 0) + { + DPRINTF(E_LOG, L_LAUDIO, "Could not prepare ALSA device '%s' (state %d): %s\n", as->devname, state, snd_strerror(ret)); + return; + } + } + + // Negotiate quality (sample rate) with device - first we try to use the source quality + as->quality = obuf->data[0].quality; + ret = device_quality_set(as, &as->quality, &errmsg); + if (ret < 0) + { + DPRINTF(E_INFO, L_LAUDIO, "Input quality (%d/%d/%d) not supported, falling back to default. ALSA said: %s\n", + as->quality.sample_rate, as->quality.bits_per_sample, as->quality.channels, errmsg); + free(errmsg); + as->quality = alsa_fallback_quality; + ret = device_quality_set(as, &as->quality, &errmsg); + if (ret < 0) + { + DPRINTF(E_LOG, L_LAUDIO, "ALSA device failed setting fallback quality: %s\n", errmsg); + free(errmsg); + as->state = OUTPUT_STATE_FAILED; + alsa_status(as); + return; + } + } + + dump_config(as); + + // Clear prebuffer in case start got called twice without a stop in between + ringbuffer_free(&as->prebuf, 1); + + as->pos = 0; + + // Time stamps used for syncing, here we set when playback should start + ts.tv_sec = OUTPUTS_BUFFER_DURATION; + ts.tv_nsec = (uint64_t)as->offset_ms * 1000000UL; + as->stamp_pts = timespec_add(obuf->pts, ts); + + // The difference between pos and start pos should match the 2 second buffer + // that AirPlay uses (OUTPUTS_BUFFER_DURATION) + user configured offset_ms. We + // will not use alsa's buffer for the initial buffering, because my sound + // card's start_threshold is not to be counted on. Instead we allocate our own + // buffer, and when it is time to play we write as much as we can to alsa's + // buffer. + offset_nsamp = (as->offset_ms * as->quality.sample_rate / 1000); + + as->buffer_nsamp = OUTPUTS_BUFFER_DURATION * as->quality.sample_rate + offset_nsamp; + size = STOB(as->buffer_nsamp, as->quality.bits_per_sample, as->quality.channels); + ringbuffer_init(&as->prebuf, size); + + as->state = OUTPUT_STATE_STREAMING; +} + +// This function writes the sample buf into either the prebuffer or directly to +// ALSA, depending on how much room there is in ALSA, and whether we are +// prebuffering or not. It also transfers from the the prebuffer to ALSA, if +// needed. Returns 0 on success, negative on error. +static int +buffer_write(struct alsa_session *as, struct output_data *odata, snd_pcm_sframes_t avail) +{ + uint8_t *buf; + size_t bufsize; + size_t wrote; + snd_pcm_sframes_t nsamp; + snd_pcm_sframes_t ret; + + // Prebuffering, no actual writing + if (avail == 0) + { + wrote = ringbuffer_write(&as->prebuf, odata->buffer, odata->bufsize); + nsamp = BTOS(wrote, as->quality.bits_per_sample, as->quality.channels); + return nsamp; + } + + // Read from prebuffer if it has data and write to device + if (as->prebuf.read_avail != 0) + { + // Maximum amount of bytes we want to read + bufsize = STOB(avail, as->quality.bits_per_sample, as->quality.channels); + + bufsize = ringbuffer_read(&buf, bufsize, &as->prebuf); + if (bufsize == 0) + return 0; + + nsamp = BTOS(bufsize, as->quality.bits_per_sample, as->quality.channels); + ret = snd_pcm_writei(as->hdl, buf, nsamp); + if (ret < 0) + return -1; + + avail -= ret; + } + + // Write to prebuffer if device buffer does not have availability. Note that + // if the prebuffer doesn't have enough room, which can happen if avail stays + // low, i.e. device buffer is overrunning, then the extra samples get dropped + if (odata->samples > avail) + { + ringbuffer_write(&as->prebuf, odata->buffer, odata->bufsize); + return odata->samples; + } + + ret = snd_pcm_writei(as->hdl, odata->buffer, odata->samples); + if (ret < 0) + return ret; + + if (ret != odata->samples) + DPRINTF(E_WARN, L_LAUDIO, "ALSA partial write detected\n"); + + return ret; +} + +static enum alsa_sync_state +sync_check(double *drift, double *latency, struct alsa_session *as, snd_pcm_sframes_t delay) +{ + enum alsa_sync_state sync; + struct timespec ts; + int elapsed; + uint64_t cur_pos; + uint64_t exp_pos; + int32_t diff; + double r2; + int ret; + + // Would be nice to use snd_pcm_status_get_audio_htstamp here, but it doesn't + // seem to be supported on my computer + clock_gettime(CLOCK_MONOTONIC, &ts); + + // Here we calculate elapsed time since last reference position (which is + // equal to playback start time, unless we have reset due to sync correction), + // taking into account buffer time and configuration of offset_ms. We then + // calculate our expected position based on elapsed time, and if different + // from where we are + what is in the buffers then ALSA is out of sync. + elapsed = (ts.tv_sec - as->stamp_pts.tv_sec) * 1000L + (ts.tv_nsec - as->stamp_pts.tv_nsec) / 1000000; + if (elapsed < 0) + return ALSA_SYNC_OK; + + cur_pos = (uint64_t)as->pos - as->stamp_pos - (delay + BTOS(as->prebuf.read_avail, as->quality.bits_per_sample, as->quality.channels)); + exp_pos = (uint64_t)elapsed * as->quality.sample_rate / 1000; + diff = cur_pos - exp_pos; + + DPRINTF(E_SPAM, L_LAUDIO, "counter %d/%d, stamp %lu:%lu, now %lu:%lu, elapsed is %d ms, cur_pos=%" PRIu64 ", exp_pos=%" PRIu64 ", diff=%d\n", + as->latency_counter, alsa_latency_history_size, as->stamp_pts.tv_sec, as->stamp_pts.tv_nsec / 1000000, ts.tv_sec, ts.tv_nsec / 1000000, elapsed, cur_pos, exp_pos, diff); + + // Add the latency to our measurement history + as->latency_history[as->latency_counter] = (double)diff; + as->latency_counter++; + + // Haven't collected enough samples for sync evaluation yet, so just return + if (as->latency_counter < alsa_latency_history_size) + return ALSA_SYNC_OK; + + as->latency_counter = 0; + + ret = linear_regression(drift, latency, &r2, NULL, as->latency_history, alsa_latency_history_size); + if (ret < 0) + { + DPRINTF(E_WARN, L_LAUDIO, "Linear regression of collected latency samples failed\n"); + return ALSA_SYNC_OK; + } + + // Set *latency to the "average" within the period + *latency = (*drift) * alsa_latency_history_size / 2 + (*latency); + + if (abs(*latency) < ALSA_MAX_LATENCY && abs(*drift) < ALSA_MAX_DRIFT) + sync = ALSA_SYNC_OK; // If both latency and drift are within thresholds -> no action + else if (*latency > 0 && *drift > 0) + sync = ALSA_SYNC_AHEAD; + else if (*latency < 0 && *drift < 0) + sync = ALSA_SYNC_BEHIND; + else + sync = ALSA_SYNC_OK; // Drift is counteracting latency -> no action + + if (sync != ALSA_SYNC_OK && r2 < ALSA_MAX_VARIANCE) + { + DPRINTF(E_DBG, L_LAUDIO, "Too much variance in latency measurements (r2=%f/%f), won't try to compensate\n", r2, ALSA_MAX_VARIANCE); + sync = ALSA_SYNC_OK; + } + + DPRINTF(E_DBG, L_LAUDIO, "Sync check result: drift=%f, latency=%f, r2=%f, sync=%d\n", *drift, *latency, r2, sync); + + return sync; +} + +static void +sync_correct(struct alsa_session *as, double drift, double latency, struct timespec pts, snd_pcm_sframes_t delay) +{ + int step; + int sign; + + // We change the sample_rate in steps that are a multiple of 50. So we might + // step 44100 -> 44000 -> 40900 -> 44000 -> 44100. If we used percentages to + // to step, we would have to deal with rounding; we don't want to step 44100 + // -> 39996 -> 44099. + step = ALSA_RESAMPLE_STEP_MULTIPLE * (as->quality.sample_rate / 20000); + + sign = (drift < 0) ? -1 : 1; + + if (abs(as->sync_resample_step) == ALSA_RESAMPLE_STEP_MAX) + { + DPRINTF(E_LOG, L_LAUDIO, "The sync of ALSA device '%s' cannot be corrected (drift=%f, latency=%f)\n", as->devname, drift, latency); + as->sync_resample_step += sign; + return; + } + else if (abs(as->sync_resample_step) > ALSA_RESAMPLE_STEP_MAX) + return; // Don't do anything, we have given up + + // Step 0 is the original audio quality (or the fallback quality), which we + // will just keep receiving + if (as->sync_resample_step != 0) + outputs_quality_unsubscribe(&as->quality); + + as->sync_resample_step += sign; + as->quality.sample_rate += sign * step; + + if (as->sync_resample_step != 0) + outputs_quality_subscribe(&as->quality); + + // Reset position so next sync_correct latency correction is only based on + // what has elapsed since our correction + as->stamp_pos = (uint64_t)as->pos - (delay + BTOS(as->prebuf.read_avail, as->quality.bits_per_sample, as->quality.channels));; + as->stamp_pts = pts; + + DPRINTF(E_INFO, L_LAUDIO, "Adjusted sample rate to %d to sync ALSA device '%s' (drift=%f, latency=%f)\n", as->quality.sample_rate, as->devname, drift, latency); +} + +static void +playback_write(struct alsa_session *as, struct output_buffer *obuf) +{ + snd_pcm_sframes_t ret; + snd_pcm_sframes_t avail; + snd_pcm_sframes_t delay; + enum alsa_sync_state sync; + double drift; + double latency; + bool prebuffering; + int i; + + // Find the quality we want + for (i = 0; obuf->data[i].buffer; i++) + { + if (quality_is_equal(&as->quality, &obuf->data[i].quality)) + break; + } + + if (!obuf->data[i].buffer) + { + DPRINTF(E_LOG, L_LAUDIO, "Output not delivering required data quality, aborting\n"); + as->state = OUTPUT_STATE_FAILED; + alsa_status(as); + return; + } + + prebuffering = (as->pos < as->buffer_nsamp); + if (prebuffering) + { + // Can never fail since we don't actually write to the device + as->pos += buffer_write(as, &obuf->data[i], 0); + return; + } + + // Check sync each second (or if this is first write where last_pts is zero) + if (!alsa_sync_disable && (obuf->pts.tv_sec != as->last_pts.tv_sec)) + { + ret = snd_pcm_delay(as->hdl, &delay); + if (ret == 0) + { + sync = sync_check(&drift, &latency, as, delay); + if (sync != ALSA_SYNC_OK) + sync_correct(as, drift, latency, obuf->pts, delay); + } + + as->last_pts = obuf->pts; + } + + avail = snd_pcm_avail(as->hdl); + + ret = buffer_write(as, &obuf->data[i], avail); + if (ret < 0) + goto alsa_error; + + as->pos += ret; + + return; + + alsa_error: + if (ret == -EPIPE) + { + DPRINTF(E_WARN, L_LAUDIO, "ALSA buffer underrun\n"); + + ret = snd_pcm_prepare(as->hdl); + if (ret < 0) + { + DPRINTF(E_WARN, L_LAUDIO, "ALSA couldn't recover from underrun: %s\n", snd_strerror(ret)); + return; + } + + // Fill the prebuf with audio before restarting, so we don't underrun again + playback_restart(as, obuf); + return; + } + + DPRINTF(E_LOG, L_LAUDIO, "ALSA write error: %s\n", snd_strerror(ret)); + + as->state = OUTPUT_STATE_FAILED; + alsa_status(as); +} + /* ---------------------------- SESSION HANDLING ---------------------------- */ -static void -prebuf_free(struct alsa_session *as) -{ - if (as->prebuf) - free(as->prebuf); - - as->prebuf = NULL; - as->prebuf_len = 0; - as->prebuf_head = 0; - as->prebuf_tail = 0; -} - static void alsa_session_free(struct alsa_session *as) { if (!as) return; - if (as->deferredev) - event_free(as->deferredev); + device_close(as); - prebuf_free(as); + outputs_quality_unsubscribe(&alsa_fallback_quality); + + ringbuffer_free(&as->prebuf, 1); + snd_pcm_status_free(as->pcm_status); - free(as->output_session); free(as); } @@ -166,897 +840,265 @@ alsa_session_cleanup(struct alsa_session *as) s->next = as->next; } + outputs_device_session_remove(as->device_id); + alsa_session_free(as); } static struct alsa_session * -alsa_session_make(struct output_device *device, output_status_cb cb) +alsa_session_make(struct output_device *device, int callback_id) { struct alsa_session *as; + cfg_t *cfg_audio; + char *errmsg; + int ret; - as = calloc(1, sizeof(struct alsa_session)); - if (!as) - { - DPRINTF(E_LOG, L_LAUDIO, "Out of memory for ALSA session (as)\n"); - return NULL; - } + CHECK_NULL(L_LAUDIO, as = calloc(1, sizeof(struct alsa_session))); - as->output_session = calloc(1, sizeof(struct output_session)); - if (!as->output_session) - { - DPRINTF(E_LOG, L_LAUDIO, "Out of memory for ALSA session (output_session)\n"); - goto failure_cleanup; - } - as->output_session->session = as; - as->output_session->type = device->type; - - as->deferredev = evtimer_new(evbase_player, defer_cb, as); - if (!as->deferredev) - { - DPRINTF(E_LOG, L_LAUDIO, "Out of memory for ALSA deferred event\n"); - goto failure_cleanup; - } - - as->state = ALSA_STATE_STOPPED; - as->device = device; - as->status_cb = cb; + as->device_id = device->id; + as->callback_id = callback_id; as->volume = device->volume; - as->devname = card_name; - as->source_sample_rate = 44100; - as->target_sample_rate = 44100; // TODO: make ALSA device sample rate configurable + cfg_audio = cfg_getsec(cfg, "audio"); + + as->devname = cfg_getstr(cfg_audio, "card"); + as->mixer_name = cfg_getstr(cfg_audio, "mixer"); + as->mixer_device_name = cfg_getstr(cfg_audio, "mixer_device"); + if (!as->mixer_device_name || strlen(as->mixer_device_name) == 0) + as->mixer_device_name = cfg_getstr(cfg_audio, "card"); + + as->offset_ms = cfg_getint(cfg_audio, "offset_ms"); + if (abs(as->offset_ms) > 1000) + { + DPRINTF(E_LOG, L_LAUDIO, "The ALSA offset_ms (%d) set in the configuration is out of bounds\n", as->offset_ms); + as->offset_ms = 1000 * (as->offset_ms/abs(as->offset_ms)); + } + + CHECK_NULL(L_LAUDIO, as->latency_history = calloc(alsa_latency_history_size, sizeof(double))); + + snd_pcm_status_malloc(&as->pcm_status); + + ret = device_open(as); + if (ret < 0) + goto out_free_session; + + ret = device_quality_set(as, &alsa_fallback_quality, &errmsg); + if (ret < 0) + { + DPRINTF(E_LOG, L_LAUDIO, "%s\n", errmsg); + free(errmsg); + goto out_device_close; + } + + // If this fails it just means we won't get timestamps, which we can handle + device_configure(as); + + ret = outputs_quality_subscribe(&alsa_fallback_quality); + if (ret < 0) + { + DPRINTF(E_LOG, L_LAUDIO, "Could not subscribe to fallback audio quality\n"); + goto out_device_close; + } + + as->state = OUTPUT_STATE_CONNECTED; as->next = sessions; sessions = as; + + // as is now the official device session + outputs_device_session_add(device->id, as); + return as; - failure_cleanup: - alsa_session_free(as); + out_device_close: + device_close(as); + out_free_session: + free(as); return NULL; } - -/* ---------------------------- 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 alsa_session *as = arg; - enum output_device_state state; - - switch (as->state) - { - case ALSA_STATE_FAILED: - state = OUTPUT_STATE_FAILED; - break; - case ALSA_STATE_STOPPED: - state = OUTPUT_STATE_STOPPED; - break; - case ALSA_STATE_STARTED: - state = OUTPUT_STATE_CONNECTED; - break; - case ALSA_STATE_STREAMING: - state = OUTPUT_STATE_STREAMING; - break; - default: - DPRINTF(E_LOG, L_LAUDIO, "Bug! Unhandled state in alsa_status()\n"); - state = OUTPUT_STATE_FAILED; - } - - if (as->defer_cb) - as->defer_cb(as->device, as->output_session, state); - - if (!(as->state & ALSA_F_STARTED)) - alsa_session_cleanup(as); -} - -// Note: alsa_states also nukes the session if it is not ALSA_F_STARTED static void alsa_status(struct alsa_session *as) { - as->defer_cb = as->status_cb; - event_active(as->deferredev, 0, 0); - as->status_cb = NULL; + outputs_cb(as->callback_id, as->device_id, as->state); + as->callback_id = -1; + + if (as->state == OUTPUT_STATE_FAILED || as->state == OUTPUT_STATE_STOPPED) + alsa_session_cleanup(as); } -/* ------------------------------- MISC HELPERS ----------------------------- */ - -/*static int -start_threshold_set(snd_pcm_uframes_t threshold) -{ - snd_pcm_sw_params_t *sw_params; - int ret; - - ret = snd_pcm_sw_params_malloc(&sw_params); - if (ret < 0) - { - DPRINTF(E_LOG, L_LAUDIO, "Could not allocate sw params: %s\n", snd_strerror(ret)); - - goto out_fail; - } - - ret = snd_pcm_sw_params_current(hdl, sw_params); - if (ret < 0) - { - DPRINTF(E_LOG, L_LAUDIO, "Could not retrieve current sw params: %s\n", snd_strerror(ret)); - - goto out_fail; - } - - ret = snd_pcm_sw_params_set_start_threshold(hdl, sw_params, threshold); - if (ret < 0) - { - DPRINTF(E_LOG, L_LAUDIO, "Could not set start threshold: %s\n", snd_strerror(ret)); - - goto out_fail; - } - - ret = snd_pcm_sw_params(hdl, sw_params); - if (ret < 0) - { - DPRINTF(E_LOG, L_LAUDIO, "Could not set sw params: %s\n", snd_strerror(ret)); - - goto out_fail; - } - - return 0; - - out_fail: - snd_pcm_sw_params_free(sw_params); - - return -1; -} -*/ - -static int -mixer_open(void) -{ - snd_mixer_elem_t *elem; - snd_mixer_elem_t *master; - snd_mixer_elem_t *pcm; - snd_mixer_elem_t *custom; - snd_mixer_selem_id_t *sid; - int ret; - - ret = snd_mixer_open(&mixer_hdl, 0); - if (ret < 0) - { - DPRINTF(E_LOG, L_LAUDIO, "Failed to open mixer: %s\n", snd_strerror(ret)); - - mixer_hdl = NULL; - return -1; - } - - ret = snd_mixer_attach(mixer_hdl, mixer_device_name); - if (ret < 0) - { - DPRINTF(E_LOG, L_LAUDIO, "Failed to attach mixer: %s\n", snd_strerror(ret)); - - goto out_close; - } - - ret = snd_mixer_selem_register(mixer_hdl, NULL, NULL); - if (ret < 0) - { - DPRINTF(E_LOG, L_LAUDIO, "Failed to register mixer: %s\n", snd_strerror(ret)); - - goto out_detach; - } - - ret = snd_mixer_load(mixer_hdl); - if (ret < 0) - { - DPRINTF(E_LOG, L_LAUDIO, "Failed to load mixer: %s\n", snd_strerror(ret)); - - goto out_detach; - } - - // Grab interesting elements - snd_mixer_selem_id_alloca(&sid); - - pcm = NULL; - master = NULL; - custom = NULL; - for (elem = snd_mixer_first_elem(mixer_hdl); elem; elem = snd_mixer_elem_next(elem)) - { - snd_mixer_selem_get_id(elem, sid); - - if (mixer_name && (strcmp(snd_mixer_selem_id_get_name(sid), mixer_name) == 0)) - { - custom = elem; - break; - } - else if (strcmp(snd_mixer_selem_id_get_name(sid), "PCM") == 0) - pcm = elem; - else if (strcmp(snd_mixer_selem_id_get_name(sid), "Master") == 0) - master = elem; - } - - if (mixer_name) - { - if (custom) - vol_elem = custom; - else - { - DPRINTF(E_LOG, L_LAUDIO, "Failed to open configured mixer element '%s'\n", mixer_name); - - goto out_detach; - } - } - else if (pcm) - vol_elem = pcm; - else if (master) - vol_elem = master; - else - { - DPRINTF(E_LOG, L_LAUDIO, "Failed to open PCM or Master mixer element\n"); - - goto out_detach; - } - - // Get min & max volume - snd_mixer_selem_get_playback_volume_range(vol_elem, &vol_min, &vol_max); - - return 0; - - out_detach: - snd_mixer_detach(mixer_hdl, card_name); - out_close: - snd_mixer_close(mixer_hdl); - mixer_hdl = NULL; - vol_elem = NULL; - - return -1; -} - -static int -device_open(struct alsa_session *as) -{ - snd_pcm_hw_params_t *hw_params; - snd_pcm_uframes_t bufsize; - int ret; - - hw_params = NULL; - - ret = snd_pcm_open(&hdl, card_name, SND_PCM_STREAM_PLAYBACK, 0); - if (ret < 0) - { - DPRINTF(E_LOG, L_LAUDIO, "Could not open playback device: %s\n", snd_strerror(ret)); - - return -1; - } - - // HW params - ret = snd_pcm_hw_params_malloc(&hw_params); - if (ret < 0) - { - DPRINTF(E_LOG, L_LAUDIO, "Could not allocate hw params: %s\n", snd_strerror(ret)); - - goto out_fail; - } - - ret = snd_pcm_hw_params_any(hdl, hw_params); - if (ret < 0) - { - DPRINTF(E_LOG, L_LAUDIO, "Could not retrieve hw params: %s\n", snd_strerror(ret)); - - goto out_fail; - } - - ret = snd_pcm_hw_params_set_access(hdl, hw_params, SND_PCM_ACCESS_RW_INTERLEAVED); - if (ret < 0) - { - DPRINTF(E_LOG, L_LAUDIO, "Could not set access method: %s\n", snd_strerror(ret)); - - goto out_fail; - } - - ret = snd_pcm_hw_params_set_format(hdl, hw_params, SND_PCM_FORMAT_S16_LE); - if (ret < 0) - { - DPRINTF(E_LOG, L_LAUDIO, "Could not set S16LE format: %s\n", snd_strerror(ret)); - - goto out_fail; - } - - ret = snd_pcm_hw_params_set_channels(hdl, hw_params, 2); - if (ret < 0) - { - DPRINTF(E_LOG, L_LAUDIO, "Could not set stereo output: %s\n", snd_strerror(ret)); - - goto out_fail; - } - - ret = snd_pcm_hw_params_set_rate(hdl, hw_params, as->target_sample_rate, 0); - if (ret < 0) - { - DPRINTF(E_LOG, L_LAUDIO, "Hardware doesn't support %u Hz: %s\n", as->target_sample_rate, snd_strerror(ret)); - - goto out_fail; - } - - ret = snd_pcm_hw_params_get_buffer_size_max(hw_params, &bufsize); - if (ret < 0) - { - DPRINTF(E_LOG, L_LAUDIO, "Could not get max buffer size: %s\n", snd_strerror(ret)); - - goto out_fail; - } - - ret = snd_pcm_hw_params_set_buffer_size_max(hdl, hw_params, &bufsize); - if (ret < 0) - { - DPRINTF(E_LOG, L_LAUDIO, "Could not set buffer size to max: %s\n", snd_strerror(ret)); - - goto out_fail; - } - - ret = snd_pcm_hw_params(hdl, hw_params); - if (ret < 0) - { - DPRINTF(E_LOG, L_LAUDIO, "Could not set hw params: %s\n", snd_strerror(ret)); - - goto out_fail; - } - - snd_pcm_hw_params_free(hw_params); - hw_params = NULL; - - ret = mixer_open(); - if (ret < 0) - { - DPRINTF(E_LOG, L_LAUDIO, "Could not open mixer\n"); - - goto out_fail; - } - - return 0; - - out_fail: - if (hw_params) - snd_pcm_hw_params_free(hw_params); - - snd_pcm_close(hdl); - hdl = NULL; - - return -1; -} - -static void -device_close(void) -{ - snd_pcm_close(hdl); - hdl = NULL; - - if (mixer_hdl) - { - snd_mixer_detach(mixer_hdl, card_name); - snd_mixer_close(mixer_hdl); - - mixer_hdl = NULL; - vol_elem = NULL; - } -} - -static void -playback_start(struct alsa_session *as, uint64_t pos, uint64_t start_pos) -{ - snd_output_t *output; - snd_pcm_state_t state; - char *debug_pcm_cfg; - int ret; - - state = snd_pcm_state(hdl); - if (state != SND_PCM_STATE_PREPARED) - { - if (state == SND_PCM_STATE_RUNNING) - snd_pcm_drop(hdl); - - ret = snd_pcm_prepare(hdl); - if (ret < 0) - { - DPRINTF(E_LOG, L_LAUDIO, "Could not prepare ALSA device '%s' (state %d): %s\n", as->devname, state, snd_strerror(ret)); - return; - } - } - - // Clear prebuffer in case start somehow got called twice without a stop in between - prebuf_free(as); - - // Adjust the starting position with the configured value - start_pos -= offset; - - // The difference between pos and start_pos should match the 2 second - // buffer that AirPlay uses. We will not use alsa's buffer for the initial - // buffering, because my sound card's start_threshold is not to be counted on. - // Instead we allocate our own buffer, and when it is time to play we write as - // much as we can to alsa's buffer. - as->prebuf_len = (start_pos - pos) / AIRTUNES_V2_PACKET_SAMPLES + 1; - if (as->prebuf_len > (3 * 44100 - offset) / AIRTUNES_V2_PACKET_SAMPLES) - { - DPRINTF(E_LOG, L_LAUDIO, "Sanity check of prebuf_len (%" PRIu32 " packets) failed\n", as->prebuf_len); - return; - } - DPRINTF(E_DBG, L_LAUDIO, "Will prebuffer %d packets\n", as->prebuf_len); - - as->prebuf = malloc(as->prebuf_len * PACKET_SIZE); - if (!as->prebuf) - { - DPRINTF(E_LOG, L_LAUDIO, "Out of memory for audio buffer (requested %" PRIu32 " packets)\n", as->prebuf_len); - return; - } - - as->pos = pos; - as->start_pos = start_pos - AIRTUNES_V2_PACKET_SAMPLES; - - // Dump PCM config data for E_DBG logging - ret = snd_output_buffer_open(&output); - if (ret == 0) - { - if (snd_pcm_dump_setup(hdl, output) == 0) - { - snd_output_buffer_string(output, &debug_pcm_cfg); - DPRINTF(E_DBG, L_LAUDIO, "Dump of sound device config:\n%s\n", debug_pcm_cfg); - } - - snd_output_close(output); - } - - as->state = ALSA_STATE_STREAMING; -} - - -// This function writes the sample buf into either the prebuffer or directly to -// ALSA, depending on how much room there is in ALSA, and whether we are -// prebuffering or not. It also transfers from the the prebuffer to ALSA, if -// needed. Returns 0 on success, negative on error. -static int -buffer_write(struct alsa_session *as, uint8_t *buf, snd_pcm_sframes_t *avail, int prebuffering, int prebuf_empty) -{ - uint8_t *pkt; - int npackets; - snd_pcm_sframes_t nsamp; - snd_pcm_sframes_t ret; - - nsamp = AIRTUNES_V2_PACKET_SAMPLES; - - if (as->prebuf && (prebuffering || !prebuf_empty || *avail < AIRTUNES_V2_PACKET_SAMPLES)) - { - pkt = &as->prebuf[as->prebuf_head * PACKET_SIZE]; - - memcpy(pkt, buf, PACKET_SIZE); - - as->prebuf_head = (as->prebuf_head + 1) % as->prebuf_len; - - if (prebuffering || *avail < AIRTUNES_V2_PACKET_SAMPLES) - return 0; // No actual writing - - // We will now set buf so that we will transfer as much as possible to ALSA - buf = &as->prebuf[as->prebuf_tail * PACKET_SIZE]; - - if (as->prebuf_head > as->prebuf_tail) - npackets = as->prebuf_head - as->prebuf_tail; - else - npackets = as->prebuf_len - as->prebuf_tail; - - nsamp = npackets * AIRTUNES_V2_PACKET_SAMPLES; - while (nsamp > *avail) - { - npackets -= 1; - nsamp -= AIRTUNES_V2_PACKET_SAMPLES; - } - - as->prebuf_tail = (as->prebuf_tail + npackets) % as->prebuf_len; - } - - ret = snd_pcm_writei(hdl, buf, nsamp); - if (ret < 0) - return ret; - - if (ret != nsamp) - DPRINTF(E_WARN, L_LAUDIO, "ALSA partial write detected\n"); - - if (avail) - *avail -= ret; - - return 0; -} - -// Checks if ALSA's playback position is ahead or behind the player's -enum alsa_sync_state -sync_check(struct alsa_session *as, uint64_t rtptime, snd_pcm_sframes_t delay, int prebuf_empty) -{ - enum alsa_sync_state sync; - struct timespec now; - uint64_t cur_pos; - uint64_t pb_pos; - int32_t latency; - int npackets; - - sync = ALSA_SYNC_OK; - - if (player_get_current_pos(&cur_pos, &now, 0) != 0) - return sync; - - if (!prebuf_empty) - npackets = (as->prebuf_head - (as->prebuf_tail + 1) + as->prebuf_len) % as->prebuf_len + 1; - else - npackets = 0; - - pb_pos = rtptime - delay - AIRTUNES_V2_PACKET_SAMPLES * npackets; - latency = cur_pos - (pb_pos - offset); - - // If the latency is low or very different from our last measurement, we reset the sync_counter - if (abs(latency) < ALSA_MAX_LATENCY || abs(as->last_latency - latency) > ALSA_MAX_LATENCY_VARIANCE) - { - as->sync_counter = 0; - sync = ALSA_SYNC_OK; - } - // If we have measured a consistent latency for configured period, then we take action - else if (as->sync_counter >= adjust_period_seconds * 126) - { - DPRINTF(E_INFO, L_LAUDIO, "Taking action to compensate for ALSA latency of %d samples\n", latency); - - as->sync_counter = 0; - if (latency > 0) - sync = ALSA_SYNC_BEHIND; - else - sync = ALSA_SYNC_AHEAD; - } - - as->last_latency = latency; - - if (latency) - DPRINTF(E_SPAM, L_LAUDIO, "Sync %d cur_pos %" PRIu64 ", pb_pos %" PRIu64 " (diff %d, delay %li), pos %" PRIu64 "\n", sync, cur_pos, pb_pos, latency, delay, as->pos); - - return sync; -} - -static void -playback_write(struct alsa_session *as, uint8_t *buf, uint64_t rtptime) -{ - snd_pcm_sframes_t ret; - snd_pcm_sframes_t avail; - snd_pcm_sframes_t delay; - enum alsa_sync_state sync; - int prebuffering; - int prebuf_empty; - - prebuffering = (as->pos < as->start_pos); - prebuf_empty = (as->prebuf_head == as->prebuf_tail); - - as->pos += AIRTUNES_V2_PACKET_SAMPLES; - - if (prebuffering) - { - buffer_write(as, buf, NULL, prebuffering, prebuf_empty); - return; - } - - ret = snd_pcm_avail_delay(hdl, &avail, &delay); - if (ret < 0) - goto alsa_error; - - // Every second we do a sync check - sync = ALSA_SYNC_OK; - as->sync_counter++; - if (as->sync_counter % 126 == 0) - sync = sync_check(as, rtptime, delay, prebuf_empty); - - // Skip write -> reduce the delay - if (sync == ALSA_SYNC_BEHIND) - return; - - ret = buffer_write(as, buf, &avail, prebuffering, prebuf_empty); - // Double write -> increase the delay - if (sync == ALSA_SYNC_AHEAD && (ret == 0)) - ret = buffer_write(as, buf, &avail, prebuffering, prebuf_empty); - if (ret < 0) - goto alsa_error; - - return; - - alsa_error: - if (ret == -EPIPE) - { - DPRINTF(E_WARN, L_LAUDIO, "ALSA buffer underrun\n"); - - ret = snd_pcm_prepare(hdl); - if (ret < 0) - { - DPRINTF(E_WARN, L_LAUDIO, "ALSA couldn't recover from underrun: %s\n", snd_strerror(ret)); - return; - } - - // Fill the prebuf with audio before restarting, so we don't underrun again - as->start_pos = as->pos + AIRTUNES_V2_PACKET_SAMPLES * (as->prebuf_len - 1); - - return; - } - - DPRINTF(E_LOG, L_LAUDIO, "ALSA write error: %s\n", snd_strerror(ret)); - - as->state = ALSA_STATE_FAILED; - alsa_status(as); -} - -static void -playback_pos_get(uint64_t *pos, uint64_t next_pkt) -{ - uint64_t cur_pos; - struct timespec now; - int ret; - - ret = player_get_current_pos(&cur_pos, &now, 0); - if (ret < 0) - { - DPRINTF(E_LOG, L_LAUDIO, "Could not get playback position, setting to next_pkt - 2 seconds\n"); - cur_pos = next_pkt - 88200; - } - - // Make pos the rtptime of the packet containing cur_pos - *pos = next_pkt; - while (*pos > cur_pos) - *pos -= AIRTUNES_V2_PACKET_SAMPLES; -} - /* ------------------ INTERFACE FUNCTIONS CALLED BY OUTPUTS.C --------------- */ static int -alsa_device_start(struct output_device *device, output_status_cb cb, uint64_t rtptime) +alsa_device_start(struct output_device *device, int callback_id) { struct alsa_session *as; - int ret; - as = alsa_session_make(device, cb); + as = alsa_session_make(device, callback_id); if (!as) return -1; - ret = device_open(as); - if (ret < 0) - { - alsa_session_cleanup(as); - return -1; - } - - as->state = ALSA_STATE_STARTED; - alsa_status(as); - - return 0; -} - -static void -alsa_device_stop(struct output_session *session) -{ - struct alsa_session *as = session->session; - - device_close(); - - as->state = ALSA_STATE_STOPPED; - alsa_status(as); -} - -static int -alsa_device_probe(struct output_device *device, output_status_cb cb) -{ - struct alsa_session *as; - int ret; - - as = alsa_session_make(device, cb); - if (!as) - return -1; - - ret = device_open(as); - if (ret < 0) - { - alsa_session_cleanup(as); - return -1; - } - - device_close(); - - as->state = ALSA_STATE_STOPPED; + as->state = OUTPUT_STATE_CONNECTED; alsa_status(as); return 0; } static int -alsa_device_volume_set(struct output_device *device, output_status_cb cb) +alsa_device_stop(struct output_device *device, int callback_id) +{ + struct alsa_session *as = device->session; + + as->callback_id = callback_id; + as->state = OUTPUT_STATE_STOPPED; + alsa_status(as); // Will terminate the session since the state is STOPPED + + return 0; +} + +static int +alsa_device_flush(struct output_device *device, int callback_id) +{ + struct alsa_session *as = device->session; + + snd_pcm_drop(as->hdl); + + ringbuffer_free(&as->prebuf, 1); + + as->callback_id = callback_id; + as->state = OUTPUT_STATE_CONNECTED; + alsa_status(as); + + return 0; +} + +static int +alsa_device_probe(struct output_device *device, int callback_id) { struct alsa_session *as; + + as = alsa_session_make(device, callback_id); + if (!as) + return -1; + + as->state = OUTPUT_STATE_STOPPED; + alsa_status(as); // Will terminate the session since the state is STOPPED + + return 0; +} + +static int +alsa_device_volume_set(struct output_device *device, int callback_id) +{ + struct alsa_session *as = device->session; int pcm_vol; - if (!device->session || !device->session->session) + if (!as) return 0; - as = device->session->session; + snd_mixer_handle_events(as->mixer_hdl); - if (!mixer_hdl || !vol_elem) - return 0; - - snd_mixer_handle_events(mixer_hdl); - - if (!snd_mixer_selem_is_active(vol_elem)) + if (!snd_mixer_selem_is_active(as->vol_elem)) return 0; switch (device->volume) { case 0: - pcm_vol = vol_min; + pcm_vol = as->vol_min; break; case 100: - pcm_vol = vol_max; + pcm_vol = as->vol_max; break; default: - pcm_vol = vol_min + (device->volume * (vol_max - vol_min)) / 100; + pcm_vol = as->vol_min + (device->volume * (as->vol_max - as->vol_min)) / 100; break; } DPRINTF(E_DBG, L_LAUDIO, "Setting ALSA volume to %d (%d)\n", pcm_vol, device->volume); - snd_mixer_selem_set_playback_volume_all(vol_elem, pcm_vol); + snd_mixer_selem_set_playback_volume_all(as->vol_elem, pcm_vol); - as->status_cb = cb; + as->callback_id = callback_id; alsa_status(as); return 1; } static void -alsa_playback_start(uint64_t next_pkt, struct timespec *ts) +alsa_device_cb_set(struct output_device *device, int callback_id) { - struct alsa_session *as; - uint64_t pos; + struct alsa_session *as = device->session; - if (!sessions) - return; - - playback_pos_get(&pos, next_pkt); - - DPRINTF(E_DBG, L_LAUDIO, "Starting ALSA audio (pos %" PRIu64 ", next_pkt %" PRIu64 ")\n", pos, next_pkt); - - for (as = sessions; as; as = as->next) - playback_start(as, pos, next_pkt); + as->callback_id = callback_id; } static void -alsa_playback_stop(void) +alsa_write(struct output_buffer *obuf) { struct alsa_session *as; + struct alsa_session *next; - for (as = sessions; as; as = as->next) + for (as = sessions; as; as = next) { - snd_pcm_drop(hdl); - prebuf_free(as); + next = as->next; + // Need to adjust buffers and device params if sample rate changed, or if + // this was the first write to the device + if (!quality_is_equal(&obuf->data[0].quality, &alsa_last_quality) || as->state == OUTPUT_STATE_CONNECTED) + playback_restart(as, obuf); - as->state = ALSA_STATE_STARTED; - alsa_status(as); + playback_write(as, obuf); + + alsa_last_quality = obuf->data[0].quality; } } -static void -alsa_write(uint8_t *buf, uint64_t rtptime) -{ - struct alsa_session *as; - uint64_t pos; - - for (as = sessions; as; as = as->next) - { - if (as->state == ALSA_STATE_STARTED) - { - playback_pos_get(&pos, rtptime); - - DPRINTF(E_DBG, L_LAUDIO, "Starting ALSA device '%s' (pos %" PRIu64 ", rtptime %" PRIu64 ")\n", as->devname, pos, rtptime); - - playback_start(as, pos, rtptime); - } - - playback_write(as, buf, rtptime); - } -} - -static int -alsa_flush(output_status_cb cb, uint64_t rtptime) -{ - struct alsa_session *as; - int i; - - i = 0; - for (as = sessions; as; as = as->next) - { - i++; - - snd_pcm_drop(hdl); - prebuf_free(as); - - as->status_cb = cb; - as->state = ALSA_STATE_STARTED; - alsa_status(as); - } - - return i; -} - -static void -alsa_set_status_cb(struct output_session *session, output_status_cb cb) -{ - struct alsa_session *as = session->session; - - as->status_cb = cb; -} - static int alsa_init(void) { struct output_device *device; cfg_t *cfg_audio; - char *nickname; - char *type; - int original_adjust; + const char *type; + // Is ALSA enabled in config? cfg_audio = cfg_getsec(cfg, "audio"); type = cfg_getstr(cfg_audio, "type"); - if (type && (strcasecmp(type, "alsa") != 0)) return -1; - card_name = cfg_getstr(cfg_audio, "card"); - mixer_name = cfg_getstr(cfg_audio, "mixer"); - mixer_device_name = cfg_getstr(cfg_audio, "mixer_device"); - if (mixer_device_name == NULL || strlen(mixer_device_name) == 0) - mixer_device_name = card_name; - nickname = cfg_getstr(cfg_audio, "nickname"); - offset = cfg_getint(cfg_audio, "offset"); - if (abs(offset) > 44100) - { - DPRINTF(E_LOG, L_LAUDIO, "The ALSA offset (%d) set in the configuration is out of bounds\n", offset); - offset = 44100 * (offset/abs(offset)); - } + alsa_sync_disable = cfg_getbool(cfg_audio, "sync_disable"); + alsa_latency_history_size = cfg_getint(cfg_audio, "adjust_period_seconds"); - original_adjust = adjust_period_seconds = cfg_getint(cfg_audio, "adjust_period_seconds"); - if (adjust_period_seconds < 1) - adjust_period_seconds = 1; - else if (adjust_period_seconds > 20) - adjust_period_seconds = 20; - if (original_adjust != adjust_period_seconds) - DPRINTF(E_LOG, L_LAUDIO, "Clamped ALSA adjust_period_seconds from %d to %d\n", original_adjust, adjust_period_seconds); - - device = calloc(1, sizeof(struct output_device)); - if (!device) - { - DPRINTF(E_LOG, L_LAUDIO, "Out of memory for ALSA device\n"); - return -1; - } + CHECK_NULL(L_LAUDIO, device = calloc(1, sizeof(struct output_device))); device->id = 0; - device->name = strdup(nickname); + device->name = strdup(cfg_getstr(cfg_audio, "nickname")); device->type = OUTPUT_TYPE_ALSA; device->type_name = outputs_name(device->type); - device->advertised = 1; device->has_video = 0; - DPRINTF(E_INFO, L_LAUDIO, "Adding ALSA device '%s' with name '%s'\n", card_name, nickname); + DPRINTF(E_INFO, L_LAUDIO, "Adding ALSA device '%s' with name '%s'\n", cfg_getstr(cfg_audio, "card"), device->name); player_device_add(device); snd_lib_error_set_handler(logger_alsa); - hdl = NULL; - mixer_hdl = NULL; - vol_elem = NULL; - return 0; } static void alsa_deinit(void) { + struct alsa_session *as; + snd_lib_error_set_handler(NULL); + + for (as = sessions; sessions; as = sessions) + { + sessions = as->next; + alsa_session_free(as); + } } struct output_definition output_alsa = @@ -1069,11 +1111,9 @@ struct output_definition output_alsa = .deinit = alsa_deinit, .device_start = alsa_device_start, .device_stop = alsa_device_stop, + .device_flush = alsa_device_flush, .device_probe = alsa_device_probe, .device_volume_set = alsa_device_volume_set, - .playback_start = alsa_playback_start, - .playback_stop = alsa_playback_stop, + .device_cb_set = alsa_device_cb_set, .write = alsa_write, - .flush = alsa_flush, - .status_cb = alsa_set_status_cb, }; diff --git a/src/outputs/cast.c b/src/outputs/cast.c index f777cac9..f646fc05 100644 --- a/src/outputs/cast.c +++ b/src/outputs/cast.c @@ -1,8 +1,5 @@ /* - * Copyright (C) 2015-2016 Espen Jürgensen - * - * Credit goes to the authors of pychromecast and those before that who have - * discovered how to do this. + * Copyright (C) 2015-2019 Espen Jürgensen * * 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 @@ -50,8 +47,10 @@ #include "conffile.h" #include "mdns.h" +#include "transcode.h" #include "logger.h" #include "player.h" +#include "rtp_common.h" #include "outputs.h" #ifdef HAVE_PROTOBUF_OLD @@ -66,20 +65,21 @@ #define CAFILE "/etc/ssl/certs/ca-certificates.crt" // Seconds without a heartbeat from the Chromecast before we close the session -#define HEARTBEAT_TIMEOUT 8 -// Seconds after a flush (pause) before we close the session -#define FLUSH_TIMEOUT 30 +//#define HEARTBEAT_TIMEOUT 30 // Seconds to wait for a reply before making the callback requested by caller #define REPLY_TIMEOUT 5 -// ID of the default receiver app -#define CAST_APP_ID "CC1AD845" +// ID of the audio mirroring app used by Chrome (Google Home) +#define CAST_APP_ID "85CDB22F" +// Old mirroring app (Chromecast) +#define CAST_APP_ID_OLD "0F5096E8" // Namespaces #define NS_CONNECTION "urn:x-cast:com.google.cast.tp.connection" #define NS_RECEIVER "urn:x-cast:com.google.cast.receiver" #define NS_HEARTBEAT "urn:x-cast:com.google.cast.tp.heartbeat" #define NS_MEDIA "urn:x-cast:com.google.cast.media" +#define NS_WEBRTC "urn:x-cast:com.google.cast.webrtc" #define USE_TRANSPORT_ID (1 << 1) #define USE_REQUEST_ID (1 << 2) @@ -87,6 +87,23 @@ #define CALLBACK_REGISTER_SIZE 32 +// Chromium will send OPUS encoded 10 ms packets (48kHz), about 120 bytes. We +// use a 20 ms packet, so 50 pkts/sec, because that's the default for ffmpeg. +// A 20 ms audio packet at 48000 kHz makes this number 48000 * (20 / 1000) +#define CAST_SAMPLES_PER_PACKET 960 + +#define CAST_QUALITY_SAMPLE_RATE_DEFAULT 48000 +#define CAST_QUALITY_BITS_PER_SAMPLE_DEFAULT 16 +#define CAST_QUALITY_CHANNELS_DEFAULT 2 + +/* Notes + * OFFER/ANSWER <-webrtc + * RTCP/RTP + * XR custom receiver report + * Control and data on same UDP connection + * OPUS encoded + */ + //#define DEBUG_CONNECTION 1 union sockaddr_all @@ -100,6 +117,13 @@ union sockaddr_all struct cast_session; struct cast_msg_payload; +// See cast_packet_header_make() +#define CAST_HEADER_SIZE 11 +#define CAST_PACKET_BUFFER_SIZE 1000 + +static struct encode_ctx *cast_encode_ctx; +static struct evbuffer *cast_encoded_data; + typedef void (*cast_reply_cb)(struct cast_session *cs, struct cast_msg_payload *payload); // Session is starting up @@ -109,7 +133,7 @@ typedef void (*cast_reply_cb)(struct cast_session *cs, struct cast_msg_payload * // Media is loaded in the receiver app #define CAST_STATE_F_MEDIA_LOADED (1 << 15) // Media is playing in the receiver app -#define CAST_STATE_F_MEDIA_PLAYING (1 << 16) +#define CAST_STATE_F_MEDIA_STREAMING (1 << 16) // Beware, the order of this enum has meaning enum cast_state @@ -122,22 +146,40 @@ enum cast_state CAST_STATE_DISCONNECTED = CAST_STATE_F_STARTUP | 0x01, // TCP connect, TLS handshake, CONNECT and GET_STATUS request CAST_STATE_CONNECTED = CAST_STATE_F_STARTUP | 0x02, - // Default media receiver app is launched + // Receiver app has been launched CAST_STATE_MEDIA_LAUNCHED = CAST_STATE_F_STARTUP | 0x03, - // CONNECT and GET_STATUS made to receiver app + // CONNECT, GET_STATUS and OFFER made to receiver app CAST_STATE_MEDIA_CONNECTED = CAST_STATE_F_MEDIA_CONNECTED, - // Receiver app has loaded our media - CAST_STATE_MEDIA_LOADED = CAST_STATE_F_MEDIA_CONNECTED | CAST_STATE_F_MEDIA_LOADED, - // After PAUSE - CAST_STATE_MEDIA_PAUSED = CAST_STATE_F_MEDIA_CONNECTED | CAST_STATE_F_MEDIA_LOADED | 0x01, - // After LOAD - CAST_STATE_MEDIA_BUFFERING = CAST_STATE_F_MEDIA_CONNECTED | CAST_STATE_F_MEDIA_LOADED | CAST_STATE_F_MEDIA_PLAYING, - // After PLAY - CAST_STATE_MEDIA_PLAYING = CAST_STATE_F_MEDIA_CONNECTED | CAST_STATE_F_MEDIA_LOADED | CAST_STATE_F_MEDIA_PLAYING | 0x01, + // After OFFER + CAST_STATE_MEDIA_STREAMING = CAST_STATE_F_MEDIA_CONNECTED | CAST_STATE_F_MEDIA_STREAMING, +}; + +struct cast_master_session +{ + struct evbuffer *evbuf; + int evbuf_samples; + + struct rtp_session *rtp_session; + + struct media_quality quality; + + uint8_t *rawbuf; + size_t rawbuf_size; + int samples_per_packet; + + // Number of samples that we tell the output to buffer (this will mean that + // the position that we send in the sync packages are offset by this amount + // compared to the rtptimes of the corresponding RTP packages we are sending) + int output_buffer_samples; }; struct cast_session { + uint64_t device_id; + int callback_id; + + struct cast_master_session *master_session; + // Current state enum cast_state state; @@ -145,17 +187,20 @@ struct cast_session enum cast_state wanted_state; // Connection fd and session, and listener event - int server_fd; + int64_t server_fd; // Use int64 so we can cast in gnutls_transport_set_ptr() gnutls_session_t tls_session; struct event *ev; char *devname; char *address; + int family; unsigned short port; // ChromeCast uses a float between 0 - 1 float volume; + uint32_t ssrc_id; + // IP address URL of forked-daapd's mp3 stream char stream_url[128]; @@ -172,15 +217,13 @@ struct cast_session // register our retry so that we on only retry once. int retry; - // Session info from the ChromeCast + // Session info from the Chromecast char *transport_id; char *session_id; int media_session_id; - /* Do not dereference - only passed to the status cb */ - struct output_device *device; - struct output_session *output_session; - output_status_cb status_cb; + int udp_fd; + unsigned short udp_port; struct cast_session *next; }; @@ -195,9 +238,13 @@ enum cast_msg_types GET_STATUS, RECEIVER_STATUS, LAUNCH, + LAUNCH_OLD, + LAUNCH_ERROR, STOP, MEDIA_CONNECT, MEDIA_CLOSE, + OFFER, + ANSWER, MEDIA_GET_STATUS, MEDIA_STATUS, MEDIA_LOAD, @@ -207,6 +254,7 @@ enum cast_msg_types MEDIA_LOAD_FAILED, MEDIA_LOAD_CANCELLED, SET_VOLUME, + PRESENTATION, }; struct cast_msg_basic @@ -227,7 +275,9 @@ struct cast_msg_payload const char *session_id; const char *transport_id; const char *player_state; + const char *result; int media_session_id; + unsigned short udp_port; }; // Array of the cast messages that we use. Must be in sync with cast_msg_types. @@ -278,6 +328,16 @@ struct cast_msg_basic cast_msg[] = .payload = "{'type':'LAUNCH','requestId':%d,'appId':'" CAST_APP_ID "'}", .flags = USE_REQUEST_ID_ONLY, }, + { + .type = LAUNCH_OLD, + .namespace = NS_RECEIVER, + .payload = "{'type':'LAUNCH','requestId':%d,'appId':'" CAST_APP_ID_OLD "'}", + .flags = USE_REQUEST_ID_ONLY, + }, + { + .type = LAUNCH_ERROR, + .tag = "LAUNCH_ERROR", + }, { .type = STOP, .namespace = NS_RECEIVER, @@ -296,6 +356,21 @@ struct cast_msg_basic cast_msg[] = .payload = "{'type':'CLOSE'}", .flags = USE_TRANSPORT_ID, }, + { + .type = OFFER, + .namespace = NS_WEBRTC, + // codecName can be aac or opus, ssrc should be random + // We don't set 'aesKey' and 'aesIvMask' + // sampleRate seems to be ignored + // storeTime unknown meaning - perhaps size of buffer? + // targetDelay - should be RTP delay in ms, but doesn't seem to change anything? + .payload = "{'type':'OFFER','seqNum':%d,'offer':{'castMode':'mirroring','supportedStreams':[{'index':0,'type':'audio_source','codecName':'opus','rtpProfile':'cast','rtpPayloadType':127,'ssrc':%d,'storeTime':400,'targetDelay':400,'bitRate':128000,'sampleRate':48000,'timeBase':'1/48000','channels':2,'receiverRtcpEventLog':false}]}}", + .flags = USE_TRANSPORT_ID | USE_REQUEST_ID, + }, + { + .type = ANSWER, + .tag = "ANSWER", + }, { .type = MEDIA_GET_STATUS, .namespace = NS_MEDIA, @@ -344,6 +419,12 @@ struct cast_msg_basic cast_msg[] = .payload = "{'type':'SET_VOLUME','volume':{'level':%.2f,'muted':0},'requestId':%d}", .flags = USE_REQUEST_ID, }, + { + .type = PRESENTATION, + .namespace = NS_WEBRTC, + .payload = "{'type':'PRESENTATION','sessionId':'%s',seqnum:%d,'title':'forked-daapd','icons':[{'url':'http://www.gyfgafguf.dk/images/fugl.jpg'}] }", + .flags = USE_TRANSPORT_ID | USE_REQUEST_ID, + }, { .type = 0, }, @@ -354,29 +435,31 @@ extern struct event_base *evbase_player; /* Globals */ static gnutls_certificate_credentials_t tls_credentials; -static struct cast_session *sessions; -static struct event *flush_timer; -static struct timeval heartbeat_timeout = { HEARTBEAT_TIMEOUT, 0 }; -static struct timeval flush_timeout = { FLUSH_TIMEOUT, 0 }; +static struct cast_session *cast_sessions; +static struct cast_master_session *cast_master_session; +//static struct timeval heartbeat_timeout = { HEARTBEAT_TIMEOUT, 0 }; static struct timeval reply_timeout = { REPLY_TIMEOUT, 0 }; +static struct media_quality cast_quality_default = { CAST_QUALITY_SAMPLE_RATE_DEFAULT, CAST_QUALITY_BITS_PER_SAMPLE_DEFAULT, CAST_QUALITY_CHANNELS_DEFAULT }; /* ------------------------------- MISC HELPERS ----------------------------- */ static int -tcp_connect(const char *address, unsigned int port, int family) +cast_connect(const char *address, unsigned short port, int family, int type) { union sockaddr_all sa; int fd; int len; int ret; + DPRINTF(E_DBG, L_CAST, "Connecting to %s (family=%d), port %u\n", address, family, port); + // TODO Open non-block right away so we don't block the player while connecting // and during TLS handshake (we would probably need to introduce a deferredev) #ifdef SOCK_CLOEXEC - fd = socket(family, SOCK_STREAM | SOCK_CLOEXEC, 0); + fd = socket(family, type | SOCK_CLOEXEC, 0); #else - fd = socket(family, SOCK_STREAM, 0); + fd = socket(family, type, 0); #endif if (fd < 0) { @@ -425,7 +508,7 @@ tcp_connect(const char *address, unsigned int port, int family) } static void -tcp_close(int fd) +cast_disconnect(int fd) { /* no more receptions */ shutdown(fd, SHUT_RDWR); @@ -518,17 +601,52 @@ squote_to_dquote(char *buf) /* ----------------------------- SESSION CLEANUP ---------------------------- */ +static void +master_session_free(struct cast_master_session *cms) +{ + if (!cms) + return; + + outputs_quality_unsubscribe(&cms->rtp_session->quality); + rtp_session_free(cms->rtp_session); + evbuffer_free(cms->evbuf); + free(cms->rawbuf); + free(cms); +} + +static void +master_session_cleanup(struct cast_master_session *cms) +{ + struct cast_session *cs; + + // First check if any other session is using the master session + for (cs = cast_sessions; cs; cs=cs->next) + { + if (cs->master_session == cms) + return; + } + + if (cms == cast_master_session) + cast_master_session = NULL; + + master_session_free(cms); +} + static void cast_session_free(struct cast_session *cs) { if (!cs) return; + master_session_cleanup(cs->master_session); + event_free(cs->reply_timeout); event_free(cs->ev); if (cs->server_fd >= 0) - tcp_close(cs->server_fd); + cast_disconnect(cs->server_fd); + if (cs->udp_fd >= 0) + cast_disconnect(cs->udp_fd); gnutls_deinit(cs->tls_session); @@ -542,8 +660,6 @@ cast_session_free(struct cast_session *cs) if (cs->transport_id) free(cs->transport_id); - free(cs->output_session); - free(cs); } @@ -552,11 +668,11 @@ cast_session_cleanup(struct cast_session *cs) { struct cast_session *s; - if (cs == sessions) - sessions = sessions->next; + if (cs == cast_sessions) + cast_sessions = cast_sessions->next; else { - for (s = sessions; s && (s->next != cs); s = s->next) + for (s = cast_sessions; s && (s->next != cs); s = s->next) ; /* EMPTY */ if (!s) @@ -565,6 +681,8 @@ cast_session_cleanup(struct cast_session *cs) s->next = cs->next; } + outputs_device_session_remove(cs->device_id); + cast_session_free(cs); } @@ -618,6 +736,10 @@ cast_msg_send(struct cast_session *cs, enum cast_msg_types type, cast_reply_cb r snprintf(msg_buf, sizeof(msg_buf), cast_msg[type].payload, cs->request_id); else if (type == STOP) snprintf(msg_buf, sizeof(msg_buf), cast_msg[type].payload, cs->session_id, cs->request_id); + else if (type == OFFER) + snprintf(msg_buf, sizeof(msg_buf), cast_msg[type].payload, cs->request_id, cs->ssrc_id); + else if (type == PRESENTATION) + snprintf(msg_buf, sizeof(msg_buf), cast_msg[type].payload, cs->session_id, cs->request_id); else if (type == MEDIA_LOAD) snprintf(msg_buf, sizeof(msg_buf), cast_msg[type].payload, cs->stream_url, cs->session_id, cs->request_id); else if ((type == MEDIA_PLAY) || (type == MEDIA_PAUSE) || (type == MEDIA_STOP)) @@ -694,6 +816,17 @@ cast_msg_parse(struct cast_msg_payload *payload, char *s) if (json_object_object_get_ex(haystack, "requestId", &needle)) payload->request_id = json_object_get_int(needle); + else if (json_object_object_get_ex(haystack, "seqNum", &needle)) + payload->request_id = json_object_get_int(needle); + + if (json_object_object_get_ex(haystack, "answer", &somehay) && + json_object_object_get_ex(somehay, "udpPort", &needle) && + json_object_get_type(needle) == json_type_int ) + payload->udp_port = json_object_get_int(needle); + + if (json_object_object_get_ex(haystack, "result", &needle) && + json_object_get_type(needle) == json_type_string ) + payload->result = json_object_get_string(needle); // Might be done now if ((payload->type != RECEIVER_STATUS) && (payload->type != MEDIA_STATUS)) @@ -753,7 +886,6 @@ cast_msg_process(struct cast_session *cs, const uint8_t *data, size_t len) cast_reply_cb reply_cb; struct cast_msg_payload payload = { 0 }; void *hdl; - int unknown_app_id; int unknown_session_id; int i; @@ -812,11 +944,10 @@ cast_msg_process(struct cast_session *cs, const uint8_t *data, size_t len) if (payload.type == RECEIVER_STATUS && (cs->state & CAST_STATE_F_MEDIA_CONNECTED)) { - unknown_app_id = payload.app_id && (strcmp(payload.app_id, CAST_APP_ID) != 0); unknown_session_id = payload.session_id && (strcmp(payload.session_id, cs->session_id) != 0); - if (unknown_app_id || unknown_session_id) + if (unknown_session_id) { - DPRINTF(E_WARN, L_CAST, "Our session on '%s' was hijacked\n", cs->devname); + DPRINTF(E_LOG, L_CAST, "Our session '%s' on '%s' was lost to session '%s'\n", cs->session_id, cs->devname, payload.session_id); // Downgrade state, we don't have the receiver app any more cs->state = CAST_STATE_CONNECTED; @@ -825,7 +956,15 @@ cast_msg_process(struct cast_session *cs, const uint8_t *data, size_t len) } } - if (payload.type == MEDIA_STATUS && (cs->state & CAST_STATE_F_MEDIA_PLAYING)) + if (payload.type == CLOSE && (cs->state & CAST_STATE_F_MEDIA_CONNECTED)) + { + // Downgrade state, we can't write any more + cs->state = CAST_STATE_CONNECTED; + cast_session_shutdown(cs, CAST_STATE_FAILED); + goto out_free_parsed; + } + + if (payload.type == MEDIA_STATUS && (cs->state & CAST_STATE_F_MEDIA_STREAMING)) { if (payload.player_state && (strcmp(payload.player_state, "PAUSED") == 0)) { @@ -853,7 +992,6 @@ cast_msg_process(struct cast_session *cs, const uint8_t *data, size_t len) static void cast_status(struct cast_session *cs) { - output_status_cb status_cb = cs->status_cb; enum output_device_state state; switch (cs->state) @@ -870,10 +1008,7 @@ cast_status(struct cast_session *cs) case CAST_STATE_MEDIA_CONNECTED: state = OUTPUT_STATE_CONNECTED; break; - case CAST_STATE_MEDIA_LOADED ... CAST_STATE_MEDIA_PAUSED: - state = OUTPUT_STATE_CONNECTED; - break; - case CAST_STATE_MEDIA_BUFFERING ... CAST_STATE_MEDIA_PLAYING: + case CAST_STATE_MEDIA_STREAMING: state = OUTPUT_STATE_STREAMING; break; default: @@ -881,9 +1016,8 @@ cast_status(struct cast_session *cs) state = OUTPUT_STATE_FAILED; } - cs->status_cb = NULL; - if (status_cb) - status_cb(cs->device, cs->output_session, state); + outputs_cb(cs->callback_id, cs->device_id, state); + cs->callback_id = -1; } /* cast_cb_stop*: Callback chain for shutting down a session */ @@ -930,6 +1064,47 @@ cast_cb_startup_volume(struct cast_session *cs, struct cast_msg_payload *payload cast_status(cs); } +static void +cast_cb_startup_offer(struct cast_session *cs, struct cast_msg_payload *payload) +{ + int ret; + + if (!payload) + { + DPRINTF(E_LOG, L_CAST, "No reply from '%s' to our OFFER request\n", cs->devname); + goto error; + } + else if (payload->type != ANSWER) + { + DPRINTF(E_LOG, L_CAST, "The device '%s' did not give us an ANSWER to our OFFER\n", cs->devname); + goto error; + } + else if (!payload->udp_port || strcmp(payload->result, "ok") != 0) + { + DPRINTF(E_LOG, L_CAST, "Missing UDP port (or unexpected result '%s') in ANSWER - aborting\n", payload->result); + goto error; + } + + DPRINTF(E_INFO, L_CAST, "UDP port in ANSWER is %d\n", payload->udp_port); + + cs->udp_port = payload->udp_port; + + cs->udp_fd = cast_connect(cs->address, cs->udp_port, cs->family, SOCK_DGRAM); + if (cs->udp_fd < 0) + goto error; + + ret = cast_msg_send(cs, SET_VOLUME, cast_cb_startup_volume); + if (ret < 0) + goto error; + + cs->state = CAST_STATE_MEDIA_CONNECTED; + + return; + + error: + cast_session_shutdown(cs, CAST_STATE_FAILED); +} + static void cast_cb_startup_media(struct cast_session *cs, struct cast_msg_payload *payload) { @@ -946,12 +1121,10 @@ cast_cb_startup_media(struct cast_session *cs, struct cast_msg_payload *payload) goto error; } - ret = cast_msg_send(cs, SET_VOLUME, cast_cb_startup_volume); + ret = cast_msg_send(cs, OFFER, cast_cb_startup_offer); if (ret < 0) goto error; - cs->state = CAST_STATE_MEDIA_CONNECTED; - return; error: @@ -984,6 +1157,17 @@ cast_cb_startup_launch(struct cast_session *cs, struct cast_msg_payload *payload goto error; } + if (payload->type == LAUNCH_ERROR && !cs->retry) + { + DPRINTF(E_WARN, L_CAST, "Device '%s' does not support app id '%s', trying '%s' instead\n", cs->devname, CAST_APP_ID, CAST_APP_ID_OLD); + cs->retry++; + ret = cast_msg_send(cs, LAUNCH_OLD, cast_cb_startup_launch); + if (ret < 0) + goto error; + + return; + } + if (payload->type != RECEIVER_STATUS) { DPRINTF(E_LOG, L_CAST, "No RECEIVER_STATUS reply to our LAUNCH (got type: %d) - aborting\n", payload->type); @@ -1074,56 +1258,22 @@ cast_cb_probe(struct cast_session *cs, struct cast_msg_payload *payload) cast_session_shutdown(cs, CAST_STATE_FAILED); } -/* cast_cb_load: Callback from starting playback */ -static void -cast_cb_load(struct cast_session *cs, struct cast_msg_payload *payload) -{ - if (!payload) - { - DPRINTF(E_LOG, L_CAST, "No reply from '%s' to our LOAD request\n", cs->devname); - goto error; - } - else if ((payload->type == MEDIA_LOAD_FAILED) || (payload->type == MEDIA_LOAD_CANCELLED)) - { - DPRINTF(E_LOG, L_CAST, "The device '%s' could not start playback\n", cs->devname); - goto error; - } - else if (!payload->media_session_id) - { - DPRINTF(E_LOG, L_CAST, "Missing media session id in MEDIA_STATUS - aborting\n"); - goto error; - } - - cs->media_session_id = payload->media_session_id; - // We autoplay for the time being - cs->state = CAST_STATE_MEDIA_PLAYING; - - cast_status(cs); - - return; - - error: - cast_session_shutdown(cs, CAST_STATE_FAILED); -} - static void cast_cb_volume(struct cast_session *cs, struct cast_msg_payload *payload) { cast_status(cs); } +/* static void -cast_cb_flush(struct cast_session *cs, struct cast_msg_payload *payload) +cast_cb_presentation(struct cast_session *cs, struct cast_msg_payload *payload) { if (!payload) - DPRINTF(E_LOG, L_CAST, "No reply to PAUSE request from '%s' - will continue\n", cs->devname); + DPRINTF(E_LOG, L_CAST, "No reply to PRESENTATION request from '%s' - will continue\n", cs->devname); else if (payload->type != MEDIA_STATUS) - DPRINTF(E_LOG, L_CAST, "Unexpected reply to PAUSE request from '%s' - will continue\n", cs->devname); - - cs->state = CAST_STATE_MEDIA_PAUSED; - - cast_status(cs); + DPRINTF(E_LOG, L_CAST, "Unexpected reply to PRESENTATION request from '%s' - will continue\n", cs->devname); } +*/ /* The core of this module. Libevent makes a callback to this function whenever * there is new data to be read on the fd from the ChromeCast. If everything is @@ -1140,7 +1290,7 @@ cast_listen_cb(int fd, short what, void *arg) int received; int ret; - for (cs = sessions; cs; cs = cs->next) + for (cs = cast_sessions; cs; cs = cs->next) { if (cs == (struct cast_session *)arg) break; @@ -1245,7 +1395,6 @@ cast_device_cb(const char *name, const char *type, const char *domain, const cha { struct output_device *device; const char *friendly_name; - cfg_t *chromecast; uint32_t id; id = djb_hash(name, strlen(name)); @@ -1261,13 +1410,6 @@ cast_device_cb(const char *name, const char *type, const char *domain, const cha DPRINTF(E_DBG, L_CAST, "Event for Chromecast device '%s' (port %d, id %" PRIu32 ")\n", name, port, id); - chromecast = cfg_gettsec(cfg, "chromecast", name); - if (chromecast && cfg_getbool(chromecast, "exclude")) - { - DPRINTF(E_LOG, L_CAST, "Excluding Chromecast device '%s' as set in config\n", name); - return; - } - device = calloc(1, sizeof(struct output_device)); if (!device) { @@ -1320,14 +1462,62 @@ cast_device_cb(const char *name, const char *type, const char *domain, const cha } +/* --------------------------------- METADATA ------------------------------- */ + +/* +static void +metadata_send(struct cast_session *cs) +{ + cast_msg_send(cs, PRESENTATION, cast_cb_presentation); +} +*/ + /* --------------------- SESSION CONSTRUCTION AND SHUTDOWN ------------------ */ -// Allocates a session and sets of the startup sequence until the session reaches -// the CAST_STATE_MEDIA_CONNECTED status (so it is ready to load media) -static struct cast_session * -cast_session_make(struct output_device *device, int family, output_status_cb cb) +static struct cast_master_session * +master_session_make(struct media_quality *quality) +{ + struct cast_master_session *cms; + int ret; + + // First check if we already have a master session, then just use that + if (cast_master_session) + return cast_master_session; + + // Let's create a master session + ret = outputs_quality_subscribe(quality); + if (ret < 0) + { + DPRINTF(E_LOG, L_CAST, "Could not subscribe to required audio quality (%d/%d/%d)\n", quality->sample_rate, quality->bits_per_sample, quality->channels); + return NULL; + } + + CHECK_NULL(L_CAST, cms = calloc(1, sizeof(struct cast_master_session))); + + cms->rtp_session = rtp_session_new(quality, CAST_PACKET_BUFFER_SIZE, 0); + if (!cms->rtp_session) + { + outputs_quality_unsubscribe(quality); + free(cms); + return NULL; + } + + cms->quality = *quality; + cms->samples_per_packet = CAST_SAMPLES_PER_PACKET; + cms->rawbuf_size = STOB(cms->samples_per_packet, quality->bits_per_sample, quality->channels); + cms->output_buffer_samples = OUTPUTS_BUFFER_DURATION * quality->sample_rate; + + CHECK_NULL(L_CAST, cms->rawbuf = malloc(cms->rawbuf_size)); + CHECK_NULL(L_CAST, cms->evbuf = evbuffer_new()); + + cast_master_session = cms; + + return cms; +} + +static struct cast_session * +cast_session_make(struct output_device *device, int family, int callback_id) { - struct output_session *os; struct cast_session *cs; const char *proto; const char *err; @@ -1359,28 +1549,20 @@ cast_session_make(struct output_device *device, int family, output_status_cb cb) return NULL; } - os = calloc(1, sizeof(struct output_session)); - if (!os) - { - DPRINTF(E_LOG, L_CAST, "Out of memory (os)\n"); - return NULL; - } + CHECK_NULL(L_CAST, cs = calloc(1, sizeof(struct cast_session))); - cs = calloc(1, sizeof(struct cast_session)); - if (!cs) - { - DPRINTF(E_LOG, L_CAST, "Out of memory (cs)\n"); - free(os); - return NULL; - } - - os->session = cs; - os->type = device->type; - - cs->output_session = os; cs->state = CAST_STATE_DISCONNECTED; - cs->device = device; - cs->status_cb = cb; + cs->device_id = device->id; + cs->callback_id = callback_id; + + cs->master_session = master_session_make(&cast_quality_default); + if (!cs->master_session) + { + DPRINTF(E_LOG, L_CAST, "Could not attach a master session for device '%s'\n", device->name); + goto out_free_session; + } + + cs->ssrc_id = cs->master_session->rtp_session->ssrc_id; /* Init TLS session, use default priorities and put the x509 credentials to the current session */ if ( ((ret = gnutls_init(&cs->tls_session, GNUTLS_CLIENT)) != GNUTLS_E_SUCCESS) || @@ -1388,10 +1570,10 @@ cast_session_make(struct output_device *device, int family, output_status_cb cb) ((ret = gnutls_credentials_set(cs->tls_session, GNUTLS_CRD_CERTIFICATE, tls_credentials)) != GNUTLS_E_SUCCESS) ) { DPRINTF(E_LOG, L_CAST, "Could not initialize GNUTLS session: %s\n", gnutls_strerror(ret)); - goto out_free_session; + goto out_free_master_session; } - cs->server_fd = tcp_connect(address, port, family); + cs->server_fd = cast_connect(address, port, family, SOCK_STREAM); if (cs->server_fd < 0) { DPRINTF(E_LOG, L_CAST, "Could not connect to %s\n", device->name); @@ -1430,15 +1612,21 @@ cast_session_make(struct output_device *device, int family, output_status_cb cb) flags = fcntl(cs->server_fd, F_GETFL, 0); fcntl(cs->server_fd, F_SETFL, flags | O_NONBLOCK); - event_add(cs->ev, &heartbeat_timeout); + event_add(cs->ev, NULL); // &heartbeat_timeout cs->devname = strdup(device->name); cs->address = strdup(address); + cs->family = family; + + cs->udp_fd = -1; cs->volume = 0.01 * device->volume; - cs->next = sessions; - sessions = cs; + cs->next = cast_sessions; + cast_sessions = cs; + + // cs is now the official device session + outputs_device_session_add(device->id, cs); proto = gnutls_protocol_get_name(gnutls_protocol_get_version(cs->tls_session)); @@ -1450,9 +1638,11 @@ cast_session_make(struct output_device *device, int family, output_status_cb cb) event_free(cs->reply_timeout); event_free(cs->ev); out_close_connection: - tcp_close(cs->server_fd); + cast_disconnect(cs->server_fd); out_deinit_gnutls: gnutls_deinit(cs->tls_session); + out_free_master_session: + master_session_cleanup(cs->master_session); out_free_session: free(cs); @@ -1483,12 +1673,14 @@ cast_session_shutdown(struct cast_session *cs, enum cast_state wanted_state) pending = 0; switch (cs->state) { - case CAST_STATE_MEDIA_LOADED ... CAST_STATE_MEDIA_PLAYING: + case CAST_STATE_MEDIA_STREAMING: ret = cast_msg_send(cs, MEDIA_STOP, cast_cb_stop_media); pending = 1; break; case CAST_STATE_MEDIA_CONNECTED: + cast_disconnect(cs->udp_fd); + cs->udp_fd = -1; ret = cast_msg_send(cs, MEDIA_CLOSE, NULL); cs->state = CAST_STATE_MEDIA_LAUNCHED; if ((ret < 0) || (wanted_state >= CAST_STATE_MEDIA_LAUNCHED)) @@ -1505,7 +1697,7 @@ cast_session_shutdown(struct cast_session *cs, enum cast_state wanted_state) ret = cast_msg_send(cs, CLOSE, NULL); if (ret == 0) gnutls_bye(cs->tls_session, GNUTLS_SHUT_RDWR); - tcp_close(cs->server_fd); + cast_disconnect(cs->server_fd); cs->server_fd = -1; cs->state = CAST_STATE_DISCONNECTED; break; @@ -1545,20 +1737,207 @@ cast_session_shutdown(struct cast_session *cs, enum cast_state wanted_state) } +/* ------------------ PREPARING AND SENDING CAST RTP PACKETS ---------------- */ + +// Makes a Cast RTP packet (source: Chromium's media/cast/net/rtp/rtp_packetizer.cc) +// +// A Cast RTP packet is made of: +// RTP header (12 bytes) +// Cast header (7 bytes) +// Extension data (4 bytes) +// Packet data +// +// The Cast header + extension (optional?) consists of: +// 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 +// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +// |k|r| n_ext | frame_id | packet id | +// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +// | max_packet_id | ref_frame_id | ext_type | +// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +// | ext_size | new_playout_delay_ms | +// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +// +// k: Is the frame a key frame? +// r: Is there a reference frame id? +// n_ext: Number of Cast extensions (Chromium uses 1: Adaptive Latency) +// ext_type: 0x04 Adaptive Latency extension +// ext_size: 0x02 -> 2 bytes +// new_playout_delay_ms: ?? + +// OPUS encodes the rawbuf payload +static int +payload_encode(struct evbuffer *evbuf, uint8_t *rawbuf, size_t rawbuf_size, int nsamples, struct media_quality *quality) +{ + transcode_frame *frame; + int len; + + frame = transcode_frame_new(rawbuf, rawbuf_size, nsamples, quality); + if (!frame) + { + DPRINTF(E_LOG, L_CAST, "Could not convert raw PCM to frame (bufsize=%zu)\n", rawbuf_size); + return -1; + } + + len = transcode_encode(evbuf, cast_encode_ctx, frame, 0); + transcode_frame_free(frame); + if (len < 0) + { + DPRINTF(E_LOG, L_CAST, "Could not Opus encode frame\n"); + return -1; + } + + return len; +} + +static int +packet_prepare(struct rtp_packet *pkt, struct evbuffer *evbuf) +{ + + // Cast header + memset(pkt->payload, 0, CAST_HEADER_SIZE); + pkt->payload[0] = 0xc1; // k = 1, r = 1 and one extension + pkt->payload[1] = (char)pkt->seqnum; + // packet_id and max_packet_id don't seem to be used, so leave them at 0 + pkt->payload[6] = (char)pkt->seqnum; + pkt->payload[7] = 0x04; // kCastRtpExtensionAdaptiveLatency has id (1 << 2) + pkt->payload[8] = 0x02; // Extension will use two bytes + // leave extension values at 0, but Chromium sets them to: + // (frame.new_playout_delay_ms >> 8) and frame.new_playout_delay_ms (normal byte values are 0x03 0x20) + + // Copy payload + return evbuffer_remove(evbuf, pkt->payload + CAST_HEADER_SIZE, pkt->payload_len - CAST_HEADER_SIZE); +} + +static int +packet_send(struct cast_session *cs, struct rtp_packet *pkt) +{ + int ret; + + ret = send(cs->udp_fd, pkt->data, pkt->data_len, 0); + if (ret < 0) + { + DPRINTF(E_LOG, L_CAST, "Send error for '%s': %s\n", cs->devname, strerror(errno)); + return -1; + } + else if (ret != pkt->data_len) + { + DPRINTF(E_WARN, L_CAST, "Partial send (%d) for '%s'\n", ret, cs->devname); + return 0; + } + +/* DPRINTF(E_DBG, L_PLAYER, "RTP PACKET seqnum %u, rtptime %u, payload 0x%x, pktbuf_s %zu\n", + cs->master_session->rtp_session->seqnum, + cs->master_session->rtp_session->pos, + pkt->header[1], + cs->master_session->rtp_session->pktbuf_len + ); +*/ + return 0; +} + +static int +packets_send(struct cast_master_session *cms) +{ + struct rtp_packet *pkt; + struct cast_session *cs; + struct cast_session *next; + int len; + int ret; + + // Encode payload into cast_encoded_data + len = payload_encode(cast_encoded_data, cms->rawbuf, cms->rawbuf_size, cms->samples_per_packet, &cms->quality); + if (len < 0) + return -1; + + // Chromium uses a RTP payload type that is 0xff + pkt = rtp_packet_next(cms->rtp_session, CAST_HEADER_SIZE + len, cms->samples_per_packet, 0xff); + + // Creates Cast header + adds payload + ret = packet_prepare(pkt, cast_encoded_data); + if (ret < 0) + return -1; + + for (cs = cast_sessions; cs; cs = next) + { + next = cs->next; + + if (cs->master_session != cms || !(cs->state & CAST_STATE_F_MEDIA_CONNECTED)) + continue; + + ret = packet_send(cs, pkt); + if (ret < 0) + { + // Downgrade state immediately to avoid further write attempts + cs->state = CAST_STATE_MEDIA_LAUNCHED; + cast_session_shutdown(cs, CAST_STATE_FAILED); + } + } + + // Commits packet to retransmit buffer, and prepares the session for the next packet + rtp_packet_commit(cms->rtp_session, pkt); + + return 0; +} + +/* TODO This does not currently work - need to investigate what sync the devices support +static void +packets_sync_send(struct cast_master_session *cms, struct timespec pts) +{ + struct rtp_packet *sync_pkt; + struct cast_session *cs; + struct rtcp_timestamp cur_stamp; + struct timespec ts; + bool is_sync_time; + + // Check if it is time send a sync packet to sessions that are already running + is_sync_time = rtp_sync_is_time(cms->rtp_session); + + // (See raop.c for more comments on sync packets) + cur_stamp.ts.tv_sec = pts.tv_sec; + cur_stamp.ts.tv_nsec = pts.tv_nsec; + + clock_gettime(CLOCK_MONOTONIC, &ts); + + cur_stamp.pos = cms->rtp_session->pos + cms->evbuf_samples - cms->output_buffer_samples; + + for (cs = cast_sessions; cs; cs = cs->next) + { + if (cs->master_session != cms) + continue; + + // A device has joined and should get an init sync packet + if (cs->state == CAST_STATE_MEDIA_CONNECTED) + { + sync_pkt = rtp_sync_packet_next(cms->rtp_session, &cur_stamp, 0x80); + packet_send(cs, sync_pkt); + + DPRINTF(E_DBG, L_PLAYER, "Start sync packet sent to '%s': cur_pos=%" PRIu32 ", cur_ts=%lu:%lu, now=%lu:%lu, rtptime=%" PRIu32 ",\n", + cs->devname, cur_stamp.pos, cur_stamp.ts.tv_sec, cur_stamp.ts.tv_nsec, ts.tv_sec, ts.tv_nsec, cms->rtp_session->pos); + } + else if (is_sync_time && cs->state == CAST_STATE_MEDIA_STREAMING) + { + sync_pkt = rtp_sync_packet_next(cms->rtp_session, &cur_stamp, 0x80); + packet_send(cs, sync_pkt); + } + } +} +*/ + /* ------------------ INTERFACE FUNCTIONS CALLED BY OUTPUTS.C --------------- */ static int -cast_device_start(struct output_device *device, output_status_cb cb, uint64_t rtptime) +cast_device_start_generic(struct output_device *device, int callback_id, cast_reply_cb reply_cb) { struct cast_session *cs; int ret; - cs = cast_session_make(device, AF_INET6, cb); + cs = cast_session_make(device, AF_INET6, callback_id); if (cs) { ret = cast_msg_send(cs, CONNECT, NULL); if (ret == 0) - ret = cast_msg_send(cs, GET_STATUS, cast_cb_startup_connect); + ret = cast_msg_send(cs, GET_STATUS, reply_cb); if (ret < 0) { @@ -1569,13 +1948,13 @@ cast_device_start(struct output_device *device, output_status_cb cb, uint64_t rt return 0; } - cs = cast_session_make(device, AF_INET, cb); + cs = cast_session_make(device, AF_INET, callback_id); if (!cs) return -1; ret = cast_msg_send(cs, CONNECT, NULL); if (ret == 0) - ret = cast_msg_send(cs, GET_STATUS, cast_cb_startup_connect); + ret = cast_msg_send(cs, GET_STATUS, reply_cb); if (ret < 0) { @@ -1587,66 +1966,57 @@ cast_device_start(struct output_device *device, output_status_cb cb, uint64_t rt return 0; } +static int +cast_device_start(struct output_device *device, int callback_id) +{ + return cast_device_start_generic(device, callback_id, cast_cb_startup_connect); +} + +static int +cast_device_probe(struct output_device *device, int callback_id) +{ + return cast_device_start_generic(device, callback_id, cast_cb_probe); +} + +static int +cast_device_stop(struct output_device *device, int callback_id) +{ + struct cast_session *cs = device->session; + + cs->callback_id = callback_id; + + cast_session_shutdown(cs, CAST_STATE_NONE); + + return 0; +} + +static int +cast_device_flush(struct output_device *device, int callback_id) +{ + struct cast_session *cs = device->session; + + cs->callback_id = callback_id; + cs->state = CAST_STATE_MEDIA_CONNECTED; + cast_status(cs); + + return 0; +} + static void -cast_device_stop(struct output_session *session) +cast_device_cb_set(struct output_device *device, int callback_id) { - struct cast_session *cs = session->session; + struct cast_session *cs = device->session; - cast_session_shutdown(cs, CAST_STATE_NONE); + cs->callback_id = callback_id; } static int -cast_device_probe(struct output_device *device, output_status_cb cb) +cast_device_volume_set(struct output_device *device, int callback_id) { - struct cast_session *cs; + struct cast_session *cs = device->session; int ret; - cs = cast_session_make(device, AF_INET6, cb); - if (cs) - { - ret = cast_msg_send(cs, CONNECT, NULL); - if (ret == 0) - ret = cast_msg_send(cs, GET_STATUS, cast_cb_probe); - - if (ret < 0) - { - DPRINTF(E_WARN, L_CAST, "Could not send CONNECT or GET_STATUS request on IPv6 (start)\n"); - cast_session_cleanup(cs); - } - else - return 0; - } - - cs = cast_session_make(device, AF_INET, cb); - if (!cs) - return -1; - - ret = cast_msg_send(cs, CONNECT, NULL); - if (ret == 0) - ret = cast_msg_send(cs, GET_STATUS, cast_cb_probe); - - if (ret < 0) - { - DPRINTF(E_LOG, L_CAST, "Could not send CONNECT or GET_STATUS request on IPv4 (start)\n"); - cast_session_cleanup(cs); - return -1; - } - - return 0; -} - -static int -cast_volume_set(struct output_device *device, output_status_cb cb) -{ - struct cast_session *cs; - int ret; - - if (!device->session || !device->session->session) - return 0; - - cs = device->session->session; - - if (!(cs->state & CAST_STATE_F_MEDIA_CONNECTED)) + if (!cs || !(cs->state & CAST_STATE_F_MEDIA_CONNECTED)) return 0; cs->volume = 0.01 * device->volume; @@ -1659,93 +2029,82 @@ cast_volume_set(struct output_device *device, output_status_cb cb) } // Setting it here means it will not be used for the above cast_session_shutdown - cs->status_cb = cb; + cs->callback_id = callback_id; return 1; } static void -cast_playback_start(uint64_t next_pkt, struct timespec *ts) +cast_write(struct output_buffer *obuf) { + struct cast_master_session *cms; struct cast_session *cs; + int i; - if (evtimer_pending(flush_timer, NULL)) - event_del(flush_timer); + if (!cast_sessions) + return; - // TODO Maybe we could avoid reloading and instead support play->pause->play - for (cs = sessions; cs; cs = cs->next) + cms = cast_master_session; + + for (i = 0; obuf->data[i].buffer; i++) { - if (cs->state & CAST_STATE_F_MEDIA_CONNECTED) - cast_msg_send(cs, MEDIA_LOAD, cast_cb_load); - } -} - -static void -cast_playback_stop(void) -{ - struct cast_session *cs; - struct cast_session *next; - - for (cs = sessions; cs; cs = next) - { - next = cs->next; - if (cs->state & CAST_STATE_F_MEDIA_CONNECTED) - cast_session_shutdown(cs, CAST_STATE_NONE); - } -} - -static void -cast_flush_timer_cb(int fd, short what, void *arg) -{ - DPRINTF(E_DBG, L_CAST, "Flush timer expired; tearing down all sessions\n"); - - cast_playback_stop(); -} - -static int -cast_flush(output_status_cb cb, uint64_t rtptime) -{ - struct cast_session *cs; - struct cast_session *next; - int pending; - int ret; - - pending = 0; - for (cs = sessions; cs; cs = next) - { - next = cs->next; - - if (!(cs->state & CAST_STATE_F_MEDIA_PLAYING)) + if (!quality_is_equal(&obuf->data[i].quality, &cast_quality_default)) continue; - ret = cast_msg_send(cs, MEDIA_PAUSE, cast_cb_flush); - if (ret < 0) - { - cast_session_shutdown(cs, CAST_STATE_FAILED); - continue; - } + // Sends sync packets to new sessions, and if it is sync time then also to old sessions +// packets_sync_send(cms, obuf->pts); - cs->status_cb = cb; - pending++; + // TODO avoid this copy + evbuffer_add(cms->evbuf, obuf->data[i].buffer, obuf->data[i].bufsize); + cms->evbuf_samples += obuf->data[i].samples; + + // Send as many packets as we have data for (one packet requires rawbuf_size bytes) + while (evbuffer_get_length(cms->evbuf) >= cms->rawbuf_size) + { + evbuffer_remove(cms->evbuf, cms->rawbuf, cms->rawbuf_size); + cms->evbuf_samples -= cms->samples_per_packet; + + packets_send(cms); + } } - if (pending > 0) - evtimer_add(flush_timer, &flush_timeout); + // Check for devices that have joined since last write (we have already sent them + // initialization sync and rtp packets via packets_sync_send and packets_send) + for (cs = cast_sessions; cs; cs = cs->next) + { + if (cs->state != CAST_STATE_MEDIA_CONNECTED) + continue; - return pending; + cs->state = CAST_STATE_MEDIA_STREAMING; + // Make a cb? + } } +/* Doesn't work, but left here so it can be fixed static void -cast_set_status_cb(struct output_session *session, output_status_cb cb) +cast_metadata_send(struct output_metadata *metadata) { - struct cast_session *cs = session->session; + struct cast_session *cs; + struct cast_session *next; - cs->status_cb = cb; + for (cs = cast_sessions; cs; cs = next) + { + next = cs->next; + + if (cs->state != CAST_STATE_MEDIA_CONNECTED) + continue; + + metadata_send(cs); + } + + // TODO free the metadata } +*/ static int cast_init(void) { + struct decode_ctx *decode_ctx; int family; int i; int ret; @@ -1770,10 +2129,18 @@ cast_init(void) return -1; } - flush_timer = evtimer_new(evbase_player, cast_flush_timer_cb, NULL); - if (!flush_timer) + decode_ctx = transcode_decode_setup_raw(XCODE_PCM16, &cast_quality_default); + if (!decode_ctx) { - DPRINTF(E_LOG, L_CAST, "Out of memory for flush timer\n"); + DPRINTF(E_LOG, L_CAST, "Could not create decoding context\n"); + goto out_tls_deinit; + } + + cast_encode_ctx = transcode_encode_setup(XCODE_OPUS, &cast_quality_default, decode_ctx, NULL, 0, 0); + transcode_decode_cleanup(&decode_ctx); + if (!cast_encode_ctx) + { + DPRINTF(E_LOG, L_CAST, "Will not be able to stream Chromecast, libav does not support Opus encoding\n"); goto out_tls_deinit; } @@ -1786,13 +2153,15 @@ cast_init(void) if (ret < 0) { DPRINTF(E_LOG, L_CAST, "Could not add mDNS browser for Chromecast devices\n"); - goto out_free_flush_timer; + goto out_encode_ctx_free; } + CHECK_NULL(L_CAST, cast_encoded_data = evbuffer_new()); + return 0; - out_free_flush_timer: - event_free(flush_timer); + out_encode_ctx_free: + transcode_encode_cleanup(&cast_encode_ctx); out_tls_deinit: gnutls_certificate_free_credentials(tls_credentials); gnutls_global_deinit(); @@ -1805,13 +2174,14 @@ cast_deinit(void) { struct cast_session *cs; - for (cs = sessions; sessions; cs = sessions) + for (cs = cast_sessions; cast_sessions; cs = cast_sessions) { - sessions = cs->next; + cast_sessions = cs->next; cast_session_free(cs); } - event_free(flush_timer); + evbuffer_free(cast_encoded_data); + transcode_encode_cleanup(&cast_encode_ctx); gnutls_certificate_free_credentials(tls_credentials); gnutls_global_deinit(); @@ -1826,19 +2196,13 @@ struct output_definition output_cast = .init = cast_init, .deinit = cast_deinit, .device_start = cast_device_start, - .device_stop = cast_device_stop, .device_probe = cast_device_probe, -// .device_free_extra is unset - nothing to free - .device_volume_set = cast_volume_set, - .playback_start = cast_playback_start, - .playback_stop = cast_playback_stop, -// .write is unset - we don't write, the Chromecast will read our mp3 stream - .flush = cast_flush, - .status_cb = cast_set_status_cb, -/* TODO metadata support - .metadata_prepare = cast_metadata_prepare, - .metadata_send = cast_metadata_send, - .metadata_purge = cast_metadata_purge, - .metadata_prune = cast_metadata_prune, -*/ + .device_stop = cast_device_stop, + .device_flush = cast_device_flush, + .device_cb_set = cast_device_cb_set, + .device_volume_set = cast_device_volume_set, + .write = cast_write, +// .metadata_prepare = cast_metadata_prepare, +// .metadata_send = cast_metadata_send, +// .metadata_purge = cast_metadata_purge, }; diff --git a/src/outputs/dummy.c b/src/outputs/dummy.c index 3083ead4..a1865652 100644 --- a/src/outputs/dummy.c +++ b/src/outputs/dummy.c @@ -33,8 +33,7 @@ #include #include -#include - +#include "misc.h" #include "conffile.h" #include "logger.h" #include "player.h" @@ -44,24 +43,12 @@ struct dummy_session { enum output_device_state state; - struct event *deferredev; - output_status_cb defer_cb; - - /* Do not dereference - only passed to the status cb */ - struct output_device *device; - struct output_session *output_session; - output_status_cb status_cb; + uint64_t device_id; + int callback_id; }; -/* From player.c */ -extern struct event_base *evbase_player; - struct dummy_session *sessions; -/* Forwards */ -static void -defer_cb(int fd, short what, void *arg); - /* ---------------------------- SESSION HANDLING ---------------------------- */ static void @@ -70,9 +57,6 @@ dummy_session_free(struct dummy_session *ds) if (!ds) return; - event_free(ds->deferredev); - - free(ds->output_session); free(ds); } @@ -82,86 +66,50 @@ dummy_session_cleanup(struct dummy_session *ds) // Normally some here code to remove from linked list - here we just say: sessions = NULL; + outputs_device_session_remove(ds->device_id); + dummy_session_free(ds); } static struct dummy_session * -dummy_session_make(struct output_device *device, output_status_cb cb) +dummy_session_make(struct output_device *device, int callback_id) { - struct output_session *os; struct dummy_session *ds; - os = calloc(1, sizeof(struct output_session)); - if (!os) - { - DPRINTF(E_LOG, L_LAUDIO, "Out of memory for dummy session (os)\n"); - return NULL; - } + CHECK_NULL(L_LAUDIO, ds = calloc(1, sizeof(struct dummy_session))); - ds = calloc(1, sizeof(struct dummy_session)); - if (!ds) - { - DPRINTF(E_LOG, L_LAUDIO, "Out of memory for dummy session (as)\n"); - free(os); - return NULL; - } - - ds->deferredev = evtimer_new(evbase_player, defer_cb, ds); - if (!ds->deferredev) - { - DPRINTF(E_LOG, L_LAUDIO, "Out of memory for dummy deferred event\n"); - free(os); - free(ds); - return NULL; - } - - os->session = ds; - os->type = device->type; - - ds->output_session = os; ds->state = OUTPUT_STATE_CONNECTED; - ds->device = device; - ds->status_cb = cb; + ds->device_id = device->id; + ds->callback_id = callback_id; sessions = ds; + outputs_device_session_add(device->id, ds); + return ds; } /* ---------------------------- STATUS HANDLERS ----------------------------- */ -// Maps our internal state to the generic output state and then makes a callback -// to the player to tell that state -static void -defer_cb(int fd, short what, void *arg) -{ - struct dummy_session *ds = arg; - - if (ds->defer_cb) - ds->defer_cb(ds->device, ds->output_session, ds->state); - - if (ds->state == OUTPUT_STATE_STOPPED) - dummy_session_cleanup(ds); -} - static void dummy_status(struct dummy_session *ds) { - ds->defer_cb = ds->status_cb; - event_active(ds->deferredev, 0, 0); - ds->status_cb = NULL; + outputs_cb(ds->callback_id, ds->device_id, ds->state); + + if (ds->state == OUTPUT_STATE_STOPPED) + dummy_session_cleanup(ds); } /* ------------------ INTERFACE FUNCTIONS CALLED BY OUTPUTS.C --------------- */ static int -dummy_device_start(struct output_device *device, output_status_cb cb, uint64_t rtptime) +dummy_device_start(struct output_device *device, int callback_id) { struct dummy_session *ds; - ds = dummy_session_make(device, cb); + ds = dummy_session_make(device, callback_id); if (!ds) return -1; @@ -170,25 +118,12 @@ dummy_device_start(struct output_device *device, output_status_cb cb, uint64_t r return 0; } -static void -dummy_device_stop(struct output_session *session) -{ - struct dummy_session *ds = session->session; - - ds->state = OUTPUT_STATE_STOPPED; - dummy_status(ds); -} - static int -dummy_device_probe(struct output_device *device, output_status_cb cb) +dummy_device_stop(struct output_device *device, int callback_id) { - struct dummy_session *ds; + struct dummy_session *ds = device->session; - ds = dummy_session_make(device, cb); - if (!ds) - return -1; - - ds->status_cb = cb; + ds->callback_id = callback_id; ds->state = OUTPUT_STATE_STOPPED; dummy_status(ds); @@ -197,51 +132,55 @@ dummy_device_probe(struct output_device *device, output_status_cb cb) } static int -dummy_device_volume_set(struct output_device *device, output_status_cb cb) +dummy_device_flush(struct output_device *device, int callback_id) +{ + struct dummy_session *ds = device->session; + + ds->callback_id = callback_id; + ds->state = OUTPUT_STATE_STOPPED; + + dummy_status(ds); + + return 0; +} + +static int +dummy_device_probe(struct output_device *device, int callback_id) { struct dummy_session *ds; - if (!device->session || !device->session->session) + ds = dummy_session_make(device, callback_id); + if (!ds) + return -1; + + ds->callback_id = callback_id; + ds->state = OUTPUT_STATE_STOPPED; + + dummy_status(ds); + + return 0; +} + +static int +dummy_device_volume_set(struct output_device *device, int callback_id) +{ + struct dummy_session *ds = device->session; + + if (!ds) return 0; - ds = device->session->session; - - ds->status_cb = cb; + ds->callback_id = callback_id; dummy_status(ds); return 1; } static void -dummy_playback_start(uint64_t next_pkt, struct timespec *ts) +dummy_device_cb_set(struct output_device *device, int callback_id) { - struct dummy_session *ds = sessions; + struct dummy_session *ds = device->session; - if (!sessions) - return; - - ds->state = OUTPUT_STATE_STREAMING; - dummy_status(ds); -} - -static void -dummy_playback_stop(void) -{ - struct dummy_session *ds = sessions; - - if (!sessions) - return; - - ds->state = OUTPUT_STATE_CONNECTED; - dummy_status(ds); -} - -static void -dummy_set_status_cb(struct output_session *session, output_status_cb cb) -{ - struct dummy_session *ds = session->session; - - ds->status_cb = cb; + ds->callback_id = callback_id; } static int @@ -259,18 +198,12 @@ dummy_init(void) nickname = cfg_getstr(cfg_audio, "nickname"); - device = calloc(1, sizeof(struct output_device)); - if (!device) - { - DPRINTF(E_LOG, L_LAUDIO, "Out of memory for dummy device\n"); - return -1; - } + CHECK_NULL(L_LAUDIO, device = calloc(1, sizeof(struct output_device))); device->id = 0; device->name = strdup(nickname); device->type = OUTPUT_TYPE_DUMMY; device->type_name = outputs_name(device->type); - device->advertised = 1; device->has_video = 0; DPRINTF(E_INFO, L_LAUDIO, "Adding dummy output device '%s'\n", nickname); @@ -296,9 +229,8 @@ struct output_definition output_dummy = .deinit = dummy_deinit, .device_start = dummy_device_start, .device_stop = dummy_device_stop, + .device_flush = dummy_device_flush, .device_probe = dummy_device_probe, .device_volume_set = dummy_device_volume_set, - .playback_start = dummy_playback_start, - .playback_stop = dummy_playback_stop, - .status_cb = dummy_set_status_cb, + .device_cb_set = dummy_device_cb_set, }; diff --git a/src/outputs/fifo.c b/src/outputs/fifo.c index 0aa7c10e..0e246adb 100644 --- a/src/outputs/fifo.c +++ b/src/outputs/fifo.c @@ -31,24 +31,23 @@ #include #include -#include - #include "misc.h" #include "conffile.h" #include "logger.h" #include "player.h" #include "outputs.h" -#define FIFO_BUFFER_SIZE 65536 /* pipe capacity on Linux >= 2.6.11 */ - +#define FIFO_BUFFER_SIZE 65536 // pipe capacity on Linux >= 2.6.11 +#define FIFO_PACKET_SIZE 1408 // 352 samples/packet * 16 bit/sample * 2 channels struct fifo_packet { /* pcm data */ - uint8_t samples[1408]; // STOB(AIRTUNES_V2_PACKET_SAMPLES) + uint8_t *samples; + size_t samples_size; - /* RTP-time of the first sample*/ - uint64_t rtptime; + /* Presentation timestamp of the first sample */ + struct timespec pts; struct fifo_packet *next; struct fifo_packet *prev; @@ -59,8 +58,11 @@ struct fifo_buffer struct fifo_packet *head; struct fifo_packet *tail; }; + static struct fifo_buffer buffer; +static struct media_quality fifo_quality = { 44100, 16, 2 }; + static void free_buffer() @@ -91,24 +93,12 @@ struct fifo_session int created; - struct event *deferredev; - output_status_cb defer_cb; - - /* Do not dereference - only passed to the status cb */ - struct output_device *device; - struct output_session *output_session; - output_status_cb status_cb; + uint64_t device_id; + int callback_id; }; -/* From player.c */ -extern struct event_base *evbase_player; - static struct fifo_session *sessions; -/* Forwards */ -static void -defer_cb(int fd, short what, void *arg); - /* ---------------------------- FIFO HANDLING ---------------------------- */ @@ -240,9 +230,6 @@ fifo_session_free(struct fifo_session *fifo_session) if (!fifo_session) return; - event_free(fifo_session->deferredev); - - free(fifo_session->output_session); free(fifo_session); free_buffer(); } @@ -253,46 +240,21 @@ fifo_session_cleanup(struct fifo_session *fifo_session) // Normally some here code to remove from linked list - here we just say: sessions = NULL; + outputs_device_session_remove(fifo_session->device_id); + fifo_session_free(fifo_session); } static struct fifo_session * -fifo_session_make(struct output_device *device, output_status_cb cb) +fifo_session_make(struct output_device *device, int callback_id) { - struct output_session *output_session; struct fifo_session *fifo_session; - output_session = calloc(1, sizeof(struct output_session)); - if (!output_session) - { - DPRINTF(E_LOG, L_FIFO, "Out of memory (os)\n"); - return NULL; - } + CHECK_NULL(L_FIFO, fifo_session = calloc(1, sizeof(struct fifo_session))); - fifo_session = calloc(1, sizeof(struct fifo_session)); - if (!fifo_session) - { - DPRINTF(E_LOG, L_FIFO, "Out of memory (fs)\n"); - free(output_session); - return NULL; - } - - fifo_session->deferredev = evtimer_new(evbase_player, defer_cb, fifo_session); - if (!fifo_session->deferredev) - { - DPRINTF(E_LOG, L_FIFO, "Out of memory for fifo deferred event\n"); - free(output_session); - free(fifo_session); - return NULL; - } - - output_session->session = fifo_session; - output_session->type = device->type; - - fifo_session->output_session = output_session; fifo_session->state = OUTPUT_STATE_CONNECTED; - fifo_session->device = device; - fifo_session->status_cb = cb; + fifo_session->device_id = device->id; + fifo_session->callback_id = callback_id; fifo_session->created = 0; fifo_session->path = device->extra_device_info; @@ -301,44 +263,36 @@ fifo_session_make(struct output_device *device, output_status_cb cb) sessions = fifo_session; + outputs_device_session_add(device->id, fifo_session); + return fifo_session; } /* ---------------------------- STATUS HANDLERS ----------------------------- */ -// Maps our internal state to the generic output state and then makes a callback -// to the player to tell that state -static void -defer_cb(int fd, short what, void *arg) -{ - struct fifo_session *ds = arg; - - if (ds->defer_cb) - ds->defer_cb(ds->device, ds->output_session, ds->state); - - if (ds->state == OUTPUT_STATE_STOPPED) - fifo_session_cleanup(ds); -} - static void fifo_status(struct fifo_session *fifo_session) { - fifo_session->defer_cb = fifo_session->status_cb; - event_active(fifo_session->deferredev, 0, 0); - fifo_session->status_cb = NULL; -} + outputs_cb(fifo_session->callback_id, fifo_session->device_id, fifo_session->state); + if (fifo_session->state == OUTPUT_STATE_STOPPED) + fifo_session_cleanup(fifo_session); +} /* ------------------ INTERFACE FUNCTIONS CALLED BY OUTPUTS.C --------------- */ static int -fifo_device_start(struct output_device *device, output_status_cb cb, uint64_t rtptime) +fifo_device_start(struct output_device *device, int callback_id) { struct fifo_session *fifo_session; int ret; - fifo_session = fifo_session_make(device, cb); + ret = outputs_quality_subscribe(&fifo_quality); + if (ret < 0) + return -1; + + fifo_session = fifo_session_make(device, callback_id); if (!fifo_session) return -1; @@ -351,25 +305,46 @@ fifo_device_start(struct output_device *device, output_status_cb cb, uint64_t rt return 0; } -static void -fifo_device_stop(struct output_session *output_session) +static int +fifo_device_stop(struct output_device *device, int callback_id) { - struct fifo_session *fifo_session = output_session->session; + struct fifo_session *fifo_session = device->session; + + outputs_quality_unsubscribe(&fifo_quality); + + fifo_session->callback_id = callback_id; fifo_close(fifo_session); free_buffer(); fifo_session->state = OUTPUT_STATE_STOPPED; fifo_status(fifo_session); + + return 0; } static int -fifo_device_probe(struct output_device *device, output_status_cb cb) +fifo_device_flush(struct output_device *device, int callback_id) +{ + struct fifo_session *fifo_session = device->session; + + fifo_empty(fifo_session); + free_buffer(); + + fifo_session->callback_id = callback_id; + fifo_session->state = OUTPUT_STATE_CONNECTED; + fifo_status(fifo_session); + + return 0; +} + +static int +fifo_device_probe(struct output_device *device, int callback_id) { struct fifo_session *fifo_session; int ret; - fifo_session = fifo_session_make(device, cb); + fifo_session = fifo_session_make(device, callback_id); if (!fifo_session) return -1; @@ -382,7 +357,7 @@ fifo_device_probe(struct output_device *device, output_status_cb cb) fifo_close(fifo_session); - fifo_session->status_cb = cb; + fifo_session->callback_id = callback_id; fifo_session->state = OUTPUT_STATE_STOPPED; fifo_status(fifo_session); @@ -391,104 +366,81 @@ fifo_device_probe(struct output_device *device, output_status_cb cb) } static int -fifo_device_volume_set(struct output_device *device, output_status_cb cb) +fifo_device_volume_set(struct output_device *device, int callback_id) { - struct fifo_session *fifo_session; + struct fifo_session *fifo_session = device->session; - if (!device->session || !device->session->session) + if (!fifo_session) return 0; - fifo_session = device->session->session; - - fifo_session->status_cb = cb; + fifo_session->callback_id = callback_id; fifo_status(fifo_session); return 1; } static void -fifo_playback_start(uint64_t next_pkt, struct timespec *ts) +fifo_device_cb_set(struct output_device *device, int callback_id) +{ + struct fifo_session *fifo_session = device->session; + + fifo_session->callback_id = callback_id; +} + +static void +fifo_write(struct output_buffer *obuf) { struct fifo_session *fifo_session = sessions; + struct fifo_packet *packet; + struct timespec now; + ssize_t bytes; + int i; if (!fifo_session) return; + for (i = 0; obuf->data[i].buffer; i++) + { + if (quality_is_equal(&fifo_quality, &obuf->data[i].quality)) + break; + } + + if (!obuf->data[i].buffer) + { + DPRINTF(E_LOG, L_FIFO, "Bug! Did not get audio in quality required\n"); + return; + } + fifo_session->state = OUTPUT_STATE_STREAMING; - fifo_status(fifo_session); -} -static void -fifo_playback_stop(void) -{ - struct fifo_session *fifo_session = sessions; + CHECK_NULL(L_FIFO, packet = calloc(1, sizeof(struct fifo_packet))); + CHECK_NULL(L_FIFO, packet->samples = malloc(obuf->data[i].bufsize)); - if (!fifo_session) - return; + memcpy(packet->samples, obuf->data[i].buffer, obuf->data[i].bufsize); + packet->samples_size = obuf->data[i].bufsize; + packet->pts = obuf->pts; - free_buffer(); - - fifo_session->state = OUTPUT_STATE_CONNECTED; - fifo_status(fifo_session); -} - -static int -fifo_flush(output_status_cb cb, uint64_t rtptime) -{ - struct fifo_session *fifo_session = sessions; - - if (!fifo_session) - return 0; - - fifo_empty(fifo_session); - free_buffer(); - - fifo_session->status_cb = cb; - fifo_session->state = OUTPUT_STATE_CONNECTED; - fifo_status(fifo_session); - return 1; -} - -static void -fifo_write(uint8_t *buf, uint64_t rtptime) -{ - struct fifo_session *fifo_session = sessions; - size_t length = STOB(AIRTUNES_V2_PACKET_SAMPLES); - ssize_t bytes; - struct fifo_packet *packet; - uint64_t cur_pos; - struct timespec now; - int ret; - - if (!fifo_session || !fifo_session->device->selected) - return; - - packet = (struct fifo_packet *) calloc(1, sizeof(struct fifo_packet)); - memcpy(packet->samples, buf, sizeof(packet->samples)); - packet->rtptime = rtptime; if (buffer.head) { buffer.head->next = packet; packet->prev = buffer.head; } + buffer.head = packet; if (!buffer.tail) buffer.tail = packet; - ret = player_get_current_pos(&cur_pos, &now, 0); - if (ret < 0) - { - DPRINTF(E_LOG, L_FIFO, "Could not get playback position\n"); - return; - } + now.tv_sec = obuf->pts.tv_sec - OUTPUTS_BUFFER_DURATION; + now.tv_nsec = obuf->pts.tv_sec; - while (buffer.tail && buffer.tail->rtptime <= cur_pos) + while (buffer.tail && (timespec_cmp(buffer.tail->pts, now) == -1)) { - bytes = write(fifo_session->output_fd, buffer.tail->samples, length); + bytes = write(fifo_session->output_fd, buffer.tail->samples, buffer.tail->samples_size); if (bytes > 0) { packet = buffer.tail; buffer.tail = buffer.tail->next; + free(packet->samples); free(packet); return; } @@ -511,14 +463,6 @@ fifo_write(uint8_t *buf, uint64_t rtptime) } } -static void -fifo_set_status_cb(struct output_session *session, output_status_cb cb) -{ - struct fifo_session *fifo_session = session->session; - - fifo_session->status_cb = cb; -} - static int fifo_init(void) { @@ -539,18 +483,12 @@ fifo_init(void) memset(&buffer, 0, sizeof(struct fifo_buffer)); - device = calloc(1, sizeof(struct output_device)); - if (!device) - { - DPRINTF(E_LOG, L_FIFO, "Out of memory for fifo device\n"); - return -1; - } + CHECK_NULL(L_FIFO, device = calloc(1, sizeof(struct output_device))); device->id = 100; device->name = strdup(nickname); device->type = OUTPUT_TYPE_FIFO; device->type_name = outputs_name(device->type); - device->advertised = 1; device->has_video = 0; device->extra_device_info = path; DPRINTF(E_INFO, L_FIFO, "Adding fifo output device '%s' with path '%s'\n", nickname, path); @@ -576,11 +514,9 @@ struct output_definition output_fifo = .deinit = fifo_deinit, .device_start = fifo_device_start, .device_stop = fifo_device_stop, + .device_flush = fifo_device_flush, .device_probe = fifo_device_probe, .device_volume_set = fifo_device_volume_set, - .playback_start = fifo_playback_start, - .playback_stop = fifo_playback_stop, + .device_cb_set = fifo_device_cb_set, .write = fifo_write, - .flush = fifo_flush, - .status_cb = fifo_set_status_cb, }; diff --git a/src/outputs/pulse.c b/src/outputs/pulse.c index 86e23cea..74d2f9ac 100644 --- a/src/outputs/pulse.c +++ b/src/outputs/pulse.c @@ -58,21 +58,21 @@ struct pulse struct pulse_session { + uint64_t device_id; + int callback_id; + + char *devname; + pa_stream_state_t state; pa_stream *stream; pa_buffer_attr attr; pa_volume_t volume; + struct media_quality quality; + int logcount; - char *devname; - - /* Do not dereference - only passed to the status cb */ - struct output_device *device; - struct output_session *output_session; - output_status_cb status_cb; - struct pulse_session *next; }; @@ -85,6 +85,9 @@ static struct pulse_session *sessions; // Internal list with indeces of the Pulseaudio devices (sinks) we have registered static uint32_t pulse_known_devices[PULSE_MAX_DEVICES]; +static struct media_quality pulse_last_quality; +static struct media_quality pulse_fallback_quality = { 44100, 16, 2 }; + // Converts from 0 - 100 to Pulseaudio's scale static inline pa_volume_t pulse_from_device_volume(int device_volume) @@ -113,10 +116,9 @@ pulse_session_free(struct pulse_session *ps) pa_threaded_mainloop_unlock(pulse.mainloop); } - if (ps->devname) - free(ps->devname); + outputs_quality_unsubscribe(&pulse_fallback_quality); - free(ps->output_session); + free(ps->devname); free(ps); } @@ -139,43 +141,37 @@ pulse_session_cleanup(struct pulse_session *ps) p->next = ps->next; } + outputs_device_session_remove(ps->device_id); + pulse_session_free(ps); } static struct pulse_session * -pulse_session_make(struct output_device *device, output_status_cb cb) +pulse_session_make(struct output_device *device, int callback_id) { - struct output_session *os; struct pulse_session *ps; + int ret; - os = calloc(1, sizeof(struct output_session)); - if (!os) + ret = outputs_quality_subscribe(&pulse_fallback_quality); + if (ret < 0) { - DPRINTF(E_LOG, L_LAUDIO, "Out of memory (os)\n"); + DPRINTF(E_LOG, L_LAUDIO, "Could not subscribe to fallback audio quality\n"); return NULL; } - ps = calloc(1, sizeof(struct pulse_session)); - if (!ps) - { - DPRINTF(E_LOG, L_LAUDIO, "Out of memory (ps)\n"); - free(os); - return NULL; - } + CHECK_NULL(L_LAUDIO, ps = calloc(1, sizeof(struct pulse_session))); - os->session = ps; - os->type = device->type; - - ps->output_session = os; ps->state = PA_STREAM_UNCONNECTED; - ps->device = device; - ps->status_cb = cb; + ps->device_id = device->id; + ps->callback_id = callback_id; ps->volume = pulse_from_device_volume(device->volume); ps->devname = strdup(device->extra_device_info); ps->next = sessions; sessions = ps; + outputs_device_session_add(device->id, ps); + return ps; } @@ -187,7 +183,6 @@ static enum command_state send_status(void *arg, int *ptr) { struct pulse_session *ps = arg; - output_status_cb status_cb; enum output_device_state state; switch (ps->state) @@ -210,10 +205,8 @@ send_status(void *arg, int *ptr) state = OUTPUT_STATE_FAILED; } - status_cb = ps->status_cb; - ps->status_cb = NULL; - if (status_cb) - status_cb(ps->device, ps->output_session, state); + outputs_cb(ps->callback_id, ps->device_id, state); + ps->callback_id = -1; return COMMAND_PENDING; // Don't want the command module to clean up ps } @@ -442,7 +435,6 @@ sinklist_cb(pa_context *ctx, const pa_sink_info *info, int eol, void *userdata) device->name = strdup(name); device->type = OUTPUT_TYPE_PULSE; device->type_name = outputs_name(device->type); - device->advertised = 1; device->extra_device_info = strdup(info->name); player_device_add(device); @@ -572,25 +564,33 @@ pulse_free(void) } static int -stream_open(struct pulse_session *ps, pa_stream_notify_cb_t cb) +stream_open(struct pulse_session *ps, struct media_quality *quality, pa_stream_notify_cb_t cb) { pa_stream_flags_t flags; pa_sample_spec ss; pa_cvolume cvol; - int offset; + int offset_ms; int ret; DPRINTF(E_DBG, L_LAUDIO, "Opening Pulseaudio stream to '%s'\n", ps->devname); - ss.format = PA_SAMPLE_S16LE; - ss.channels = 2; - ss.rate = 44100; + if (quality->bits_per_sample == 16) + ss.format = PA_SAMPLE_S16LE; + else if (quality->bits_per_sample == 24) + ss.format = PA_SAMPLE_S24LE; + else if (quality->bits_per_sample == 32) + ss.format = PA_SAMPLE_S32LE; + else + ss.format = 0; - offset = cfg_getint(cfg_getsec(cfg, "audio"), "offset"); - if (abs(offset) > 44100) + ss.channels = quality->channels; + ss.rate = quality->sample_rate; + + offset_ms = cfg_getint(cfg_getsec(cfg, "audio"), "offset_ms"); + if (abs(offset_ms) > 1000) { - DPRINTF(E_LOG, L_LAUDIO, "The audio offset (%d) set in the configuration is out of bounds\n", offset); - offset = 44100 * (offset/abs(offset)); + DPRINTF(E_LOG, L_LAUDIO, "The audio offset (%d) set in the configuration is out of bounds\n", offset_ms); + offset_ms = 1000 * (offset_ms/abs(offset_ms)); } pa_threaded_mainloop_lock(pulse.mainloop); @@ -602,7 +602,7 @@ stream_open(struct pulse_session *ps, pa_stream_notify_cb_t cb) flags = PA_STREAM_INTERPOLATE_TIMING | PA_STREAM_AUTO_TIMING_UPDATE; - ps->attr.tlength = STOB(2 * ss.rate + AIRTUNES_V2_PACKET_SAMPLES - offset); // 2 second latency + ps->attr.tlength = STOB((OUTPUTS_BUFFER_DURATION * 1000 + offset_ms) * ss.rate / 1000, quality->bits_per_sample, quality->channels); ps->attr.maxlength = 2 * ps->attr.tlength; ps->attr.prebuf = (uint32_t)-1; ps->attr.minreq = (uint32_t)-1; @@ -625,7 +625,8 @@ stream_open(struct pulse_session *ps, pa_stream_notify_cb_t cb) unlock_and_fail: ret = pa_context_errno(pulse.context); - DPRINTF(E_LOG, L_LAUDIO, "Pulseaudio could not start '%s': %s\n", ps->devname, pa_strerror(ret)); + DPRINTF(E_LOG, L_LAUDIO, "Pulseaudio could not start '%s' using quality %d/%d/%d: %s\n", + ps->devname, quality->sample_rate, quality->bits_per_sample, quality->channels, pa_strerror(ret)); pa_threaded_mainloop_unlock(pulse.mainloop); @@ -635,11 +636,15 @@ stream_open(struct pulse_session *ps, pa_stream_notify_cb_t cb) static void stream_close(struct pulse_session *ps, pa_stream_notify_cb_t cb) { + if (!ps->stream) + return; + pa_threaded_mainloop_lock(pulse.mainloop); pa_stream_set_underflow_callback(ps->stream, NULL, NULL); pa_stream_set_overflow_callback(ps->stream, NULL, NULL); pa_stream_set_state_callback(ps->stream, cb, ps); + pa_stream_disconnect(ps->stream); pa_stream_unref(ps->stream); @@ -649,54 +654,169 @@ stream_close(struct pulse_session *ps, pa_stream_notify_cb_t cb) pa_threaded_mainloop_unlock(pulse.mainloop); } +static void +playback_restart(struct pulse_session *ps, struct output_buffer *obuf) +{ + int ret; + + stream_close(ps, NULL); + + // Negotiate quality (sample rate) with device - first we try to use the source quality + ps->quality = obuf->data[0].quality; + ret = stream_open(ps, &ps->quality, start_cb); + if (ret < 0) + { + DPRINTF(E_INFO, L_LAUDIO, "Input quality (%d/%d/%d) not supported, falling back to default\n", + ps->quality.sample_rate, ps->quality.bits_per_sample, ps->quality.channels); + + ps->quality = pulse_fallback_quality; + ret = stream_open(ps, &ps->quality, start_cb); + if (ret < 0) + { + DPRINTF(E_LOG, L_LAUDIO, "Pulseaudio device failed setting fallback quality\n"); + ps->state = PA_STREAM_FAILED; + pulse_session_shutdown(ps); + return; + } + } +} + +static void +playback_write(struct pulse_session *ps, struct output_buffer *obuf) +{ + int i; + int ret; + + // Find the quality we want + for (i = 0; obuf->data[i].buffer; i++) + { + if (quality_is_equal(&ps->quality, &obuf->data[i].quality)) + break; + } + + if (!obuf->data[i].buffer) + { + DPRINTF(E_LOG, L_LAUDIO, "Output not delivering required data quality, aborting\n"); + ps->state = PA_STREAM_FAILED; + pulse_session_shutdown(ps); + return; + } + + pa_threaded_mainloop_lock(pulse.mainloop); + + ret = pa_stream_write(ps->stream, obuf->data[i].buffer, obuf->data[i].bufsize, NULL, 0LL, PA_SEEK_RELATIVE); + if (ret < 0) + { + ret = pa_context_errno(pulse.context); + DPRINTF(E_LOG, L_LAUDIO, "Error writing Pulseaudio stream data to '%s': %s\n", ps->devname, pa_strerror(ret)); + ps->state = PA_STREAM_FAILED; + pulse_session_shutdown(ps); + goto unlock; + } + + unlock: + pa_threaded_mainloop_unlock(pulse.mainloop); +} + +static void +playback_resume(struct pulse_session *ps) +{ + pa_operation* o; + + pa_threaded_mainloop_lock(pulse.mainloop); + + o = pa_stream_cork(ps->stream, 0, NULL, NULL); + if (!o) + { + DPRINTF(E_LOG, L_LAUDIO, "Pulseaudio could not resume '%s': %s\n", ps->devname, pa_strerror(pa_context_errno(pulse.context))); + goto unlock; + } + + pa_operation_unref(o); + + unlock: + pa_threaded_mainloop_unlock(pulse.mainloop); +} + /* ------------------ INTERFACE FUNCTIONS CALLED BY OUTPUTS.C --------------- */ static int -pulse_device_start(struct output_device *device, output_status_cb cb, uint64_t rtptime) +pulse_device_start(struct output_device *device, int callback_id) { struct pulse_session *ps; - int ret; DPRINTF(E_DBG, L_LAUDIO, "Pulseaudio starting '%s'\n", device->name); - ps = pulse_session_make(device, cb); + ps = pulse_session_make(device, callback_id); if (!ps) return -1; - ret = stream_open(ps, start_cb); - if (ret < 0) - { - pulse_session_cleanup(ps); - return -1; - } + pulse_status(ps); return 0; } -static void -pulse_device_stop(struct output_session *session) +static int +pulse_device_stop(struct output_device *device, int callback_id) { - struct pulse_session *ps = session->session; + struct pulse_session *ps = device->session; DPRINTF(E_DBG, L_LAUDIO, "Pulseaudio stopping '%s'\n", ps->devname); + ps->callback_id = callback_id; + stream_close(ps, close_cb); + + return 0; } static int -pulse_device_probe(struct output_device *device, output_status_cb cb) +pulse_device_flush(struct output_device *device, int callback_id) +{ + struct pulse_session *ps = device->session; + pa_operation* o; + + DPRINTF(E_DBG, L_LAUDIO, "Pulseaudio flush\n"); + + pa_threaded_mainloop_lock(pulse.mainloop); + + ps->callback_id = callback_id; + + o = pa_stream_cork(ps->stream, 1, NULL, NULL); + if (!o) + { + DPRINTF(E_LOG, L_LAUDIO, "Pulseaudio could not pause '%s': %s\n", ps->devname, pa_strerror(pa_context_errno(pulse.context))); + return -1; + } + pa_operation_unref(o); + + o = pa_stream_flush(ps->stream, flush_cb, ps); + if (!o) + { + DPRINTF(E_LOG, L_LAUDIO, "Pulseaudio could not flush '%s': %s\n", ps->devname, pa_strerror(pa_context_errno(pulse.context))); + return -1; + } + pa_operation_unref(o); + + pa_threaded_mainloop_unlock(pulse.mainloop); + + return 0; +} + +static int +pulse_device_probe(struct output_device *device, int callback_id) { struct pulse_session *ps; int ret; DPRINTF(E_DBG, L_LAUDIO, "Pulseaudio probing '%s'\n", device->name); - ps = pulse_session_make(device, cb); + ps = pulse_session_make(device, callback_id); if (!ps) return -1; - ret = stream_open(ps, probe_cb); + ret = stream_open(ps, &pulse_fallback_quality, probe_cb); if (ret < 0) { pulse_session_cleanup(ps); @@ -712,18 +832,25 @@ pulse_device_free_extra(struct output_device *device) free(device->extra_device_info); } -static int -pulse_device_volume_set(struct output_device *device, output_status_cb cb) +static void +pulse_device_cb_set(struct output_device *device, int callback_id) { - struct pulse_session *ps; + struct pulse_session *ps = device->session; + + ps->callback_id = callback_id; +} + +static int +pulse_device_volume_set(struct output_device *device, int callback_id) +{ + struct pulse_session *ps = device->session; uint32_t idx; pa_operation* o; pa_cvolume cvol; - if (!sessions || !device->session || !device->session->session) + if (!ps) return 0; - ps = device->session->session; idx = pa_stream_get_index(ps->stream); ps->volume = pulse_from_device_volume(device->volume); @@ -733,7 +860,7 @@ pulse_device_volume_set(struct output_device *device, output_status_cb cb) pa_threaded_mainloop_lock(pulse.mainloop); - ps->status_cb = cb; + ps->callback_id = callback_id; o = pa_context_set_sink_input_volume(pulse.context, idx, &cvol, volume_cb, ps); if (!o) @@ -750,141 +877,33 @@ pulse_device_volume_set(struct output_device *device, output_status_cb cb) } static void -pulse_write(uint8_t *buf, uint64_t rtptime) +pulse_write(struct output_buffer *obuf) { struct pulse_session *ps; struct pulse_session *next; - size_t length; - int ret; if (!sessions) return; - length = STOB(AIRTUNES_V2_PACKET_SAMPLES); - - pa_threaded_mainloop_lock(pulse.mainloop); - for (ps = sessions; ps; ps = next) { next = ps->next; - if (ps->state != PA_STREAM_READY) + // We have not set up a stream OR the quality changed, so we need to set it up again + if (ps->state == PA_STREAM_UNCONNECTED || !quality_is_equal(&obuf->data[0].quality, &pulse_last_quality)) + { + playback_restart(ps, obuf); + pulse_last_quality = obuf->data[0].quality; + continue; // Async, so the device won't be ready for writing just now + } + else if (ps->state != PA_STREAM_READY) continue; - ret = pa_stream_write(ps->stream, buf, length, NULL, 0LL, PA_SEEK_RELATIVE); - if (ret < 0) - { - ret = pa_context_errno(pulse.context); - DPRINTF(E_LOG, L_LAUDIO, "Error writing Pulseaudio stream data to '%s': %s\n", ps->devname, pa_strerror(ret)); + if (ps->stream && pa_stream_is_corked(ps->stream)) + playback_resume(ps); - ps->state = PA_STREAM_FAILED; - pulse_session_shutdown(ps); - - continue; - } + playback_write(ps, obuf); } - - pa_threaded_mainloop_unlock(pulse.mainloop); -} - -static void -pulse_playback_start(uint64_t next_pkt, struct timespec *ts) -{ - struct pulse_session *ps; - pa_operation* o; - - pa_threaded_mainloop_lock(pulse.mainloop); - - for (ps = sessions; ps; ps = ps->next) - { - o = pa_stream_cork(ps->stream, 0, NULL, NULL); - if (!o) - { - DPRINTF(E_LOG, L_LAUDIO, "Pulseaudio could not resume '%s': %s\n", ps->devname, pa_strerror(pa_context_errno(pulse.context))); - continue; - } - pa_operation_unref(o); - } - - pa_threaded_mainloop_unlock(pulse.mainloop); -} - -static void -pulse_playback_stop(void) -{ - struct pulse_session *ps; - pa_operation* o; - - pa_threaded_mainloop_lock(pulse.mainloop); - - for (ps = sessions; ps; ps = ps->next) - { - o = pa_stream_cork(ps->stream, 1, NULL, NULL); - if (!o) - { - DPRINTF(E_LOG, L_LAUDIO, "Pulseaudio could not pause '%s': %s\n", ps->devname, pa_strerror(pa_context_errno(pulse.context))); - continue; - } - pa_operation_unref(o); - - o = pa_stream_flush(ps->stream, NULL, NULL); - if (!o) - { - DPRINTF(E_LOG, L_LAUDIO, "Pulseaudio could not flush '%s': %s\n", ps->devname, pa_strerror(pa_context_errno(pulse.context))); - continue; - } - pa_operation_unref(o); - } - - pa_threaded_mainloop_unlock(pulse.mainloop); -} - -static int -pulse_flush(output_status_cb cb, uint64_t rtptime) -{ - struct pulse_session *ps; - pa_operation* o; - int i; - - DPRINTF(E_DBG, L_LAUDIO, "Pulseaudio flush\n"); - - pa_threaded_mainloop_lock(pulse.mainloop); - - i = 0; - for (ps = sessions; ps; ps = ps->next) - { - i++; - - ps->status_cb = cb; - - o = pa_stream_cork(ps->stream, 1, NULL, NULL); - if (!o) - { - DPRINTF(E_LOG, L_LAUDIO, "Pulseaudio could not pause '%s': %s\n", ps->devname, pa_strerror(pa_context_errno(pulse.context))); - continue; - } - pa_operation_unref(o); - - o = pa_stream_flush(ps->stream, flush_cb, ps); - if (!o) - { - DPRINTF(E_LOG, L_LAUDIO, "Pulseaudio could not flush '%s': %s\n", ps->devname, pa_strerror(pa_context_errno(pulse.context))); - continue; - } - pa_operation_unref(o); - } - - pa_threaded_mainloop_unlock(pulse.mainloop); - - return i; -} - -static void -pulse_set_status_cb(struct output_session *session, output_status_cb cb) -{ - struct pulse_session *ps = session->session; - - ps->status_cb = cb; } static int @@ -977,13 +996,11 @@ struct output_definition output_pulse = .deinit = pulse_deinit, .device_start = pulse_device_start, .device_stop = pulse_device_stop, + .device_flush = pulse_device_flush, .device_probe = pulse_device_probe, .device_free_extra = pulse_device_free_extra, + .device_cb_set = pulse_device_cb_set, .device_volume_set = pulse_device_volume_set, - .playback_start = pulse_playback_start, - .playback_stop = pulse_playback_stop, .write = pulse_write, - .flush = pulse_flush, - .status_cb = pulse_set_status_cb, }; diff --git a/src/outputs/raop.c b/src/outputs/raop.c index 61b5f0eb..82eeba31 100644 --- a/src/outputs/raop.c +++ b/src/outputs/raop.c @@ -1,5 +1,5 @@ /* - * Copyright (C) 2012-2017 Espen Jürgensen + * Copyright (C) 2012-2019 Espen Jürgensen * Copyright (C) 2010-2011 Julien BLACHE * * RAOP AirTunes v2 @@ -73,27 +73,31 @@ #include "db.h" #include "artwork.h" #include "dmap_common.h" +#include "rtp_common.h" #include "outputs.h" #ifdef RAOP_VERIFICATION #include "raop_verification.h" #endif -#ifndef MIN -# define MIN(a, b) ((a < b) ? a : b) -#endif +#define ALAC_HEADER_LEN 3 -#define AIRTUNES_V2_HDR_LEN 12 -#define ALAC_HDR_LEN 3 -#define AIRTUNES_V2_PKT_LEN (AIRTUNES_V2_HDR_LEN + ALAC_HDR_LEN + STOB(AIRTUNES_V2_PACKET_SAMPLES)) -#define AIRTUNES_V2_PKT_TAIL_LEN (AIRTUNES_V2_PKT_LEN - AIRTUNES_V2_HDR_LEN - ((AIRTUNES_V2_PKT_LEN / 16) * 16)) -#define AIRTUNES_V2_PKT_TAIL_OFF (AIRTUNES_V2_PKT_LEN - AIRTUNES_V2_PKT_TAIL_LEN) -#define RETRANSMIT_BUFFER_SIZE 1000 +#define RAOP_QUALITY_SAMPLE_RATE_DEFAULT 44100 +#define RAOP_QUALITY_BITS_PER_SAMPLE_DEFAULT 16 +#define RAOP_QUALITY_CHANNELS_DEFAULT 2 + +// AirTunes v2 number of samples per packet +// Probably using this value because 44100/352 and 48000/352 has good 32 byte +// alignment, which improves performance of some encoders +#define RAOP_SAMPLES_PER_PACKET 352 + +// How many RTP packets keep in a buffer for retransmission +#define RAOP_PACKET_BUFFER_SIZE 1000 #define RAOP_MD_DELAY_STARTUP 15360 #define RAOP_MD_DELAY_SWITCH (RAOP_MD_DELAY_STARTUP * 2) -/* This is an arbitrary value which just needs to be kept in sync with the config */ +// This is an arbitrary value which just needs to be kept in sync with the config #define RAOP_CONFIG_MAX_VOLUME 11 union sockaddr_all @@ -104,17 +108,6 @@ union sockaddr_all struct sockaddr_storage ss; }; -struct raop_v2_packet -{ - uint8_t clear[AIRTUNES_V2_PKT_LEN]; - uint8_t encrypted[AIRTUNES_V2_PKT_LEN]; - - uint16_t seqnum; - - struct raop_v2_packet *prev; - struct raop_v2_packet *next; -}; - enum raop_devtype { RAOP_DEV_APEX1_80211G, RAOP_DEV_APEX2_80211N, @@ -164,8 +157,33 @@ struct raop_extra bool supports_auth_setup; }; +struct raop_master_session +{ + struct evbuffer *evbuf; + int evbuf_samples; + + struct rtp_session *rtp_session; + + uint8_t *rawbuf; + size_t rawbuf_size; + int samples_per_packet; + bool encrypt; + + // Number of samples that we tell the output to buffer (this will mean that + // the position that we send in the sync packages are offset by this amount + // compared to the rtptimes of the corresponding RTP packages we are sending) + int output_buffer_samples; + + struct raop_master_session *next; +}; + struct raop_session { + uint64_t device_id; + int callback_id; + + struct raop_master_session *master_session; + struct evrtsp_connection *ctrl; enum raop_state state; @@ -194,12 +212,7 @@ struct raop_session int family; int volume; - uint64_t start_rtptime; - - /* Do not dereference - only passed to the status cb */ - struct output_device *device; - struct output_session *output_session; - output_status_cb status_cb; + uint32_t start_rtptime; /* AirTunes v2 */ unsigned short server_port; @@ -225,15 +238,8 @@ struct raop_session struct raop_metadata { struct evbuffer *metadata; - struct evbuffer *artwork; int artwork_fmt; - - /* Progress data */ - uint64_t start; - uint64_t end; - - struct raop_metadata *next; }; struct raop_service @@ -252,6 +258,7 @@ typedef void (*evrtsp_req_cb)(struct evrtsp_request *req, void *arg); #define FRAC 4294967296. /* 2^32 as a double */ #define NTP_EPOCH_DELTA 0x83aa7e80 /* 2208988800 - that's 1970 - 1900 in seconds */ +// TODO move to rtp_common struct ntp_stamp { uint32_t sec; @@ -295,6 +302,14 @@ static const char *raop_devtype[] = "Other", }; +/* Struct with default quality levels */ +static struct media_quality raop_quality_default = +{ + RAOP_QUALITY_SAMPLE_RATE_DEFAULT, + RAOP_QUALITY_BITS_PER_SAMPLE_DEFAULT, + RAOP_QUALITY_CHANNELS_DEFAULT +}; + /* From player.c */ extern struct event_base *evbase_player; @@ -314,31 +329,24 @@ static struct raop_service timing_6svc; /* AirTunes v2 playback synchronization / control */ static struct raop_service control_4svc; static struct raop_service control_6svc; -static int sync_counter; - -/* AirTunes v2 audio stream */ -static uint32_t ssrc_id; -static uint16_t stream_seq; - -/* Retransmit packet buffer */ -static int pktbuf_size; -static struct raop_v2_packet *pktbuf_head; -static struct raop_v2_packet *pktbuf_tail; /* Metadata */ -static struct raop_metadata *metadata_head; -static struct raop_metadata *metadata_tail; - -/* FLUSH timer */ -static struct event *flush_timer; +static struct output_metadata *raop_cur_metadata; /* Keep-alive timer - hack for ATV's with tvOS 10 */ static struct event *keep_alive_timer; static struct timeval keep_alive_tv = { 30, 0 }; /* Sessions */ -static struct raop_session *sessions; +static struct raop_master_session *raop_master_sessions; +static struct raop_session *raop_sessions; +// Forwards +static int +raop_device_start(struct output_device *rd, int callback_id); + + +/* ------------------------------- MISC HELPERS ----------------------------- */ /* ALAC bits writer - big endian * p outgoing buffer pointer @@ -391,30 +399,30 @@ alac_write_bits(uint8_t **p, uint8_t val, int blen, int *bpos) /* Raw data must be little endian */ static void -alac_encode(uint8_t *raw, uint8_t *buf, int buflen) +alac_encode(uint8_t *dst, uint8_t *raw, int len) { uint8_t *maxraw; int bpos; bpos = 0; - maxraw = raw + buflen; + maxraw = raw + len; - alac_write_bits(&buf, 1, 3, &bpos); /* channel=1, stereo */ - alac_write_bits(&buf, 0, 4, &bpos); /* unknown */ - alac_write_bits(&buf, 0, 8, &bpos); /* unknown */ - alac_write_bits(&buf, 0, 4, &bpos); /* unknown */ - alac_write_bits(&buf, 0, 1, &bpos); /* hassize */ + alac_write_bits(&dst, 1, 3, &bpos); /* channel=1, stereo */ + alac_write_bits(&dst, 0, 4, &bpos); /* unknown */ + alac_write_bits(&dst, 0, 8, &bpos); /* unknown */ + alac_write_bits(&dst, 0, 4, &bpos); /* unknown */ + alac_write_bits(&dst, 0, 1, &bpos); /* hassize */ - alac_write_bits(&buf, 0, 2, &bpos); /* unused */ - alac_write_bits(&buf, 1, 1, &bpos); /* is-not-compressed */ + alac_write_bits(&dst, 0, 2, &bpos); /* unused */ + alac_write_bits(&dst, 1, 1, &bpos); /* is-not-compressed */ for (; raw < maxraw; raw += 4) { /* Byteswap to big endian */ - alac_write_bits(&buf, *(raw + 1), 8, &bpos); - alac_write_bits(&buf, *raw, 8, &bpos); - alac_write_bits(&buf, *(raw + 3), 8, &bpos); - alac_write_bits(&buf, *(raw + 2), 8, &bpos); + alac_write_bits(&dst, *(raw + 1), 8, &bpos); + alac_write_bits(&dst, *raw, 8, &bpos); + alac_write_bits(&dst, *(raw + 3), 8, &bpos); + alac_write_bits(&dst, *(raw + 2), 8, &bpos); } } @@ -457,10 +465,10 @@ raop_v2_timing_get_clock_ntp(struct ntp_stamp *ns) } -/* RAOP crypto stuff - from VLC */ -/* MGF1 is specified in RFC2437, section 10.2.1. Variables are named after the - * specification. - */ +/* ----------------------- RAOP crypto stuff - from VLC --------------------- */ + +// MGF1 is specified in RFC2437, section 10.2.1. Variables are named after the +// specification. static int raop_crypt_mgf1(uint8_t *mask, size_t l, const uint8_t *z, const size_t zlen, const int hash) { @@ -799,146 +807,8 @@ raop_crypt_encrypt_aes_key_base64(void) } -/* RAOP metadata */ -static void -raop_metadata_free(struct raop_metadata *rmd) -{ - evbuffer_free(rmd->metadata); - if (rmd->artwork) - evbuffer_free(rmd->artwork); - free(rmd); -} +/* ------------------ Helpers for sending RAOP/RTSP requests ---------------- */ -static void -raop_metadata_purge(void) -{ - struct raop_metadata *rmd; - - for (rmd = metadata_head; rmd; rmd = metadata_head) - { - metadata_head = rmd->next; - - raop_metadata_free(rmd); - } - - metadata_tail = NULL; -} - -static void -raop_metadata_prune(uint64_t rtptime) -{ - struct raop_metadata *rmd; - - for (rmd = metadata_head; rmd; rmd = metadata_head) - { - if (rmd->end >= rtptime) - break; - - if (metadata_tail == metadata_head) - metadata_tail = rmd->next; - - metadata_head = rmd->next; - - raop_metadata_free(rmd); - } -} - -/* Thread: worker */ -static void * -raop_metadata_prepare(int id) -{ - struct db_queue_item *queue_item; - struct raop_metadata *rmd; - struct evbuffer *tmp; - int ret; - - rmd = (struct raop_metadata *)malloc(sizeof(struct raop_metadata)); - if (!rmd) - { - DPRINTF(E_LOG, L_RAOP, "Out of memory for RAOP metadata\n"); - - return NULL; - } - - memset(rmd, 0, sizeof(struct raop_metadata)); - - queue_item = db_queue_fetch_byitemid(id); - if (!queue_item) - { - DPRINTF(E_LOG, L_RAOP, "Out of memory for queue item\n"); - - goto out_rmd; - } - - /* Get artwork */ - rmd->artwork = evbuffer_new(); - if (!rmd->artwork) - { - DPRINTF(E_LOG, L_RAOP, "Out of memory for artwork evbuffer; no artwork will be sent\n"); - - goto skip_artwork; - } - - ret = artwork_get_item(rmd->artwork, queue_item->file_id, ART_DEFAULT_WIDTH, ART_DEFAULT_HEIGHT); - if (ret < 0) - { - DPRINTF(E_INFO, L_RAOP, "Failed to retrieve artwork for file id %d; no artwork will be sent\n", id); - - evbuffer_free(rmd->artwork); - rmd->artwork = NULL; - } - - rmd->artwork_fmt = ret; - - skip_artwork: - - /* Turn it into DAAP metadata */ - tmp = evbuffer_new(); - if (!tmp) - { - DPRINTF(E_LOG, L_RAOP, "Out of memory for temporary metadata evbuffer; metadata will not be sent\n"); - - goto out_qi; - } - - rmd->metadata = evbuffer_new(); - if (!rmd->metadata) - { - DPRINTF(E_LOG, L_RAOP, "Out of memory for metadata evbuffer; metadata will not be sent\n"); - - evbuffer_free(tmp); - goto out_qi; - } - - ret = dmap_encode_queue_metadata(rmd->metadata, tmp, queue_item); - evbuffer_free(tmp); - if (ret < 0) - { - DPRINTF(E_LOG, L_RAOP, "Could not encode file metadata; metadata will not be sent\n"); - - goto out_metadata; - } - - /* Progress - raop_metadata_send() will add rtptime to these */ - rmd->start = 0; - rmd->end = ((uint64_t)queue_item->song_length * 44100UL) / 1000UL; - - free_queue_item(queue_item, 0); - - return rmd; - - out_metadata: - evbuffer_free(rmd->metadata); - out_qi: - free_queue_item(queue_item, 0); - out_rmd: - free(rmd); - - return NULL; -} - - -/* Helpers */ static int raop_add_auth(struct raop_session *rs, struct evrtsp_request *req, const char *method, const char *uri) { @@ -1207,7 +1077,7 @@ raop_add_headers(struct raop_session *rs, struct evrtsp_request *req, enum evrts // We set Active-Remote as 32 bit unsigned decimal, as at least my device // can't handle any larger. Must be aligned with volume_byactiveremote(). - snprintf(buf, sizeof(buf), "%" PRIu32, (uint32_t)rs->device->id); + snprintf(buf, sizeof(buf), "%" PRIu32, (uint32_t)rs->device_id); evrtsp_add_header(req->output_headers, "Active-Remote", buf); if (rs->session) @@ -1268,11 +1138,11 @@ raop_make_sdp(struct raop_session *rs, struct evrtsp_request *req, char *address /* Add SDP payload - but don't add RSA/AES key/iv if no encryption - important for ATV3 update 6.0 */ if (rs->encrypt) ret = evbuffer_add_printf(req->output_buffer, SDP_PLD_FMT, - session_id, af, address, rs_af, rs->address, AIRTUNES_V2_PACKET_SAMPLES, + session_id, af, address, rs_af, rs->address, RAOP_SAMPLES_PER_PACKET, raop_aes_key_b64, raop_aes_iv_b64); else ret = evbuffer_add_printf(req->output_buffer, SDP_PLD_FMT_NO_ENC, - session_id, af, address, rs_af, rs->address, AIRTUNES_V2_PACKET_SAMPLES); + session_id, af, address, rs_af, rs->address, RAOP_SAMPLES_PER_PACKET); if (p) *p = '%'; @@ -1292,7 +1162,8 @@ raop_make_sdp(struct raop_session *rs, struct evrtsp_request *req, char *address } -/* RAOP/RTSP requests */ +/* ----------------- Handlers for sending RAOP/RTSP requests ---------------- */ + /* * Request queueing HOWTO * @@ -1306,7 +1177,7 @@ raop_make_sdp(struct raop_session *rs, struct evrtsp_request *req, char *address * - if rs->reqs_in_flight == 0, setup evrtsp connection closecb * * When a request fails, the whole RAOP session is declared failed and - * torn down by calling raop_session_failure(), even if there are requests + * torn down by calling session_failure(), even if there are requests * queued on the evrtsp connection. There is no reason to think pending * requests would work out better than the one that just failed and recovery * would be tricky to get right. @@ -1360,10 +1231,11 @@ raop_send_req_teardown(struct raop_session *rs, evrtsp_req_cb cb, const char *lo } static int -raop_send_req_flush(struct raop_session *rs, uint64_t rtptime, evrtsp_req_cb cb, const char *log_caller) +raop_send_req_flush(struct raop_session *rs, evrtsp_req_cb cb, const char *log_caller) { - char buf[64]; + struct raop_master_session *rms = rs->master_session; struct evrtsp_request *req; + char buf[64]; int ret; DPRINTF(E_DBG, L_RAOP, "%s: Sending FLUSH to '%s'\n", log_caller, rs->devname); @@ -1383,8 +1255,8 @@ raop_send_req_flush(struct raop_session *rs, uint64_t rtptime, evrtsp_req_cb cb, return -1; } - /* Restart sequence: last sequence + 1 */ - ret = snprintf(buf, sizeof(buf), "seq=%" PRIu16 ";rtptime=%u", stream_seq + 1, RAOP_RTPTIME(rtptime)); + /* Restart sequence */ + ret = snprintf(buf, sizeof(buf), "seq=%" PRIu16 ";rtptime=%u", rms->rtp_session->seqnum, rms->rtp_session->pos); if ((ret < 0) || (ret >= sizeof(buf))) { DPRINTF(E_LOG, L_RAOP, "RTP-Info too big for buffer in FLUSH request\n"); @@ -1464,8 +1336,9 @@ raop_send_req_set_parameter(struct raop_session *rs, struct evbuffer *evbuf, cha static int raop_send_req_record(struct raop_session *rs, evrtsp_req_cb cb, const char *log_caller) { - char buf[64]; + struct raop_master_session *rms = rs->master_session; struct evrtsp_request *req; + char buf[64]; int ret; DPRINTF(E_DBG, L_RAOP, "%s: Sending RECORD to '%s'\n", log_caller, rs->devname); @@ -1488,7 +1361,7 @@ raop_send_req_record(struct raop_session *rs, evrtsp_req_cb cb, const char *log_ evrtsp_add_header(req->output_headers, "Range", "npt=0-"); /* Start sequence: next sequence */ - ret = snprintf(buf, sizeof(buf), "seq=%" PRIu16 ";rtptime=%u", stream_seq + 1, RAOP_RTPTIME(rs->start_rtptime)); + ret = snprintf(buf, sizeof(buf), "seq=%" PRIu16 ";rtptime=%u", rms->rtp_session->seqnum, rms->rtp_session->pos); if ((ret < 0) || (ret >= sizeof(buf))) { DPRINTF(E_LOG, L_RAOP, "RTP-Info too big for buffer in RECORD request\n"); @@ -1498,6 +1371,8 @@ raop_send_req_record(struct raop_session *rs, evrtsp_req_cb cb, const char *log_ } evrtsp_add_header(req->output_headers, "RTP-Info", buf); + DPRINTF(E_DBG, L_RAOP, "RTP-Info is %s\n", buf); + ret = evrtsp_make_request(rs->ctrl, req, EVRTSP_REQ_RECORD, rs->session_url); if (ret < 0) { @@ -1830,13 +1705,14 @@ raop_send_req_pin_start(struct raop_session *rs, evrtsp_req_cb cb, const char *l } #endif -/* Maps our internal state to the generic output state and then makes a callback - * to the player to tell that state - */ + +/* ------------------------------ Session handling -------------------------- */ + +// Maps our internal state to the generic output state and then makes a callback +// to the player to tell that state static void raop_status(struct raop_session *rs) { - output_status_cb status_cb = rs->status_cb; enum output_device_state state; switch (rs->state) @@ -1860,24 +1736,113 @@ raop_status(struct raop_session *rs) state = OUTPUT_STATE_STREAMING; break; default: - DPRINTF(E_LOG, L_RAOP, "Bug! Unhandled state in cast_status()\n"); + DPRINTF(E_LOG, L_RAOP, "Bug! Unhandled state in raop_status()\n"); state = OUTPUT_STATE_FAILED; } - rs->status_cb = NULL; - if (status_cb) - status_cb(rs->device, rs->output_session, state); + outputs_cb(rs->callback_id, rs->device_id, state); + rs->callback_id = -1; + // Ugly... fixme... if (rs->state == RAOP_STATE_UNVERIFIED) - player_speaker_status_trigger(); + outputs_listener_notify(); +} + +static struct raop_master_session * +master_session_make(struct media_quality *quality, bool encrypt) +{ + struct raop_master_session *rms; + int ret; + + // First check if we already have a suitable session + for (rms = raop_master_sessions; rms; rms = rms->next) + { + if (encrypt == rms->encrypt && quality_is_equal(quality, &rms->rtp_session->quality)) + return rms; + } + + // Let's create a master session + ret = outputs_quality_subscribe(quality); + if (ret < 0) + { + DPRINTF(E_LOG, L_RAOP, "Could not subscribe to required audio quality (%d/%d/%d)\n", quality->sample_rate, quality->bits_per_sample, quality->channels); + return NULL; + } + + CHECK_NULL(L_RAOP, rms = calloc(1, sizeof(struct raop_master_session))); + + rms->rtp_session = rtp_session_new(quality, RAOP_PACKET_BUFFER_SIZE, 0); + if (!rms->rtp_session) + { + outputs_quality_unsubscribe(quality); + free(rms); + return NULL; + } + + rms->encrypt = encrypt; + rms->samples_per_packet = RAOP_SAMPLES_PER_PACKET; + rms->rawbuf_size = STOB(rms->samples_per_packet, quality->bits_per_sample, quality->channels); + rms->output_buffer_samples = OUTPUTS_BUFFER_DURATION * quality->sample_rate; + + CHECK_NULL(L_RAOP, rms->rawbuf = malloc(rms->rawbuf_size)); + CHECK_NULL(L_RAOP, rms->evbuf = evbuffer_new()); + + rms->next = raop_master_sessions; + raop_master_sessions = rms; + + return rms; } static void -raop_session_free(struct raop_session *rs) +master_session_free(struct raop_master_session *rms) +{ + if (!rms) + return; + + outputs_quality_unsubscribe(&rms->rtp_session->quality); + rtp_session_free(rms->rtp_session); + evbuffer_free(rms->evbuf); + free(rms->rawbuf); + free(rms); +} + +static void +master_session_cleanup(struct raop_master_session *rms) +{ + struct raop_master_session *s; + struct raop_session *rs; + + // First check if any other session is using the master session + for (rs = raop_sessions; rs; rs=rs->next) + { + if (rs->master_session == rms) + return; + } + + if (rms == raop_master_sessions) + raop_master_sessions = raop_master_sessions->next; + else + { + for (s = raop_master_sessions; s && (s->next != rms); s = s->next) + ; /* EMPTY */ + + if (!s) + DPRINTF(E_WARN, L_RAOP, "WARNING: struct raop_master_session not found in list; BUG!\n"); + else + s->next = rms->next; + } + + master_session_free(rms); +} + +static void +session_free(struct raop_session *rs) { if (!rs) return; + master_session_cleanup(rs->master_session); + evrtsp_connection_set_closecb(rs->ctrl, NULL, NULL); evrtsp_connection_free(rs->ctrl); @@ -1899,23 +1864,19 @@ raop_session_free(struct raop_session *rs) if (rs->devname) free(rs->devname); - free(rs->output_session); - free(rs); } static void -raop_session_cleanup(struct raop_session *rs) +session_cleanup(struct raop_session *rs) { struct raop_session *s; - struct raop_v2_packet *pkt; - struct raop_v2_packet *next_pkt; - if (rs == sessions) - sessions = sessions->next; + if (rs == raop_sessions) + raop_sessions = raop_sessions->next; else { - for (s = sessions; s && (s->next != rs); s = s->next) + for (s = raop_sessions; s && (s->next != rs); s = s->next) ; /* EMPTY */ if (!s) @@ -1924,27 +1885,13 @@ raop_session_cleanup(struct raop_session *rs) s->next = rs->next; } - raop_session_free(rs); + outputs_device_session_remove(rs->device_id); - /* No more active sessions, free retransmit buffer */ - if (!sessions) - { - pkt = pktbuf_head; - while (pkt) - { - next_pkt = pkt->next; - free(pkt); - pkt = next_pkt; - } - - pktbuf_head = NULL; - pktbuf_tail = NULL; - pktbuf_size = 0; - } + session_free(rs); } static void -raop_session_failure(struct raop_session *rs) +session_failure(struct raop_session *rs) { /* Session failed, let our user know */ if (rs->state != RAOP_STATE_PASSWORD) @@ -1952,37 +1899,84 @@ raop_session_failure(struct raop_session *rs) raop_status(rs); - raop_session_cleanup(rs); + session_cleanup(rs); } static void -raop_deferredev_cb(int fd, short what, void *arg) +deferred_session_failure(struct raop_session *rs) { - struct raop_session *rs = arg; - - DPRINTF(E_DBG, L_RAOP, "Cleaning up failed session (deferred) on device '%s'\n", rs->devname); - - raop_session_failure(rs); -} - -static void -raop_rtsp_close_cb(struct evrtsp_connection *evcon, void *arg) -{ - struct raop_session *rs = arg; struct timeval tv; - DPRINTF(E_LOG, L_RAOP, "Device '%s' closed RTSP connection\n", rs->devname); - rs->state = RAOP_STATE_FAILED; evutil_timerclear(&tv); evtimer_add(rs->deferredev, &tv); } -static struct raop_session * -raop_session_make(struct output_device *rd, int family, output_status_cb cb, bool only_probe) +static void +raop_rtsp_close_cb(struct evrtsp_connection *evcon, void *arg) +{ + struct raop_session *rs = arg; + + DPRINTF(E_LOG, L_RAOP, "Device '%s' closed RTSP connection\n", rs->devname); + + deferred_session_failure(rs); +} + +static void +session_teardown_cb(struct evrtsp_request *req, void *arg) +{ + struct raop_session *rs = arg; + + rs->reqs_in_flight--; + + if (!req) + DPRINTF(E_LOG, L_RAOP, "TEARDOWN request failed in session shutdown\n"); + else if (req->response_code != RTSP_OK) + DPRINTF(E_LOG, L_RAOP, "TEARDOWN request failed in session shutdown: %d %s\n", req->response_code, req->response_code_line); + + rs->state = RAOP_STATE_STOPPED; + + raop_status(rs); + + session_cleanup(rs); +} + +static int +session_teardown(struct raop_session *rs, const char *log_caller) +{ + int ret; + + ret = raop_send_req_teardown(rs, session_teardown_cb, log_caller); + if (ret < 0) + { + DPRINTF(E_LOG, L_RAOP, "%s: TEARDOWN request failed!\n", log_caller); + deferred_session_failure(rs); + } + + return ret; +} + +static void +deferredev_cb(int fd, short what, void *arg) +{ + struct raop_session *rs = arg; + + if (rs->state == RAOP_STATE_FAILED) + { + DPRINTF(E_DBG, L_RAOP, "Cleaning up failed session (deferred) on device '%s'\n", rs->devname); + session_failure(rs); + } + else + { + DPRINTF(E_DBG, L_RAOP, "Flush timer expired; tearing down RAOP session on '%s'\n", rs->devname); + session_teardown(rs, "deferredev_cb"); + } +} + +static struct raop_session * +session_make(struct output_device *rd, int family, int callback_id, bool only_probe) { - struct output_session *os; struct raop_session *rs; struct raop_extra *re; char *address; @@ -2015,32 +2009,17 @@ raop_session_make(struct output_device *rd, int family, output_status_cb cb, boo return NULL; } - os = calloc(1, sizeof(struct output_session)); - if (!os) - { - DPRINTF(E_LOG, L_RAOP, "Out of memory (os)\n"); - return NULL; - } + CHECK_NULL(L_PLAYER, rs = calloc(1, sizeof(struct raop_session))); + CHECK_NULL(L_RAOP, rs->deferredev = evtimer_new(evbase_player, deferredev_cb, rs)); - rs = calloc(1, sizeof(struct raop_session)); - if (!rs) - { - DPRINTF(E_LOG, L_RAOP, "Out of memory (rs)\n"); - free(os); - return NULL; - } - - os->session = rs; - os->type = rd->type; - - rs->output_session = os; rs->state = RAOP_STATE_STOPPED; rs->only_probe = only_probe; rs->reqs_in_flight = 0; rs->cseq = 1; - rs->device = rd; - rs->status_cb = cb; + rs->device_id = rd->id; + rs->callback_id = callback_id; + rs->server_fd = -1; rs->password = rd->password; @@ -2081,14 +2060,6 @@ raop_session_make(struct output_device *rd, int family, output_status_cb cb, boo break; } - rs->deferredev = evtimer_new(evbase_player, raop_deferredev_cb, rs); - if (!rs->deferredev) - { - DPRINTF(E_LOG, L_RAOP, "Out of memory for deferred error handling!\n"); - - goto out_free_rs; - } - rs->ctrl = evrtsp_connection_new(address, port); if (!rs->ctrl) { @@ -2155,8 +2126,19 @@ raop_session_make(struct output_device *rd, int family, output_status_cb cb, boo rs->volume = rd->volume; - rs->next = sessions; - sessions = rs; + rs->master_session = master_session_make(&rd->quality, rs->encrypt); + if (!rs->master_session) + { + DPRINTF(E_LOG, L_RAOP, "Could not attach a master session for device '%s'\n", rd->name); + goto out_free_evcon; + } + + // Attach to list of sessions + rs->next = raop_sessions; + raop_sessions = rs; + + // rs is now the official device session + outputs_device_session_add(rd->id, rs); return rs; @@ -2164,22 +2146,83 @@ raop_session_make(struct output_device *rd, int family, output_status_cb cb, boo evrtsp_connection_free(rs->ctrl); out_free_event: event_free(rs->deferredev); - out_free_rs: free(rs); return NULL; } -static void -raop_session_failure_cb(struct evrtsp_request *req, void *arg) -{ - struct raop_session *rs = arg; - raop_session_failure(rs); +/* ----------------------------- Metadata handling -------------------------- */ + +static void +raop_metadata_free(struct raop_metadata *rmd) +{ + if (!rmd) + return; + + if (rmd->metadata) + evbuffer_free(rmd->metadata); + if (rmd->artwork) + evbuffer_free(rmd->artwork); + + free(rmd); } +static void +raop_metadata_purge(void) +{ + if (!raop_cur_metadata) + return; + + raop_metadata_free(raop_cur_metadata->priv); + free(raop_cur_metadata); + raop_cur_metadata = NULL; +} + +// *** Thread: worker *** +static void * +raop_metadata_prepare(struct output_metadata *metadata) +{ + struct db_queue_item *queue_item; + struct raop_metadata *rmd; + struct evbuffer *tmp; + int ret; + + queue_item = db_queue_fetch_byitemid(metadata->item_id); + if (!queue_item) + { + DPRINTF(E_LOG, L_RAOP, "Could not fetch queue item\n"); + return NULL; + } + + CHECK_NULL(L_RAOP, rmd = calloc(1, sizeof(struct raop_metadata))); + CHECK_NULL(L_RAOP, rmd->artwork = evbuffer_new()); + CHECK_NULL(L_RAOP, rmd->metadata = evbuffer_new()); + CHECK_NULL(L_RAOP, tmp = evbuffer_new()); + + ret = artwork_get_item(rmd->artwork, queue_item->file_id, ART_DEFAULT_WIDTH, ART_DEFAULT_HEIGHT); + if (ret < 0) + { + DPRINTF(E_INFO, L_RAOP, "Failed to retrieve artwork for file '%s'; no artwork will be sent\n", queue_item->path); + evbuffer_free(rmd->artwork); + rmd->artwork = NULL; + } + + rmd->artwork_fmt = ret; + + ret = dmap_encode_queue_metadata(rmd->metadata, tmp, queue_item); + evbuffer_free(tmp); + free_queue_item(queue_item, 0); + if (ret < 0) + { + DPRINTF(E_LOG, L_RAOP, "Could not encode file metadata; metadata will not be sent\n"); + raop_metadata_free(rmd); + return NULL; + } + + return rmd; +} -/* Metadata handling */ static void raop_cb_metadata(struct evrtsp_request *req, void *arg) { @@ -2198,7 +2241,7 @@ raop_cb_metadata(struct evrtsp_request *req, void *arg) if (ret < 0) goto error; - /* No status_cb call, user doesn't want/need to know about the status + /* No callback to player, user doesn't want/need to know about the status * of metadata requests unless they cause the session to fail. */ @@ -2208,34 +2251,62 @@ raop_cb_metadata(struct evrtsp_request *req, void *arg) return; error: - raop_session_failure(rs); + session_failure(rs); } -static int -raop_metadata_send_progress(struct raop_session *rs, struct evbuffer *evbuf, struct raop_metadata *rmd, uint64_t offset, uint32_t delay) +static void +raop_metadata_rtptimes_get(uint32_t *start, uint32_t *display, uint32_t *pos, uint32_t *end, struct raop_master_session *rms, struct output_metadata *metadata) { - uint32_t display; - int ret; + struct rtp_session *rtp_session = rms->rtp_session; + uint64_t sample_rate; + uint32_t elapsed_ms; + int delay; /* Here's the deal with progress values: * - first value, called display, is always start minus a delay * -> delay x1 if streaming is starting for this device (joining or not) * -> delay x2 if stream is switching to a new song - * - second value, called start, is the RTP time of the first sample for this + * - second value, called pos, is the RTP time of the first sample for this * song for this device * -> start of song * -> start of song + offset if device is joining in the middle of a song, * or getting out of a pause or seeking * - third value, called end, is the RTP time of the last sample for this song */ + sample_rate = rtp_session->quality.sample_rate; - display = RAOP_RTPTIME(rmd->start - delay); + /* First calculate the rtptime that streaming of this item started: + * - at time metadata->pts the elapsed time was metadata->pos_ms + * - the time is now rtp_session->pts and the position is rtp_session->pos + * -> time since item started is elapsed = metadata->pos_ms + (rtp_session->pts - metadata->pts) + * -> start must then be start = rtp_session->pos - elapsed * sample_rate; + */ + elapsed_ms = metadata->pos_ms; - ret = evbuffer_add_printf(evbuf, "progress: %u/%u/%u\r\n", display, RAOP_RTPTIME(rmd->start + offset), RAOP_RTPTIME(rmd->end)); + *start = rtp_session->pos - sample_rate * elapsed_ms / 1000; + + if (metadata->startup) + delay = RAOP_MD_DELAY_STARTUP; + else + delay = RAOP_MD_DELAY_SWITCH; + + *display = *start - delay; + *pos = MAX(rtp_session->pos, *start); // TODO is this calculation correct? It is not in line with the description above + *end = *start + sample_rate * metadata->len_ms / 1000; + + DPRINTF(E_DBG, L_RAOP, "Metadata sr=%" PRIu64 ", pos_ms=%u, len_ms=%u, start=%u, display=%u, pos=%u, end=%u, rtptime=%u\n", + sample_rate, metadata->pos_ms, metadata->len_ms, *start, *display, *pos, *end, rtp_session->pos); +} + +static int +raop_metadata_send_progress(struct raop_session *rs, struct evbuffer *evbuf, struct raop_metadata *rmd, uint32_t display, uint32_t pos, uint32_t end) +{ + int ret; + + ret = evbuffer_add_printf(evbuf, "progress: %u/%u/%u\r\n", display, pos, end); if (ret < 0) { DPRINTF(E_LOG, L_RAOP, "Could not build progress string for sending\n"); - return -1; } @@ -2314,25 +2385,25 @@ raop_metadata_send_metadata(struct raop_session *rs, struct evbuffer *evbuf, str } static int -raop_metadata_send_internal(struct raop_session *rs, struct raop_metadata *rmd, uint64_t offset, uint32_t delay) +raop_metadata_send_generic(struct raop_session *rs, struct output_metadata *metadata) { - char rtptime[32]; + struct raop_metadata *rmd = metadata->priv; struct evbuffer *evbuf; + uint32_t start; + uint32_t display; + uint32_t pos; + uint32_t end; + char rtptime[32]; int ret; - evbuf = evbuffer_new(); - if (!evbuf) - { - DPRINTF(E_LOG, L_RAOP, "Could not allocate temp evbuffer for metadata processing\n"); + raop_metadata_rtptimes_get(&start, &display, &pos, &end, rs->master_session, metadata); - return -1; - } + CHECK_NULL(L_RAOP, evbuf = evbuffer_new()); - ret = snprintf(rtptime, sizeof(rtptime), "rtptime=%u", RAOP_RTPTIME(rmd->start)); + ret = snprintf(rtptime, sizeof(rtptime), "rtptime=%u", start); if ((ret < 0) || (ret >= sizeof(rtptime))) { DPRINTF(E_LOG, L_RAOP, "RTP-Info too big for buffer while sending metadata\n"); - ret = -1; goto out; } @@ -2341,29 +2412,25 @@ raop_metadata_send_internal(struct raop_session *rs, struct raop_metadata *rmd, if (ret < 0) { DPRINTF(E_LOG, L_RAOP, "Could not send metadata to '%s'\n", rs->devname); - ret = -1; goto out; } - if (!rmd->artwork) - goto skip_artwork; - - ret = raop_metadata_send_artwork(rs, evbuf, rmd, rtptime); - if (ret < 0) + if (rmd->artwork) { - DPRINTF(E_LOG, L_RAOP, "Could not send artwork to '%s'\n", rs->devname); - - ret = -1; - goto out; + ret = raop_metadata_send_artwork(rs, evbuf, rmd, rtptime); + if (ret < 0) + { + DPRINTF(E_LOG, L_RAOP, "Could not send artwork to '%s'\n", rs->devname); + ret = -1; + goto out; + } } - skip_artwork: - ret = raop_metadata_send_progress(rs, evbuf, rmd, offset, delay); + ret = raop_metadata_send_progress(rs, evbuf, rmd, display, pos, end); if (ret < 0) { DPRINTF(E_LOG, L_RAOP, "Could not send progress to '%s'\n", rs->devname); - ret = -1; goto out; } @@ -2374,93 +2441,48 @@ raop_metadata_send_internal(struct raop_session *rs, struct raop_metadata *rmd, return ret; } -static void +static int raop_metadata_startup_send(struct raop_session *rs) { - struct raop_metadata *rmd; - uint64_t offset; - int sent; - int ret; + if (!rs->wants_metadata || !raop_cur_metadata) + return 0; - if (!rs->wants_metadata) - return; + // We don't need to preserve the previous value, this function is the only one + // using raop_cur_metadata + raop_cur_metadata->startup = true; - sent = 0; - for (rmd = metadata_head; rmd; rmd = rmd->next) - { - // Current song, rmd->start >= rmd->end if endless stream - if ((rs->start_rtptime >= rmd->start) && ( (rs->start_rtptime < rmd->end) || (rmd->start >= rmd->end) )) - { - offset = rs->start_rtptime - rmd->start; - - ret = raop_metadata_send_internal(rs, rmd, offset, RAOP_MD_DELAY_STARTUP); - if (ret < 0) - { - raop_session_failure(rs); - - return; - } - - sent = 1; - } - // Next song(s) - else if (sent && (rs->start_rtptime < rmd->start)) - { - ret = raop_metadata_send_internal(rs, rmd, 0, RAOP_MD_DELAY_SWITCH); - if (ret < 0) - { - raop_session_failure(rs); - - return; - } - } - } + return raop_metadata_send_generic(rs, raop_cur_metadata); } static void -raop_metadata_send(void *metadata, uint64_t rtptime, uint64_t offset, int startup) +raop_metadata_send(struct output_metadata *metadata) { - struct raop_metadata *rmd; struct raop_session *rs; struct raop_session *next; - uint32_t delay; int ret; - rmd = metadata; - rmd->start += rtptime; - rmd->end += rtptime; - - /* Add the rmd to the metadata list */ - if (metadata_tail) - metadata_tail->next = rmd; - else - { - metadata_head = rmd; - metadata_tail = rmd; - } - - for (rs = sessions; rs; rs = next) + for (rs = raop_sessions; rs; rs = next) { next = rs->next; - if (!(rs->state & RAOP_STATE_F_CONNECTED)) + if (!(rs->state & RAOP_STATE_F_CONNECTED) || !rs->wants_metadata) continue; - if (!rs->wants_metadata) - continue; - - delay = (startup) ? RAOP_MD_DELAY_STARTUP : RAOP_MD_DELAY_SWITCH; - - ret = raop_metadata_send_internal(rs, rmd, offset, delay); + ret = raop_metadata_send_generic(rs, metadata); if (ret < 0) { - raop_session_failure(rs); + session_failure(rs); continue; } } + + // Replace current metadata with the new stuff + raop_metadata_purge(); + raop_cur_metadata = metadata; } -/* Volume handling */ + +/* ------------------------------ Volume handling --------------------------- */ static float raop_volume_from_pct(int volume, char *name) @@ -2599,33 +2621,28 @@ raop_cb_set_volume(struct evrtsp_request *req, void *arg) return; error: - raop_session_failure(rs); + session_failure(rs); } /* Volume in [0 - 100] */ static int -raop_set_volume_one(struct output_device *rd, output_status_cb cb) +raop_set_volume_one(struct output_device *device, int callback_id) { - struct raop_session *rs; + struct raop_session *rs = device->session; int ret; - if (!rd->session || !rd->session->session) + if (!rs || !(rs->state & RAOP_STATE_F_CONNECTED)) return 0; - rs = rd->session->session; - - if (!(rs->state & RAOP_STATE_F_CONNECTED)) - return 0; - - ret = raop_set_volume_internal(rs, rd->volume, raop_cb_set_volume); + ret = raop_set_volume_internal(rs, device->volume, raop_cb_set_volume); if (ret < 0) { - raop_session_failure(rs); + session_failure(rs); return 0; } - rs->status_cb = cb; + rs->callback_id = callback_id; return 1; } @@ -2663,7 +2680,7 @@ raop_cb_flush(struct evrtsp_request *req, void *arg) return; error: - raop_session_failure(rs); + session_failure(rs); } static void @@ -2676,7 +2693,7 @@ raop_cb_keep_alive(struct evrtsp_request *req, void *arg) if (!req) { DPRINTF(E_LOG, L_RAOP, "No reply from '%s' to our keep alive request, hanging up\n", rs->devname); - raop_session_failure(rs); + session_failure(rs); return; } @@ -2694,70 +2711,18 @@ raop_cb_keep_alive(struct evrtsp_request *req, void *arg) return; } -static void -raop_cb_pin_start(struct evrtsp_request *req, void *arg) -{ - struct raop_session *rs = arg; - int ret; - - rs->reqs_in_flight--; - - if (!req) - goto error; - - if (req->response_code != RTSP_OK) - { - DPRINTF(E_LOG, L_RAOP, "Request for starting PIN verification failed: %d %s\n", req->response_code, req->response_code_line); - - goto error; - } - - ret = raop_check_cseq(rs, req); - if (ret < 0) - goto error; - - rs->state = RAOP_STATE_UNVERIFIED; - - raop_status(rs); - - // TODO If the user never verifies the session will remain stale - - return; - - error: - raop_session_failure(rs); -} - - -// Forward -static int -raop_device_start(struct output_device *rd, output_status_cb cb, uint64_t rtptime); - -static void -raop_device_stop(struct output_session *session); - -static void -raop_flush_timer_cb(int fd, short what, void *arg) -{ - struct raop_session *rs; - - DPRINTF(E_DBG, L_RAOP, "Flush timer expired; tearing down RAOP sessions\n"); - - for (rs = sessions; rs; rs = rs->next) - { - if (!(rs->state & RAOP_STATE_F_CONNECTED)) - continue; - - raop_device_stop(rs->output_session); - } -} - static void raop_keep_alive_timer_cb(int fd, short what, void *arg) { struct raop_session *rs; - for (rs = sessions; rs; rs = rs->next) + if (!raop_sessions) + { + event_del(keep_alive_timer); + return; + } + + for (rs = raop_sessions; rs; rs = rs->next) { if (!(rs->state & RAOP_STATE_F_CONNECTED)) continue; @@ -2766,47 +2731,224 @@ raop_keep_alive_timer_cb(int fd, short what, void *arg) } } + +/* -------------------- Creation and sending of RTP packets ---------------- */ + static int -raop_flush(output_status_cb cb, uint64_t rtptime) +packet_prepare(struct rtp_packet *pkt, uint8_t *rawbuf, size_t rawbuf_size, bool encrypt) +{ + char ebuf[64]; + gpg_error_t gc_err; + + alac_encode(pkt->payload, rawbuf, rawbuf_size); + + if (!encrypt) + return 0; + + // Reset cipher + gc_err = gcry_cipher_reset(raop_aes_ctx); + if (gc_err != GPG_ERR_NO_ERROR) + { + gpg_strerror_r(gc_err, ebuf, sizeof(ebuf)); + DPRINTF(E_LOG, L_RAOP, "Could not reset AES cipher: %s\n", ebuf); + return -1; + } + + // Set IV + gc_err = gcry_cipher_setiv(raop_aes_ctx, raop_aes_iv, sizeof(raop_aes_iv)); + if (gc_err != GPG_ERR_NO_ERROR) + { + gpg_strerror_r(gc_err, ebuf, sizeof(ebuf)); + DPRINTF(E_LOG, L_RAOP, "Could not set AES IV: %s\n", ebuf); + return -1; + } + + // Encrypt in blocks of 16 bytes + gc_err = gcry_cipher_encrypt(raop_aes_ctx, pkt->payload, (pkt->payload_len / 16) * 16, NULL, 0); + if (gc_err != GPG_ERR_NO_ERROR) + { + gpg_strerror_r(gc_err, ebuf, sizeof(ebuf)); + DPRINTF(E_LOG, L_RAOP, "Could not encrypt payload: %s\n", ebuf); + return -1; + } + + return 0; +} + +static int +packet_send(struct raop_session *rs, struct rtp_packet *pkt) { - struct timeval tv; - struct raop_session *rs; - struct raop_session *next; - int pending; int ret; - pending = 0; - for (rs = sessions; rs; rs = next) - { - next = rs->next; + if (!rs) + return -1; - if (rs->state != RAOP_STATE_STREAMING) + ret = send(rs->server_fd, pkt->data, pkt->data_len, 0); + if (ret < 0) + { + DPRINTF(E_LOG, L_RAOP, "Send error for '%s': %s\n", rs->devname, strerror(errno)); + + // Can't free it right away, it would make the ->next in the calling + // master_session and session loops invalid + deferred_session_failure(rs); + return -1; + } + else if (ret != pkt->data_len) + { + DPRINTF(E_WARN, L_RAOP, "Partial send (%d) for '%s'\n", ret, rs->devname); + return -1; + } + +/* DPRINTF(E_DBG, L_PLAYER, "RTP PACKET seqnum %u, rtptime %u, payload 0x%x, pktbuf_s %zu\n", + rs->master_session->rtp_session->seqnum, + rs->master_session->rtp_session->pos, + pkt->header[1], + rs->master_session->rtp_session->pktbuf_len + ); +*/ + return 0; +} + +static void +control_packet_send(struct raop_session *rs, struct rtp_packet *pkt) +{ + int len; + int ret; + + switch (rs->sa.ss.ss_family) + { + case AF_INET: + rs->sa.sin.sin_port = htons(rs->control_port); + len = sizeof(rs->sa.sin); + break; + + case AF_INET6: + rs->sa.sin6.sin6_port = htons(rs->control_port); + len = sizeof(rs->sa.sin6); + break; + + default: + DPRINTF(E_WARN, L_RAOP, "Unknown family %d\n", rs->sa.ss.ss_family); + return; + } + + ret = sendto(rs->control_svc->fd, pkt->data, pkt->data_len, 0, &rs->sa.sa, len); + if (ret < 0) + DPRINTF(E_LOG, L_RAOP, "Could not send playback sync to device '%s': %s\n", rs->devname, strerror(errno)); +} + +static void +packets_resend(struct raop_session *rs, uint16_t seqnum, uint16_t len) +{ + struct rtp_packet *pkt; + uint16_t s; + bool pkt_missing = false; + + for (s = seqnum; s < seqnum + len; s++) + { + pkt = rtp_packet_get(rs->master_session->rtp_session, s); + if (pkt) + packet_send(rs, pkt); + else + pkt_missing = true; + } + + if (pkt_missing) + DPRINTF(E_WARN, L_RAOP, "Device '%s' asking for seqnum %" PRIu16 " (len %" PRIu16 "), but not in buffer\n", rs->devname, seqnum, len); +} + +static int +packets_send(struct raop_master_session *rms) +{ + struct rtp_packet *pkt; + struct raop_session *rs; + int ret; + + pkt = rtp_packet_next(rms->rtp_session, ALAC_HEADER_LEN + rms->rawbuf_size, rms->samples_per_packet, 0x60); + + ret = packet_prepare(pkt, rms->rawbuf, rms->rawbuf_size, rms->encrypt); + if (ret < 0) + return -1; + + for (rs = raop_sessions; rs; rs = rs->next) + { + if (rs->master_session != rms) continue; - ret = raop_send_req_flush(rs, rtptime, raop_cb_flush, "flush"); - if (ret < 0) + // Device just joined + if (rs->state == RAOP_STATE_CONNECTED) { - raop_session_failure(rs); - - continue; + pkt->header[1] = 0xe0; + packet_send(rs, pkt); + } + else if (rs->state == RAOP_STATE_STREAMING) + { + pkt->header[1] = 0x60; + packet_send(rs, pkt); } - - rs->status_cb = cb; - pending++; } - if (pending > 0) + // Commits packet to retransmit buffer, and prepares the session for the next packet + rtp_packet_commit(rms->rtp_session, pkt); + + return 0; +} + +static void +packets_sync_send(struct raop_master_session *rms, struct timespec pts) +{ + struct rtp_packet *sync_pkt; + struct raop_session *rs; + struct rtcp_timestamp cur_stamp; + struct timespec ts; + bool is_sync_time; + + // Check if it is time send a sync packet to sessions that are already running + is_sync_time = rtp_sync_is_time(rms->rtp_session); + + // The last write from the player had a timestamp which has been passed to + // this function as pts. This is the time the device should be playing the + // samples just written by the player, so it is a time which is + // OUTPUTS_BUFFER_DURATION secs into the future. However, in the sync packet + // we want to tell the device what it should be playing right now. So we give + // it a cur_time where we subtract this duration. +// TODO update comment to match reality + cur_stamp.ts.tv_sec = pts.tv_sec; + cur_stamp.ts.tv_nsec = pts.tv_nsec; + + clock_gettime(CLOCK_MONOTONIC, &ts); + + // The cur_pos will be the rtptime of the coming packet, minus + // OUTPUTS_BUFFER_DURATION in samples (output_buffer_samples). Because we + // might also have some data lined up in rms->evbuf, we also need to account + // for that. + cur_stamp.pos = rms->rtp_session->pos + rms->evbuf_samples - rms->output_buffer_samples; + + for (rs = raop_sessions; rs; rs = rs->next) { - evutil_timerclear(&tv); - tv.tv_sec = 10; - evtimer_add(flush_timer, &tv); - } + if (rs->master_session != rms) + continue; - return pending; + // A device has joined and should get an init sync packet + if (rs->state == RAOP_STATE_CONNECTED) + { + sync_pkt = rtp_sync_packet_next(rms->rtp_session, &cur_stamp, 0x90); + control_packet_send(rs, sync_pkt); + + DPRINTF(E_DBG, L_PLAYER, "Start sync packet sent to '%s': cur_pos=%" PRIu32 ", cur_ts=%lu:%lu, now=%lu:%lu, rtptime=%" PRIu32 ",\n", + rs->devname, cur_stamp.pos, cur_stamp.ts.tv_sec, cur_stamp.ts.tv_nsec, ts.tv_sec, ts.tv_nsec, rms->rtp_session->pos); + } + else if (is_sync_time && rs->state == RAOP_STATE_STREAMING) + { + sync_pkt = rtp_sync_packet_next(rms->rtp_session, &cur_stamp, 0x80); + control_packet_send(rs, sync_pkt); + } + } } -/* AirTunes v2 time synchronization */ +/* ------------------------------ Time service ------------------------------ */ + static void raop_v2_timing_cb(int fd, short what, void *arg) { @@ -3050,86 +3192,8 @@ raop_v2_timing_start(int v6enabled) return 0; } -/* AirTunes v2 playback synchronization */ -static void -raop_v2_control_send_sync(uint64_t next_pkt, struct timespec *init) -{ - uint8_t msg[20]; - struct timespec ts; - struct ntp_stamp cur_stamp; - struct raop_session *rs; - uint64_t cur_pos; - uint32_t cur_pos32; - uint32_t next_pkt32; - int len; - int ret; - memset(msg, 0, sizeof(msg)); - - msg[0] = (sync_counter == 0) ? 0x90 : 0x80; - msg[1] = 0xd4; - msg[3] = 0x07; - - next_pkt32 = htobe32(RAOP_RTPTIME(next_pkt)); - memcpy(msg + 16, &next_pkt32, 4); - - if (!init) - { - ret = player_get_current_pos(&cur_pos, &ts, 1); - if (ret < 0) - { - DPRINTF(E_LOG, L_RAOP, "Could not get current playback position and clock\n"); - - return; - } - - timespec_to_ntp(&ts, &cur_stamp); - } - else - { - cur_pos = next_pkt - 88200; - timespec_to_ntp(init, &cur_stamp); - } - - cur_pos32 = htobe32(RAOP_RTPTIME(cur_pos)); - cur_stamp.sec = htobe32(cur_stamp.sec); - cur_stamp.frac = htobe32(cur_stamp.frac); - - memcpy(msg + 4, &cur_pos32, 4); - memcpy(msg + 8, &cur_stamp.sec, 4); - memcpy(msg + 12, &cur_stamp.frac, 4); - - for (rs = sessions; rs; rs = rs->next) - { - if (rs->state != RAOP_STATE_STREAMING) - continue; - - switch (rs->sa.ss.ss_family) - { - case AF_INET: - rs->sa.sin.sin_port = htons(rs->control_port); - len = sizeof(rs->sa.sin); - break; - - case AF_INET6: - rs->sa.sin6.sin6_port = htons(rs->control_port); - len = sizeof(rs->sa.sin6); - break; - - default: - DPRINTF(E_WARN, L_RAOP, "Unknown family %d\n", rs->sa.ss.ss_family); - continue; - } - - ret = sendto(rs->control_svc->fd, msg, sizeof(msg), 0, &rs->sa.sa, len); - if (ret < 0) - DPRINTF(E_LOG, L_RAOP, "Could not send playback sync to device '%s': %s\n", rs->devname, strerror(errno)); - } -} - -/* Forward */ -static void -raop_v2_resend_range(struct raop_session *rs, uint16_t seqnum, uint16_t len); +/* ----------------- Control service (retransmission and sync) ---------------*/ static void raop_v2_control_cb(int fd, short what, void *arg) @@ -3168,7 +3232,7 @@ raop_v2_control_cb(int fd, short what, void *arg) if (svc != &control_4svc) goto readd; - for (rs = sessions; rs; rs = rs->next) + for (rs = raop_sessions; rs; rs = rs->next) { if ((rs->sa.ss.ss_family == AF_INET) && (sa.sin.sin_addr.s_addr == rs->sa.sin.sin_addr.s_addr)) @@ -3184,7 +3248,7 @@ raop_v2_control_cb(int fd, short what, void *arg) if (svc != &control_6svc) goto readd; - for (rs = sessions; rs; rs = rs->next) + for (rs = raop_sessions; rs; rs = rs->next) { if ((rs->sa.ss.ss_family == AF_INET6) && IN6_ARE_ADDR_EQUAL(&sa.sin6.sin6_addr, &rs->sa.sin6.sin6_addr)) @@ -3226,7 +3290,7 @@ raop_v2_control_cb(int fd, short what, void *arg) DPRINTF(E_DBG, L_RAOP, "Got retransmit request from '%s', seq_start %u len %u\n", rs->devname, seq_start, seq_len); - raop_v2_resend_range(rs, seq_start, seq_len); + packets_resend(rs, seq_start, seq_len); readd: ret = event_add(svc->ev, NULL); @@ -3383,259 +3447,99 @@ raop_v2_control_start(int v6enabled) } -/* AirTunes v2 streaming */ -static struct raop_v2_packet * -raop_v2_new_packet(void) +/* ------------------------------ Session startup --------------------------- */ + +static void +raop_cb_startup_retry(struct evrtsp_request *req, void *arg) { - struct raop_v2_packet *pkt; + struct raop_session *rs = arg; + struct output_device *device; + int callback_id = rs->callback_id; - if (pktbuf_size >= RETRANSMIT_BUFFER_SIZE) + device = outputs_device_get(rs->device_id); + if (!device) { - pktbuf_size--; - - pkt = pktbuf_tail; - - pktbuf_tail = pktbuf_tail->prev; - pktbuf_tail->next = NULL; - } - else - { - pkt = (struct raop_v2_packet *)malloc(sizeof(struct raop_v2_packet)); - if (!pkt) - { - DPRINTF(E_LOG, L_RAOP, "Out of memory for RAOP packet\n"); - - return NULL; - } + session_failure(rs); + return; } - return pkt; + session_cleanup(rs); + raop_device_start(device, callback_id); } -static struct raop_v2_packet * -raop_v2_make_packet(uint8_t *rawbuf, uint64_t rtptime) +static void +raop_cb_startup_cancel(struct evrtsp_request *req, void *arg) { - char ebuf[64]; - struct raop_v2_packet *pkt; - gpg_error_t gc_err; - uint32_t rtptime32; - uint16_t seq; + struct raop_session *rs = arg; - pkt = raop_v2_new_packet(); - if (!pkt) - return NULL; - - memset(pkt, 0, sizeof(struct raop_v2_packet)); - - alac_encode(rawbuf, pkt->clear + AIRTUNES_V2_HDR_LEN, STOB(AIRTUNES_V2_PACKET_SAMPLES)); - - stream_seq++; - - pkt->seqnum = stream_seq; - - seq = htobe16(pkt->seqnum); - rtptime32 = htobe32(RAOP_RTPTIME(rtptime)); - - pkt->clear[0] = 0x80; - pkt->clear[1] = (sync_counter == 0) ? 0xe0 : 0x60; - - memcpy(pkt->clear + 2, &seq, 2); - memcpy(pkt->clear + 4, &rtptime32, 4); - - /* RTP SSRC ID - * Note: should htobe32() that value, but it's just a - * random/unique ID so it's no big deal - */ - memcpy(pkt->clear + 8, &ssrc_id, 4); - - /* Copy AirTunes v2 header to encrypted packet */ - memcpy(pkt->encrypted, pkt->clear, AIRTUNES_V2_HDR_LEN); - - /* Copy the tail of the audio packet that is left unencrypted */ - memcpy(pkt->encrypted + AIRTUNES_V2_PKT_TAIL_OFF, - pkt->clear + AIRTUNES_V2_PKT_TAIL_OFF, - AIRTUNES_V2_PKT_TAIL_LEN); - - /* Reset cipher */ - gc_err = gcry_cipher_reset(raop_aes_ctx); - if (gc_err != GPG_ERR_NO_ERROR) - { - gpg_strerror_r(gc_err, ebuf, sizeof(ebuf)); - DPRINTF(E_LOG, L_RAOP, "Could not reset AES cipher: %s\n", ebuf); - - free(pkt); - return NULL; - } - - /* Set IV */ - gc_err = gcry_cipher_setiv(raop_aes_ctx, raop_aes_iv, sizeof(raop_aes_iv)); - if (gc_err != GPG_ERR_NO_ERROR) - { - gpg_strerror_r(gc_err, ebuf, sizeof(ebuf)); - DPRINTF(E_LOG, L_RAOP, "Could not set AES IV: %s\n", ebuf); - - free(pkt); - return NULL; - } - - /* Encrypt in blocks of 16 bytes */ - gc_err = gcry_cipher_encrypt(raop_aes_ctx, - pkt->encrypted + AIRTUNES_V2_HDR_LEN, ((AIRTUNES_V2_PKT_LEN - AIRTUNES_V2_HDR_LEN) / 16) * 16, - pkt->clear + AIRTUNES_V2_HDR_LEN, ((AIRTUNES_V2_PKT_LEN - AIRTUNES_V2_HDR_LEN) / 16) * 16); - if (gc_err != GPG_ERR_NO_ERROR) - { - gpg_strerror_r(gc_err, ebuf, sizeof(ebuf)); - DPRINTF(E_LOG, L_RAOP, "Could not encrypt payload: %s\n", ebuf); - - free(pkt); - return NULL; - } - - pkt->prev = NULL; - pkt->next = pktbuf_head; - - if (pktbuf_head) - pktbuf_head->prev = pkt; - - if (!pktbuf_tail) - pktbuf_tail = pkt; - - pktbuf_head = pkt; - - pktbuf_size++; - - return pkt; + session_failure(rs); } -static int -raop_v2_send_packet(struct raop_session *rs, struct raop_v2_packet *pkt) +static void +raop_startup_cancel(struct raop_session *rs) { - uint8_t *data; + struct output_device *device; int ret; - if (!rs) - return -1; - - data = (rs->encrypt) ? pkt->encrypted : pkt->clear; - - ret = send(rs->server_fd, data, AIRTUNES_V2_PKT_LEN, 0); - if (ret < 0) + device = outputs_device_get(rs->device_id); + if (!device || !rs->session) { - DPRINTF(E_LOG, L_RAOP, "Send error for '%s': %s\n", rs->devname, strerror(errno)); - - raop_session_failure(rs); - return -1; - } - else if (ret != AIRTUNES_V2_PKT_LEN) - { - DPRINTF(E_WARN, L_RAOP, "Partial send (%d) for '%s'\n", ret, rs->devname); - return -1; + session_failure(rs); + return; } - return 0; -} - -// Forward -static void -raop_playback_stop(void); - -static void -raop_v2_write(uint8_t *buf, uint64_t rtptime) -{ - struct raop_v2_packet *pkt; - struct raop_session *rs; - struct raop_session *next; - - pkt = raop_v2_make_packet(buf, rtptime); - if (!pkt) + // Some devices don't seem to work with ipv6, so if the error wasn't a hard + // failure (bad password) we fall back to ipv4 and flag device as bad for ipv6 + if (rs->family == AF_INET6 && !(rs->state & RAOP_STATE_F_FAILED)) { - raop_playback_stop(); + // This flag is permanent and will not be overwritten by mdns advertisements + device->v6_disabled = 1; + + // Stop current session and wait for call back + ret = raop_send_req_teardown(rs, raop_cb_startup_retry, "startup_cancel"); + if (ret < 0) + raop_cb_startup_retry(NULL, rs); // No connection at all, call retry directly return; } - if (sync_counter == 126) + ret = raop_send_req_teardown(rs, raop_cb_startup_cancel, "startup_cancel"); + if (ret < 0) + session_failure(rs); +} + +static void +raop_cb_pin_start(struct evrtsp_request *req, void *arg) +{ + struct raop_session *rs = arg; + int ret; + + rs->reqs_in_flight--; + + if (!req) + goto error; + + if (req->response_code != RTSP_OK) { - raop_v2_control_send_sync(rtptime, NULL); + DPRINTF(E_LOG, L_RAOP, "Request for starting PIN verification failed: %d %s\n", req->response_code, req->response_code_line); - sync_counter = 1; + goto error; } - else - sync_counter++; - for (rs = sessions; rs; rs = next) - { - // raop_v2_send_packet may free rs on failure, so save rs->next now - next = rs->next; + ret = raop_check_cseq(rs, req); + if (ret < 0) + goto error; - if (rs->state != RAOP_STATE_STREAMING) - continue; + rs->state = RAOP_STATE_UNVERIFIED; - raop_v2_send_packet(rs, pkt); - } + raop_status(rs); + + // TODO If the user never verifies the session will remain stale return; -} -static void -raop_v2_resend_range(struct raop_session *rs, uint16_t seqnum, uint16_t len) -{ - struct raop_v2_packet *pktbuf; - int ret; - uint16_t distance; - - /* Check that seqnum is in the retransmit buffer */ - if ((seqnum > pktbuf_head->seqnum) || (seqnum < pktbuf_tail->seqnum)) - { - DPRINTF(E_WARN, L_RAOP, "Device '%s' asking for seqnum %" PRIu16 "; not in buffer (h %" PRIu16 " t %" PRIu16 ")\n", rs->devname, seqnum, pktbuf_head->seqnum, pktbuf_tail->seqnum); - return; - } - - if (seqnum > pktbuf_head->seqnum) - { - distance = seqnum - pktbuf_tail->seqnum; - - if (distance > (RETRANSMIT_BUFFER_SIZE / 2)) - pktbuf = pktbuf_head; - else - pktbuf = pktbuf_tail; - } - else - { - distance = pktbuf_head->seqnum - seqnum; - - if (distance > (RETRANSMIT_BUFFER_SIZE / 2)) - pktbuf = pktbuf_tail; - else - pktbuf = pktbuf_head; - } - - if (pktbuf == pktbuf_head) - { - while (pktbuf && seqnum != pktbuf->seqnum) - pktbuf = pktbuf->next; - } - else - { - while (pktbuf && seqnum != pktbuf->seqnum) - pktbuf = pktbuf->prev; - } - - while (len && pktbuf) - { - ret = raop_v2_send_packet(rs, pktbuf); - if (ret < 0) - { - DPRINTF(E_LOG, L_RAOP, "Error retransmit packet, aborting retransmission\n"); - return; - } - - pktbuf = pktbuf->prev; - len--; - } - - if (len != 0) - DPRINTF(E_LOG, L_RAOP, "WARNING: len non-zero at end of retransmission\n"); + error: + session_failure(rs); } static int @@ -3681,13 +3585,7 @@ raop_v2_stream_open(struct raop_session *rs) goto out_fail; } - /* Include the device into the set of active devices if - * playback is in progress. - */ - if (sync_counter != 0) - rs->state = RAOP_STATE_STREAMING; - else - rs->state = RAOP_STATE_CONNECTED; + rs->state = RAOP_STATE_CONNECTED; return 0; @@ -3698,38 +3596,6 @@ raop_v2_stream_open(struct raop_session *rs) return -1; } - -/* Session startup */ -static void -raop_startup_cancel(struct raop_session *rs) -{ - if (!rs->session) - { - raop_session_failure(rs); - return; - } - - // Some devices don't seem to work with ipv6, so if the error wasn't a hard - // failure (bad password) we fall back to ipv4 and flag device as bad for ipv6 - if (rs->family == AF_INET6 && !(rs->state & RAOP_STATE_F_FAILED)) - { - // This flag is permanent and will not be overwritten by mdns advertisements - rs->device->v6_disabled = 1; - - // Be nice to our peer + raop_session_failure_cb() cleans up old session - raop_send_req_teardown(rs, raop_session_failure_cb, "startup_cancel"); - - // Try to start a new session - raop_device_start(rs->device, rs->status_cb, rs->start_rtptime); - - // Don't let the failed session make a negative status callback - rs->status_cb = NULL; - return; - } - - raop_send_req_teardown(rs, raop_session_failure_cb, "startup_cancel"); -} - static void raop_cb_startup_volume(struct evrtsp_request *req, void *arg) { @@ -3752,15 +3618,13 @@ raop_cb_startup_volume(struct evrtsp_request *req, void *arg) if (ret < 0) goto cleanup; - raop_metadata_startup_send(rs); + ret = raop_metadata_startup_send(rs); + if (ret < 0) + goto cleanup; ret = raop_v2_stream_open(rs); if (ret < 0) - { - DPRINTF(E_LOG, L_RAOP, "Could not open streaming socket\n"); - - goto cleanup; - } + goto cleanup; /* Session startup and setup is done, tell our user */ raop_status(rs); @@ -4027,6 +3891,7 @@ static void raop_cb_startup_options(struct evrtsp_request *req, void *arg) { struct raop_session *rs = arg; + struct output_device *device; const char *param; int ret; @@ -4076,7 +3941,11 @@ raop_cb_startup_options(struct evrtsp_request *req, void *arg) if (req->response_code == RTSP_FORBIDDEN) { - rs->device->requires_auth = 1; + device = outputs_device_get(rs->device_id); + if (!device) + goto cleanup; + + device->requires_auth = 1; ret = raop_send_req_pin_start(rs, raop_cb_pin_start, "startup_options"); if (ret < 0) @@ -4102,7 +3971,7 @@ raop_cb_startup_options(struct evrtsp_request *req, void *arg) raop_status(rs); // We're not going further with this session - raop_session_cleanup(rs); + session_cleanup(rs); } else if (rs->supports_post && rs->supports_auth_setup) { @@ -4123,48 +3992,14 @@ raop_cb_startup_options(struct evrtsp_request *req, void *arg) cleanup: if (rs->only_probe) - raop_session_failure(rs); + session_failure(rs); else raop_startup_cancel(rs); } -static void -raop_cb_shutdown_teardown(struct evrtsp_request *req, void *arg) -{ - struct raop_session *rs = arg; - int ret; - rs->reqs_in_flight--; - - if (!req) - goto error; - - if (req->response_code != RTSP_OK) - { - DPRINTF(E_LOG, L_RAOP, "TEARDOWN request failed in session shutdown: %d %s\n", req->response_code, req->response_code_line); - - goto error; - } - - ret = raop_check_cseq(rs, req); - if (ret < 0) - goto error; - - rs->state = RAOP_STATE_STOPPED; - - /* Session shut down, tell our user */ - raop_status(rs); - - raop_session_cleanup(rs); - - return; - - error: - raop_session_failure(rs); -} - - -/* tvOS device verification - e.g. for the ATV4 (read it from the bottom and up) */ +/* ------------------------- tvOS device verification ----------------------- */ +/* e.g. for the ATV4 (read it from the bottom and up) */ #ifdef RAOP_VERIFICATION static int @@ -4317,6 +4152,7 @@ static void raop_cb_verification_verify_step2(struct evrtsp_request *req, void *arg) { struct raop_session *rs = arg; + struct output_device *device; int ret; verification_verify_free(rs->verification_verify_ctx); @@ -4324,9 +4160,13 @@ raop_cb_verification_verify_step2(struct evrtsp_request *req, void *arg) ret = raop_verification_response_process(5, req, rs); if (ret < 0) { + device = outputs_device_get(rs->device_id); + if (!device) + goto error; + // Clear auth_key, the device did not accept it - free(rs->device->auth_key); - rs->device->auth_key = NULL; + free(device->auth_key); + device->auth_key = NULL; goto error; } @@ -4336,7 +4176,7 @@ raop_cb_verification_verify_step2(struct evrtsp_request *req, void *arg) raop_send_req_options(rs, raop_cb_startup_options, "verify_step2"); - player_speaker_status_trigger(); + outputs_listener_notify(); return; @@ -4349,14 +4189,19 @@ static void raop_cb_verification_verify_step1(struct evrtsp_request *req, void *arg) { struct raop_session *rs = arg; + struct output_device *device; int ret; ret = raop_verification_response_process(4, req, rs); if (ret < 0) { + device = outputs_device_get(rs->device_id); + if (!device) + goto error; + // Clear auth_key, the device did not accept it - free(rs->device->auth_key); - rs->device->auth_key = NULL; + free(device->auth_key); + device->auth_key = NULL; goto error; } @@ -4377,14 +4222,14 @@ raop_cb_verification_verify_step1(struct evrtsp_request *req, void *arg) static int raop_verification_verify(struct raop_session *rs) { + struct output_device *device; int ret; - rs->verification_verify_ctx = verification_verify_new(rs->device->auth_key); // Naughty boy is dereferencing device - if (!rs->verification_verify_ctx) - { - DPRINTF(E_LOG, L_RAOP, "Out of memory for verification verify context\n"); - return -1; - } + device = outputs_device_get(rs->device_id); + if (!device) + goto error; + + CHECK_NULL(L_RAOP, rs->verification_verify_ctx = verification_verify_new(device->auth_key)); ret = raop_verification_request_send(4, rs, raop_cb_verification_verify_step1); if (ret < 0) @@ -4406,6 +4251,7 @@ static void raop_cb_verification_setup_step3(struct evrtsp_request *req, void *arg) { struct raop_session *rs = arg; + struct output_device *device; const char *authorization_key; int ret; @@ -4422,14 +4268,18 @@ raop_cb_verification_setup_step3(struct evrtsp_request *req, void *arg) DPRINTF(E_LOG, L_RAOP, "Verification setup stage complete, saving authorization key\n"); - // Dereferencing output_device and a blocking db call... :-~ - free(rs->device->auth_key); - rs->device->auth_key = strdup(authorization_key); + device = outputs_device_get(rs->device_id); + if (!device) + goto error; - db_speaker_save(rs->device); + free(device->auth_key); + device->auth_key = strdup(authorization_key); + + // A blocking db call... :-~ + db_speaker_save(device); // The player considers this session failed, so we don't need it any more - raop_session_cleanup(rs); + session_cleanup(rs); /* Fallthrough */ @@ -4486,7 +4336,7 @@ raop_verification_setup(const char *pin) struct raop_session *rs; int ret; - for (rs = sessions; rs; rs = rs->next) + for (rs = raop_sessions; rs; rs = rs->next) { if (rs->state == RAOP_STATE_UNVERIFIED) break; @@ -4525,8 +4375,10 @@ raop_verification_verify(struct raop_session *rs) } #endif /* RAOP_VERIFICATION */ -/* RAOP devices discovery - mDNS callback */ -/* Thread: main (mdns) */ + +/* ------------------ RAOP devices discovery - mDNS callback ---------------- */ +/* Thread: main (mdns) */ + /* Examples of txt content: * Apple TV 2: ["sf=0x4" "am=AppleTV2,1" "vs=130.14" "vn=65537" "tp=UDP" "ss=16" "sr=4 4100" "sv=false" "pw=false" "md=0,1,2" "et=0,3,5" "da=true" "cn=0,1,2,3" "ch=2"] @@ -4612,7 +4464,7 @@ raop_device_cb(const char *name, const char *type, const char *domain, const cha if (port < 0) { - /* Device stopped advertising */ + // Device stopped advertising switch (family) { case AF_INET: @@ -4631,7 +4483,7 @@ raop_device_cb(const char *name, const char *type, const char *domain, const cha return; } - /* Protocol */ + // Protocol p = keyval_get(txt, "tp"); if (!p) { @@ -4654,7 +4506,7 @@ raop_device_cb(const char *name, const char *type, const char *domain, const cha goto free_rd; } - /* Password protection */ + // Password protection password = NULL; p = keyval_get(txt, "pw"); if (!p) @@ -4686,7 +4538,7 @@ raop_device_cb(const char *name, const char *type, const char *domain, const cha rd->password = password; - /* Device verification */ + // Device verification p = keyval_get(txt, "sf"); if (p && (safe_hextou64(p, &sf) == 0)) { @@ -4696,7 +4548,24 @@ raop_device_cb(const char *name, const char *type, const char *domain, const cha // Note: device_add() in player.c will get the auth key from the db if available } - /* Device type */ + // Quality supported - note this is mostly WIP, since newer devices that support + // higher than 44100/16 don't seem to use the below fields (probably use sf instead) + p = keyval_get(txt, "sr"); + if (!p || (safe_atoi32(p, &rd->quality.sample_rate) != 0)) + rd->quality.sample_rate = RAOP_QUALITY_SAMPLE_RATE_DEFAULT; + + p = keyval_get(txt, "ss"); + if (!p || (safe_atoi32(p, &rd->quality.bits_per_sample) != 0)) + rd->quality.bits_per_sample = RAOP_QUALITY_BITS_PER_SAMPLE_DEFAULT; + + p = keyval_get(txt, "ch"); + if (!p || (safe_atoi32(p, &rd->quality.channels) != 0)) + rd->quality.channels = RAOP_QUALITY_CHANNELS_DEFAULT; + + if (!quality_is_equal(&rd->quality, &raop_quality_default)) + DPRINTF(E_LOG, L_RAOP, "Device '%s' requested non-default audio quality (%d/%d/%d)\n", rd->name, rd->quality.sample_rate, rd->quality.bits_per_sample, rd->quality.channels); + + // Device type re->devtype = RAOP_DEV_OTHER; p = keyval_get(txt, "am"); @@ -4713,14 +4582,14 @@ raop_device_cb(const char *name, const char *type, const char *domain, const cha else if (*p == '\0') DPRINTF(E_LOG, L_RAOP, "AirPlay device '%s': am has no value\n", at_name); - /* Encrypt stream */ + // Encrypt stream p = keyval_get(txt, "ek"); if (p && (*p == '1')) re->encrypt = 1; else re->encrypt = 0; - /* Metadata support */ + // Metadata support p = keyval_get(txt, "md"); if (p && (*p != '\0')) re->wants_metadata = 1; @@ -4743,8 +4612,6 @@ raop_device_cb(const char *name, const char *type, const char *domain, const cha free(et); } - rd->advertised = 1; - switch (family) { case AF_INET: @@ -4776,8 +4643,12 @@ raop_device_cb(const char *name, const char *type, const char *domain, const cha outputs_device_free(rd); } + +/* ---------------------------- Module definitions -------------------------- */ +/* Thread: player */ + static int -raop_device_start_generic(struct output_device *rd, output_status_cb cb, uint64_t rtptime, bool only_probe) +raop_device_start_generic(struct output_device *device, int callback_id, bool only_probe) { struct raop_session *rs; int ret; @@ -4787,14 +4658,12 @@ raop_device_start_generic(struct output_device *rd, output_status_cb cb, uint64_ * address and build our session URL for all subsequent requests. */ - rs = raop_session_make(rd, AF_INET6, cb, only_probe); + rs = session_make(device, AF_INET6, callback_id, only_probe); if (rs) { - rs->start_rtptime = rtptime; - - if (rd->auth_key) + if (device->auth_key) ret = raop_verification_verify(rs); - else if (rd->requires_auth) + else if (device->requires_auth) ret = raop_send_req_pin_start(rs, raop_cb_pin_start, "device_start"); else ret = raop_send_req_options(rs, raop_cb_startup_options, "device_start"); @@ -4804,19 +4673,17 @@ raop_device_start_generic(struct output_device *rd, output_status_cb cb, uint64_ else { DPRINTF(E_WARN, L_RAOP, "Could not send verification or OPTIONS request on IPv6\n"); - raop_session_cleanup(rs); + session_cleanup(rs); } } - rs = raop_session_make(rd, AF_INET, cb, only_probe); + rs = session_make(device, AF_INET, callback_id, only_probe); if (!rs) return -1; - rs->start_rtptime = rtptime; - - if (rd->auth_key) + if (device->auth_key) ret = raop_verification_verify(rs); - else if (rd->requires_auth) + else if (device->requires_auth) ret = raop_send_req_pin_start(rs, raop_cb_pin_start, "device_start"); else ret = raop_send_req_options(rs, raop_cb_startup_options, "device_start"); @@ -4824,7 +4691,7 @@ raop_device_start_generic(struct output_device *rd, output_status_cb cb, uint64_ if (ret < 0) { DPRINTF(E_WARN, L_RAOP, "Could not send verification or OPTIONS request on IPv4\n"); - raop_session_cleanup(rs); + session_cleanup(rs); return -1; } @@ -4832,29 +4699,53 @@ raop_device_start_generic(struct output_device *rd, output_status_cb cb, uint64_ } static int -raop_device_probe(struct output_device *rd, output_status_cb cb) +raop_device_probe(struct output_device *device, int callback_id) { - return raop_device_start_generic(rd, cb, 0, 1); + return raop_device_start_generic(device, callback_id, 1); } static int -raop_device_start(struct output_device *rd, output_status_cb cb, uint64_t rtptime) +raop_device_start(struct output_device *device, int callback_id) { - return raop_device_start_generic(rd, cb, rtptime, 0); + return raop_device_start_generic(device, callback_id, 0); +} + +static int +raop_device_stop(struct output_device *device, int callback_id) +{ + struct raop_session *rs = device->session; + + rs->callback_id = callback_id; + + return session_teardown(rs, "device_stop"); +} + +static int +raop_device_flush(struct output_device *device, int callback_id) +{ + struct raop_session *rs = device->session; + int ret; + + if (rs->state != RAOP_STATE_STREAMING) + return -1; + + ret = raop_send_req_flush(rs, raop_cb_flush, "flush"); + if (ret < 0) + return -1; + + rs->callback_id = callback_id; + + return 0; } static void -raop_device_stop(struct output_session *session) +raop_device_cb_set(struct output_device *device, int callback_id) { - struct raop_session *rs = session->session; + struct raop_session *rs = device->session; - if (rs->state & RAOP_STATE_F_CONNECTED) - raop_send_req_teardown(rs, raop_cb_shutdown_teardown, "device_stop"); - else - raop_session_cleanup(rs); + rs->callback_id = callback_id; } - static void raop_device_free_extra(struct output_device *device) { @@ -4864,55 +4755,54 @@ raop_device_free_extra(struct output_device *device) } static void -raop_playback_start(uint64_t next_pkt, struct timespec *ts) +raop_write(struct output_buffer *obuf) { + struct raop_master_session *rms; struct raop_session *rs; + int i; - event_del(flush_timer); - evtimer_add(keep_alive_timer, &keep_alive_tv); - - sync_counter = 0; - - for (rs = sessions; rs; rs = rs->next) + for (rms = raop_master_sessions; rms; rms = rms->next) { - if (rs->state == RAOP_STATE_CONNECTED) - rs->state = RAOP_STATE_STREAMING; + for (i = 0; obuf->data[i].buffer; i++) + { + if (!quality_is_equal(&obuf->data[i].quality, &rms->rtp_session->quality)) + continue; + + // Sends sync packets to new sessions, and if it is sync time then also to old sessions + packets_sync_send(rms, obuf->pts); + + // TODO avoid this copy + evbuffer_add(rms->evbuf, obuf->data[i].buffer, obuf->data[i].bufsize); + rms->evbuf_samples += obuf->data[i].samples; + + // Send as many packets as we have data for (one packet requires rawbuf_size bytes) + while (evbuffer_get_length(rms->evbuf) >= rms->rawbuf_size) + { + evbuffer_remove(rms->evbuf, rms->rawbuf, rms->rawbuf_size); + rms->evbuf_samples -= rms->samples_per_packet; + + packets_send(rms); + } + } } - /* Send initial playback sync */ - raop_v2_control_send_sync(next_pkt, ts); -} - -static void -raop_playback_stop(void) -{ - struct raop_session *rs; - int ret; - - evtimer_del(keep_alive_timer); - - for (rs = sessions; rs; rs = rs->next) + // Check for devices that have joined since last write (we have already sent them + // initialization sync and rtp packets via packets_sync_send and packets_send) + for (rs = raop_sessions; rs; rs = rs->next) { - ret = raop_send_req_teardown(rs, raop_cb_shutdown_teardown, "playback_stop"); - if (ret < 0) - DPRINTF(E_LOG, L_RAOP, "playback_stop: TEARDOWN request failed!\n"); + if (rs->state != RAOP_STATE_CONNECTED) + continue; + + rs->state = RAOP_STATE_STREAMING; + // Make a cb? } } -static void -raop_set_status_cb(struct output_session *session, output_status_cb cb) -{ - struct raop_session *rs = session->session; - - rs->status_cb = cb; -} - static int raop_init(void) { char ebuf[64]; char *ptr; - char *libname; gpg_error_t gc_err; int v6enabled; int family; @@ -4930,27 +4820,11 @@ raop_init(void) control_6svc.fd = -1; control_6svc.port = 0; - sessions = NULL; - - pktbuf_size = 0; - pktbuf_head = NULL; - pktbuf_tail = NULL; - - metadata_head = NULL; - metadata_tail = NULL; - - /* Generate RTP SSRC ID from library name */ - libname = cfg_getstr(cfg_getsec(cfg, "library"), "name"); - ssrc_id = djb_hash(libname, strlen(libname)); - - /* Random RTP sequence start */ - gcry_randomize(&stream_seq, sizeof(stream_seq), GCRY_STRONG_RANDOM); - - /* Generate AES key and IV */ + // Generate AES key and IV gcry_randomize(raop_aes_key, sizeof(raop_aes_key), GCRY_STRONG_RANDOM); gcry_randomize(raop_aes_iv, sizeof(raop_aes_iv), GCRY_STRONG_RANDOM); - /* Setup AES */ + // Setup AES gc_err = gcry_cipher_open(&raop_aes_ctx, GCRY_CIPHER_AES, GCRY_CIPHER_MODE_CBC, 0); if (gc_err != GPG_ERR_NO_ERROR) { @@ -4960,7 +4834,7 @@ raop_init(void) return -1; } - /* Set key */ + // Set key gc_err = gcry_cipher_setkey(raop_aes_ctx, raop_aes_key, sizeof(raop_aes_key)); if (gc_err != GPG_ERR_NO_ERROR) { @@ -4970,7 +4844,7 @@ raop_init(void) goto out_close_cipher; } - /* Prepare Base64-encoded key & IV for SDP */ + // Prepare Base64-encoded key & IV for SDP raop_aes_key_b64 = raop_crypt_encrypt_aes_key_base64(); if (!raop_aes_key_b64) { @@ -4987,7 +4861,7 @@ raop_init(void) goto out_free_b64_key; } - /* Remove base64 padding */ + // Remove base64 padding ptr = strchr(raop_aes_key_b64, '='); if (ptr) *ptr = '\0'; @@ -4996,14 +4870,7 @@ raop_init(void) if (ptr) *ptr = '\0'; - flush_timer = evtimer_new(evbase_player, raop_flush_timer_cb, NULL); - keep_alive_timer = evtimer_new(evbase_player, raop_keep_alive_timer_cb, NULL); - if (!flush_timer || !keep_alive_timer) - { - DPRINTF(E_LOG, L_RAOP, "Out of memory for flush timer or keep alive timer\n"); - - goto out_free_b64_iv; - } + CHECK_NULL(L_RAOP, keep_alive_timer = evtimer_new(evbase_player, raop_keep_alive_timer_cb, NULL)); v6enabled = cfg_getbool(cfg_getsec(cfg, "general"), "ipv6"); @@ -5012,7 +4879,7 @@ raop_init(void) { DPRINTF(E_LOG, L_RAOP, "AirPlay time synchronization failed to start\n"); - goto out_free_timers; + goto out_free_timer; } ret = raop_v2_control_start(v6enabled); @@ -5039,17 +4906,14 @@ raop_init(void) goto out_stop_control; } - return 0; out_stop_control: raop_v2_control_stop(); out_stop_timing: raop_v2_timing_stop(); - out_free_timers: - event_free(flush_timer); + out_free_timer: event_free(keep_alive_timer); - out_free_b64_iv: free(raop_aes_iv_b64); out_free_b64_key: free(raop_aes_key_b64); @@ -5064,17 +4928,16 @@ raop_deinit(void) { struct raop_session *rs; - for (rs = sessions; sessions; rs = sessions) + for (rs = raop_sessions; raop_sessions; rs = raop_sessions) { - sessions = rs->next; + raop_sessions = rs->next; - raop_session_free(rs); + session_free(rs); } raop_v2_control_stop(); raop_v2_timing_stop(); - event_free(flush_timer); event_free(keep_alive_timer); gcry_cipher_close(raop_aes_ctx); @@ -5093,19 +4956,16 @@ struct output_definition output_raop = .deinit = raop_deinit, .device_start = raop_device_start, .device_stop = raop_device_stop, + .device_flush = raop_device_flush, .device_probe = raop_device_probe, + .device_cb_set = raop_device_cb_set, .device_free_extra = raop_device_free_extra, .device_volume_set = raop_set_volume_one, .device_volume_to_pct = raop_volume_to_pct, - .playback_start = raop_playback_start, - .playback_stop = raop_playback_stop, - .write = raop_v2_write, - .flush = raop_flush, - .status_cb = raop_set_status_cb, + .write = raop_write, .metadata_prepare = raop_metadata_prepare, .metadata_send = raop_metadata_send, .metadata_purge = raop_metadata_purge, - .metadata_prune = raop_metadata_prune, #ifdef RAOP_VERIFICATION .authorize = raop_verification_setup, #endif diff --git a/src/outputs/rtp_common.c b/src/outputs/rtp_common.c new file mode 100644 index 00000000..cdb9fb80 --- /dev/null +++ b/src/outputs/rtp_common.c @@ -0,0 +1,275 @@ +/* + * Copyright (C) 2019- Espen Jürgensen + * + * 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 +#endif + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#ifdef HAVE_ENDIAN_H +# include +#elif defined(HAVE_SYS_ENDIAN_H) +# include +#elif defined(HAVE_LIBKERN_OSBYTEORDER_H) +#include +#define htobe16(x) OSSwapHostToBigInt16(x) +#define be16toh(x) OSSwapBigToHostInt16(x) +#define htobe32(x) OSSwapHostToBigInt32(x) +#endif + +#include + +#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; +} + diff --git a/src/outputs/rtp_common.h b/src/outputs/rtp_common.h new file mode 100644 index 00000000..217353e4 --- /dev/null +++ b/src/outputs/rtp_common.h @@ -0,0 +1,80 @@ +#ifndef __RTP_COMMON_H__ +#define __RTP_COMMON_H__ + +#include +#include +#include + +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__ */ diff --git a/src/outputs/streaming.c b/src/outputs/streaming.c index 00a963bc..a6d02413 100644 --- a/src/outputs/streaming.c +++ b/src/outputs/streaming.c @@ -24,7 +24,6 @@ #include "outputs.h" #include "httpd_streaming.h" - struct output_definition output_streaming = { .name = "mp3 streaming", diff --git a/src/player.c b/src/player.c index db9b6c22..1054b189 100644 --- a/src/player.c +++ b/src/player.c @@ -1,6 +1,6 @@ /* + * Copyright (C) 2016-2019 Espen Jürgensen * Copyright (C) 2010-2011 Julien BLACHE - * Copyright (C) 2016-2017 Espen Jürgensen * * 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 @@ -33,22 +33,6 @@ * not always obeyed, for instance some outputs do their setup in ways that * could block. * - * - * About metadata - * -------------- - * The player gets metadata from library + inputs and passes it to the outputs - * and other clients (e.g. Remotes). - * - * 1. On playback start, metadata from the library is loaded into the queue - * items, and these items are then the source of metadata for clients. - * 2. During playback, the input may signal new metadata by making a - * input_write() with the INPUT_FLAG_METADATA flag. When the player read - * reaches that data, the player will request the metadata from the input - * with input_metadata_get(). This metadata is then saved to the currently - * playing queue item, and the clients are told to update metadata. - * 3. Artwork works differently than textual metadata. The artwork module will - * look for artwork in the library, and addition also check the artwork_url - * of the queue_item. */ #ifdef HAVE_CONFIG_H @@ -101,22 +85,24 @@ # include "lastfm.h" #endif -#ifndef MIN -# define MIN(a, b) ((a < b) ? a : b) -#endif - -#ifndef MAX -#define MAX(a, b) ((a > b) ? a : b) -#endif - // Default volume (must be from 0 - 100) #define PLAYER_DEFAULT_VOLUME 50 -// For every tick_interval, we will read a packet from the input buffer and + +// The interval between each tick of the playback clock in ms. This means that +// we read 10 ms frames from the input and pass to the output, so the clock +// ticks 100 times a second. We use this value because most common sample rates +// are divisible by 100, and because it keeps delay low. +// TODO sample rates of 22050 might cause underruns, since we would be reading +// only 100 x 220 = 22000 samples each second. +#define PLAYER_TICK_INTERVAL 10 + +// For every tick_interval, we will read a frame from the input buffer and // write it to the outputs. If the input is empty, we will try to catch up next // tick. However, at some point we will owe the outputs so much data that we // have to suspend playback and wait for the input to get its act together. // (value is in milliseconds and should be low enough to avoid output underrun) #define PLAYER_READ_BEHIND_MAX 1500 + // Generally, an output must not block (for long) when outputs_write() is // called. If an output does that anyway, the next tick event will be late, and // by extension playback_cb(). We will try to catch up, but if the delay @@ -124,6 +110,8 @@ // (value is in milliseconds) #define PLAYER_WRITE_BEHIND_MAX 1500 +//#define DEBUG_PLAYER 1 + struct volume_param { int volume; uint64_t spk_id; @@ -152,12 +140,6 @@ struct speaker_get_param struct player_speaker_info *spk_info; }; -struct metadata_param -{ - struct input_metadata *input; - struct output_metadata *output; -}; - struct speaker_auth_param { enum output_types type; @@ -172,6 +154,86 @@ union player_arg int intval; }; +struct player_source +{ + // Id of the file/item in the files database + uint32_t id; + + // Item-Id of the file/item in the queue + uint32_t item_id; + + // Length of the file/item in milliseconds + uint32_t len_ms; + + // Quality of the source (sample rate etc.) + struct media_quality quality; + + enum data_kind data_kind; + enum media_kind media_kind; + char *path; + + // This is the position (measured in samples) the session was at when we + // started reading from the input source. + uint64_t read_start; + + // This is the position (measured in samples) the session was at when we got + // a EOF or error from the input. + uint64_t read_end; + + // Same as the above, but added with samples equivalent to + // OUTPUTS_BUFFER_DURATION. So when the session position reaches play_start it + // means the media should actually be playing on your device. + uint64_t play_start; + uint64_t play_end; + + // The number of milliseconds into the media that we started + uint32_t seek_ms; + // This should at any time match the millisecond position of the media that is + // coming out of your device. Will be 0 during initial buffering. + uint32_t pos_ms; + + // How many samples the outputs buffer before playing (=delay) + int output_buffer_samples; +}; + +struct player_session +{ + uint8_t *buffer; + size_t bufsize; + + // The time the playback session started + struct timespec start_ts; + + // The time the first sample in the buffer should be played by the output, + // without taking output buffer time (OUTPUTS_BUFFER_DURATION) into account. + // It will be equal to: + // pts = start_ts + ticks_elapsed * player_tick_interval + struct timespec pts; + + // Equals current number of samples written to outputs + uint32_t pos; + + // The player sources also have a quality property, but in some situations + // they may get cleared. So we also save it here. + struct media_quality quality; + + // We try to read a fixed number of bytes from the source each clock tick, + // but if it gives us less we increase this correspondingly + size_t read_deficit; + size_t read_deficit_max; + + // The item from the queue being read by the input now, previously and next + struct player_source *reading_now; + struct player_source *reading_next; + struct player_source *reading_prev; + + // The item from the queue being played right now. This will normally point at + // reading_now or reading_prev. It should only be NULL if no playback. + struct player_source *playing_now; +}; + +static struct player_session pb_session; + struct event_base *evbase_player; static int player_exit; @@ -199,36 +261,18 @@ static int pb_timer_fd; timer_t pb_timer; #endif static struct event *pb_timer_ev; -static struct timespec pb_timer_last; -static struct timespec packet_timer_last; -// How often the playback timer triggers playback_cb() -static struct timespec tick_interval; +// Time between ticks, i.e. time between when playback_cb() is invoked +static struct timespec player_tick_interval; // Timer resolution -static struct timespec timer_res; -// Time between two packets -static struct timespec packet_time = { 0, AIRTUNES_V2_STREAM_PERIOD }; +static struct timespec player_timer_res; -// How many writes we owe the output (when the input is underrunning) -static int pb_read_deficit; - -// PLAYER_READ_BEHIND_MAX and PLAYER_WRITE_BEHIND_MAX converted to clock ticks -static int pb_read_deficit_max; +// PLAYER_WRITE_BEHIND_MAX converted to clock ticks static int pb_write_deficit_max; // True if we are trying to recover from a major playback timer overrun (write problems) static bool pb_write_recovery; -// Sync values -static struct timespec pb_pos_stamp; -static uint64_t pb_pos; - -// Stream position (packets) -static uint64_t last_rtptime; - -// Output devices -static struct output_device *dev_list; - // Output status static int output_sessions; @@ -236,15 +280,9 @@ static int output_sessions; static int master_volume; // Audio source -static struct player_source *cur_playing; -static struct player_source *cur_streaming; static uint32_t cur_plid; static uint32_t cur_plversion; -// Player buffer (holds one packet) -static uint8_t pb_buffer[STOB(AIRTUNES_V2_PACKET_SAMPLES)]; -static size_t pb_buffer_offset; - // Play history static struct player_history *history; @@ -252,10 +290,10 @@ static struct player_history *history; /* -------------------------------- Forwards -------------------------------- */ static void -playback_abort(void); +pb_abort(void); static void -playback_suspend(void); +pb_suspend(void); /* ----------------------------- Volume helpers ----------------------------- */ @@ -294,7 +332,7 @@ volume_master_update(int newvol) master_volume = newvol; - for (device = dev_list; device; device = device->next) + for (device = output_device_list; device; device = device->next) { if (device->selected) device->relvol = vol_to_rel(device->volume); @@ -309,7 +347,7 @@ volume_master_find(void) newmaster = -1; - for (device = dev_list; device; device = device->next) + for (device = output_device_list; device; device = device->next) { if (device->selected && (device->volume > newmaster)) newmaster = device->volume; @@ -377,77 +415,29 @@ scrobble_cb(void *arg) } #endif -// Callback from the worker thread. Here the heavy lifting is done: updating the -// db_queue_item, retrieving artwork (through outputs_metadata_prepare) and -// when done, telling the player to send the metadata to the clients -static void -metadata_update_cb(void *arg) +static int +metadata_finalize_cb(struct output_metadata *metadata) { - struct input_metadata *metadata = arg; - struct output_metadata *o_metadata; - struct db_queue_item *queue_item; - int ret; - - queue_item = db_queue_fetch_byitemid(metadata->item_id); - if (!queue_item) + if (!pb_session.playing_now) { - DPRINTF(E_LOG, L_PLAYER, "Bug! Input metadata item_id does not match anything in queue\n"); - goto out_free_metadata; + DPRINTF(E_WARN, L_PLAYER, "Aborting metadata_send(), playback stopped during metadata preparation\n"); + return -1; + } + else if (metadata->item_id != pb_session.playing_now->item_id) + { + DPRINTF(E_WARN, L_PLAYER, "Aborting metadata_send(), item_id changed during metadata preparation (%" PRIu32 " -> %" PRIu32 ")\n", + metadata->item_id, pb_session.playing_now->item_id); + return -1; } - // Update queue item if metadata changed - if (metadata->artist || metadata->title || metadata->album || metadata->genre || metadata->artwork_url || metadata->song_length) - { - // Since we won't be using the metadata struct values for anything else than - // this we just swap pointers - if (metadata->artist) - swap_pointers(&queue_item->artist, &metadata->artist); - if (metadata->title) - swap_pointers(&queue_item->title, &metadata->title); - if (metadata->album) - swap_pointers(&queue_item->album, &metadata->album); - if (metadata->genre) - swap_pointers(&queue_item->genre, &metadata->genre); - if (metadata->artwork_url) - swap_pointers(&queue_item->artwork_url, &metadata->artwork_url); - if (metadata->song_length) - queue_item->song_length = metadata->song_length; + if (!metadata->pos_ms) + metadata->pos_ms = pb_session.playing_now->pos_ms; + if (!metadata->len_ms) + metadata->len_ms = pb_session.playing_now->len_ms; + if (!metadata->pts.tv_sec) + metadata->pts = pb_session.pts; - ret = db_queue_update_item(queue_item); - if (ret < 0) - { - DPRINTF(E_LOG, L_PLAYER, "Database error while updating queue with new metadata\n"); - goto out_free_queueitem; - } - } - - o_metadata = outputs_metadata_prepare(metadata->item_id); - - // Actual sending must be done by player, since the worker does not own the outputs - player_metadata_send(metadata, o_metadata); - - outputs_metadata_free(o_metadata); - - out_free_queueitem: - free_queue_item(queue_item, 0); - - out_free_metadata: - input_metadata_free(metadata, 1); -} - -// Gets the metadata, but since the actual update requires db writes and -// possibly retrieving artwork we let the worker do the next step -static void -metadata_trigger(int startup) -{ - struct input_metadata metadata; - int ret; - - ret = input_metadata_get(&metadata, cur_streaming, startup, last_rtptime + AIRTUNES_V2_PACKET_SAMPLES); - if (ret < 0) - return; - - worker_execute(metadata_update_cb, &metadata, sizeof(metadata), 0); + return 0; } /* @@ -482,16 +472,61 @@ history_add(uint32_t id, uint32_t item_id) static void seek_save(void) { - int seek; + struct player_source *ps = pb_session.playing_now; - if (!cur_streaming) - return; + if (ps && (ps->media_kind & (MEDIA_KIND_MOVIE | MEDIA_KIND_PODCAST | MEDIA_KIND_AUDIOBOOK | MEDIA_KIND_TVSHOW))) + db_file_seek_update(ps->id, ps->pos_ms); +} - if (cur_streaming->media_kind & (MEDIA_KIND_MOVIE | MEDIA_KIND_PODCAST | MEDIA_KIND_AUDIOBOOK | MEDIA_KIND_TVSHOW)) +/* + * Returns the next queue item based on the current streaming source and repeat mode + * + * If repeat mode is repeat all, shuffle is active and the current streaming source is the + * last item in the queue, the queue is reshuffled prior to returning the first item of the + * queue. + */ +static struct db_queue_item * +queue_item_next(uint32_t item_id) +{ + struct db_queue_item *queue_item; + + if (repeat == REPEAT_SONG) { - seek = (cur_streaming->output_start - cur_streaming->stream_start) / 44100 * 1000; - db_file_seek_update(cur_streaming->id, seek); + queue_item = db_queue_fetch_byitemid(item_id); + if (!queue_item) + goto error; } + else + { + queue_item = db_queue_fetch_next(item_id, shuffle); + if (!queue_item && repeat == REPEAT_ALL) + { + if (shuffle) + db_queue_reshuffle(0); + + queue_item = db_queue_fetch_bypos(0, shuffle); + if (!queue_item) + goto error; + } + } + + if (!queue_item) + { + DPRINTF(E_DBG, L_PLAYER, "Reached end of queue\n"); + return NULL; + } + + return queue_item; + + error: + DPRINTF(E_LOG, L_PLAYER, "Error fetching next item from queue (item-id=%" PRIu32 ", repeat=%d)\n", item_id, repeat); + return NULL; +} + +static struct db_queue_item * +queue_item_prev(uint32_t item_id) +{ + return db_queue_fetch_prev(item_id, shuffle); } static void @@ -505,15 +540,6 @@ status_update(enum play_status status) /* ----------- Audio source handling (interfaces with input module) --------- */ -static struct player_source * -source_now_playing() -{ - if (cur_playing) - return cur_playing; - - return cur_streaming; -} - /* * Creates a new player source for the given queue item */ @@ -522,565 +548,514 @@ source_new(struct db_queue_item *queue_item) { struct player_source *ps; - ps = calloc(1, sizeof(struct player_source)); - if (!ps) - { - DPRINTF(E_LOG, L_PLAYER, "Out of memory (ps)\n"); - return NULL; - } + CHECK_NULL(L_PLAYER, ps = calloc(1, sizeof(struct player_source))); ps->id = queue_item->file_id; ps->item_id = queue_item->id; ps->data_kind = queue_item->data_kind; ps->media_kind = queue_item->media_kind; ps->len_ms = queue_item->song_length; - ps->play_next = NULL; ps->path = strdup(queue_item->path); return ps; } static void -source_free(struct player_source *ps) +source_free(struct player_source **ps) { - if (ps->path) - free(ps->path); + if (!(*ps)) + return; - free(ps); + free((*ps)->path); + free(*ps); + + *ps = NULL; } -/* - * Stops playback for the current streaming source and frees all - * player sources (starting from the playing source). Sets current streaming - * and playing sources to NULL. - */ static void -source_stop() +source_stop(void) { - struct player_source *ps_playing; - struct player_source *ps_temp; - - if (cur_streaming) - input_stop(cur_streaming); - - ps_playing = source_now_playing(); - - while (ps_playing) - { - ps_temp = ps_playing; - ps_playing = ps_playing->play_next; - - ps_temp->play_next = NULL; - source_free(ps_temp); - } - - cur_playing = NULL; - cur_streaming = NULL; + input_stop(); } -/* - * Pauses playback - * - * Resets the streaming source to the playing source and adjusts stream-start - * and output-start values to the playing time. Sets the current streaming - * source to NULL. - */ -static int -source_pause(uint64_t pos) +static struct player_source * +source_next_create(struct player_source *current) { - struct player_source *ps_playing; - struct player_source *ps_playnext; - struct player_source *ps_temp; - uint64_t seek_frames; - int seek_ms; - int ret; - - ps_playing = source_now_playing(); - if (!ps_playing) - return -1; - - if (cur_streaming) - { - if (ps_playing != cur_streaming) - { - DPRINTF(E_DBG, L_PLAYER, - "Pause called on playing source (id=%d) and streaming source already " - "switched to the next item (id=%d)\n", ps_playing->id, cur_streaming->id); - ret = input_stop(cur_streaming); - if (ret < 0) - return -1; - } - else - { - ret = input_pause(cur_streaming); - if (ret < 0) - return -1; - } - } - - ps_playnext = ps_playing->play_next; - while (ps_playnext) - { - ps_temp = ps_playnext; - ps_playnext = ps_playnext->play_next; - - ps_temp->play_next = NULL; - source_free(ps_temp); - } - ps_playing->play_next = NULL; - - cur_playing = NULL; - cur_streaming = ps_playing; - - if (!cur_streaming->setup_done) - { - DPRINTF(E_INFO, L_PLAYER, "Opening '%s'\n", cur_streaming->path); - - ret = input_setup(cur_streaming); - if (ret < 0) - { - DPRINTF(E_LOG, L_PLAYER, "Failed to open '%s'\n", cur_streaming->path); - return -1; - } - } - - // Seek back to the pause position - seek_frames = (pos - cur_streaming->stream_start); - seek_ms = (int)((seek_frames * 1000) / 44100); - ret = input_seek(cur_streaming, seek_ms); - -// TODO what if ret < 0? - - // Adjust start_pos to take into account the pause and seek back - cur_streaming->stream_start = last_rtptime + AIRTUNES_V2_PACKET_SAMPLES - ((uint64_t)ret * 44100) / 1000; - cur_streaming->output_start = last_rtptime + AIRTUNES_V2_PACKET_SAMPLES; - cur_streaming->end = 0; - - return 0; -} - -/* - * Seeks the current streaming source to the given postion in milliseconds - * and adjusts stream-start and output-start values. - * - * @param seek_ms Position in milliseconds to seek - * @return The new position in milliseconds or -1 on error - */ -static int -source_seek(int seek_ms) -{ - int ret; - - ret = input_seek(cur_streaming, seek_ms); - if (ret < 0) - return -1; - - // Adjust start_pos to take into account the pause and seek back - cur_streaming->stream_start = last_rtptime + AIRTUNES_V2_PACKET_SAMPLES - ((uint64_t)ret * 44100) / 1000; - cur_streaming->output_start = last_rtptime + AIRTUNES_V2_PACKET_SAMPLES; - - return ret; -} - -/* - * Starts or resumes playback - */ -static int -source_play() -{ - int ret; - - ret = input_start(cur_streaming); - - return ret; -} - -/* - * Opens the given player source for playback (but does not start playback) - * - * The given source is appended to the current streaming source (if one exists) and - * becomes the new current streaming source. - * - * Stream-start and output-start values are set to the given start position. - */ -static int -source_open(struct player_source *ps, uint64_t start_pos, int seek_ms) -{ - int ret; - - DPRINTF(E_INFO, L_PLAYER, "Opening '%s' (id=%d, item-id=%d)\n", ps->path, ps->id, ps->item_id); - - if (cur_streaming && cur_streaming->end == 0) - { - DPRINTF(E_LOG, L_PLAYER, "Current streaming source not at eof '%s' (id=%d, item-id=%d)\n", - cur_streaming->path, cur_streaming->id, cur_streaming->item_id); - return -1; - } - - ret = input_setup(ps); - if (ret < 0) - { - DPRINTF(E_LOG, L_PLAYER, "Failed to open '%s' (id=%d, item-id=%d)\n", ps->path, ps->id, ps->item_id); - return -1; - } - - // If a streaming source exists, append the new source as play-next and set it - // as the new streaming source - if (cur_streaming) - cur_streaming->play_next = ps; - - cur_streaming = ps; - - cur_streaming->stream_start = start_pos; - cur_streaming->output_start = cur_streaming->stream_start; - cur_streaming->end = 0; - - // Seek to the given seek position - if (seek_ms) - { - DPRINTF(E_INFO, L_PLAYER, "Seek to %d ms for '%s' (id=%d, item-id=%d)\n", seek_ms, ps->path, ps->id, ps->item_id); - source_seek(seek_ms); - } - - return ret; -} - -/* - * Closes the current streaming source and sets its end-time to the given - * position - */ -static int -source_close(uint64_t end_pos) -{ - input_stop(cur_streaming); - - cur_streaming->end = end_pos; - - return 0; -} - -/* - * Updates the now playing item (cur_playing) and notifies remotes and raop devices - * about changes. Also takes care of stopping playback after the last item. - * - * @return Returns the current playback position as rtp-time - */ -static uint64_t -source_check(void) -{ - struct timespec ts; struct player_source *ps; - uint64_t pos; - int i; - int id; - int ret; - - ret = player_get_current_pos(&pos, &ts, 0); - if (ret < 0) - { - DPRINTF(E_LOG, L_PLAYER, "Couldn't get current playback position\n"); - - return 0; - } - - if (player_state == PLAY_STOPPED) - { - DPRINTF(E_LOG, L_PLAYER, "Bug! source_check called but playback has already stopped\n"); - - return pos; - } - - // If cur_playing is NULL, we are still in the first two seconds after starting the stream - if (!cur_playing) - { - if (pos >= cur_streaming->output_start) - { - cur_playing = cur_streaming; - status_update(PLAY_PLAYING); - - // Start of streaming, no metadata to prune yet - } - - return pos; - } - - // Check if we are still in the middle of the current playing song - if ((cur_playing->end == 0) || (pos < cur_playing->end)) - return pos; - - // We have reached the end of the current playing song, update cur_playing to - // the next song in the queue and initialize stream_start and output_start values. - - i = 0; - while (cur_playing && (cur_playing->end != 0) && (pos > cur_playing->end)) - { - i++; - - id = (int)cur_playing->id; - - if (id != DB_MEDIA_FILE_NON_PERSISTENT_ID) - { - worker_execute(playcount_inc_cb, &id, sizeof(int), 5); -#ifdef LASTFM - worker_execute(scrobble_cb, &id, sizeof(int), 8); -#endif - history_add(cur_playing->id, cur_playing->item_id); - } - - if (consume) - db_queue_delete_byitemid(cur_playing->item_id); - - if (!cur_playing->play_next) - { - playback_abort(); - return pos; - } - - ps = cur_playing; - cur_playing = cur_playing->play_next; - - source_free(ps); - } - - if (i > 0) - { - DPRINTF(E_DBG, L_PLAYER, "Playback switched to next song\n"); - - status_update(PLAY_PLAYING); - - outputs_metadata_prune(pos); - } - - return pos; -} - -/* - * Returns the next player source based on the current streaming source and repeat mode - * - * If repeat mode is repeat all, shuffle is active and the current streaming source is the - * last item in the queue, the queue is reshuffled prior to returning the first item of the - * queue. - */ -static struct player_source * -source_next() -{ - struct player_source *ps = NULL; struct db_queue_item *queue_item; - if (!cur_streaming) + if (!current) { - DPRINTF(E_LOG, L_PLAYER, "source_next() called with no current streaming source available\n"); + DPRINTF(E_LOG, L_PLAYER, "Bug! source_next_create called without a current source\n"); return NULL; } - if (repeat == REPEAT_SONG) - { - queue_item = db_queue_fetch_byitemid(cur_streaming->item_id); - if (!queue_item) - { - DPRINTF(E_LOG, L_PLAYER, "Error fetching item from queue '%s' (id=%d, item-id=%d)\n", cur_streaming->path, cur_streaming->id, cur_streaming->item_id); - return NULL; - } - } - else - { - queue_item = db_queue_fetch_next(cur_streaming->item_id, shuffle); - if (!queue_item && repeat == REPEAT_ALL) - { - if (shuffle) - { - db_queue_reshuffle(0); - } - - queue_item = db_queue_fetch_bypos(0, shuffle); - if (!queue_item) - { - DPRINTF(E_LOG, L_PLAYER, "Error fetching item from queue '%s' (id=%d, item-id=%d)\n", cur_streaming->path, cur_streaming->id, cur_streaming->item_id); - return NULL; - } - } - } - - if (!queue_item) - { - DPRINTF(E_DBG, L_PLAYER, "Reached end of queue\n"); - return NULL; - } - - ps = source_new(queue_item); - free_queue_item(queue_item, 0); - return ps; -} - -/* - * Returns the previous player source based on the current streaming source - */ -static struct player_source * -source_prev() -{ - struct player_source *ps = NULL; - struct db_queue_item *queue_item; - - if (!cur_streaming) - { - DPRINTF(E_LOG, L_PLAYER, "source_prev() called with no current streaming source available\n"); - return NULL; - } - - queue_item = db_queue_fetch_prev(cur_streaming->item_id, shuffle); + queue_item = queue_item_next(current->item_id); if (!queue_item) return NULL; ps = source_new(queue_item); + free_queue_item(queue_item, 0); return ps; } -static int -source_switch(int nbytes) +static void +source_next(void) { - struct player_source *ps; - int ret; + if (!pb_session.reading_next) + return; - DPRINTF(E_DBG, L_PLAYER, "Switching track\n"); + DPRINTF(E_DBG, L_PLAYER, "Opening next track: '%s' (id=%d)\n", pb_session.reading_next->path, pb_session.reading_next->item_id); - source_close(last_rtptime + AIRTUNES_V2_PACKET_SAMPLES + BTOS(nbytes) - 1); + input_start(pb_session.reading_next->item_id); +} - while ((ps = source_next())) - { - ret = source_open(ps, cur_streaming->end + 1, 0); - if (ret < 0) - { - db_queue_delete_byitemid(ps->item_id); - continue; - } +static int +source_start(void) +{ + short flags; - ret = source_play(); - if (ret < 0) - { - db_queue_delete_byitemid(ps->item_id); - source_close(last_rtptime + AIRTUNES_V2_PACKET_SAMPLES + BTOS(nbytes) - 1); - continue; - } + if (!pb_session.reading_next) + return 0; - break; - } + DPRINTF(E_DBG, L_PLAYER, "(Re)opening track: '%s' (id=%d, seek=%d)\n", pb_session.reading_next->path, pb_session.reading_next->item_id, pb_session.reading_next->seek_ms); - if (!ps) // End of queue - { - cur_streaming = NULL; - return 0; - } + input_flush(&flags); - metadata_trigger(0); - - return 0; + return input_seek(pb_session.reading_next->item_id, (int)pb_session.reading_next->seek_ms); } -/* ----------------- Main read, write and playback timer event -------------- */ +/* ------------------------ Playback session upkeep ------------------------- */ + +// The below update the playback session so it is always in mint condition. That +// is all they do, they should not do anything else. If you are looking for a +// place to add some non session actions, look further down at the events. + +#ifdef DEBUG_PLAYER +static int debug_dump_counter = -1; -// Returns -1 on error (caller should abort playback), or bytes read (possibly 0) static int -source_read(uint8_t *buf, int len) +source_print(char *line, size_t linesize, struct player_source *ps, const char *name) { - int nbytes; - uint32_t item_id; - int ret; - short flags; + int pos = 0; - // Nothing to read, stream silence until source_check() stops playback - if (!cur_streaming) + if (ps) { - memset(buf, 0, len); - return len; + pos += snprintf(line + pos, linesize - pos, "%s.path=%s; ", name, ps->path); + pos += snprintf(line + pos, linesize - pos, "%s.quality=%d; ", name, ps->quality.sample_rate); + pos += snprintf(line + pos, linesize - pos, "%s.item_id=%u; ", name, ps->item_id); + pos += snprintf(line + pos, linesize - pos, "%s.read_start=%lu; ", name, ps->read_start); + pos += snprintf(line + pos, linesize - pos, "%s.play_start=%lu; ", name, ps->play_start); + pos += snprintf(line + pos, linesize - pos, "%s.read_end=%lu; ", name, ps->read_end); + pos += snprintf(line + pos, linesize - pos, "%s.play_end=%lu; ", name, ps->play_end); + pos += snprintf(line + pos, linesize - pos, "%s.pos_ms=%d; ", name, ps->pos_ms); + pos += snprintf(line + pos, linesize - pos, "%s.seek_ms=%d; ", name, ps->seek_ms); } + else + pos += snprintf(line + pos, linesize - pos, "%s=(null); ", name); - nbytes = input_read(buf, len, &flags); - if ((nbytes < 0) || (flags & INPUT_FLAG_ERROR)) - { - DPRINTF(E_LOG, L_PLAYER, "Error reading source %d\n", cur_streaming->id); - - nbytes = 0; - item_id = cur_streaming->item_id; - ret = source_switch(0); - db_queue_delete_byitemid(item_id); - if (ret < 0) - return -1; - } - else if (flags & INPUT_FLAG_EOF) - { - ret = source_switch(nbytes); - if (ret < 0) - return -1; - } - else if (flags & INPUT_FLAG_METADATA) - { - metadata_trigger(0); - } - - // We pad the output buffer with silence if we don't have enough data for a - // full packet and there is no more data coming up (no more tracks in queue) - if ((nbytes < len) && (!cur_streaming)) - { - memset(buf + nbytes, 0, len - nbytes); - nbytes = len; - } - - return nbytes; + return pos; } static void -playback_write(void) +session_dump(bool use_counter) { - int want; - int got; + char line[4096]; + int pos = 0; - source_check(); + if (use_counter) + { + debug_dump_counter++; + if (debug_dump_counter % 100 != 0) + return; + } - // Make sure playback is still running after source_check() - if (player_state == PLAY_STOPPED) + pos += snprintf(line + pos, sizeof(line) - pos, "pos=%d; ", pb_session.pos); + pos += source_print(line + pos, sizeof(line) - pos, pb_session.reading_now, "reading_now"); + + DPRINTF(E_DBG, L_PLAYER, "%s\n", line); + + pos = 0; + pos += snprintf(line + pos, sizeof(line) - pos, "pos=%d; ", pb_session.pos); + pos += source_print(line + pos, sizeof(line) - pos, pb_session.playing_now, "playing_now"); + + DPRINTF(E_DBG, L_PLAYER, "%s\n", line); + + pos = 0; + pos += snprintf(line + pos, sizeof(line) - pos, "pos=%d; ", pb_session.pos); + pos += source_print(line + pos, sizeof(line) - pos, pb_session.reading_prev, "reading_prev"); + + DPRINTF(E_DBG, L_PLAYER, "%s\n", line); + + pos = 0; + pos += snprintf(line + pos, sizeof(line) - pos, "pos=%d; ", pb_session.pos); + pos += source_print(line + pos, sizeof(line) - pos, pb_session.reading_next, "reading_next"); + + DPRINTF(E_DBG, L_PLAYER, "%s\n", line); +} +#endif + +static void +session_update_play_eof(void) +{ + pb_session.playing_now = pb_session.reading_now; +} + +static void +session_update_play_start(void) +{ + pb_session.playing_now = pb_session.reading_now; + + // This is a stupid work-around to make sure pos_ms is non-zero, because a + // zero value means that get_status() tells the clients that we are paused. + pb_session.playing_now->pos_ms = pb_session.playing_now->seek_ms + 1; +} + +static void +session_update_read_next(struct player_source *current) +{ + struct player_source *ps; + + ps = source_next_create(current); + source_free(&pb_session.reading_next); + pb_session.reading_next = ps; +} + +static void +session_update_read_eof(void) +{ + pb_session.reading_now->read_end = pb_session.pos; + pb_session.reading_now->play_end = pb_session.pos + pb_session.reading_now->output_buffer_samples; + + source_free(&pb_session.reading_prev); + pb_session.reading_prev = pb_session.reading_now; + pb_session.reading_now = pb_session.reading_next; + pb_session.reading_next = NULL; + + // There is nothing else to play + if (!pb_session.reading_now) return; - pb_read_deficit++; - while (pb_read_deficit) + // We inherit this because the input will only notify on quality changes, not + // if it is the same as the previous track + pb_session.reading_now->quality = pb_session.reading_prev->quality; + pb_session.reading_now->output_buffer_samples = pb_session.reading_prev->output_buffer_samples; + + pb_session.reading_now->read_start = pb_session.pos; + pb_session.reading_now->play_start = pb_session.pos + pb_session.reading_now->output_buffer_samples; +} + +static void +session_update_read_start(uint32_t seek_ms) +{ + source_free(&pb_session.reading_prev); + pb_session.reading_prev = pb_session.reading_now; + pb_session.reading_now = pb_session.reading_next; + pb_session.reading_next = NULL; + + // There is nothing else to play + if (!pb_session.reading_now) + return; + + pb_session.reading_now->pos_ms = seek_ms; + pb_session.reading_now->seek_ms = seek_ms; + pb_session.reading_now->read_start = pb_session.pos; + + pb_session.playing_now = pb_session.reading_now; +} + +static inline void +session_update_read(int nsamples) +{ + // Did we just complete our first read? Then set the start timestamp + if (pb_session.start_ts.tv_sec == 0) { - want = sizeof(pb_buffer) - pb_buffer_offset; - got = source_read(pb_buffer + pb_buffer_offset, want); - if (got == want) + clock_gettime_with_res(CLOCK_MONOTONIC, &pb_session.start_ts, &player_timer_res); + pb_session.pts = pb_session.start_ts; + } + + // Advance position + pb_session.pos += nsamples; + + // After we have started playing we also must calculate new pos_ms + if (pb_session.playing_now->quality.sample_rate && pb_session.pos > pb_session.playing_now->play_start) + pb_session.playing_now->pos_ms = pb_session.playing_now->seek_ms + 1000UL * (pb_session.pos - pb_session.playing_now->play_start) / pb_session.playing_now->quality.sample_rate; +} + +static void +session_update_read_quality(struct media_quality *quality) +{ + int samples_per_read; + + if (quality_is_equal(quality, &pb_session.quality)) + goto out; + + pb_session.quality = *quality; + pb_session.reading_now->quality = *quality; + + samples_per_read = ((uint64_t)quality->sample_rate * (player_tick_interval.tv_nsec / 1000000)) / 1000; + pb_session.reading_now->output_buffer_samples = OUTPUTS_BUFFER_DURATION * quality->sample_rate; + + pb_session.bufsize = STOB(samples_per_read, quality->bits_per_sample, quality->channels); + pb_session.read_deficit_max = STOB(((uint64_t)quality->sample_rate * PLAYER_READ_BEHIND_MAX) / 1000, quality->bits_per_sample, quality->channels); + + DPRINTF(E_DBG, L_PLAYER, "New session values (q=%d/%d/%d, spr=%d, bufsize=%zu)\n", + quality->sample_rate, quality->bits_per_sample, quality->channels, samples_per_read, pb_session.bufsize); + + if (pb_session.buffer) + pb_session.buffer = realloc(pb_session.buffer, pb_session.bufsize); + else + pb_session.buffer = malloc(pb_session.bufsize); + + CHECK_NULL(L_PLAYER, pb_session.buffer); + + pb_session.reading_now->play_start = pb_session.reading_now->read_start + pb_session.reading_now->output_buffer_samples; + + out: + free(quality); +} + +static void +session_resume(void) +{ + pb_session.start_ts.tv_sec = 0; + pb_session.start_ts.tv_nsec = 0; + pb_session.pts.tv_sec = 0; + pb_session.pts.tv_nsec = 0; + pb_session.read_deficit = 0; +} + +static void +session_stop(void) +{ + free(pb_session.buffer); + pb_session.buffer = NULL; + + source_free(&pb_session.reading_prev); + source_free(&pb_session.reading_now); + source_free(&pb_session.reading_next); + + memset(&pb_session, 0, sizeof(struct player_session)); +} + +static void +session_start(struct player_source *ps, uint32_t seek_ms) +{ + session_stop(); + + // Add the item to play as reading_next + pb_session.reading_next = ps; + pb_session.reading_next->seek_ms = seek_ms; +} + + +/* ------------------------- Playback event handlers ------------------------ */ + +static void +event_read_quality(struct media_quality *quality) +{ + DPRINTF(E_DBG, L_PLAYER, "event_read_quality()\n"); + + session_update_read_quality(quality); +} + +// Stuff to do when read of current track ends +static void +event_read_eof() +{ + DPRINTF(E_DBG, L_PLAYER, "event_read_eof()\n"); + + session_update_read_eof(); +} + +static void +event_read_error() +{ + DPRINTF(E_DBG, L_PLAYER, "event_read_error()\n"); + + db_queue_delete_byitemid(pb_session.reading_now->item_id); + + event_read_eof(); +} + +// Kicks of input reading of next source (async) +static void +event_read_start_next() +{ + DPRINTF(E_DBG, L_PLAYER, "event_read_start_next()\n"); + + // Attaches next item to session as reading_next + session_update_read_next(pb_session.reading_now); + + source_next(); +} + +static void +event_read_metadata(struct input_metadata *metadata) +{ + DPRINTF(E_DBG, L_PLAYER, "event_read_metadata()\n"); + + outputs_metadata_send(pb_session.playing_now->item_id, false, metadata_finalize_cb); + + status_update(player_state); +} + +static void +event_play_end() +{ + DPRINTF(E_DBG, L_PLAYER, "event_play_end()\n"); + + pb_abort(); +} + +// Stuff to do when playback of current track ends +static void +event_play_eof() +{ + DPRINTF(E_DBG, L_PLAYER, "event_play_eof()\n"); + + int id = (int)pb_session.playing_now->id; + + if (id != DB_MEDIA_FILE_NON_PERSISTENT_ID) + { + worker_execute(playcount_inc_cb, &id, sizeof(int), 5); +#ifdef LASTFM + worker_execute(scrobble_cb, &id, sizeof(int), 8); +#endif + history_add(pb_session.playing_now->id, pb_session.playing_now->item_id); + } + + if (consume) + db_queue_delete_byitemid(pb_session.playing_now->item_id); + + if (pb_session.reading_next) + outputs_metadata_send(pb_session.reading_next->item_id, false, metadata_finalize_cb); + + session_update_play_eof(); +} + +static void +event_play_start() +{ + DPRINTF(E_DBG, L_PLAYER, "event_play_start()\n"); + + session_update_play_start(); + + status_update(PLAY_PLAYING); +} + +// Checks if the new playback position requires change of play status, plus +// calls session_update_read that updates playback position +static inline void +event_read(int nsamples) +{ + // Shouldn't happen, playing_now must be set during playback, but check anyway + if (!pb_session.playing_now) + return; + + if (pb_session.playing_now->play_end != 0 && pb_session.pos + nsamples >= pb_session.playing_now->play_end) + { + event_play_eof(); + + if (!pb_session.playing_now) { - pb_read_deficit--; - last_rtptime += AIRTUNES_V2_PACKET_SAMPLES; - outputs_write(pb_buffer, last_rtptime); - pb_buffer_offset = 0; - } - else if (got < 0) - { - DPRINTF(E_LOG, L_PLAYER, "Error reading from source, aborting playback\n"); - playback_abort(); - return; - } - else if (pb_read_deficit > pb_read_deficit_max) - { - DPRINTF(E_LOG, L_PLAYER, "Source is not providing sufficient data, temporarily suspending playback (deficit=%d)\n", pb_read_deficit); - playback_suspend(); - return; - } - else - { - DPRINTF(E_SPAM, L_PLAYER, "Partial read (offset=%zu, deficit=%d)\n", pb_buffer_offset, pb_read_deficit); - pb_buffer_offset += got; + event_play_end(); return; } } + + // Check if the playback position will be passing the play_start position + if (pb_session.pos < pb_session.playing_now->play_start && pb_session.pos + nsamples >= pb_session.playing_now->play_start) + event_play_start(); + + session_update_read(nsamples); +} + + +/* ---- Main playback stuff: Start, read, write and playback timer event ---- */ + +// Returns -1 on error or bytes read (possibly 0) +static inline int +source_read(int *nbytes, int *nsamples, uint8_t *buf, int len) +{ + short flag; + void *flagdata; + + // We can get into this condition if a) we finished reading, but are still + // playing (playing_now is non-null), or b) the calling loop tries to catch up + // with an overrun or a deficit, but playback ended in the first iteration (in + // which case playing_now is null) + if (!pb_session.reading_now) + { + // This is only for case a). If we are in case b) the session was zeroed, + // which means nsamples will become zero. + *nbytes = len; + *nsamples = BTOS(*nbytes, pb_session.quality.bits_per_sample, pb_session.quality.channels); + + // In case a) this advances playback position and possibly ends playback, + // i.e. sets playing_now to null + event_read(*nsamples); + if (!pb_session.playing_now) + { + *nbytes = 0; + *nsamples = 0; + return 0; + } + + // Stream silence if playback didn't end yet + memset(buf, 0, len); + return 0; + } + + *nsamples = 0; + *nbytes = input_read(buf, len, &flag, &flagdata); + if ((*nbytes < 0) || (flag == INPUT_FLAG_ERROR)) + { + DPRINTF(E_LOG, L_PLAYER, "Error reading source '%s' (id=%d)\n", pb_session.reading_now->path, pb_session.reading_now->id); + event_read_error(); + return -1; + } + else if (flag == INPUT_FLAG_START_NEXT) + { + event_read_start_next(); + } + else if (flag == INPUT_FLAG_EOF) + { + event_read_eof(); + } + else if (flag == INPUT_FLAG_METADATA) + { + event_read_metadata((struct input_metadata *)flagdata); + } + else if (flag == INPUT_FLAG_QUALITY) + { + event_read_quality((struct media_quality *)flagdata); + } + + if (*nbytes == 0 || pb_session.quality.channels == 0) + { + event_read(0); // This will set start_ts even if source isn't open yet + return 0; + } + + *nsamples = BTOS(*nbytes, pb_session.quality.bits_per_sample, pb_session.quality.channels); + + event_read(*nsamples); + + return 0; } static void playback_cb(int fd, short what, void *arg) { - struct timespec next_tick; + struct timespec ts; uint64_t overrun; + int nbytes; + int nsamples; + int i; int ret; // Check if we missed any timer expirations @@ -1105,13 +1080,13 @@ playback_cb(int fd, short what, void *arg) if (pb_write_recovery) { DPRINTF(E_LOG, L_PLAYER, "Permanent output delay detected (behind=%" PRIu64 ", max=%d), aborting\n", overrun, pb_write_deficit_max); - playback_abort(); + pb_abort(); return; } DPRINTF(E_LOG, L_PLAYER, "Output delay detected (behind=%" PRIu64 ", max=%d), resetting all outputs\n", overrun, pb_write_deficit_max); pb_write_recovery = true; - playback_suspend(); + pb_suspend(); return; } else @@ -1122,222 +1097,101 @@ playback_cb(int fd, short what, void *arg) pb_write_recovery = false; } +#ifdef DEBUG_PLAYER + session_dump(true); +#endif + + // The pessimistic approach: Assume you won't get anything, then anything that + // comes your way is a positive surprise. + pb_session.read_deficit += (1 + overrun) * pb_session.bufsize; + // If there was an overrun, we will try to read/write a corresponding number // of times so we catch up. The read from the input is non-blocking, so it // should not bring us further behind, even if there is no data. - next_tick = timespec_add(pb_timer_last, tick_interval); - for (; overrun > 0; overrun--) - next_tick = timespec_add(next_tick, tick_interval); - - do + for (i = 1 + overrun; i > 0; i--) { - playback_write(); - packet_timer_last = timespec_add(packet_timer_last, packet_time); + ret = source_read(&nbytes, &nsamples, pb_session.buffer, pb_session.bufsize); + if (ret < 0) + { + DPRINTF(E_LOG, L_PLAYER, "Error reading from source\n"); + pb_session.read_deficit -= pb_session.bufsize; + break; + } + if (nbytes == 0) + { + break; + } + + pb_session.read_deficit -= nbytes; + + outputs_write(pb_session.buffer, nbytes, nsamples, &pb_session.quality, &pb_session.pts); + + if (nbytes < pb_session.bufsize) + { + // How much the number of samples we got corresponds to in time (nanoseconds) + ts.tv_sec = 0; + ts.tv_nsec = 1000000000UL * (uint64_t)nsamples / pb_session.quality.sample_rate; + + DPRINTF(E_DBG, L_PLAYER, "Incomplete read, wanted %zu, got %d (samples=%d/time=%lu), deficit %zu\n", pb_session.bufsize, nbytes, nsamples, ts.tv_nsec, pb_session.read_deficit); + + pb_session.pts = timespec_add(pb_session.pts, ts); + } + else + { + // We got a full frame, so that means we can also advance the presentation timestamp by a full tick + pb_session.pts = timespec_add(pb_session.pts, player_tick_interval); + + // It is going well, lets take another round to repay our debt + if (i == 1 && pb_session.read_deficit > pb_session.bufsize) + i = 2; + } } - while ((timespec_cmp(packet_timer_last, next_tick) < 0) && (player_state == PLAY_PLAYING)); - // Make sure playback is still running - if (player_state == PLAY_STOPPED) - return; + if (pb_session.read_deficit_max && pb_session.read_deficit > pb_session.read_deficit_max) + { + DPRINTF(E_LOG, L_PLAYER, "Source is not providing sufficient data, temporarily suspending playback (deficit=%zu/%zu bytes)\n", + pb_session.read_deficit, pb_session.read_deficit_max); - pb_timer_last = next_tick; + pb_suspend(); + } } /* ----------------- Output device handling (add/remove etc) ---------------- */ -static void -device_list_sort(void) -{ - struct output_device *device; - struct output_device *next; - struct output_device *prev; - int swaps; - - // Swap sorting since even the most inefficient sorting should do fine here - do - { - swaps = 0; - prev = NULL; - for (device = dev_list; device && device->next; device = device->next) - { - next = device->next; - if ( (outputs_priority(device) > outputs_priority(next)) || - (outputs_priority(device) == outputs_priority(next) && strcasecmp(device->name, next->name) > 0) ) - { - if (device == dev_list) - dev_list = next; - if (prev) - prev->next = next; - - device->next = next->next; - next->next = device; - swaps++; - } - prev = device; - } - } - while (swaps > 0); -} - -static void -device_remove(struct output_device *remove) -{ - struct output_device *device; - struct output_device *prev; - int ret; - - prev = NULL; - for (device = dev_list; device; device = device->next) - { - if (device == remove) - break; - - prev = device; - } - - if (!device) - return; - - // Save device volume - ret = db_speaker_save(remove); - if (ret < 0) - DPRINTF(E_LOG, L_PLAYER, "Could not save state for %s device '%s'\n", remove->type_name, remove->name); - - DPRINTF(E_INFO, L_PLAYER, "Removing %s device '%s'; stopped advertising\n", remove->type_name, remove->name); - - // Make sure device isn't selected anymore - if (remove->selected) - speaker_deselect_output(remove); - - if (!prev) - dev_list = remove->next; - else - prev->next = remove->next; - - outputs_device_free(remove); -} - -static int -device_check(struct output_device *check) -{ - struct output_device *device; - - for (device = dev_list; device; device = device->next) - { - if (device == check) - break; - } - - return (device) ? 0 : -1; -} - static enum command_state device_add(void *arg, int *retval) { - union player_arg *cmdarg; - struct output_device *add; - struct output_device *device; - char *keep_name; - int ret; + union player_arg *cmdarg = arg; + struct output_device *device = cmdarg->device; + bool new_deselect; + int default_volume; - cmdarg = arg; - add = cmdarg->device; + // Default volume for new devices + default_volume = (master_volume >= 0) ? master_volume : PLAYER_DEFAULT_VOLUME; - for (device = dev_list; device; device = device->next) - { - if (device->id == add->id) - break; - } + // Never turn on new devices during playback + new_deselect = (player_state == PLAY_PLAYING); - // New device - if (!device) - { - device = add; + device = outputs_device_add(device, new_deselect, default_volume); + *retval = device ? 0 : -1; - keep_name = strdup(device->name); - ret = db_speaker_get(device, device->id); - if (ret < 0) - { - device->selected = 0; - device->volume = (master_volume >= 0) ? master_volume : PLAYER_DEFAULT_VOLUME; - } + if (device && device->selected) + speaker_select_output(device); - free(device->name); - device->name = keep_name; - - if (device->selected && (player_state != PLAY_PLAYING)) - speaker_select_output(device); - else - device->selected = 0; - - device->next = dev_list; - dev_list = device; - } - // Update to a device already in the list - else - { - device->advertised = 1; - - if (add->v4_address) - { - if (device->v4_address) - free(device->v4_address); - - device->v4_address = add->v4_address; - device->v4_port = add->v4_port; - - // Address is ours now - add->v4_address = NULL; - } - - if (add->v6_address) - { - if (device->v6_address) - free(device->v6_address); - - device->v6_address = add->v6_address; - device->v6_port = add->v6_port; - - // Address is ours now - add->v6_address = NULL; - } - - if (device->name) - free(device->name); - device->name = add->name; - add->name = NULL; - - device->has_password = add->has_password; - device->password = add->password; - - outputs_device_free(add); - } - - device_list_sort(); - - listener_notify(LISTENER_SPEAKER); - - *retval = 0; return COMMAND_END; } static enum command_state device_remove_family(void *arg, int *retval) { - union player_arg *cmdarg; + union player_arg *cmdarg = arg; struct output_device *remove; struct output_device *device; - cmdarg = arg; remove = cmdarg->device; - for (device = dev_list; device; device = device->next) - { - if (device->id == remove->id) - break; - } - + device = outputs_device_get(remove->id); if (!device) { DPRINTF(E_WARN, L_PLAYER, "The %s device '%s' stopped advertising, but not in our list\n", remove->type_name, remove->name); @@ -1366,14 +1220,19 @@ device_remove_family(void *arg, int *retval) { device->advertised = 0; + // If there is a session we will keep the device in the list until the + // backend gives us a callback with a failure. Then outputs.c will remove + // the device. If the output backend never gives a callback (can that + // happen?) then the device will never be removed. if (!device->session) - device_remove(device); + { + outputs_device_remove(device); + volume_master_find(); + } } outputs_device_free(remove); - listener_notify(LISTENER_SPEAKER); - *retval = 0; return COMMAND_END; } @@ -1390,36 +1249,12 @@ device_auth_kickoff(void *arg, int *retval) } -static enum command_state -device_metadata_send(void *arg, int *retval) -{ - struct metadata_param *metadata_param = arg; - struct input_metadata *imd; - struct output_metadata *omd; - - imd = metadata_param->input; - omd = metadata_param->output; - - outputs_metadata_send(omd, imd->rtptime, imd->offset, imd->startup); - - status_update(player_state); - - *retval = 0; - return COMMAND_END; -} - - /* -------- Output device callbacks executed in the player thread ----------- */ static void -device_streaming_cb(struct output_device *device, struct output_session *session, enum output_device_state status) +device_streaming_cb(struct output_device *device, enum output_device_state status) { - int ret; - - DPRINTF(E_DBG, L_PLAYER, "Callback from %s to device_streaming_cb\n", outputs_name(device->type)); - - ret = device_check(device); - if (ret < 0) + if (!device) { DPRINTF(E_LOG, L_PLAYER, "Output device disappeared during streaming!\n"); @@ -1427,6 +1262,8 @@ device_streaming_cb(struct output_device *device, struct output_session *session return; } + DPRINTF(E_DBG, L_PLAYER, "Callback from %s to device_streaming_cb (status %d)\n", outputs_name(device->type), status); + if (status == OUTPUT_STATE_FAILED) { DPRINTF(E_LOG, L_PLAYER, "The %s device '%s' FAILED\n", device->type_name, device->name); @@ -1436,40 +1273,54 @@ device_streaming_cb(struct output_device *device, struct output_session *session if (player_state == PLAY_PLAYING) speaker_deselect_output(device); - device->session = NULL; - - if (!device->advertised) - device_remove(device); - if (output_sessions == 0) - playback_abort(); + pb_abort(); } else if (status == OUTPUT_STATE_STOPPED) { DPRINTF(E_INFO, L_PLAYER, "The %s device '%s' stopped\n", device->type_name, device->name); output_sessions--; - - device->session = NULL; - - if (!device->advertised) - device_remove(device); } else - outputs_status_cb(session, device_streaming_cb); + outputs_device_cb_set(device, device_streaming_cb); } static void -device_command_cb(struct output_device *device, struct output_session *session, enum output_device_state status) +device_command_cb(struct output_device *device, enum output_device_state status) { - DPRINTF(E_DBG, L_PLAYER, "Callback from %s to device_command_cb\n", outputs_name(device->type)); + if (!device) + { + DPRINTF(E_LOG, L_PLAYER, "Output device disappeared before command completion!\n"); + goto out; + } - outputs_status_cb(session, device_streaming_cb); + DPRINTF(E_DBG, L_PLAYER, "Callback from %s to device_command_cb (status %d)\n", outputs_name(device->type), status); + + outputs_device_cb_set(device, device_streaming_cb); if (status == OUTPUT_STATE_FAILED) - device_streaming_cb(device, session, status); + device_streaming_cb(device, status); - // Used by playback_suspend - is basically the bottom half + out: + commands_exec_end(cmdbase, 0); +} + +static void +device_flush_cb(struct output_device *device, enum output_device_state status) +{ + if (!device) + { + DPRINTF(E_LOG, L_PLAYER, "Output device disappeared before flush completion!\n"); + goto out; + } + + DPRINTF(E_DBG, L_PLAYER, "Callback from %s to device_flush_cb (status %d)\n", outputs_name(device->type), status); + + if (status == OUTPUT_STATE_FAILED) + device_streaming_cb(device, status); + + // Used by pb_suspend - is basically the bottom half if (player_flush_pending > 0) { player_flush_pending--; @@ -1477,23 +1328,22 @@ device_command_cb(struct output_device *device, struct output_session *session, input_buffer_full_cb(player_playback_start); } + outputs_device_stop_delayed(device, device_streaming_cb); + + out: commands_exec_end(cmdbase, 0); } static void -device_shutdown_cb(struct output_device *device, struct output_session *session, enum output_device_state status) +device_shutdown_cb(struct output_device *device, enum output_device_state status) { int retval; - int ret; - - DPRINTF(E_DBG, L_PLAYER, "Callback from %s to device_shutdown_cb\n", outputs_name(device->type)); if (output_sessions) output_sessions--; retval = commands_exec_returnvalue(cmdbase); - ret = device_check(device); - if (ret < 0) + if (!device) { DPRINTF(E_WARN, L_PLAYER, "Output device disappeared before shutdown completion!\n"); @@ -1502,10 +1352,7 @@ device_shutdown_cb(struct output_device *device, struct output_session *session, goto out; } - device->session = NULL; - - if (!device->advertised) - device_remove(device); + DPRINTF(E_DBG, L_PLAYER, "Callback from %s to device_shutdown_cb (status %d)\n", outputs_name(device->type), status); out: /* cur_cmd->ret already set @@ -1516,40 +1363,22 @@ device_shutdown_cb(struct output_device *device, struct output_session *session, } static void -device_lost_cb(struct output_device *device, struct output_session *session, enum output_device_state status) +device_activate_cb(struct output_device *device, enum output_device_state status) { - DPRINTF(E_DBG, L_PLAYER, "Callback from %s to device_lost_cb\n", outputs_name(device->type)); - - // We lost that device during startup for some reason, not much we can do here - if (status == OUTPUT_STATE_FAILED) - DPRINTF(E_WARN, L_PLAYER, "Failed to stop lost device\n"); - else - DPRINTF(E_INFO, L_PLAYER, "Lost device stopped properly\n"); -} - -static void -device_activate_cb(struct output_device *device, struct output_session *session, enum output_device_state status) -{ - struct timespec ts; int retval; - int ret; - - DPRINTF(E_DBG, L_PLAYER, "Callback from %s to device_activate_cb\n", outputs_name(device->type)); retval = commands_exec_returnvalue(cmdbase); - ret = device_check(device); - if (ret < 0) + if (!device) { DPRINTF(E_WARN, L_PLAYER, "Output device disappeared during startup!\n"); - outputs_status_cb(session, device_lost_cb); - outputs_device_stop(session); - if (retval != -2) retval = -1; goto out; } + DPRINTF(E_DBG, L_PLAYER, "Callback from %s to device_activate_cb (status %d)\n", outputs_name(device->type), status); + if (status == OUTPUT_STATE_PASSWORD) { status = OUTPUT_STATE_FAILED; @@ -1560,34 +1389,14 @@ device_activate_cb(struct output_device *device, struct output_session *session, { speaker_deselect_output(device); - if (!device->advertised) - device_remove(device); - if (retval != -2) retval = -1; goto out; } - device->session = session; - output_sessions++; - if ((player_state == PLAY_PLAYING) && (output_sessions == 1)) - { - ret = clock_gettime_with_res(CLOCK_MONOTONIC, &ts, &timer_res); - if (ret < 0) - { - DPRINTF(E_LOG, L_PLAYER, "Could not get current time: %s\n", strerror(errno)); - - // Fallback to nearest timer expiration time - ts.tv_sec = pb_timer_last.tv_sec; - ts.tv_nsec = pb_timer_last.tv_nsec; - } - - outputs_playback_start(last_rtptime + AIRTUNES_V2_PACKET_SAMPLES, &ts); - } - - outputs_status_cb(session, device_streaming_cb); + outputs_device_cb_set(device, device_streaming_cb); out: /* cur_cmd->ret already set @@ -1599,16 +1408,12 @@ device_activate_cb(struct output_device *device, struct output_session *session, } static void -device_probe_cb(struct output_device *device, struct output_session *session, enum output_device_state status) +device_probe_cb(struct output_device *device, enum output_device_state status) { int retval; - int ret; - - DPRINTF(E_DBG, L_PLAYER, "Callback from %s to device_probe_cb\n", outputs_name(device->type)); retval = commands_exec_returnvalue(cmdbase); - ret = device_check(device); - if (ret < 0) + if (!device) { DPRINTF(E_WARN, L_PLAYER, "Output device disappeared during probe!\n"); @@ -1617,6 +1422,8 @@ device_probe_cb(struct output_device *device, struct output_session *session, en goto out; } + DPRINTF(E_DBG, L_PLAYER, "Callback from %s to device_probe_cb (status %d)\n", outputs_name(device->type), status); + if (status == OUTPUT_STATE_PASSWORD) { status = OUTPUT_STATE_FAILED; @@ -1627,9 +1434,6 @@ device_probe_cb(struct output_device *device, struct output_session *session, en { speaker_deselect_output(device); - if (!device->advertised) - device_remove(device); - if (retval != -2) retval = -1; goto out; @@ -1645,27 +1449,22 @@ device_probe_cb(struct output_device *device, struct output_session *session, en } static void -device_restart_cb(struct output_device *device, struct output_session *session, enum output_device_state status) +device_restart_cb(struct output_device *device, enum output_device_state status) { int retval; - int ret; - - DPRINTF(E_DBG, L_PLAYER, "Callback from %s to device_restart_cb\n", outputs_name(device->type)); retval = commands_exec_returnvalue(cmdbase); - ret = device_check(device); - if (ret < 0) + if (!device) { DPRINTF(E_WARN, L_PLAYER, "Output device disappeared during restart!\n"); - outputs_status_cb(session, device_lost_cb); - outputs_device_stop(session); - if (retval != -2) retval = -1; goto out; } + DPRINTF(E_DBG, L_PLAYER, "Callback from %s to device_restart_cb (status %d)\n", outputs_name(device->type), status); + if (status == OUTPUT_STATE_PASSWORD) { status = OUTPUT_STATE_FAILED; @@ -1676,32 +1475,51 @@ device_restart_cb(struct output_device *device, struct output_session *session, { speaker_deselect_output(device); - if (!device->advertised) - device_remove(device); - if (retval != -2) retval = -1; goto out; } - device->session = session; - output_sessions++; - outputs_status_cb(session, device_streaming_cb); + outputs_device_cb_set(device, device_streaming_cb); out: commands_exec_end(cmdbase, retval); } +const char * +player_pmap(void *p) +{ + if (p == device_restart_cb) + return "device_restart_cb"; + else if (p == device_probe_cb) + return "device_probe_cb"; + else if (p == device_activate_cb) + return "device_activate_cb"; + else if (p == device_streaming_cb) + return "device_streaming_cb"; + else if (p == device_command_cb) + return "device_command_cb"; + else if (p == device_flush_cb) + return "device_flush_cb"; + else if (p == device_shutdown_cb) + return "device_shutdown_cb"; + else + return "unknown"; +} /* ------------------------- Internal playback routines --------------------- */ static int -playback_timer_start(void) +pb_timer_start(void) { struct itimerspec tick; int ret; + // The stop timers will be active if we have recently paused, but now that the + // playback loop has been kicked off, we deactivate them + outputs_stop_delayed_cancel(); + ret = event_add(pb_timer_ev, NULL); if (ret < 0) { @@ -1710,8 +1528,8 @@ playback_timer_start(void) return -1; } - tick.it_interval = tick_interval; - tick.it_value = tick_interval; + tick.it_interval = player_tick_interval; + tick.it_value = player_tick_interval; #ifdef HAVE_TIMERFD ret = timerfd_settime(pb_timer_fd, 0, &tick, NULL); @@ -1729,7 +1547,7 @@ playback_timer_start(void) } static int -playback_timer_stop(void) +pb_timer_stop(void) { struct itimerspec tick; int ret; @@ -1746,38 +1564,92 @@ playback_timer_stop(void) if (ret < 0) { DPRINTF(E_LOG, L_PLAYER, "Could not disarm playback timer: %s\n", strerror(errno)); - return -1; } return 0; } -static void -playback_abort(void) +// Initiates the session and starts the input source +static int +pb_session_start(struct db_queue_item *queue_item, uint32_t seek_ms) { - outputs_playback_stop(); + struct player_source *ps; + uint32_t item_id; + int ret; - playback_timer_stop(); + ps = source_new(queue_item); + + // Clears the session and attaches the new source as reading_next + session_start(ps, seek_ms); + + // Sets of opening of the new source + while ( (ret = source_start()) < 0) + { + // Couldn't start requested item, skip to next and remove failed item from queue + item_id = pb_session.reading_next->item_id; + session_update_read_next(pb_session.reading_next); + db_queue_delete_byitemid(item_id); + } + + session_update_read_start((uint32_t)ret); + + if (!pb_session.playing_now) + return -1; + + return ret; +} + +// Stops input source and stops read loop +static void +pb_session_pause(void) +{ + pb_timer_stop(); + + source_stop(); +} + +// Stops input source and deallocates pb_session content +static void +pb_session_stop(void) +{ + pb_timer_stop(); source_stop(); - if (!clear_queue_on_stop_disabled) - db_queue_clear(0); + session_stop(); status_update(PLAY_STOPPED); +} +static void +pb_abort(void) +{ + // Immediate stop of all outputs + outputs_stop(device_streaming_cb); outputs_metadata_purge(); + + pb_session_stop(); + + if (!clear_queue_on_stop_disabled) + db_queue_clear(0); +} + +// Resets session start timestamp and deficits, which is necessary after pb_suspend +static void +pb_resume() +{ + session_resume(); } // Temporarily suspends/resets playback, used when input buffer underruns or in // case of problems writing to the outputs static void -playback_suspend(void) +pb_suspend(void) { - player_flush_pending = outputs_flush(device_command_cb, last_rtptime + AIRTUNES_V2_PACKET_SAMPLES); + player_flush_pending = outputs_flush(device_flush_cb); - playback_timer_stop(); + pb_timer_stop(); status_update(PLAY_PAUSED); @@ -1795,10 +1667,6 @@ static enum command_state get_status(void *arg, int *retval) { struct player_status *status = arg; - struct timespec ts; - struct player_source *ps; - uint64_t pos; - int ret; memset(status, 0, sizeof(struct player_status)); @@ -1815,59 +1683,40 @@ get_status(void *arg, int *retval) case PLAY_STOPPED: DPRINTF(E_DBG, L_PLAYER, "Player status: stopped\n"); - status->status = PLAY_STOPPED; + status->status = PLAY_STOPPED; break; case PLAY_PAUSED: DPRINTF(E_DBG, L_PLAYER, "Player status: paused\n"); - status->status = PLAY_PAUSED; - status->id = cur_streaming->id; - status->item_id = cur_streaming->item_id; + status->status = PLAY_PAUSED; + status->id = pb_session.playing_now->id; + status->item_id = pb_session.playing_now->item_id; - pos = last_rtptime + AIRTUNES_V2_PACKET_SAMPLES - cur_streaming->stream_start; - status->pos_ms = (pos * 1000) / 44100; - status->len_ms = cur_streaming->len_ms; + status->pos_ms = pb_session.playing_now->pos_ms; + status->len_ms = pb_session.playing_now->len_ms; break; case PLAY_PLAYING: - if (!cur_playing) + if (pb_session.playing_now->pos_ms == 0) { DPRINTF(E_DBG, L_PLAYER, "Player status: playing (buffering)\n"); status->status = PLAY_PAUSED; - ps = cur_streaming; - - // Avoid a visible 2-second jump backward for the client - pos = ps->output_start - ps->stream_start; } else { DPRINTF(E_DBG, L_PLAYER, "Player status: playing\n"); status->status = PLAY_PLAYING; - ps = cur_playing; - - ret = player_get_current_pos(&pos, &ts, 0); - if (ret < 0) - { - DPRINTF(E_LOG, L_PLAYER, "Could not get current stream position for playstatus\n"); - - pos = 0; - } - - if (pos < ps->stream_start) - pos = 0; - else - pos -= ps->stream_start; } - status->pos_ms = (pos * 1000) / 44100; - status->len_ms = ps->len_ms; + status->id = pb_session.playing_now->id; + status->item_id = pb_session.playing_now->item_id; - status->id = ps->id; - status->item_id = ps->item_id; + status->pos_ms = pb_session.playing_now->pos_ms; + status->len_ms = pb_session.playing_now->len_ms; break; } @@ -1877,21 +1726,18 @@ get_status(void *arg, int *retval) } static enum command_state -now_playing(void *arg, int *retval) +playing_now(void *arg, int *retval) { uint32_t *id = arg; - struct player_source *ps_playing; - ps_playing = source_now_playing(); - - if (ps_playing) - *id = ps_playing->id; - else + if (player_state == PLAY_STOPPED) { *retval = -1; return COMMAND_END; } + *id = pb_session.playing_now->id; + *retval = 0; return COMMAND_END; } @@ -1899,32 +1745,25 @@ now_playing(void *arg, int *retval) static enum command_state playback_stop(void *arg, int *retval) { - struct player_source *ps_playing; - if (player_state == PLAY_STOPPED) { *retval = 0; return COMMAND_END; } + if (pb_session.playing_now && pb_session.playing_now->pos_ms > 0) + history_add(pb_session.playing_now->id, pb_session.playing_now->item_id); + // We may be restarting very soon, so we don't bring the devices to a full // stop just yet; this saves time when restarting, which is nicer for the user - *retval = outputs_flush(device_command_cb, last_rtptime + AIRTUNES_V2_PACKET_SAMPLES); + *retval = outputs_flush(device_flush_cb); + outputs_metadata_purge(); - playback_timer_stop(); - - ps_playing = source_now_playing(); - if (ps_playing) - { - history_add(ps_playing->id, ps_playing->item_id); - } - - source_stop(); + // Stops the input + pb_session_stop(); status_update(PLAY_STOPPED); - outputs_metadata_purge(); - // We're async if we need to flush devices if (*retval > 0) return COMMAND_PENDING; @@ -1932,47 +1771,33 @@ playback_stop(void *arg, int *retval) return COMMAND_END; } +static enum command_state +playback_abort(void *arg, int *retval) +{ + pb_abort(); + + *retval = 0; + return COMMAND_END; +} + static enum command_state playback_start_bh(void *arg, int *retval) { int ret; - ret = clock_gettime_with_res(CLOCK_MONOTONIC, &pb_pos_stamp, &timer_res); + ret = pb_timer_start(); if (ret < 0) - { - DPRINTF(E_LOG, L_PLAYER, "Couldn't get current clock: %s\n", strerror(errno)); + goto error; - goto out_fail; - } - - playback_timer_stop(); - - // initialize the packet timer to the same relative time that we have - // for the playback timer. - packet_timer_last.tv_sec = pb_pos_stamp.tv_sec; - packet_timer_last.tv_nsec = pb_pos_stamp.tv_nsec; - - pb_timer_last.tv_sec = pb_pos_stamp.tv_sec; - pb_timer_last.tv_nsec = pb_pos_stamp.tv_nsec; - - pb_buffer_offset = 0; - pb_read_deficit = 0; - - ret = playback_timer_start(); - if (ret < 0) - goto out_fail; - - // Everything OK, start outputs - outputs_playback_start(last_rtptime + AIRTUNES_V2_PACKET_SAMPLES, &pb_pos_stamp); + outputs_metadata_send(pb_session.playing_now->item_id, true, metadata_finalize_cb); status_update(PLAY_PLAYING); *retval = 0; return COMMAND_END; - out_fail: - playback_abort(); - + error: + pb_abort(); *retval = -1; return COMMAND_END; } @@ -1984,7 +1809,7 @@ playback_start_item(void *arg, int *retval) struct media_file_info *mfi; struct output_device *device; struct player_source *ps; - int seek_ms; + uint32_t seek_ms; int ret; if (player_state == PLAY_PLAYING) @@ -1997,9 +1822,6 @@ playback_start_item(void *arg, int *retval) return COMMAND_END; } - // Update global playback position - pb_pos = last_rtptime + AIRTUNES_V2_PACKET_SAMPLES - 88200; - if (player_state == PLAY_STOPPED && !queue_item) { DPRINTF(E_LOG, L_PLAYER, "Failed to start/resume playback, no queue item given\n"); @@ -2010,24 +1832,24 @@ playback_start_item(void *arg, int *retval) if (!queue_item) { - // Resume playback of current source - ps = source_now_playing(); + ps = pb_session.playing_now; + if (!ps) + { + DPRINTF(E_WARN, L_PLAYER, "Bug! playing_now is null but playback is not stopped!\n"); + *retval = -1; + return COMMAND_END; + } + DPRINTF(E_DBG, L_PLAYER, "Resume playback of '%s' (id=%d, item-id=%d)\n", ps->path, ps->id, ps->item_id); + + pb_resume(); } else { // Start playback for given queue item DPRINTF(E_DBG, L_PLAYER, "Start playback of '%s' (id=%d, item-id=%d)\n", queue_item->path, queue_item->file_id, queue_item->id); - source_stop(); - - ps = source_new(queue_item); - if (!ps) - { - playback_abort(); - *retval = -1; - return COMMAND_END; - } + // Look up where we should start seek_ms = 0; if (queue_item->file_id > 0) { @@ -2039,34 +1861,22 @@ playback_start_item(void *arg, int *retval) } } - ret = source_open(ps, last_rtptime + AIRTUNES_V2_PACKET_SAMPLES, seek_ms); + ret = pb_session_start(queue_item, seek_ms); if (ret < 0) { - playback_abort(); - source_free(ps); *retval = -1; return COMMAND_END; } } - ret = source_play(); - if (ret < 0) - { - playback_abort(); - *retval = -1; - return COMMAND_END; - } - - metadata_trigger(1); - // Start sessions on selected devices *retval = 0; - for (device = dev_list; device; device = device->next) + for (device = output_device_list; device; device = device->next) { if (device->selected && !device->session) { - ret = outputs_device_start(device, device_restart_cb, last_rtptime + AIRTUNES_V2_PACKET_SAMPLES); + ret = outputs_device_start(device, device_restart_cb); if (ret < 0) { DPRINTF(E_LOG, L_PLAYER, "Could not start selected %s device '%s'\n", device->type_name, device->name); @@ -2080,13 +1890,13 @@ playback_start_item(void *arg, int *retval) // If autoselecting is enabled, try to autoselect a non-selected device if the above failed if (speaker_autoselect && (*retval == 0) && (output_sessions == 0)) - for (device = dev_list; device; device = device->next) + for (device = output_device_list; device; device = device->next) { if ((outputs_priority(device) == 0) || device->session) continue; speaker_select_output(device); - ret = outputs_device_start(device, device_restart_cb, last_rtptime + AIRTUNES_V2_PACKET_SAMPLES); + ret = outputs_device_start(device, device_restart_cb); if (ret < 0) { DPRINTF(E_DBG, L_PLAYER, "Could not autoselect %s device '%s'\n", device->type_name, device->name); @@ -2164,70 +1974,38 @@ playback_start(void *arg, int *retval) static enum command_state playback_prev_bh(void *arg, int *retval) { + struct db_queue_item *queue_item; int ret; - int pos_sec; - struct player_source *ps; - // The upper half is playback_pause, therefor the current playing item is - // already set as the cur_streaming (cur_playing is NULL). - if (!cur_streaming) + // outputs_flush() in playback_pause() may have a caused a failure callback + // from the output, which in streaming_cb() can cause pb_abort() + if (player_state == PLAY_STOPPED) { - DPRINTF(E_LOG, L_PLAYER, "Could not get current stream source\n"); - *retval = -1; - return COMMAND_END; + goto error; } // Only add to history if playback started - if (cur_streaming->output_start > cur_streaming->stream_start) - history_add(cur_streaming->id, cur_streaming->item_id); - - // Compute the playing time in seconds for the current song - if (cur_streaming->output_start > cur_streaming->stream_start) - pos_sec = (cur_streaming->output_start - cur_streaming->stream_start) / 44100; - else - pos_sec = 0; + if (pb_session.playing_now->pos_ms > 0) + history_add(pb_session.playing_now->id, pb_session.playing_now->item_id); // Only skip to the previous song if the playing time is less than 3 seconds, // otherwise restart the current song. - DPRINTF(E_DBG, L_PLAYER, "Skipping song played %d sec\n", pos_sec); - if (pos_sec < 3) - { - ps = source_prev(); - if (!ps) - { - playback_abort(); - *retval = -1; - return COMMAND_END; - } - - source_stop(); - - ret = source_open(ps, last_rtptime + AIRTUNES_V2_PACKET_SAMPLES, 0); - if (ret < 0) - { - source_free(ps); - playback_abort(); - - *retval = -1; - return COMMAND_END; - } - } + if (pb_session.playing_now->pos_ms < 3000) + queue_item = queue_item_prev(pb_session.playing_now->item_id); else + queue_item = db_queue_fetch_byitemid(pb_session.playing_now->item_id); + if (!queue_item) { - ret = source_seek(0); - if (ret < 0) - { - playback_abort(); - - *retval = -1; - return COMMAND_END; - } + DPRINTF(E_DBG, L_PLAYER, "Error finding previous source, queue item has disappeared\n"); + goto error; } - if (player_state == PLAY_STOPPED) + ret = pb_session_start(queue_item, 0); + free_queue_item(queue_item, 0); + if (ret < 0) { - *retval = -1; - return COMMAND_END; + DPRINTF(E_DBG, L_PLAYER, "Error skipping to previous item, aborting playback\n"); + goto error; } // Silent status change - playback_start() sends the real status update @@ -2235,90 +2013,93 @@ playback_prev_bh(void *arg, int *retval) *retval = 0; return COMMAND_END; + + error: + pb_abort(); + *retval = -1; + return COMMAND_END; } static enum command_state playback_next_bh(void *arg, int *retval) { - struct player_source *ps; + struct db_queue_item *queue_item; int ret; int id; - uint32_t item_id; - // The upper half is playback_pause, therefor the current playing item is - // already set as the cur_streaming (cur_playing is NULL). - if (!cur_streaming) + // outputs_flush() in playback_pause() may have a caused a failure callback + // from the output, which in streaming_cb() can cause pb_abort() + if (player_state == PLAY_STOPPED) { - DPRINTF(E_LOG, L_PLAYER, "Could not get current stream source\n"); - *retval = -1; - return COMMAND_END; + goto error; } - item_id = cur_streaming->item_id; - // Only add to history if playback started - if (cur_streaming->output_start > cur_streaming->stream_start) + if (pb_session.playing_now->pos_ms > 0) { - history_add(cur_streaming->id, item_id); + history_add(pb_session.playing_now->id, pb_session.playing_now->item_id); - id = (int)cur_streaming->id; + id = (int)(pb_session.playing_now->id); worker_execute(skipcount_inc_cb, &id, sizeof(int), 5); } - ps = source_next(); - if (!ps) + if (consume) + db_queue_delete_byitemid(pb_session.playing_now->item_id); + + queue_item = queue_item_next(pb_session.playing_now->item_id); + if (!queue_item) { - playback_abort(); - *retval = -1; - return COMMAND_END; + DPRINTF(E_DBG, L_PLAYER, "Error finding next source, queue item has disappeared\n"); + goto error; } - source_stop(); - - ret = source_open(ps, last_rtptime + AIRTUNES_V2_PACKET_SAMPLES, 0); + ret = pb_session_start(queue_item, 0); + free_queue_item(queue_item, 0); if (ret < 0) { - source_free(ps); - playback_abort(); - *retval = -1; - return COMMAND_END; + DPRINTF(E_DBG, L_PLAYER, "Error skipping to next item, aborting playback\n"); + goto error; } - if (player_state == PLAY_STOPPED) - { - *retval = -1; - return COMMAND_END; - } - - if (consume) - db_queue_delete_byitemid(item_id); - // Silent status change - playback_start() sends the real status update player_state = PLAY_PAUSED; *retval = 0; return COMMAND_END; + + error: + pb_abort(); + *retval = -1; + return COMMAND_END; } static enum command_state playback_seek_bh(void *arg, int *retval) { + struct db_queue_item *queue_item; union player_arg *cmdarg = arg; - int ms; int ret; - *retval = -1; + // outputs_flush() in playback_pause() may have a caused a failure callback + // from the output, which in streaming_cb() can cause pb_abort() + if (player_state == PLAY_STOPPED) + { + goto error; + } - if (!cur_streaming) - return COMMAND_END; + queue_item = db_queue_fetch_byitemid(pb_session.playing_now->item_id); + if (!queue_item) + { + DPRINTF(E_DBG, L_PLAYER, "Error seeking in source, queue item has disappeared\n"); + goto error; + } - ms = cmdarg->intval; - - ret = source_seek(ms); + ret = pb_session_start(queue_item, cmdarg->intval); + free_queue_item(queue_item, 0); if (ret < 0) { - playback_abort(); - return COMMAND_END; + DPRINTF(E_DBG, L_PLAYER, "Error seeking to %d, aborting playback\n", cmdarg->intval); + goto error; } // Silent status change - playback_start() sends the real status update @@ -2326,39 +2107,57 @@ playback_seek_bh(void *arg, int *retval) *retval = 0; return COMMAND_END; + + error: + pb_abort(); + *retval = -1; + return COMMAND_END; } static enum command_state playback_pause_bh(void *arg, int *retval) { - *retval = -1; + struct db_queue_item *queue_item; + int ret; // outputs_flush() in playback_pause() may have a caused a failure callback - // from the output, which in streaming_cb() can cause playback_abort() -> - // cur_streaming is NULL - if (!cur_streaming) - return COMMAND_END; - - if (cur_streaming->data_kind == DATA_KIND_HTTP || cur_streaming->data_kind == DATA_KIND_PIPE) + // from the output, which in streaming_cb() can cause pb_abort() + if (player_state == PLAY_STOPPED) { - DPRINTF(E_DBG, L_PLAYER, "Source is not pausable, abort playback\n"); - - playback_abort(); - return COMMAND_END; + goto error; } + + queue_item = db_queue_fetch_byitemid(pb_session.playing_now->item_id); + if (!queue_item) + { + DPRINTF(E_DBG, L_PLAYER, "Error pausing source, queue item has disappeared\n"); + goto error; + } + + ret = pb_session_start(queue_item, pb_session.playing_now->pos_ms); + free_queue_item(queue_item, 0); + if (ret < 0) + { + DPRINTF(E_DBG, L_PLAYER, "Error pausing source, aborting playback\n"); + goto error; + } + status_update(PLAY_PAUSED); seek_save(); *retval = 0; return COMMAND_END; + + error: + pb_abort(); + *retval = -1; + return COMMAND_END; } static enum command_state playback_pause(void *arg, int *retval) { - uint64_t pos; - if (player_state == PLAY_STOPPED) { *retval = -1; @@ -2371,29 +2170,9 @@ playback_pause(void *arg, int *retval) return COMMAND_END; } - pos = source_check(); - if (pos == 0) - { - DPRINTF(E_LOG, L_PLAYER, "Could not retrieve current position for pause\n"); - - playback_abort(); - *retval = -1; - return COMMAND_END; - } - - // Make sure playback is still running after source_check() - if (player_state == PLAY_STOPPED) - { - *retval = -1; - return COMMAND_END; - } - - *retval = outputs_flush(device_command_cb, last_rtptime + AIRTUNES_V2_PACKET_SAMPLES); - - playback_timer_stop(); - - source_pause(pos); + pb_session_pause(); + *retval = outputs_flush(device_flush_cb); outputs_metadata_purge(); // We're async if we need to flush devices @@ -2404,15 +2183,6 @@ playback_pause(void *arg, int *retval) return COMMAND_END; } -/* - * Notify of speaker/device changes - */ -void -player_speaker_status_trigger(void) -{ - listener_notify(LISTENER_SPEAKER); -} - static void device_to_speaker_info(struct player_speaker_info *spk, struct output_device *device) { @@ -2439,13 +2209,10 @@ speaker_enumerate(void *arg, int *retval) struct output_device *device; struct player_speaker_info spk; - for (device = dev_list; device; device = device->next) + for (device = output_device_list; device; device = device->next) { - if (device->advertised || device->selected) - { - device_to_speaker_info(&spk, device); - spk_enum->cb(&spk, spk_enum->arg); - } + device_to_speaker_info(&spk, device); + spk_enum->cb(&spk, spk_enum->arg); } *retval = 0; @@ -2458,7 +2225,7 @@ speaker_get_byid(void *arg, int *retval) struct speaker_get_param *spk_param = arg; struct output_device *device; - for (device = dev_list; device; device = device->next) + for (device = output_device_list; device; device = device->next) { if ((device->advertised || device->selected) && device->id == spk_param->spk_id) @@ -2498,7 +2265,7 @@ speaker_activate(struct output_device *device) { DPRINTF(E_DBG, L_PLAYER, "Activating %s device '%s'\n", device->type_name, device->name); - ret = outputs_device_start(device, device_activate_cb, last_rtptime + AIRTUNES_V2_PACKET_SAMPLES); + ret = outputs_device_start(device, device_activate_cb); if (ret < 0) { DPRINTF(E_LOG, L_PLAYER, "Could not start %s device '%s'\n", device->type_name, device->name); @@ -2536,8 +2303,7 @@ speaker_deactivate(struct output_device *device) if (!device->session) return 0; - outputs_status_cb(device->session, device_shutdown_cb); - outputs_device_stop(device->session); + outputs_device_stop(device, device_shutdown_cb); return 1; } @@ -2563,7 +2329,7 @@ speaker_set(void *arg, int *retval) *retval = 0; - for (device = dev_list; device; device = device->next) + for (device = output_device_list; device; device = device->next) { for (i = 1; i <= nspk; i++) { @@ -2604,20 +2370,13 @@ speaker_enable(void *arg, int *retval) uint64_t *id = arg; struct output_device *device; - *retval = 0; + device = outputs_device_get(*id); + if (!device) + return COMMAND_END; - DPRINTF(E_DBG, L_PLAYER, "Speaker enable: %" PRIu64 "\n", *id); + DPRINTF(E_DBG, L_PLAYER, "Speaker enable: '%s' (id=%" PRIu64 ")\n", device->name, *id); - *retval = 0; - - for (device = dev_list; device; device = device->next) - { - if (*id == device->id) - { - *retval = speaker_activate(device); - break; - } - } + *retval = speaker_activate(device); if (*retval > 0) return COMMAND_PENDING; // async @@ -2631,20 +2390,13 @@ speaker_disable(void *arg, int *retval) uint64_t *id = arg; struct output_device *device; - *retval = 0; + device = outputs_device_get(*id); + if (!device) + return COMMAND_END; - DPRINTF(E_DBG, L_PLAYER, "Speaker disable: %" PRIu64 "\n", *id); + DPRINTF(E_DBG, L_PLAYER, "Speaker disable: '%s' (id=%" PRIu64 ")\n", device->name, *id); - *retval = 0; - - for (device = dev_list; device; device = device->next) - { - if (*id == device->id) - { - *retval = speaker_deactivate(device); - break; - } - } + *retval = speaker_deactivate(device); if (*retval > 0) return COMMAND_PENDING; // async @@ -2667,7 +2419,7 @@ volume_set(void *arg, int *retval) master_volume = volume; - for (device = dev_list; device; device = device->next) + for (device = output_device_list; device; device = device->next) { if (!device->selected) continue; @@ -2697,7 +2449,7 @@ static void debug_print_speaker() DPRINTF(E_DBG, L_PLAYER, "*** Master: %d\n", master_volume); - for (device = dev_list; device; device = device->next) + for (device = output_device_list; device; device = device->next) { if (!device->selected) continue; @@ -2719,7 +2471,7 @@ volume_setrel_speaker(void *arg, int *retval) id = vol_param->spk_id; relvol = vol_param->volume; - for (device = dev_list; device; device = device->next) + for (device = output_device_list; device; device = device->next) { if (device->id != id) continue; @@ -2738,7 +2490,7 @@ volume_setrel_speaker(void *arg, int *retval) #endif if (device->session) - *retval = outputs_device_volume_set(device, device_command_cb); + *retval += outputs_device_volume_set(device, device_command_cb); break; } @@ -2770,7 +2522,7 @@ volume_setabs_speaker(void *arg, int *retval) master_volume = volume; - for (device = dev_list; device; device = device->next) + for (device = output_device_list; device; device = device->next) { if (!device->selected) continue; @@ -2794,7 +2546,7 @@ volume_setabs_speaker(void *arg, int *retval) #endif if (device->session) - *retval = outputs_device_volume_set(device, device_command_cb);//FIXME Does this need to be += ? + *retval += outputs_device_volume_set(device, device_command_cb); } } @@ -2824,7 +2576,7 @@ volume_byactiveremote(void *arg, int *retval) *retval = 0; activeremote = ar_param->activeremote; - for (device = dev_list; device; device = device->next) + for (device = output_device_list; device; device = device->next) { if ((uint32_t)device->id == activeremote) break; @@ -2894,7 +2646,6 @@ shuffle_set(void *arg, int *retval) { union player_arg *cmdarg = arg; char new_shuffle; - uint32_t cur_id; new_shuffle = (cmdarg->intval == 0) ? 0 : 1; @@ -2905,8 +2656,10 @@ shuffle_set(void *arg, int *retval) // Update queue and notify listeners if (new_shuffle) { - cur_id = cur_streaming ? cur_streaming->item_id : 0; - db_queue_reshuffle(cur_id); + if (pb_session.playing_now) + db_queue_reshuffle(pb_session.playing_now->item_id); + else + db_queue_reshuffle(0); } else { @@ -2964,49 +2717,6 @@ playerqueue_plid(void *arg, int *retval) /* ------------------------------- Player API ------------------------------- */ -int -player_get_current_pos(uint64_t *pos, struct timespec *ts, int commit) -{ - uint64_t delta; - int ret; - - ret = clock_gettime_with_res(CLOCK_MONOTONIC, ts, &timer_res); - if (ret < 0) - { - DPRINTF(E_LOG, L_PLAYER, "Couldn't get clock: %s\n", strerror(errno)); - - return -1; - } - - delta = (ts->tv_sec - pb_pos_stamp.tv_sec) * 1000000 + (ts->tv_nsec - pb_pos_stamp.tv_nsec) / 1000; - -#ifdef DEBUG_SYNC - DPRINTF(E_DBG, L_PLAYER, "Delta is %" PRIu64 " usec\n", delta); -#endif - - delta = (delta * 44100) / 1000000; - -#ifdef DEBUG_SYNC - DPRINTF(E_DBG, L_PLAYER, "Delta is %" PRIu64 " samples\n", delta); -#endif - - *pos = pb_pos + delta; - - if (commit) - { - pb_pos = *pos; - - pb_pos_stamp.tv_sec = ts->tv_sec; - pb_pos_stamp.tv_nsec = ts->tv_nsec; - -#ifdef DEBUG_SYNC - DPRINTF(E_DBG, L_PLAYER, "Pos: %" PRIu64 " (clock)\n", *pos); -#endif - } - - return 0; -} - int player_get_status(struct player_status *status) { @@ -3026,11 +2736,11 @@ player_get_status(struct player_status *status) * @return 0 on success, -1 on failure (e. g. no playing item found) */ int -player_now_playing(uint32_t *id) +player_playing_now(uint32_t *id) { int ret; - ret = commands_exec_sync(cmdbase, now_playing, NULL, id); + ret = commands_exec_sync(cmdbase, playing_now, NULL, id); return ret; } @@ -3093,6 +2803,15 @@ player_playback_stop(void) return ret; } +int +player_playback_abort(void) +{ + int ret; + + ret = commands_exec_sync(cmdbase, playback_abort, NULL, NULL); + return ret; +} + int player_playback_pause(void) { @@ -3389,20 +3108,6 @@ player_raop_verification_kickoff(char **arglist) } -/* ---------------------------- Thread: worker ------------------------------ */ - -void -player_metadata_send(void *imd, void *omd) -{ - struct metadata_param metadata_param; - - metadata_param.input = imd; - metadata_param.output = omd; - - commands_exec_sync(cmdbase, device_metadata_send, NULL, &metadata_param); -} - - /* ---------------------------- Thread: player ------------------------------ */ static void * @@ -3426,7 +3131,7 @@ player(void *arg) db_speaker_clear_all(); - for (device = dev_list; device; device = device->next) + for (device = output_device_list; device; device = device->next) { ret = db_speaker_save(device); if (ret < 0) @@ -3445,55 +3150,38 @@ int player_init(void) { uint64_t interval; - uint32_t rnd; int ret; - player_exit = 0; - speaker_autoselect = cfg_getbool(cfg_getsec(cfg, "general"), "speaker_autoselect"); clear_queue_on_stop_disabled = cfg_getbool(cfg_getsec(cfg, "mpd"), "clear_queue_on_stop_disable"); - dev_list = NULL; - master_volume = -1; - output_sessions = 0; - - cur_playing = NULL; - cur_streaming = NULL; - cur_plid = 0; - cur_plversion = 0; - player_state = PLAY_STOPPED; repeat = REPEAT_OFF; - shuffle = 0; - consume = 0; - history = (struct player_history *)calloc(1, sizeof(struct player_history)); + CHECK_NULL(L_PLAYER, history = calloc(1, sizeof(struct player_history))); // Determine if the resolution of the system timer is > or < the size // of an audio packet. NOTE: this assumes the system clock resolution // is less than one second. - if (clock_getres(CLOCK_MONOTONIC, &timer_res) < 0) + if (clock_getres(CLOCK_MONOTONIC, &player_timer_res) < 0) { DPRINTF(E_LOG, L_PLAYER, "Could not get the system timer resolution.\n"); - - return -1; + goto error_history_free; } if (!cfg_getbool(cfg_getsec(cfg, "general"), "high_resolution_clock")) { - DPRINTF(E_INFO, L_PLAYER, "High resolution clock not enabled on this system (res is %ld)\n", timer_res.tv_nsec); - - timer_res.tv_nsec = 2 * AIRTUNES_V2_STREAM_PERIOD; + DPRINTF(E_INFO, L_PLAYER, "High resolution clock not enabled on this system (res is %ld)\n", player_timer_res.tv_nsec); + player_timer_res.tv_nsec = 10 * PLAYER_TICK_INTERVAL * 1000000; } // Set the tick interval for the playback timer - interval = MAX(timer_res.tv_nsec, AIRTUNES_V2_STREAM_PERIOD); - tick_interval.tv_nsec = interval; + interval = MAX(player_timer_res.tv_nsec, PLAYER_TICK_INTERVAL * 1000000); + player_tick_interval.tv_nsec = interval; pb_write_deficit_max = (PLAYER_WRITE_BEHIND_MAX * 1000000 / interval); - pb_read_deficit_max = (PLAYER_READ_BEHIND_MAX * 1000000 / interval); // Create the playback timer #ifdef HAVE_TIMERFD @@ -3505,54 +3193,36 @@ player_init(void) if (ret < 0) { DPRINTF(E_LOG, L_PLAYER, "Could not create playback timer: %s\n", strerror(errno)); - - return -1; - } - - // Random RTP time start - gcry_randomize(&rnd, sizeof(rnd), GCRY_STRONG_RANDOM); - last_rtptime = ((uint64_t)1 << 32) | rnd; - - evbase_player = event_base_new(); - if (!evbase_player) - { - DPRINTF(E_LOG, L_PLAYER, "Could not create an event base\n"); - - goto evbase_fail; + goto error_history_free; } + CHECK_NULL(L_PLAYER, evbase_player = event_base_new()); #ifdef HAVE_TIMERFD - pb_timer_ev = event_new(evbase_player, pb_timer_fd, EV_READ | EV_PERSIST, playback_cb, NULL); + CHECK_NULL(L_PLAYER, pb_timer_ev = event_new(evbase_player, pb_timer_fd, EV_READ | EV_PERSIST, playback_cb, NULL)); #else - pb_timer_ev = event_new(evbase_player, SIGALRM, EV_SIGNAL | EV_PERSIST, playback_cb, NULL); + CHECK_NULL(L_PLAYER, pb_timer_ev = event_new(evbase_player, SIGALRM, EV_SIGNAL | EV_PERSIST, playback_cb, NULL)); #endif - if (!pb_timer_ev) - { - DPRINTF(E_LOG, L_PLAYER, "Could not create playback timer event\n"); - goto evnew_fail; - } - - cmdbase = commands_base_new(evbase_player, NULL); + CHECK_NULL(L_PLAYER, cmdbase = commands_base_new(evbase_player, NULL)); ret = outputs_init(); if (ret < 0) { DPRINTF(E_FATAL, L_PLAYER, "Output initiation failed\n"); - goto outputs_fail; + goto error_evbase_free; } ret = input_init(); if (ret < 0) { DPRINTF(E_FATAL, L_PLAYER, "Input initiation failed\n"); - goto input_fail; + goto error_outputs_deinit; } ret = pthread_create(&tid_player, NULL, player, NULL); if (ret < 0) { DPRINTF(E_FATAL, L_PLAYER, "Could not spawn player thread: %s\n", strerror(errno)); - goto thread_fail; + goto error_input_deinit; } #if defined(HAVE_PTHREAD_SETNAME_NP) pthread_setname_np(tid_player, "player"); @@ -3562,20 +3232,20 @@ player_init(void) return 0; - thread_fail: + error_input_deinit: input_deinit(); - input_fail: + error_outputs_deinit: outputs_deinit(); - outputs_fail: + error_evbase_free: commands_base_free(cmdbase); - evnew_fail: event_base_free(evbase_player); - evbase_fail: #ifdef HAVE_TIMERFD close(pb_timer_fd); #else timer_delete(pb_timer); #endif + error_history_free: + free(history); return -1; } @@ -3585,7 +3255,7 @@ player_deinit(void) { int ret; - player_playback_stop(); + player_playback_abort(); #ifdef HAVE_TIMERFD close(pb_timer_fd); diff --git a/src/player.h b/src/player.h index 3cae9c2d..3d55ffcd 100644 --- a/src/player.h +++ b/src/player.h @@ -7,14 +7,7 @@ #include "db.h" -/* AirTunes v2 packet interval in ns */ -/* (352 samples/packet * 1e9 ns/s) / 44100 samples/s = 7981859 ns/packet */ -# define AIRTUNES_V2_STREAM_PERIOD 7981859 - -/* AirTunes v2 number of samples per packet */ -#define AIRTUNES_V2_PACKET_SAMPLES 352 - -/* Maximum number of previously played songs that are remembered */ +// Maximum number of previously played songs that are remembered #define MAX_HISTORY_COUNT 20 enum play_status { @@ -79,15 +72,11 @@ struct player_history uint32_t item_id[MAX_HISTORY_COUNT]; }; - -int -player_get_current_pos(uint64_t *pos, struct timespec *ts, int commit); - int player_get_status(struct player_status *status); int -player_now_playing(uint32_t *id); +player_playing_now(uint32_t *id); void player_speaker_enumerate(spk_enum_cb cb, void *arg); @@ -104,9 +93,6 @@ player_speaker_enable(uint64_t id); int player_speaker_disable(uint64_t id); -void -player_speaker_status_trigger(void); - int player_playback_start(void); @@ -152,7 +138,6 @@ player_shuffle_set(int enable); int player_consume_set(int enable); - void player_queue_clear_history(void); @@ -171,8 +156,8 @@ player_device_remove(void *device); void player_raop_verification_kickoff(char **arglist); -void -player_metadata_send(void *imd, void *omd); +const char * +player_pmap(void *p); int player_init(void); diff --git a/src/spotify.c b/src/spotify.c index ed6d5745..ffe94bff 100644 --- a/src/spotify.c +++ b/src/spotify.c @@ -718,8 +718,7 @@ playback_eot(void *arg, int *retval) g_state = SPOTIFY_STATE_STOPPING; - // TODO 1) This will block for a while, but perhaps ok? - input_write(spotify_audio_buffer, INPUT_FLAG_EOF); + input_write(spotify_audio_buffer, NULL, INPUT_FLAG_EOF); *retval = 0; return COMMAND_END; @@ -1007,17 +1006,22 @@ logged_out(sp_session *sess) static int music_delivery(sp_session *sess, const sp_audioformat *format, const void *frames, int num_frames) { + struct media_quality quality = { 0 }; size_t size; int ret; /* No support for resampling right now */ - if ((format->sample_rate != 44100) || (format->channels != 2)) + if ((format->sample_type != SP_SAMPLETYPE_INT16_NATIVE_ENDIAN) || (format->channels != 2)) { - DPRINTF(E_LOG, L_SPOTIFY, "Got music with unsupported samplerate or channels, stopping playback\n"); + DPRINTF(E_LOG, L_SPOTIFY, "Got music with unsupported sample format or number of channels, stopping playback\n"); spotify_playback_stop_nonblock(); return num_frames; } + quality.sample_rate = format->sample_rate; + quality.bits_per_sample = 16; + quality.channels = format->channels; + // Audio discontinuity, e.g. seek if (num_frames == 0) { @@ -1037,7 +1041,7 @@ static int music_delivery(sp_session *sess, const sp_audioformat *format, // The input buffer only accepts writing when it is approaching depletion, and // because we use NONBLOCK it will just return if this is not the case. So in // most cases no actual write is made and spotify_audio_buffer will just grow. - input_write(spotify_audio_buffer, INPUT_FLAG_NONBLOCK); + input_write(spotify_audio_buffer, &quality, 0); return num_frames; } diff --git a/src/transcode.c b/src/transcode.c index fa4ca863..580795b8 100644 --- a/src/transcode.c +++ b/src/transcode.c @@ -40,6 +40,7 @@ #include "conffile.h" #include "db.h" #include "avio_evbuffer.h" +#include "misc.h" #include "transcode.h" // Interval between ICY metadata checks for streams, in seconds @@ -75,12 +76,10 @@ struct settings_ctx // Audio settings enum AVCodecID audio_codec; - const char *audio_codec_name; int sample_rate; uint64_t channel_layout; int channels; enum AVSampleFormat sample_format; - int byte_depth; bool wavheader; bool icy; @@ -178,47 +177,56 @@ struct encode_ctx uint8_t header[44]; }; -struct transcode_ctx -{ - struct decode_ctx *decode_ctx; - struct encode_ctx *encode_ctx; -}; - /* -------------------------- PROFILE CONFIGURATION ------------------------ */ static int -init_settings(struct settings_ctx *settings, enum transcode_profile profile) +init_settings(struct settings_ctx *settings, enum transcode_profile profile, struct media_quality *quality) { - const AVCodecDescriptor *codec_desc; - memset(settings, 0, sizeof(struct settings_ctx)); switch (profile) { + case XCODE_PCM_NATIVE: // Sample rate and bit depth determined by source + settings->encode_audio = 1; + settings->icy = 1; + break; + case XCODE_PCM16_HEADER: settings->wavheader = 1; - case XCODE_PCM16_NOHEADER: + case XCODE_PCM16: settings->encode_audio = 1; settings->format = "s16le"; settings->audio_codec = AV_CODEC_ID_PCM_S16LE; - settings->sample_rate = 44100; - settings->channel_layout = AV_CH_LAYOUT_STEREO; - settings->channels = 2; settings->sample_format = AV_SAMPLE_FMT_S16; - settings->byte_depth = 2; // Bytes per sample = 16/8 - settings->icy = 1; + break; + + case XCODE_PCM24: + settings->encode_audio = 1; + settings->format = "s24le"; + settings->audio_codec = AV_CODEC_ID_PCM_S24LE; + settings->sample_format = AV_SAMPLE_FMT_S32; + break; + + case XCODE_PCM32: + settings->encode_audio = 1; + settings->format = "s32le"; + settings->audio_codec = AV_CODEC_ID_PCM_S32LE; + settings->sample_format = AV_SAMPLE_FMT_S32; break; case XCODE_MP3: settings->encode_audio = 1; settings->format = "mp3"; settings->audio_codec = AV_CODEC_ID_MP3; - settings->sample_rate = 44100; - settings->channel_layout = AV_CH_LAYOUT_STEREO; - settings->channels = 2; settings->sample_format = AV_SAMPLE_FMT_S16P; - settings->byte_depth = 2; // Bytes per sample = 16/8 + break; + + case XCODE_OPUS: + settings->encode_audio = 1; + settings->format = "data"; // Means we get the raw packet from the encoder, no muxing + settings->audio_codec = AV_CODEC_ID_OPUS; + settings->sample_format = AV_SAMPLE_FMT_S16; // Only libopus support break; case XCODE_JPEG: @@ -226,6 +234,7 @@ init_settings(struct settings_ctx *settings, enum transcode_profile profile) settings->silent = 1; settings->format = "image2"; settings->in_format = "mjpeg"; + settings->pix_fmt = AV_PIX_FMT_YUVJ420P; settings->video_codec = AV_CODEC_ID_MJPEG; break; @@ -233,6 +242,7 @@ init_settings(struct settings_ctx *settings, enum transcode_profile profile) settings->encode_video = 1; settings->silent = 1; settings->format = "image2"; + settings->pix_fmt = AV_PIX_FMT_RGB24; settings->video_codec = AV_CODEC_ID_PNG; break; @@ -241,16 +251,21 @@ init_settings(struct settings_ctx *settings, enum transcode_profile profile) return -1; } - if (settings->audio_codec) + if (quality && quality->sample_rate) { - codec_desc = avcodec_descriptor_get(settings->audio_codec); - settings->audio_codec_name = codec_desc->name; + settings->sample_rate = quality->sample_rate; } - if (settings->video_codec) + if (quality && quality->channels) { - codec_desc = avcodec_descriptor_get(settings->video_codec); - settings->video_codec_name = codec_desc->name; + settings->channels = quality->channels; + settings->channel_layout = av_get_default_channel_layout(quality->channels); + } + + if (quality && quality->bits_per_sample && (quality->bits_per_sample != 8 * av_get_bytes_per_sample(settings->sample_format))) + { + DPRINTF(E_LOG, L_XCODE, "Bug! Mismatch between profile and media quality\n"); + return -1; } return 0; @@ -279,6 +294,19 @@ stream_settings_set(struct stream_ctx *s, struct settings_ctx *settings, enum AV /* -------------------------------- HELPERS -------------------------------- */ +static enum AVSampleFormat +bitdepth2format(int bits_per_sample) +{ + if (bits_per_sample == 16) + return AV_SAMPLE_FMT_S16; + else if (bits_per_sample == 24) + return AV_SAMPLE_FMT_S32; + else if (bits_per_sample == 32) + return AV_SAMPLE_FMT_S32; + else + return AV_SAMPLE_FMT_NONE; +} + static inline char * err2str(int errnum) { @@ -307,15 +335,18 @@ make_wav_header(struct encode_ctx *ctx, struct decode_ctx *src_ctx, off_t *est_s { uint32_t wav_len; int duration; + int bps; if (src_ctx->duration) duration = src_ctx->duration; else duration = 3 * 60 * 1000; /* 3 minutes, in ms */ - wav_len = ctx->settings.channels * ctx->settings.byte_depth * ctx->settings.sample_rate * (duration / 1000); + bps = av_get_bytes_per_sample(ctx->settings.sample_format); + wav_len = ctx->settings.channels * bps * ctx->settings.sample_rate * (duration / 1000); - *est_size = wav_len + sizeof(ctx->header); + if (est_size) + *est_size = wav_len + sizeof(ctx->header); memcpy(ctx->header, "RIFF", 4); add_le32(ctx->header + 4, 36 + wav_len); @@ -324,9 +355,9 @@ make_wav_header(struct encode_ctx *ctx, struct decode_ctx *src_ctx, off_t *est_s add_le16(ctx->header + 20, 1); add_le16(ctx->header + 22, ctx->settings.channels); /* channels */ add_le32(ctx->header + 24, ctx->settings.sample_rate); /* samplerate */ - add_le32(ctx->header + 28, ctx->settings.sample_rate * ctx->settings.channels * ctx->settings.byte_depth); /* byte rate */ - add_le16(ctx->header + 32, ctx->settings.channels * ctx->settings.byte_depth); /* block align */ - add_le16(ctx->header + 34, ctx->settings.byte_depth * 8); /* bits per sample */ + add_le32(ctx->header + 28, ctx->settings.sample_rate * ctx->settings.channels * bps); /* byte rate */ + add_le16(ctx->header + 32, ctx->settings.channels * bps); /* block align */ + add_le16(ctx->header + 34, 8 * bps); /* bits per sample */ memcpy(ctx->header + 36, "data", 4); add_le32(ctx->header + 40, wav_len); } @@ -356,20 +387,27 @@ stream_find(struct decode_ctx *ctx, unsigned int stream_index) * @out ctx A pre-allocated stream ctx where we save stream and codec info * @in output Output to add the stream to * @in codec_id What kind of codec should we use - * @in codec_name Name of codec (only used for logging) * @return Negative on failure, otherwise zero */ static int -stream_add(struct encode_ctx *ctx, struct stream_ctx *s, enum AVCodecID codec_id, const char *codec_name) +stream_add(struct encode_ctx *ctx, struct stream_ctx *s, enum AVCodecID codec_id) { + const AVCodecDescriptor *codec_desc; AVCodec *encoder; AVDictionary *options = NULL; int ret; + codec_desc = avcodec_descriptor_get(codec_id); + if (!codec_desc) + { + DPRINTF(E_LOG, L_XCODE, "Invalid codec ID (%d)\n", codec_id); + return -1; + } + encoder = avcodec_find_encoder(codec_id); if (!encoder) { - DPRINTF(E_LOG, L_XCODE, "Necessary encoder (%s) not found\n", codec_name); + DPRINTF(E_LOG, L_XCODE, "Necessary encoder (%s) not found\n", codec_desc->name); return -1; } @@ -381,7 +419,7 @@ stream_add(struct encode_ctx *ctx, struct stream_ctx *s, enum AVCodecID codec_id if (!s->codec->pix_fmt) { s->codec->pix_fmt = avcodec_default_get_format(s->codec, encoder->pix_fmts); - DPRINTF(E_DBG, L_XCODE, "Pixel format set to %s (encoder is %s)\n", av_get_pix_fmt_name(s->codec->pix_fmt), codec_name); + DPRINTF(E_DBG, L_XCODE, "Pixel format set to %s (encoder is %s)\n", av_get_pix_fmt_name(s->codec->pix_fmt), codec_desc->name); } if (ctx->ofmt_ctx->oformat->flags & AVFMT_GLOBALHEADER) @@ -394,7 +432,7 @@ stream_add(struct encode_ctx *ctx, struct stream_ctx *s, enum AVCodecID codec_id ret = avcodec_open2(s->codec, NULL, &options); if (ret < 0) { - DPRINTF(E_LOG, L_XCODE, "Cannot open encoder (%s): %s\n", codec_name, err2str(ret)); + DPRINTF(E_LOG, L_XCODE, "Cannot open encoder (%s): %s\n", codec_desc->name, err2str(ret)); avcodec_free_context(&s->codec); return -1; } @@ -403,7 +441,7 @@ stream_add(struct encode_ctx *ctx, struct stream_ctx *s, enum AVCodecID codec_id ret = avcodec_parameters_from_context(s->stream->codecpar, s->codec); if (ret < 0) { - DPRINTF(E_LOG, L_XCODE, "Cannot copy stream parameters (%s): %s\n", codec_name, err2str(ret)); + DPRINTF(E_LOG, L_XCODE, "Cannot copy stream parameters (%s): %s\n", codec_desc->name, err2str(ret)); avcodec_free_context(&s->codec); return -1; } @@ -854,7 +892,7 @@ open_output(struct encode_ctx *ctx, struct decode_ctx *src_ctx) } // Clear AVFMT_NOFILE bit, it is not allowed as we will set our own AVIOContext - oformat->flags = ~AVFMT_NOFILE; + oformat->flags &= ~AVFMT_NOFILE; CHECK_NULL(L_XCODE, ctx->ofmt_ctx = avformat_alloc_context()); @@ -876,14 +914,14 @@ open_output(struct encode_ctx *ctx, struct decode_ctx *src_ctx) if (ctx->settings.encode_audio) { - ret = stream_add(ctx, &ctx->audio_stream, ctx->settings.audio_codec, ctx->settings.audio_codec_name); + ret = stream_add(ctx, &ctx->audio_stream, ctx->settings.audio_codec); if (ret < 0) goto out_free_streams; } if (ctx->settings.encode_video) { - ret = stream_add(ctx, &ctx->video_stream, ctx->settings.video_codec, ctx->settings.video_codec_name); + ret = stream_add(ctx, &ctx->video_stream, ctx->settings.video_codec); if (ret < 0) goto out_free_streams; } @@ -972,6 +1010,8 @@ open_filter(struct stream_ctx *out_stream, struct stream_ctx *in_stream) goto out_fail; } + DPRINTF(E_DBG, L_XCODE, "Created 'in' filter: %s\n", args); + snprintf(args, sizeof(args), "sample_fmts=%s:sample_rates=%d:channel_layouts=0x%"PRIx64, av_get_sample_fmt_name(out_stream->codec->sample_fmt), out_stream->codec->sample_rate, @@ -984,6 +1024,8 @@ open_filter(struct stream_ctx *out_stream, struct stream_ctx *in_stream) goto out_fail; } + DPRINTF(E_DBG, L_XCODE, "Created 'format' filter: %s\n", args); + ret = avfilter_graph_create_filter(&buffersink_ctx, buffersink, "out", NULL, NULL, filter_graph); if (ret < 0) { @@ -1122,7 +1164,7 @@ close_filters(struct encode_ctx *ctx) /* Setup */ struct decode_ctx * -transcode_decode_setup(enum transcode_profile profile, enum data_kind data_kind, const char *path, struct evbuffer *evbuf, uint32_t song_length) +transcode_decode_setup(enum transcode_profile profile, struct media_quality *quality, enum data_kind data_kind, const char *path, struct evbuffer *evbuf, uint32_t song_length) { struct decode_ctx *ctx; @@ -1133,7 +1175,7 @@ transcode_decode_setup(enum transcode_profile profile, enum data_kind data_kind, ctx->duration = song_length; ctx->data_kind = data_kind; - if ((init_settings(&ctx->settings, profile) < 0) || (open_input(ctx, path, evbuf) < 0)) + if ((init_settings(&ctx->settings, profile, quality) < 0) || (open_input(ctx, path, evbuf) < 0)) goto fail_free; return ctx; @@ -1146,20 +1188,52 @@ transcode_decode_setup(enum transcode_profile profile, enum data_kind data_kind, } struct encode_ctx * -transcode_encode_setup(enum transcode_profile profile, struct decode_ctx *src_ctx, off_t *est_size, int width, int height) +transcode_encode_setup(enum transcode_profile profile, struct media_quality *quality, struct decode_ctx *src_ctx, off_t *est_size, int width, int height) { struct encode_ctx *ctx; + int bps; CHECK_NULL(L_XCODE, ctx = calloc(1, sizeof(struct encode_ctx))); CHECK_NULL(L_XCODE, ctx->filt_frame = av_frame_alloc()); CHECK_NULL(L_XCODE, ctx->encoded_pkt = av_packet_alloc()); - if (init_settings(&ctx->settings, profile) < 0) + if (init_settings(&ctx->settings, profile, quality) < 0) goto fail_free; ctx->settings.width = width; ctx->settings.height = height; + // Caller did not specify a sample rate -> use same as source + if (!ctx->settings.sample_rate && ctx->settings.encode_audio) + { + ctx->settings.sample_rate = src_ctx->audio_stream.codec->sample_rate; + } + + // Caller did not specify a sample format -> use same as source + if (!ctx->settings.sample_format && ctx->settings.encode_audio) + { + bps = av_get_bytes_per_sample(src_ctx->audio_stream.codec->sample_fmt); + if (bps == 4) + { + ctx->settings.sample_format = AV_SAMPLE_FMT_S32; + ctx->settings.audio_codec = AV_CODEC_ID_PCM_S32LE; + ctx->settings.format = "s32le"; + } + else + { + ctx->settings.sample_format = AV_SAMPLE_FMT_S16; + ctx->settings.audio_codec = AV_CODEC_ID_PCM_S16LE; + ctx->settings.format = "s16le"; + } + } + + // Caller did not specify channels -> use same as source + if (!ctx->settings.channels && ctx->settings.encode_audio) + { + ctx->settings.channels = src_ctx->audio_stream.codec->channels; + ctx->settings.channel_layout = src_ctx->audio_stream.codec->channel_layout; + } + if (ctx->settings.wavheader) make_wav_header(ctx, src_ctx, est_size); @@ -1170,7 +1244,10 @@ transcode_encode_setup(enum transcode_profile profile, struct decode_ctx *src_ct goto fail_close; if (ctx->settings.icy && src_ctx->data_kind == DATA_KIND_HTTP) - ctx->icy_interval = METADATA_ICY_INTERVAL * ctx->settings.channels * ctx->settings.byte_depth * ctx->settings.sample_rate; + { + bps = av_get_bytes_per_sample(ctx->settings.sample_format); + ctx->icy_interval = METADATA_ICY_INTERVAL * ctx->settings.channels * bps * ctx->settings.sample_rate; + } return ctx; @@ -1184,20 +1261,20 @@ transcode_encode_setup(enum transcode_profile profile, struct decode_ctx *src_ct } struct transcode_ctx * -transcode_setup(enum transcode_profile profile, enum data_kind data_kind, const char *path, uint32_t song_length, off_t *est_size) +transcode_setup(enum transcode_profile profile, struct media_quality *quality, enum data_kind data_kind, const char *path, uint32_t song_length, off_t *est_size) { struct transcode_ctx *ctx; CHECK_NULL(L_XCODE, ctx = calloc(1, sizeof(struct transcode_ctx))); - ctx->decode_ctx = transcode_decode_setup(profile, data_kind, path, NULL, song_length); + ctx->decode_ctx = transcode_decode_setup(profile, quality, data_kind, path, NULL, song_length); if (!ctx->decode_ctx) { free(ctx); return NULL; } - ctx->encode_ctx = transcode_encode_setup(profile, ctx->decode_ctx, est_size, 0, 0); + ctx->encode_ctx = transcode_encode_setup(profile, quality, ctx->decode_ctx, est_size, 0, 0); if (!ctx->encode_ctx) { transcode_decode_cleanup(&ctx->decode_ctx); @@ -1209,26 +1286,34 @@ transcode_setup(enum transcode_profile profile, enum data_kind data_kind, const } struct decode_ctx * -transcode_decode_setup_raw(void) +transcode_decode_setup_raw(enum transcode_profile profile, struct media_quality *quality) { + const AVCodecDescriptor *codec_desc; struct decode_ctx *ctx; AVCodec *decoder; int ret; CHECK_NULL(L_XCODE, ctx = calloc(1, sizeof(struct decode_ctx))); - if (init_settings(&ctx->settings, XCODE_PCM16_NOHEADER) < 0) + if (init_settings(&ctx->settings, profile, quality) < 0) { goto out_free_ctx; } + codec_desc = avcodec_descriptor_get(ctx->settings.audio_codec); + if (!codec_desc) + { + DPRINTF(E_LOG, L_XCODE, "Invalid codec ID (%d)\n", ctx->settings.audio_codec); + goto out_free_ctx; + } + // In raw mode we won't actually need to read or decode, but we still setup // the decode_ctx because transcode_encode_setup() gets info about the input // through this structure (TODO dont' do that) decoder = avcodec_find_decoder(ctx->settings.audio_codec); if (!decoder) { - DPRINTF(E_LOG, L_XCODE, "Could not find decoder for: %s\n", ctx->settings.audio_codec_name); + DPRINTF(E_LOG, L_XCODE, "Could not find decoder for: %s\n", codec_desc->name); goto out_free_ctx; } @@ -1243,7 +1328,7 @@ transcode_decode_setup_raw(void) ret = avcodec_parameters_from_context(ctx->audio_stream.stream->codecpar, ctx->audio_stream.codec); if (ret < 0) { - DPRINTF(E_LOG, L_XCODE, "Cannot copy stream parameters (%s): %s\n", ctx->settings.audio_codec_name, err2str(ret)); + DPRINTF(E_LOG, L_XCODE, "Cannot copy stream parameters (%s): %s\n", codec_desc->name, err2str(ret)); goto out_free_codec; } @@ -1373,6 +1458,9 @@ transcode_encode_cleanup(struct encode_ctx **ctx) void transcode_cleanup(struct transcode_ctx **ctx) { + if (!*ctx) + return; + transcode_encode_cleanup(&(*ctx)->encode_ctx); transcode_decode_cleanup(&(*ctx)->decode_ctx); free(*ctx); @@ -1383,7 +1471,7 @@ transcode_cleanup(struct transcode_ctx **ctx) /* Encoding, decoding and transcoding */ int -transcode_decode(void **frame, struct decode_ctx *dec_ctx) +transcode_decode(transcode_frame **frame, struct decode_ctx *dec_ctx) { struct transcode_ctx ctx; int ret; @@ -1414,7 +1502,7 @@ transcode_decode(void **frame, struct decode_ctx *dec_ctx) // Filters and encodes int -transcode_encode(struct evbuffer *evbuf, struct encode_ctx *ctx, void *frame, int eof) +transcode_encode(struct evbuffer *evbuf, struct encode_ctx *ctx, transcode_frame *frame, int eof) { AVFrame *f = frame; struct stream_ctx *s; @@ -1489,8 +1577,8 @@ transcode(struct evbuffer *evbuf, int *icy_timer, struct transcode_ctx *ctx, int return processed; } -void * -transcode_frame_new(enum transcode_profile profile, uint8_t *data, size_t size) +transcode_frame * +transcode_frame_new(void *data, size_t size, int nsamples, struct media_quality *quality) { AVFrame *f; int ret; @@ -1502,19 +1590,28 @@ transcode_frame_new(enum transcode_profile profile, uint8_t *data, size_t size) return NULL; } - f->nb_samples = size / 4; - f->format = AV_SAMPLE_FMT_S16; - f->channel_layout = AV_CH_LAYOUT_STEREO; + f->format = bitdepth2format(quality->bits_per_sample); + if (f->format == AV_SAMPLE_FMT_NONE) + { + DPRINTF(E_LOG, L_XCODE, "transcode_frame_new() called with unsupported bps (%d)\n", quality->bits_per_sample); + av_frame_free(&f); + return NULL; + } + + f->sample_rate = quality->sample_rate; + f->nb_samples = nsamples; + f->channel_layout = av_get_default_channel_layout(quality->channels); #ifdef HAVE_FFMPEG - f->channels = 2; + f->channels = quality->channels; #endif f->pts = AV_NOPTS_VALUE; - f->sample_rate = 44100; - ret = avcodec_fill_audio_frame(f, 2, f->format, data, size, 0); + // We don't align because the frame won't be given directly to the encoder + // anyway, it will first go through the filter (which might align it...?) + ret = avcodec_fill_audio_frame(f, 2, f->format, data, size, 1); if (ret < 0) { - DPRINTF(E_LOG, L_XCODE, "Error filling frame with rawbuf: %s\n", err2str(ret)); + DPRINTF(E_LOG, L_XCODE, "Error filling frame with rawbuf, size %zu, samples %d (%d/%d/2): %s\n", size, nsamples, quality->sample_rate, quality->bits_per_sample, err2str(ret)); av_frame_free(&f); return NULL; } @@ -1523,7 +1620,7 @@ transcode_frame_new(enum transcode_profile profile, uint8_t *data, size_t size) } void -transcode_frame_free(void *frame) +transcode_frame_free(transcode_frame *frame) { AVFrame *f = frame; @@ -1645,6 +1742,29 @@ transcode_decode_query(struct decode_ctx *ctx, const char *query) return -1; } +int +transcode_encode_query(struct encode_ctx *ctx, const char *query) +{ + if (strcmp(query, "sample_rate") == 0) + { + if (ctx->audio_stream.stream) + return ctx->audio_stream.stream->codecpar->sample_rate; + } + else if (strcmp(query, "bits_per_sample") == 0) + { + if (ctx->audio_stream.stream) + return av_get_bits_per_sample(ctx->audio_stream.stream->codecpar->codec_id); + } + else if (strcmp(query, "channels") == 0) + { + if (ctx->audio_stream.stream) + return ctx->audio_stream.stream->codecpar->channels; + } + + return -1; +} + + /* Metadata */ struct http_icy_metadata * diff --git a/src/transcode.h b/src/transcode.h index 3e6cfc86..6b1f753c 100644 --- a/src/transcode.h +++ b/src/transcode.h @@ -5,15 +5,24 @@ #include #include "db.h" #include "http.h" +#include "misc.h" enum transcode_profile { - // Transcodes the best audio stream into PCM16 (does not add wav header) - XCODE_PCM16_NOHEADER, - // Transcodes the best audio stream into PCM16 (with wav header) + // Used for errors + XCODE_UNKNOWN = 0, + // Decodes the best audio stream into PCM16 or PCM24, no resampling (does not add wav header) + XCODE_PCM_NATIVE, + // Decodes/resamples the best audio stream into PCM16 (with wav header) XCODE_PCM16_HEADER, + // Decodes/resamples the best audio stream into PCM16/24/32 (no wav headers) + XCODE_PCM16, + XCODE_PCM24, + XCODE_PCM32, // Transcodes the best audio stream into MP3 XCODE_MP3, + // Transcodes the best audio stream into OPUS + XCODE_OPUS, // Transcodes the best video stream into JPEG/PNG XCODE_JPEG, XCODE_PNG, @@ -21,20 +30,26 @@ enum transcode_profile struct decode_ctx; struct encode_ctx; -struct transcode_ctx; +struct transcode_ctx +{ + struct decode_ctx *decode_ctx; + struct encode_ctx *encode_ctx; +}; + +typedef void transcode_frame; // Setting up struct decode_ctx * -transcode_decode_setup(enum transcode_profile profile, enum data_kind data_kind, const char *path, struct evbuffer *evbuf, uint32_t song_length); +transcode_decode_setup(enum transcode_profile profile, struct media_quality *quality, enum data_kind data_kind, const char *path, struct evbuffer *evbuf, uint32_t song_length); struct encode_ctx * -transcode_encode_setup(enum transcode_profile profile, struct decode_ctx *src_ctx, off_t *est_size, int width, int height); +transcode_encode_setup(enum transcode_profile profile, struct media_quality *quality, struct decode_ctx *src_ctx, off_t *est_size, int width, int height); struct transcode_ctx * -transcode_setup(enum transcode_profile profile, enum data_kind data_kind, const char *path, uint32_t song_length, off_t *est_size); +transcode_setup(enum transcode_profile profile, struct media_quality *quality, enum data_kind data_kind, const char *path, uint32_t song_length, off_t *est_size); struct decode_ctx * -transcode_decode_setup_raw(void); +transcode_decode_setup_raw(enum transcode_profile profile, struct media_quality *quality); int transcode_needed(const char *user_agent, const char *client_codecs, char *file_codectype); @@ -60,7 +75,7 @@ transcode_cleanup(struct transcode_ctx **ctx); * @return Positive if OK, negative if error, 0 if EOF */ int -transcode_decode(void **frame, struct decode_ctx *ctx); +transcode_decode(transcode_frame **frame, struct decode_ctx *ctx); /* Encodes and remuxes a frame. Also resamples if needed. * @@ -71,7 +86,7 @@ transcode_decode(void **frame, struct decode_ctx *ctx); * @return Bytes added if OK, negative if error */ int -transcode_encode(struct evbuffer *evbuf, struct encode_ctx *ctx, void *frame, int eof); +transcode_encode(struct evbuffer *evbuf, struct encode_ctx *ctx, transcode_frame *frame, int eof); /* Demuxes, decodes, encodes and remuxes from the input. * @@ -87,17 +102,19 @@ int transcode(struct evbuffer *evbuf, int *icy_timer, struct transcode_ctx *ctx, int want_bytes); /* Converts a buffer with raw data to a frame that can be passed directly to the - * transcode_encode() function + * transcode_encode() function. It does not copy, so if you free the data the + * frame will become invalid. * - * @in profile Tells the function what kind of frame to create * @in data Buffer with raw data * @in size Size of buffer + * @in nsamples Number of samples in the buffer + * @in quality Sample rate, bits per sample and channels * @return Opaque pointer to frame if OK, otherwise NULL */ -void * -transcode_frame_new(enum transcode_profile profile, uint8_t *data, size_t size); +transcode_frame * +transcode_frame_new(void *data, size_t size, int nsamples, struct media_quality *quality); void -transcode_frame_free(void *frame); +transcode_frame_free(transcode_frame *frame); /* Seek to the specified position - next transcode() will return this packet * @@ -117,6 +134,16 @@ transcode_seek(struct transcode_ctx *ctx, int ms); int transcode_decode_query(struct decode_ctx *ctx, const char *query); +/* Query for information (e.g. sample rate) about the output being produced by + * the transcoding + * + * @in ctx Encode context + * @in query Query - see implementation for supported queries + * @return Negative if error, otherwise query dependent + */ +int +transcode_encode_query(struct encode_ctx *ctx, const char *query); + // Metadata struct http_icy_metadata * transcode_metadata(struct transcode_ctx *ctx, int *changed); diff --git a/src/transcode_legacy.c b/src/transcode_legacy.c deleted file mode 100644 index ef2a0b32..00000000 --- a/src/transcode_legacy.c +++ /dev/null @@ -1,1678 +0,0 @@ -/* - * Copyright (C) 2015 Espen Jurgensen - * - * This program is free software; you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation; either version 2 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program; if not, write to the Free Software - * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA - */ - -#ifdef HAVE_CONFIG_H -# include -#endif - -#include -#include -#include - -#include -#include -#include -#include -#include -#include -#include -#include - -#include "ffmpeg-compat.h" - -#include "logger.h" -#include "conffile.h" -#include "db.h" -#include "avio_evbuffer.h" -#include "transcode.h" - -// Interval between ICY metadata checks for streams, in seconds -#define METADATA_ICY_INTERVAL 5 -// Maximum number of streams in a file that we will accept -#define MAX_STREAMS 64 -// Maximum number of times we retry when we encounter bad packets -#define MAX_BAD_PACKETS 5 -// How long to wait (in microsec) before interrupting av_read_frame -#define READ_TIMEOUT 15000000 - -static const char *default_codecs = "mpeg,wav"; -static const char *roku_codecs = "mpeg,mp4a,wma,wav"; -static const char *itunes_codecs = "mpeg,mp4a,mp4v,alac,wav"; - -// Used for passing errors to DPRINTF (can't count on av_err2str being present) -static char errbuf[64]; - -struct filter_ctx { - AVFilterContext *buffersink_ctx; - AVFilterContext *buffersrc_ctx; - AVFilterGraph *filter_graph; -}; - -struct decode_ctx { - // Input format context - AVFormatContext *ifmt_ctx; - - // Will point to the stream that we will transcode - AVStream *audio_stream; - - // Duration (used to make wav header) - uint32_t duration; - - // Data kind (used to determine if ICY metadata is relevant to look for) - enum data_kind data_kind; - - // Contains the most recent packet from av_read_frame - // Used for resuming after seek and for freeing correctly - // in transcode_decode() - AVPacket packet; - int resume; - int resume_offset; - - // Used to measure if av_read_frame is taking too long - int64_t timestamp; -}; - -struct encode_ctx { - // Output format context - AVFormatContext *ofmt_ctx; - - // We use filters to resample - struct filter_ctx *filter_ctx; - - // The ffmpeg muxer writes to this buffer using the avio_evbuffer interface - struct evbuffer *obuf; - - // Maps input stream number -> output stream number - // So if we are decoding audio stream 3 and encoding it to 0, then - // out_stream_map[3] is 0. A value of -1 means the stream is ignored. - int out_stream_map[MAX_STREAMS]; - - // Maps output stream number -> input stream number - unsigned int in_stream_map[MAX_STREAMS]; - - // Used for seeking - int64_t prev_pts[MAX_STREAMS]; - int64_t offset_pts[MAX_STREAMS]; - - // Settings for encoding and muxing - const char *format; - - // Audio settings - enum AVCodecID audio_codec; - int sample_rate; - uint64_t channel_layout; - int channels; - enum AVSampleFormat sample_format; - int byte_depth; - - // How many output bytes we have processed in total - off_t total_bytes; - - // Used to check for ICY metadata changes at certain intervals - uint32_t icy_interval; - uint32_t icy_hash; - - // WAV header - int wavhdr; - uint8_t header[44]; -}; - -struct transcode_ctx { - struct decode_ctx *decode_ctx; - struct encode_ctx *encode_ctx; -}; - -struct decoded_frame -{ - AVFrame *frame; - unsigned int stream_index; -}; - - -/* -------------------------- PROFILE CONFIGURATION ------------------------ */ - -static int -init_profile(struct encode_ctx *ctx, enum transcode_profile profile) -{ - switch (profile) - { - case XCODE_PCM16_NOHEADER: - case XCODE_PCM16_HEADER: - ctx->format = "s16le"; - ctx->audio_codec = AV_CODEC_ID_PCM_S16LE; - ctx->sample_rate = 44100; - ctx->channel_layout = AV_CH_LAYOUT_STEREO; - ctx->channels = 2; - ctx->sample_format = AV_SAMPLE_FMT_S16; - ctx->byte_depth = 2; // Bytes per sample = 16/8 - return 0; - - case XCODE_MP3: - ctx->format = "mp3"; - ctx->audio_codec = AV_CODEC_ID_MP3; - ctx->sample_rate = 44100; - ctx->channel_layout = AV_CH_LAYOUT_STEREO; - ctx->channels = 2; - ctx->sample_format = AV_SAMPLE_FMT_S16P; - ctx->byte_depth = 2; // Bytes per sample = 16/8 - return 0; - - default: - DPRINTF(E_LOG, L_XCODE, "Bug! Unknown transcoding profile\n"); - return -1; - } -} - - -/* -------------------------------- HELPERS -------------------------------- */ - -static inline char * -err2str(int errnum) -{ - av_strerror(errnum, errbuf, sizeof(errbuf)); - return errbuf; -} - -static inline void -add_le16(uint8_t *dst, uint16_t val) -{ - dst[0] = val & 0xff; - dst[1] = (val >> 8) & 0xff; -} - -static inline void -add_le32(uint8_t *dst, uint32_t val) -{ - dst[0] = val & 0xff; - dst[1] = (val >> 8) & 0xff; - dst[2] = (val >> 16) & 0xff; - dst[3] = (val >> 24) & 0xff; -} - -static void -make_wav_header(struct encode_ctx *ctx, struct decode_ctx *src_ctx, off_t *est_size) -{ - uint32_t wav_len; - int duration; - - if (src_ctx->duration) - duration = src_ctx->duration; - else - duration = 3 * 60 * 1000; /* 3 minutes, in ms */ - - wav_len = ctx->channels * ctx->byte_depth * ctx->sample_rate * (duration / 1000); - - *est_size = wav_len + sizeof(ctx->header); - - memcpy(ctx->header, "RIFF", 4); - add_le32(ctx->header + 4, 36 + wav_len); - memcpy(ctx->header + 8, "WAVEfmt ", 8); - add_le32(ctx->header + 16, 16); - add_le16(ctx->header + 20, 1); - add_le16(ctx->header + 22, ctx->channels); /* channels */ - add_le32(ctx->header + 24, ctx->sample_rate); /* samplerate */ - add_le32(ctx->header + 28, ctx->sample_rate * ctx->channels * ctx->byte_depth); /* byte rate */ - add_le16(ctx->header + 32, ctx->channels * ctx->byte_depth); /* block align */ - add_le16(ctx->header + 34, ctx->byte_depth * 8); /* bits per sample */ - memcpy(ctx->header + 36, "data", 4); - add_le32(ctx->header + 40, wav_len); -} - -/* - * Returns true if in_stream is a stream we should decode, otherwise false - * - * @in ctx Decode context - * @in in_stream Pointer to AVStream - * @return True if stream should be decoded, otherwise false - */ -static int -decode_stream(struct decode_ctx *ctx, AVStream *in_stream) -{ - return (in_stream == ctx->audio_stream); -} - -/* - * Called by libavformat while demuxing. Used to interrupt/unblock av_read_frame - * in case a source (especially a network stream) becomes unavailable. - * - * @in arg Will point to the decode context - * @return Non-zero if av_read_frame should be interrupted - */ -static int decode_interrupt_cb(void *arg) -{ - struct decode_ctx *ctx; - - ctx = (struct decode_ctx *)arg; - - if (av_gettime() - ctx->timestamp > READ_TIMEOUT) - { - DPRINTF(E_LOG, L_XCODE, "Timeout while reading source (connection problem?)\n"); - - return 1; - } - - return 0; -} - -/* Will read the next packet from the source, unless we are in resume mode, in - * which case the most recent packet will be returned, but with an adjusted data - * pointer. Use ctx->resume and ctx->resume_offset to make the function resume - * from the most recent packet. - * - * @out packet Pointer to an already allocated AVPacket. The content of the - * packet will be updated, and packet->data is pointed to the data - * returned by av_read_frame(). The packet struct is owned by the - * caller, but *not* packet->data, so don't free the packet with - * av_free_packet()/av_packet_unref() - * @out stream Set to the input AVStream corresponding to the packet - * @out stream_index - * Set to the input stream index corresponding to the packet - * @in ctx Decode context - * @return 0 if OK, < 0 on error or end of file - */ -static int -read_packet(AVPacket *packet, AVStream **stream, unsigned int *stream_index, struct decode_ctx *ctx) -{ - AVStream *in_stream; - int ret; - - do - { - if (ctx->resume) - { - // Copies packet struct, but not actual packet payload, and adjusts - // data pointer to somewhere inside the payload if resume_offset is set - *packet = ctx->packet; - packet->data += ctx->resume_offset; - packet->size -= ctx->resume_offset; - ctx->resume = 0; - } - else - { - // We are going to read a new packet from source, so now it is safe to - // discard the previous packet and reset resume_offset - av_packet_unref(&ctx->packet); - - ctx->resume_offset = 0; - ctx->timestamp = av_gettime(); - - ret = av_read_frame(ctx->ifmt_ctx, &ctx->packet); - if (ret < 0) - { - DPRINTF(E_WARN, L_XCODE, "Could not read frame: %s\n", err2str(ret)); - return ret; - } - - *packet = ctx->packet; - } - - in_stream = ctx->ifmt_ctx->streams[packet->stream_index]; - } - while (!decode_stream(ctx, in_stream)); - - av_packet_rescale_ts(packet, in_stream->time_base, in_stream->codec->time_base); - - *stream = in_stream; - *stream_index = packet->stream_index; - - return 0; -} - -static int -encode_write_frame(struct encode_ctx *ctx, AVFrame *filt_frame, unsigned int stream_index, int *got_frame) -{ - AVStream *out_stream; - AVPacket enc_pkt; - int ret; - int got_frame_local; - - if (!got_frame) - got_frame = &got_frame_local; - - out_stream = ctx->ofmt_ctx->streams[stream_index]; - - // Encode filtered frame - enc_pkt.data = NULL; - enc_pkt.size = 0; - av_init_packet(&enc_pkt); - - if (out_stream->codec->codec_type == AVMEDIA_TYPE_AUDIO) - ret = avcodec_encode_audio2(out_stream->codec, &enc_pkt, filt_frame, got_frame); - else - return -1; - - if (ret < 0) - return -1; - if (!(*got_frame)) - return 0; - - // Prepare packet for muxing - enc_pkt.stream_index = stream_index; - - // This "wonderful" peace of code makes sure that the timestamp never decreases, - // even if the user seeked backwards. The muxer will not accept decreasing - // timestamps - enc_pkt.pts += ctx->offset_pts[stream_index]; - if (enc_pkt.pts < ctx->prev_pts[stream_index]) - { - ctx->offset_pts[stream_index] += ctx->prev_pts[stream_index] - enc_pkt.pts; - enc_pkt.pts = ctx->prev_pts[stream_index]; - } - ctx->prev_pts[stream_index] = enc_pkt.pts; - enc_pkt.dts = enc_pkt.pts; //FIXME - - av_packet_rescale_ts(&enc_pkt, out_stream->codec->time_base, out_stream->time_base); - - // Mux encoded frame - ret = av_interleaved_write_frame(ctx->ofmt_ctx, &enc_pkt); - return ret; -} - -#if HAVE_DECL_AV_BUFFERSRC_ADD_FRAME_FLAGS && HAVE_DECL_AV_BUFFERSINK_GET_FRAME -static int -filter_encode_write_frame(struct encode_ctx *ctx, AVFrame *frame, unsigned int stream_index) -{ - AVFrame *filt_frame; - int ret; - - // Push the decoded frame into the filtergraph - if (frame) - { - ret = av_buffersrc_add_frame_flags(ctx->filter_ctx[stream_index].buffersrc_ctx, frame, 0); - if (ret < 0) - { - DPRINTF(E_LOG, L_XCODE, "Error while feeding the filtergraph: %s\n", err2str(ret)); - return -1; - } - } - - // Pull filtered frames from the filtergraph - while (1) - { - filt_frame = av_frame_alloc(); - if (!filt_frame) - { - DPRINTF(E_LOG, L_XCODE, "Out of memory for filt_frame\n"); - return -1; - } - - ret = av_buffersink_get_frame(ctx->filter_ctx[stream_index].buffersink_ctx, filt_frame); - if (ret < 0) - { - /* if no more frames for output - returns AVERROR(EAGAIN) - * if flushed and no more frames for output - returns AVERROR_EOF - * rewrite retcode to 0 to show it as normal procedure completion - */ - if (ret == AVERROR(EAGAIN) || ret == AVERROR_EOF) - ret = 0; - av_frame_free(&filt_frame); - break; - } - - filt_frame->pict_type = AV_PICTURE_TYPE_NONE; - ret = encode_write_frame(ctx, filt_frame, stream_index, NULL); - av_frame_free(&filt_frame); - if (ret < 0) - break; - } - - return ret; -} -#else -static int -filter_encode_write_frame(struct encode_ctx *ctx, AVFrame *frame, unsigned int stream_index) -{ - AVFilterBufferRef *picref; - AVCodecContext *enc_ctx; - AVFrame *filt_frame; - int ret; - - enc_ctx = ctx->ofmt_ctx->streams[stream_index]->codec; - - // Push the decoded frame into the filtergraph - if (frame) - { - ret = av_buffersrc_write_frame(ctx->filter_ctx[stream_index].buffersrc_ctx, frame); - if (ret < 0) - { - DPRINTF(E_LOG, L_XCODE, "Error while feeding the filtergraph: %s\n", err2str(ret)); - return -1; - } - } - - // Pull filtered frames from the filtergraph - while (1) - { - filt_frame = av_frame_alloc(); - if (!filt_frame) - { - DPRINTF(E_LOG, L_XCODE, "Out of memory for filt_frame\n"); - return -1; - } - - if (enc_ctx->codec_type == AVMEDIA_TYPE_AUDIO && !(enc_ctx->codec->capabilities & CODEC_CAP_VARIABLE_FRAME_SIZE)) - ret = av_buffersink_read_samples(ctx->filter_ctx[stream_index].buffersink_ctx, &picref, enc_ctx->frame_size); - else - ret = av_buffersink_read(ctx->filter_ctx[stream_index].buffersink_ctx, &picref); - - if (ret < 0) - { - /* if no more frames for output - returns AVERROR(EAGAIN) - * if flushed and no more frames for output - returns AVERROR_EOF - * rewrite retcode to 0 to show it as normal procedure completion - */ - if (ret == AVERROR(EAGAIN) || ret == AVERROR_EOF) - ret = 0; - av_frame_free(&filt_frame); - break; - } - - avfilter_copy_buf_props(filt_frame, picref); - ret = encode_write_frame(ctx, filt_frame, stream_index, NULL); - av_frame_free(&filt_frame); - avfilter_unref_buffer(picref); - if (ret < 0) - break; - } - - return ret; -} -#endif - -/* Will step through each stream and feed the stream decoder with empty packets - * to see if the decoder has more frames lined up. Will return non-zero if a - * frame is found. Should be called until it stops returning anything. - * - * @out frame AVFrame if there was anything to flush, otherwise undefined - * @out stream Set to the AVStream where a decoder returned a frame - * @out stream_index - * Set to the stream index of the stream returning a frame - * @in ctx Decode context - * @return Non-zero (true) if frame found, otherwise 0 (false) - */ -static int -flush_decoder(AVFrame *frame, AVStream **stream, unsigned int *stream_index, struct decode_ctx *ctx) -{ - AVStream *in_stream; - AVPacket dummypacket; - int got_frame; - int i; - - memset(&dummypacket, 0, sizeof(AVPacket)); - - for (i = 0; i < ctx->ifmt_ctx->nb_streams; i++) - { - in_stream = ctx->ifmt_ctx->streams[i]; - if (!decode_stream(ctx, in_stream)) - continue; - - avcodec_decode_audio4(in_stream->codec, frame, &got_frame, &dummypacket); - - if (!got_frame) - continue; - - DPRINTF(E_DBG, L_XCODE, "Flushing decoders produced a frame from stream %d\n", i); - - *stream = in_stream; - *stream_index = i; - return got_frame; - } - - return 0; -} - -static void -flush_encoder(struct encode_ctx *ctx, unsigned int stream_index) -{ - int ret; - int got_frame; - - DPRINTF(E_DBG, L_XCODE, "Flushing output stream #%u encoder\n", stream_index); - - if (!(ctx->ofmt_ctx->streams[stream_index]->codec->codec->capabilities & CODEC_CAP_DELAY)) - return; - - do - { - ret = encode_write_frame(ctx, NULL, stream_index, &got_frame); - } - while ((ret == 0) && got_frame); -} - - -/* --------------------------- INPUT/OUTPUT INIT --------------------------- */ - -static int -open_input(struct decode_ctx *ctx, const char *path) -{ - AVDictionary *options; - AVCodec *decoder; - int stream_index; - int ret; - - options = NULL; - ctx->ifmt_ctx = avformat_alloc_context();; - if (!ctx->ifmt_ctx) - { - DPRINTF(E_LOG, L_XCODE, "Out of memory for input format context\n"); - return -1; - } - -# ifndef HAVE_FFMPEG - // Without this, libav is slow to probe some internet streams, which leads to RAOP timeouts - if (ctx->data_kind == DATA_KIND_HTTP) - ctx->ifmt_ctx->probesize = 64000; -# endif - if (ctx->data_kind == DATA_KIND_HTTP) - av_dict_set(&options, "icy", "1", 0); - - // TODO Newest versions of ffmpeg have timeout and reconnect options we should use - ctx->ifmt_ctx->interrupt_callback.callback = decode_interrupt_cb; - ctx->ifmt_ctx->interrupt_callback.opaque = ctx; - ctx->timestamp = av_gettime(); - - ret = avformat_open_input(&ctx->ifmt_ctx, path, NULL, &options); - - if (options) - av_dict_free(&options); - - if (ret < 0) - { - DPRINTF(E_LOG, L_XCODE, "Cannot open '%s': %s\n", path, err2str(ret)); - return -1; - } - - ret = avformat_find_stream_info(ctx->ifmt_ctx, NULL); - if (ret < 0) - { - DPRINTF(E_LOG, L_XCODE, "Cannot find stream information: %s\n", err2str(ret)); - goto out_fail; - } - - if (ctx->ifmt_ctx->nb_streams > MAX_STREAMS) - { - DPRINTF(E_LOG, L_XCODE, "File '%s' has too many streams (%u)\n", path, ctx->ifmt_ctx->nb_streams); - goto out_fail; - } - - // Find audio stream and open decoder - stream_index = av_find_best_stream(ctx->ifmt_ctx, AVMEDIA_TYPE_AUDIO, -1, -1, &decoder, 0); - if ((stream_index < 0) || (!decoder)) - { - DPRINTF(E_LOG, L_XCODE, "Did not find audio stream or suitable decoder for %s\n", path); - goto out_fail; - } - - ctx->ifmt_ctx->streams[stream_index]->codec->request_sample_fmt = AV_SAMPLE_FMT_S16; - ctx->ifmt_ctx->streams[stream_index]->codec->request_channel_layout = AV_CH_LAYOUT_STEREO; - -// Disabled to see if it is still required -// if (decoder->capabilities & CODEC_CAP_TRUNCATED) -// ctx->ifmt_ctx->streams[stream_index]->codec->flags |= CODEC_FLAG_TRUNCATED; - - ret = avcodec_open2(ctx->ifmt_ctx->streams[stream_index]->codec, decoder, NULL); - if (ret < 0) - { - DPRINTF(E_LOG, L_XCODE, "Failed to open decoder for stream #%d: %s\n", stream_index, err2str(ret)); - goto out_fail; - } - - ctx->audio_stream = ctx->ifmt_ctx->streams[stream_index]; - - return 0; - - out_fail: - avformat_close_input(&ctx->ifmt_ctx); - - return -1; -} - -static void -close_input(struct decode_ctx *ctx) -{ - if (ctx->audio_stream) - avcodec_close(ctx->audio_stream->codec); - - avformat_close_input(&ctx->ifmt_ctx); -} - -static int -open_output(struct encode_ctx *ctx, struct decode_ctx *src_ctx) -{ - AVStream *out_stream; - AVStream *in_stream; - AVCodecContext *dec_ctx; - AVCodecContext *enc_ctx; - AVCodec *encoder; - const AVCodecDescriptor *codec_desc; - enum AVCodecID codec_id; - int ret; - int i; - - ctx->ofmt_ctx = NULL; - avformat_alloc_output_context2(&ctx->ofmt_ctx, NULL, ctx->format, NULL); - if (!ctx->ofmt_ctx) - { - DPRINTF(E_LOG, L_XCODE, "Could not create output context\n"); - return -1; - } - - ctx->obuf = evbuffer_new(); - if (!ctx->obuf) - { - DPRINTF(E_LOG, L_XCODE, "Could not create output evbuffer\n"); - goto out_fail_evbuf; - } - - ctx->ofmt_ctx->pb = avio_output_evbuffer_open(ctx->obuf); - if (!ctx->ofmt_ctx->pb) - { - DPRINTF(E_LOG, L_XCODE, "Could not create output avio pb\n"); - goto out_fail_pb; - } - - for (i = 0; i < src_ctx->ifmt_ctx->nb_streams; i++) - { - in_stream = src_ctx->ifmt_ctx->streams[i]; - if (!decode_stream(src_ctx, in_stream)) - { - ctx->out_stream_map[i] = -1; - continue; - } - - out_stream = avformat_new_stream(ctx->ofmt_ctx, NULL); - if (!out_stream) - { - DPRINTF(E_LOG, L_XCODE, "Failed allocating output stream\n"); - goto out_fail_stream; - } - - ctx->out_stream_map[i] = out_stream->index; - ctx->in_stream_map[out_stream->index] = i; - - dec_ctx = in_stream->codec; - enc_ctx = out_stream->codec; - - // TODO Enough to just remux subtitles? - if (dec_ctx->codec_type == AVMEDIA_TYPE_SUBTITLE) - { - avcodec_copy_context(enc_ctx, dec_ctx); - continue; - } - - if (dec_ctx->codec_type == AVMEDIA_TYPE_AUDIO) - codec_id = ctx->audio_codec; - else - continue; - - codec_desc = avcodec_descriptor_get(codec_id); - encoder = avcodec_find_encoder(codec_id); - if (!encoder) - { - if (codec_desc) - DPRINTF(E_LOG, L_XCODE, "Necessary encoder (%s) for input stream %u not found\n", codec_desc->name, i); - else - DPRINTF(E_LOG, L_XCODE, "Necessary encoder (unknown) for input stream %u not found\n", i); - goto out_fail_stream; - } - - enc_ctx->sample_rate = ctx->sample_rate; - enc_ctx->channel_layout = ctx->channel_layout; - enc_ctx->channels = ctx->channels; - enc_ctx->sample_fmt = ctx->sample_format; - enc_ctx->time_base = (AVRational){1, ctx->sample_rate}; - - ret = avcodec_open2(enc_ctx, encoder, NULL); - if (ret < 0) - { - DPRINTF(E_LOG, L_XCODE, "Cannot open encoder (%s) for input stream #%u: %s\n", codec_desc->name, i, err2str(ret)); - goto out_fail_codec; - } - - if (ctx->ofmt_ctx->oformat->flags & AVFMT_GLOBALHEADER) - enc_ctx->flags |= CODEC_FLAG_GLOBAL_HEADER; - } - - // Notice, this will not write WAV header (so we do that manually) - ret = avformat_write_header(ctx->ofmt_ctx, NULL); - if (ret < 0) - { - DPRINTF(E_LOG, L_XCODE, "Error writing header to output buffer: %s\n", err2str(ret)); - goto out_fail_write; - } - - return 0; - - out_fail_write: - out_fail_codec: - for (i = 0; i < ctx->ofmt_ctx->nb_streams; i++) - { - enc_ctx = ctx->ofmt_ctx->streams[i]->codec; - if (enc_ctx) - avcodec_close(enc_ctx); - } - out_fail_stream: - avio_evbuffer_close(ctx->ofmt_ctx->pb); - out_fail_pb: - evbuffer_free(ctx->obuf); - out_fail_evbuf: - avformat_free_context(ctx->ofmt_ctx); - - return -1; -} - -static void -close_output(struct encode_ctx *ctx) -{ - int i; - - for (i = 0; i < ctx->ofmt_ctx->nb_streams; i++) - { - if (ctx->ofmt_ctx->streams[i]->codec) - avcodec_close(ctx->ofmt_ctx->streams[i]->codec); - } - - avio_evbuffer_close(ctx->ofmt_ctx->pb); - evbuffer_free(ctx->obuf); - avformat_free_context(ctx->ofmt_ctx); -} - -#if HAVE_DECL_AVFILTER_GRAPH_PARSE_PTR -static int -open_filter(struct filter_ctx *filter_ctx, AVCodecContext *dec_ctx, AVCodecContext *enc_ctx, const char *filter_spec) -{ - AVFilter *buffersrc = NULL; - AVFilter *buffersink = NULL; - AVFilterContext *buffersrc_ctx = NULL; - AVFilterContext *buffersink_ctx = NULL; - AVFilterInOut *outputs = avfilter_inout_alloc(); - AVFilterInOut *inputs = avfilter_inout_alloc(); - AVFilterGraph *filter_graph = avfilter_graph_alloc(); - char args[512]; - int ret; - - if (!outputs || !inputs || !filter_graph) - { - DPRINTF(E_LOG, L_XCODE, "Out of memory for filter_graph, input or output\n"); - goto out_fail; - } - - if (dec_ctx->codec_type != AVMEDIA_TYPE_AUDIO) - { - DPRINTF(E_LOG, L_XCODE, "Bug! Unknown type passed to filter graph init\n"); - goto out_fail; - } - - buffersrc = avfilter_get_by_name("abuffer"); - buffersink = avfilter_get_by_name("abuffersink"); - if (!buffersrc || !buffersink) - { - DPRINTF(E_LOG, L_XCODE, "Filtering source or sink element not found\n"); - goto out_fail; - } - - if (!dec_ctx->channel_layout) - dec_ctx->channel_layout = av_get_default_channel_layout(dec_ctx->channels); - - snprintf(args, sizeof(args), - "time_base=%d/%d:sample_rate=%d:sample_fmt=%s:channel_layout=0x%"PRIx64, - dec_ctx->time_base.num, dec_ctx->time_base.den, dec_ctx->sample_rate, - av_get_sample_fmt_name(dec_ctx->sample_fmt), - dec_ctx->channel_layout); - - ret = avfilter_graph_create_filter(&buffersrc_ctx, buffersrc, "in", args, NULL, filter_graph); - if (ret < 0) - { - DPRINTF(E_LOG, L_XCODE, "Cannot create audio buffer source: %s\n", err2str(ret)); - goto out_fail; - } - - ret = avfilter_graph_create_filter(&buffersink_ctx, buffersink, "out", NULL, NULL, filter_graph); - if (ret < 0) - { - DPRINTF(E_LOG, L_XCODE, "Cannot create audio buffer sink: %s\n", err2str(ret)); - goto out_fail; - } - - ret = av_opt_set_bin(buffersink_ctx, "sample_fmts", - (uint8_t*)&enc_ctx->sample_fmt, sizeof(enc_ctx->sample_fmt), AV_OPT_SEARCH_CHILDREN); - if (ret < 0) - { - DPRINTF(E_LOG, L_XCODE, "Cannot set output sample format: %s\n", err2str(ret)); - goto out_fail; - } - - ret = av_opt_set_bin(buffersink_ctx, "channel_layouts", - (uint8_t*)&enc_ctx->channel_layout, sizeof(enc_ctx->channel_layout), AV_OPT_SEARCH_CHILDREN); - if (ret < 0) - { - DPRINTF(E_LOG, L_XCODE, "Cannot set output channel layout: %s\n", err2str(ret)); - goto out_fail; - } - - ret = av_opt_set_bin(buffersink_ctx, "sample_rates", - (uint8_t*)&enc_ctx->sample_rate, sizeof(enc_ctx->sample_rate), AV_OPT_SEARCH_CHILDREN); - if (ret < 0) - { - DPRINTF(E_LOG, L_XCODE, "Cannot set output sample rate: %s\n", err2str(ret)); - goto out_fail; - } - - /* Endpoints for the filter graph. */ - outputs->name = av_strdup("in"); - outputs->filter_ctx = buffersrc_ctx; - outputs->pad_idx = 0; - outputs->next = NULL; - inputs->name = av_strdup("out"); - inputs->filter_ctx = buffersink_ctx; - inputs->pad_idx = 0; - inputs->next = NULL; - if (!outputs->name || !inputs->name) - { - DPRINTF(E_LOG, L_XCODE, "Out of memory for outputs/inputs\n"); - goto out_fail; - } - - ret = avfilter_graph_parse_ptr(filter_graph, filter_spec, &inputs, &outputs, NULL); - if (ret < 0) - goto out_fail; - - ret = avfilter_graph_config(filter_graph, NULL); - if (ret < 0) - goto out_fail; - - /* Fill filtering context */ - filter_ctx->buffersrc_ctx = buffersrc_ctx; - filter_ctx->buffersink_ctx = buffersink_ctx; - filter_ctx->filter_graph = filter_graph; - - avfilter_inout_free(&inputs); - avfilter_inout_free(&outputs); - - return 0; - - out_fail: - avfilter_graph_free(&filter_graph); - avfilter_inout_free(&inputs); - avfilter_inout_free(&outputs); - - return -1; -} -#else -static int -open_filter(struct filter_ctx *filter_ctx, AVCodecContext *dec_ctx, AVCodecContext *enc_ctx, const char *filter_spec) -{ - - AVFilter *buffersrc = NULL; - AVFilter *format = NULL; - AVFilter *buffersink = NULL; - AVFilterContext *buffersrc_ctx = NULL; - AVFilterContext *format_ctx = NULL; - AVFilterContext *buffersink_ctx = NULL; - AVFilterGraph *filter_graph = avfilter_graph_alloc(); - char args[512]; - int ret; - - if (!filter_graph) - { - DPRINTF(E_LOG, L_XCODE, "Out of memory for filter_graph\n"); - goto out_fail; - } - - if (dec_ctx->codec_type != AVMEDIA_TYPE_AUDIO) - { - DPRINTF(E_LOG, L_XCODE, "Bug! Unknown type passed to filter graph init\n"); - goto out_fail; - } - - - buffersrc = avfilter_get_by_name("abuffer"); - format = avfilter_get_by_name("aformat"); - buffersink = avfilter_get_by_name("abuffersink"); - if (!buffersrc || !format || !buffersink) - { - DPRINTF(E_LOG, L_XCODE, "Filtering source, format or sink element not found\n"); - goto out_fail; - } - - if (!dec_ctx->channel_layout) - dec_ctx->channel_layout = av_get_default_channel_layout(dec_ctx->channels); - - snprintf(args, sizeof(args), - "time_base=%d/%d:sample_rate=%d:sample_fmt=%s:channel_layout=0x%"PRIx64, - dec_ctx->time_base.num, dec_ctx->time_base.den, dec_ctx->sample_rate, - av_get_sample_fmt_name(dec_ctx->sample_fmt), - dec_ctx->channel_layout); - - ret = avfilter_graph_create_filter(&buffersrc_ctx, buffersrc, "in", args, NULL, filter_graph); - if (ret < 0) - { - DPRINTF(E_LOG, L_XCODE, "Cannot create audio buffer source: %s\n", err2str(ret)); - goto out_fail; - } - - snprintf(args, sizeof(args), - "sample_fmts=%s:sample_rates=%d:channel_layouts=0x%"PRIx64, - av_get_sample_fmt_name(enc_ctx->sample_fmt), enc_ctx->sample_rate, - enc_ctx->channel_layout); - - ret = avfilter_graph_create_filter(&format_ctx, format, "format", args, NULL, filter_graph); - if (ret < 0) - { - DPRINTF(E_LOG, L_XCODE, "Cannot create audio format filter: %s\n", err2str(ret)); - goto out_fail; - } - - ret = avfilter_graph_create_filter(&buffersink_ctx, buffersink, "out", NULL, NULL, filter_graph); - if (ret < 0) - { - DPRINTF(E_LOG, L_XCODE, "Cannot create audio buffer sink: %s\n", err2str(ret)); - goto out_fail; - } - - ret = avfilter_link(buffersrc_ctx, 0, format_ctx, 0); - if (ret >= 0) - ret = avfilter_link(format_ctx, 0, buffersink_ctx, 0); - if (ret < 0) - DPRINTF(E_LOG, L_XCODE, "Error connecting filters: %s\n", err2str(ret)); - - ret = avfilter_graph_config(filter_graph, NULL); - if (ret < 0) - goto out_fail; - - /* Fill filtering context */ - filter_ctx->buffersrc_ctx = buffersrc_ctx; - filter_ctx->buffersink_ctx = buffersink_ctx; - filter_ctx->filter_graph = filter_graph; - - return 0; - - out_fail: - avfilter_graph_free(&filter_graph); - - return -1; -} -#endif - -static int -open_filters(struct encode_ctx *ctx, struct decode_ctx *src_ctx) -{ - AVCodecContext *enc_ctx; - AVCodecContext *dec_ctx; - const char *filter_spec; - unsigned int stream_index; - int i; - int ret; - - ctx->filter_ctx = av_malloc_array(ctx->ofmt_ctx->nb_streams, sizeof(*ctx->filter_ctx)); - if (!ctx->filter_ctx) - { - DPRINTF(E_LOG, L_XCODE, "Out of memory for outputs/inputs\n"); - return -1; - } - - for (i = 0; i < ctx->ofmt_ctx->nb_streams; i++) - { - ctx->filter_ctx[i].buffersrc_ctx = NULL; - ctx->filter_ctx[i].buffersink_ctx = NULL; - ctx->filter_ctx[i].filter_graph = NULL; - - stream_index = ctx->in_stream_map[i]; - - enc_ctx = ctx->ofmt_ctx->streams[i]->codec; - dec_ctx = src_ctx->ifmt_ctx->streams[stream_index]->codec; - - if (enc_ctx->codec_type == AVMEDIA_TYPE_AUDIO) - filter_spec = "anull"; /* passthrough (dummy) filter for audio */ - else - continue; - - ret = open_filter(&ctx->filter_ctx[i], dec_ctx, enc_ctx, filter_spec); - if (ret < 0) - goto out_fail; - } - - return 0; - - out_fail: - for (i = 0; i < ctx->ofmt_ctx->nb_streams; i++) - { - if (ctx->filter_ctx && ctx->filter_ctx[i].filter_graph) - avfilter_graph_free(&ctx->filter_ctx[i].filter_graph); - } - av_free(ctx->filter_ctx); - - return -1; -} - -static void -close_filters(struct encode_ctx *ctx) -{ - int i; - - for (i = 0; i < ctx->ofmt_ctx->nb_streams; i++) - { - if (ctx->filter_ctx && ctx->filter_ctx[i].filter_graph) - avfilter_graph_free(&ctx->filter_ctx[i].filter_graph); - } - av_free(ctx->filter_ctx); -} - - -/* ----------------------------- TRANSCODE API ----------------------------- */ - -/* Setup */ - -struct decode_ctx * -transcode_decode_setup(enum transcode_profile profile, enum data_kind data_kind, const char *path, struct evbuffer *evbuf, uint32_t song_length) -{ - struct decode_ctx *ctx; - - ctx = calloc(1, sizeof(struct decode_ctx)); - if (!ctx) - { - DPRINTF(E_LOG, L_XCODE, "Out of memory for decode ctx\n"); - return NULL; - } - - ctx->duration = song_length; - ctx->data_kind = data_kind; - - if (open_input(ctx, path) < 0) - { - free(ctx); - return NULL; - } - - av_init_packet(&ctx->packet); - - return ctx; -} - -struct encode_ctx * -transcode_encode_setup(enum transcode_profile profile, struct decode_ctx *src_ctx, off_t *est_size, int width, int height) -{ - struct encode_ctx *ctx; - - ctx = calloc(1, sizeof(struct encode_ctx)); - if (!ctx) - { - DPRINTF(E_LOG, L_XCODE, "Out of memory for encode ctx\n"); - return NULL; - } - - if ((init_profile(ctx, profile) < 0) || (open_output(ctx, src_ctx) < 0)) - { - free(ctx); - return NULL; - } - - if (open_filters(ctx, src_ctx) < 0) - { - close_output(ctx); - free(ctx); - return NULL; - } - - if (src_ctx->data_kind == DATA_KIND_HTTP) - ctx->icy_interval = METADATA_ICY_INTERVAL * ctx->channels * ctx->byte_depth * ctx->sample_rate; - - if (profile == XCODE_PCM16_HEADER) - { - ctx->wavhdr = 1; - make_wav_header(ctx, src_ctx, est_size); - } - - return 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) -{ - struct transcode_ctx *ctx; - - ctx = malloc(sizeof(struct transcode_ctx)); - if (!ctx) - { - DPRINTF(E_LOG, L_XCODE, "Out of memory for transcode ctx\n"); - return NULL; - } - - ctx->decode_ctx = transcode_decode_setup(profile, data_kind, path, NULL, song_length); - if (!ctx->decode_ctx) - { - free(ctx); - return NULL; - } - - ctx->encode_ctx = transcode_encode_setup(profile, ctx->decode_ctx, est_size, 0, 0); - if (!ctx->encode_ctx) - { - transcode_decode_cleanup(&ctx->decode_ctx); - free(ctx); - return NULL; - } - - return ctx; -} - -struct decode_ctx * -transcode_decode_setup_raw(void) -{ - struct decode_ctx *ctx; - struct AVCodec *decoder; - - ctx = calloc(1, sizeof(struct decode_ctx)); - if (!ctx) - { - DPRINTF(E_LOG, L_XCODE, "Out of memory for decode ctx\n"); - return NULL; - } - - ctx->ifmt_ctx = avformat_alloc_context(); - if (!ctx->ifmt_ctx) - { - DPRINTF(E_LOG, L_XCODE, "Out of memory for decode format ctx\n"); - free(ctx); - return NULL; - } - - decoder = avcodec_find_decoder(AV_CODEC_ID_PCM_S16LE); - - ctx->audio_stream = avformat_new_stream(ctx->ifmt_ctx, decoder); - if (!ctx->audio_stream) - { - DPRINTF(E_LOG, L_XCODE, "Could not create stream with PCM16 decoder\n"); - avformat_free_context(ctx->ifmt_ctx); - free(ctx); - return NULL; - } - - ctx->audio_stream->codec->time_base.num = 1; - ctx->audio_stream->codec->time_base.den = 44100; - ctx->audio_stream->codec->sample_rate = 44100; - ctx->audio_stream->codec->sample_fmt = AV_SAMPLE_FMT_S16; - ctx->audio_stream->codec->channel_layout = AV_CH_LAYOUT_STEREO; - - return ctx; -} - -int -transcode_needed(const char *user_agent, const char *client_codecs, char *file_codectype) -{ - char *codectype; - cfg_t *lib; - int size; - int i; - - if (!file_codectype) - { - DPRINTF(E_LOG, L_XCODE, "Can't determine decode status, codec type is unknown\n"); - return -1; - } - - lib = cfg_getsec(cfg, "library"); - - size = cfg_size(lib, "no_decode"); - if (size > 0) - { - for (i = 0; i < size; i++) - { - codectype = cfg_getnstr(lib, "no_decode", i); - - if (strcmp(file_codectype, codectype) == 0) - return 0; // Codectype is in no_decode - } - } - - size = cfg_size(lib, "force_decode"); - if (size > 0) - { - for (i = 0; i < size; i++) - { - codectype = cfg_getnstr(lib, "force_decode", i); - - if (strcmp(file_codectype, codectype) == 0) - return 1; // Codectype is in force_decode - } - } - - if (!client_codecs) - { - if (user_agent) - { - if (strncmp(user_agent, "iTunes", strlen("iTunes")) == 0) - client_codecs = itunes_codecs; - else if (strncmp(user_agent, "QuickTime", strlen("QuickTime")) == 0) - client_codecs = itunes_codecs; // Use iTunes codecs - else if (strncmp(user_agent, "Front%20Row", strlen("Front%20Row")) == 0) - client_codecs = itunes_codecs; // Use iTunes codecs - else if (strncmp(user_agent, "AppleCoreMedia", strlen("AppleCoreMedia")) == 0) - client_codecs = itunes_codecs; // Use iTunes codecs - else if (strncmp(user_agent, "Roku", strlen("Roku")) == 0) - client_codecs = roku_codecs; - else if (strncmp(user_agent, "Hifidelio", strlen("Hifidelio")) == 0) - /* Allegedly can't transcode for Hifidelio because their - * HTTP implementation doesn't honour Connection: close. - * At least, that's why mt-daapd didn't do it. - */ - return 0; - } - } - else - DPRINTF(E_DBG, L_XCODE, "Client advertises codecs: %s\n", client_codecs); - - if (!client_codecs) - { - DPRINTF(E_DBG, L_XCODE, "Could not identify client, using default codectype set\n"); - client_codecs = default_codecs; - } - - if (strstr(client_codecs, file_codectype)) - { - DPRINTF(E_DBG, L_XCODE, "Codectype supported by client, no decoding needed\n"); - return 0; - } - - DPRINTF(E_DBG, L_XCODE, "Will decode\n"); - return 1; -} - - -/* Cleanup */ - -void -transcode_decode_cleanup(struct decode_ctx **ctx) -{ - av_packet_unref(&(*ctx)->packet); - close_input(*ctx); - free(*ctx); - *ctx = NULL; -} - -void -transcode_encode_cleanup(struct encode_ctx **ctx) -{ - int i; - - // Flush filters and encoders - for (i = 0; i < (*ctx)->ofmt_ctx->nb_streams; i++) - { - if (!(*ctx)->filter_ctx[i].filter_graph) - continue; - filter_encode_write_frame((*ctx), NULL, i); - flush_encoder((*ctx), i); - } - - av_write_trailer((*ctx)->ofmt_ctx); - - close_filters(*ctx); - close_output(*ctx); - free(*ctx); - *ctx = NULL; -} - -void -transcode_cleanup(struct transcode_ctx **ctx) -{ - transcode_encode_cleanup(&(*ctx)->encode_ctx); - transcode_decode_cleanup(&(*ctx)->decode_ctx); - free(*ctx); - *ctx = NULL; -} - -void -transcode_frame_free(void *frame) -{ - struct decoded_frame *decoded = frame; - - av_frame_free(&decoded->frame); - free(decoded); -} - - -/* Encoding, decoding and transcoding */ - - -int -transcode_decode(void **frame, struct decode_ctx *ctx) -{ - struct decoded_frame *decoded; - AVPacket packet; - AVStream *in_stream; - AVFrame *f; - unsigned int stream_index; - int got_frame; - int retry; - int ret; - int used; - - // Alloc the frame we will return on success - f = av_frame_alloc(); - if (!f) - { - DPRINTF(E_LOG, L_XCODE, "Out of memory for decode frame\n"); - return -1; - } - - // Loop until we either fail or get a frame - retry = 0; - do - { - ret = read_packet(&packet, &in_stream, &stream_index, ctx); - if (ret < 0) - { - // Some decoders need to be flushed, meaning the decoder is to be called - // with empty input until no more frames are returned - DPRINTF(E_DBG, L_XCODE, "Could not read packet, will flush decoders\n"); - - got_frame = flush_decoder(f, &in_stream, &stream_index, ctx); - if (got_frame) - break; - - av_frame_free(&f); - if (ret == AVERROR_EOF) - return 0; - else - return -1; - } - - // "used" will tell us how much of the packet was decoded. We may - // not get a frame because of insufficient input, in which case we loop to - // read another packet. - used = avcodec_decode_audio4(in_stream->codec, f, &got_frame, &packet); - - // decoder returned an error, but maybe the packet was just a bad apple, - // so let's try MAX_BAD_PACKETS times before giving up - if (used < 0) - { - DPRINTF(E_DBG, L_XCODE, "Couldn't decode packet\n"); - - retry += 1; - if (retry < MAX_BAD_PACKETS) - continue; - - DPRINTF(E_LOG, L_XCODE, "Couldn't decode packet after %i retries\n", MAX_BAD_PACKETS); - - av_frame_free(&f); - return -1; - } - - // decoder didn't process the entire packet, so flag a resume, meaning - // that the next read_packet() will return this same packet, but where the - // data pointer is adjusted with an offset - if (used < packet.size) - { - DPRINTF(E_SPAM, L_XCODE, "Decoder did not finish packet, packet will be resumed\n"); - - ctx->resume_offset += used; - ctx->resume = 1; - } - } - while (!got_frame); - - if (got_frame > 0) - { - // Return the decoded frame and stream index - decoded = malloc(sizeof(struct decoded_frame)); - if (!decoded) - { - DPRINTF(E_LOG, L_XCODE, "Out of memory for decoded result\n"); - - av_frame_free(&f); - return -1; - } - - decoded->frame = f; - decoded->stream_index = stream_index; - *frame = decoded; - } - else - *frame = NULL; - - return got_frame; -} - -// Filters and encodes -int -transcode_encode(struct evbuffer *evbuf, struct encode_ctx *ctx, void *frame, int eof) -{ - struct decoded_frame *decoded = frame; - int stream_index; - int encoded_length; - int ret; - - encoded_length = 0; - - stream_index = ctx->out_stream_map[decoded->stream_index]; - if (stream_index < 0) - return -1; - - if (ctx->wavhdr) - { - encoded_length += sizeof(ctx->header); - evbuffer_add(evbuf, ctx->header, sizeof(ctx->header)); - ctx->wavhdr = 0; - } - - ret = filter_encode_write_frame(ctx, decoded->frame, stream_index); - if (ret < 0) - { - DPRINTF(E_LOG, L_XCODE, "Error occurred: %s\n", err2str(ret)); - return ret; - } - - encoded_length += evbuffer_get_length(ctx->obuf); - evbuffer_add_buffer(evbuf, ctx->obuf); - - return encoded_length; -} - -int -transcode(struct evbuffer *evbuf, int *icy_timer, struct transcode_ctx *ctx, int want_bytes) -{ - void *frame; - int processed; - int ret; - - if (icy_timer) - *icy_timer = 0; - - processed = 0; - while (processed < want_bytes) - { - ret = transcode_decode(&frame, ctx->decode_ctx); - if (ret <= 0) - return ret; - - ret = transcode_encode(evbuf, ctx->encode_ctx, frame, 0); - transcode_frame_free(frame); - if (ret < 0) - return -1; - - processed += ret; - } - - ctx->encode_ctx->total_bytes += processed; - if (icy_timer && ctx->encode_ctx->icy_interval) - *icy_timer = (ctx->encode_ctx->total_bytes % ctx->encode_ctx->icy_interval < processed); - - return processed; -} - -void * -transcode_frame_new(enum transcode_profile profile, uint8_t *data, size_t size) -{ - struct decoded_frame *decoded; - AVFrame *f; - int ret; - - decoded = malloc(sizeof(struct decoded_frame)); - if (!decoded) - { - DPRINTF(E_LOG, L_XCODE, "Out of memory for decoded struct\n"); - return NULL; - } - - f = av_frame_alloc(); - if (!f) - { - DPRINTF(E_LOG, L_XCODE, "Out of memory for frame\n"); - free(decoded); - return NULL; - } - - decoded->stream_index = 0; - decoded->frame = f; - - f->nb_samples = size / 4; - f->format = AV_SAMPLE_FMT_S16; - f->channel_layout = AV_CH_LAYOUT_STEREO; -#ifdef HAVE_FFMPEG - f->channels = 2; -#endif - f->pts = AV_NOPTS_VALUE; - f->sample_rate = 44100; - - ret = avcodec_fill_audio_frame(f, 2, f->format, data, size, 0); - if (ret < 0) - { - DPRINTF(E_LOG, L_XCODE, "Error filling frame with rawbuf: %s\n", err2str(ret)); - transcode_frame_free(decoded); - return NULL; - } - - return decoded; -} - - -/* Seeking */ - -int -transcode_seek(struct transcode_ctx *ctx, int ms) -{ - struct decode_ctx *decode_ctx; - AVStream *in_stream; - int64_t start_time; - int64_t target_pts; - int64_t got_pts; - int got_ms; - int ret; - int i; - - decode_ctx = ctx->decode_ctx; - in_stream = ctx->decode_ctx->audio_stream; - start_time = in_stream->start_time; - - target_pts = ms; - target_pts = target_pts * AV_TIME_BASE / 1000; - target_pts = av_rescale_q(target_pts, AV_TIME_BASE_Q, in_stream->time_base); - - if ((start_time != AV_NOPTS_VALUE) && (start_time > 0)) - target_pts += start_time; - - ret = av_seek_frame(decode_ctx->ifmt_ctx, in_stream->index, target_pts, AVSEEK_FLAG_BACKWARD); - if (ret < 0) - { - DPRINTF(E_WARN, L_XCODE, "Could not seek into stream: %s\n", err2str(ret)); - return -1; - } - - for (i = 0; i < decode_ctx->ifmt_ctx->nb_streams; i++) - { - if (decode_stream(decode_ctx, decode_ctx->ifmt_ctx->streams[i])) - avcodec_flush_buffers(decode_ctx->ifmt_ctx->streams[i]->codec); -// avcodec_flush_buffers(ctx->ofmt_ctx->streams[stream_nb]->codec); - } - - // Fast forward until first packet with a timestamp is found - in_stream->codec->skip_frame = AVDISCARD_NONREF; - while (1) - { - av_packet_unref(&decode_ctx->packet); - - decode_ctx->timestamp = av_gettime(); - - ret = av_read_frame(decode_ctx->ifmt_ctx, &decode_ctx->packet); - if (ret < 0) - { - DPRINTF(E_WARN, L_XCODE, "Could not read more data while seeking: %s\n", err2str(ret)); - in_stream->codec->skip_frame = AVDISCARD_DEFAULT; - return -1; - } - - if (decode_ctx->packet.stream_index != in_stream->index) - continue; - - // Need a pts to return the real position - if (decode_ctx->packet.pts == AV_NOPTS_VALUE) - continue; - - break; - } - in_stream->codec->skip_frame = AVDISCARD_DEFAULT; - - // Tell transcode_decode() to resume with ctx->packet - decode_ctx->resume = 1; - decode_ctx->resume_offset = 0; - - // Compute position in ms from pts - got_pts = decode_ctx->packet.pts; - - if ((start_time != AV_NOPTS_VALUE) && (start_time > 0)) - got_pts -= start_time; - - got_pts = av_rescale_q(got_pts, in_stream->time_base, AV_TIME_BASE_Q); - got_ms = got_pts / (AV_TIME_BASE / 1000); - - // Since negative return would mean error, we disallow it here - if (got_ms < 0) - got_ms = 0; - - DPRINTF(E_DBG, L_XCODE, "Seek wanted %d ms, got %d ms\n", ms, got_ms); - - return got_ms; -} - -int -transcode_decode_query(struct decode_ctx *ctx, const char *query) -{ - return -1; // Not implemented -} - -/* Metadata */ - -struct http_icy_metadata * -transcode_metadata(struct transcode_ctx *ctx, int *changed) -{ - struct http_icy_metadata *m; - - if (!ctx->decode_ctx->ifmt_ctx) - return NULL; - - m = http_icy_metadata_get(ctx->decode_ctx->ifmt_ctx, 1); - if (!m) - return NULL; - - *changed = (m->hash != ctx->encode_ctx->icy_hash); - - ctx->encode_ctx->icy_hash = m->hash; - - return m; -} -